@succinctlabs/react-native-zcam1 0.3.0 → 0.4.0-alpha.2

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 (127) hide show
  1. package/Zcam1Sdk.podspec +2 -2
  2. package/android/CMakeLists.txt +114 -0
  3. package/android/build.gradle +213 -0
  4. package/android/cpp-adapter-proving.cpp +35 -0
  5. package/android/cpp-adapter.cpp +35 -0
  6. package/android/src/main/AndroidManifest.xml +5 -0
  7. package/android/src/main/java/com/succinctlabs/zcam1sdk/Zcam1CaptureModule.kt +156 -0
  8. package/android/src/main/java/com/succinctlabs/zcam1sdk/Zcam1CapturePackage.kt +38 -0
  9. package/android/src/main/java/com/succinctlabs/zcam1sdk/Zcam1ProvingModule.kt +43 -0
  10. package/android/src/main/java/com/succinctlabs/zcam1sdk/Zcam1ProvingPackage.kt +34 -0
  11. package/android/src/main/java/com/succinctlabs/zcam1sdk/Zcam1SdkModule.kt +43 -0
  12. package/android/src/main/java/com/succinctlabs/zcam1sdk/Zcam1SdkPackage.kt +34 -0
  13. package/android/src/main/java/com/succinctlabs/zcam1sdk/camera/CameraUtils.kt +80 -0
  14. package/android/src/main/java/com/succinctlabs/zcam1sdk/camera/Zcam1CameraService.kt +588 -0
  15. package/android/src/main/java/com/succinctlabs/zcam1sdk/camera/Zcam1CameraView.kt +107 -0
  16. package/android/src/main/java/com/succinctlabs/zcam1sdk/camera/Zcam1CameraViewManager.kt +33 -0
  17. package/android/src/main/java/com/succinctlabs/zcam1sdk/camera/Zcam1OrientationManager.kt +73 -0
  18. package/cpp/generated/zcam1_c2pa_utils.cpp +170 -365
  19. package/cpp/generated/zcam1_c2pa_utils.hpp +0 -4
  20. package/cpp/generated/zcam1_certs_utils.cpp +121 -250
  21. package/cpp/generated/zcam1_common.cpp +1871 -0
  22. package/cpp/generated/zcam1_common.hpp +52 -0
  23. package/cpp/generated/zcam1_verify_utils.cpp +138 -265
  24. package/cpp/generated/zcam1_verify_utils.hpp +2 -2
  25. package/cpp/proving/generated/zcam1_common.cpp +1871 -0
  26. package/cpp/proving/generated/zcam1_common.hpp +52 -0
  27. package/cpp/proving/generated/zcam1_proving_utils.cpp +355 -417
  28. package/cpp/proving/generated/zcam1_proving_utils.hpp +13 -17
  29. package/cpp/proving/zcam1-proving.cpp +2 -0
  30. package/cpp/zcam1-sdk.cpp +2 -0
  31. package/lib/module/bindings.js +4 -0
  32. package/lib/module/bindings.js.map +1 -1
  33. package/lib/module/camera.js +71 -13
  34. package/lib/module/camera.js.map +1 -1
  35. package/lib/module/capture.js +115 -38
  36. package/lib/module/capture.js.map +1 -1
  37. package/lib/module/common.js +18 -2
  38. package/lib/module/common.js.map +1 -1
  39. package/lib/module/generated/zcam1_c2pa_utils-ffi.js +4 -0
  40. package/lib/module/generated/zcam1_c2pa_utils-ffi.js.map +1 -1
  41. package/lib/module/generated/zcam1_c2pa_utils.js +117 -9
  42. package/lib/module/generated/zcam1_c2pa_utils.js.map +1 -1
  43. package/lib/module/generated/zcam1_certs_utils-ffi.js +4 -0
  44. package/lib/module/generated/zcam1_certs_utils-ffi.js.map +1 -1
  45. package/lib/module/generated/zcam1_certs_utils.js +6 -2
  46. package/lib/module/generated/zcam1_certs_utils.js.map +1 -1
  47. package/lib/module/generated/zcam1_common-ffi.js +47 -0
  48. package/lib/module/generated/zcam1_common-ffi.js.map +1 -0
  49. package/lib/module/generated/zcam1_common.js +60 -0
  50. package/lib/module/generated/zcam1_common.js.map +1 -0
  51. package/lib/module/generated/zcam1_proving_utils-ffi.js +4 -0
  52. package/lib/module/generated/zcam1_proving_utils-ffi.js.map +1 -1
  53. package/lib/module/generated/zcam1_proving_utils.js +53 -46
  54. package/lib/module/generated/zcam1_proving_utils.js.map +1 -1
  55. package/lib/module/generated/zcam1_verify_utils-ffi.js +4 -0
  56. package/lib/module/generated/zcam1_verify_utils-ffi.js.map +1 -1
  57. package/lib/module/generated/zcam1_verify_utils.js +70 -22
  58. package/lib/module/generated/zcam1_verify_utils.js.map +1 -1
  59. package/lib/module/index.js +1 -1
  60. package/lib/module/index.js.map +1 -1
  61. package/lib/module/proving/NativeZcam1Proving.js +1 -1
  62. package/lib/module/proving/index.js +1 -1
  63. package/lib/module/proving/index.js.map +1 -1
  64. package/lib/module/proving/prove.js +14 -8
  65. package/lib/module/proving/prove.js.map +1 -1
  66. package/lib/module/utils.js +19 -14
  67. package/lib/module/utils.js.map +1 -1
  68. package/lib/module/verify.js +14 -22
  69. package/lib/module/verify.js.map +1 -1
  70. package/lib/typescript/src/bindings.d.ts +3 -0
  71. package/lib/typescript/src/bindings.d.ts.map +1 -1
  72. package/lib/typescript/src/camera.d.ts +15 -0
  73. package/lib/typescript/src/camera.d.ts.map +1 -1
  74. package/lib/typescript/src/capture.d.ts +40 -1
  75. package/lib/typescript/src/capture.d.ts.map +1 -1
  76. package/lib/typescript/src/common.d.ts.map +1 -1
  77. package/lib/typescript/src/generated/zcam1_c2pa_utils-ffi.d.ts +37 -46
  78. package/lib/typescript/src/generated/zcam1_c2pa_utils-ffi.d.ts.map +1 -1
  79. package/lib/typescript/src/generated/zcam1_c2pa_utils.d.ts +110 -8
  80. package/lib/typescript/src/generated/zcam1_c2pa_utils.d.ts.map +1 -1
  81. package/lib/typescript/src/generated/zcam1_certs_utils-ffi.d.ts +27 -32
  82. package/lib/typescript/src/generated/zcam1_certs_utils-ffi.d.ts.map +1 -1
  83. package/lib/typescript/src/generated/zcam1_certs_utils.d.ts.map +1 -1
  84. package/lib/typescript/src/generated/zcam1_common-ffi.d.ts +77 -0
  85. package/lib/typescript/src/generated/zcam1_common-ffi.d.ts.map +1 -0
  86. package/lib/typescript/src/generated/zcam1_common.d.ts +17 -0
  87. package/lib/typescript/src/generated/zcam1_common.d.ts.map +1 -0
  88. package/lib/typescript/src/generated/zcam1_proving_utils-ffi.d.ts +44 -51
  89. package/lib/typescript/src/generated/zcam1_proving_utils-ffi.d.ts.map +1 -1
  90. package/lib/typescript/src/generated/zcam1_proving_utils.d.ts +26 -26
  91. package/lib/typescript/src/generated/zcam1_proving_utils.d.ts.map +1 -1
  92. package/lib/typescript/src/generated/zcam1_verify_utils-ffi.d.ts +29 -34
  93. package/lib/typescript/src/generated/zcam1_verify_utils-ffi.d.ts.map +1 -1
  94. package/lib/typescript/src/generated/zcam1_verify_utils.d.ts +94 -14
  95. package/lib/typescript/src/generated/zcam1_verify_utils.d.ts.map +1 -1
  96. package/lib/typescript/src/index.d.ts +1 -1
  97. package/lib/typescript/src/index.d.ts.map +1 -1
  98. package/lib/typescript/src/proving/NativeZcam1Proving.d.ts +1 -1
  99. package/lib/typescript/src/proving/index.d.ts +1 -1
  100. package/lib/typescript/src/proving/index.d.ts.map +1 -1
  101. package/lib/typescript/src/proving/prove.d.ts +3 -3
  102. package/lib/typescript/src/proving/prove.d.ts.map +1 -1
  103. package/lib/typescript/src/utils.d.ts.map +1 -1
  104. package/lib/typescript/src/verify.d.ts +4 -3
  105. package/lib/typescript/src/verify.d.ts.map +1 -1
  106. package/package.json +13 -6
  107. package/react-native.config.js +11 -0
  108. package/src/bindings.tsx +4 -0
  109. package/src/camera.tsx +116 -11
  110. package/src/capture.tsx +150 -53
  111. package/src/common.tsx +22 -2
  112. package/src/generated/zcam1_c2pa_utils-ffi.ts +42 -56
  113. package/src/generated/zcam1_c2pa_utils.ts +224 -67
  114. package/src/generated/zcam1_certs_utils-ffi.ts +33 -36
  115. package/src/generated/zcam1_certs_utils.ts +27 -24
  116. package/src/generated/zcam1_common-ffi.ts +183 -0
  117. package/src/generated/zcam1_common.ts +116 -0
  118. package/src/generated/zcam1_proving_utils-ffi.ts +54 -67
  119. package/src/generated/zcam1_proving_utils.ts +133 -138
  120. package/src/generated/zcam1_verify_utils-ffi.ts +39 -40
  121. package/src/generated/zcam1_verify_utils.ts +109 -47
  122. package/src/index.ts +1 -1
  123. package/src/proving/NativeZcam1Proving.ts +2 -2
  124. package/src/proving/index.ts +1 -1
  125. package/src/proving/prove.tsx +22 -11
  126. package/src/utils.ts +26 -20
  127. package/src/verify.tsx +25 -42
package/src/camera.tsx CHANGED
@@ -1,14 +1,18 @@
1
+ import Geolocation from "@react-native-community/geolocation";
1
2
  import JailMonkey from "jail-monkey";
2
3
  import React from "react";
3
- import { requireNativeComponent, type StyleProp, type ViewStyle } from "react-native";
4
+ import { Platform, requireNativeComponent, type StyleProp, type ViewStyle } from "react-native";
5
+ import { isEmulator } from "react-native-device-info";
4
6
  import { Dirs, Util } from "react-native-file-access";
5
7
 
6
8
  import {
9
+ AuthenticityData,
7
10
  buildSelfSignedCertificate,
8
11
  computeHash,
9
12
  DepthData,
10
13
  ExistingCertChain,
11
14
  formatFromPath,
15
+ LocationInfo,
12
16
  ManifestEditor,
13
17
  type PhotoMetadataInfo,
14
18
  SelfSignedCertChain,
@@ -153,6 +157,22 @@ export interface ZCameraProps {
153
157
  * Use with `filmStyle` prop by casting the custom name: `filmStyle={"myStyle" as CameraFilmStyle}`.
154
158
  */
155
159
  customFilmStyles?: Record<string, FilmStyleRecipe>;
160
+ /**
161
+ * When true, embeds a trusted GPS timestamp in the C2PA manifest at capture time.
162
+ * Requires location permission. The timestamp is sourced from GPS rather than device clock,
163
+ * making it tamper-evident. Stored as `trustedTimestamp` in photo/video metadata.
164
+ * @default false
165
+ */
166
+ captureTimestampEnabled?: boolean;
167
+ /**
168
+ * When true, embeds GPS coordinates in the C2PA manifest at capture time.
169
+ * Requires location permission. Stored as `location` in photo/video metadata.
170
+ * If location retrieval fails, `isLocationAvailable` is set to `false` and
171
+ * `locationRetrievalStatus` contains the error reason.
172
+ * @default false
173
+ */
174
+ captureLocationEnabled?: boolean;
175
+
156
176
  /**
157
177
  * Enable depth data capture at session level.
158
178
  * When true, depth data can be captured but zoom may be restricted on dual-camera devices.
@@ -472,6 +492,16 @@ export class ZCamera extends React.PureComponent<ZCameraProps> {
472
492
  const when = new Date().toISOString().replace("T", " ").split(".")[0]!;
473
493
  const isJailBroken = JailMonkey.isJailBroken();
474
494
  const isLocationSpoofingAvailable = JailMonkey.canMockLocation();
495
+ const location = await retrieveLocationData(
496
+ this.props.captureTimestampEnabled,
497
+ this.props.captureLocationEnabled,
498
+ );
499
+ const authenticityData: AuthenticityData = {
500
+ isJailBroken,
501
+ isLocationSpoofingAvailable,
502
+ isLocationAvailable: location.isLocationAvailable,
503
+ locationRetrievalStatus: location.locationRetrievalStatus,
504
+ };
475
505
 
476
506
  result.filePath = await embedBindings(
477
507
  result.filePath,
@@ -492,11 +522,10 @@ export class ZCamera extends React.PureComponent<ZCameraProps> {
492
522
  audioCodec: result.audioCodec,
493
523
  audioSampleRate: result.audioSampleRate,
494
524
  audioChannels: result.audioChannels,
495
- authenticityData: {
496
- isJailBroken,
497
- isLocationSpoofingAvailable,
498
- },
525
+ authenticityData,
499
526
  filmStyle: this.resolveFilmStyleInfo(),
527
+ trustedTimestamp: location.trustedTimestamp,
528
+ location: location.coords,
500
529
  },
501
530
  this.props.captureInfo,
502
531
  this.certChainPem,
@@ -550,11 +579,21 @@ export class ZCamera extends React.PureComponent<ZCameraProps> {
550
579
  const tiff = metadata["{TIFF}"] ?? {};
551
580
 
552
581
  const when = tiff.DateTime || new Date().toISOString().replace("T", " ").split(".")[0];
553
- const deviceMake = tiff.Make || "Apple";
582
+ const deviceMake = tiff.Make || (Platform.OS === "android" ? "Android" : "Apple");
554
583
  const deviceModel = tiff.Model || "Unknown";
555
584
  const softwareVersion = tiff.Software || "Unknown";
556
585
  const isJailBroken = JailMonkey.isJailBroken();
557
586
  const isLocationSpoofingAvailable = JailMonkey.canMockLocation();
587
+ const location = await retrieveLocationData(
588
+ this.props.captureTimestampEnabled,
589
+ this.props.captureLocationEnabled,
590
+ );
591
+ const authenticityData: AuthenticityData = {
592
+ isJailBroken,
593
+ isLocationSpoofingAvailable,
594
+ isLocationAvailable: location.isLocationAvailable,
595
+ locationRetrievalStatus: location.locationRetrievalStatus,
596
+ };
558
597
 
559
598
  const destinationPath = await embedBindings(
560
599
  originalPath,
@@ -570,12 +609,11 @@ export class ZCamera extends React.PureComponent<ZCameraProps> {
570
609
  exposureTime: exif.ExposureTime,
571
610
  depthOfField: exif.FNumber,
572
611
  focalLength: exif.FocalLength,
573
- authenticityData: {
574
- isJailBroken,
575
- isLocationSpoofingAvailable,
576
- },
612
+ authenticityData,
577
613
  depthData: result.depthData as DepthData | undefined,
578
614
  filmStyle: this.resolveFilmStyleInfo(),
615
+ trustedTimestamp: location.trustedTimestamp,
616
+ location: location.coords,
579
617
  },
580
618
  this.props.captureInfo,
581
619
  this.certChainPem,
@@ -656,9 +694,18 @@ async function embedBindings(
656
694
  const destinationPath =
657
695
  Dirs.CacheDir + `/zcam-${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
658
696
 
697
+ // On Android, pass the KeyStore alias so Rust signs via JNI.
698
+ // On iOS, pass the contentKeyId (SHA1 of public key) so Rust signs via Secure Enclave.
699
+ const keyTag =
700
+ Platform.OS === "android"
701
+ ? new TextEncoder().encode(
702
+ (await isEmulator()) ? "ZCAM1_MOCK_CONTENT_KEY_TAG" : "ZCAM1_CONTENT_KEY_TAG",
703
+ )
704
+ : captureInfo.contentKeyId;
705
+
659
706
  const manifestEditor = new ManifestEditor(
660
707
  originalPath,
661
- captureInfo.contentKeyId.buffer as ArrayBuffer,
708
+ keyTag.buffer as ArrayBuffer,
662
709
  certChainPem,
663
710
  );
664
711
 
@@ -692,3 +739,61 @@ async function embedBindings(
692
739
 
693
740
  return destinationPath;
694
741
  }
742
+
743
+ type LocationData = {
744
+ coords: LocationInfo | undefined;
745
+ trustedTimestamp: bigint | undefined;
746
+ isLocationAvailable: boolean | undefined;
747
+ locationRetrievalStatus: string | undefined;
748
+ };
749
+
750
+ function retrieveLocationData(
751
+ captureTimestampEnabled: boolean | undefined,
752
+ captureLocationEnabled: boolean | undefined,
753
+ ): Promise<LocationData> {
754
+ if (!captureTimestampEnabled && !captureLocationEnabled) {
755
+ return Promise.resolve({
756
+ coords: undefined,
757
+ trustedTimestamp: undefined,
758
+ isLocationAvailable: undefined,
759
+ locationRetrievalStatus: undefined,
760
+ });
761
+ }
762
+
763
+ return new Promise((resolve) => {
764
+ Geolocation.getCurrentPosition(
765
+ (position) => {
766
+ resolve({
767
+ coords: captureLocationEnabled
768
+ ? {
769
+ latitude: position.coords.latitude.toFixed(6),
770
+ longitude: position.coords.longitude.toFixed(6),
771
+ altitude: position.coords.altitude?.toFixed(6),
772
+ accuracy: position.coords.accuracy.toFixed(6),
773
+ altitudeAccuracy: position.coords.altitudeAccuracy?.toFixed(6),
774
+ }
775
+ : undefined,
776
+ trustedTimestamp: captureTimestampEnabled
777
+ ? BigInt(Math.trunc(position.timestamp))
778
+ : undefined,
779
+ isLocationAvailable: true,
780
+ locationRetrievalStatus: "success",
781
+ });
782
+ },
783
+ (error) => {
784
+ console.warn(`[ZCAM1] failed to retrieve GPS location data: ${error.message}`);
785
+ resolve({
786
+ coords: undefined,
787
+ trustedTimestamp: undefined,
788
+ isLocationAvailable: false,
789
+ locationRetrievalStatus: error.message,
790
+ });
791
+ },
792
+ {
793
+ timeout: 1000, // 1 second
794
+ maximumAge: 60 * 1000, // 1 minute
795
+ enableHighAccuracy: true,
796
+ },
797
+ );
798
+ });
799
+ }
package/src/capture.tsx CHANGED
@@ -1,4 +1,16 @@
1
- import { generateHardwareKey, getAttestation } from "@pagopa/io-react-native-integrity";
1
+ import {
2
+ getPublicKeyFixed,
3
+ isKeyStrongboxBacked,
4
+ sign as cryptoSign,
5
+ } from "@pagopa/io-react-native-crypto";
6
+ import {
7
+ generateHardwareKey,
8
+ getAttestation,
9
+ isPlayServicesAvailable,
10
+ } from "@pagopa/io-react-native-integrity";
11
+ import Geolocation from "@react-native-community/geolocation";
12
+ import { PermissionsAndroid, Platform } from "react-native";
13
+ import { getBundleId, isEmulator } from "react-native-device-info";
2
14
  import EncryptedStorage from "react-native-encrypted-storage";
3
15
 
4
16
  import { type ECKey, getContentPublicKey, getSecureEnclaveKeyId } from "./common";
@@ -6,15 +18,15 @@ export { buildSelfSignedCertificate, SelfSignedCertChain } from "./bindings";
6
18
 
7
19
  /**
8
20
  * Camera component for capturing photos with secure enclave integration.
21
+ * Lazy-loaded to avoid native module errors on platforms without camera support.
9
22
  */
10
- export {
11
- type CameraFilmStyle,
12
- type FilmStyleEffect,
13
- type FilmStyleRecipe,
14
- type HighlightShadowConfig,
15
- type MonochromeConfig,
16
- type WhiteBalanceConfig,
17
- ZCamera,
23
+ export type {
24
+ CameraFilmStyle,
25
+ FilmStyleEffect,
26
+ FilmStyleRecipe,
27
+ HighlightShadowConfig,
28
+ MonochromeConfig,
29
+ WhiteBalanceConfig,
18
30
  } from "./camera";
19
31
 
20
32
  import NativeZcam1Capture from "./NativeZcam1Capture";
@@ -84,9 +96,11 @@ export class ZPhoto {
84
96
  * @returns Device information including keys, certificate chain, and attestation
85
97
  */
86
98
  export async function initCapture(settings: Settings): Promise<CaptureInfo> {
87
- let deviceKeyId = await EncryptedStorage.getItem(`deviceKeyId-${settings.appId}`);
88
-
89
99
  const contentPublicKey = await getContentPublicKey();
100
+ const isSimulator = await isEmulator();
101
+
102
+ // On Android, the appId is the package name.
103
+ const appId = Platform.OS == "android" ? getBundleId() : settings.appId;
90
104
 
91
105
  if (contentPublicKey.kty !== "EC") {
92
106
  throw new Error("Only EC public keys are supported");
@@ -94,42 +108,63 @@ export async function initCapture(settings: Settings): Promise<CaptureInfo> {
94
108
 
95
109
  const contentKeyId = getSecureEnclaveKeyId(contentPublicKey);
96
110
 
97
- if (deviceKeyId == null) {
98
- // Try to generate hardware key, but fall back to mock for simulator
99
- try {
100
- deviceKeyId = await generateHardwareKey();
101
- } catch (error: unknown) {
102
- // If running in simulator, hardware key generation is not supported
103
- const err = error as { code?: string; message?: string } | undefined;
104
- if (err?.code === "-1" || err?.message?.includes("UNSUPPORTED_SERVICE")) {
105
- console.warn(
106
- "[ZCAM] Running in simulator - using mock device key. This is for development only.",
107
- );
108
- // Generate a mock device key for simulator testing
109
- deviceKeyId = `SIMULATOR_DEVICE_KEY_${Date.now()}`;
110
- } else {
111
- throw error;
112
- }
111
+ let deviceKeyId = await EncryptedStorage.getItem(`deviceKeyId-${settings.appId}`);
112
+ let attestation = deviceKeyId
113
+ ? await EncryptedStorage.getItem(`attestation-${deviceKeyId}`)
114
+ : null;
115
+
116
+ if (deviceKeyId == null || attestation == null) {
117
+ switch (Platform.OS) {
118
+ case "android":
119
+ // On Android, getAttestation() creates the key AND returns the attestation
120
+ // certificate chain in a single call. generateHardwareKey() is iOS-only.
121
+ deviceKeyId = `ZCAM1_ANDROID_DEVICE_${appId}`;
122
+
123
+ if (isSimulator) {
124
+ // Emulator or device without Play Integrity support — use mock attestation.
125
+ console.warn(
126
+ "[ZCAM] Play Integrity not available - using mock attestation. This is for development only.",
127
+ );
128
+ attestation = `SIMULATOR_MOCK_${deviceKeyId}_${Date.now()}`;
129
+ } else {
130
+ attestation = await getAttestation(deviceKeyId, deviceKeyId);
131
+ }
132
+ break;
133
+
134
+ case "ios":
135
+ case "macos":
136
+ // iOS: generate key first, then attest separately
137
+ if (deviceKeyId == null) {
138
+ if (isSimulator) {
139
+ console.warn(
140
+ "[ZCAM] Running in simulator - using mock device key. This is for development only.",
141
+ );
142
+ deviceKeyId = `SIMULATOR_DEVICE_KEY_${Date.now()}`;
143
+ } else {
144
+ deviceKeyId = await generateHardwareKey();
145
+ }
146
+ }
147
+ attestation = await updateRegistration(deviceKeyId!, settings);
148
+ break;
149
+
150
+ default:
151
+ throw new Error(`initCapture: ${Platform.OS} not supported`);
113
152
  }
114
- await EncryptedStorage.setItem(`deviceKeyId-${settings.appId}`, deviceKeyId);
153
+
154
+ await EncryptedStorage.setItem(`deviceKeyId-${settings.appId}`, deviceKeyId!);
155
+ await EncryptedStorage.setItem(`attestation-${deviceKeyId}`, attestation!);
115
156
  }
116
157
 
117
158
  if (deviceKeyId == null) {
118
159
  throw new Error("Failed to generate a device key");
119
160
  }
120
161
 
121
- let attestation = await EncryptedStorage.getItem(`attestation-${deviceKeyId}`);
122
-
123
- if (attestation == null) {
124
- attestation = await updateRegistration(deviceKeyId, settings);
125
- }
126
-
127
162
  return {
128
- appId: settings.appId,
163
+ appId,
129
164
  deviceKeyId,
130
165
  contentPublicKey,
131
166
  contentKeyId,
132
- attestation,
167
+ attestation: attestation!,
133
168
  };
134
169
  }
135
170
 
@@ -140,26 +175,88 @@ export async function initCapture(settings: Settings): Promise<CaptureInfo> {
140
175
  * @returns Attestation data and challenge
141
176
  */
142
177
  export async function updateRegistration(keyId: string, _settings: Settings): Promise<string> {
143
- // Try to get real attestation, but fall back to mock for simulator
144
- let attestation: string;
145
- try {
146
- attestation = await getAttestation(keyId, keyId);
147
- } catch (error: unknown) {
148
- // If running in simulator, App Attest is not supported
149
- const err = error as { code?: string; message?: string } | undefined;
150
- if (err?.code === "-1" || err?.message?.includes("UNSUPPORTED_SERVICE")) {
151
- console.warn(
152
- "[ZCAM] Running in simulator - using mock attestation. This is for development only.",
153
- );
154
- // Use a mock attestation for simulator testing
155
- // In production, this would need to be rejected by the backend
156
- return `SIMULATOR_MOCK_${keyId}_${Date.now()}`;
157
- } else {
158
- throw error;
159
- }
178
+ const isSimulator = await isEmulator();
179
+
180
+ if (isSimulator) {
181
+ console.warn(
182
+ "[ZCAM] Running in simulator - using mock attestation. This is for development only.",
183
+ );
184
+ return `SIMULATOR_MOCK_${keyId}_${Date.now()}`;
160
185
  }
161
186
 
187
+ const attestation = await getAttestation(keyId, keyId);
162
188
  await EncryptedStorage.setItem(`attestation-${keyId}`, attestation);
163
189
 
164
190
  return attestation;
165
191
  }
192
+
193
+ /**
194
+ * Requests camera (and microphone) permissions on Android.
195
+ * No-op on iOS — the system prompts automatically when the camera is accessed.
196
+ */
197
+ export async function requestCameraPermission(): Promise<void> {
198
+ if (Platform.OS !== "android") return;
199
+ await PermissionsAndroid.requestMultiple([
200
+ PermissionsAndroid.PERMISSIONS.CAMERA,
201
+ PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
202
+ ]);
203
+ }
204
+
205
+ /**
206
+ * Requests location permission from the user.
207
+ * This function triggers the native location authorization prompt on the device.
208
+ * @throws {string} Error message if permission request fails
209
+ */
210
+ export function requestLocationPermission() {
211
+ Geolocation.requestAuthorization(
212
+ () => {},
213
+ (error) => {
214
+ throw error.message;
215
+ },
216
+ );
217
+ }
218
+
219
+ /**
220
+ * Sign a message with the device hardware key.
221
+ * Uses SHA256withECDSA on Android (Android KeyStore) or SecureEnclave on iOS.
222
+ * @param deviceKeyId - The key alias/tag from initCapture().deviceKeyId
223
+ * @param message - The UTF-8 string message to sign
224
+ * @returns Base64-encoded ECDSA signature
225
+ */
226
+ export async function signWithDeviceKey(deviceKeyId: string, message: string): Promise<string> {
227
+ return cryptoSign(message, deviceKeyId);
228
+ }
229
+
230
+ /**
231
+ * Get the public key for a device key in JWK format.
232
+ * @param deviceKeyId - The key alias/tag from initCapture().deviceKeyId
233
+ * @returns The public key as an ECKey (JWK format with x, y coordinates)
234
+ */
235
+ export async function getDevicePublicKey(deviceKeyId: string): Promise<ECKey> {
236
+ const key = await getPublicKeyFixed(deviceKeyId);
237
+ if (key.kty !== "EC") {
238
+ throw new Error("Expected EC key, got " + key.kty);
239
+ }
240
+ return key;
241
+ }
242
+
243
+ /**
244
+ * Check if the device key is backed by StrongBox (highest hardware security level).
245
+ * Only meaningful on Android. Returns false on iOS.
246
+ * @param deviceKeyId - The key alias/tag from initCapture().deviceKeyId
247
+ * @returns true if StrongBox-backed, false if TEE-backed or unsupported
248
+ */
249
+ export async function isDeviceKeyStrongboxBacked(deviceKeyId: string): Promise<boolean> {
250
+ if (Platform.OS !== "android") return false;
251
+ return isKeyStrongboxBacked(deviceKeyId);
252
+ }
253
+
254
+ /**
255
+ * Check if Google Play Services is available (Android only).
256
+ * Required for Play Integrity token requests.
257
+ * @returns true if Play Services available, false otherwise. Always false on iOS.
258
+ */
259
+ export async function checkPlayServicesAvailable(): Promise<boolean> {
260
+ if (Platform.OS !== "android") return false;
261
+ return isPlayServicesAvailable();
262
+ }
package/src/common.tsx CHANGED
@@ -1,9 +1,20 @@
1
1
  import { sha1 } from "@noble/hashes/legacy.js";
2
2
  import { generate, getPublicKeyFixed, type PublicKey } from "@pagopa/io-react-native-crypto";
3
3
  import { base64, base64nopad, base64url, base64urlnopad } from "@scure/base";
4
+ import { Platform } from "react-native";
5
+ import { isEmulator } from "react-native-device-info";
4
6
 
5
7
  const CONTENT_KEY_TAG = "ZCAM1_CONTENT_KEY_TAG";
6
8
 
9
+ // Mock P-256 key used on emulators where hardware-backed key generation is unavailable.
10
+ // Public key coordinates correspond to MOCK_KEY in crates/c2pa-utils/src/manifest_editor.rs.
11
+ const MOCK_EMULATOR_CONTENT_KEY = {
12
+ kty: "EC" as const,
13
+ crv: "P-256",
14
+ x: "hcUZSvoPr0QDDmC0CwMFLgGcHUTas1g4RXET2nFv_BA",
15
+ y: "v5WB7DJhhKed3SmZpO8hVJQXRUSOzNSxrfnQ9kv1zTg",
16
+ };
17
+
7
18
  export interface ECKey {
8
19
  kty: "EC";
9
20
  crv: string;
@@ -24,8 +35,17 @@ function flexibleBase64Decode(str: string): Uint8Array {
24
35
  }
25
36
 
26
37
  export async function getContentPublicKey(): Promise<PublicKey> {
27
- return await getPublicKeyFixed(CONTENT_KEY_TAG).catch(() => {
28
- return generate(CONTENT_KEY_TAG);
38
+ const isAndroidEmulator = await isEmulator().then(
39
+ (isEmulator) => isEmulator && Platform.OS === "android",
40
+ );
41
+
42
+ if (isAndroidEmulator) {
43
+ return MOCK_EMULATOR_CONTENT_KEY;
44
+ }
45
+
46
+ return await getPublicKeyFixed(CONTENT_KEY_TAG).catch(async () => {
47
+ await generate(CONTENT_KEY_TAG);
48
+ return getPublicKeyFixed(CONTENT_KEY_TAG);
29
49
  });
30
50
  }
31
51