@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.
- package/CHANGELOG.md +40 -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 +1157 -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 +338 -0
- package/android/src/main/java/com/necessarysdk/reactnative/NecessaryPackage.kt +38 -0
- package/android/src/main/java/com/necessarysdk/reactnative/ProbeReads.kt +333 -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 +767 -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 +959 -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 +370 -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 +699 -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 +134 -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/answers.d.ts +6 -0
- package/lib/util/answers.js +16 -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 +851 -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 +168 -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/answers.ts +15 -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
package/package.json
CHANGED
|
@@ -1,6 +1,83 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@necessarysdk/react-native",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "Device integrity for React Native apps: a key the SDK never exports, signed measurements of the phone and the app, and request proofs. The app's backend decides.",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"react-native": "src/index.ts",
|
|
8
|
+
"source": "src/index.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"lib",
|
|
12
|
+
"android/build.gradle",
|
|
13
|
+
"android/src/main",
|
|
14
|
+
"ios",
|
|
15
|
+
"NecessaryReactNative.podspec",
|
|
16
|
+
"doc",
|
|
17
|
+
"contract",
|
|
18
|
+
"vectors",
|
|
19
|
+
"CHANGELOG.md",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"NOTICE"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.build.json",
|
|
25
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
26
|
+
"test": "jest",
|
|
27
|
+
"prepack": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"react-native",
|
|
31
|
+
"device-integrity",
|
|
32
|
+
"device-binding",
|
|
33
|
+
"proof-of-possession",
|
|
34
|
+
"secure-enclave",
|
|
35
|
+
"keystore",
|
|
36
|
+
"tamper-detection",
|
|
37
|
+
"necessary"
|
|
38
|
+
],
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/merajhasan88/necessary_reactnative.git"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://necessarysdk.com",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/merajhasan88/necessary_reactnative/issues"
|
|
46
|
+
},
|
|
47
|
+
"license": "Apache-2.0",
|
|
48
|
+
"author": "Meraj Hasan",
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"react-native": ">=0.76.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/jest": "29.5.14",
|
|
54
|
+
"@types/node": "^24.19.2",
|
|
55
|
+
"jest": "29.7.0",
|
|
56
|
+
"react-native": "0.88.1",
|
|
57
|
+
"ts-jest": "29.4.15",
|
|
58
|
+
"typescript": "6.0.3"
|
|
59
|
+
},
|
|
60
|
+
"codegenConfig": {
|
|
61
|
+
"name": "NecessarySpec",
|
|
62
|
+
"type": "modules",
|
|
63
|
+
"jsSrcsDir": "src/native",
|
|
64
|
+
"android": {
|
|
65
|
+
"javaPackageName": "com.necessarysdk.reactnative"
|
|
66
|
+
},
|
|
67
|
+
"ios": {
|
|
68
|
+
"modulesProvider": {
|
|
69
|
+
"NecessaryInstallationKey": "NecessaryInstallationKey",
|
|
70
|
+
"NecessaryIntegrity": "NecessaryIntegrity",
|
|
71
|
+
"NecessaryStepUpKey": "NecessaryStepUpKey",
|
|
72
|
+
"NecessarySecureScreen": "NecessarySecureScreen",
|
|
73
|
+
"NecessaryStorage": "NecessaryStorage"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"publishConfig": {
|
|
78
|
+
"access": "public"
|
|
79
|
+
},
|
|
80
|
+
"engines": {
|
|
81
|
+
"node": ">= 20.19.4"
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public guide's five calls (section 4) over a [DeviceTrustTransport], the two scan calls each
|
|
3
|
+
* with its request proof (section 3), and the app PIN's two calls.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ApiException } from './errors';
|
|
7
|
+
import { currentPlatform } from './util/native';
|
|
8
|
+
import type { InstallationIdentity } from './installationKey';
|
|
9
|
+
import type { ReinstallHint } from './reinstallHint';
|
|
10
|
+
import { ScanChallenge, type SignedReport } from './report';
|
|
11
|
+
import { appPinProofRequest, RequestProof, signRequest } from './requestProof';
|
|
12
|
+
import type { StepUpKeyMetadata } from './stepUpKey';
|
|
13
|
+
import type { DeviceTrustTransport, JsonObject } from './transport';
|
|
14
|
+
|
|
15
|
+
export class DeviceTrustClient {
|
|
16
|
+
/** The app PIN's default paths (the public guide, section 4). */
|
|
17
|
+
static readonly appPinPath = '/v1/account/app-pin';
|
|
18
|
+
static readonly appPinProofPath = '/v1/account/app-pin/proof';
|
|
19
|
+
|
|
20
|
+
/** The fields of the guide's registration, which [registerInstallation]'s `extra` cannot replace. */
|
|
21
|
+
static readonly registrationFields: ReadonlySet<string> = new Set([
|
|
22
|
+
'installation_id',
|
|
23
|
+
'platform',
|
|
24
|
+
'public_key',
|
|
25
|
+
'reinstall_hint',
|
|
26
|
+
'key_security',
|
|
27
|
+
'stepup_public_key',
|
|
28
|
+
'stepup_key_auth',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
readonly transport: DeviceTrustTransport;
|
|
32
|
+
private readonly platform: () => string;
|
|
33
|
+
|
|
34
|
+
/** [platform] replaces the phone's (`android` or `ios`) in tests. */
|
|
35
|
+
constructor(transport: DeviceTrustTransport, options: { platform?: () => string } = {}) {
|
|
36
|
+
this.transport = transport;
|
|
37
|
+
this.platform = options.platform ?? currentPlatform;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Sends [proof]'s request. [method], [path] and [encodedBody] replace what the proof was signed
|
|
42
|
+
* for -- only to test that a backend refuses them.
|
|
43
|
+
*/
|
|
44
|
+
sendSigned(proof: RequestProof, overrides: { method?: string; path?: string; encodedBody?: string } = {}): Promise<JsonObject> {
|
|
45
|
+
const sentMethod = (overrides.method ?? proof.method).toUpperCase();
|
|
46
|
+
return this.transport.send(sentMethod, overrides.path ?? proof.path, {
|
|
47
|
+
bearerToken: proof.bearerToken,
|
|
48
|
+
encodedBody: sentMethod === 'GET' ? null : overrides.encodedBody ?? proof.encodedBody,
|
|
49
|
+
headers: proof.headers,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Signs and sends one request under [bearerToken], its [body] as JSON. [installationId] replaces
|
|
55
|
+
* the installation the proof names, only to test that a backend refuses it.
|
|
56
|
+
*/
|
|
57
|
+
async signAndSend(
|
|
58
|
+
method: string,
|
|
59
|
+
path: string,
|
|
60
|
+
args: { bearerToken: string; identity: InstallationIdentity; body?: Record<string, unknown> | null; installationId?: string | null },
|
|
61
|
+
): Promise<JsonObject> {
|
|
62
|
+
return this.sendSigned(
|
|
63
|
+
await signRequest({
|
|
64
|
+
identity: args.identity,
|
|
65
|
+
method,
|
|
66
|
+
path,
|
|
67
|
+
bearerToken: args.bearerToken,
|
|
68
|
+
encodedBody: JSON.stringify(args.body ?? {}),
|
|
69
|
+
installationId: args.installationId,
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Registers the installation's key, and with [stepUpKey] the step-up key. The answer's
|
|
76
|
+
* `installation_id` is the installation your backend holds for this key, which may not be the one
|
|
77
|
+
* proposed. [extra] adds fields of your backend's own.
|
|
78
|
+
*/
|
|
79
|
+
registerInstallation(
|
|
80
|
+
identity: InstallationIdentity,
|
|
81
|
+
hint: ReinstallHint | null,
|
|
82
|
+
options: { stepUpKey?: StepUpKeyMetadata | null; extra?: Record<string, unknown> } = {},
|
|
83
|
+
): Promise<JsonObject> {
|
|
84
|
+
const extra = options.extra ?? {};
|
|
85
|
+
const replaced = Object.keys(extra).filter((name) => DeviceTrustClient.registrationFields.has(name));
|
|
86
|
+
if (replaced.length > 0) {
|
|
87
|
+
throw new TypeError(`extra replaces the registration fields ${replaced.join(', ')}`);
|
|
88
|
+
}
|
|
89
|
+
const stepUpKey = options.stepUpKey ?? null;
|
|
90
|
+
const body: Record<string, unknown> = {
|
|
91
|
+
installation_id: identity.installationId,
|
|
92
|
+
platform: this.platform() === 'android' ? 'android' : 'ios',
|
|
93
|
+
public_key: identity.publicKeyJson,
|
|
94
|
+
reinstall_hint: hint === null ? null : hint.toJSON(),
|
|
95
|
+
// What the platform's keystore says about how the key is protected: a claim, not proof. Its
|
|
96
|
+
// value is comparative -- a non-exportable hardware key cannot move into software, so the
|
|
97
|
+
// same key later claiming a weaker level contradicts itself.
|
|
98
|
+
key_security: {
|
|
99
|
+
security_level: identity.securityLevel,
|
|
100
|
+
hardware_backed: identity.hardwareBacked,
|
|
101
|
+
provider: identity.provider,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
// The step-up key, and how the phone says it is unlocked: also a claim, recorded and compared
|
|
105
|
+
// with what the backend requires.
|
|
106
|
+
if (stepUpKey !== null) {
|
|
107
|
+
body['stepup_public_key'] = stepUpKey.publicKey;
|
|
108
|
+
body['stepup_key_auth'] = stepUpKey.authJson;
|
|
109
|
+
}
|
|
110
|
+
Object.assign(body, extra);
|
|
111
|
+
return this.transport.send('POST', '/v1/installations/register', { encodedBody: JSON.stringify(body) });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Asks for a key challenge: `challenge_id` and the `payload` to sign. */
|
|
115
|
+
createInstallationChallenge(installationId: string): Promise<JsonObject> {
|
|
116
|
+
return this.transport.send('POST', '/v1/installations/challenge', {
|
|
117
|
+
encodedBody: JSON.stringify({ installation_id: installationId }),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Answers a key challenge; the answer carries `device_token`, your backend's session for this installation. */
|
|
122
|
+
verifyInstallation(args: { installationId: string; challengeId: string; payload: string; signature: string }): Promise<JsonObject> {
|
|
123
|
+
return this.transport.send('POST', '/v1/installations/verify', {
|
|
124
|
+
encodedBody: JSON.stringify({
|
|
125
|
+
installation_id: args.installationId,
|
|
126
|
+
challenge_id: args.challengeId,
|
|
127
|
+
payload: args.payload,
|
|
128
|
+
signature: args.signature,
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Asks for a scan under [session]: the installation's session, from [verifyInstallation] -- or,
|
|
135
|
+
* for a scan that approves one [operation] ([describeOperation]), your account session for this
|
|
136
|
+
* installation.
|
|
137
|
+
*/
|
|
138
|
+
async integrityChallenge(args: {
|
|
139
|
+
session: string;
|
|
140
|
+
identity: InstallationIdentity;
|
|
141
|
+
operation?: Record<string, unknown> | null;
|
|
142
|
+
}): Promise<ScanChallenge> {
|
|
143
|
+
const body: Record<string, unknown> = {};
|
|
144
|
+
if (args.operation != null) {
|
|
145
|
+
body['operation'] = args.operation;
|
|
146
|
+
}
|
|
147
|
+
return ScanChallenge.fromJson(
|
|
148
|
+
await this.signAndSend('POST', '/v1/integrity/challenge', {
|
|
149
|
+
bearerToken: args.session,
|
|
150
|
+
identity: args.identity,
|
|
151
|
+
body,
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Sends [report] under the [session] that asked for its scan. The answer is your backend's own;
|
|
158
|
+
* for a scan that approves one request it carries `report_id`, sent with that request as
|
|
159
|
+
* `X-Integrity-Report`.
|
|
160
|
+
*/
|
|
161
|
+
submitIntegrityReport(args: { session: string; identity: InstallationIdentity; report: SignedReport }): Promise<JsonObject> {
|
|
162
|
+
return this.signAndSend('POST', '/v1/integrity/report', {
|
|
163
|
+
bearerToken: args.session,
|
|
164
|
+
identity: args.identity,
|
|
165
|
+
body: args.report.toJSON(),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Sets the account's app PIN under its account [session]. The backend requires the person's own
|
|
171
|
+
* re-authentication with it -- [reauthentication] carries it, the account's password for example:
|
|
172
|
+
* `{password: ...}`.
|
|
173
|
+
*/
|
|
174
|
+
setAppPin(args: {
|
|
175
|
+
session: string;
|
|
176
|
+
identity: InstallationIdentity;
|
|
177
|
+
pin: string;
|
|
178
|
+
reauthentication?: Record<string, unknown>;
|
|
179
|
+
path?: string;
|
|
180
|
+
}): Promise<JsonObject> {
|
|
181
|
+
return this.signAndSend('POST', args.path ?? DeviceTrustClient.appPinPath, {
|
|
182
|
+
bearerToken: args.session,
|
|
183
|
+
identity: args.identity,
|
|
184
|
+
body: { ...(args.reauthentication ?? {}), pin: args.pin },
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Has the backend check the app [pin] for the one signed [request] -- sent once, in a signed body,
|
|
190
|
+
* never in a header -- and answers that request unlocked by its proof, valid for that request
|
|
191
|
+
* alone.
|
|
192
|
+
*/
|
|
193
|
+
async proveAppPin(args: {
|
|
194
|
+
session: string;
|
|
195
|
+
identity: InstallationIdentity;
|
|
196
|
+
pin: string;
|
|
197
|
+
request: RequestProof;
|
|
198
|
+
path?: string;
|
|
199
|
+
}): Promise<RequestProof> {
|
|
200
|
+
const answer = await this.signAndSend('POST', args.path ?? DeviceTrustClient.appPinProofPath, {
|
|
201
|
+
bearerToken: args.session,
|
|
202
|
+
identity: args.identity,
|
|
203
|
+
body: appPinProofRequest({
|
|
204
|
+
pin: args.pin,
|
|
205
|
+
method: args.request.method,
|
|
206
|
+
path: args.request.path,
|
|
207
|
+
encodedBody: args.request.encodedBody,
|
|
208
|
+
nonce: args.request.nonce,
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
const proof = answer['app_pin_proof'];
|
|
212
|
+
if (typeof proof !== 'string' || proof.length === 0) {
|
|
213
|
+
throw new ApiException('The backend answered no app PIN proof.', { code: 'app_pin_proof_missing' });
|
|
214
|
+
}
|
|
215
|
+
return args.request.withAppPinProof(proof);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
close(): void {
|
|
219
|
+
this.transport.close();
|
|
220
|
+
}
|
|
221
|
+
}
|
package/src/collector.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** The native integrity collector: measures what a challenge asks for and the app enabled, and nothing else. */
|
|
2
|
+
|
|
3
|
+
import { NativeKeyException } from './errors';
|
|
4
|
+
import { integrityModule, isObject, nativeFailure } from './util/native';
|
|
5
|
+
|
|
6
|
+
const INTEGRITY_MODULE_MISSING = (): NativeKeyException =>
|
|
7
|
+
new NativeKeyException(
|
|
8
|
+
'NATIVE_INTEGRITY_PLUGIN_MISSING',
|
|
9
|
+
"The SDK's integrity module is not linked into this app: rebuild the app.",
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
export class NativeIntegrityCollector {
|
|
13
|
+
/**
|
|
14
|
+
* [enabledMeasurements]: the personal measurements this app's developers allow; with
|
|
15
|
+
* [optionalProbes], what the backend asks for, the collector measures only what is in both.
|
|
16
|
+
*/
|
|
17
|
+
async collect(args: {
|
|
18
|
+
requiredProbes: string[];
|
|
19
|
+
challengeNonce: string;
|
|
20
|
+
enabledMeasurements: string[];
|
|
21
|
+
optionalProbes?: string[];
|
|
22
|
+
}): Promise<Record<string, unknown>> {
|
|
23
|
+
const native = integrityModule();
|
|
24
|
+
if (native === null) {
|
|
25
|
+
throw INTEGRITY_MODULE_MISSING();
|
|
26
|
+
}
|
|
27
|
+
let value: unknown;
|
|
28
|
+
try {
|
|
29
|
+
// What the server asks for and what this app allows; the native collector measures only what
|
|
30
|
+
// is in both.
|
|
31
|
+
value = await native.collect(
|
|
32
|
+
args.requiredProbes,
|
|
33
|
+
args.challengeNonce,
|
|
34
|
+
args.optionalProbes ?? [],
|
|
35
|
+
args.enabledMeasurements,
|
|
36
|
+
);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw nativeFailure(error, 'The native integrity collector failed.');
|
|
39
|
+
}
|
|
40
|
+
if (!isObject(value)) {
|
|
41
|
+
throw new NativeKeyException(
|
|
42
|
+
'EMPTY_INTEGRITY_RESPONSE',
|
|
43
|
+
'The native integrity collector returned no measurements.',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Asks the person for the OS location permission and returns what the app now holds: precise,
|
|
51
|
+
* approximate or none. Only the app asks, from its own screen; a scan never prompts.
|
|
52
|
+
*/
|
|
53
|
+
async requestLocationPermission(): Promise<string> {
|
|
54
|
+
const native = integrityModule();
|
|
55
|
+
if (native === null) {
|
|
56
|
+
throw INTEGRITY_MODULE_MISSING();
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
return (await native.requestLocationPermission()) ?? 'none';
|
|
60
|
+
} catch (error) {
|
|
61
|
+
throw nativeFailure(error, 'The location permission request failed.');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|