@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.
- package/Zcam1Sdk.podspec +2 -2
- package/android/CMakeLists.txt +114 -0
- package/android/build.gradle +213 -0
- package/android/cpp-adapter-proving.cpp +35 -0
- package/android/cpp-adapter.cpp +35 -0
- package/android/src/main/AndroidManifest.xml +5 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/Zcam1CaptureModule.kt +156 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/Zcam1CapturePackage.kt +38 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/Zcam1ProvingModule.kt +43 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/Zcam1ProvingPackage.kt +34 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/Zcam1SdkModule.kt +43 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/Zcam1SdkPackage.kt +34 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/camera/CameraUtils.kt +80 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/camera/Zcam1CameraService.kt +588 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/camera/Zcam1CameraView.kt +107 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/camera/Zcam1CameraViewManager.kt +33 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/camera/Zcam1OrientationManager.kt +73 -0
- package/cpp/generated/zcam1_c2pa_utils.cpp +170 -365
- package/cpp/generated/zcam1_c2pa_utils.hpp +0 -4
- package/cpp/generated/zcam1_certs_utils.cpp +121 -250
- package/cpp/generated/zcam1_common.cpp +1871 -0
- package/cpp/generated/zcam1_common.hpp +52 -0
- package/cpp/generated/zcam1_verify_utils.cpp +138 -265
- package/cpp/generated/zcam1_verify_utils.hpp +2 -2
- package/cpp/proving/generated/zcam1_common.cpp +1871 -0
- package/cpp/proving/generated/zcam1_common.hpp +52 -0
- package/cpp/proving/generated/zcam1_proving_utils.cpp +355 -417
- package/cpp/proving/generated/zcam1_proving_utils.hpp +13 -17
- package/cpp/proving/zcam1-proving.cpp +2 -0
- package/cpp/zcam1-sdk.cpp +2 -0
- package/lib/module/bindings.js +4 -0
- package/lib/module/bindings.js.map +1 -1
- package/lib/module/camera.js +71 -13
- package/lib/module/camera.js.map +1 -1
- package/lib/module/capture.js +115 -38
- package/lib/module/capture.js.map +1 -1
- package/lib/module/common.js +18 -2
- package/lib/module/common.js.map +1 -1
- package/lib/module/generated/zcam1_c2pa_utils-ffi.js +4 -0
- package/lib/module/generated/zcam1_c2pa_utils-ffi.js.map +1 -1
- package/lib/module/generated/zcam1_c2pa_utils.js +117 -9
- package/lib/module/generated/zcam1_c2pa_utils.js.map +1 -1
- package/lib/module/generated/zcam1_certs_utils-ffi.js +4 -0
- package/lib/module/generated/zcam1_certs_utils-ffi.js.map +1 -1
- package/lib/module/generated/zcam1_certs_utils.js +6 -2
- package/lib/module/generated/zcam1_certs_utils.js.map +1 -1
- package/lib/module/generated/zcam1_common-ffi.js +47 -0
- package/lib/module/generated/zcam1_common-ffi.js.map +1 -0
- package/lib/module/generated/zcam1_common.js +60 -0
- package/lib/module/generated/zcam1_common.js.map +1 -0
- package/lib/module/generated/zcam1_proving_utils-ffi.js +4 -0
- package/lib/module/generated/zcam1_proving_utils-ffi.js.map +1 -1
- package/lib/module/generated/zcam1_proving_utils.js +53 -46
- package/lib/module/generated/zcam1_proving_utils.js.map +1 -1
- package/lib/module/generated/zcam1_verify_utils-ffi.js +4 -0
- package/lib/module/generated/zcam1_verify_utils-ffi.js.map +1 -1
- package/lib/module/generated/zcam1_verify_utils.js +70 -22
- package/lib/module/generated/zcam1_verify_utils.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/proving/NativeZcam1Proving.js +1 -1
- package/lib/module/proving/index.js +1 -1
- package/lib/module/proving/index.js.map +1 -1
- package/lib/module/proving/prove.js +14 -8
- package/lib/module/proving/prove.js.map +1 -1
- package/lib/module/utils.js +19 -14
- package/lib/module/utils.js.map +1 -1
- package/lib/module/verify.js +14 -22
- package/lib/module/verify.js.map +1 -1
- package/lib/typescript/src/bindings.d.ts +3 -0
- package/lib/typescript/src/bindings.d.ts.map +1 -1
- package/lib/typescript/src/camera.d.ts +15 -0
- package/lib/typescript/src/camera.d.ts.map +1 -1
- package/lib/typescript/src/capture.d.ts +40 -1
- package/lib/typescript/src/capture.d.ts.map +1 -1
- package/lib/typescript/src/common.d.ts.map +1 -1
- package/lib/typescript/src/generated/zcam1_c2pa_utils-ffi.d.ts +37 -46
- package/lib/typescript/src/generated/zcam1_c2pa_utils-ffi.d.ts.map +1 -1
- package/lib/typescript/src/generated/zcam1_c2pa_utils.d.ts +110 -8
- package/lib/typescript/src/generated/zcam1_c2pa_utils.d.ts.map +1 -1
- package/lib/typescript/src/generated/zcam1_certs_utils-ffi.d.ts +27 -32
- package/lib/typescript/src/generated/zcam1_certs_utils-ffi.d.ts.map +1 -1
- package/lib/typescript/src/generated/zcam1_certs_utils.d.ts.map +1 -1
- package/lib/typescript/src/generated/zcam1_common-ffi.d.ts +77 -0
- package/lib/typescript/src/generated/zcam1_common-ffi.d.ts.map +1 -0
- package/lib/typescript/src/generated/zcam1_common.d.ts +17 -0
- package/lib/typescript/src/generated/zcam1_common.d.ts.map +1 -0
- package/lib/typescript/src/generated/zcam1_proving_utils-ffi.d.ts +44 -51
- package/lib/typescript/src/generated/zcam1_proving_utils-ffi.d.ts.map +1 -1
- package/lib/typescript/src/generated/zcam1_proving_utils.d.ts +26 -26
- package/lib/typescript/src/generated/zcam1_proving_utils.d.ts.map +1 -1
- package/lib/typescript/src/generated/zcam1_verify_utils-ffi.d.ts +29 -34
- package/lib/typescript/src/generated/zcam1_verify_utils-ffi.d.ts.map +1 -1
- package/lib/typescript/src/generated/zcam1_verify_utils.d.ts +94 -14
- package/lib/typescript/src/generated/zcam1_verify_utils.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/proving/NativeZcam1Proving.d.ts +1 -1
- package/lib/typescript/src/proving/index.d.ts +1 -1
- package/lib/typescript/src/proving/index.d.ts.map +1 -1
- package/lib/typescript/src/proving/prove.d.ts +3 -3
- package/lib/typescript/src/proving/prove.d.ts.map +1 -1
- package/lib/typescript/src/utils.d.ts.map +1 -1
- package/lib/typescript/src/verify.d.ts +4 -3
- package/lib/typescript/src/verify.d.ts.map +1 -1
- package/package.json +13 -6
- package/react-native.config.js +11 -0
- package/src/bindings.tsx +4 -0
- package/src/camera.tsx +116 -11
- package/src/capture.tsx +150 -53
- package/src/common.tsx +22 -2
- package/src/generated/zcam1_c2pa_utils-ffi.ts +42 -56
- package/src/generated/zcam1_c2pa_utils.ts +224 -67
- package/src/generated/zcam1_certs_utils-ffi.ts +33 -36
- package/src/generated/zcam1_certs_utils.ts +27 -24
- package/src/generated/zcam1_common-ffi.ts +183 -0
- package/src/generated/zcam1_common.ts +116 -0
- package/src/generated/zcam1_proving_utils-ffi.ts +54 -67
- package/src/generated/zcam1_proving_utils.ts +133 -138
- package/src/generated/zcam1_verify_utils-ffi.ts +39 -40
- package/src/generated/zcam1_verify_utils.ts +109 -47
- package/src/index.ts +1 -1
- package/src/proving/NativeZcam1Proving.ts +2 -2
- package/src/proving/index.ts +1 -1
- package/src/proving/prove.tsx +22 -11
- package/src/utils.ts +26 -20
- 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
|
-
|
|
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 {
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
|