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