@necessarysdk/react-native 0.0.0-stage → 0.1.6

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 (117) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +2 -0
  4. package/NecessaryReactNative.podspec +23 -0
  5. package/README.md +429 -2
  6. package/android/build.gradle +83 -0
  7. package/android/src/main/AndroidManifest.xml +41 -0
  8. package/android/src/main/cpp/CMakeLists.txt +7 -0
  9. package/android/src/main/cpp/code_integrity.cpp +484 -0
  10. package/android/src/main/java/com/necessarysdk/reactnative/Causes.kt +49 -0
  11. package/android/src/main/java/com/necessarysdk/reactnative/InstallationKeyManager.kt +388 -0
  12. package/android/src/main/java/com/necessarysdk/reactnative/IntegrityProbeManager.kt +1157 -0
  13. package/android/src/main/java/com/necessarysdk/reactnative/KeyPresence.kt +62 -0
  14. package/android/src/main/java/com/necessarysdk/reactnative/NecessaryModules.kt +94 -0
  15. package/android/src/main/java/com/necessarysdk/reactnative/NecessaryNative.kt +338 -0
  16. package/android/src/main/java/com/necessarysdk/reactnative/NecessaryPackage.kt +38 -0
  17. package/android/src/main/java/com/necessarysdk/reactnative/ProbeReads.kt +333 -0
  18. package/android/src/main/java/com/necessarysdk/reactnative/ScreenGuard.kt +56 -0
  19. package/android/src/main/java/com/necessarysdk/reactnative/SecureStore.kt +127 -0
  20. package/android/src/main/java/com/necessarysdk/reactnative/StepUpKeyEntry.kt +57 -0
  21. package/android/src/main/java/com/necessarysdk/reactnative/StepUpKeyManager.kt +767 -0
  22. package/contract/report-1/android-8-ios-5/RULES.md +149 -0
  23. package/contract/report-1/android-8-ios-5/report.schema.json +2753 -0
  24. package/contract/report-1/android-8-ios-5/samples/android-8-complete.json +205 -0
  25. package/contract/report-1/android-8-ios-5/samples/android-8-incomplete.json +205 -0
  26. package/contract/report-1/android-8-ios-5/samples/android-8-invalid.json +205 -0
  27. package/contract/report-1/android-8-ios-5/samples/android-8-location-one-fix.json +228 -0
  28. package/contract/report-1/android-8-ios-5/samples/android-8-location-two-fixes.json +228 -0
  29. package/contract/report-1/android-8-ios-5/samples/android-8-refused.json +208 -0
  30. package/contract/report-1/android-8-ios-5/samples/ios-5-complete.json +106 -0
  31. package/contract/report-1/android-8-ios-5/samples/ios-5-incomplete.json +105 -0
  32. package/contract/report-1/android-8-ios-5/samples/ios-5-invalid.json +107 -0
  33. package/contract/report-1/android-8-ios-5/samples/ios-5-partial.json +110 -0
  34. package/contract/report-1/android-8-ios-5/samples/ios-5-refused.json +109 -0
  35. package/doc/integration_guide.md +771 -0
  36. package/doc/measurements.md +774 -0
  37. package/ios/InstallationKeyManager.swift +694 -0
  38. package/ios/IntegrityProbeManager.swift +959 -0
  39. package/ios/NecessaryModules.h +34 -0
  40. package/ios/NecessaryModules.mm +208 -0
  41. package/ios/NecessaryNative.swift +327 -0
  42. package/ios/PrivacyInfo.xcprivacy +14 -0
  43. package/ios/ProbeReads.swift +370 -0
  44. package/lib/client.d.ts +105 -0
  45. package/lib/client.js +176 -0
  46. package/lib/collector.d.ts +18 -0
  47. package/lib/collector.js +49 -0
  48. package/lib/deviceTrust.d.ts +253 -0
  49. package/lib/deviceTrust.js +699 -0
  50. package/lib/errors.d.ts +43 -0
  51. package/lib/errors.js +56 -0
  52. package/lib/index.d.ts +18 -0
  53. package/lib/index.js +34 -0
  54. package/lib/installationKey.d.ts +62 -0
  55. package/lib/installationKey.js +185 -0
  56. package/lib/installationStore.d.ts +49 -0
  57. package/lib/installationStore.js +127 -0
  58. package/lib/native/NativeNecessaryInstallationKey.d.ts +10 -0
  59. package/lib/native/NativeNecessaryInstallationKey.js +4 -0
  60. package/lib/native/NativeNecessaryIntegrity.d.ts +8 -0
  61. package/lib/native/NativeNecessaryIntegrity.js +4 -0
  62. package/lib/native/NativeNecessarySecureScreen.d.ts +7 -0
  63. package/lib/native/NativeNecessarySecureScreen.js +4 -0
  64. package/lib/native/NativeNecessaryStepUpKey.d.ts +9 -0
  65. package/lib/native/NativeNecessaryStepUpKey.js +4 -0
  66. package/lib/native/NativeNecessaryStorage.d.ts +14 -0
  67. package/lib/native/NativeNecessaryStorage.js +4 -0
  68. package/lib/protocol.d.ts +24 -0
  69. package/lib/protocol.js +50 -0
  70. package/lib/reinstallHint.d.ts +23 -0
  71. package/lib/reinstallHint.js +65 -0
  72. package/lib/report.d.ts +62 -0
  73. package/lib/report.js +134 -0
  74. package/lib/requestProof.d.ts +116 -0
  75. package/lib/requestProof.js +220 -0
  76. package/lib/secureScreen.d.ts +15 -0
  77. package/lib/secureScreen.js +32 -0
  78. package/lib/stepUpKey.d.ts +105 -0
  79. package/lib/stepUpKey.js +187 -0
  80. package/lib/transport.d.ts +39 -0
  81. package/lib/transport.js +117 -0
  82. package/lib/util/answers.d.ts +6 -0
  83. package/lib/util/answers.js +16 -0
  84. package/lib/util/bytes.d.ts +18 -0
  85. package/lib/util/bytes.js +207 -0
  86. package/lib/util/native.d.ts +20 -0
  87. package/lib/util/native.js +49 -0
  88. package/lib/util/release.d.ts +2 -0
  89. package/lib/util/release.js +7 -0
  90. package/lib/util/target.d.ts +18 -0
  91. package/lib/util/target.js +128 -0
  92. package/package.json +81 -4
  93. package/src/client.ts +221 -0
  94. package/src/collector.ts +64 -0
  95. package/src/deviceTrust.ts +851 -0
  96. package/src/errors.ts +71 -0
  97. package/src/index.ts +19 -0
  98. package/src/installationKey.ts +223 -0
  99. package/src/installationStore.ts +143 -0
  100. package/src/native/NativeNecessaryInstallationKey.ts +12 -0
  101. package/src/native/NativeNecessaryIntegrity.ts +15 -0
  102. package/src/native/NativeNecessarySecureScreen.ts +9 -0
  103. package/src/native/NativeNecessaryStepUpKey.ts +11 -0
  104. package/src/native/NativeNecessaryStorage.ts +16 -0
  105. package/src/protocol.ts +52 -0
  106. package/src/reinstallHint.ts +69 -0
  107. package/src/report.ts +168 -0
  108. package/src/requestProof.ts +295 -0
  109. package/src/secureScreen.ts +29 -0
  110. package/src/stepUpKey.ts +238 -0
  111. package/src/transport.ts +145 -0
  112. package/src/util/answers.ts +15 -0
  113. package/src/util/bytes.ts +194 -0
  114. package/src/util/native.ts +56 -0
  115. package/src/util/release.ts +6 -0
  116. package/src/util/target.ts +132 -0
  117. package/vectors/client_server.json +264 -0
package/src/errors.ts ADDED
@@ -0,0 +1,71 @@
1
+ /** The failures the SDK reports. */
2
+
3
+ /**
4
+ * A failure of the phone's keystore or of a prompt: [code] says which -- `KEY_UNAVAILABLE` (the
5
+ * keystore could not be reached for the installation key; no key was made over it: try again, or
6
+ * reset()), `STEPUP_KEY_INVALIDATED` (the key can never sign again: re-enrol),
7
+ * `STEPUP_KEY_UNAVAILABLE` (the keystore could not say whether the key is usable, or could not be
8
+ * reached; it was kept: try again, or re-enrol), `STEPUP_CANCELLED`, `STEPUP_REENROL_PENDING` and
9
+ * the like -- and [message] says it in words, followed by everything that failed beneath it
10
+ * (`[cause: ...]`) down to the keystore's own answer wherever the phone gives one.
11
+ */
12
+ export class NativeKeyException extends Error {
13
+ /** What failed, for the app to act on. */
14
+ readonly code: string;
15
+
16
+ constructor(code: string, message: string) {
17
+ super(message);
18
+ this.name = 'NativeKeyException';
19
+ this.code = code;
20
+ }
21
+
22
+ override toString(): string {
23
+ return `${this.message} [${this.code}]`;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * A refusal from your backend, or a request that did not reach it: the backend's
29
+ * `{"error": {"code", "message", "details"}}` (the guide, section 4), with the HTTP status.
30
+ */
31
+ export class ApiException extends Error {
32
+ /** The HTTP status, when the backend answered. */
33
+ readonly statusCode?: number;
34
+
35
+ /** The backend's `code`: what the app acts on. */
36
+ readonly code?: string;
37
+
38
+ /** The backend's `details`, such as `retry_after_seconds`. */
39
+ readonly details?: Record<string, unknown>;
40
+
41
+ constructor(
42
+ message: string,
43
+ options: { statusCode?: number; code?: string; details?: Record<string, unknown> } = {},
44
+ ) {
45
+ super(message);
46
+ this.name = 'ApiException';
47
+ this.statusCode = options.statusCode;
48
+ this.code = options.code;
49
+ this.details = options.details;
50
+ }
51
+
52
+ override toString(): string {
53
+ return this.message;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * A call that DeviceTrust.reset or DeviceTrust.close ended while it ran: what it found belongs to
59
+ * an installation that is gone, or to a client that is closed, so nothing of it was kept. After a
60
+ * reset, call again.
61
+ */
62
+ export class InterruptedCallException extends Error {
63
+ constructor(message: string) {
64
+ super(message);
65
+ this.name = 'InterruptedCallException';
66
+ }
67
+
68
+ override toString(): string {
69
+ return this.message;
70
+ }
71
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Device integrity for React Native apps: the client half of the public integration guide. A key
3
+ * the SDK never exports, signed measurements of the phone and the app, and request proofs; the
4
+ * app's backend decides what they mean.
5
+ */
6
+
7
+ export * from './client';
8
+ export * from './collector';
9
+ export * from './deviceTrust';
10
+ export * from './errors';
11
+ export * from './installationKey';
12
+ export * from './installationStore';
13
+ export * from './protocol';
14
+ export * from './reinstallHint';
15
+ export * from './report';
16
+ export * from './requestProof';
17
+ export * from './secureScreen';
18
+ export * from './stepUpKey';
19
+ export * from './transport';
@@ -0,0 +1,223 @@
1
+ /**
2
+ * The installation key: one non-exportable P-256 key per installation, made and held by the
3
+ * phone's keystore -- StrongBox or the trusted execution environment, the Secure Enclave, or
4
+ * where that refuses the Keychain.
5
+ */
6
+
7
+ import { NativeKeyException } from './errors';
8
+ import { sha256Hex } from './util/bytes';
9
+ import { installationKeyModule, isObject, nativeFailure } from './util/native';
10
+
11
+ /** A P-256 public key as a JWK: `kty`, `crv`, `alg`, `x`, `y`. */
12
+ export type PublicJwk = Record<string, unknown>;
13
+
14
+ /** The members of [jwk] in the order the phone writes them: kty, crv, alg, x, y, then any other. */
15
+ export function orderedJwk(jwk: Record<string, unknown>): PublicJwk {
16
+ const ordered: PublicJwk = {};
17
+ for (const name of ['kty', 'crv', 'alg', 'x', 'y']) {
18
+ if (name in jwk) {
19
+ ordered[name] = jwk[name];
20
+ }
21
+ }
22
+ for (const [name, value] of Object.entries(jwk)) {
23
+ if (!(name in ordered)) {
24
+ ordered[name] = value;
25
+ }
26
+ }
27
+ return ordered;
28
+ }
29
+
30
+ /** RFC 7638 thumbprint of an EC public JWK, in lowercase hex: the same form a backend derives. */
31
+ export function jwkThumbprint(jwk: PublicJwk): string {
32
+ // RFC 7638 canonical member order for an EC public JWK.
33
+ return sha256Hex(JSON.stringify({ crv: jwk['crv'], kty: 'EC', x: jwk['x'], y: jwk['y'] }));
34
+ }
35
+
36
+ export class NativeKeyMetadata {
37
+ readonly publicKey: PublicJwk;
38
+ readonly algorithm: string;
39
+ readonly keyAlias: string;
40
+ readonly provider: string;
41
+ readonly securityLevel: string;
42
+ readonly hardwareBacked: boolean;
43
+ readonly privateKeyExportable: boolean;
44
+ readonly signatureFormat: string;
45
+ readonly created: boolean;
46
+
47
+ constructor(value: {
48
+ publicKey: PublicJwk;
49
+ algorithm: string;
50
+ keyAlias: string;
51
+ provider: string;
52
+ securityLevel: string;
53
+ hardwareBacked: boolean;
54
+ privateKeyExportable: boolean;
55
+ signatureFormat: string;
56
+ created: boolean;
57
+ }) {
58
+ this.publicKey = value.publicKey;
59
+ this.algorithm = value.algorithm;
60
+ this.keyAlias = value.keyAlias;
61
+ this.provider = value.provider;
62
+ this.securityLevel = value.securityLevel;
63
+ this.hardwareBacked = value.hardwareBacked;
64
+ this.privateKeyExportable = value.privateKeyExportable;
65
+ this.signatureFormat = value.signatureFormat;
66
+ this.created = value.created;
67
+ }
68
+
69
+ static fromPlatform(value: Record<string, unknown>): NativeKeyMetadata {
70
+ const rawPublicKey = value['public_key'];
71
+ if (!isObject(rawPublicKey)) {
72
+ throw new TypeError('The native key provider returned no public key.');
73
+ }
74
+ const publicKey = orderedJwk(rawPublicKey);
75
+
76
+ const requiredString = (field: string): string => {
77
+ const item = value[field];
78
+ if (typeof item !== 'string' || item.length === 0) {
79
+ throw new TypeError(`The native key provider returned an invalid ${field}.`);
80
+ }
81
+ return item;
82
+ };
83
+ const requiredBool = (field: string): boolean => {
84
+ const item = value[field];
85
+ if (typeof item !== 'boolean') {
86
+ throw new TypeError(`The native key provider returned an invalid ${field}.`);
87
+ }
88
+ return item;
89
+ };
90
+
91
+ if (
92
+ publicKey['kty'] !== 'EC' ||
93
+ publicKey['crv'] !== 'P-256' ||
94
+ publicKey['alg'] !== 'ES256' ||
95
+ typeof publicKey['x'] !== 'string' ||
96
+ typeof publicKey['y'] !== 'string'
97
+ ) {
98
+ throw new TypeError('The native key provider did not return an EC P-256 public JWK.');
99
+ }
100
+
101
+ return new NativeKeyMetadata({
102
+ publicKey,
103
+ algorithm: requiredString('algorithm'),
104
+ keyAlias: requiredString('key_alias'),
105
+ provider: requiredString('provider'),
106
+ securityLevel: requiredString('security_level'),
107
+ hardwareBacked: requiredBool('hardware_backed'),
108
+ privateKeyExportable: requiredBool('private_key_exportable'),
109
+ signatureFormat: requiredString('signature_format'),
110
+ created: requiredBool('created'),
111
+ });
112
+ }
113
+ }
114
+
115
+ const KEY_MODULE_MISSING = new NativeKeyException(
116
+ 'NATIVE_KEY_PLUGIN_MISSING',
117
+ "The SDK's installation-key module is not linked into this app: rebuild the app.",
118
+ );
119
+
120
+ export class NativeInstallationKey {
121
+ async getOrCreateKey(): Promise<NativeKeyMetadata> {
122
+ const native = installationKeyModule();
123
+ if (native === null) {
124
+ throw KEY_MODULE_MISSING;
125
+ }
126
+ let value: unknown;
127
+ try {
128
+ value = await native.getOrCreateKey();
129
+ } catch (error) {
130
+ throw nativeFailure(error, 'The native key provider failed.');
131
+ }
132
+ if (!isObject(value)) {
133
+ throw new NativeKeyException('EMPTY_NATIVE_RESPONSE', 'The native key provider returned no key metadata.');
134
+ }
135
+ return NativeKeyMetadata.fromPlatform(value);
136
+ }
137
+
138
+ async signPayload(payloadBase64Url: string): Promise<string> {
139
+ const native = installationKeyModule();
140
+ if (native === null) {
141
+ throw KEY_MODULE_MISSING;
142
+ }
143
+ let signature: unknown;
144
+ try {
145
+ signature = await native.sign(payloadBase64Url);
146
+ } catch (error) {
147
+ throw nativeFailure(error, 'The native key provider could not sign the challenge.');
148
+ }
149
+ if (typeof signature !== 'string' || signature.length === 0) {
150
+ throw new NativeKeyException('EMPTY_NATIVE_SIGNATURE', 'The native key provider returned no signature.');
151
+ }
152
+ return signature;
153
+ }
154
+
155
+ async deleteKey(): Promise<void> {
156
+ const native = installationKeyModule();
157
+ if (native === null) {
158
+ throw KEY_MODULE_MISSING;
159
+ }
160
+ try {
161
+ await native.deleteKey();
162
+ } catch (error) {
163
+ throw nativeFailure(error, 'The native installation key could not be deleted.');
164
+ }
165
+ }
166
+ }
167
+
168
+ export class InstallationIdentity {
169
+ readonly installationId: string;
170
+ readonly nativeKey: NativeInstallationKey;
171
+ readonly metadata: NativeKeyMetadata;
172
+
173
+ constructor(value: { installationId: string; nativeKey: NativeInstallationKey; metadata: NativeKeyMetadata }) {
174
+ this.installationId = value.installationId;
175
+ this.nativeKey = value.nativeKey;
176
+ this.metadata = value.metadata;
177
+ }
178
+
179
+ get publicKeyJson(): PublicJwk {
180
+ return this.metadata.publicKey;
181
+ }
182
+ get algorithm(): string {
183
+ return this.metadata.algorithm;
184
+ }
185
+ get keyAlias(): string {
186
+ return this.metadata.keyAlias;
187
+ }
188
+ get provider(): string {
189
+ return this.metadata.provider;
190
+ }
191
+ get securityLevel(): string {
192
+ return this.metadata.securityLevel;
193
+ }
194
+ get hardwareBacked(): boolean {
195
+ return this.metadata.hardwareBacked;
196
+ }
197
+ get privateKeyExportable(): boolean {
198
+ return this.metadata.privateKeyExportable;
199
+ }
200
+ get signatureFormat(): string {
201
+ return this.metadata.signatureFormat;
202
+ }
203
+ get createdThisLaunch(): boolean {
204
+ return this.metadata.created;
205
+ }
206
+
207
+ /** The key's RFC 7638 thumbprint, in lowercase hex. */
208
+ get keyThumbprint(): string {
209
+ return jwkThumbprint(this.publicKeyJson);
210
+ }
211
+
212
+ signPayload(payloadBase64Url: string): Promise<string> {
213
+ return this.nativeKey.signPayload(payloadBase64Url);
214
+ }
215
+
216
+ withInstallationId(canonicalInstallationId: string): InstallationIdentity {
217
+ return new InstallationIdentity({
218
+ installationId: canonicalInstallationId,
219
+ nativeKey: this.nativeKey,
220
+ metadata: this.metadata,
221
+ });
222
+ }
223
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Where the SDK keeps the installation ID: in the platform's secure storage beside the key -- on
3
+ * iOS in the Keychain, on this device only, readable after the first unlock; elsewhere encrypted
4
+ * under a key the platform keystore holds.
5
+ */
6
+
7
+ import { NativeKeyException } from './errors';
8
+ import { nativeFailure, storageModule } from './util/native';
9
+ import { secureRandomBytes } from './requestProof';
10
+ import { InstallationIdentity, NativeInstallationKey } from './installationKey';
11
+
12
+ /** The secure storage the store writes to: replaced in tests. */
13
+ export interface SecureStorage {
14
+ read(key: string): Promise<string | null>;
15
+ write(key: string, value: string): Promise<void>;
16
+ delete(key: string): Promise<void>;
17
+ }
18
+
19
+ const STORAGE_MODULE_MISSING = (): NativeKeyException =>
20
+ new NativeKeyException(
21
+ 'NATIVE_STORAGE_PLUGIN_MISSING',
22
+ "The SDK's storage module is not linked into this app: rebuild the app.",
23
+ );
24
+
25
+ /** The phone's secure storage, through the SDK's native module. */
26
+ export class NativeSecureStorage implements SecureStorage {
27
+ async read(key: string): Promise<string | null> {
28
+ const native = storageModule();
29
+ if (native === null) throw STORAGE_MODULE_MISSING();
30
+ try {
31
+ const value = await native.read(key);
32
+ return typeof value === 'string' ? value : null;
33
+ } catch (error) {
34
+ throw nativeFailure(error, 'The secure storage could not be read.');
35
+ }
36
+ }
37
+
38
+ async write(key: string, value: string): Promise<void> {
39
+ const native = storageModule();
40
+ if (native === null) throw STORAGE_MODULE_MISSING();
41
+ try {
42
+ await native.write(key, value);
43
+ } catch (error) {
44
+ throw nativeFailure(error, 'The secure storage could not be written.');
45
+ }
46
+ }
47
+
48
+ async delete(key: string): Promise<void> {
49
+ const native = storageModule();
50
+ if (native === null) throw STORAGE_MODULE_MISSING();
51
+ try {
52
+ await native.remove(key);
53
+ } catch (error) {
54
+ throw nativeFailure(error, 'The secure storage could not be cleared.');
55
+ }
56
+ }
57
+ }
58
+
59
+ /** A random (version 4) UUID, from the platform's secure random source. */
60
+ export async function randomUuid(): Promise<string> {
61
+ const bytes = await secureRandomBytes(16);
62
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
63
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
64
+ const hexBytes = Array.from(bytes, (byte) => (byte < 16 ? '0' : '') + byte.toString(16));
65
+ return [
66
+ hexBytes.slice(0, 4).join(''),
67
+ hexBytes.slice(4, 6).join(''),
68
+ hexBytes.slice(6, 8).join(''),
69
+ hexBytes.slice(8, 10).join(''),
70
+ hexBytes.slice(10, 16).join(''),
71
+ ].join('-');
72
+ }
73
+
74
+ export class InstallationStore {
75
+ static readonly installationIdKey = 'recognition.installation_id.v2';
76
+ static readonly stepUpReenrolKey = 'recognition.stepup_reenrol_pending.v1';
77
+
78
+ readonly key: NativeInstallationKey;
79
+ protected readonly storage: SecureStorage;
80
+ private loading: Promise<InstallationIdentity> | null = null;
81
+
82
+ constructor(options: { key?: NativeInstallationKey; storage?: SecureStorage } = {}) {
83
+ this.key = options.key ?? new NativeInstallationKey();
84
+ this.storage = options.storage ?? new NativeSecureStorage();
85
+ }
86
+
87
+ /**
88
+ * The installation: its key, made on first use, and its ID. A key made now
89
+ * ([InstallationIdentity.createdThisLaunch]) is a new installation, so it gets a new ID even where
90
+ * one was stored: that ID belongs to the key that was lost, and a backend refuses a known ID under
91
+ * another key. Calls at once share one load, so that one key never gets two IDs.
92
+ */
93
+ loadOrCreate(): Promise<InstallationIdentity> {
94
+ const running = this.loading;
95
+ if (running !== null) {
96
+ return running;
97
+ }
98
+ const loading: Promise<InstallationIdentity> = this.load().finally(() => {
99
+ if (this.loading === loading) {
100
+ this.loading = null;
101
+ }
102
+ });
103
+ this.loading = loading;
104
+ return loading;
105
+ }
106
+
107
+ private async load(): Promise<InstallationIdentity> {
108
+ const stored = await this.storage.read(InstallationStore.installationIdKey);
109
+ const metadata = await this.key.getOrCreateKey();
110
+ let installationId = stored ?? '';
111
+ if (installationId.length === 0 || metadata.created) {
112
+ installationId = await randomUuid();
113
+ await this.saveInstallationId(installationId);
114
+ }
115
+ return new InstallationIdentity({ installationId, nativeKey: this.key, metadata });
116
+ }
117
+
118
+ /**
119
+ * Keeps the installation your backend answered for this key, which may not be the ID proposed.
120
+ * Only this opaque ID is stored here; the private key never leaves the platform's keystore.
121
+ */
122
+ saveInstallationId(installationId: string): Promise<void> {
123
+ return this.storage.write(InstallationStore.installationIdKey, installationId);
124
+ }
125
+
126
+ /** Whether a step-up key was replaced and the backend has not yet taken the new one (DeviceTrust.reenrolStepUp) -- kept across restarts. */
127
+ async stepUpReenrolPending(): Promise<boolean> {
128
+ return (await this.storage.read(InstallationStore.stepUpReenrolKey)) !== null;
129
+ }
130
+
131
+ setStepUpReenrolPending(pending: boolean): Promise<void> {
132
+ return pending
133
+ ? this.storage.write(InstallationStore.stepUpReenrolKey, '1')
134
+ : this.storage.delete(InstallationStore.stepUpReenrolKey);
135
+ }
136
+
137
+ /** Deletes the key and the installation ID: the next start is a new installation. */
138
+ async clear(): Promise<void> {
139
+ await this.key.deleteKey();
140
+ await this.storage.delete(InstallationStore.installationIdKey);
141
+ await this.storage.delete(InstallationStore.stepUpReenrolKey);
142
+ }
143
+ }
@@ -0,0 +1,12 @@
1
+ import type { TurboModule } from 'react-native';
2
+ import { TurboModuleRegistry } from 'react-native';
3
+
4
+ /** The installation key, in the phone's keystore: made on first use, never exported. */
5
+ export interface Spec extends TurboModule {
6
+ getOrCreateKey(): Promise<Object>;
7
+ sign(payload: string): Promise<string>;
8
+ deleteKey(): Promise<boolean>;
9
+ reinstallHintIdentifier(): Promise<string>;
10
+ }
11
+
12
+ export default TurboModuleRegistry.get<Spec>('NecessaryInstallationKey');
@@ -0,0 +1,15 @@
1
+ import type { TurboModule } from 'react-native';
2
+ import { TurboModuleRegistry } from 'react-native';
3
+
4
+ /** The collector: measures what a challenge asks for and the app enabled, and nothing else. */
5
+ export interface Spec extends TurboModule {
6
+ collect(
7
+ requiredProbes: string[],
8
+ challengeNonce: string,
9
+ optionalProbes: string[],
10
+ enabledMeasurements: string[],
11
+ ): Promise<Object>;
12
+ requestLocationPermission(): Promise<string>;
13
+ }
14
+
15
+ export default TurboModuleRegistry.get<Spec>('NecessaryIntegrity');
@@ -0,0 +1,9 @@
1
+ import type { TurboModule } from 'react-native';
2
+ import { TurboModuleRegistry } from 'react-native';
3
+
4
+ /** Protection of the app's window while a secret is typed. */
5
+ export interface Spec extends TurboModule {
6
+ protect(on: boolean): Promise<Object>;
7
+ }
8
+
9
+ export default TurboModuleRegistry.get<Spec>('NecessarySecureScreen');
@@ -0,0 +1,11 @@
1
+ import type { TurboModule } from 'react-native';
2
+ import { TurboModuleRegistry } from 'react-native';
3
+
4
+ /** The step-up key: a second key that the person must unlock with the screen lock. */
5
+ export interface Spec extends TurboModule {
6
+ getOrCreateKey(factor: string, mode: string, windowSeconds: number): Promise<Object>;
7
+ sign(payload: string, reason: string, mode: string): Promise<string>;
8
+ deleteKey(): Promise<boolean>;
9
+ }
10
+
11
+ export default TurboModuleRegistry.get<Spec>('NecessaryStepUpKey');
@@ -0,0 +1,16 @@
1
+ import type { TurboModule } from 'react-native';
2
+ import { TurboModuleRegistry } from 'react-native';
3
+
4
+ /**
5
+ * The phone's secure storage for the installation ID and the pending step-up re-enrolment (the
6
+ * Keychain, readable after the first unlock, on this device only; values encrypted under a key in
7
+ * the platform keystore), and the platform's secure random source.
8
+ */
9
+ export interface Spec extends TurboModule {
10
+ read(key: string): Promise<string | null>;
11
+ write(key: string, value: string): Promise<boolean>;
12
+ remove(key: string): Promise<boolean>;
13
+ randomBytes(count: number): Promise<string>;
14
+ }
15
+
16
+ export default TurboModuleRegistry.get<Spec>('NecessaryStorage');
@@ -0,0 +1,52 @@
1
+ /** What the client does about the protocol errors of the public guide's section 4. */
2
+
3
+ import { ApiException } from './errors';
4
+
5
+ /**
6
+ * What the client does about a refusal from your backend's API: the protocol errors of the
7
+ * integration guide's section 4, each acted on once. Any other refusal goes to the app as it came.
8
+ */
9
+ export enum ProtocolStep {
10
+ askAgain = 'askAgain',
11
+ proveAgain = 'proveAgain',
12
+ registerAgain = 'registerAgain',
13
+ waitThenRetry = 'waitThenRetry',
14
+ stop = 'stop',
15
+ }
16
+
17
+ /** The longest wait the client sits through before it retries a 429 itself; a longer one goes to the app. */
18
+ export const protocolMaxWaitSeconds = 10;
19
+
20
+ /**
21
+ * [deviceSession]: the request ran under the session from /v1/installations/verify, which the
22
+ * client can renew by proving its key again; an account session is the app's to renew.
23
+ */
24
+ export function protocolStepFor(error: ApiException, options: { deviceSession: boolean }): ProtocolStep {
25
+ switch (error.code) {
26
+ case 'challenge_expired':
27
+ case 'challenge_used':
28
+ case 'integrity_challenge_expired':
29
+ case 'integrity_challenge_replay':
30
+ return ProtocolStep.askAgain;
31
+ case 'expired_token':
32
+ return options.deviceSession ? ProtocolStep.proveAgain : ProtocolStep.stop;
33
+ case 'installation_not_found':
34
+ return ProtocolStep.registerAgain;
35
+ }
36
+ return protocolWait(error) === null ? ProtocolStep.stop : ProtocolStep.waitThenRetry;
37
+ }
38
+
39
+ /** How long a 429 asks the client to wait, in milliseconds, when that is short enough to wait. */
40
+ export function protocolWait(error: ApiException): number | null {
41
+ const seconds = error.details?.['retry_after_seconds'];
42
+ if (
43
+ error.statusCode !== 429 ||
44
+ typeof seconds !== 'number' ||
45
+ !Number.isFinite(seconds) ||
46
+ seconds < 0 ||
47
+ seconds > protocolMaxWaitSeconds
48
+ ) {
49
+ return null;
50
+ }
51
+ return Math.round(seconds * 1000);
52
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The reinstall hint: a SHA-256 of the platform's per-app device identifier, which the backend
3
+ * keys before storing. Client input, never proof.
4
+ */
5
+
6
+ import { sha256Hex } from './util/bytes';
7
+ import { currentPlatform, installationKeyModule } from './util/native';
8
+
9
+ export class ReinstallHint {
10
+ readonly kind: string;
11
+ readonly value: string;
12
+
13
+ constructor(value: { kind: string; value: string }) {
14
+ this.kind = value.kind;
15
+ this.value = value.value;
16
+ }
17
+
18
+ toJSON(): Record<string, unknown> {
19
+ return { kind: this.kind, value: this.value };
20
+ }
21
+ }
22
+
23
+ export class ReinstallHintReader {
24
+ private readonly platform: () => string;
25
+ private readonly identifier: () => Promise<string>;
26
+
27
+ /** [platform] and [identifier] replace the phone's in tests. */
28
+ constructor(options: { platform?: () => string; identifier?: () => Promise<string> } = {}) {
29
+ this.platform = options.platform ?? currentPlatform;
30
+ this.identifier =
31
+ options.identifier ??
32
+ (async () => {
33
+ const native = installationKeyModule();
34
+ return native === null ? '' : await native.reinstallHintIdentifier();
35
+ });
36
+ }
37
+
38
+ async read(): Promise<ReinstallHint | null> {
39
+ // The phone's per-app identifier: ANDROID_ID, or the identifier for vendor on iOS.
40
+ let kind: string;
41
+ let platform: string;
42
+ try {
43
+ platform = this.platform();
44
+ } catch {
45
+ return null;
46
+ }
47
+ switch (platform) {
48
+ case 'android':
49
+ kind = 'android_id';
50
+ break;
51
+ case 'ios':
52
+ kind = 'idfv';
53
+ break;
54
+ default:
55
+ return null;
56
+ }
57
+ try {
58
+ const value = ((await this.identifier()) ?? '').trim().toLowerCase();
59
+ if (value.length === 0) {
60
+ return null;
61
+ }
62
+ const digest = sha256Hex(`device-recognition-hint-v1|${kind}|${value}`);
63
+ return new ReinstallHint({ kind: `${kind}_sha256`, value: digest });
64
+ } catch {
65
+ // A missing hint must never stop cryptographic installation identity.
66
+ }
67
+ return null;
68
+ }
69
+ }