feat(bus): TypeScript SDK crypto + frame, parity-verified against Go
First half of the browser-native bus SDK (issue 0001, Phase 1): - crypto.ts: Ed25519 sign/verify (@noble), ChaCha20-Poly1305 AEAD (@noble), endpoint id (sha256+base64url), and the anonymous sealed box for room-key distribution. The sealed-box nonce is BLAKE2b-192 over ephPub||recipientPub, matching Go's nacl/box.SealAnonymous (NOT SHA-512) so a Go-sealed key opens here. - frame.ts: the Frame wire format, reproducing Go encoding/json byte-for-byte — struct field order, omitempty rules, base64-std byte fields, and the default HTML escaping (<, >, &, U+2028/U+2029) — plus sign/verify over canonical bytes. vectors.test.ts checks all of it against the golden vectors generated by unibus cmd/busvectors. 11/11 green: endpoint id, Ed25519 (incl. frame signature), AEAD seal+open, sealed box open + round-trip, and frame signing-bytes + wire marshal. This pins cross-language interop with Go/Kotlin peers. Adds @noble/ciphers, tweetnacl (runtime) and vitest (dev).
This commit is contained in:
+7
-3
@@ -6,17 +6,20 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/core": "^9.3.0",
|
"@mantine/core": "^9.3.0",
|
||||||
"@mantine/hooks": "^9.3.0",
|
"@mantine/hooks": "^9.3.0",
|
||||||
|
"@noble/ciphers": "^2.2.0",
|
||||||
"@noble/curves": "^2.2.0",
|
"@noble/curves": "^2.2.0",
|
||||||
"@noble/hashes": "^2.2.0",
|
"@noble/hashes": "^2.2.0",
|
||||||
"@scure/bip39": "^2.2.0",
|
"@scure/bip39": "^2.2.0",
|
||||||
"@tabler/icons-react": "^3.36.0",
|
"@tabler/icons-react": "^3.36.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0"
|
"react-dom": "^19.2.0",
|
||||||
|
"tweetnacl": "^1.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
@@ -26,6 +29,7 @@
|
|||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"typescript": "~5.6.3",
|
"typescript": "~5.6.3",
|
||||||
"vite": "^6.0.3"
|
"vite": "^6.0.3",
|
||||||
|
"vitest": "^4.1.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+264
@@ -14,6 +14,9 @@ importers:
|
|||||||
'@mantine/hooks':
|
'@mantine/hooks':
|
||||||
specifier: ^9.3.0
|
specifier: ^9.3.0
|
||||||
version: 9.3.0(react@19.2.7)
|
version: 9.3.0(react@19.2.7)
|
||||||
|
'@noble/ciphers':
|
||||||
|
specifier: ^2.2.0
|
||||||
|
version: 2.2.0
|
||||||
'@noble/curves':
|
'@noble/curves':
|
||||||
specifier: ^2.2.0
|
specifier: ^2.2.0
|
||||||
version: 2.2.0
|
version: 2.2.0
|
||||||
@@ -32,6 +35,9 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^19.2.0
|
specifier: ^19.2.0
|
||||||
version: 19.2.7(react@19.2.7)
|
version: 19.2.7(react@19.2.7)
|
||||||
|
tweetnacl:
|
||||||
|
specifier: ^1.0.3
|
||||||
|
version: 1.0.3
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^19.2.0
|
specifier: ^19.2.0
|
||||||
@@ -57,6 +63,9 @@ importers:
|
|||||||
vite:
|
vite:
|
||||||
specifier: ^6.0.3
|
specifier: ^6.0.3
|
||||||
version: 6.4.3(sugarss@5.0.1(postcss@8.5.15))
|
version: 6.4.3(sugarss@5.0.1(postcss@8.5.15))
|
||||||
|
vitest:
|
||||||
|
specifier: ^4.1.8
|
||||||
|
version: 4.1.8(vite@6.4.3(sugarss@5.0.1(postcss@8.5.15)))
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -348,6 +357,10 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^19.2.0
|
react: ^19.2.0
|
||||||
|
|
||||||
|
'@noble/ciphers@2.2.0':
|
||||||
|
resolution: {integrity: sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==}
|
||||||
|
engines: {node: '>= 20.19.0'}
|
||||||
|
|
||||||
'@noble/curves@2.2.0':
|
'@noble/curves@2.2.0':
|
||||||
resolution: {integrity: sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==}
|
resolution: {integrity: sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==}
|
||||||
engines: {node: '>= 20.19.0'}
|
engines: {node: '>= 20.19.0'}
|
||||||
@@ -503,6 +516,9 @@ packages:
|
|||||||
'@scure/bip39@2.2.0':
|
'@scure/bip39@2.2.0':
|
||||||
resolution: {integrity: sha512-T/Bj/YvYMNkIPq6EENO6/rcs2e7qTNuyoUXf0KBFDmp0ZDu0H2X4Lq6yC3i0c8PcWkov5EbW+yQZZbdMmk154A==}
|
resolution: {integrity: sha512-T/Bj/YvYMNkIPq6EENO6/rcs2e7qTNuyoUXf0KBFDmp0ZDu0H2X4Lq6yC3i0c8PcWkov5EbW+yQZZbdMmk154A==}
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.1.0':
|
||||||
|
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||||
|
|
||||||
'@tabler/icons-react@3.44.0':
|
'@tabler/icons-react@3.44.0':
|
||||||
resolution: {integrity: sha512-8+rvzBbVm/1Z3sG3x7GUNAaxIKxwgz8xaMhRs23nrCnMTKRFAhEC+82zAIFeAA0seXdrAGX5HFCkaLpGK2rVHg==}
|
resolution: {integrity: sha512-8+rvzBbVm/1Z3sG3x7GUNAaxIKxwgz8xaMhRs23nrCnMTKRFAhEC+82zAIFeAA0seXdrAGX5HFCkaLpGK2rVHg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -523,6 +539,12 @@ packages:
|
|||||||
'@types/babel__traverse@7.28.0':
|
'@types/babel__traverse@7.28.0':
|
||||||
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
||||||
|
|
||||||
|
'@types/chai@5.2.3':
|
||||||
|
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||||
|
|
||||||
|
'@types/deep-eql@4.0.2':
|
||||||
|
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||||
|
|
||||||
'@types/estree@1.0.9':
|
'@types/estree@1.0.9':
|
||||||
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||||
|
|
||||||
@@ -540,6 +562,39 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||||
|
|
||||||
|
'@vitest/expect@4.1.8':
|
||||||
|
resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==}
|
||||||
|
|
||||||
|
'@vitest/mocker@4.1.8':
|
||||||
|
resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==}
|
||||||
|
peerDependencies:
|
||||||
|
msw: ^2.4.9
|
||||||
|
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
msw:
|
||||||
|
optional: true
|
||||||
|
vite:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@vitest/pretty-format@4.1.8':
|
||||||
|
resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==}
|
||||||
|
|
||||||
|
'@vitest/runner@4.1.8':
|
||||||
|
resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==}
|
||||||
|
|
||||||
|
'@vitest/snapshot@4.1.8':
|
||||||
|
resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==}
|
||||||
|
|
||||||
|
'@vitest/spy@4.1.8':
|
||||||
|
resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==}
|
||||||
|
|
||||||
|
'@vitest/utils@4.1.8':
|
||||||
|
resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==}
|
||||||
|
|
||||||
|
assertion-error@2.0.1:
|
||||||
|
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
baseline-browser-mapping@2.10.34:
|
baseline-browser-mapping@2.10.34:
|
||||||
resolution: {integrity: sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw==}
|
resolution: {integrity: sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
@@ -557,6 +612,10 @@ packages:
|
|||||||
caniuse-lite@1.0.30001797:
|
caniuse-lite@1.0.30001797:
|
||||||
resolution: {integrity: sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==}
|
resolution: {integrity: sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==}
|
||||||
|
|
||||||
|
chai@6.2.2:
|
||||||
|
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
clsx@2.1.1:
|
clsx@2.1.1:
|
||||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -587,6 +646,9 @@ packages:
|
|||||||
electron-to-chromium@1.5.368:
|
electron-to-chromium@1.5.368:
|
||||||
resolution: {integrity: sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==}
|
resolution: {integrity: sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==}
|
||||||
|
|
||||||
|
es-module-lexer@2.1.0:
|
||||||
|
resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
|
||||||
|
|
||||||
esbuild@0.25.12:
|
esbuild@0.25.12:
|
||||||
resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
|
resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -596,6 +658,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
estree-walker@3.0.3:
|
||||||
|
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||||
|
|
||||||
|
expect-type@1.3.0:
|
||||||
|
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
fdir@6.5.0:
|
fdir@6.5.0:
|
||||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@@ -634,6 +703,9 @@ packages:
|
|||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||||
|
|
||||||
|
magic-string@0.30.21:
|
||||||
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -646,6 +718,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==}
|
resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
obug@2.1.3:
|
||||||
|
resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==}
|
||||||
|
engines: {node: '>=12.20.0'}
|
||||||
|
|
||||||
|
pathe@2.0.3:
|
||||||
|
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@@ -751,10 +830,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
siginfo@2.0.0:
|
||||||
|
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||||
|
|
||||||
source-map-js@1.2.1:
|
source-map-js@1.2.1:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
stackback@0.0.2:
|
||||||
|
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||||
|
|
||||||
|
std-env@4.1.0:
|
||||||
|
resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
|
||||||
|
|
||||||
sugarss@5.0.1:
|
sugarss@5.0.1:
|
||||||
resolution: {integrity: sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw==}
|
resolution: {integrity: sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw==}
|
||||||
engines: {node: '>=18.0'}
|
engines: {node: '>=18.0'}
|
||||||
@@ -768,13 +856,27 @@ packages:
|
|||||||
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
|
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
tinybench@2.9.0:
|
||||||
|
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||||
|
|
||||||
|
tinyexec@1.2.4:
|
||||||
|
resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
tinyglobby@0.2.17:
|
tinyglobby@0.2.17:
|
||||||
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
tinyrainbow@3.1.0:
|
||||||
|
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
|
tweetnacl@1.0.3:
|
||||||
|
resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==}
|
||||||
|
|
||||||
type-fest@5.7.0:
|
type-fest@5.7.0:
|
||||||
resolution: {integrity: sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==}
|
resolution: {integrity: sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
@@ -853,6 +955,52 @@ packages:
|
|||||||
yaml:
|
yaml:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
vitest@4.1.8:
|
||||||
|
resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==}
|
||||||
|
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@edge-runtime/vm': '*'
|
||||||
|
'@opentelemetry/api': ^1.9.0
|
||||||
|
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
|
||||||
|
'@vitest/browser-playwright': 4.1.8
|
||||||
|
'@vitest/browser-preview': 4.1.8
|
||||||
|
'@vitest/browser-webdriverio': 4.1.8
|
||||||
|
'@vitest/coverage-istanbul': 4.1.8
|
||||||
|
'@vitest/coverage-v8': 4.1.8
|
||||||
|
'@vitest/ui': 4.1.8
|
||||||
|
happy-dom: '*'
|
||||||
|
jsdom: '*'
|
||||||
|
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@edge-runtime/vm':
|
||||||
|
optional: true
|
||||||
|
'@opentelemetry/api':
|
||||||
|
optional: true
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
'@vitest/browser-playwright':
|
||||||
|
optional: true
|
||||||
|
'@vitest/browser-preview':
|
||||||
|
optional: true
|
||||||
|
'@vitest/browser-webdriverio':
|
||||||
|
optional: true
|
||||||
|
'@vitest/coverage-istanbul':
|
||||||
|
optional: true
|
||||||
|
'@vitest/coverage-v8':
|
||||||
|
optional: true
|
||||||
|
'@vitest/ui':
|
||||||
|
optional: true
|
||||||
|
happy-dom:
|
||||||
|
optional: true
|
||||||
|
jsdom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
why-is-node-running@2.3.0:
|
||||||
|
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
yallist@3.1.1:
|
yallist@3.1.1:
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||||
|
|
||||||
@@ -1109,6 +1257,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
|
|
||||||
|
'@noble/ciphers@2.2.0': {}
|
||||||
|
|
||||||
'@noble/curves@2.2.0':
|
'@noble/curves@2.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@noble/hashes': 2.2.0
|
'@noble/hashes': 2.2.0
|
||||||
@@ -1199,6 +1349,8 @@ snapshots:
|
|||||||
'@noble/hashes': 2.2.0
|
'@noble/hashes': 2.2.0
|
||||||
'@scure/base': 2.2.0
|
'@scure/base': 2.2.0
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.1.0': {}
|
||||||
|
|
||||||
'@tabler/icons-react@3.44.0(react@19.2.7)':
|
'@tabler/icons-react@3.44.0(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tabler/icons': 3.44.0
|
'@tabler/icons': 3.44.0
|
||||||
@@ -1227,6 +1379,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
|
|
||||||
|
'@types/chai@5.2.3':
|
||||||
|
dependencies:
|
||||||
|
'@types/deep-eql': 4.0.2
|
||||||
|
assertion-error: 2.0.1
|
||||||
|
|
||||||
|
'@types/deep-eql@4.0.2': {}
|
||||||
|
|
||||||
'@types/estree@1.0.9': {}
|
'@types/estree@1.0.9': {}
|
||||||
|
|
||||||
'@types/react-dom@19.2.3(@types/react@19.2.17)':
|
'@types/react-dom@19.2.3(@types/react@19.2.17)':
|
||||||
@@ -1249,6 +1408,49 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@vitest/expect@4.1.8':
|
||||||
|
dependencies:
|
||||||
|
'@standard-schema/spec': 1.1.0
|
||||||
|
'@types/chai': 5.2.3
|
||||||
|
'@vitest/spy': 4.1.8
|
||||||
|
'@vitest/utils': 4.1.8
|
||||||
|
chai: 6.2.2
|
||||||
|
tinyrainbow: 3.1.0
|
||||||
|
|
||||||
|
'@vitest/mocker@4.1.8(vite@6.4.3(sugarss@5.0.1(postcss@8.5.15)))':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/spy': 4.1.8
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
magic-string: 0.30.21
|
||||||
|
optionalDependencies:
|
||||||
|
vite: 6.4.3(sugarss@5.0.1(postcss@8.5.15))
|
||||||
|
|
||||||
|
'@vitest/pretty-format@4.1.8':
|
||||||
|
dependencies:
|
||||||
|
tinyrainbow: 3.1.0
|
||||||
|
|
||||||
|
'@vitest/runner@4.1.8':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/utils': 4.1.8
|
||||||
|
pathe: 2.0.3
|
||||||
|
|
||||||
|
'@vitest/snapshot@4.1.8':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/pretty-format': 4.1.8
|
||||||
|
'@vitest/utils': 4.1.8
|
||||||
|
magic-string: 0.30.21
|
||||||
|
pathe: 2.0.3
|
||||||
|
|
||||||
|
'@vitest/spy@4.1.8': {}
|
||||||
|
|
||||||
|
'@vitest/utils@4.1.8':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/pretty-format': 4.1.8
|
||||||
|
convert-source-map: 2.0.0
|
||||||
|
tinyrainbow: 3.1.0
|
||||||
|
|
||||||
|
assertion-error@2.0.1: {}
|
||||||
|
|
||||||
baseline-browser-mapping@2.10.34: {}
|
baseline-browser-mapping@2.10.34: {}
|
||||||
|
|
||||||
browserslist@4.28.2:
|
browserslist@4.28.2:
|
||||||
@@ -1263,6 +1465,8 @@ snapshots:
|
|||||||
|
|
||||||
caniuse-lite@1.0.30001797: {}
|
caniuse-lite@1.0.30001797: {}
|
||||||
|
|
||||||
|
chai@6.2.2: {}
|
||||||
|
|
||||||
clsx@2.1.1: {}
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
@@ -1279,6 +1483,8 @@ snapshots:
|
|||||||
|
|
||||||
electron-to-chromium@1.5.368: {}
|
electron-to-chromium@1.5.368: {}
|
||||||
|
|
||||||
|
es-module-lexer@2.1.0: {}
|
||||||
|
|
||||||
esbuild@0.25.12:
|
esbuild@0.25.12:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@esbuild/aix-ppc64': 0.25.12
|
'@esbuild/aix-ppc64': 0.25.12
|
||||||
@@ -1310,6 +1516,12 @@ snapshots:
|
|||||||
|
|
||||||
escalade@3.2.0: {}
|
escalade@3.2.0: {}
|
||||||
|
|
||||||
|
estree-walker@3.0.3:
|
||||||
|
dependencies:
|
||||||
|
'@types/estree': 1.0.9
|
||||||
|
|
||||||
|
expect-type@1.3.0: {}
|
||||||
|
|
||||||
fdir@6.5.0(picomatch@4.0.4):
|
fdir@6.5.0(picomatch@4.0.4):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
@@ -1331,12 +1543,20 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist: 3.1.1
|
yallist: 3.1.1
|
||||||
|
|
||||||
|
magic-string@0.30.21:
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
nanoid@3.3.12: {}
|
nanoid@3.3.12: {}
|
||||||
|
|
||||||
node-releases@2.0.47: {}
|
node-releases@2.0.47: {}
|
||||||
|
|
||||||
|
obug@2.1.3: {}
|
||||||
|
|
||||||
|
pathe@2.0.3: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@4.0.4: {}
|
picomatch@4.0.4: {}
|
||||||
@@ -1456,8 +1676,14 @@ snapshots:
|
|||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
|
|
||||||
|
siginfo@2.0.0: {}
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
stackback@0.0.2: {}
|
||||||
|
|
||||||
|
std-env@4.1.0: {}
|
||||||
|
|
||||||
sugarss@5.0.1(postcss@8.5.15):
|
sugarss@5.0.1(postcss@8.5.15):
|
||||||
dependencies:
|
dependencies:
|
||||||
postcss: 8.5.15
|
postcss: 8.5.15
|
||||||
@@ -1466,13 +1692,21 @@ snapshots:
|
|||||||
|
|
||||||
tagged-tag@1.0.0: {}
|
tagged-tag@1.0.0: {}
|
||||||
|
|
||||||
|
tinybench@2.9.0: {}
|
||||||
|
|
||||||
|
tinyexec@1.2.4: {}
|
||||||
|
|
||||||
tinyglobby@0.2.17:
|
tinyglobby@0.2.17:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.5.0(picomatch@4.0.4)
|
fdir: 6.5.0(picomatch@4.0.4)
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
|
|
||||||
|
tinyrainbow@3.1.0: {}
|
||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
|
tweetnacl@1.0.3: {}
|
||||||
|
|
||||||
type-fest@5.7.0:
|
type-fest@5.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
tagged-tag: 1.0.0
|
tagged-tag: 1.0.0
|
||||||
@@ -1514,4 +1748,34 @@ snapshots:
|
|||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
sugarss: 5.0.1(postcss@8.5.15)
|
sugarss: 5.0.1(postcss@8.5.15)
|
||||||
|
|
||||||
|
vitest@4.1.8(vite@6.4.3(sugarss@5.0.1(postcss@8.5.15))):
|
||||||
|
dependencies:
|
||||||
|
'@vitest/expect': 4.1.8
|
||||||
|
'@vitest/mocker': 4.1.8(vite@6.4.3(sugarss@5.0.1(postcss@8.5.15)))
|
||||||
|
'@vitest/pretty-format': 4.1.8
|
||||||
|
'@vitest/runner': 4.1.8
|
||||||
|
'@vitest/snapshot': 4.1.8
|
||||||
|
'@vitest/spy': 4.1.8
|
||||||
|
'@vitest/utils': 4.1.8
|
||||||
|
es-module-lexer: 2.1.0
|
||||||
|
expect-type: 1.3.0
|
||||||
|
magic-string: 0.30.21
|
||||||
|
obug: 2.1.3
|
||||||
|
pathe: 2.0.3
|
||||||
|
picomatch: 4.0.4
|
||||||
|
std-env: 4.1.0
|
||||||
|
tinybench: 2.9.0
|
||||||
|
tinyexec: 1.2.4
|
||||||
|
tinyglobby: 0.2.17
|
||||||
|
tinyrainbow: 3.1.0
|
||||||
|
vite: 6.4.3(sugarss@5.0.1(postcss@8.5.15))
|
||||||
|
why-is-node-running: 2.3.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- msw
|
||||||
|
|
||||||
|
why-is-node-running@2.3.0:
|
||||||
|
dependencies:
|
||||||
|
siginfo: 2.0.0
|
||||||
|
stackback: 0.0.2
|
||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
// Bus crypto primitives, ported to the browser to match the Go reference
|
||||||
|
// implementation (functions/cybersecurity in fn-registry) byte-for-byte. The bus
|
||||||
|
// is end-to-end encrypted; doing the crypto here is what keeps the user's private
|
||||||
|
// key on the device and out of any server (issue 0001). Parity with Go is enforced
|
||||||
|
// by the vectors in testdata/vectors.json (see vectors.test.ts).
|
||||||
|
//
|
||||||
|
// Primitive map (Go -> here):
|
||||||
|
// EndpointID -> endpointID : base64url(sha256(signPub)), unpadded
|
||||||
|
// SignEd25519 -> signEd25519 : Ed25519 detached signature
|
||||||
|
// verify -> verifyEd25519
|
||||||
|
// SealAEAD/Open -> sealAEAD/openAEAD : ChaCha20-Poly1305 (IETF, 12-byte nonce)
|
||||||
|
// SealKeyBox/Open -> sealKeyBox/openKeyBox : NaCl anonymous sealed box (X25519),
|
||||||
|
// with the nonce derived as sha512(ephPub||recipientPub)[:24]
|
||||||
|
// EXACTLY as Go's nacl/box.SealAnonymous (Go uses SHA-512, not
|
||||||
|
// libsodium's blake2b — matching this is the whole point).
|
||||||
|
|
||||||
|
import { ed25519 } from "@noble/curves/ed25519.js";
|
||||||
|
import { chacha20poly1305 } from "@noble/ciphers/chacha.js";
|
||||||
|
import { sha256 } from "@noble/hashes/sha2.js";
|
||||||
|
import { blake2b } from "@noble/hashes/blake2.js";
|
||||||
|
import { concatBytes } from "@noble/hashes/utils.js";
|
||||||
|
import nacl from "tweetnacl";
|
||||||
|
|
||||||
|
// sealedBoxNonce derives the 24-byte nonce for an anonymous sealed box the same way
|
||||||
|
// Go's nacl/box.SealAnonymous (and libsodium's crypto_box_seal) do: BLAKE2b-192 over
|
||||||
|
// ephemeralPub || recipientPub. NOT SHA-512 — matching the exact hash is what makes
|
||||||
|
// a Go-sealed room key openable here.
|
||||||
|
function sealedBoxNonce(ephPub: Uint8Array, recipientPub: Uint8Array): Uint8Array {
|
||||||
|
return blake2b(concatBytes(ephPub, recipientPub), { dkLen: 24 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- byte / encoding helpers (browser-safe; no Buffer) -----------------------
|
||||||
|
|
||||||
|
export function bytesToHex(b: Uint8Array): string {
|
||||||
|
let s = "";
|
||||||
|
for (const x of b) s += x.toString(16).padStart(2, "0");
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hexToBytes(hex: string): Uint8Array {
|
||||||
|
if (hex.length % 2 !== 0) throw new Error("hex: odd length");
|
||||||
|
const out = new Uint8Array(hex.length / 2);
|
||||||
|
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// base64 standard (with padding) — matches Go's encoding/json for []byte fields.
|
||||||
|
export function bytesToBase64(b: Uint8Array): string {
|
||||||
|
let bin = "";
|
||||||
|
for (const x of b) bin += String.fromCharCode(x);
|
||||||
|
return btoa(bin);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function base64ToBytes(s: string): Uint8Array {
|
||||||
|
const bin = atob(s);
|
||||||
|
const out = new Uint8Array(bin.length);
|
||||||
|
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// base64url without padding — matches Go's base64.RawURLEncoding (EndpointID).
|
||||||
|
export function bytesToBase64URL(b: Uint8Array): string {
|
||||||
|
return bytesToBase64(b).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- identity / signing ------------------------------------------------------
|
||||||
|
|
||||||
|
// endpointID is the stable, transport-agnostic peer id: base64url(sha256(signPub)).
|
||||||
|
export function endpointID(signPub: Uint8Array): string {
|
||||||
|
return bytesToBase64URL(sha256(signPub));
|
||||||
|
}
|
||||||
|
|
||||||
|
// signEd25519 signs msg with an Ed25519 private key. It accepts the bus/Go 64-byte
|
||||||
|
// private key (seed || pub) OR a bare 32-byte seed; @noble signs from the 32-byte
|
||||||
|
// seed, so we slice the seed out of the 64-byte form.
|
||||||
|
export function signEd25519(priv: Uint8Array, msg: Uint8Array): Uint8Array {
|
||||||
|
const seed = priv.length === 64 ? priv.subarray(0, 32) : priv;
|
||||||
|
return ed25519.sign(msg, seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyEd25519(sig: Uint8Array, msg: Uint8Array, pub: Uint8Array): boolean {
|
||||||
|
return ed25519.verify(sig, msg, pub);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- AEAD (room message content) ---------------------------------------------
|
||||||
|
|
||||||
|
// sealAEAD encrypts plaintext with ChaCha20-Poly1305 (IETF, 12-byte nonce). The
|
||||||
|
// caller supplies the nonce so the operation is testable; in the bus a fresh random
|
||||||
|
// nonce is generated per message and stored alongside the ciphertext.
|
||||||
|
export function sealAEAD(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Uint8Array {
|
||||||
|
return chacha20poly1305(key, nonce, aad).encrypt(plaintext);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openAEAD(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, aad?: Uint8Array): Uint8Array {
|
||||||
|
return chacha20poly1305(key, nonce, aad).decrypt(ciphertext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// randomNonce returns a fresh 12-byte AEAD nonce (ChaCha20-Poly1305 IETF size).
|
||||||
|
export function randomNonce(): Uint8Array {
|
||||||
|
return crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- anonymous sealed box (room key distribution) ----------------------------
|
||||||
|
|
||||||
|
// sealKeyBox seals secret to a recipient's X25519 public key as an anonymous NaCl
|
||||||
|
// sealed box, matching Go's nacl/box.SealAnonymous: an ephemeral keypair is created,
|
||||||
|
// the nonce is sha512(ephPub || recipientPub)[:24], and the output is
|
||||||
|
// ephPub(32) || box(secret). The recipient opens it with openKeyBox; the sender is
|
||||||
|
// anonymous (no long-term sender key is revealed).
|
||||||
|
export function sealKeyBox(recipientKexPub: Uint8Array, secret: Uint8Array): Uint8Array {
|
||||||
|
const eph = nacl.box.keyPair();
|
||||||
|
const nonce = sealedBoxNonce(eph.publicKey, recipientKexPub);
|
||||||
|
const boxed = nacl.box(secret, nonce, recipientKexPub, eph.secretKey);
|
||||||
|
return concatBytes(eph.publicKey, boxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// openKeyBox opens an anonymous sealed box produced by sealKeyBox (or Go's
|
||||||
|
// SealKeyBox). It re-derives the same sha512-based nonce from the embedded ephemeral
|
||||||
|
// public key and the recipient's own public key, then opens the box with the
|
||||||
|
// recipient's private key. Returns null if authentication fails.
|
||||||
|
export function openKeyBox(
|
||||||
|
recipientKexPub: Uint8Array,
|
||||||
|
recipientKexPriv: Uint8Array,
|
||||||
|
sealed: Uint8Array,
|
||||||
|
): Uint8Array | null {
|
||||||
|
if (sealed.length < 32) return null;
|
||||||
|
const ephPub = sealed.subarray(0, 32);
|
||||||
|
const boxed = sealed.subarray(32);
|
||||||
|
const nonce = sealedBoxNonce(ephPub, recipientKexPub);
|
||||||
|
return nacl.box.open(boxed, nonce, ephPub, recipientKexPriv);
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
// The wire format of the unibus message bus, ported from Go pkg/frame. A Frame is
|
||||||
|
// the unit transported over NATS: a cleartext envelope plus an optional AEAD
|
||||||
|
// ciphertext payload, signed end-to-end with Ed25519.
|
||||||
|
//
|
||||||
|
// The signature covers the canonical JSON of the frame with the signature field
|
||||||
|
// cleared, so the marshaler here must reproduce Go's encoding/json BYTE FOR BYTE or
|
||||||
|
// signatures verified by Go peers would fail. That means: struct field order, the
|
||||||
|
// `omitempty` rules, base64-standard encoding of []byte fields, and Go's default
|
||||||
|
// HTML escaping of <, >, & and the U+2028/U+2029 separators inside strings. Parity
|
||||||
|
// is pinned by testdata/vectors.json (vectors.test.ts).
|
||||||
|
|
||||||
|
import {
|
||||||
|
bytesToBase64,
|
||||||
|
base64ToBytes,
|
||||||
|
signEd25519,
|
||||||
|
verifyEd25519,
|
||||||
|
endpointID,
|
||||||
|
} from "./crypto.js";
|
||||||
|
|
||||||
|
export enum FrameType {
|
||||||
|
PUB = 0,
|
||||||
|
INVITE = 1,
|
||||||
|
JOIN = 2,
|
||||||
|
LEAVE = 3,
|
||||||
|
KICK = 4,
|
||||||
|
ACK = 5,
|
||||||
|
REACT = 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlobRef {
|
||||||
|
hash: string; // sha256 hex of the blob ciphertext
|
||||||
|
nonce: Uint8Array; // AEAD nonce used to encrypt the blob
|
||||||
|
size: number; // ciphertext size in bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Frame {
|
||||||
|
type: FrameType;
|
||||||
|
subject: string;
|
||||||
|
sender: string; // endpoint id = endpointID(signPub)
|
||||||
|
msgID: string; // ULID
|
||||||
|
epoch: number; // epoch of the room key used to encrypt
|
||||||
|
threadID?: string; // root message id of the thread (optional)
|
||||||
|
replyTo?: string; // message id this frame replies to / reacts to (optional)
|
||||||
|
nonce?: Uint8Array; // AEAD nonce (encrypted rooms only)
|
||||||
|
payload?: Uint8Array; // AEAD ciphertext (or cleartext if the room is not encrypted)
|
||||||
|
blob?: BlobRef;
|
||||||
|
sig?: Uint8Array; // Ed25519 signature over signingBytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go's encoding/json HTML-escapes these code points inside strings by default. We
|
||||||
|
// replay the exact same set so our canonical bytes match Go's. The two separators
|
||||||
|
// (U+2028 line separator, U+2029 paragraph separator) are built via fromCharCode so
|
||||||
|
// this source file holds no invisible characters while the RegExp still matches the
|
||||||
|
// real code points at runtime.
|
||||||
|
const GO_ESCAPES: ReadonlyArray<[RegExp, string]> = [
|
||||||
|
[/</g, "\\u003c"],
|
||||||
|
[/>/g, "\\u003e"],
|
||||||
|
[/&/g, "\\u0026"],
|
||||||
|
[new RegExp(String.fromCharCode(0x2028), "g"), "\\u2028"],
|
||||||
|
[new RegExp(String.fromCharCode(0x2029), "g"), "\\u2029"],
|
||||||
|
];
|
||||||
|
|
||||||
|
// goJSONStringify serializes obj the way Go's encoding/json does: compact (no
|
||||||
|
// spaces), insertion-ordered keys, and the default HTML escaping above. Apply only
|
||||||
|
// to objects built key-by-key in field order, so the output matches Go's struct
|
||||||
|
// marshaling exactly.
|
||||||
|
function goJSONStringify(obj: Record<string, unknown>): string {
|
||||||
|
let s = JSON.stringify(obj);
|
||||||
|
for (const [re, rep] of GO_ESCAPES) s = s.replace(re, rep);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// frameObject builds the plain object with keys inserted in Go struct-declaration
|
||||||
|
// order, applying each field's omitempty rule. includeSig controls whether the
|
||||||
|
// signature field is emitted: false yields the canonical signing-bytes object.
|
||||||
|
function frameObject(f: Frame, includeSig: boolean): Record<string, unknown> {
|
||||||
|
const o: Record<string, unknown> = {};
|
||||||
|
// Always-present fields (no omitempty in Go).
|
||||||
|
o.t = f.type;
|
||||||
|
o.s = f.subject;
|
||||||
|
o.from = f.sender;
|
||||||
|
o.id = f.msgID;
|
||||||
|
o.e = f.epoch;
|
||||||
|
// omitempty fields, in declaration order.
|
||||||
|
if (f.threadID) o.thr = f.threadID;
|
||||||
|
if (f.replyTo) o.re = f.replyTo;
|
||||||
|
if (f.nonce && f.nonce.length) o.n = bytesToBase64(f.nonce);
|
||||||
|
if (f.payload && f.payload.length) o.p = bytesToBase64(f.payload);
|
||||||
|
if (f.blob) o.b = { h: f.blob.hash, n: bytesToBase64(f.blob.nonce), sz: f.blob.size };
|
||||||
|
if (includeSig && f.sig && f.sig.length) o.sig = bytesToBase64(f.sig);
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
// marshal returns the wire bytes of the frame (UTF-8 of the canonical JSON).
|
||||||
|
export function marshal(f: Frame): Uint8Array {
|
||||||
|
return new TextEncoder().encode(goJSONStringify(frameObject(f, true)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// signingBytes returns the canonical bytes that are signed and verified: the frame
|
||||||
|
// JSON with the signature field cleared.
|
||||||
|
export function signingBytes(f: Frame): Uint8Array {
|
||||||
|
return new TextEncoder().encode(goJSONStringify(frameObject(f, false)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshal parses wire bytes back into a Frame, decoding the base64 []byte fields.
|
||||||
|
export function unmarshal(b: Uint8Array): Frame {
|
||||||
|
const o = JSON.parse(new TextDecoder().decode(b));
|
||||||
|
const f: Frame = {
|
||||||
|
type: o.t ?? 0,
|
||||||
|
subject: o.s ?? "",
|
||||||
|
sender: o.from ?? "",
|
||||||
|
msgID: o.id ?? "",
|
||||||
|
epoch: o.e ?? 0,
|
||||||
|
};
|
||||||
|
if (o.thr) f.threadID = o.thr;
|
||||||
|
if (o.re) f.replyTo = o.re;
|
||||||
|
if (o.n) f.nonce = base64ToBytes(o.n);
|
||||||
|
if (o.p) f.payload = base64ToBytes(o.p);
|
||||||
|
if (o.b) f.blob = { hash: o.b.h, nonce: base64ToBytes(o.b.n), size: o.b.sz };
|
||||||
|
if (o.sig) f.sig = base64ToBytes(o.sig);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// signFrame fills f.sig with an Ed25519 signature over signingBytes(f). signPriv is
|
||||||
|
// the 64-byte (seed||pub) or 32-byte seed private key.
|
||||||
|
export function signFrame(f: Frame, signPriv: Uint8Array): Frame {
|
||||||
|
f.sig = signEd25519(signPriv, signingBytes(f));
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyFrame checks f.sig against signPub over signingBytes(f).
|
||||||
|
export function verifyFrame(f: Frame, signPub: Uint8Array): boolean {
|
||||||
|
if (!f.sig) return false;
|
||||||
|
return verifyEd25519(f.sig, signingBytes(f), signPub);
|
||||||
|
}
|
||||||
|
|
||||||
|
// senderEndpoint derives the canonical sender endpoint id from a signing public key.
|
||||||
|
export function senderEndpoint(signPub: Uint8Array): string {
|
||||||
|
return endpointID(signPub);
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
// Cross-language parity tests: the TypeScript bus SDK must reproduce the Go
|
||||||
|
// reference implementation byte-for-byte. The golden vectors in testdata/vectors.json
|
||||||
|
// are generated by unibus `cmd/busvectors`. Any divergence here means a browser
|
||||||
|
// client and a Go/Kotlin peer would not interoperate (issue 0001, Phase 1).
|
||||||
|
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import vectors from "./testdata/vectors.json";
|
||||||
|
import {
|
||||||
|
hexToBytes,
|
||||||
|
bytesToHex,
|
||||||
|
base64ToBytes,
|
||||||
|
endpointID,
|
||||||
|
signEd25519,
|
||||||
|
verifyEd25519,
|
||||||
|
sealAEAD,
|
||||||
|
openAEAD,
|
||||||
|
openKeyBox,
|
||||||
|
sealKeyBox,
|
||||||
|
} from "./crypto.js";
|
||||||
|
import { Frame, FrameType, marshal, signingBytes, signFrame, verifyFrame } from "./frame.js";
|
||||||
|
|
||||||
|
describe("endpoint id", () => {
|
||||||
|
it("matches Go EndpointID = base64url(sha256(signPub))", () => {
|
||||||
|
const v = vectors.endpoint_id;
|
||||||
|
expect(endpointID(hexToBytes(v.sign_pub_hex))).toBe(v.endpoint_id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Ed25519 signing", () => {
|
||||||
|
it("produces the same deterministic signature as Go", () => {
|
||||||
|
const v = vectors.sign;
|
||||||
|
const sig = signEd25519(hexToBytes(v.sign_priv_hex), hexToBytes(v.message_hex));
|
||||||
|
expect(bytesToHex(sig)).toBe(v.sig_hex);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("verifies the Go-produced signature", () => {
|
||||||
|
const v = vectors.sign;
|
||||||
|
const ok = verifyEd25519(hexToBytes(v.sig_hex), hexToBytes(v.message_hex), hexToBytes(v.sign_pub_hex));
|
||||||
|
expect(ok).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ChaCha20-Poly1305 AEAD", () => {
|
||||||
|
it("opens the Go-sealed ciphertext", () => {
|
||||||
|
const v = vectors.aead;
|
||||||
|
const pt = openAEAD(
|
||||||
|
hexToBytes(v.key_hex),
|
||||||
|
hexToBytes(v.nonce_hex),
|
||||||
|
hexToBytes(v.ciphertext_hex),
|
||||||
|
hexToBytes(v.aad_hex),
|
||||||
|
);
|
||||||
|
expect(bytesToHex(pt)).toBe(v.plaintext_hex);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("seals to the same ciphertext as Go with a fixed nonce", () => {
|
||||||
|
const v = vectors.aead;
|
||||||
|
const ct = sealAEAD(
|
||||||
|
hexToBytes(v.key_hex),
|
||||||
|
hexToBytes(v.nonce_hex),
|
||||||
|
hexToBytes(v.plaintext_hex),
|
||||||
|
hexToBytes(v.aad_hex),
|
||||||
|
);
|
||||||
|
expect(bytesToHex(ct)).toBe(v.ciphertext_hex);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("anonymous sealed box (room key distribution)", () => {
|
||||||
|
it("opens the Go-sealed room key", () => {
|
||||||
|
const v = vectors.keybox;
|
||||||
|
const secret = openKeyBox(
|
||||||
|
hexToBytes(v.recipient_kex_pub_hex),
|
||||||
|
hexToBytes(v.recipient_kex_priv_hex),
|
||||||
|
hexToBytes(v.sealed_hex),
|
||||||
|
);
|
||||||
|
expect(secret).not.toBeNull();
|
||||||
|
expect(bytesToHex(secret!)).toBe(v.secret_hex);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("round-trips a TS-sealed box (seal then open)", () => {
|
||||||
|
const v = vectors.keybox;
|
||||||
|
const pub = hexToBytes(v.recipient_kex_pub_hex);
|
||||||
|
const priv = hexToBytes(v.recipient_kex_priv_hex);
|
||||||
|
const secret = hexToBytes(v.secret_hex);
|
||||||
|
const sealed = sealKeyBox(pub, secret);
|
||||||
|
const opened = openKeyBox(pub, priv, sealed);
|
||||||
|
expect(opened).not.toBeNull();
|
||||||
|
expect(bytesToHex(opened!)).toBe(v.secret_hex);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Frame wire format", () => {
|
||||||
|
function vectorFrame(): Frame {
|
||||||
|
const v = vectors.frame;
|
||||||
|
return {
|
||||||
|
type: v.type as FrameType,
|
||||||
|
subject: v.subject,
|
||||||
|
sender: v.sender,
|
||||||
|
msgID: v.msg_id,
|
||||||
|
epoch: v.epoch,
|
||||||
|
nonce: hexToBytes(v.nonce_hex),
|
||||||
|
payload: hexToBytes(v.payload_hex),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
it("produces the same canonical signing bytes as Go", () => {
|
||||||
|
const got = signingBytes(vectorFrame());
|
||||||
|
const want = base64ToBytes(vectors.frame.signing_bytes_b64);
|
||||||
|
expect(bytesToHex(got)).toBe(bytesToHex(want));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("signs the frame to the same Ed25519 signature as Go", () => {
|
||||||
|
const f = signFrame(vectorFrame(), hexToBytes(vectors.sign.sign_priv_hex));
|
||||||
|
expect(bytesToHex(f.sig!)).toBe(vectors.frame.sig_hex);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marshals the signed frame to the same wire bytes as Go", () => {
|
||||||
|
const f = signFrame(vectorFrame(), hexToBytes(vectors.sign.sign_priv_hex));
|
||||||
|
const got = marshal(f);
|
||||||
|
const want = base64ToBytes(vectors.frame.wire_b64);
|
||||||
|
expect(bytesToHex(got)).toBe(bytesToHex(want));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("verifies the marshaled frame signature against the signer pubkey", () => {
|
||||||
|
const f = signFrame(vectorFrame(), hexToBytes(vectors.sign.sign_priv_hex));
|
||||||
|
expect(verifyFrame(f, hexToBytes(vectors.sign.sign_pub_hex))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user