@signwho/verify 0.1.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/LICENSE +21 -0
- package/README.md +48 -0
- package/dist/chunk-HSRXDJDV.js +38 -0
- package/dist/chunk-HSRXDJDV.js.map +1 -0
- package/dist/crypto.cjs +46 -0
- package/dist/crypto.cjs.map +1 -0
- package/dist/crypto.d.cts +19 -0
- package/dist/crypto.d.ts +19 -0
- package/dist/crypto.js +3 -0
- package/dist/crypto.js.map +1 -0
- package/dist/index.cjs +481 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +68 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +401 -0
- package/dist/index.js.map +1 -0
- package/dist/sample.cjs +67 -0
- package/dist/sample.cjs.map +1 -0
- package/dist/sample.d.cts +6 -0
- package/dist/sample.d.ts +6 -0
- package/dist/sample.js +64 -0
- package/dist/sample.js.map +1 -0
- package/dist/verify-checks-BTlBVglW.d.cts +68 -0
- package/dist/verify-checks-BTlBVglW.d.ts +68 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Particular Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @signwho/verify
|
|
2
|
+
|
|
3
|
+
Verify a [SignWho](https://signwho.com) signature package — the signed PDF plus its
|
|
4
|
+
`evidence.json` — without SignWho: the same four checks as <https://signwho.com/verify>
|
|
5
|
+
(file integrity, the WebAuthn assertion, the RFC 3161 timestamp chained to FreeTSA's CA as of
|
|
6
|
+
its `genTime`, the triangle hash), the honest assurance tier, and the dispute-summary text.
|
|
7
|
+
Pure, framework-free, no network.
|
|
8
|
+
|
|
9
|
+
## Use in Node
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { readFileSync } from 'node:fs'
|
|
13
|
+
import { verifyPackage, buildDisputeSummary } from '@signwho/verify'
|
|
14
|
+
|
|
15
|
+
const pdf = new Uint8Array(readFileSync('agreement-signed.pdf'))
|
|
16
|
+
const evidence = JSON.parse(readFileSync('SignWho-Evidence-agreement.json', 'utf8'))
|
|
17
|
+
|
|
18
|
+
const report = await verifyPackage(pdf, evidence)
|
|
19
|
+
for (const c of report.checks) console.log(c.id.padEnd(10), c.status.toUpperCase().padEnd(5), c.detail)
|
|
20
|
+
console.log('tier:', report.tier, '| all good:', report.allGood)
|
|
21
|
+
|
|
22
|
+
const summary = buildDisputeSummary({ report, generatedAt: new Date(), pdfName: 'agreement-signed.pdf', evidenceFileName: 'SignWho-Evidence-agreement.json', evidenceSource: 'standalone' })
|
|
23
|
+
console.log(summary.text) // the plain-text summary a lawyer can read; summary.filename names it
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Node 20 or newer (WebCrypto is global). CommonJS works too: `const { verifyPackage } = require('@signwho/verify')`.
|
|
27
|
+
A known-good package to try it on ships as `@signwho/verify/sample` (`SAMPLE_PDF_B64`,
|
|
28
|
+
`SAMPLE_EVIDENCE`).
|
|
29
|
+
The signing formulas alone (SHA-256 helpers, the triangle hash, the WebAuthn challenge) are
|
|
30
|
+
`@signwho/verify/crypto`, with none of the verifier's dependencies — for code that signs.
|
|
31
|
+
|
|
32
|
+
## What a report says
|
|
33
|
+
|
|
34
|
+
`report.checks` has one entry per check with `status` `pass` / `fail` / `skip` / `info` and a
|
|
35
|
+
human `detail`; `report.tier` is `identity` (selfie + device assertion + timestamp),
|
|
36
|
+
`device` (assertion + timestamp, no selfie) or `floor` (document + timestamp only);
|
|
37
|
+
`report.allGood` is true only when nothing failed. The evidence copy embedded in the PDF can be
|
|
38
|
+
read with `extractEmbeddedEvidence(pdf)` — it verifies everything except file integrity, since a
|
|
39
|
+
file cannot carry its own hash; use the standalone evidence file for that.
|
|
40
|
+
|
|
41
|
+
## Without JavaScript
|
|
42
|
+
|
|
43
|
+
`docs/third-party-verifier.py` in the SignWho repository re-runs the same checks in Python with
|
|
44
|
+
nothing from SignWho, for anyone who would rather not run JS.
|
|
45
|
+
|
|
46
|
+
## License
|
|
47
|
+
|
|
48
|
+
MIT © Particular Ltd.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// src/signwho-crypto.ts
|
|
2
|
+
async function hashData(data) {
|
|
3
|
+
const encoded = new TextEncoder().encode(data);
|
|
4
|
+
const buffer = await crypto.subtle.digest("SHA-256", encoded);
|
|
5
|
+
return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
6
|
+
}
|
|
7
|
+
async function hashArrayBuffer(buffer) {
|
|
8
|
+
const hash = await crypto.subtle.digest("SHA-256", buffer);
|
|
9
|
+
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
10
|
+
}
|
|
11
|
+
function uint8ToBase64(bytes) {
|
|
12
|
+
let binary = "";
|
|
13
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
14
|
+
return btoa(binary);
|
|
15
|
+
}
|
|
16
|
+
function bytesToBase64Url(bytes) {
|
|
17
|
+
return uint8ToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
18
|
+
}
|
|
19
|
+
function base64UrlToBytes(s) {
|
|
20
|
+
const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
|
|
21
|
+
const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + pad;
|
|
22
|
+
const bin = atob(b64);
|
|
23
|
+
const out = new Uint8Array(bin.length);
|
|
24
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
async function computeTriangleHash(selfieHash, biometricTimestamp, documentHash) {
|
|
28
|
+
return hashData(`${selfieHash}||${biometricTimestamp}||${documentHash}`);
|
|
29
|
+
}
|
|
30
|
+
async function computeWebAuthnChallenge(documentHash, challengeNonce) {
|
|
31
|
+
const input = new TextEncoder().encode(`${documentHash}||${challengeNonce}`);
|
|
32
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", input));
|
|
33
|
+
return bytesToBase64Url(digest);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { base64UrlToBytes, bytesToBase64Url, computeTriangleHash, computeWebAuthnChallenge, hashArrayBuffer, hashData, uint8ToBase64 };
|
|
37
|
+
//# sourceMappingURL=chunk-HSRXDJDV.js.map
|
|
38
|
+
//# sourceMappingURL=chunk-HSRXDJDV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/signwho-crypto.ts"],"names":[],"mappings":";AAeA,eAAsB,SAAS,IAAA,EAA+B;AAC5D,EAAA,MAAM,OAAA,GAAU,IAAI,WAAA,EAAY,CAAE,OAAO,IAAI,CAAA;AAC7C,EAAA,MAAM,SAAS,MAAM,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,WAAW,OAAO,CAAA;AAC5D,EAAA,OAAO,MAAM,IAAA,CAAK,IAAI,WAAW,MAAM,CAAC,EAAE,GAAA,CAAI,CAAA,CAAA,KAAK,EAAE,QAAA,CAAS,EAAE,EAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAC,CAAA,CAAE,KAAK,EAAE,CAAA;AAC7F;AAIA,eAAsB,gBAAgB,MAAA,EAAsC;AAC1E,EAAA,MAAM,OAAO,MAAM,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,WAAW,MAAM,CAAA;AACzD,EAAA,OAAO,MAAM,IAAA,CAAK,IAAI,WAAW,IAAI,CAAC,EAAE,GAAA,CAAI,CAAA,CAAA,KAAK,EAAE,QAAA,CAAS,EAAE,EAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAC,CAAA,CAAE,KAAK,EAAE,CAAA;AAC3F;AAGO,SAAS,cAAc,KAAA,EAA2B;AACvD,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,MAAA,EAAQ,CAAA,EAAA,EAAK,MAAA,IAAU,MAAA,CAAO,YAAA,CAAa,KAAA,CAAM,CAAC,CAAC,CAAA;AAC7E,EAAA,OAAO,KAAK,MAAM,CAAA;AACpB;AAGO,SAAS,iBAAiB,KAAA,EAA2B;AAC1D,EAAA,OAAO,aAAA,CAAc,KAAK,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,OAAO,EAAE,CAAA;AACvF;AAGO,SAAS,iBAAiB,CAAA,EAAuB;AACtD,EAAA,MAAM,GAAA,GAAM,CAAA,CAAE,MAAA,GAAS,CAAA,KAAM,CAAA,GAAI,EAAA,GAAK,GAAA,CAAI,MAAA,CAAO,CAAA,GAAK,CAAA,CAAE,MAAA,GAAS,CAAE,CAAA;AACnE,EAAA,MAAM,GAAA,GAAM,EAAE,OAAA,CAAQ,IAAA,EAAM,GAAG,CAAA,CAAE,OAAA,CAAQ,IAAA,EAAM,GAAG,CAAA,GAAI,GAAA;AACtD,EAAA,MAAM,GAAA,GAAM,KAAK,GAAG,CAAA;AACpB,EAAA,MAAM,GAAA,GAAM,IAAI,UAAA,CAAW,GAAA,CAAI,MAAM,CAAA;AACrC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,GAAA,CAAI,MAAA,EAAQ,CAAA,EAAA,EAAK,GAAA,CAAI,CAAC,CAAA,GAAI,GAAA,CAAI,UAAA,CAAW,CAAC,CAAA;AAC9D,EAAA,OAAO,GAAA;AACT;AAIA,eAAsB,mBAAA,CACpB,UAAA,EACA,kBAAA,EACA,YAAA,EACiB;AACjB,EAAA,OAAO,SAAS,CAAA,EAAG,UAAU,KAAK,kBAAkB,CAAA,EAAA,EAAK,YAAY,CAAA,CAAE,CAAA;AACzE;AAIA,eAAsB,wBAAA,CACpB,cACA,cAAA,EACiB;AACjB,EAAA,MAAM,KAAA,GAAQ,IAAI,WAAA,EAAY,CAAE,OAAO,CAAA,EAAG,YAAY,CAAA,EAAA,EAAK,cAAc,CAAA,CAAE,CAAA;AAC3E,EAAA,MAAM,MAAA,GAAS,IAAI,UAAA,CAAW,MAAM,OAAO,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,KAAK,CAAC,CAAA;AAC1E,EAAA,OAAO,iBAAiB,MAAM,CAAA;AAChC","file":"chunk-HSRXDJDV.js","sourcesContent":["// ============================================================================\n// SignWho shared crypto — SINGLE SOURCE OF TRUTH\n// ============================================================================\n// These functions are imported by BOTH the signer (App.tsx) and the public\n// verifier (verify.tsx). Keeping one copy is deliberate: if the verifier ever\n// re-implemented a formula and drifted from the signer, it would produce false\n// verdicts — catastrophic for a \"verify it yourself\" product.\n//\n// hashData / hashArrayBuffer / uint8ToBase64 / bytesToBase64Url were RELOCATED\n// verbatim from App.tsx (zero logic change — see signwho-crypto.test.ts for the\n// equivalence check against the old inline implementations). base64UrlToBytes,\n// computeTriangleHash and computeWebAuthnChallenge formalize logic that was\n// previously inline in App.tsx, byte-for-byte identical.\n\n/** SHA-256 of a UTF-8 string → lowercase hex. (Was App.tsx `hashData`.) */\nexport async function hashData(data: string): Promise<string> {\n const encoded = new TextEncoder().encode(data)\n const buffer = await crypto.subtle.digest('SHA-256', encoded)\n return Array.from(new Uint8Array(buffer)).map(b => b.toString(16).padStart(2, '0')).join('')\n}\n\n/** SHA-256 of raw bytes → lowercase hex. (Was App.tsx `hashArrayBuffer`.) Used for\n * documentHash (original upload) AND signedPdfHash / verifier file-integrity. */\nexport async function hashArrayBuffer(buffer: ArrayBuffer): Promise<string> {\n const hash = await crypto.subtle.digest('SHA-256', buffer)\n return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('')\n}\n\n/** Stack-safe base64 encode for large Uint8Arrays. (Was App.tsx `uint8ToBase64`.) */\nexport function uint8ToBase64(bytes: Uint8Array): string {\n let binary = ''\n for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i])\n return btoa(binary)\n}\n\n/** base64url (no padding) of bytes. (Was App.tsx `bytesToBase64Url`.) */\nexport function bytesToBase64Url(bytes: Uint8Array): string {\n return uint8ToBase64(bytes).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\n/** Decode base64url (padded or not) → bytes. */\nexport function base64UrlToBytes(s: string): Uint8Array {\n const pad = s.length % 4 === 0 ? '' : '='.repeat(4 - (s.length % 4))\n const b64 = s.replace(/-/g, '+').replace(/_/g, '/') + pad\n const bin = atob(b64)\n const out = new Uint8Array(bin.length)\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)\n return out\n}\n\n/** Triangle Authentication binding: SHA-256(\"selfieHash||biometricTimestamp||documentHash\") → hex.\n * Order and the literal \"||\" separators must match the signer exactly. */\nexport async function computeTriangleHash(\n selfieHash: string,\n biometricTimestamp: number,\n documentHash: string,\n): Promise<string> {\n return hashData(`${selfieHash}||${biometricTimestamp}||${documentHash}`)\n}\n\n/** Document-bound WebAuthn challenge: base64url(SHA-256(\"documentHash||challengeNonce\")).\n * NOTE the literal \"||\" separator — the challenge is NOT a bare concatenation. */\nexport async function computeWebAuthnChallenge(\n documentHash: string,\n challengeNonce: string,\n): Promise<string> {\n const input = new TextEncoder().encode(`${documentHash}||${challengeNonce}`)\n const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', input))\n return bytesToBase64Url(digest)\n}\n"]}
|
package/dist/crypto.cjs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/signwho-crypto.ts
|
|
4
|
+
async function hashData(data) {
|
|
5
|
+
const encoded = new TextEncoder().encode(data);
|
|
6
|
+
const buffer = await crypto.subtle.digest("SHA-256", encoded);
|
|
7
|
+
return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
8
|
+
}
|
|
9
|
+
async function hashArrayBuffer(buffer) {
|
|
10
|
+
const hash = await crypto.subtle.digest("SHA-256", buffer);
|
|
11
|
+
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
12
|
+
}
|
|
13
|
+
function uint8ToBase64(bytes) {
|
|
14
|
+
let binary = "";
|
|
15
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
16
|
+
return btoa(binary);
|
|
17
|
+
}
|
|
18
|
+
function bytesToBase64Url(bytes) {
|
|
19
|
+
return uint8ToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
20
|
+
}
|
|
21
|
+
function base64UrlToBytes(s) {
|
|
22
|
+
const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
|
|
23
|
+
const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + pad;
|
|
24
|
+
const bin = atob(b64);
|
|
25
|
+
const out = new Uint8Array(bin.length);
|
|
26
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
async function computeTriangleHash(selfieHash, biometricTimestamp, documentHash) {
|
|
30
|
+
return hashData(`${selfieHash}||${biometricTimestamp}||${documentHash}`);
|
|
31
|
+
}
|
|
32
|
+
async function computeWebAuthnChallenge(documentHash, challengeNonce) {
|
|
33
|
+
const input = new TextEncoder().encode(`${documentHash}||${challengeNonce}`);
|
|
34
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", input));
|
|
35
|
+
return bytesToBase64Url(digest);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
exports.base64UrlToBytes = base64UrlToBytes;
|
|
39
|
+
exports.bytesToBase64Url = bytesToBase64Url;
|
|
40
|
+
exports.computeTriangleHash = computeTriangleHash;
|
|
41
|
+
exports.computeWebAuthnChallenge = computeWebAuthnChallenge;
|
|
42
|
+
exports.hashArrayBuffer = hashArrayBuffer;
|
|
43
|
+
exports.hashData = hashData;
|
|
44
|
+
exports.uint8ToBase64 = uint8ToBase64;
|
|
45
|
+
//# sourceMappingURL=crypto.cjs.map
|
|
46
|
+
//# sourceMappingURL=crypto.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/signwho-crypto.ts"],"names":[],"mappings":";;;AAeA,eAAsB,SAAS,IAAA,EAA+B;AAC5D,EAAA,MAAM,OAAA,GAAU,IAAI,WAAA,EAAY,CAAE,OAAO,IAAI,CAAA;AAC7C,EAAA,MAAM,SAAS,MAAM,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,WAAW,OAAO,CAAA;AAC5D,EAAA,OAAO,MAAM,IAAA,CAAK,IAAI,WAAW,MAAM,CAAC,EAAE,GAAA,CAAI,CAAA,CAAA,KAAK,EAAE,QAAA,CAAS,EAAE,EAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAC,CAAA,CAAE,KAAK,EAAE,CAAA;AAC7F;AAIA,eAAsB,gBAAgB,MAAA,EAAsC;AAC1E,EAAA,MAAM,OAAO,MAAM,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,WAAW,MAAM,CAAA;AACzD,EAAA,OAAO,MAAM,IAAA,CAAK,IAAI,WAAW,IAAI,CAAC,EAAE,GAAA,CAAI,CAAA,CAAA,KAAK,EAAE,QAAA,CAAS,EAAE,EAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAC,CAAA,CAAE,KAAK,EAAE,CAAA;AAC3F;AAGO,SAAS,cAAc,KAAA,EAA2B;AACvD,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,MAAA,EAAQ,CAAA,EAAA,EAAK,MAAA,IAAU,MAAA,CAAO,YAAA,CAAa,KAAA,CAAM,CAAC,CAAC,CAAA;AAC7E,EAAA,OAAO,KAAK,MAAM,CAAA;AACpB;AAGO,SAAS,iBAAiB,KAAA,EAA2B;AAC1D,EAAA,OAAO,aAAA,CAAc,KAAK,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,OAAO,EAAE,CAAA;AACvF;AAGO,SAAS,iBAAiB,CAAA,EAAuB;AACtD,EAAA,MAAM,GAAA,GAAM,CAAA,CAAE,MAAA,GAAS,CAAA,KAAM,CAAA,GAAI,EAAA,GAAK,GAAA,CAAI,MAAA,CAAO,CAAA,GAAK,CAAA,CAAE,MAAA,GAAS,CAAE,CAAA;AACnE,EAAA,MAAM,GAAA,GAAM,EAAE,OAAA,CAAQ,IAAA,EAAM,GAAG,CAAA,CAAE,OAAA,CAAQ,IAAA,EAAM,GAAG,CAAA,GAAI,GAAA;AACtD,EAAA,MAAM,GAAA,GAAM,KAAK,GAAG,CAAA;AACpB,EAAA,MAAM,GAAA,GAAM,IAAI,UAAA,CAAW,GAAA,CAAI,MAAM,CAAA;AACrC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,GAAA,CAAI,MAAA,EAAQ,CAAA,EAAA,EAAK,GAAA,CAAI,CAAC,CAAA,GAAI,GAAA,CAAI,UAAA,CAAW,CAAC,CAAA;AAC9D,EAAA,OAAO,GAAA;AACT;AAIA,eAAsB,mBAAA,CACpB,UAAA,EACA,kBAAA,EACA,YAAA,EACiB;AACjB,EAAA,OAAO,SAAS,CAAA,EAAG,UAAU,KAAK,kBAAkB,CAAA,EAAA,EAAK,YAAY,CAAA,CAAE,CAAA;AACzE;AAIA,eAAsB,wBAAA,CACpB,cACA,cAAA,EACiB;AACjB,EAAA,MAAM,KAAA,GAAQ,IAAI,WAAA,EAAY,CAAE,OAAO,CAAA,EAAG,YAAY,CAAA,EAAA,EAAK,cAAc,CAAA,CAAE,CAAA;AAC3E,EAAA,MAAM,MAAA,GAAS,IAAI,UAAA,CAAW,MAAM,OAAO,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,KAAK,CAAC,CAAA;AAC1E,EAAA,OAAO,iBAAiB,MAAM,CAAA;AAChC","file":"crypto.cjs","sourcesContent":["// ============================================================================\n// SignWho shared crypto — SINGLE SOURCE OF TRUTH\n// ============================================================================\n// These functions are imported by BOTH the signer (App.tsx) and the public\n// verifier (verify.tsx). Keeping one copy is deliberate: if the verifier ever\n// re-implemented a formula and drifted from the signer, it would produce false\n// verdicts — catastrophic for a \"verify it yourself\" product.\n//\n// hashData / hashArrayBuffer / uint8ToBase64 / bytesToBase64Url were RELOCATED\n// verbatim from App.tsx (zero logic change — see signwho-crypto.test.ts for the\n// equivalence check against the old inline implementations). base64UrlToBytes,\n// computeTriangleHash and computeWebAuthnChallenge formalize logic that was\n// previously inline in App.tsx, byte-for-byte identical.\n\n/** SHA-256 of a UTF-8 string → lowercase hex. (Was App.tsx `hashData`.) */\nexport async function hashData(data: string): Promise<string> {\n const encoded = new TextEncoder().encode(data)\n const buffer = await crypto.subtle.digest('SHA-256', encoded)\n return Array.from(new Uint8Array(buffer)).map(b => b.toString(16).padStart(2, '0')).join('')\n}\n\n/** SHA-256 of raw bytes → lowercase hex. (Was App.tsx `hashArrayBuffer`.) Used for\n * documentHash (original upload) AND signedPdfHash / verifier file-integrity. */\nexport async function hashArrayBuffer(buffer: ArrayBuffer): Promise<string> {\n const hash = await crypto.subtle.digest('SHA-256', buffer)\n return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('')\n}\n\n/** Stack-safe base64 encode for large Uint8Arrays. (Was App.tsx `uint8ToBase64`.) */\nexport function uint8ToBase64(bytes: Uint8Array): string {\n let binary = ''\n for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i])\n return btoa(binary)\n}\n\n/** base64url (no padding) of bytes. (Was App.tsx `bytesToBase64Url`.) */\nexport function bytesToBase64Url(bytes: Uint8Array): string {\n return uint8ToBase64(bytes).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\n/** Decode base64url (padded or not) → bytes. */\nexport function base64UrlToBytes(s: string): Uint8Array {\n const pad = s.length % 4 === 0 ? '' : '='.repeat(4 - (s.length % 4))\n const b64 = s.replace(/-/g, '+').replace(/_/g, '/') + pad\n const bin = atob(b64)\n const out = new Uint8Array(bin.length)\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)\n return out\n}\n\n/** Triangle Authentication binding: SHA-256(\"selfieHash||biometricTimestamp||documentHash\") → hex.\n * Order and the literal \"||\" separators must match the signer exactly. */\nexport async function computeTriangleHash(\n selfieHash: string,\n biometricTimestamp: number,\n documentHash: string,\n): Promise<string> {\n return hashData(`${selfieHash}||${biometricTimestamp}||${documentHash}`)\n}\n\n/** Document-bound WebAuthn challenge: base64url(SHA-256(\"documentHash||challengeNonce\")).\n * NOTE the literal \"||\" separator — the challenge is NOT a bare concatenation. */\nexport async function computeWebAuthnChallenge(\n documentHash: string,\n challengeNonce: string,\n): Promise<string> {\n const input = new TextEncoder().encode(`${documentHash}||${challengeNonce}`)\n const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', input))\n return bytesToBase64Url(digest)\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** SHA-256 of a UTF-8 string → lowercase hex. (Was App.tsx `hashData`.) */
|
|
2
|
+
declare function hashData(data: string): Promise<string>;
|
|
3
|
+
/** SHA-256 of raw bytes → lowercase hex. (Was App.tsx `hashArrayBuffer`.) Used for
|
|
4
|
+
* documentHash (original upload) AND signedPdfHash / verifier file-integrity. */
|
|
5
|
+
declare function hashArrayBuffer(buffer: ArrayBuffer): Promise<string>;
|
|
6
|
+
/** Stack-safe base64 encode for large Uint8Arrays. (Was App.tsx `uint8ToBase64`.) */
|
|
7
|
+
declare function uint8ToBase64(bytes: Uint8Array): string;
|
|
8
|
+
/** base64url (no padding) of bytes. (Was App.tsx `bytesToBase64Url`.) */
|
|
9
|
+
declare function bytesToBase64Url(bytes: Uint8Array): string;
|
|
10
|
+
/** Decode base64url (padded or not) → bytes. */
|
|
11
|
+
declare function base64UrlToBytes(s: string): Uint8Array;
|
|
12
|
+
/** Triangle Authentication binding: SHA-256("selfieHash||biometricTimestamp||documentHash") → hex.
|
|
13
|
+
* Order and the literal "||" separators must match the signer exactly. */
|
|
14
|
+
declare function computeTriangleHash(selfieHash: string, biometricTimestamp: number, documentHash: string): Promise<string>;
|
|
15
|
+
/** Document-bound WebAuthn challenge: base64url(SHA-256("documentHash||challengeNonce")).
|
|
16
|
+
* NOTE the literal "||" separator — the challenge is NOT a bare concatenation. */
|
|
17
|
+
declare function computeWebAuthnChallenge(documentHash: string, challengeNonce: string): Promise<string>;
|
|
18
|
+
|
|
19
|
+
export { base64UrlToBytes, bytesToBase64Url, computeTriangleHash, computeWebAuthnChallenge, hashArrayBuffer, hashData, uint8ToBase64 };
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** SHA-256 of a UTF-8 string → lowercase hex. (Was App.tsx `hashData`.) */
|
|
2
|
+
declare function hashData(data: string): Promise<string>;
|
|
3
|
+
/** SHA-256 of raw bytes → lowercase hex. (Was App.tsx `hashArrayBuffer`.) Used for
|
|
4
|
+
* documentHash (original upload) AND signedPdfHash / verifier file-integrity. */
|
|
5
|
+
declare function hashArrayBuffer(buffer: ArrayBuffer): Promise<string>;
|
|
6
|
+
/** Stack-safe base64 encode for large Uint8Arrays. (Was App.tsx `uint8ToBase64`.) */
|
|
7
|
+
declare function uint8ToBase64(bytes: Uint8Array): string;
|
|
8
|
+
/** base64url (no padding) of bytes. (Was App.tsx `bytesToBase64Url`.) */
|
|
9
|
+
declare function bytesToBase64Url(bytes: Uint8Array): string;
|
|
10
|
+
/** Decode base64url (padded or not) → bytes. */
|
|
11
|
+
declare function base64UrlToBytes(s: string): Uint8Array;
|
|
12
|
+
/** Triangle Authentication binding: SHA-256("selfieHash||biometricTimestamp||documentHash") → hex.
|
|
13
|
+
* Order and the literal "||" separators must match the signer exactly. */
|
|
14
|
+
declare function computeTriangleHash(selfieHash: string, biometricTimestamp: number, documentHash: string): Promise<string>;
|
|
15
|
+
/** Document-bound WebAuthn challenge: base64url(SHA-256("documentHash||challengeNonce")).
|
|
16
|
+
* NOTE the literal "||" separator — the challenge is NOT a bare concatenation. */
|
|
17
|
+
declare function computeWebAuthnChallenge(documentHash: string, challengeNonce: string): Promise<string>;
|
|
18
|
+
|
|
19
|
+
export { base64UrlToBytes, bytesToBase64Url, computeTriangleHash, computeWebAuthnChallenge, hashArrayBuffer, hashData, uint8ToBase64 };
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"crypto.js"}
|