@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
package/lib/transport.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* How the SDK's messages reach your backend: by default over HTTPS, under the base URL your app
|
|
4
|
+
* sets (the public guide, section 4).
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.HttpTransport = void 0;
|
|
8
|
+
const errors_1 = require("./errors");
|
|
9
|
+
const release_1 = require("./util/release");
|
|
10
|
+
const target_1 = require("./util/target");
|
|
11
|
+
function isObject(value) {
|
|
12
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* JSON over HTTPS under [baseUrl]. A release build refuses any other scheme, so a mistyped
|
|
16
|
+
* endpoint fails loudly instead of sending tokens in the clear.
|
|
17
|
+
*/
|
|
18
|
+
class HttpTransport {
|
|
19
|
+
constructor(baseUrl, options = {}) {
|
|
20
|
+
this.running = new Set();
|
|
21
|
+
this.closed = false;
|
|
22
|
+
this.baseUrl = baseUrl;
|
|
23
|
+
this.timeoutMs = options.timeoutMs ?? 15000;
|
|
24
|
+
this.fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
|
|
25
|
+
}
|
|
26
|
+
get base() {
|
|
27
|
+
if (this.baseUrl.length === 0) {
|
|
28
|
+
throw new errors_1.ApiException('No API base URL was set for the backend.', { code: 'api_base_url_missing' });
|
|
29
|
+
}
|
|
30
|
+
if ((0, release_1.isReleaseBuild)() && !this.baseUrl.toLowerCase().startsWith('https://')) {
|
|
31
|
+
throw new errors_1.ApiException('The API base URL must be an https:// URL in a release build.', {
|
|
32
|
+
code: 'api_base_url_insecure',
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return this.baseUrl.endsWith('/') ? this.baseUrl.substring(0, this.baseUrl.length - 1) : this.baseUrl;
|
|
36
|
+
}
|
|
37
|
+
async send(method, path, options = {}) {
|
|
38
|
+
const url = `${this.base}${(0, target_1.normalizedTarget)(path)}`;
|
|
39
|
+
if (this.closed) {
|
|
40
|
+
throw new errors_1.ApiException('HTTP connection failed: the transport is closed.');
|
|
41
|
+
}
|
|
42
|
+
const headers = {
|
|
43
|
+
Accept: 'application/json',
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
...(options.bearerToken != null ? { Authorization: `Bearer ${options.bearerToken}` } : {}),
|
|
46
|
+
...(options.headers ?? {}),
|
|
47
|
+
};
|
|
48
|
+
const controller = new AbortController();
|
|
49
|
+
this.running.add(controller);
|
|
50
|
+
let timedOut = false;
|
|
51
|
+
const timer = setTimeout(() => {
|
|
52
|
+
timedOut = true;
|
|
53
|
+
controller.abort();
|
|
54
|
+
}, this.timeoutMs);
|
|
55
|
+
let status;
|
|
56
|
+
let text;
|
|
57
|
+
try {
|
|
58
|
+
const response = await this.fetchImpl(url, {
|
|
59
|
+
method,
|
|
60
|
+
headers,
|
|
61
|
+
body: method === 'GET' ? undefined : options.encodedBody ?? '{}',
|
|
62
|
+
signal: controller.signal,
|
|
63
|
+
});
|
|
64
|
+
status = response.status;
|
|
65
|
+
text = await response.text();
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (timedOut) {
|
|
69
|
+
throw new errors_1.ApiException('The server did not respond before the timeout.');
|
|
70
|
+
}
|
|
71
|
+
if (this.closed) {
|
|
72
|
+
throw new errors_1.ApiException('HTTP connection failed: the transport is closed.');
|
|
73
|
+
}
|
|
74
|
+
const words = error instanceof Error ? error.message : String(error);
|
|
75
|
+
throw new errors_1.ApiException(`Network connection failed: ${words}`);
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
this.running.delete(controller);
|
|
80
|
+
}
|
|
81
|
+
let decoded = {};
|
|
82
|
+
if (text.trim().length > 0) {
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = JSON.parse(text);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
parsed = undefined;
|
|
89
|
+
}
|
|
90
|
+
if (!isObject(parsed)) {
|
|
91
|
+
throw new errors_1.ApiException('The server returned a non-JSON response.', { statusCode: status });
|
|
92
|
+
}
|
|
93
|
+
decoded = parsed;
|
|
94
|
+
}
|
|
95
|
+
if (status < 200 || status >= 300) {
|
|
96
|
+
const error = decoded['error'];
|
|
97
|
+
const refusal = isObject(error) ? error : null;
|
|
98
|
+
const details = refusal?.['details'];
|
|
99
|
+
const message = refusal?.['message'];
|
|
100
|
+
const code = refusal?.['code'];
|
|
101
|
+
throw new errors_1.ApiException(typeof message === 'string' ? message : `The server rejected the request (${status}).`, {
|
|
102
|
+
statusCode: status,
|
|
103
|
+
code: typeof code === 'string' ? code : undefined,
|
|
104
|
+
details: isObject(details) ? details : undefined,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return decoded;
|
|
108
|
+
}
|
|
109
|
+
close() {
|
|
110
|
+
this.closed = true;
|
|
111
|
+
for (const controller of this.running) {
|
|
112
|
+
controller.abort();
|
|
113
|
+
}
|
|
114
|
+
this.running.clear();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
exports.HttpTransport = HttpTransport;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bytes, the same on the phone and in Node: UTF-8, base64url without padding and SHA-256, written
|
|
3
|
+
* here so that nothing depends on what a JavaScript engine happens to provide.
|
|
4
|
+
*/
|
|
5
|
+
/** The UTF-8 bytes of [text]; a lone surrogate becomes U+FFFD, as every UTF-8 encoder writes it. */
|
|
6
|
+
export declare function utf8Encode(text: string): Uint8Array;
|
|
7
|
+
/** The text of UTF-8 [bytes]; a malformed sequence becomes U+FFFD. */
|
|
8
|
+
export declare function utf8Decode(bytes: Uint8Array): string;
|
|
9
|
+
/** Base64url without padding: how every binary value travels. */
|
|
10
|
+
export declare function base64UrlNoPadding(bytes: Uint8Array | readonly number[]): string;
|
|
11
|
+
/** The bytes of base64url [text], with or without padding; throws on any other character. */
|
|
12
|
+
export declare function base64UrlDecode(text: string): Uint8Array;
|
|
13
|
+
/** The SHA-256 digest of [bytes]. */
|
|
14
|
+
export declare function sha256(bytes: Uint8Array): Uint8Array;
|
|
15
|
+
/** Lowercase hex of [bytes]. */
|
|
16
|
+
export declare function hex(bytes: Uint8Array): string;
|
|
17
|
+
/** The SHA-256 of [text]'s UTF-8 bytes, in lowercase hex. */
|
|
18
|
+
export declare function sha256Hex(text: string): string;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Bytes, the same on the phone and in Node: UTF-8, base64url without padding and SHA-256, written
|
|
4
|
+
* here so that nothing depends on what a JavaScript engine happens to provide.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.utf8Encode = utf8Encode;
|
|
8
|
+
exports.utf8Decode = utf8Decode;
|
|
9
|
+
exports.base64UrlNoPadding = base64UrlNoPadding;
|
|
10
|
+
exports.base64UrlDecode = base64UrlDecode;
|
|
11
|
+
exports.sha256 = sha256;
|
|
12
|
+
exports.hex = hex;
|
|
13
|
+
exports.sha256Hex = sha256Hex;
|
|
14
|
+
/** The UTF-8 bytes of [text]; a lone surrogate becomes U+FFFD, as every UTF-8 encoder writes it. */
|
|
15
|
+
function utf8Encode(text) {
|
|
16
|
+
const out = [];
|
|
17
|
+
for (let i = 0; i < text.length; i++) {
|
|
18
|
+
let code = text.charCodeAt(i);
|
|
19
|
+
if (code >= 0xd800 && code <= 0xdbff && i + 1 < text.length) {
|
|
20
|
+
const next = text.charCodeAt(i + 1);
|
|
21
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
22
|
+
code = 0x10000 + ((code - 0xd800) << 10) + (next - 0xdc00);
|
|
23
|
+
i++;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
code = 0xfffd;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else if (code >= 0xd800 && code <= 0xdfff) {
|
|
30
|
+
code = 0xfffd;
|
|
31
|
+
}
|
|
32
|
+
if (code < 0x80) {
|
|
33
|
+
out.push(code);
|
|
34
|
+
}
|
|
35
|
+
else if (code < 0x800) {
|
|
36
|
+
out.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
|
|
37
|
+
}
|
|
38
|
+
else if (code < 0x10000) {
|
|
39
|
+
out.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
out.push(0xf0 | (code >> 18), 0x80 | ((code >> 12) & 0x3f), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return Uint8Array.from(out);
|
|
46
|
+
}
|
|
47
|
+
/** The text of UTF-8 [bytes]; a malformed sequence becomes U+FFFD. */
|
|
48
|
+
function utf8Decode(bytes) {
|
|
49
|
+
let out = '';
|
|
50
|
+
let i = 0;
|
|
51
|
+
while (i < bytes.length) {
|
|
52
|
+
const b0 = bytes[i];
|
|
53
|
+
let code = 0xfffd;
|
|
54
|
+
let length = 1;
|
|
55
|
+
if (b0 < 0x80) {
|
|
56
|
+
code = b0;
|
|
57
|
+
}
|
|
58
|
+
else if (b0 >= 0xc2 && b0 < 0xe0 && i + 1 < bytes.length && (bytes[i + 1] & 0xc0) === 0x80) {
|
|
59
|
+
code = ((b0 & 0x1f) << 6) | (bytes[i + 1] & 0x3f);
|
|
60
|
+
length = 2;
|
|
61
|
+
}
|
|
62
|
+
else if (b0 >= 0xe0 && b0 < 0xf0 && i + 2 < bytes.length &&
|
|
63
|
+
(bytes[i + 1] & 0xc0) === 0x80 && (bytes[i + 2] & 0xc0) === 0x80) {
|
|
64
|
+
const value = ((b0 & 0x0f) << 12) | ((bytes[i + 1] & 0x3f) << 6) | (bytes[i + 2] & 0x3f);
|
|
65
|
+
if (value >= 0x800 && (value < 0xd800 || value > 0xdfff)) {
|
|
66
|
+
code = value;
|
|
67
|
+
length = 3;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else if (b0 >= 0xf0 && b0 < 0xf5 && i + 3 < bytes.length &&
|
|
71
|
+
(bytes[i + 1] & 0xc0) === 0x80 && (bytes[i + 2] & 0xc0) === 0x80 && (bytes[i + 3] & 0xc0) === 0x80) {
|
|
72
|
+
const value = ((b0 & 0x07) << 18) | ((bytes[i + 1] & 0x3f) << 12) | ((bytes[i + 2] & 0x3f) << 6) | (bytes[i + 3] & 0x3f);
|
|
73
|
+
if (value >= 0x10000 && value <= 0x10ffff) {
|
|
74
|
+
code = value;
|
|
75
|
+
length = 4;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (code > 0xffff) {
|
|
79
|
+
code -= 0x10000;
|
|
80
|
+
out += String.fromCharCode(0xd800 + (code >> 10), 0xdc00 + (code & 0x3ff));
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
out += String.fromCharCode(code);
|
|
84
|
+
}
|
|
85
|
+
i += length;
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
90
|
+
/** Base64url without padding: how every binary value travels. */
|
|
91
|
+
function base64UrlNoPadding(bytes) {
|
|
92
|
+
let out = '';
|
|
93
|
+
let i = 0;
|
|
94
|
+
for (; i + 2 < bytes.length; i += 3) {
|
|
95
|
+
const n = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
|
|
96
|
+
out += ALPHABET[(n >> 18) & 63] + ALPHABET[(n >> 12) & 63] + ALPHABET[(n >> 6) & 63] + ALPHABET[n & 63];
|
|
97
|
+
}
|
|
98
|
+
const rest = bytes.length - i;
|
|
99
|
+
if (rest === 1) {
|
|
100
|
+
const n = bytes[i] << 16;
|
|
101
|
+
out += ALPHABET[(n >> 18) & 63] + ALPHABET[(n >> 12) & 63];
|
|
102
|
+
}
|
|
103
|
+
else if (rest === 2) {
|
|
104
|
+
const n = (bytes[i] << 16) | (bytes[i + 1] << 8);
|
|
105
|
+
out += ALPHABET[(n >> 18) & 63] + ALPHABET[(n >> 12) & 63] + ALPHABET[(n >> 6) & 63];
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
/** The bytes of base64url [text], with or without padding; throws on any other character. */
|
|
110
|
+
function base64UrlDecode(text) {
|
|
111
|
+
const clean = text.replace(/=+$/, '');
|
|
112
|
+
const out = [];
|
|
113
|
+
let buffer = 0;
|
|
114
|
+
let bits = 0;
|
|
115
|
+
for (const char of clean) {
|
|
116
|
+
const value = ALPHABET.indexOf(char === '+' ? '-' : char === '/' ? '_' : char);
|
|
117
|
+
if (value < 0) {
|
|
118
|
+
throw new TypeError(`Not base64url: ${JSON.stringify(text)}`);
|
|
119
|
+
}
|
|
120
|
+
buffer = (buffer << 6) | value;
|
|
121
|
+
bits += 6;
|
|
122
|
+
if (bits >= 8) {
|
|
123
|
+
bits -= 8;
|
|
124
|
+
out.push((buffer >> bits) & 0xff);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return Uint8Array.from(out);
|
|
128
|
+
}
|
|
129
|
+
const K = new Uint32Array([
|
|
130
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
131
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
132
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
133
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
134
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
135
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
136
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
137
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
138
|
+
]);
|
|
139
|
+
/** The SHA-256 digest of [bytes]. */
|
|
140
|
+
function sha256(bytes) {
|
|
141
|
+
const h = new Uint32Array([
|
|
142
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
|
143
|
+
]);
|
|
144
|
+
const bitLength = bytes.length * 8;
|
|
145
|
+
const padded = new Uint8Array(((bytes.length + 9 + 63) >> 6) << 6);
|
|
146
|
+
padded.set(bytes);
|
|
147
|
+
padded[bytes.length] = 0x80;
|
|
148
|
+
const view = new DataView(padded.buffer);
|
|
149
|
+
view.setUint32(padded.length - 8, Math.floor(bitLength / 0x100000000));
|
|
150
|
+
view.setUint32(padded.length - 4, bitLength >>> 0);
|
|
151
|
+
const w = new Uint32Array(64);
|
|
152
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
153
|
+
for (let t = 0; t < 16; t++) {
|
|
154
|
+
w[t] = view.getUint32(offset + t * 4);
|
|
155
|
+
}
|
|
156
|
+
for (let t = 16; t < 64; t++) {
|
|
157
|
+
const x = w[t - 15];
|
|
158
|
+
const y = w[t - 2];
|
|
159
|
+
const s0 = ((x >>> 7) | (x << 25)) ^ ((x >>> 18) | (x << 14)) ^ (x >>> 3);
|
|
160
|
+
const s1 = ((y >>> 17) | (y << 15)) ^ ((y >>> 19) | (y << 13)) ^ (y >>> 10);
|
|
161
|
+
w[t] = (w[t - 16] + s0 + w[t - 7] + s1) >>> 0;
|
|
162
|
+
}
|
|
163
|
+
let a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], k = h[7];
|
|
164
|
+
for (let t = 0; t < 64; t++) {
|
|
165
|
+
const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
|
|
166
|
+
const ch = (e & f) ^ (~e & g);
|
|
167
|
+
const t1 = (k + S1 + ch + K[t] + w[t]) >>> 0;
|
|
168
|
+
const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
|
|
169
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
170
|
+
const t2 = (S0 + maj) >>> 0;
|
|
171
|
+
k = g;
|
|
172
|
+
g = f;
|
|
173
|
+
f = e;
|
|
174
|
+
e = (d + t1) >>> 0;
|
|
175
|
+
d = c;
|
|
176
|
+
c = b;
|
|
177
|
+
b = a;
|
|
178
|
+
a = (t1 + t2) >>> 0;
|
|
179
|
+
}
|
|
180
|
+
h[0] = (h[0] + a) >>> 0;
|
|
181
|
+
h[1] = (h[1] + b) >>> 0;
|
|
182
|
+
h[2] = (h[2] + c) >>> 0;
|
|
183
|
+
h[3] = (h[3] + d) >>> 0;
|
|
184
|
+
h[4] = (h[4] + e) >>> 0;
|
|
185
|
+
h[5] = (h[5] + f) >>> 0;
|
|
186
|
+
h[6] = (h[6] + g) >>> 0;
|
|
187
|
+
h[7] = (h[7] + k) >>> 0;
|
|
188
|
+
}
|
|
189
|
+
const out = new Uint8Array(32);
|
|
190
|
+
const outView = new DataView(out.buffer);
|
|
191
|
+
for (let i = 0; i < 8; i++) {
|
|
192
|
+
outView.setUint32(i * 4, h[i]);
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
/** Lowercase hex of [bytes]. */
|
|
197
|
+
function hex(bytes) {
|
|
198
|
+
let out = '';
|
|
199
|
+
for (const byte of bytes) {
|
|
200
|
+
out += (byte < 16 ? '0' : '') + byte.toString(16);
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
/** The SHA-256 of [text]'s UTF-8 bytes, in lowercase hex. */
|
|
205
|
+
function sha256Hex(text) {
|
|
206
|
+
return hex(sha256(utf8Encode(text)));
|
|
207
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SDK's native modules, loaded when first used -- so that tests, which replace them, never
|
|
3
|
+
* load React Native -- and their failures as [NativeKeyException]s.
|
|
4
|
+
*/
|
|
5
|
+
import { NativeKeyException } from '../errors';
|
|
6
|
+
import type { Spec as InstallationKeySpec } from '../native/NativeNecessaryInstallationKey';
|
|
7
|
+
import type { Spec as IntegritySpec } from '../native/NativeNecessaryIntegrity';
|
|
8
|
+
import type { Spec as SecureScreenSpec } from '../native/NativeNecessarySecureScreen';
|
|
9
|
+
import type { Spec as StepUpKeySpec } from '../native/NativeNecessaryStepUpKey';
|
|
10
|
+
import type { Spec as StorageSpec } from '../native/NativeNecessaryStorage';
|
|
11
|
+
export declare function installationKeyModule(): InstallationKeySpec | null;
|
|
12
|
+
export declare function integrityModule(): IntegritySpec | null;
|
|
13
|
+
export declare function stepUpKeyModule(): StepUpKeySpec | null;
|
|
14
|
+
export declare function secureScreenModule(): SecureScreenSpec | null;
|
|
15
|
+
export declare function storageModule(): StorageSpec | null;
|
|
16
|
+
/** The phone's platform, as the guide names it: `android` or `ios`. */
|
|
17
|
+
export declare function currentPlatform(): string;
|
|
18
|
+
/** A native module's refusal, with the code and the words it gave. */
|
|
19
|
+
export declare function nativeFailure(error: unknown, fallback: string): NativeKeyException;
|
|
20
|
+
export declare function isObject(value: unknown): value is Record<string, unknown>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The SDK's native modules, loaded when first used -- so that tests, which replace them, never
|
|
4
|
+
* load React Native -- and their failures as [NativeKeyException]s.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.installationKeyModule = installationKeyModule;
|
|
8
|
+
exports.integrityModule = integrityModule;
|
|
9
|
+
exports.stepUpKeyModule = stepUpKeyModule;
|
|
10
|
+
exports.secureScreenModule = secureScreenModule;
|
|
11
|
+
exports.storageModule = storageModule;
|
|
12
|
+
exports.currentPlatform = currentPlatform;
|
|
13
|
+
exports.nativeFailure = nativeFailure;
|
|
14
|
+
exports.isObject = isObject;
|
|
15
|
+
const errors_1 = require("../errors");
|
|
16
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
17
|
+
function installationKeyModule() {
|
|
18
|
+
return require('../native/NativeNecessaryInstallationKey').default ?? null;
|
|
19
|
+
}
|
|
20
|
+
function integrityModule() {
|
|
21
|
+
return require('../native/NativeNecessaryIntegrity').default ?? null;
|
|
22
|
+
}
|
|
23
|
+
function stepUpKeyModule() {
|
|
24
|
+
return require('../native/NativeNecessaryStepUpKey').default ?? null;
|
|
25
|
+
}
|
|
26
|
+
function secureScreenModule() {
|
|
27
|
+
return require('../native/NativeNecessarySecureScreen').default ?? null;
|
|
28
|
+
}
|
|
29
|
+
function storageModule() {
|
|
30
|
+
return require('../native/NativeNecessaryStorage').default ?? null;
|
|
31
|
+
}
|
|
32
|
+
/** The phone's platform, as the guide names it: `android` or `ios`. */
|
|
33
|
+
function currentPlatform() {
|
|
34
|
+
return require('react-native').Platform.OS;
|
|
35
|
+
}
|
|
36
|
+
/* eslint-enable @typescript-eslint/no-require-imports */
|
|
37
|
+
/** A native module's refusal, with the code and the words it gave. */
|
|
38
|
+
function nativeFailure(error, fallback) {
|
|
39
|
+
if (error instanceof errors_1.NativeKeyException) {
|
|
40
|
+
return error;
|
|
41
|
+
}
|
|
42
|
+
const failure = (typeof error === 'object' && error !== null ? error : {});
|
|
43
|
+
const code = typeof failure.code === 'string' && failure.code.length > 0 ? failure.code : 'NATIVE_ERROR';
|
|
44
|
+
const message = typeof failure.message === 'string' && failure.message.length > 0 ? failure.message : fallback;
|
|
45
|
+
return new errors_1.NativeKeyException(code, message);
|
|
46
|
+
}
|
|
47
|
+
function isObject(value) {
|
|
48
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
49
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isReleaseBuild = isReleaseBuild;
|
|
4
|
+
/** Whether this is a release build: React Native's `__DEV__` false. Tests in Node have none. */
|
|
5
|
+
function isReleaseBuild() {
|
|
6
|
+
return typeof __DEV__ !== 'undefined' && __DEV__ === false;
|
|
7
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A request's target as a proof names it: its path and its query, normalized exactly as the
|
|
3
|
+
* request is sent -- an escape of an unreserved character decoded, every other escape in upper
|
|
4
|
+
* case, a character a path or a query may not hold escaped as UTF-8, and dot segments removed.
|
|
5
|
+
* The transport sends the same normalized target, so what is signed is what is sent.
|
|
6
|
+
*/
|
|
7
|
+
export interface Target {
|
|
8
|
+
/** The path, without the query. */
|
|
9
|
+
path: string;
|
|
10
|
+
/** The query as sent, without its `?`; empty when there is none. */
|
|
11
|
+
query: string;
|
|
12
|
+
/** Whether the target has a `?` at all. */
|
|
13
|
+
hasQuery: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** [target] -- a path with its query, such as `/v1/items?limit=5` -- as a proof names it. */
|
|
16
|
+
export declare function parseTarget(target: string): Target;
|
|
17
|
+
/** [target] as the transport sends it: the same normalized path and query. */
|
|
18
|
+
export declare function normalizedTarget(target: string): string;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* A request's target as a proof names it: its path and its query, normalized exactly as the
|
|
4
|
+
* request is sent -- an escape of an unreserved character decoded, every other escape in upper
|
|
5
|
+
* case, a character a path or a query may not hold escaped as UTF-8, and dot segments removed.
|
|
6
|
+
* The transport sends the same normalized target, so what is signed is what is sent.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.parseTarget = parseTarget;
|
|
10
|
+
exports.normalizedTarget = normalizedTarget;
|
|
11
|
+
const UNRESERVED = /[A-Za-z0-9\-._~]/;
|
|
12
|
+
const SUB_DELIMS = "!$&'()*+,;=";
|
|
13
|
+
const PATH_EXTRA = ':@/';
|
|
14
|
+
const QUERY_EXTRA = ':@/?';
|
|
15
|
+
function allowed(char, extra) {
|
|
16
|
+
return UNRESERVED.test(char) || SUB_DELIMS.includes(char) || extra.includes(char);
|
|
17
|
+
}
|
|
18
|
+
function isHex(code) {
|
|
19
|
+
return (code >= 48 && code <= 57) || (code >= 65 && code <= 70) || (code >= 97 && code <= 102);
|
|
20
|
+
}
|
|
21
|
+
function escapeByte(byte) {
|
|
22
|
+
return '%' + (byte < 16 ? '0' : '') + byte.toString(16).toUpperCase();
|
|
23
|
+
}
|
|
24
|
+
function escapeCodePoint(code) {
|
|
25
|
+
const bytes = [];
|
|
26
|
+
if (code < 0x80) {
|
|
27
|
+
bytes.push(code);
|
|
28
|
+
}
|
|
29
|
+
else if (code < 0x800) {
|
|
30
|
+
bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
|
|
31
|
+
}
|
|
32
|
+
else if (code < 0x10000) {
|
|
33
|
+
bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
bytes.push(0xf0 | (code >> 18), 0x80 | ((code >> 12) & 0x3f), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
|
|
37
|
+
}
|
|
38
|
+
return bytes.map(escapeByte).join('');
|
|
39
|
+
}
|
|
40
|
+
function normalize(component, extra) {
|
|
41
|
+
let out = '';
|
|
42
|
+
for (let i = 0; i < component.length; i++) {
|
|
43
|
+
const char = component[i];
|
|
44
|
+
const code = component.charCodeAt(i);
|
|
45
|
+
if (code < 128 && allowed(char, extra)) {
|
|
46
|
+
out += char;
|
|
47
|
+
}
|
|
48
|
+
else if (char === '%') {
|
|
49
|
+
const valid = i + 2 < component.length &&
|
|
50
|
+
isHex(component.charCodeAt(i + 1)) && isHex(component.charCodeAt(i + 2));
|
|
51
|
+
if (!valid) {
|
|
52
|
+
out += '%25';
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const value = parseInt(component.substr(i + 1, 2), 16);
|
|
56
|
+
const decoded = String.fromCharCode(value);
|
|
57
|
+
if (UNRESERVED.test(decoded)) {
|
|
58
|
+
out += decoded;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
out += component.substr(i, 3).toUpperCase();
|
|
62
|
+
}
|
|
63
|
+
i += 2;
|
|
64
|
+
}
|
|
65
|
+
else if (code >= 0xd800 && code <= 0xdbff && i + 1 < component.length &&
|
|
66
|
+
component.charCodeAt(i + 1) >= 0xdc00 && component.charCodeAt(i + 1) <= 0xdfff) {
|
|
67
|
+
out += escapeCodePoint(0x10000 + ((code - 0xd800) << 10) + (component.charCodeAt(i + 1) - 0xdc00));
|
|
68
|
+
i++;
|
|
69
|
+
}
|
|
70
|
+
else if (code >= 0xd800 && code <= 0xdfff) {
|
|
71
|
+
out += escapeCodePoint(0xfffd);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
out += escapeCodePoint(code);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
function removeDotSegments(path) {
|
|
80
|
+
if (!(path.startsWith('.') || path.includes('/.'))) {
|
|
81
|
+
return path;
|
|
82
|
+
}
|
|
83
|
+
const output = [];
|
|
84
|
+
let appendSlash = false;
|
|
85
|
+
for (const segment of path.split('/')) {
|
|
86
|
+
appendSlash = false;
|
|
87
|
+
if (segment === '..') {
|
|
88
|
+
if (output.length > 0) {
|
|
89
|
+
output.pop();
|
|
90
|
+
if (output.length === 0) {
|
|
91
|
+
output.push('');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
appendSlash = true;
|
|
95
|
+
}
|
|
96
|
+
else if (segment === '.') {
|
|
97
|
+
appendSlash = true;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
output.push(segment);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (appendSlash) {
|
|
104
|
+
output.push('');
|
|
105
|
+
}
|
|
106
|
+
return output.join('/');
|
|
107
|
+
}
|
|
108
|
+
/** [target] -- a path with its query, such as `/v1/items?limit=5` -- as a proof names it. */
|
|
109
|
+
function parseTarget(target) {
|
|
110
|
+
let rest = target;
|
|
111
|
+
const hash = rest.indexOf('#');
|
|
112
|
+
if (hash >= 0) {
|
|
113
|
+
rest = rest.substring(0, hash);
|
|
114
|
+
}
|
|
115
|
+
const mark = rest.indexOf('?');
|
|
116
|
+
const rawPath = mark >= 0 ? rest.substring(0, mark) : rest;
|
|
117
|
+
const rawQuery = mark >= 0 ? rest.substring(mark + 1) : '';
|
|
118
|
+
return {
|
|
119
|
+
path: removeDotSegments(normalize(rawPath, PATH_EXTRA)),
|
|
120
|
+
query: normalize(rawQuery, QUERY_EXTRA),
|
|
121
|
+
hasQuery: mark >= 0,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** [target] as the transport sends it: the same normalized path and query. */
|
|
125
|
+
function normalizedTarget(target) {
|
|
126
|
+
const parsed = parseTarget(target);
|
|
127
|
+
return parsed.hasQuery ? `${parsed.path}?${parsed.query}` : parsed.path;
|
|
128
|
+
}
|
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.5",
|
|
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
|
+
}
|