@trustchex/react-native-sdk 1.490.1 → 1.492.0
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/lib/module/Shared/Libs/diagnosticPow.js +45 -0
- package/lib/module/Shared/Libs/sendDiagnosticReport.js +30 -18
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Shared/Libs/diagnosticPow.d.ts +22 -0
- package/lib/typescript/src/Shared/Libs/diagnosticPow.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts +0 -18
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/Shared/Libs/diagnosticPow.ts +54 -0
- package/src/Shared/Libs/sendDiagnosticReport.ts +24 -14
- package/src/version.ts +1 -1
- package/lib/module/Shared/Libs/diagnosticAuth.js +0 -36
- package/lib/typescript/src/Shared/Libs/diagnosticAuth.d.ts +0 -8
- package/lib/typescript/src/Shared/Libs/diagnosticAuth.d.ts.map +0 -1
- package/src/Shared/Libs/diagnosticAuth.ts +0 -47
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { sha256 } from '@noble/hashes/sha2';
|
|
4
|
+
import { Buffer } from 'buffer';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Proof-of-work solver for the demo-diagnostics authenticity flow.
|
|
8
|
+
*
|
|
9
|
+
* No secret ships in the SDK. To upload a demo report, the SDK first mints a
|
|
10
|
+
* server-signed challenge token (`POST …/diagnostics/challenge`) and then finds
|
|
11
|
+
* a `solution` nonce such that `SHA256(challenge.solution)` has at least
|
|
12
|
+
* `difficulty` leading zero bits (hashcash-style). The server re-verifies the
|
|
13
|
+
* token signature (server-only key) and the PoW. See web-app diagnostic-pow.ts.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const leadingZeroBits = bytes => {
|
|
17
|
+
let bits = 0;
|
|
18
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
19
|
+
const b = bytes[i];
|
|
20
|
+
if (b === 0) {
|
|
21
|
+
bits += 8;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
// bits before the highest set bit in this byte (b is 1..255)
|
|
25
|
+
bits += Math.clz32(b) - 24;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
return bits;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Solve the PoW for a challenge. Returns the solution nonce as a string.
|
|
33
|
+
* Bounded by `maxIterations` so a pathological difficulty can't hang the app;
|
|
34
|
+
* returns null if unsolved within the bound.
|
|
35
|
+
*/
|
|
36
|
+
export const solvePow = (challenge, difficulty, maxIterations = 8_000_000) => {
|
|
37
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
38
|
+
const solution = i.toString();
|
|
39
|
+
const digest = sha256(Buffer.from(`${challenge}.${solution}`, 'utf-8'));
|
|
40
|
+
if (leadingZeroBits(digest) >= difficulty) {
|
|
41
|
+
return solution;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
};
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
*
|
|
6
6
|
* This runs from the SDK's DEMO flow only, which is simulated client-side and
|
|
7
7
|
* has no real verification session or account — so there's no session key to
|
|
8
|
-
* encrypt with and
|
|
9
|
-
*
|
|
10
|
-
* in
|
|
11
|
-
*
|
|
8
|
+
* encrypt with and no credential to authenticate with. Instead, authenticity is
|
|
9
|
+
* proven with a proof-of-work: the SDK mints a server-signed challenge token
|
|
10
|
+
* (no secret ships in the SDK), solves it, and submits the solution with the
|
|
11
|
+
* report over HTTPS. The backend verifies the token (server-only key) + PoW and
|
|
12
|
+
* encrypts the PII fields at rest. Internal developers view and download the
|
|
13
|
+
* reports from a hidden dashboard page.
|
|
12
14
|
*
|
|
13
15
|
* Uploaded payload (one JSON body):
|
|
14
16
|
* - non-PII technical metadata (platform, sdkVersion, documentType,
|
|
@@ -17,11 +19,10 @@
|
|
|
17
19
|
* - `scanDataJson` — the scanned identity data (PII),
|
|
18
20
|
* - `images` — the captured document/face images (base64, PII).
|
|
19
21
|
*/
|
|
20
|
-
import 'react-native-get-random-values';
|
|
21
22
|
import { diagnostics } from "./diagnostics.js";
|
|
22
23
|
import { gatherDeviceDetails } from "./native-device-info.utils.js";
|
|
23
24
|
import { buildDiagnosticReport } from "./diagnosticReport.js";
|
|
24
|
-
import {
|
|
25
|
+
import { solvePow } from "./diagnosticPow.js";
|
|
25
26
|
import { logError } from "./debug.utils.js";
|
|
26
27
|
const mimeToType = mime => mime === 'png' ? 'image/png' : 'image/jpeg';
|
|
27
28
|
|
|
@@ -80,23 +81,34 @@ export const sendDiagnosticReport = async params => {
|
|
|
80
81
|
}))
|
|
81
82
|
};
|
|
82
83
|
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
84
|
+
// Authenticity via proof of work: mint a server-signed challenge, solve it,
|
|
85
|
+
// and submit the solution. No secret ships in the SDK.
|
|
86
|
+
const challengeRes = await fetch(`${baseUrl}/api/app/mobile/diagnostics/challenge`, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: {
|
|
89
|
+
'Content-Type': 'application/json'
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
if (!challengeRes.ok) {
|
|
93
|
+
return {
|
|
94
|
+
uploaded: false
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const challenge = await challengeRes.json();
|
|
98
|
+
const solution = solvePow(challenge.challenge, challenge.difficulty);
|
|
99
|
+
if (!solution) {
|
|
100
|
+
return {
|
|
101
|
+
uploaded: false
|
|
102
|
+
};
|
|
103
|
+
}
|
|
91
104
|
const response = await fetch(`${baseUrl}/api/app/mobile/diagnostics`, {
|
|
92
105
|
method: 'POST',
|
|
93
106
|
headers: {
|
|
94
107
|
'Content-Type': 'application/json',
|
|
95
|
-
'X-Trustchex-
|
|
96
|
-
'X-Trustchex-
|
|
97
|
-
'X-Trustchex-Signature': signature
|
|
108
|
+
'X-Trustchex-Pow-Token': challenge.token,
|
|
109
|
+
'X-Trustchex-Pow-Solution': solution
|
|
98
110
|
},
|
|
99
|
-
body:
|
|
111
|
+
body: JSON.stringify(payload)
|
|
100
112
|
});
|
|
101
113
|
return {
|
|
102
114
|
uploaded: response.ok
|
package/lib/module/version.js
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proof-of-work solver for the demo-diagnostics authenticity flow.
|
|
3
|
+
*
|
|
4
|
+
* No secret ships in the SDK. To upload a demo report, the SDK first mints a
|
|
5
|
+
* server-signed challenge token (`POST …/diagnostics/challenge`) and then finds
|
|
6
|
+
* a `solution` nonce such that `SHA256(challenge.solution)` has at least
|
|
7
|
+
* `difficulty` leading zero bits (hashcash-style). The server re-verifies the
|
|
8
|
+
* token signature (server-only key) and the PoW. See web-app diagnostic-pow.ts.
|
|
9
|
+
*/
|
|
10
|
+
export interface PowChallenge {
|
|
11
|
+
token: string;
|
|
12
|
+
challenge: string;
|
|
13
|
+
difficulty: number;
|
|
14
|
+
expiresAt: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Solve the PoW for a challenge. Returns the solution nonce as a string.
|
|
18
|
+
* Bounded by `maxIterations` so a pathological difficulty can't hang the app;
|
|
19
|
+
* returns null if unsolved within the bound.
|
|
20
|
+
*/
|
|
21
|
+
export declare const solvePow: (challenge: string, difficulty: number, maxIterations?: number) => string | null;
|
|
22
|
+
//# sourceMappingURL=diagnosticPow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnosticPow.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/diagnosticPow.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AAEH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAiBD;;;;GAIG;AACH,eAAO,MAAM,QAAQ,GACnB,WAAW,MAAM,EACjB,YAAY,MAAM,EAClB,sBAAyB,KACxB,MAAM,GAAG,IASX,CAAC"}
|
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Assembles and uploads the support diagnostic report to the backend.
|
|
3
|
-
*
|
|
4
|
-
* This runs from the SDK's DEMO flow only, which is simulated client-side and
|
|
5
|
-
* has no real verification session or account — so there's no session key to
|
|
6
|
-
* encrypt with and nothing to authenticate against. The report is POSTed
|
|
7
|
-
* unauthenticated to the global demo-diagnostics endpoint over HTTPS (encrypted
|
|
8
|
-
* in transit); the backend encrypts the PII fields at rest. Internal developers
|
|
9
|
-
* view and download the reports from a hidden dashboard page.
|
|
10
|
-
*
|
|
11
|
-
* Uploaded payload (one JSON body):
|
|
12
|
-
* - non-PII technical metadata (platform, sdkVersion, documentType,
|
|
13
|
-
* nfcErrorCode, paceFallbackReason, succeeded) for the list/filter,
|
|
14
|
-
* - `diagnosticsJson` — technical signals (device, camera, MRZ, NFC steps),
|
|
15
|
-
* - `scanDataJson` — the scanned identity data (PII),
|
|
16
|
-
* - `images` — the captured document/face images (base64, PII).
|
|
17
|
-
*/
|
|
18
|
-
import 'react-native-get-random-values';
|
|
19
1
|
import { type ScanDataForReport } from './diagnosticReport';
|
|
20
2
|
export interface DiagnosticImage {
|
|
21
3
|
/** Stable file name without extension, e.g. "mrz-side". */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sendDiagnosticReport.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/sendDiagnosticReport.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sendDiagnosticReport.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/sendDiagnosticReport.ts"],"names":[],"mappings":"AAqBA,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,oBAAoB,CAAC;AAI5B,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,iBAAiB,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,UAAU;IACzB,oDAAoD;IACpD,QAAQ,EAAE,OAAO,CAAC;CACnB;AAKD;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,oBAAoB,KAC3B,OAAO,CAAC,UAAU,CAsFpB,CAAC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "1.
|
|
1
|
+
export declare const SDK_VERSION = "1.492.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { sha256 } from '@noble/hashes/sha2';
|
|
2
|
+
import { Buffer } from 'buffer';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Proof-of-work solver for the demo-diagnostics authenticity flow.
|
|
6
|
+
*
|
|
7
|
+
* No secret ships in the SDK. To upload a demo report, the SDK first mints a
|
|
8
|
+
* server-signed challenge token (`POST …/diagnostics/challenge`) and then finds
|
|
9
|
+
* a `solution` nonce such that `SHA256(challenge.solution)` has at least
|
|
10
|
+
* `difficulty` leading zero bits (hashcash-style). The server re-verifies the
|
|
11
|
+
* token signature (server-only key) and the PoW. See web-app diagnostic-pow.ts.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface PowChallenge {
|
|
15
|
+
token: string;
|
|
16
|
+
challenge: string;
|
|
17
|
+
difficulty: number;
|
|
18
|
+
expiresAt: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const leadingZeroBits = (bytes: Uint8Array): number => {
|
|
22
|
+
let bits = 0;
|
|
23
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
24
|
+
const b = bytes[i];
|
|
25
|
+
if (b === 0) {
|
|
26
|
+
bits += 8;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
// bits before the highest set bit in this byte (b is 1..255)
|
|
30
|
+
bits += Math.clz32(b) - 24;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
return bits;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Solve the PoW for a challenge. Returns the solution nonce as a string.
|
|
38
|
+
* Bounded by `maxIterations` so a pathological difficulty can't hang the app;
|
|
39
|
+
* returns null if unsolved within the bound.
|
|
40
|
+
*/
|
|
41
|
+
export const solvePow = (
|
|
42
|
+
challenge: string,
|
|
43
|
+
difficulty: number,
|
|
44
|
+
maxIterations = 8_000_000
|
|
45
|
+
): string | null => {
|
|
46
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
47
|
+
const solution = i.toString();
|
|
48
|
+
const digest = sha256(Buffer.from(`${challenge}.${solution}`, 'utf-8'));
|
|
49
|
+
if (leadingZeroBits(digest) >= difficulty) {
|
|
50
|
+
return solution;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
};
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This runs from the SDK's DEMO flow only, which is simulated client-side and
|
|
5
5
|
* has no real verification session or account — so there's no session key to
|
|
6
|
-
* encrypt with and
|
|
7
|
-
*
|
|
8
|
-
* in
|
|
9
|
-
*
|
|
6
|
+
* encrypt with and no credential to authenticate with. Instead, authenticity is
|
|
7
|
+
* proven with a proof-of-work: the SDK mints a server-signed challenge token
|
|
8
|
+
* (no secret ships in the SDK), solves it, and submits the solution with the
|
|
9
|
+
* report over HTTPS. The backend verifies the token (server-only key) + PoW and
|
|
10
|
+
* encrypts the PII fields at rest. Internal developers view and download the
|
|
11
|
+
* reports from a hidden dashboard page.
|
|
10
12
|
*
|
|
11
13
|
* Uploaded payload (one JSON body):
|
|
12
14
|
* - non-PII technical metadata (platform, sdkVersion, documentType,
|
|
@@ -15,14 +17,13 @@
|
|
|
15
17
|
* - `scanDataJson` — the scanned identity data (PII),
|
|
16
18
|
* - `images` — the captured document/face images (base64, PII).
|
|
17
19
|
*/
|
|
18
|
-
import 'react-native-get-random-values';
|
|
19
20
|
import { diagnostics } from './diagnostics';
|
|
20
21
|
import { gatherDeviceDetails } from './native-device-info.utils';
|
|
21
22
|
import {
|
|
22
23
|
buildDiagnosticReport,
|
|
23
24
|
type ScanDataForReport,
|
|
24
25
|
} from './diagnosticReport';
|
|
25
|
-
import {
|
|
26
|
+
import { solvePow, type PowChallenge } from './diagnosticPow';
|
|
26
27
|
import { logError } from './debug.utils';
|
|
27
28
|
|
|
28
29
|
export interface DiagnosticImage {
|
|
@@ -110,20 +111,29 @@ export const sendDiagnosticReport = async (
|
|
|
110
111
|
})),
|
|
111
112
|
};
|
|
112
113
|
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
const
|
|
116
|
-
|
|
114
|
+
// Authenticity via proof of work: mint a server-signed challenge, solve it,
|
|
115
|
+
// and submit the solution. No secret ships in the SDK.
|
|
116
|
+
const challengeRes = await fetch(
|
|
117
|
+
`${baseUrl}/api/app/mobile/diagnostics/challenge`,
|
|
118
|
+
{ method: 'POST', headers: { 'Content-Type': 'application/json' } }
|
|
119
|
+
);
|
|
120
|
+
if (!challengeRes.ok) {
|
|
121
|
+
return { uploaded: false };
|
|
122
|
+
}
|
|
123
|
+
const challenge = (await challengeRes.json()) as PowChallenge;
|
|
124
|
+
const solution = solvePow(challenge.challenge, challenge.difficulty);
|
|
125
|
+
if (!solution) {
|
|
126
|
+
return { uploaded: false };
|
|
127
|
+
}
|
|
117
128
|
|
|
118
129
|
const response = await fetch(`${baseUrl}/api/app/mobile/diagnostics`, {
|
|
119
130
|
method: 'POST',
|
|
120
131
|
headers: {
|
|
121
132
|
'Content-Type': 'application/json',
|
|
122
|
-
'X-Trustchex-
|
|
123
|
-
'X-Trustchex-
|
|
124
|
-
'X-Trustchex-Signature': signature,
|
|
133
|
+
'X-Trustchex-Pow-Token': challenge.token,
|
|
134
|
+
'X-Trustchex-Pow-Solution': solution,
|
|
125
135
|
},
|
|
126
|
-
body:
|
|
136
|
+
body: JSON.stringify(payload),
|
|
127
137
|
});
|
|
128
138
|
|
|
129
139
|
return { uploaded: response.ok };
|
package/src/version.ts
CHANGED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { Buffer } from 'buffer';
|
|
4
|
-
import { hmac } from '@noble/hashes/hmac';
|
|
5
|
-
import { sha256 } from '@noble/hashes/sha2';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Authenticity signing for the demo-diagnostics upload.
|
|
9
|
-
*
|
|
10
|
-
* Demo reports have no session/account to authenticate against, so the SDK signs
|
|
11
|
-
* each upload with HMAC-SHA256 over `timestamp.nonce.rawBody` using a shared
|
|
12
|
-
* secret. The backend recomputes and rejects mismatches and stale timestamps
|
|
13
|
-
* (replay). The secret ships in the SDK, so it's an obfuscation barrier (paired
|
|
14
|
-
* with server rate limiting), not a true secret — rotate via the env var on both
|
|
15
|
-
* sides. MUST stay byte-for-byte in sync with web-app/src/lib/diagnostic-hmac.ts.
|
|
16
|
-
*/
|
|
17
|
-
const SECRET = process.env.TRUSTCHEX_DIAGNOSTICS_SECRET ?? 'tcx-diag-v1-2f9c4a7e8b1d4f63a05e9c7b3d61f8a2';
|
|
18
|
-
const randomNonce = () => {
|
|
19
|
-
// 16 random bytes as hex. crypto.getRandomValues is polyfilled by
|
|
20
|
-
// react-native-get-random-values (already imported in crypto.utils).
|
|
21
|
-
const bytes = new Uint8Array(16);
|
|
22
|
-
globalThis.crypto.getRandomValues(bytes);
|
|
23
|
-
return Buffer.from(bytes).toString('hex');
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
/** Sign a raw JSON body for the demo-diagnostics endpoint. */
|
|
27
|
-
export const signDiagnosticBody = rawBody => {
|
|
28
|
-
const timestamp = Date.now().toString();
|
|
29
|
-
const nonce = randomNonce();
|
|
30
|
-
const mac = hmac(sha256, Buffer.from(SECRET, 'utf-8'), Buffer.from(`${timestamp}.${nonce}.${rawBody}`, 'utf-8'));
|
|
31
|
-
return {
|
|
32
|
-
timestamp,
|
|
33
|
-
nonce,
|
|
34
|
-
signature: Buffer.from(mac).toString('hex')
|
|
35
|
-
};
|
|
36
|
-
};
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export interface DiagnosticSignature {
|
|
2
|
-
timestamp: string;
|
|
3
|
-
nonce: string;
|
|
4
|
-
signature: string;
|
|
5
|
-
}
|
|
6
|
-
/** Sign a raw JSON body for the demo-diagnostics endpoint. */
|
|
7
|
-
export declare const signDiagnosticBody: (rawBody: string) => DiagnosticSignature;
|
|
8
|
-
//# sourceMappingURL=diagnosticAuth.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"diagnosticAuth.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/diagnosticAuth.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAUD,8DAA8D;AAC9D,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,KAAG,mBAapD,CAAC"}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { Buffer } from 'buffer';
|
|
2
|
-
import { hmac } from '@noble/hashes/hmac';
|
|
3
|
-
import { sha256 } from '@noble/hashes/sha2';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Authenticity signing for the demo-diagnostics upload.
|
|
7
|
-
*
|
|
8
|
-
* Demo reports have no session/account to authenticate against, so the SDK signs
|
|
9
|
-
* each upload with HMAC-SHA256 over `timestamp.nonce.rawBody` using a shared
|
|
10
|
-
* secret. The backend recomputes and rejects mismatches and stale timestamps
|
|
11
|
-
* (replay). The secret ships in the SDK, so it's an obfuscation barrier (paired
|
|
12
|
-
* with server rate limiting), not a true secret — rotate via the env var on both
|
|
13
|
-
* sides. MUST stay byte-for-byte in sync with web-app/src/lib/diagnostic-hmac.ts.
|
|
14
|
-
*/
|
|
15
|
-
const SECRET =
|
|
16
|
-
process.env.TRUSTCHEX_DIAGNOSTICS_SECRET ??
|
|
17
|
-
'tcx-diag-v1-2f9c4a7e8b1d4f63a05e9c7b3d61f8a2';
|
|
18
|
-
|
|
19
|
-
export interface DiagnosticSignature {
|
|
20
|
-
timestamp: string;
|
|
21
|
-
nonce: string;
|
|
22
|
-
signature: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const randomNonce = (): string => {
|
|
26
|
-
// 16 random bytes as hex. crypto.getRandomValues is polyfilled by
|
|
27
|
-
// react-native-get-random-values (already imported in crypto.utils).
|
|
28
|
-
const bytes = new Uint8Array(16);
|
|
29
|
-
globalThis.crypto.getRandomValues(bytes);
|
|
30
|
-
return Buffer.from(bytes).toString('hex');
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
/** Sign a raw JSON body for the demo-diagnostics endpoint. */
|
|
34
|
-
export const signDiagnosticBody = (rawBody: string): DiagnosticSignature => {
|
|
35
|
-
const timestamp = Date.now().toString();
|
|
36
|
-
const nonce = randomNonce();
|
|
37
|
-
const mac = hmac(
|
|
38
|
-
sha256,
|
|
39
|
-
Buffer.from(SECRET, 'utf-8'),
|
|
40
|
-
Buffer.from(`${timestamp}.${nonce}.${rawBody}`, 'utf-8')
|
|
41
|
-
);
|
|
42
|
-
return {
|
|
43
|
-
timestamp,
|
|
44
|
-
nonce,
|
|
45
|
-
signature: Buffer.from(mac).toString('hex'),
|
|
46
|
-
};
|
|
47
|
-
};
|