@succinctlabs/react-native-zcam1 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +61 -0
  2. package/Zcam1Sdk.podspec +157 -0
  3. package/app.plugin.js +11 -0
  4. package/cpp/generated/zcam1_c2pa_utils.cpp +4091 -0
  5. package/cpp/generated/zcam1_c2pa_utils.hpp +367 -0
  6. package/cpp/generated/zcam1_certs_utils.cpp +1799 -0
  7. package/cpp/generated/zcam1_certs_utils.hpp +72 -0
  8. package/cpp/generated/zcam1_verify_utils.cpp +1857 -0
  9. package/cpp/generated/zcam1_verify_utils.hpp +79 -0
  10. package/cpp/proving/generated/zcam1_proving_utils.cpp +3661 -0
  11. package/cpp/proving/generated/zcam1_proving_utils.hpp +275 -0
  12. package/cpp/proving/zcam1-proving.cpp +16 -0
  13. package/cpp/proving/zcam1-proving.h +15 -0
  14. package/cpp/zcam1-sdk.cpp +20 -0
  15. package/cpp/zcam1-sdk.h +15 -0
  16. package/ios/Zcam1Camera.swift +2945 -0
  17. package/ios/Zcam1CameraFilmStyle.swift +191 -0
  18. package/ios/Zcam1CameraViewManager.m +86 -0
  19. package/ios/Zcam1Capture.h +13 -0
  20. package/ios/Zcam1Capture.mm +500 -0
  21. package/ios/Zcam1DepthData.swift +417 -0
  22. package/ios/Zcam1Sdk.h +16 -0
  23. package/ios/Zcam1Sdk.mm +66 -0
  24. package/ios/proving/Zcam1Proving.h +16 -0
  25. package/ios/proving/Zcam1Proving.mm +66 -0
  26. package/lib/module/NativeZcam1Capture.js +12 -0
  27. package/lib/module/NativeZcam1Capture.js.map +1 -0
  28. package/lib/module/NativeZcam1Sdk.js +7 -0
  29. package/lib/module/NativeZcam1Sdk.js.map +1 -0
  30. package/lib/module/bindings.js +51 -0
  31. package/lib/module/bindings.js.map +1 -0
  32. package/lib/module/camera.js +522 -0
  33. package/lib/module/camera.js.map +1 -0
  34. package/lib/module/capture.js +120 -0
  35. package/lib/module/capture.js.map +1 -0
  36. package/lib/module/common.js +35 -0
  37. package/lib/module/common.js.map +1 -0
  38. package/lib/module/generated/zcam1_c2pa_utils-ffi.js +43 -0
  39. package/lib/module/generated/zcam1_c2pa_utils-ffi.js.map +1 -0
  40. package/lib/module/generated/zcam1_c2pa_utils.js +1202 -0
  41. package/lib/module/generated/zcam1_c2pa_utils.js.map +1 -0
  42. package/lib/module/generated/zcam1_certs_utils-ffi.js +43 -0
  43. package/lib/module/generated/zcam1_certs_utils-ffi.js.map +1 -0
  44. package/lib/module/generated/zcam1_certs_utils.js +399 -0
  45. package/lib/module/generated/zcam1_certs_utils.js.map +1 -0
  46. package/lib/module/generated/zcam1_proving_utils-ffi.js +43 -0
  47. package/lib/module/generated/zcam1_proving_utils-ffi.js.map +1 -0
  48. package/lib/module/generated/zcam1_proving_utils.js +515 -0
  49. package/lib/module/generated/zcam1_proving_utils.js.map +1 -0
  50. package/lib/module/generated/zcam1_verify_utils-ffi.js +43 -0
  51. package/lib/module/generated/zcam1_verify_utils-ffi.js.map +1 -0
  52. package/lib/module/generated/zcam1_verify_utils.js +252 -0
  53. package/lib/module/generated/zcam1_verify_utils.js.map +1 -0
  54. package/lib/module/index.js +31 -0
  55. package/lib/module/index.js.map +1 -0
  56. package/lib/module/package.json +1 -0
  57. package/lib/module/picker.js +222 -0
  58. package/lib/module/picker.js.map +1 -0
  59. package/lib/module/proving/NativeZcam1Proving.js +7 -0
  60. package/lib/module/proving/NativeZcam1Proving.js.map +1 -0
  61. package/lib/module/proving/bindings.js +46 -0
  62. package/lib/module/proving/bindings.js.map +1 -0
  63. package/lib/module/proving/index.js +5 -0
  64. package/lib/module/proving/index.js.map +1 -0
  65. package/lib/module/proving/prove.js +346 -0
  66. package/lib/module/proving/prove.js.map +1 -0
  67. package/lib/module/utils.js +27 -0
  68. package/lib/module/utils.js.map +1 -0
  69. package/lib/module/verify.js +82 -0
  70. package/lib/module/verify.js.map +1 -0
  71. package/lib/typescript/package.json +1 -0
  72. package/lib/typescript/src/NativeZcam1Capture.d.ts +280 -0
  73. package/lib/typescript/src/NativeZcam1Capture.d.ts.map +1 -0
  74. package/lib/typescript/src/NativeZcam1Sdk.d.ts +8 -0
  75. package/lib/typescript/src/NativeZcam1Sdk.d.ts.map +1 -0
  76. package/lib/typescript/src/bindings.d.ts +14 -0
  77. package/lib/typescript/src/bindings.d.ts.map +1 -0
  78. package/lib/typescript/src/camera.d.ts +300 -0
  79. package/lib/typescript/src/camera.d.ts.map +1 -0
  80. package/lib/typescript/src/capture.d.ts +59 -0
  81. package/lib/typescript/src/capture.d.ts.map +1 -0
  82. package/lib/typescript/src/common.d.ts +10 -0
  83. package/lib/typescript/src/common.d.ts.map +1 -0
  84. package/lib/typescript/src/generated/zcam1_c2pa_utils-ffi.d.ts +175 -0
  85. package/lib/typescript/src/generated/zcam1_c2pa_utils-ffi.d.ts.map +1 -0
  86. package/lib/typescript/src/generated/zcam1_c2pa_utils.d.ts +811 -0
  87. package/lib/typescript/src/generated/zcam1_c2pa_utils.d.ts.map +1 -0
  88. package/lib/typescript/src/generated/zcam1_certs_utils-ffi.d.ts +82 -0
  89. package/lib/typescript/src/generated/zcam1_certs_utils-ffi.d.ts.map +1 -0
  90. package/lib/typescript/src/generated/zcam1_certs_utils.d.ts +413 -0
  91. package/lib/typescript/src/generated/zcam1_certs_utils.d.ts.map +1 -0
  92. package/lib/typescript/src/generated/zcam1_proving_utils-ffi.d.ts +153 -0
  93. package/lib/typescript/src/generated/zcam1_proving_utils-ffi.d.ts.map +1 -0
  94. package/lib/typescript/src/generated/zcam1_proving_utils.d.ts +321 -0
  95. package/lib/typescript/src/generated/zcam1_proving_utils.d.ts.map +1 -0
  96. package/lib/typescript/src/generated/zcam1_verify_utils-ffi.d.ts +84 -0
  97. package/lib/typescript/src/generated/zcam1_verify_utils-ffi.d.ts.map +1 -0
  98. package/lib/typescript/src/generated/zcam1_verify_utils.d.ts +286 -0
  99. package/lib/typescript/src/generated/zcam1_verify_utils.d.ts.map +1 -0
  100. package/lib/typescript/src/index.d.ts +29 -0
  101. package/lib/typescript/src/index.d.ts.map +1 -0
  102. package/lib/typescript/src/picker.d.ts +103 -0
  103. package/lib/typescript/src/picker.d.ts.map +1 -0
  104. package/lib/typescript/src/proving/NativeZcam1Proving.d.ts +8 -0
  105. package/lib/typescript/src/proving/NativeZcam1Proving.d.ts.map +1 -0
  106. package/lib/typescript/src/proving/bindings.d.ts +8 -0
  107. package/lib/typescript/src/proving/bindings.d.ts.map +1 -0
  108. package/lib/typescript/src/proving/index.d.ts +3 -0
  109. package/lib/typescript/src/proving/index.d.ts.map +1 -0
  110. package/lib/typescript/src/proving/prove.d.ts +74 -0
  111. package/lib/typescript/src/proving/prove.d.ts.map +1 -0
  112. package/lib/typescript/src/utils.d.ts +2 -0
  113. package/lib/typescript/src/utils.d.ts.map +1 -0
  114. package/lib/typescript/src/verify.d.ts +45 -0
  115. package/lib/typescript/src/verify.d.ts.map +1 -0
  116. package/package.json +118 -0
  117. package/src/NativeZcam1Capture.ts +335 -0
  118. package/src/NativeZcam1Sdk.ts +10 -0
  119. package/src/bindings.tsx +49 -0
  120. package/src/camera.tsx +705 -0
  121. package/src/capture.tsx +165 -0
  122. package/src/common.tsx +46 -0
  123. package/src/generated/zcam1_c2pa_utils-ffi.ts +456 -0
  124. package/src/generated/zcam1_c2pa_utils.ts +1866 -0
  125. package/src/generated/zcam1_certs_utils-ffi.ts +187 -0
  126. package/src/generated/zcam1_certs_utils.ts +549 -0
  127. package/src/generated/zcam1_proving_utils-ffi.ts +374 -0
  128. package/src/generated/zcam1_proving_utils.ts +804 -0
  129. package/src/generated/zcam1_verify_utils-ffi.ts +196 -0
  130. package/src/generated/zcam1_verify_utils.ts +372 -0
  131. package/src/index.ts +73 -0
  132. package/src/picker.tsx +342 -0
  133. package/src/proving/NativeZcam1Proving.ts +10 -0
  134. package/src/proving/bindings.tsx +50 -0
  135. package/src/proving/index.ts +8 -0
  136. package/src/proving/prove.tsx +492 -0
  137. package/src/utils.ts +38 -0
  138. package/src/verify.tsx +119 -0
  139. package/turbo.json +27 -0
@@ -0,0 +1,492 @@
1
+ import { base64 } from "@scure/base";
2
+ import React, {
3
+ createContext,
4
+ useContext,
5
+ useEffect,
6
+ useMemo,
7
+ useReducer,
8
+ useRef,
9
+ useState,
10
+ } from "react";
11
+ import { Dirs, Util } from "react-native-file-access";
12
+
13
+ import {
14
+ buildSelfSignedCertificate,
15
+ ExistingCertChain,
16
+ formatFromPath,
17
+ ManifestEditor,
18
+ SelfSignedCertChain,
19
+ } from "../bindings";
20
+ import { getContentPublicKey, getSecureEnclaveKeyId } from "../common";
21
+ import {
22
+ FulfillmentStatus,
23
+ type Initialized,
24
+ IosProvingClient,
25
+ type IosProvingClientInterface,
26
+ ProofRequestStatus,
27
+ ProverNetworkMode,
28
+ } from "./bindings";
29
+
30
+ export { ProverNetworkMode } from "./bindings";
31
+
32
+ /**
33
+ * Configuration settings for backend communication.
34
+ */
35
+ export type Settings = {
36
+ /** Private key used to authenticate with the prover network. If omitted, a mock prover is used. */
37
+ privateKey?: string;
38
+ /** Certificate chain used to sign C2PA manifests. If omitted, a self signed certificate chain.is build using default values */
39
+ certChain?: SelfSignedCertChain | ExistingCertChain;
40
+ /** Whether to target the production environment. */
41
+ production: boolean;
42
+ /** Prover network mode (mainnet or reserved capacity). Defaults to `Reserved`. */
43
+ proverNetworkMode?: ProverNetworkMode;
44
+ };
45
+
46
+ /**
47
+ * Creates a `ProvingClient` (non-React helper).
48
+ *
49
+ * Note: If the proofs are generated on the same app where the photos are captured,
50
+ * call the initDevice() function from `react-native-zcam1-capture` instead of
51
+ * this one.
52
+ */
53
+ async function createProvingClient(
54
+ settings: Settings,
55
+ onInitialized: Initialized,
56
+ onProofRequest?: (requestId: string, photoPath: string) => void,
57
+ ): Promise<ProvingClient> {
58
+ let certChainPem: string;
59
+ let client: IosProvingClientInterface;
60
+ const contentPublicKey = await getContentPublicKey();
61
+
62
+ if (contentPublicKey.kty !== "EC") {
63
+ throw "Only EC public keys are supported";
64
+ }
65
+
66
+ const contentKeyId = getSecureEnclaveKeyId(contentPublicKey);
67
+
68
+ if (settings.certChain && "pem" in settings.certChain) {
69
+ certChainPem = settings.certChain.pem;
70
+ } else {
71
+ console.warn("[ZCAM1] Using a self signed certificate");
72
+
73
+ certChainPem = buildSelfSignedCertificate(contentPublicKey, settings.certChain);
74
+ }
75
+
76
+ if (settings.privateKey) {
77
+ const proverNetworkMode = settings.proverNetworkMode ?? ProverNetworkMode.Mainnet;
78
+ client = new IosProvingClient(settings.privateKey, onInitialized, proverNetworkMode);
79
+ } else {
80
+ client = IosProvingClient.mock(onInitialized);
81
+ }
82
+
83
+ return new ProvingClient(client, contentKeyId, certChainPem, settings.production, onProofRequest);
84
+ }
85
+
86
+ export type ProverContextValue = {
87
+ provingClient: ProvingClient | null;
88
+ provingTasks: ProvingTasksState;
89
+ provingTasksCount: number;
90
+ isInitializing: boolean;
91
+ error: unknown;
92
+ };
93
+
94
+ export type ProofRequestContextValue = {
95
+ isInitializing: boolean;
96
+ error: unknown;
97
+ fulfillementStatus: FulfillmentStatus;
98
+ proof: ArrayBuffer | undefined;
99
+ };
100
+
101
+ export type ProvingTask = {
102
+ photoPath: string;
103
+ createdAtMs: number;
104
+ };
105
+
106
+ export type ProvingTasksState = Record<string, ProvingTask>;
107
+
108
+ type ProvingTasksAction =
109
+ | { type: "reset" }
110
+ | { type: "requested"; requestId: string; photoPath: string }
111
+ | { type: "removed"; requestId: string };
112
+
113
+ function provingTasksReducer(
114
+ state: ProvingTasksState,
115
+ action: ProvingTasksAction,
116
+ ): ProvingTasksState {
117
+ switch (action.type) {
118
+ case "reset":
119
+ return {};
120
+ case "requested":
121
+ return {
122
+ ...state,
123
+ [action.requestId]: {
124
+ photoPath: action.photoPath,
125
+ createdAtMs: Date.now(),
126
+ },
127
+ };
128
+ case "removed": {
129
+ if (!(action.requestId in state)) return state;
130
+ const { [action.requestId]: _, ...rest } = state;
131
+ return rest;
132
+ }
133
+ default:
134
+ return state;
135
+ }
136
+ }
137
+
138
+ const ProverContext = createContext<ProverContextValue | null>(null);
139
+
140
+ export type ProverProviderProps = {
141
+ children: React.ReactNode;
142
+ /**
143
+ * Provider configuration. The provider always initializes itself from these settings.
144
+ */
145
+ settings: Settings;
146
+
147
+ onFulfilled?: (
148
+ requestId: string,
149
+ photoPath: string,
150
+ proof: ArrayBuffer,
151
+ provingClient: ProvingClient,
152
+ ) => void;
153
+ onUnfulfillable?: (requestId: string) => void;
154
+ };
155
+
156
+ export function ProverProvider({
157
+ children,
158
+ settings,
159
+ onFulfilled,
160
+ onUnfulfillable,
161
+ }: ProverProviderProps) {
162
+ const [provingClient, setProvingClient] = useState<ProvingClient | null>(null);
163
+ const [isInitializing, setIsInitializing] = useState(false);
164
+ const [error, setError] = useState<unknown>(null);
165
+
166
+ const [provingTasks, dispatchProvingTasks] = useReducer(provingTasksReducer, {});
167
+
168
+ const provingTasksRef = useRef<ProvingTasksState>({});
169
+
170
+ // Keep a ref to the latest tasks so async callbacks don't read stale state.
171
+ useEffect(() => {
172
+ provingTasksRef.current = provingTasks;
173
+ }, [provingTasks]);
174
+
175
+ const dispatchProvingTasksSync = (action: ProvingTasksAction) => {
176
+ provingTasksRef.current = provingTasksReducer(provingTasksRef.current, action);
177
+ dispatchProvingTasks(action);
178
+ };
179
+
180
+ const provingTasksCount = useMemo(() => Object.keys(provingTasks).length, [provingTasks]);
181
+
182
+ // Initialize proving client
183
+ useEffect(() => {
184
+ let cancelled = false;
185
+
186
+ setIsInitializing(true);
187
+ setError(null);
188
+ setProvingClient(null);
189
+ dispatchProvingTasksSync({ type: "reset" });
190
+
191
+ (async () => {
192
+ try {
193
+ const provingClient = await createProvingClient(
194
+ settings,
195
+ {
196
+ initialized: () => {
197
+ if (cancelled) return;
198
+ setProvingClient(provingClient);
199
+ setIsInitializing(false);
200
+ },
201
+ },
202
+ (requestId: string, photoPath: string) => {
203
+ dispatchProvingTasksSync({
204
+ type: "requested",
205
+ requestId,
206
+ photoPath,
207
+ });
208
+ },
209
+ );
210
+ } catch (e) {
211
+ if (cancelled) return;
212
+ setError(e);
213
+ }
214
+ })();
215
+
216
+ return () => {
217
+ cancelled = true;
218
+ };
219
+ }, [settings]);
220
+
221
+ // Poll the proving client to keep task statuses fresh.
222
+ useEffect(() => {
223
+ if (!provingClient) return;
224
+
225
+ let cancelled = false;
226
+ let inFlight = false;
227
+
228
+ const pollOnce = async () => {
229
+ if (cancelled) return;
230
+ if (inFlight) return;
231
+
232
+ const entries = Object.entries(provingTasksRef.current);
233
+ if (entries.length === 0) return;
234
+
235
+ inFlight = true;
236
+ try {
237
+ const results = await Promise.allSettled(
238
+ entries.map(async ([requestId]) => {
239
+ const status = await provingClient.getProofStatus(requestId);
240
+ return { requestId, status };
241
+ }),
242
+ );
243
+
244
+ if (cancelled) return;
245
+
246
+ for (const r of results) {
247
+ if (r.status !== "fulfilled") continue;
248
+
249
+ const { requestId, status } = r.value;
250
+
251
+ const task = provingTasksRef.current[requestId];
252
+ if (!task) continue;
253
+
254
+ if (status.fulfillmentStatus === FulfillmentStatus.Fulfilled) {
255
+ dispatchProvingTasksSync({ type: "removed", requestId });
256
+
257
+ if (onFulfilled) {
258
+ if (!status.proof) {
259
+ console.warn("[ZCAM1] Fulfilled proof request returned no proof bytes", requestId);
260
+ } else {
261
+ onFulfilled(requestId, task.photoPath, status.proof, provingClient);
262
+ }
263
+ }
264
+ } else if (status.fulfillmentStatus === FulfillmentStatus.Unfulfillable) {
265
+ dispatchProvingTasksSync({ type: "removed", requestId });
266
+
267
+ if (onUnfulfillable) {
268
+ onUnfulfillable(requestId);
269
+ }
270
+ }
271
+ }
272
+ } catch (e) {
273
+ if (!cancelled) {
274
+ console.error("[ZCAM1] ProvingTasksProvider polling failed", e);
275
+ }
276
+ } finally {
277
+ inFlight = false;
278
+ }
279
+ };
280
+
281
+ // Kick off once immediately, then poll on an interval.
282
+ void pollOnce();
283
+ const intervalId = setInterval(() => {
284
+ void pollOnce();
285
+ }, 1000);
286
+
287
+ return () => {
288
+ cancelled = true;
289
+ clearInterval(intervalId);
290
+ };
291
+ }, [provingClient, onFulfilled, onUnfulfillable]);
292
+
293
+ const value = useMemo<ProverContextValue>(
294
+ () => ({
295
+ provingClient,
296
+ provingTasks,
297
+ provingTasksCount,
298
+ isInitializing,
299
+ error,
300
+ }),
301
+ [provingClient, provingTasks, provingTasksCount, isInitializing, error],
302
+ );
303
+
304
+ return <ProverContext.Provider value={value}>{children}</ProverContext.Provider>;
305
+ }
306
+
307
+ export function useProver(): ProverContextValue {
308
+ const ctx = useContext(ProverContext);
309
+ if (!ctx) {
310
+ throw new Error("useProver must be used within a ProverProvider");
311
+ }
312
+ return ctx;
313
+ }
314
+
315
+ export function useProofRequestStatus(requestId: string | null): ProofRequestContextValue {
316
+ const [fulfillementStatus, setFulfillementStatus] = useState<FulfillmentStatus>(
317
+ FulfillmentStatus.UnspecifiedFulfillmentStatus,
318
+ );
319
+ const [proof, setProof] = useState<ArrayBuffer | undefined>(undefined);
320
+ const { provingClient, isInitializing, error } = useProver();
321
+
322
+ useEffect(() => {
323
+ let cancelled = false;
324
+
325
+ // Reset per-request state when inputs change.
326
+ setFulfillementStatus(FulfillmentStatus.UnspecifiedFulfillmentStatus);
327
+ setProof(undefined);
328
+
329
+ const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
330
+
331
+ (async () => {
332
+ if (!provingClient) return;
333
+ if (!requestId) return;
334
+
335
+ if (cancelled) return;
336
+
337
+ while (!cancelled) {
338
+ const status = await provingClient.getProofStatus(requestId);
339
+ if (cancelled) return;
340
+
341
+ setFulfillementStatus(status.fulfillmentStatus);
342
+
343
+ if (status.fulfillmentStatus === FulfillmentStatus.Unfulfillable) {
344
+ setProof(undefined);
345
+ return;
346
+ }
347
+
348
+ if (status.fulfillmentStatus === FulfillmentStatus.Fulfilled) {
349
+ // Depending on how your backend behaves, proof should be present on Fulfilled.
350
+ if (!status.proof) {
351
+ throw new Error("Fulfilled proof request returned no proof bytes");
352
+ }
353
+
354
+ setProof(status.proof);
355
+
356
+ return;
357
+ }
358
+
359
+ await sleep(1000);
360
+ }
361
+ })().catch((e) => {
362
+ if (!cancelled) {
363
+ console.error("[ZCAM1] useProofRequestStatus failed", e);
364
+ }
365
+ });
366
+
367
+ return () => {
368
+ cancelled = true;
369
+ };
370
+ }, [provingClient, requestId]);
371
+
372
+ return {
373
+ isInitializing,
374
+ error,
375
+ fulfillementStatus,
376
+ proof,
377
+ };
378
+ }
379
+
380
+ export class ProvingClient {
381
+ client: IosProvingClientInterface;
382
+ contentKeyId: Uint8Array;
383
+ certChainPem: string;
384
+ production: boolean;
385
+ onProofRequest?: (requestId: string, photoPath: string) => void;
386
+
387
+ constructor(
388
+ client: IosProvingClientInterface,
389
+ contentKeyId: Uint8Array,
390
+ certChainPem: string,
391
+ production: boolean,
392
+ onProofRequest?: (requestId: string, photoPath: string) => void,
393
+ ) {
394
+ this.client = client;
395
+ this.contentKeyId = contentKeyId;
396
+ this.certChainPem = certChainPem;
397
+ this.production = production;
398
+ this.onProofRequest = onProofRequest;
399
+ }
400
+
401
+ /**
402
+ * Embeds a cryptographic proof into an image file by modifying its C2PA manifest.
403
+ * @param originalPath - Path to the original image file
404
+ * @param deviceInfo - Device information for signing
405
+ * @param settings - Configuration settings for proof generation
406
+ * @returns Request ID for polling proof status
407
+ */
408
+ async requestProof(originalPath: string): Promise<string> {
409
+ originalPath = originalPath.replace("file://", "");
410
+ const format = formatFromPath(originalPath);
411
+
412
+ if (format === undefined) {
413
+ throw new Error(`Unsupported file format: ${originalPath}`);
414
+ }
415
+
416
+ const requestId = await this.client.requestProof(originalPath, format, {
417
+ appAttestProduction: this.production,
418
+ });
419
+
420
+ if (this.onProofRequest) {
421
+ this.onProofRequest(requestId, originalPath);
422
+ }
423
+
424
+ return requestId;
425
+ }
426
+
427
+ async getProofStatus(requestId: string): Promise<ProofRequestStatus> {
428
+ return await this.client.getProofStatus(requestId);
429
+ }
430
+
431
+ /**
432
+ * Embeds a cryptographic proof into an image file by modifying its C2PA manifest.
433
+ * @param originalPath - Path to the original image file
434
+ * @param deviceInfo - Device information for signing
435
+ * @param settings - Configuration settings for proof generation
436
+ * @returns Path to the new file with embedded proof
437
+ */
438
+ async embedProof(originalPath: string, proof: ArrayBuffer): Promise<string> {
439
+ originalPath = originalPath.replace("file://", "");
440
+ const format = formatFromPath(originalPath);
441
+ const ext = Util.extname(originalPath);
442
+
443
+ if (format === undefined) {
444
+ throw new Error(`Unsupported file format: ${originalPath}`);
445
+ }
446
+
447
+ const manifestEditor = ManifestEditor.fromManifest(
448
+ originalPath,
449
+ this.contentKeyId.buffer as ArrayBuffer,
450
+ this.certChainPem,
451
+ );
452
+
453
+ const vkHash = this.client.vkHash();
454
+
455
+ // Include the proof to the C2PA manifest
456
+ manifestEditor.addAssertion(
457
+ "succinct.proof",
458
+ JSON.stringify({
459
+ data: base64.encode(new Uint8Array(proof)),
460
+ vk_hash: vkHash,
461
+ }),
462
+ );
463
+
464
+ const destinationPath =
465
+ Dirs.CacheDir + `/zcam-${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
466
+
467
+ // Embed the manifest to the photo
468
+ await manifestEditor.embedManifestToFile(destinationPath, format);
469
+
470
+ return destinationPath;
471
+ }
472
+
473
+ async waitAndEmbedProof(originalPath: string): Promise<string> {
474
+ const requestId = await this.requestProof(originalPath);
475
+ console.log(`Request ID: ${requestId}`);
476
+ const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
477
+
478
+ while (true) {
479
+ const status = await this.getProofStatus(requestId);
480
+
481
+ if (status.fulfillmentStatus === FulfillmentStatus.Unfulfillable) {
482
+ throw new Error("The proof is unfulfillable");
483
+ }
484
+
485
+ if (status.fulfillmentStatus === FulfillmentStatus.Fulfilled) {
486
+ return this.embedProof(originalPath, status.proof!);
487
+ }
488
+
489
+ await sleep(1000);
490
+ }
491
+ }
492
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { sha256 } from "@noble/hashes/sha2.js";
2
+ import { generateHardwareSignatureWithAssertion } from "@pagopa/io-react-native-integrity";
3
+ import { base64 } from "@scure/base";
4
+
5
+ function stringToArray(s: string): Uint8Array {
6
+ return new TextEncoder().encode(s);
7
+ }
8
+
9
+ export async function generateAppAttestAssertion(
10
+ dataHash: ArrayBuffer,
11
+ normalizedMetadata: string,
12
+ deviceKeyId: string,
13
+ ): Promise<string> {
14
+ let assertion: string;
15
+
16
+ const metadataBytes = stringToArray(normalizedMetadata);
17
+
18
+ try {
19
+ assertion = await generateHardwareSignatureWithAssertion(
20
+ base64.encode(new Uint8Array(dataHash)) + "|" + base64.encode(sha256(metadataBytes)),
21
+ deviceKeyId,
22
+ );
23
+ } catch (error: unknown) {
24
+ const err = error as { code?: string; message?: string } | undefined;
25
+ if (err?.code === "-1" || err?.message?.includes("UNSUPPORTED_SERVICE")) {
26
+ console.warn(
27
+ "[ZCAMs] Running in simulator - using mock attestation. This is for development only.",
28
+ );
29
+ // Use a mock attestation for simulator testing
30
+ // In production, this would need to be rejected by the backend
31
+ assertion = `SIMULATOR_MOCK_${deviceKeyId}_${Date.now()}`;
32
+ } else {
33
+ throw error;
34
+ }
35
+ }
36
+
37
+ return assertion;
38
+ }
package/src/verify.tsx ADDED
@@ -0,0 +1,119 @@
1
+ import { utf8ToBytes } from "@noble/hashes/utils.js";
2
+ import { base64 } from "@scure/base";
3
+
4
+ import {
5
+ computeHash,
6
+ extractManifest,
7
+ type ManifestInterface,
8
+ PhotoMetadataInfo,
9
+ verifyBindingsFromManifest,
10
+ verifyGroth16,
11
+ VideoMetadataInfo,
12
+ } from "./bindings";
13
+
14
+ /**
15
+ * Capture metadata extracted from the C2PA manifest.
16
+ * Contains device info and camera settings at capture time.
17
+ */
18
+ export interface CaptureMetadata {
19
+ action: string;
20
+ when: string;
21
+ parameters: PhotoMetadataInfo | VideoMetadataInfo;
22
+ }
23
+
24
+ export const APPLE_ROOT_CERT =
25
+ "MIICITCCAaegAwIBAgIQC/O+DvHN0uD7jG5yH2IXmDAKBggqhkjOPQQDAzBSMSYwJAYDVQQDDB1BcHBsZSBBcHAgQXR0ZXN0YXRpb24gUm9vdCBDQTETMBEGA1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0yMDAzMTgxODMyNTNaFw00NTAzMTUwMDAwMDBaMFIxJjAkBgNVBAMMHUFwcGxlIEFwcCBBdHRlc3RhdGlvbiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERTHhmLW07ATaFQIEVwTtT4dyctdhNbJhFs/Ii2FdCgAHGbpphY3+d8qjuDngIN3WVhQUBHAoMeQ/cLiP1sOUtgjqK9auYen1mMEvRq9Sk3Jm5X8U62H+xTD3FE9TgS41o0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSskRBTM72+aEH/pwyp5frq5eWKoTAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIwQgFGnByvsiVbpTKwSga0kP0e8EeDS4+sQmTvb7vn53O5+FRXgeLhpJ06ysC5PrOyAjEAp5U4xDgEgllF7En3VcE3iexZZtKeYnpqtijVoyFraWVIyd/dganmrduC1bmTBGwD";
26
+
27
+ /**
28
+ * Represents a file with a C2PA manifest that can be verified for authenticity.
29
+ */
30
+ export class VerifiableFile {
31
+ path: string;
32
+ activeManifest: ManifestInterface;
33
+ hash: ArrayBuffer | undefined;
34
+
35
+ /**
36
+ * Creates a VerifiableFile instance by extracting the C2PA manifest from the file.
37
+ * @param path - Path to the file to verify
38
+ */
39
+ constructor(path: string) {
40
+ const store = extractManifest(path);
41
+
42
+ this.path = path;
43
+ this.activeManifest = store.activeManifest();
44
+ }
45
+
46
+ /**
47
+ * Verifies the manifest's bindings (e.g., App Attest).
48
+ */
49
+ verifyBindings(appAttestProduction: boolean): boolean {
50
+ if (this.hash === undefined) {
51
+ this.hash = computeHash(this.path);
52
+ }
53
+
54
+ return verifyBindingsFromManifest(
55
+ this.activeManifest.bindings()!,
56
+ this.activeManifest.captureMetadataAction()!,
57
+ this.hash,
58
+ appAttestProduction,
59
+ );
60
+ }
61
+
62
+ /**
63
+ * Verifies the cryptographic proof embedded in the C2PA manifest.
64
+ * @returns True if the proof is valid, false otherwise
65
+ */
66
+ verifyProof(appId: string): boolean {
67
+ return verifyProofFromManifest(this.activeManifest, this.path, appId);
68
+ }
69
+
70
+ /**
71
+ * Returns the file's content hash as recorded in the active C2PA manifest.
72
+ * @returns The manifest data hash (base64-encoded string)
73
+ */
74
+ dataHash(): string | undefined {
75
+ if (this.hash === undefined) {
76
+ this.hash = computeHash(this.path);
77
+ }
78
+
79
+ return base64.encode(new Uint8Array(this.hash));
80
+ }
81
+
82
+ /**
83
+ * Returns the capture metadata from the C2PA manifest.
84
+ * Contains device info and camera settings recorded at capture time.
85
+ * @returns The capture metadata, or null if not present
86
+ */
87
+ captureMetadata(): CaptureMetadata | null {
88
+ const actionJson = this.activeManifest.captureMetadataAction();
89
+ if (!actionJson) return null;
90
+ return JSON.parse(actionJson) as CaptureMetadata;
91
+ }
92
+ }
93
+
94
+ function verifyProofFromManifest(
95
+ activeManifest: ManifestInterface,
96
+ path: string,
97
+ appId: string,
98
+ ): boolean {
99
+ const proof = activeManifest.proof();
100
+
101
+ if (proof === undefined) {
102
+ throw new Error("The proof was not found in the manifest");
103
+ }
104
+
105
+ const hash = new Uint8Array(computeHash(path));
106
+ const appIdBytes = utf8ToBytes(appId);
107
+ const appleRootCert = utf8ToBytes(APPLE_ROOT_CERT);
108
+
109
+ const publicInputs = new Uint8Array(hash.length + appIdBytes.length + appleRootCert.length);
110
+ publicInputs.set(hash);
111
+ publicInputs.set(appIdBytes, hash.length);
112
+ publicInputs.set(appleRootCert, hash.length + appIdBytes.length);
113
+
114
+ return verifyGroth16(
115
+ base64.decode(proof.data).buffer as ArrayBuffer,
116
+ publicInputs.buffer,
117
+ proof.vkHash,
118
+ );
119
+ }
package/turbo.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "https://turbo.build/schema.json",
3
+ "globalDependencies": [".nvmrc", ".yarnrc.yml"],
4
+ "globalEnv": ["NODE_ENV"],
5
+ "tasks": {
6
+ "build:android": {
7
+ "env": ["ANDROID_HOME", "ORG_GRADLE_PROJECT_newArchEnabled"],
8
+ "inputs": [
9
+ "package.json",
10
+ "android",
11
+ "!android/build",
12
+ "src/*.ts",
13
+ "src/*.tsx"
14
+ ],
15
+ "outputs": []
16
+ },
17
+ "build:ios": {
18
+ "env": [
19
+ "RCT_NEW_ARCH_ENABLED",
20
+ "RCT_USE_RN_DEP",
21
+ "RCT_USE_PREBUILT_RNCORE"
22
+ ],
23
+ "inputs": ["package.json", "*.podspec", "ios", "src/*.ts", "src/*.tsx"],
24
+ "outputs": []
25
+ }
26
+ }
27
+ }