@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.
Files changed (114) hide show
  1. package/CHANGELOG.md +22 -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 +1152 -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 +323 -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 +314 -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 +762 -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 +960 -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 +353 -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 +690 -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 +140 -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/bytes.d.ts +18 -0
  83. package/lib/util/bytes.js +207 -0
  84. package/lib/util/native.d.ts +20 -0
  85. package/lib/util/native.js +49 -0
  86. package/lib/util/release.d.ts +2 -0
  87. package/lib/util/release.js +7 -0
  88. package/lib/util/target.d.ts +18 -0
  89. package/lib/util/target.js +128 -0
  90. package/package.json +81 -4
  91. package/src/client.ts +221 -0
  92. package/src/collector.ts +64 -0
  93. package/src/deviceTrust.ts +842 -0
  94. package/src/errors.ts +71 -0
  95. package/src/index.ts +19 -0
  96. package/src/installationKey.ts +223 -0
  97. package/src/installationStore.ts +143 -0
  98. package/src/native/NativeNecessaryInstallationKey.ts +12 -0
  99. package/src/native/NativeNecessaryIntegrity.ts +15 -0
  100. package/src/native/NativeNecessarySecureScreen.ts +9 -0
  101. package/src/native/NativeNecessaryStepUpKey.ts +11 -0
  102. package/src/native/NativeNecessaryStorage.ts +16 -0
  103. package/src/protocol.ts +52 -0
  104. package/src/reinstallHint.ts +69 -0
  105. package/src/report.ts +174 -0
  106. package/src/requestProof.ts +295 -0
  107. package/src/secureScreen.ts +29 -0
  108. package/src/stepUpKey.ts +238 -0
  109. package/src/transport.ts +145 -0
  110. package/src/util/bytes.ts +194 -0
  111. package/src/util/native.ts +56 -0
  112. package/src/util/release.ts +6 -0
  113. package/src/util/target.ts +132 -0
  114. package/vectors/client_server.json +264 -0
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+ /**
3
+ * Request proofs (the public guide, section 3): the installation key signs each request, so a
4
+ * copied session token is useless without the key; with step-up on the person approves one
5
+ * request with the step-up key, and with the app PIN on a request carries the backend's proof of
6
+ * the PIN. Also the description of the one request a scan is to approve (section 2).
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.RequestProof = exports.secureRandomBytes = exports.base64UrlNoPadding = void 0;
10
+ exports.setSecureRandomSource = setSecureRandomSource;
11
+ exports.freshNonce = freshNonce;
12
+ exports.appPinProofRequest = appPinProofRequest;
13
+ exports.stepUpProof = stepUpProof;
14
+ exports.signRequest = signRequest;
15
+ exports.describeOperation = describeOperation;
16
+ const errors_1 = require("./errors");
17
+ const bytes_1 = require("./util/bytes");
18
+ const native_1 = require("./util/native");
19
+ const target_1 = require("./util/target");
20
+ var bytes_2 = require("./util/bytes");
21
+ Object.defineProperty(exports, "base64UrlNoPadding", { enumerable: true, get: function () { return bytes_2.base64UrlNoPadding; } });
22
+ /** Where [freshNonce] takes its bytes: the platform's secure random source, replaced in tests. */
23
+ let secureRandomBytes = async (count) => {
24
+ const native = (0, native_1.storageModule)();
25
+ if (native === null) {
26
+ throw new errors_1.NativeKeyException('NATIVE_STORAGE_PLUGIN_MISSING', "The SDK's storage module is not linked into this app: rebuild the app.");
27
+ }
28
+ let encoded;
29
+ try {
30
+ encoded = await native.randomBytes(count);
31
+ }
32
+ catch (error) {
33
+ throw (0, native_1.nativeFailure)(error, 'The platform gave no random bytes.');
34
+ }
35
+ const bytes = (0, bytes_1.base64UrlDecode)(encoded);
36
+ if (bytes.length !== count) {
37
+ throw new errors_1.NativeKeyException('RANDOM_BYTES_SHORT', `The platform gave ${bytes.length} random bytes of ${count}.`);
38
+ }
39
+ return bytes;
40
+ };
41
+ exports.secureRandomBytes = secureRandomBytes;
42
+ /** Replaces the secure random source -- for tests, which run where no phone is. */
43
+ function setSecureRandomSource(source) {
44
+ exports.secureRandomBytes = source;
45
+ }
46
+ /** 32 random bytes from the platform's secure source. */
47
+ function freshNonce() {
48
+ return (0, exports.secureRandomBytes)(32);
49
+ }
50
+ function nowSeconds() {
51
+ return Math.floor(Date.now() / 1000);
52
+ }
53
+ /**
54
+ * What the installation key signed for one request -- with step-up, what the step-up key signed;
55
+ * with the app PIN, the backend's proof of it; for a request a scan approves, that scan's report --
56
+ * and the headers that carry them beside `Authorization: Bearer`.
57
+ */
58
+ class RequestProof {
59
+ constructor(fields) {
60
+ this.bearerToken = fields.bearerToken;
61
+ this.method = fields.method;
62
+ this.path = fields.path;
63
+ this.encodedBody = fields.encodedBody;
64
+ this.payload = fields.payload;
65
+ this.signature = fields.signature;
66
+ this.timestamp = fields.timestamp;
67
+ this.nonce = fields.nonce;
68
+ this.stepUpPayload = fields.stepUpPayload ?? null;
69
+ this.stepUpSignature = fields.stepUpSignature ?? null;
70
+ this.appPinProof = fields.appPinProof ?? null;
71
+ this.integrityReport = fields.integrityReport ?? null;
72
+ }
73
+ get headers() {
74
+ const headers = {
75
+ 'X-Access-Proof': this.payload,
76
+ 'X-Access-Signature': this.signature,
77
+ };
78
+ if (this.stepUpPayload !== null)
79
+ headers['X-Step-Up-Proof'] = this.stepUpPayload;
80
+ if (this.stepUpSignature !== null)
81
+ headers['X-Step-Up-Signature'] = this.stepUpSignature;
82
+ if (this.appPinProof !== null)
83
+ headers['X-App-PIN-Proof'] = this.appPinProof;
84
+ if (this.integrityReport !== null)
85
+ headers['X-Integrity-Report'] = this.integrityReport;
86
+ return headers;
87
+ }
88
+ /** This request, unlocked by the backend's [proof] of the app PIN. */
89
+ withAppPinProof(proof) {
90
+ return this.with(proof, this.integrityReport);
91
+ }
92
+ /** This request, naming [reportId], the scan made for it alone. */
93
+ withIntegrityReport(reportId) {
94
+ return this.with(this.appPinProof, reportId);
95
+ }
96
+ with(appPinProof, integrityReport) {
97
+ return new RequestProof({
98
+ bearerToken: this.bearerToken,
99
+ method: this.method,
100
+ path: this.path,
101
+ encodedBody: this.encodedBody,
102
+ payload: this.payload,
103
+ signature: this.signature,
104
+ timestamp: this.timestamp,
105
+ nonce: this.nonce,
106
+ stepUpPayload: this.stepUpPayload,
107
+ stepUpSignature: this.stepUpSignature,
108
+ appPinProof,
109
+ integrityReport,
110
+ });
111
+ }
112
+ }
113
+ exports.RequestProof = RequestProof;
114
+ /**
115
+ * What the app sends to have its backend check the app PIN for one request: the PIN, and that
116
+ * request as its request proof signs it -- method, path, query, the hash of the body sent and the
117
+ * request proof's nonce.
118
+ */
119
+ function appPinProofRequest(args) {
120
+ const target = (0, target_1.parseTarget)(args.path);
121
+ return {
122
+ pin: args.pin,
123
+ request: {
124
+ method: args.method.toUpperCase(),
125
+ path: target.path,
126
+ query: target.query,
127
+ body_sha256: (0, bytes_1.sha256Hex)(args.encodedBody),
128
+ nonce: args.nonce,
129
+ },
130
+ };
131
+ }
132
+ /**
133
+ * The step-up proof, version 2, that the step-up key signs for one request: the base64url of its
134
+ * JSON, the members in this order. [nonce] is the request proof's nonce for the same request, so
135
+ * the approval is used up with it.
136
+ */
137
+ function stepUpProof(args) {
138
+ const target = (0, target_1.parseTarget)(args.path);
139
+ return (0, bytes_1.base64UrlNoPadding)((0, bytes_1.utf8Encode)(JSON.stringify({
140
+ version: 2,
141
+ purpose: 'stepup',
142
+ installation_id: args.installationId,
143
+ access_token_sha256: (0, bytes_1.sha256Hex)(args.accessToken),
144
+ method: args.method.toUpperCase(),
145
+ path: target.path,
146
+ query: target.query,
147
+ body_sha256: (0, bytes_1.sha256Hex)(args.encodedBody),
148
+ factor: args.factor,
149
+ nonce: args.nonce,
150
+ timestamp: args.timestamp,
151
+ })));
152
+ }
153
+ /**
154
+ * Signs [method] [path] with [encodedBody] under [bearerToken]: proof version 2, which signs the
155
+ * query string too, exactly as sent. A GET signs an empty body. With [stepUp], the step-up key signs
156
+ * first -- the phone asks the person -- and the request proof after, with the same nonce, so the
157
+ * time the person takes does not count against the request proof's clock window.
158
+ * [installationId], [timestamp] and [nonce] replace what the request proof would otherwise carry,
159
+ * for the shared test vectors and for testing a backend's refusals.
160
+ */
161
+ async function signRequest(args) {
162
+ const normalizedMethod = args.method.toUpperCase();
163
+ const body = normalizedMethod === 'GET' ? '' : args.encodedBody ?? '';
164
+ const nonceText = (0, bytes_1.base64UrlNoPadding)(args.nonce ?? (await freshNonce()));
165
+ let stepUpPayload = null;
166
+ let stepUpSignature = null;
167
+ if (args.stepUp != null) {
168
+ stepUpPayload = stepUpProof({
169
+ installationId: args.identity.installationId,
170
+ accessToken: args.bearerToken,
171
+ method: normalizedMethod,
172
+ path: args.path,
173
+ encodedBody: body,
174
+ factor: args.stepUp.factor,
175
+ nonce: nonceText,
176
+ timestamp: nowSeconds(),
177
+ });
178
+ stepUpSignature = await args.stepUp.sign(stepUpPayload);
179
+ }
180
+ const signedAt = args.timestamp ?? nowSeconds();
181
+ const target = (0, target_1.parseTarget)(args.path);
182
+ // The members in this order: the backend checks each against the request it received, and the
183
+ // shared vectors hold both sides to these bytes.
184
+ const proof = {
185
+ access_token_sha256: (0, bytes_1.sha256Hex)(args.bearerToken),
186
+ body_sha256: (0, bytes_1.sha256Hex)(body),
187
+ installation_id: args.installationId ?? args.identity.installationId,
188
+ method: normalizedMethod,
189
+ nonce: nonceText,
190
+ path: target.path,
191
+ query: target.query,
192
+ timestamp: signedAt,
193
+ version: 2,
194
+ };
195
+ // The key signs the exact JSON bytes this base64url value carries; the private key never leaves
196
+ // the phone's keystore.
197
+ const payload = (0, bytes_1.base64UrlNoPadding)((0, bytes_1.utf8Encode)(JSON.stringify(proof)));
198
+ return new RequestProof({
199
+ bearerToken: args.bearerToken,
200
+ method: normalizedMethod,
201
+ path: args.path,
202
+ encodedBody: body,
203
+ payload,
204
+ signature: await args.identity.signPayload(payload),
205
+ timestamp: signedAt,
206
+ nonce: nonceText,
207
+ stepUpPayload,
208
+ stepUpSignature,
209
+ });
210
+ }
211
+ /** The one request a scan is to approve, described exactly as it will be sent: what the challenge names as its `operation`. */
212
+ function describeOperation(method, path, encodedBody) {
213
+ const target = (0, target_1.parseTarget)(path);
214
+ return {
215
+ method: method.toUpperCase(),
216
+ path: target.path,
217
+ query: target.query,
218
+ body_sha256: (0, bytes_1.sha256Hex)(encodedBody),
219
+ };
220
+ }
@@ -0,0 +1,15 @@
1
+ /** Protection of the app's window while a secret is typed. */
2
+ /**
3
+ * Protects the app's window while a secret is typed. Android: no screenshots, recordings or casts
4
+ * of the window, other apps' windows not drawn over it (Android 12 and later), and touches that
5
+ * passed through another app's window dropped. iOS cannot block screenshots; it covers the window
6
+ * while the screen is recorded or mirrored.
7
+ *
8
+ * Calls nest: each `protect(true)` needs its `protect(false)`, and the window is put back as it
9
+ * was -- a window the app keeps secure itself stays secure -- when the last one ends. Other apps'
10
+ * overlays, which no public call reads, are shown again then. Returns what the window has now; a
11
+ * build without the native module answers nothing, and the entry stays usable.
12
+ */
13
+ export declare class SecureScreen {
14
+ static protect(on: boolean): Promise<Record<string, unknown>>;
15
+ }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ /** Protection of the app's window while a secret is typed. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.SecureScreen = void 0;
5
+ const native_1 = require("./util/native");
6
+ /**
7
+ * Protects the app's window while a secret is typed. Android: no screenshots, recordings or casts
8
+ * of the window, other apps' windows not drawn over it (Android 12 and later), and touches that
9
+ * passed through another app's window dropped. iOS cannot block screenshots; it covers the window
10
+ * while the screen is recorded or mirrored.
11
+ *
12
+ * Calls nest: each `protect(true)` needs its `protect(false)`, and the window is put back as it
13
+ * was -- a window the app keeps secure itself stays secure -- when the last one ends. Other apps'
14
+ * overlays, which no public call reads, are shown again then. Returns what the window has now; a
15
+ * build without the native module answers nothing, and the entry stays usable.
16
+ */
17
+ class SecureScreen {
18
+ static async protect(on) {
19
+ try {
20
+ const native = (0, native_1.secureScreenModule)();
21
+ if (native === null) {
22
+ return {};
23
+ }
24
+ const state = (await native.protect(on));
25
+ return state ?? {};
26
+ }
27
+ catch {
28
+ return {};
29
+ }
30
+ }
31
+ }
32
+ exports.SecureScreen = SecureScreen;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The step-up key (the public guide, section 3): a second key that the person must unlock, the
3
+ * options it is made with, and its re-enrolment proof.
4
+ */
5
+ import { type PublicJwk } from './installationKey';
6
+ /**
7
+ * The optional step-up key as the native side reports it. [factor], [mode] and [windowSeconds]
8
+ * describe what the platform keystore actually enforces (read back from the key itself), which
9
+ * on Android 9 and 10 is a short hardware window rather than per-use.
10
+ */
11
+ export declare class StepUpKeyMetadata {
12
+ readonly publicKey: PublicJwk;
13
+ readonly factor: string;
14
+ readonly mode: string;
15
+ readonly windowSeconds: number;
16
+ readonly securityLevel: string;
17
+ readonly hardwareBacked: boolean;
18
+ readonly created: boolean;
19
+ constructor(value: {
20
+ publicKey: PublicJwk;
21
+ factor: string;
22
+ mode: string;
23
+ windowSeconds: number;
24
+ securityLevel: string;
25
+ hardwareBacked: boolean;
26
+ created: boolean;
27
+ });
28
+ get authJson(): Record<string, unknown>;
29
+ get description(): string;
30
+ /** RFC 7638 thumbprint, the same form the server derives from the JWK. */
31
+ get thumbprint(): string;
32
+ static fromPlatform(value: Record<string, unknown>): StepUpKeyMetadata;
33
+ }
34
+ /** Where a backend takes a re-enrolled step-up key by default (the public guide, section 4). */
35
+ export declare const stepUpReenrolPath = "/v1/installations/stepup-key";
36
+ /**
37
+ * How the app wants its step-up key made (the public guide, section 3): [factor] `passcode` or
38
+ * `biometric`; [mode] `per_use`, or `windowed` with [windowSeconds] from 1. Android 9 and 10 cannot
39
+ * make a per-use passcode key and make a windowed one; iOS makes per-use keys only. [reenrolPath]
40
+ * is where the backend takes a new key (DeviceTrust.reenrolStepUp).
41
+ */
42
+ export declare class StepUpOptions {
43
+ readonly factor: string;
44
+ readonly mode: string;
45
+ readonly windowSeconds: number;
46
+ readonly reenrolPath: string;
47
+ constructor(options?: {
48
+ factor?: string;
49
+ mode?: string;
50
+ windowSeconds?: number;
51
+ reenrolPath?: string;
52
+ });
53
+ /**
54
+ * A key made with other values, or already made: whether it satisfies these options -- the same
55
+ * factor (a backend refuses any other), and a per-use key or a window no longer than this one. A
56
+ * per-use key satisfies windowed options.
57
+ */
58
+ accepts(key: StepUpKeyMetadata): boolean;
59
+ get description(): string;
60
+ }
61
+ /**
62
+ * The proof a new step-up key signs at re-enrolment: the base64url of its JSON, the members in this
63
+ * order. [nonce] is the request proof's nonce of the re-enrolment request.
64
+ */
65
+ export declare function stepUpReenrolProof(args: {
66
+ installationId: string;
67
+ stepUpKeyThumbprint: string;
68
+ nonce: string;
69
+ timestamp: number;
70
+ }): string;
71
+ /**
72
+ * Whether a step-up key [options] do not accept is replaced before the next registration: only
73
+ * where the new key can then be bound -- by a re-enrolment ([replaceMismatched]), which binds it
74
+ * with the person's re-authentication, or by this installation's first registration, which binds
75
+ * the key it is offered. A known installation's registration never binds an offered key; it only
76
+ * says whether it matches, so replacing the key there orphans the one the server holds: on a phone
77
+ * that cannot make the key the options ask for (Android 9 makes no per-use passcode key), the
78
+ * registration that follows every re-enrolment would replace the key the server had just bound,
79
+ * and step-up would stay unavailable.
80
+ */
81
+ export declare function replacesStepUpKey(key: StepUpKeyMetadata, options: StepUpOptions, flags: {
82
+ replaceMismatched: boolean;
83
+ knownInstallation: boolean;
84
+ }): boolean;
85
+ export declare class NativeStepUpKey {
86
+ /** The mode the app asks for, which decides whether a windowed key first tries to sign inside an open hardware window. */
87
+ configuredMode: string;
88
+ constructor(options?: {
89
+ configuredMode?: string;
90
+ });
91
+ /**
92
+ * Returns the existing key as the keystore enforces it, or creates one with these values: an
93
+ * existing key is never changed, so a caller that needs other values deletes it first.
94
+ */
95
+ getOrCreateKey(args: {
96
+ factor: string;
97
+ mode: string;
98
+ windowSeconds: number;
99
+ }): Promise<StepUpKeyMetadata>;
100
+ /** Shows the system passcode (or biometric) prompt and completes when the person answers it. A cancelled prompt is STEPUP_CANCELLED. */
101
+ sign(payloadBase64Url: string, options: {
102
+ reason: string;
103
+ }): Promise<string>;
104
+ deleteKey(): Promise<void>;
105
+ }
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ /**
3
+ * The step-up key (the public guide, section 3): a second key that the person must unlock, the
4
+ * options it is made with, and its re-enrolment proof.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.NativeStepUpKey = exports.StepUpOptions = exports.stepUpReenrolPath = exports.StepUpKeyMetadata = void 0;
8
+ exports.stepUpReenrolProof = stepUpReenrolProof;
9
+ exports.replacesStepUpKey = replacesStepUpKey;
10
+ const errors_1 = require("./errors");
11
+ const bytes_1 = require("./util/bytes");
12
+ const native_1 = require("./util/native");
13
+ const installationKey_1 = require("./installationKey");
14
+ /**
15
+ * The optional step-up key as the native side reports it. [factor], [mode] and [windowSeconds]
16
+ * describe what the platform keystore actually enforces (read back from the key itself), which
17
+ * on Android 9 and 10 is a short hardware window rather than per-use.
18
+ */
19
+ class StepUpKeyMetadata {
20
+ constructor(value) {
21
+ this.publicKey = value.publicKey;
22
+ this.factor = value.factor;
23
+ this.mode = value.mode;
24
+ this.windowSeconds = value.windowSeconds;
25
+ this.securityLevel = value.securityLevel;
26
+ this.hardwareBacked = value.hardwareBacked;
27
+ this.created = value.created;
28
+ }
29
+ get authJson() {
30
+ return { factor: this.factor, mode: this.mode, window_seconds: this.windowSeconds };
31
+ }
32
+ get description() {
33
+ return this.mode === 'per_use'
34
+ ? `${this.factor}, per-use`
35
+ : `${this.factor}, ${this.windowSeconds}s hardware window`;
36
+ }
37
+ /** RFC 7638 thumbprint, the same form the server derives from the JWK. */
38
+ get thumbprint() {
39
+ return (0, installationKey_1.jwkThumbprint)(this.publicKey);
40
+ }
41
+ static fromPlatform(value) {
42
+ const rawPublicKey = value['public_key'];
43
+ const rawAuth = value['auth'];
44
+ if (!(0, native_1.isObject)(rawPublicKey) || !(0, native_1.isObject)(rawAuth)) {
45
+ throw new TypeError('The native step-up key provider returned no public key or auth.');
46
+ }
47
+ const publicKey = (0, installationKey_1.orderedJwk)(rawPublicKey);
48
+ if (publicKey['kty'] !== 'EC' ||
49
+ publicKey['crv'] !== 'P-256' ||
50
+ publicKey['alg'] !== 'ES256' ||
51
+ typeof publicKey['x'] !== 'string' ||
52
+ typeof publicKey['y'] !== 'string') {
53
+ throw new TypeError('The native step-up key provider did not return an EC P-256 public JWK.');
54
+ }
55
+ const factor = rawAuth['factor'];
56
+ const mode = rawAuth['mode'];
57
+ const window = rawAuth['window_seconds'];
58
+ if (typeof factor !== 'string' || typeof mode !== 'string' || typeof window !== 'number' || !Number.isInteger(window)) {
59
+ throw new TypeError('The native step-up key provider returned an invalid auth description.');
60
+ }
61
+ const securityLevel = value['security_level'];
62
+ return new StepUpKeyMetadata({
63
+ publicKey,
64
+ factor,
65
+ mode,
66
+ windowSeconds: window,
67
+ securityLevel: typeof securityLevel === 'string' ? securityLevel : 'unknown',
68
+ hardwareBacked: value['hardware_backed'] === true,
69
+ created: value['created'] === true,
70
+ });
71
+ }
72
+ }
73
+ exports.StepUpKeyMetadata = StepUpKeyMetadata;
74
+ /** Where a backend takes a re-enrolled step-up key by default (the public guide, section 4). */
75
+ exports.stepUpReenrolPath = '/v1/installations/stepup-key';
76
+ /**
77
+ * How the app wants its step-up key made (the public guide, section 3): [factor] `passcode` or
78
+ * `biometric`; [mode] `per_use`, or `windowed` with [windowSeconds] from 1. Android 9 and 10 cannot
79
+ * make a per-use passcode key and make a windowed one; iOS makes per-use keys only. [reenrolPath]
80
+ * is where the backend takes a new key (DeviceTrust.reenrolStepUp).
81
+ */
82
+ class StepUpOptions {
83
+ constructor(options = {}) {
84
+ this.factor = options.factor ?? 'passcode';
85
+ this.mode = options.mode ?? 'per_use';
86
+ this.windowSeconds = options.windowSeconds ?? 0;
87
+ this.reenrolPath = options.reenrolPath ?? exports.stepUpReenrolPath;
88
+ }
89
+ /**
90
+ * A key made with other values, or already made: whether it satisfies these options -- the same
91
+ * factor (a backend refuses any other), and a per-use key or a window no longer than this one. A
92
+ * per-use key satisfies windowed options.
93
+ */
94
+ accepts(key) {
95
+ return (key.factor === this.factor &&
96
+ (key.mode === 'per_use' || (this.mode === 'windowed' && key.windowSeconds <= this.windowSeconds)));
97
+ }
98
+ get description() {
99
+ return `${this.factor}, ${this.mode === 'per_use' ? 'per-use' : `${this.windowSeconds}s window`}`;
100
+ }
101
+ }
102
+ exports.StepUpOptions = StepUpOptions;
103
+ /**
104
+ * The proof a new step-up key signs at re-enrolment: the base64url of its JSON, the members in this
105
+ * order. [nonce] is the request proof's nonce of the re-enrolment request.
106
+ */
107
+ function stepUpReenrolProof(args) {
108
+ return (0, bytes_1.base64UrlNoPadding)((0, bytes_1.utf8Encode)(JSON.stringify({
109
+ version: 1,
110
+ purpose: 'stepup_reenrol',
111
+ installation_id: args.installationId,
112
+ stepup_key_thumbprint: args.stepUpKeyThumbprint,
113
+ nonce: args.nonce,
114
+ timestamp: args.timestamp,
115
+ })));
116
+ }
117
+ /**
118
+ * Whether a step-up key [options] do not accept is replaced before the next registration: only
119
+ * where the new key can then be bound -- by a re-enrolment ([replaceMismatched]), which binds it
120
+ * with the person's re-authentication, or by this installation's first registration, which binds
121
+ * the key it is offered. A known installation's registration never binds an offered key; it only
122
+ * says whether it matches, so replacing the key there orphans the one the server holds: on a phone
123
+ * that cannot make the key the options ask for (Android 9 makes no per-use passcode key), the
124
+ * registration that follows every re-enrolment would replace the key the server had just bound,
125
+ * and step-up would stay unavailable.
126
+ */
127
+ function replacesStepUpKey(key, options, flags) {
128
+ return !key.created && !options.accepts(key) && (flags.replaceMismatched || !flags.knownInstallation);
129
+ }
130
+ const STEPUP_MODULE_MISSING = () => new errors_1.NativeKeyException('NATIVE_STEPUP_PLUGIN_MISSING', "The SDK's step-up key module is not linked into this app: rebuild the app.");
131
+ class NativeStepUpKey {
132
+ constructor(options = {}) {
133
+ this.configuredMode = options.configuredMode ?? 'per_use';
134
+ }
135
+ /**
136
+ * Returns the existing key as the keystore enforces it, or creates one with these values: an
137
+ * existing key is never changed, so a caller that needs other values deletes it first.
138
+ */
139
+ async getOrCreateKey(args) {
140
+ const native = (0, native_1.stepUpKeyModule)();
141
+ if (native === null) {
142
+ throw STEPUP_MODULE_MISSING();
143
+ }
144
+ let value;
145
+ try {
146
+ value = await native.getOrCreateKey(args.factor, args.mode, args.windowSeconds);
147
+ }
148
+ catch (error) {
149
+ throw (0, native_1.nativeFailure)(error, 'The native step-up key provider failed.');
150
+ }
151
+ if (!(0, native_1.isObject)(value)) {
152
+ throw new errors_1.NativeKeyException('EMPTY_NATIVE_RESPONSE', 'The native step-up key provider returned no key metadata.');
153
+ }
154
+ return StepUpKeyMetadata.fromPlatform(value);
155
+ }
156
+ /** Shows the system passcode (or biometric) prompt and completes when the person answers it. A cancelled prompt is STEPUP_CANCELLED. */
157
+ async sign(payloadBase64Url, options) {
158
+ const native = (0, native_1.stepUpKeyModule)();
159
+ if (native === null) {
160
+ throw STEPUP_MODULE_MISSING();
161
+ }
162
+ let signature;
163
+ try {
164
+ signature = await native.sign(payloadBase64Url, options.reason, this.configuredMode);
165
+ }
166
+ catch (error) {
167
+ throw (0, native_1.nativeFailure)(error, 'The step-up key could not sign.');
168
+ }
169
+ if (typeof signature !== 'string' || signature.length === 0) {
170
+ throw new errors_1.NativeKeyException('EMPTY_NATIVE_SIGNATURE', 'The step-up key returned no signature.');
171
+ }
172
+ return signature;
173
+ }
174
+ async deleteKey() {
175
+ const native = (0, native_1.stepUpKeyModule)();
176
+ if (native === null) {
177
+ throw STEPUP_MODULE_MISSING();
178
+ }
179
+ try {
180
+ await native.deleteKey();
181
+ }
182
+ catch (error) {
183
+ throw (0, native_1.nativeFailure)(error, 'The step-up key could not be deleted.');
184
+ }
185
+ }
186
+ }
187
+ exports.NativeStepUpKey = NativeStepUpKey;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * How the SDK's messages reach your backend: by default over HTTPS, under the base URL your app
3
+ * sets (the public guide, section 4).
4
+ */
5
+ export type JsonObject = Record<string, unknown>;
6
+ export interface SendOptions {
7
+ /** Sent exactly as given: it is what a request proof hashes. A GET sends no body. */
8
+ encodedBody?: string | null;
9
+ bearerToken?: string | null;
10
+ headers?: Record<string, string>;
11
+ }
12
+ /**
13
+ * Sends one request to your backend and answers its JSON object. A refusal is thrown as an
14
+ * [ApiException] with the backend's `code`, `message` and `details` (the public guide, section 4,
15
+ * Errors).
16
+ */
17
+ export interface DeviceTrustTransport {
18
+ send(method: string, path: string, options?: SendOptions): Promise<JsonObject>;
19
+ close(): void;
20
+ }
21
+ /**
22
+ * JSON over HTTPS under [baseUrl]. A release build refuses any other scheme, so a mistyped
23
+ * endpoint fails loudly instead of sending tokens in the clear.
24
+ */
25
+ export declare class HttpTransport implements DeviceTrustTransport {
26
+ readonly baseUrl: string;
27
+ /** How long one call may take, in milliseconds. */
28
+ readonly timeoutMs: number;
29
+ private readonly fetchImpl;
30
+ private readonly running;
31
+ private closed;
32
+ constructor(baseUrl: string, options?: {
33
+ timeoutMs?: number;
34
+ fetch?: typeof fetch;
35
+ });
36
+ private get base();
37
+ send(method: string, path: string, options?: SendOptions): Promise<JsonObject>;
38
+ close(): void;
39
+ }