keanu-weblite/src/models/proof.ts

280 lines
8.3 KiB
TypeScript
Raw Normal View History

export type AIInferenceResult = {
aiGenerated: boolean;
aiProbability: number;
humanProbability: number;
2025-08-28 10:27:57 +02:00
};
2025-07-11 14:36:58 +02:00
export const C2PASourceTypeScreenCapture = "http://cv.iptc.org/newscodes/digitalsourcetype/screenCapture";
export const C2PASourceTypeDigitalCapture = "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture";
export const C2PASourceTypeComputationalCapture = "http://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture";
export const C2PASourceTypeCompositeCapture = "http://cv.iptc.org/newscodes/digitalsourcetype/compositeCapture";
2025-08-28 10:27:57 +02:00
export const C2PASourceTypeTrainedAlgorithmicMedia =
"http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia";
export const C2PASourceTypeCompositeWithTrainedAlgorithmicMedia =
"http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia";
2025-07-11 14:36:58 +02:00
export type C2PAActionsAssertion = {
2025-08-28 10:27:57 +02:00
actions: {
action: string;
softwareAgent?: string;
digitalSourceType?: string;
}[];
};
export type C2PAAssertion = {
2025-08-28 10:27:57 +02:00
label: string;
data: C2PAActionsAssertion | undefined;
};
export type C2PAManifest = {
2025-08-28 10:27:57 +02:00
assertions: C2PAAssertion[];
signature_info: {
time: string;
};
};
2025-07-11 14:36:58 +02:00
export type C2PAValidationResults = {
2025-08-28 10:27:57 +02:00
activeManifest?: {
failure: any[];
success: any[];
informational: any[];
};
};
2025-07-11 14:36:58 +02:00
export type C2PAManifestInfo = {
2025-08-28 10:27:57 +02:00
active_manifest: string;
manifests: { [key: string]: C2PAManifest };
validation_results?: C2PAValidationResults;
};
export type C2PAData = {
2025-08-28 10:27:57 +02:00
manifest_info: C2PAManifestInfo;
};
export type Proof = {
2025-08-28 10:27:57 +02:00
data?: any;
name?: string;
json?: string;
integrity?: { pgp?: any; c2pa?: C2PAData; exif?: { [key: string]: string | Object }; opentimestamps?: any };
ai?: { inferenceResult?: AIInferenceResult };
};
2025-08-20 15:12:04 +02:00
export type ProofHintFlags = {
valid: boolean;
source?: string;
creationDate?: Date;
2025-08-28 10:27:57 +02:00
aiGenerated?: boolean;
aiEdited?: boolean;
screenshot?: boolean;
camera?: boolean;
};
2025-08-20 15:12:04 +02:00
2025-08-28 10:27:57 +02:00
type FlagMatchRule = {
field: string;
match: string[];
description: string;
};
2025-08-20 15:12:04 +02:00
2025-09-01 16:26:05 +02:00
type FlagMatchRuleValue = {
path: string;
value: string;
};
2025-08-28 10:27:57 +02:00
type FlagMatchInfo = {
field: string;
value: string;
re: string;
};
2025-08-28 10:27:57 +02:00
const ruleScreenshot = (): FlagMatchRule[] => {
return [
{
field:
2025-09-01 16:26:05 +02:00
"integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/digitalSourceType",
2025-08-28 10:27:57 +02:00
match: [C2PASourceTypeScreenCapture],
description: "Screen capture",
},
];
};
2025-09-01 16:26:05 +02:00
const ruleCamera = (): FlagMatchRule[] => {
return [
{
field:
"integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/digitalSourceType",
match: [C2PASourceTypeDigitalCapture, C2PASourceTypeComputationalCapture, C2PASourceTypeCompositeCapture],
description: "Captured by camera",
},
];
};
const ruleAiGenerated = (): FlagMatchRule[] => {
return [
{
field:
"integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/digitalSourceType",
match: [C2PASourceTypeTrainedAlgorithmicMedia, C2PASourceTypeCompositeWithTrainedAlgorithmicMedia],
description: "Generated by AI",
},
];
};
2025-08-28 10:27:57 +02:00
const aiHintFlags = (): FlagMatchRule[] => {
const knownAIServices = [
"ChatGPT",
"OpenAI-API",
"Adobe Firefly",
"RunwayML",
"Runway AI",
"Google AI",
"Stable Diffusion",
];
return [
{ field: "name", match: ["^DALL_E_", "^Gen-3"], description: "File name" },
{
field: "integrity/c2pa/manifest_info/manifests[]/claim_generator",
match: knownAIServices,
description: "C2PA claim generator",
},
{ field: "iptc/Credit", match: knownAIServices, description: "IPTC Credit" },
{ field: "iptc/Provider", match: knownAIServices, description: "IPTC Provider" },
{ field: "iptc/ImageSupplier[]", match: knownAIServices, description: "IPTC ImageSupplier" },
{ field: "iptc/ImageCreator[]", match: knownAIServices, description: "IPTC ImageCreator" },
];
};
const matchFlag = (rules: FlagMatchRule[], file: any) => {
let result = false;
let resultInfo: FlagMatchInfo[] = [];
for (let rule of rules) {
try {
const re: RegExp[] = (!Array.isArray(rule.match) ? [rule.match] : rule.match).map((m) => new RegExp(m, "gi"));
2025-09-04 14:20:45 +02:00
const values = extractFlagValues(rule.field, file);
values.forEach((v) => {
re.forEach((r) => {
if (r.test(v.value)) {
result = true;
resultInfo.push({ field: v.path, value: v.value, re: r.source });
2025-09-04 14:20:45 +02:00
}
2025-08-28 10:27:57 +02:00
});
});
2025-08-28 10:27:57 +02:00
} catch (e) {
console.error("Invalid RE", e);
}
}
return { result: result, matches: resultInfo };
};
2025-09-01 16:26:05 +02:00
const extractFlagValues = (flagPath: string, file: any): FlagMatchRuleValue[] => {
const getValues = (
path: string[],
objectPath: any[],
actualPath: string,
o: any
): FlagMatchRuleValue[] | undefined => {
2025-09-01 16:26:05 +02:00
if (path.length == 0 || o == undefined) return undefined;
let part = path[0];
const lastBracket = part.lastIndexOf("[");
if (part === "..") {
return getValues(path.slice(1), objectPath.slice(1), actualPath + "/..", objectPath[0]);
}
if (part.endsWith("]") && lastBracket > 0) {
const optionalConstraint = part.substring(lastBracket + 1, part.length - 1);
part = part.substring(0, lastBracket);
if (o[part] != undefined) {
let opart: any[] = o[part];
if (!Array.isArray(opart)) {
opart = Object.values(opart) ?? [];
}
// Any constraints controlling what array object(s) to consider?
if (optionalConstraint) {
const [prop, val] = optionalConstraint.split("=");
opart = opart.filter((item) => item[prop] === val);
}
if (path.length == 1) {
let strings = opart as string[];
return strings.map((s, i) => ({ path: actualPath + "/" + part + "[" + i + "]", value: s }));
} else {
return opart.reduce((res: FlagMatchRuleValue[] | undefined, o: any, i: number) => {
const newObjectPaths = [o, ...objectPath];
2025-09-01 16:26:05 +02:00
let matches = getValues(path.slice(1), newObjectPaths, actualPath + "/" + part + "[" + i + "]", o);
if (matches) {
const r2 = res || [];
r2.push(...matches);
return r2;
}
return res;
}, undefined);
}
} else {
return undefined;
}
} else {
if (o[part] != undefined) {
if (path.length == 1) {
return [{ path: actualPath + "/" + part, value: o[part] }];
} else {
const newObjectPaths = [o, ...objectPath];
return getValues(path.slice(1), newObjectPaths, actualPath + "/" + part, o[part]);
}
} else {
return undefined;
}
}
};
let result: FlagMatchRuleValue[] = [];
try {
let parts = flagPath.split("/");
result = getValues(parts, [], "", file) ?? [];
} catch (e) {
console.error("Invalid RE", e);
}
return result;
};
2025-08-28 10:27:57 +02:00
export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined => {
if (!proof) return undefined;
let aiEdited = false;
let valid = false;
try {
let results = proof.integrity?.c2pa?.manifest_info.validation_results?.activeManifest;
if (results) {
valid = results.failure.length == 0 && results.success.length > 0;
}
const source = extractFlagValues(
"integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/softwareAgent",
proof
);
const dateCreated = extractFlagValues(
"integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/../../../../signature_info/time",
proof
);
let date: Date | undefined = undefined;
if (dateCreated && dateCreated.length == 1) {
try {
date = new Date(Date.parse(dateCreated[0].value));
} catch (error) {}
2025-08-28 10:27:57 +02:00
}
console.log("DATE CREATED", date);
const flags: ProofHintFlags = {
valid: valid,
source: source && source.length == 1 ? source[0].value : undefined,
creationDate: date,
aiGenerated: matchFlag(ruleAiGenerated(), proof).result,
aiEdited,
screenshot: matchFlag(ruleScreenshot(), proof).result,
camera: matchFlag(ruleCamera(), proof).result,
};
return flags;
2025-08-28 10:27:57 +02:00
} catch (error) {}
return undefined;
};