diff --git a/src/models/proof.ts b/src/models/proof.ts index 2340d3a..a3019ff 100644 --- a/src/models/proof.ts +++ b/src/models/proof.ts @@ -2,129 +2,271 @@ export type AIInferenceResult = { aiGenerated: boolean; aiProbability: number; humanProbability: number; -} +}; 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"; -export const C2PASourceTypeTrainedAlgorithmicMedia = "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"; -export const C2PASourceTypeCompositeWithTrainedAlgorithmicMedia = "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"; +export const C2PASourceTypeTrainedAlgorithmicMedia = + "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"; +export const C2PASourceTypeCompositeWithTrainedAlgorithmicMedia = + "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"; export type C2PAActionsAssertion = { - actions: { - action: string; - softwareAgent?: string; - digitalSourceType?: string; - }[]; -} + actions: { + action: string; + softwareAgent?: string; + digitalSourceType?: string; + }[]; +}; export type C2PAAssertion = { - label: string; - data: C2PAActionsAssertion | undefined; -} + label: string; + data: C2PAActionsAssertion | undefined; +}; export type C2PAManifest = { - assertions: C2PAAssertion[]; - signature_info: { - time: string; - } -} + assertions: C2PAAssertion[]; + signature_info: { + time: string; + }; +}; export type C2PAValidationResults = { - activeManifest?: { - failure: any[]; - success: any[]; - informational: any[]; - } -} + activeManifest?: { + failure: any[]; + success: any[]; + informational: any[]; + }; +}; export type C2PAManifestInfo = { - active_manifest: string; - manifests: {[key: string]: C2PAManifest}; - validation_results?: C2PAValidationResults; -} + active_manifest: string; + manifests: { [key: string]: C2PAManifest }; + validation_results?: C2PAValidationResults; +}; export type C2PAData = { - manifest_info: C2PAManifestInfo; -} + manifest_info: C2PAManifestInfo; +}; export type Proof = { - data?: any; - name?: string; - json?: string; - integrity?: { pgp?: any; c2pa?: C2PAData; exif?: {[key: string]: string | Object}; opentimestamps?: any }; - ai?: { inferenceResult?: AIInferenceResult}; -} + data?: any; + name?: string; + json?: string; + integrity?: { pgp?: any; c2pa?: C2PAData; exif?: { [key: string]: string | Object }; opentimestamps?: any }; + ai?: { inferenceResult?: AIInferenceResult }; +}; export type ProofHintFlags = { - aiGenerated?: boolean; - aiEdited?: boolean; - screenshot?: boolean; - camera?: boolean; -} + aiGenerated?: boolean; + aiEdited?: boolean; + screenshot?: boolean; + camera?: boolean; +}; -export const extractProofHintFlags = (proof?: Proof): (ProofHintFlags | undefined) => { - if (!proof) return undefined; +type FlagMatchRule = { + field: string; + match: string[]; + description: string; +}; - let foundCreated = false; +type FlagMatchInfo = { + field: string; + value: string; + re: string; +}; - let screenshot = false; - let camera = false; - let aiGenerated = false; - let aiEdited = false; - let valid = false; +type FlagMatch = { + re: RegExp; + value: string; +}; - try { - let results = proof.integrity?.c2pa?.manifest_info.validation_results?.activeManifest; - if (results) { - valid = results.failure.length == 0 && results.success.length > 0; +const ruleScreenshot = (): FlagMatchRule[] => { + return [ + { + field: + "integrity/c2pa/manifest_info/manifests[]/assertions[]/{label=c2pa.actions}/data/actions[]/{action=c2pa.created}/digitalSourceType", + match: [C2PASourceTypeScreenCapture], + description: "Screen capture", + }, + ]; +}; + +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) => { + const matchParts = (parts: string[], o: any, re: RegExp[]): FlagMatch[] | undefined => { + if (parts.length == 0 || o == undefined) return undefined; + let part = parts[0]; + if (part.endsWith("[]")) { + part = part.substring(0, part.length - 2); + if (o[part] != undefined) { + let opart: any[] = o[part]; + if (!Array.isArray(opart)) { + opart = Object.values(opart) ?? []; } + if (parts.length == 1) { + let strings = opart as string[]; + if (!strings) return undefined; + return strings.reduce((res: FlagMatch[] | undefined, s: any) => { + return re.reduce((res: FlagMatch[] | undefined, r: any) => { + if (r.test(s)) { + const r2 = res || []; + r2.push({ re: r, value: s as string }); + return r2; + } + return res; + }, res); + }, undefined); + } else { + return opart.reduce((res: FlagMatch[] | undefined, o: any) => { + let matches = matchParts(parts.slice(1), o, re); + if (matches) { + const r2 = res || []; + r2.push(...matches); + return r2; + } + return res; + }, undefined); + } + } else { + return undefined; + } + } else if (part.startsWith("{") && part.endsWith("}")) { + const [prop, val] = part.substring(1, part.length - 1).split("="); + if (o[prop] === val) { + return matchParts(parts.slice(1), o, re); + } else { + return undefined; + } + } else { + if (o[part] != undefined) { + if (parts.length == 1) { + return re.reduce((res: FlagMatch[] | undefined, r: any) => { + if (r.test(o[part])) { + const r2 = res || []; + r2.push({ re: r, value: o[part] as string }); + return r2; + } + return res; + }, undefined); + } else { + return matchParts(parts.slice(1), o[part], re); + } + } else { + return undefined; + } + } + }; - const manifests = Object.values(proof.integrity?.c2pa?.manifest_info.manifests ?? {}); - for (const manifest of manifests) { - for (const assertion of manifest.assertions) { - if (assertion.label === "c2pa.actions") { - const actions = (assertion.data as C2PAActionsAssertion)?.actions ?? []; - const a = actions.find((a) => a.action === "c2pa.created"); - if (a) { + 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")); + let parts = rule.field.split("/"); + let matches = matchParts(parts, file, re); + if (matches) { + matches.forEach((m) => { + resultInfo.push({ field: rule.description, value: m.value, re: m.re.source }); + }); + result = true; + } + } catch (e) { + console.error("Invalid RE", e); + } + } + return { result: result, matches: resultInfo }; +}; + +export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined => { + if (!proof) return undefined; + + let foundCreated = false; + + let screenshot = false; + let camera = false; + let aiGenerated = false; + 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 manifests = Object.values(proof.integrity?.c2pa?.manifest_info.manifests ?? {}); + for (const manifest of manifests) { + for (const assertion of manifest.assertions) { + if (assertion.label === "c2pa.actions") { + const actions = (assertion.data as C2PAActionsAssertion)?.actions ?? []; + const a = actions.find((a) => a.action === "c2pa.created"); + if (a) { // creator.value = a.softwareAgent; // dateCreated.value = dayjs(Date.parse(manifest.signature_info.time)); - if (a.digitalSourceType === C2PASourceTypeScreenCapture) { - screenshot = true; - } - if ( - a.digitalSourceType === C2PASourceTypeDigitalCapture || - a.digitalSourceType === C2PASourceTypeComputationalCapture || - a.digitalSourceType === C2PASourceTypeCompositeCapture - ) { - camera = true; - } - if ( - a.digitalSourceType === C2PASourceTypeTrainedAlgorithmicMedia || - a.digitalSourceType === C2PASourceTypeCompositeWithTrainedAlgorithmicMedia - ) { - aiGenerated = true; - } - foundCreated = true; - break; + if (a.digitalSourceType === C2PASourceTypeScreenCapture) { + screenshot = true; } + if ( + a.digitalSourceType === C2PASourceTypeDigitalCapture || + a.digitalSourceType === C2PASourceTypeComputationalCapture || + a.digitalSourceType === C2PASourceTypeCompositeCapture + ) { + camera = true; + } + if ( + a.digitalSourceType === C2PASourceTypeTrainedAlgorithmicMedia || + a.digitalSourceType === C2PASourceTypeCompositeWithTrainedAlgorithmicMedia + ) { + aiGenerated = true; + } + foundCreated = true; + break; } } if (foundCreated) { break; } } - if (valid) { - const flags: ProofHintFlags = { - aiGenerated, - aiEdited, - screenshot, - camera - }; - return flags; + if (foundCreated) { + break; } - } catch (error) { } - return undefined; -}; \ No newline at end of file + if (valid) { + screenshot = matchFlag(ruleScreenshot(), proof).result; + const flags: ProofHintFlags = { + aiGenerated, + aiEdited, + screenshot, + camera, + }; + return flags; + } + } catch (error) {} + return undefined; +};