@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
@@ -0,0 +1,699 @@
1
+ "use strict";
2
+ /**
3
+ * DeviceTrust: the one object an app configures -- its backend, the personal measurements it
4
+ * enables, step-up and the app PIN -- and what it asks of it: start, scan, and sign a request (the
5
+ * public guide).
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.DeviceTrust = exports.ScanResult = exports.AppPinOptions = exports.personalMeasurements = exports.PersonalMeasurement = void 0;
9
+ const client_1 = require("./client");
10
+ const collector_1 = require("./collector");
11
+ const errors_1 = require("./errors");
12
+ const installationStore_1 = require("./installationStore");
13
+ const protocol_1 = require("./protocol");
14
+ const reinstallHint_1 = require("./reinstallHint");
15
+ const report_1 = require("./report");
16
+ const requestProof_1 = require("./requestProof");
17
+ const stepUpKey_1 = require("./stepUpKey");
18
+ const transport_1 = require("./transport");
19
+ const answers_1 = require("./util/answers");
20
+ /**
21
+ * The personal measurements an app may enable (the public guide, section 8). Each is collected
22
+ * only when the backend asks for it as well -- the location only with the person's permission too.
23
+ */
24
+ exports.PersonalMeasurement = {
25
+ localAddresses: 'local_addresses',
26
+ accessibility: 'accessibility',
27
+ notificationAccess: 'notification_access',
28
+ otherAccess: 'other_access',
29
+ screenCapture: 'screen_capture',
30
+ location: 'location',
31
+ mockLocation: 'mock_location',
32
+ };
33
+ /** Every personal measurement, in the guide's order. */
34
+ exports.personalMeasurements = Object.values(exports.PersonalMeasurement);
35
+ /** Where the backend takes the app PIN's two calls (the public guide, section 4). */
36
+ class AppPinOptions {
37
+ constructor(options = {}) {
38
+ this.path = options.path ?? client_1.DeviceTrustClient.appPinPath;
39
+ this.proofPath = options.proofPath ?? client_1.DeviceTrustClient.appPinProofPath;
40
+ }
41
+ }
42
+ exports.AppPinOptions = AppPinOptions;
43
+ /** One scan: the challenge it answered, the report as signed, and the backend's answer, whole. */
44
+ class ScanResult {
45
+ constructor(value) {
46
+ this.challenge = value.challenge;
47
+ this.report = value.report;
48
+ this.answer = value.answer;
49
+ }
50
+ /** The stored report (`integrity.report_id`), which a scan made for one request sends back with that request. */
51
+ get reportId() {
52
+ const integrity = this.answer['integrity'];
53
+ const id = typeof integrity === 'object' && integrity !== null ? integrity['report_id'] : null;
54
+ return typeof id === 'string' ? id : null;
55
+ }
56
+ }
57
+ exports.ScanResult = ScanResult;
58
+ function settled(promise) {
59
+ return promise.then(() => undefined, () => undefined);
60
+ }
61
+ function delay(milliseconds) {
62
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
63
+ }
64
+ function nowSeconds() {
65
+ return Math.floor(Date.now() / 1000);
66
+ }
67
+ class DeviceTrust {
68
+ constructor(options) {
69
+ this.identityValue = null;
70
+ this.session = null;
71
+ // One call of each kind in flight; a second waits for the first's result.
72
+ this.loading = null;
73
+ this.starting = null;
74
+ this.proving = null;
75
+ this.scanning = new Map();
76
+ // Which installation the calls running now belong to: reset() and close() move on to the next,
77
+ // and a call of an earlier one keeps nothing it finds.
78
+ this.generation = 0;
79
+ // A reset's deletion, which every later load waits for; and the installation ID being saved and a
80
+ // re-enrolment, which a reset waits for.
81
+ this.resetting = null;
82
+ this.saving = null;
83
+ this.reenrolling = null;
84
+ // The persistent writes and key changes running now (mutate): a reset waits for each before it
85
+ // deletes, so none outlives the deletion.
86
+ this.mutations = new Set();
87
+ this.offeredStepUpKey = null;
88
+ this.hint = null;
89
+ this.hintRead = false;
90
+ /** Why step-up is off on this phone although the app turned it on -- no screen lock, say -- or null. */
91
+ this.stepUpUnavailable = null;
92
+ const transport = options.transport ?? new transport_1.HttpTransport(options.baseUrl ?? '');
93
+ this.client = new client_1.DeviceTrustClient(transport, options.platform ? { platform: options.platform } : {});
94
+ this.personalMeasurements = new Set(options.personalMeasurements ?? []);
95
+ this.stepUp = options.stepUp ?? null;
96
+ this.appPin = options.appPin ?? null;
97
+ this.onRegistered = options.onRegistered ?? null;
98
+ this.store = options.store ?? new installationStore_1.InstallationStore();
99
+ this.collector = options.collector ?? new collector_1.NativeIntegrityCollector();
100
+ this.hintReader = options.hintReader ?? new reinstallHint_1.ReinstallHintReader();
101
+ this.stepUpKeyNative = options.stepUpKey ?? new stepUpKey_1.NativeStepUpKey();
102
+ }
103
+ /** The same over the app's own [transport]; [parts] replace the native ones in tests. */
104
+ static over(transport, parts = {}) {
105
+ return new DeviceTrust({ ...parts, transport });
106
+ }
107
+ get identity() {
108
+ return this.identityValue;
109
+ }
110
+ /** The installation session from proving the key, or null before [start]. */
111
+ get installationSession() {
112
+ return this.session;
113
+ }
114
+ /** The step-up key offered at the last registration. */
115
+ get stepUpKey() {
116
+ return this.offeredStepUpKey;
117
+ }
118
+ /** The wire names of [personalMeasurements], in the guide's order. */
119
+ get enabledMeasurements() {
120
+ return exports.personalMeasurements.filter((measurement) => this.personalMeasurements.has(measurement));
121
+ }
122
+ /**
123
+ * The installation: its key, made on first use, and its ID. A key made now is a new installation
124
+ * ([InstallationIdentity.createdThisLaunch]): whatever the app kept for the one before belongs to
125
+ * that one. Calls at once share one load, so one key gets one ID.
126
+ */
127
+ loadIdentity() {
128
+ const known = this.identityValue;
129
+ if (known !== null) {
130
+ return Promise.resolve(known);
131
+ }
132
+ const running = this.loading;
133
+ if (running !== null) {
134
+ return running;
135
+ }
136
+ const loading = this.load().finally(() => {
137
+ if (this.loading === loading) {
138
+ this.loading = null;
139
+ }
140
+ });
141
+ this.loading = loading;
142
+ return loading;
143
+ }
144
+ async load() {
145
+ // A reset's deletion first, so that nothing loads the key it deletes.
146
+ await this.resetting;
147
+ const generation = this.generation;
148
+ const loaded = await this.store.loadOrCreate();
149
+ this.current(generation);
150
+ if (this.identityValue === null) {
151
+ this.identityValue = loaded;
152
+ }
153
+ return this.identityValue;
154
+ }
155
+ /**
156
+ * A persistent write or a key change for the installation [generation]: refused before it starts
157
+ * when reset() or close() has ended that installation, and waited for by a reset that began while
158
+ * it ran. The order is the whole guarantee: endCalls() moves the generation on at once, in the
159
+ * same turn as reset() is called, so a write asked for after that throws here without writing;
160
+ * one asked for before is in [mutations], which the reset waits for before it deletes anything.
161
+ */
162
+ mutate(generation, change) {
163
+ this.current(generation);
164
+ const running = change();
165
+ const tracked = settled(running);
166
+ this.mutations.add(tracked);
167
+ void tracked.then(() => this.mutations.delete(tracked));
168
+ return running;
169
+ }
170
+ /** Throws when reset() or close() ended the installation [generation] belongs to: a call keeps nothing of one that is gone. */
171
+ current(generation) {
172
+ if (generation !== this.generation) {
173
+ throw new errors_1.InterruptedCallException('The installation was reset, or the client closed, while this call ran: nothing it found was kept.');
174
+ }
175
+ }
176
+ /**
177
+ * Loads or makes the key, registers it -- with the step-up key, when step-up is on -- and proves
178
+ * the installation holds it: the installation session. A refusal outside the guide's protocol
179
+ * errors goes to the app as it came. A second call while one runs gets its result.
180
+ */
181
+ start() {
182
+ const running = this.starting;
183
+ if (running !== null) {
184
+ return running;
185
+ }
186
+ const starting = this.startOnce().finally(() => {
187
+ if (this.starting === starting) {
188
+ this.starting = null;
189
+ }
190
+ });
191
+ this.starting = starting;
192
+ return starting;
193
+ }
194
+ async startOnce() {
195
+ const generation = this.generation;
196
+ await this.loadIdentity();
197
+ await this.register(generation);
198
+ await this.prove();
199
+ this.current(generation);
200
+ return this.identityValue;
201
+ }
202
+ /** A new installation session: the key's challenge, answered. A second call while one runs gets its result. */
203
+ prove() {
204
+ const running = this.proving;
205
+ if (running !== null) {
206
+ return running;
207
+ }
208
+ const proving = this.proveOnce(this.generation, false).finally(() => {
209
+ if (this.proving === proving) {
210
+ this.proving = null;
211
+ }
212
+ });
213
+ this.proving = proving;
214
+ return proving;
215
+ }
216
+ /**
217
+ * Deletes the key, the installation ID (InstallationStore.clear) and the step-up key, and forgets
218
+ * the session: the next start is a new installation, linked to this one by nothing it holds.
219
+ * Calls still running end with InterruptedCallException and keep nothing; calls made from now on
220
+ * wait for the deletion, and never join one made before.
221
+ */
222
+ reset() {
223
+ const loading = this.loading;
224
+ const saving = this.saving;
225
+ const reenrolling = this.reenrolling;
226
+ const earlier = this.resetting;
227
+ const changing = Array.from(this.mutations);
228
+ this.endCalls();
229
+ this.offeredStepUpKey = null;
230
+ this.hint = null;
231
+ this.hintRead = false;
232
+ const deleting = (async () => {
233
+ await earlier;
234
+ // A load or a save still running finishes first, so that nothing it writes outlives the
235
+ // deletion. So does a re-enrolment: its key, which a backend may already have taken, is
236
+ // deleted after it, and the marker it writes is cleared. So does every write and key change
237
+ // still running (mutate): the step-up marker, a step-up key being made.
238
+ await Promise.all([
239
+ ...(loading !== null ? [settled(loading)] : []),
240
+ ...(saving !== null ? [settled(saving)] : []),
241
+ ...(reenrolling !== null ? [reenrolling] : []),
242
+ ...changing,
243
+ ]);
244
+ await this.store.clear();
245
+ // The step-up key goes with the installation, as on a reinstall: kept, it would link the next
246
+ // installation to this one. None to delete is no failure; nor is a phone or a build with no
247
+ // step-up key at all. A key that stays is: reset() fails with it, the installation itself
248
+ // gone, so the app knows and may call reset() again.
249
+ try {
250
+ await this.stepUpKeyNative.deleteKey();
251
+ }
252
+ catch (error) {
253
+ if (!(error instanceof errors_1.NativeKeyException)) {
254
+ throw error;
255
+ }
256
+ if (error.code !== 'NATIVE_STEPUP_PLUGIN_MISSING' && error.code !== 'UNSUPPORTED_ANDROID_VERSION') {
257
+ throw new errors_1.NativeKeyException(error.code, `The installation was reset, but its step-up key could not be deleted: ${error.message} Call reset() again.`);
258
+ }
259
+ }
260
+ })();
261
+ // Later loads wait for the deletion, whatever came of it; the caller of reset hears what did.
262
+ this.resetting = settled(deleting);
263
+ return deleting;
264
+ }
265
+ /** Moves on to the next installation: the calls running now keep nothing, and the next ones start afresh. */
266
+ endCalls() {
267
+ this.generation++;
268
+ this.identityValue = null;
269
+ this.session = null;
270
+ this.loading = null;
271
+ this.starting = null;
272
+ this.proving = null;
273
+ this.scanning.clear();
274
+ }
275
+ async proveOnce(generation, retried) {
276
+ const identity = await this.loadIdentity();
277
+ this.current(generation);
278
+ try {
279
+ const challenge = await this.client.createInstallationChallenge(identity.installationId);
280
+ const payload = (0, answers_1.answerText)(challenge, 'payload');
281
+ const verified = await this.client.verifyInstallation({
282
+ installationId: identity.installationId,
283
+ challengeId: (0, answers_1.answerText)(challenge, 'challenge_id'),
284
+ payload,
285
+ signature: await identity.signPayload(payload),
286
+ });
287
+ this.current(generation);
288
+ this.session = (0, answers_1.answerText)(verified, 'device_token');
289
+ return this.session;
290
+ }
291
+ catch (error) {
292
+ if (!(error instanceof errors_1.ApiException) || retried) {
293
+ throw error;
294
+ }
295
+ switch ((0, protocol_1.protocolStepFor)(error, { deviceSession: true })) {
296
+ case protocol_1.ProtocolStep.askAgain:
297
+ break;
298
+ case protocol_1.ProtocolStep.registerAgain:
299
+ await this.register(generation);
300
+ break;
301
+ case protocol_1.ProtocolStep.waitThenRetry:
302
+ await delay((0, protocol_1.protocolWait)(error));
303
+ break;
304
+ case protocol_1.ProtocolStep.proveAgain:
305
+ case protocol_1.ProtocolStep.stop:
306
+ throw error;
307
+ }
308
+ return this.proveOnce(generation, true);
309
+ }
310
+ }
311
+ async register(generation) {
312
+ const identity = await this.loadIdentity();
313
+ this.current(generation);
314
+ if (!this.hintRead) {
315
+ const hint = await this.hintReader.read();
316
+ this.current(generation);
317
+ this.hint = hint;
318
+ this.hintRead = true;
319
+ }
320
+ const offered = await this.prepareStepUpKey(generation);
321
+ const answer = await this.client.registerInstallation(identity, this.hint, { stepUpKey: offered });
322
+ this.current(generation);
323
+ // The key is the installation's identity: a backend that holds it under another ID answers with
324
+ // that one, which is kept.
325
+ const held = answer['installation_id'];
326
+ if (typeof held === 'string' && held.length > 0 && held !== identity.installationId) {
327
+ this.identityValue = identity.withInstallationId(held);
328
+ const saving = this.store.saveInstallationId(held);
329
+ this.saving = saving;
330
+ await saving;
331
+ }
332
+ await this.settleStepUpKey(answer, offered, generation);
333
+ await this.onRegistered?.(answer);
334
+ }
335
+ /**
336
+ * Whether the backend holds the step-up key offered: it takes one only when it first registers an
337
+ * installation, so a key made later -- after the screen lock was removed and set again, say -- it
338
+ * takes by re-enrolment alone. Its answer says so (`stepup_key_matches`, or for an installation
339
+ * new to it `stepup_key_registered`); until it holds this key, approvals wait for reenrolStepUp,
340
+ * across restarts too. A registration without a key, the phone unable to make one, leaves any key
341
+ * made later waiting the same way. A backend whose answer says nothing is taken at its word unless
342
+ * a re-enrolment is pending.
343
+ */
344
+ async settleStepUpKey(answer, offered, generation) {
345
+ if (this.stepUp === null) {
346
+ return;
347
+ }
348
+ const matches = answer['stepup_key_matches'];
349
+ const registered = answer['stepup_key_registered'];
350
+ const bound = offered === null
351
+ ? null
352
+ : typeof matches === 'boolean'
353
+ ? matches
354
+ : typeof registered === 'boolean'
355
+ ? registered
356
+ : null;
357
+ if (offered === null || bound === false) {
358
+ await this.mutate(generation, () => this.store.setStepUpReenrolPending(true));
359
+ }
360
+ else if (bound === true) {
361
+ await this.mutate(generation, () => this.store.setStepUpReenrolPending(false));
362
+ }
363
+ const pending = offered !== null && bound !== true && (await this.store.stepUpReenrolPending());
364
+ this.current(generation);
365
+ if (pending) {
366
+ this.offeredStepUpKey = null;
367
+ this.stepUpUnavailable = DeviceTrust.reenrolPending();
368
+ }
369
+ }
370
+ /**
371
+ * The step-up key as [stepUp] asks: an existing key is kept as it is. Step-up is optional, so a
372
+ * key the phone cannot make leaves it off. Its use waits for the backend's answer
373
+ * (settleStepUpKey).
374
+ */
375
+ async prepareStepUpKey(generation) {
376
+ const options = this.stepUp;
377
+ if (options === null) {
378
+ this.stepUpUnavailable = null;
379
+ this.offeredStepUpKey = null;
380
+ return null;
381
+ }
382
+ this.stepUpKeyNative.configuredMode = options.mode;
383
+ let key = null;
384
+ let unavailable = null;
385
+ try {
386
+ key = await this.mutate(generation, () => this.stepUpKeyNative.getOrCreateKey({
387
+ factor: options.factor,
388
+ mode: options.mode,
389
+ windowSeconds: options.windowSeconds,
390
+ }));
391
+ }
392
+ catch (error) {
393
+ if (!(error instanceof errors_1.NativeKeyException)) {
394
+ throw error;
395
+ }
396
+ unavailable = error;
397
+ }
398
+ this.current(generation);
399
+ this.stepUpUnavailable = unavailable;
400
+ this.offeredStepUpKey = key;
401
+ return key;
402
+ }
403
+ /** A step-up key a re-enrolment replaces: one that can never sign again, or one the keystore cannot vouch for. */
404
+ static replaceableKey(error) {
405
+ return error.code === 'STEPUP_KEY_INVALIDATED' || error.code === 'STEPUP_KEY_UNAVAILABLE';
406
+ }
407
+ /** What approvals answer while the backend does not hold the step-up key. */
408
+ static reenrolPending() {
409
+ return new errors_1.NativeKeyException('STEPUP_REENROL_PENDING', 'The backend does not hold this step-up key -- a re-enrolment unfinished, or a key made after the ' +
410
+ 'installation was registered: reenrolStepUp() first.');
411
+ }
412
+ /**
413
+ * A scan: under the installation session -- or, for a scan that approves one [operation]
414
+ * ([describeOperation]), under the app's account [session] for this installation, the one that
415
+ * will send that request. The guide's protocol errors are acted on once; an account session is
416
+ * the app's to renew. A second call for the same scan while one runs gets its result; a scan
417
+ * whose answer was lost is simply asked for again.
418
+ */
419
+ scan(options = {}) {
420
+ const session = options.session ?? null;
421
+ const operation = options.operation ?? null;
422
+ if (operation !== null && session === null) {
423
+ throw new TypeError('session: a scan for one operation is asked under the account session that will send it');
424
+ }
425
+ // The personal measurements enabled now are part of the scan: one asked for after the person
426
+ // changed them never joins one asked for before.
427
+ const enabled = this.enabledMeasurements;
428
+ const kind = JSON.stringify([session, operation, enabled]);
429
+ const running = this.scanning.get(kind);
430
+ if (running !== undefined) {
431
+ return running;
432
+ }
433
+ const scanning = this.scanWithProtocol(this.generation, session, operation, enabled).finally(() => {
434
+ if (this.scanning.get(kind) === scanning) {
435
+ this.scanning.delete(kind);
436
+ }
437
+ });
438
+ this.scanning.set(kind, scanning);
439
+ return scanning;
440
+ }
441
+ async scanWithProtocol(generation, session, operation, enabled) {
442
+ try {
443
+ return await this.scanOnce(generation, session, operation, enabled);
444
+ }
445
+ catch (error) {
446
+ if (!(error instanceof errors_1.ApiException)) {
447
+ throw error;
448
+ }
449
+ const installationSession = session === null;
450
+ switch ((0, protocol_1.protocolStepFor)(error, { deviceSession: installationSession })) {
451
+ case protocol_1.ProtocolStep.askAgain:
452
+ break;
453
+ case protocol_1.ProtocolStep.proveAgain:
454
+ await this.prove();
455
+ break;
456
+ case protocol_1.ProtocolStep.registerAgain:
457
+ // An account session belongs to the installation the backend lost: the app signs in again.
458
+ if (!installationSession) {
459
+ throw error;
460
+ }
461
+ await this.register(generation);
462
+ await this.prove();
463
+ break;
464
+ case protocol_1.ProtocolStep.waitThenRetry:
465
+ await delay((0, protocol_1.protocolWait)(error));
466
+ break;
467
+ case protocol_1.ProtocolStep.stop:
468
+ throw error;
469
+ }
470
+ return this.scanOnce(generation, session, operation, enabled);
471
+ }
472
+ }
473
+ async scanOnce(generation, session, operation, enabled) {
474
+ const identity = await this.loadIdentity();
475
+ const bearer = session ?? this.session ?? (await this.prove());
476
+ this.current(generation);
477
+ const challenge = await this.client.integrityChallenge({ session: bearer, identity, operation });
478
+ const report = await (0, report_1.collectReport)({
479
+ challenge,
480
+ identity,
481
+ collector: this.collector,
482
+ enabledMeasurements: enabled,
483
+ });
484
+ // A report of an installation reset meanwhile is never sent.
485
+ this.current(generation);
486
+ const answer = await this.client.submitIntegrityReport({ session: bearer, identity, report });
487
+ this.current(generation);
488
+ return new ScanResult({ challenge, report, answer });
489
+ }
490
+ /**
491
+ * Signs one request to your backend under [bearer], your account session: [body] is sent exactly
492
+ * as given. With [stepUp] the person approves it with the step-up key -- the phone asks, saying
493
+ * [reason]; with [appPin] the backend checks the PIN for it first; [reportId] names the scan made
494
+ * for it alone. Send it with RequestProof.headers beside `Authorization: Bearer`, or with [send];
495
+ * each proof serves this request once.
496
+ */
497
+ async signRequest(args) {
498
+ const generation = this.generation;
499
+ const identity = await this.loadIdentity();
500
+ this.current(generation);
501
+ let approval = null;
502
+ if (args.stepUp === true) {
503
+ // Only the key the backend received at registration: a key made later would sign approvals
504
+ // the backend cannot check.
505
+ const key = this.offeredStepUpKey;
506
+ if (key === null) {
507
+ throw (this.stepUpUnavailable ??
508
+ new errors_1.NativeKeyException('STEPUP_OFF', this.stepUp === null
509
+ ? 'Step-up is off: new DeviceTrust({ stepUp: new StepUpOptions(), ... }).'
510
+ : 'No step-up key was registered: start() first.'));
511
+ }
512
+ const reason = args.reason ?? 'Approve this request';
513
+ approval = {
514
+ factor: key.factor,
515
+ sign: (payload) => this.stepUpKeyNative.sign(payload, { reason }),
516
+ };
517
+ }
518
+ let proof = await (0, requestProof_1.signRequest)({
519
+ identity,
520
+ method: args.method,
521
+ path: args.path,
522
+ bearerToken: args.bearer,
523
+ encodedBody: args.body ?? '',
524
+ stepUp: approval,
525
+ });
526
+ // The installation reset, or the client closed, while the phone signed: the proof is of an
527
+ // installation that is gone, and is never handed out.
528
+ this.current(generation);
529
+ if (args.appPin != null) {
530
+ const options = this.appPin;
531
+ if (options === null) {
532
+ throw new Error('The app PIN is off: new DeviceTrust({ appPin: new AppPinOptions(), ... }).');
533
+ }
534
+ proof = await this.client.proveAppPin({
535
+ session: args.bearer,
536
+ identity,
537
+ pin: args.appPin,
538
+ request: proof,
539
+ path: options.proofPath,
540
+ });
541
+ this.current(generation);
542
+ }
543
+ return args.reportId == null ? proof : proof.withIntegrityReport(args.reportId);
544
+ }
545
+ /**
546
+ * Re-enrols the step-up key under the account [session], with the person's own
547
+ * [reauthentication] -- the account's password, say: `{password: ...}`. For a key the phone has
548
+ * lost or can no longer use -- the screen lock removed (`STEPUP_KEY_INVALIDATED`) -- or one made
549
+ * for other options: the person unlocks the new key to sign its proof, the phone showing
550
+ * [reason], and the backend takes it for that account alone (the public guide, section 3).
551
+ * Approvals use it from then on. A key replaced before the backend took the new one -- the person
552
+ * cancelled, the password was wrong, the answer was lost -- leaves step-up waiting: approvals
553
+ * answer `STEPUP_REENROL_PENDING`, across restarts too, until a call of this succeeds.
554
+ */
555
+ reenrolStepUp(args) {
556
+ const running = this.reenrolStepUpOnce(args.session, args.reauthentication ?? {}, args.reason ?? 'Confirm your new step-up key');
557
+ // reset() waits for it, whatever comes of it.
558
+ this.reenrolling = settled(running);
559
+ return running;
560
+ }
561
+ async reenrolStepUpOnce(session, reauthentication, reason) {
562
+ const options = this.stepUp;
563
+ if (options === null) {
564
+ throw new errors_1.NativeKeyException('STEPUP_OFF', 'Step-up is off: new DeviceTrust({ stepUp: new StepUpOptions(), ... }).');
565
+ }
566
+ const generation = this.generation;
567
+ const identity = await this.loadIdentity();
568
+ this.current(generation);
569
+ this.stepUpKeyNative.configuredMode = options.mode;
570
+ // From here the phone's key and the backend's differ until the backend takes the new one:
571
+ // approvals wait for that.
572
+ const holdApprovals = async () => {
573
+ await this.mutate(generation, () => this.store.setStepUpReenrolPending(true));
574
+ this.current(generation);
575
+ this.offeredStepUpKey = null;
576
+ this.stepUpUnavailable = DeviceTrust.reenrolPending();
577
+ };
578
+ const newKey = async (replace) => {
579
+ if (replace) {
580
+ await holdApprovals();
581
+ await this.mutate(generation, () => this.stepUpKeyNative.deleteKey());
582
+ }
583
+ const made = await this.mutate(generation, () => this.stepUpKeyNative.getOrCreateKey({
584
+ factor: options.factor,
585
+ mode: options.mode,
586
+ windowSeconds: options.windowSeconds,
587
+ }));
588
+ // None to replace, the phone having lost it: this one is as new.
589
+ if (made.created && !replace) {
590
+ await holdApprovals();
591
+ }
592
+ return made;
593
+ };
594
+ // A re-enrolment is the explicit recovery: a key the keystore cannot vouch for
595
+ // (STEPUP_KEY_UNAVAILABLE), or one that can never sign again, is replaced here -- and only here.
596
+ let key;
597
+ try {
598
+ key = await newKey(false);
599
+ }
600
+ catch (error) {
601
+ if (!(error instanceof errors_1.NativeKeyException) || !DeviceTrust.replaceableKey(error)) {
602
+ throw error;
603
+ }
604
+ key = await newKey(true);
605
+ }
606
+ if (!key.created && !options.accepts(key)) {
607
+ key = await newKey(true);
608
+ }
609
+ // The new key signs a proof bound to this request's own nonce, which the request proof then
610
+ // signs too.
611
+ const nonce = await (0, requestProof_1.freshNonce)();
612
+ const proveWith = async (current) => {
613
+ const payload = (0, stepUpKey_1.stepUpReenrolProof)({
614
+ installationId: identity.installationId,
615
+ stepUpKeyThumbprint: current.thumbprint,
616
+ nonce: (0, requestProof_1.base64UrlNoPadding)(nonce),
617
+ timestamp: nowSeconds(),
618
+ });
619
+ return {
620
+ stepup_key_proof: payload,
621
+ stepup_key_signature: await this.stepUpKeyNative.sign(payload, { reason }),
622
+ };
623
+ };
624
+ let keyProof;
625
+ try {
626
+ keyProof = await proveWith(key);
627
+ }
628
+ catch (error) {
629
+ // A key that died with the screen lock is found only when it signs.
630
+ if (!(error instanceof errors_1.NativeKeyException) ||
631
+ (!DeviceTrust.replaceableKey(error) && error.code !== 'STEPUP_KEY_NOT_FOUND')) {
632
+ throw error;
633
+ }
634
+ key = await newKey(true);
635
+ keyProof = await proveWith(key);
636
+ }
637
+ const proof = await (0, requestProof_1.signRequest)({
638
+ identity,
639
+ method: 'POST',
640
+ path: options.reenrolPath,
641
+ bearerToken: session,
642
+ encodedBody: JSON.stringify({
643
+ ...reauthentication,
644
+ stepup_public_key: key.publicKey,
645
+ stepup_key_auth: key.authJson,
646
+ ...keyProof,
647
+ }),
648
+ nonce,
649
+ });
650
+ const answer = await this.send(proof);
651
+ await this.mutate(generation, () => this.store.setStepUpReenrolPending(false));
652
+ this.current(generation);
653
+ this.offeredStepUpKey = key;
654
+ this.stepUpUnavailable = null;
655
+ return answer;
656
+ }
657
+ /** Sends [proof]'s request through the SDK's transport. */
658
+ send(proof) {
659
+ return this.client.sendSigned(proof);
660
+ }
661
+ /**
662
+ * Sets the account's app PIN under the account [session], with the person's own
663
+ * [reauthentication] -- the account's password, say: `{password: ...}`.
664
+ */
665
+ async setAppPin(args) {
666
+ const options = this.appPin;
667
+ if (options === null) {
668
+ throw new Error('The app PIN is off: new DeviceTrust({ appPin: new AppPinOptions(), ... }).');
669
+ }
670
+ const generation = this.generation;
671
+ const identity = await this.loadIdentity();
672
+ this.current(generation);
673
+ const answer = await this.client.setAppPin({
674
+ session: args.session,
675
+ identity,
676
+ pin: args.pin,
677
+ reauthentication: args.reauthentication ?? {},
678
+ path: options.path,
679
+ });
680
+ this.current(generation);
681
+ return answer;
682
+ }
683
+ /**
684
+ * Asks the person for the location permission, for the location measurements: from a button on
685
+ * the app's own screen, since a scan never asks. Answers `precise`, `approximate` or `none`.
686
+ */
687
+ requestLocationPermission() {
688
+ return this.collector.requestLocationPermission();
689
+ }
690
+ /**
691
+ * Closes the transport. Calls still running end with InterruptedCallException, or with the
692
+ * transport's own failure, and keep nothing; the key and the installation stay as they are.
693
+ */
694
+ close() {
695
+ this.endCalls();
696
+ this.client.close();
697
+ }
698
+ }
699
+ exports.DeviceTrust = DeviceTrust;