@quantsafe/mcp 0.0.0-stage → 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 +163 -2
- package/dist/lib/qsafe/aead.js +39 -0
- package/dist/lib/qsafe/codec.js +132 -0
- package/dist/lib/qsafe/constants.js +37 -0
- package/dist/lib/qsafe/crypto.js +44 -0
- package/dist/lib/qsafe/file.js +97 -0
- package/dist/lib/qsafe/hkdf.js +9 -0
- package/dist/lib/qsafe/index.js +44 -0
- package/dist/lib/qsafe/kdf.js +52 -0
- package/dist/lib/qsafe/keyfile.js +107 -0
- package/dist/lib/qsafe/keys.js +23 -0
- package/dist/lib/qsafe/types.js +33 -0
- package/dist/lib/seal/constants.js +6 -0
- package/dist/lib/seal/png-text.js +312 -0
- package/dist/lib/wallet/signer-determinism.js +90 -0
- package/dist/mcp/base64.js +38 -0
- package/dist/mcp/canonical-json.js +28 -0
- package/dist/mcp/message-url.js +101 -0
- package/dist/mcp/server.js +126 -0
- package/dist/mcp/tool-result.js +17 -0
- package/dist/mcp/tools.js +381 -0
- package/dist/mcp/wallet-identity.js +60 -0
- package/package.json +34 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 QuantSafe
|
|
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
CHANGED
|
@@ -1,3 +1,164 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @quantsafe/mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Post-quantum encryption for AI agents. The same crypto core as [quantsafe.tech](https://quantsafe.tech), exposed as a Model Context Protocol server over stdio.
|
|
4
|
+
|
|
5
|
+
- Hybrid KEM: ML-KEM-768 + X25519, combined with HKDF-SHA256
|
|
6
|
+
- Signatures: ML-DSA-65, context-bound
|
|
7
|
+
- Symmetric: AES-256-GCM, random nonces
|
|
8
|
+
- Keyfiles wrapped with Argon2id
|
|
9
|
+
|
|
10
|
+
Everything runs locally. No network calls, no telemetry.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
Claude Desktop, Claude Code, Cursor or any MCP client:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"mcpServers": {
|
|
19
|
+
"qsafe": {
|
|
20
|
+
"command": "npx",
|
|
21
|
+
"args": ["-y", "@quantsafe/mcp"]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Requires Node.js 20 or later.
|
|
28
|
+
|
|
29
|
+
## Tools
|
|
30
|
+
|
|
31
|
+
| Tool | What it does |
|
|
32
|
+
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
33
|
+
| `qsafe_encrypt_file` | Encrypt bytes under ML-KEM-768 + X25519, signed with ML-DSA-65 |
|
|
34
|
+
| `qsafe_decrypt_file` | Decrypt a `.qsafe` blob with a keyfile or wallet signature |
|
|
35
|
+
| `qsafe_send_message` | Wrap text in a shareable encrypted URL, optionally wallet-signed |
|
|
36
|
+
| `qsafe_decrypt_message` | Read a message URL and return its text and the key that signed it |
|
|
37
|
+
| `qsafe_verify_seal` | Read the fields a Quantum Seal PNG carries, unsigned and unchecked; given the envelope, compare its sha256 with `file_hash` |
|
|
38
|
+
| `qsafe_sign_x402` | Sign an x402 payload with ML-DSA-65 under context `qsafe-v1-x402` |
|
|
39
|
+
|
|
40
|
+
## Key sources
|
|
41
|
+
|
|
42
|
+
A tool that signs or decrypts takes one key source: `keyfile_json` + `passphrase`, or a wallet signature given as `signature_hex` + `address`, with an optional `signature_hex_repeat`. `qsafe_encrypt_file` and `qsafe_send_message` name them `signer_keyfile_json`, `signer_passphrase`, `signer_signature_hex`, `signer_address` and `signer_signature_hex_repeat`.
|
|
43
|
+
|
|
44
|
+
A keyfile without its passphrase, a passphrase without its keyfile, a signature without its address (or the reverse), or a keyfile together with any wallet field returns an error. `qsafe_encrypt_file` and `qsafe_send_message` sign with a fresh ephemeral identity only when every signer field is omitted.
|
|
45
|
+
|
|
46
|
+
### Wallet signatures
|
|
47
|
+
|
|
48
|
+
`signature_hex` is a 65-byte EOA signature (`0x` + 130 hex characters) made with `personal_sign` (EIP-191) over this exact message: six lines joined by `\n`, no trailing newline.
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
QuantSafe keypair derivation v1
|
|
52
|
+
|
|
53
|
+
This signature derives your personal post-quantum encryption keypair.
|
|
54
|
+
It grants no permissions and costs no gas.
|
|
55
|
+
|
|
56
|
+
Domain: quantsafe.tech
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
It is the message the browser app at [quantsafe.tech/app](https://quantsafe.tech/app) asks a wallet to sign, so the same wallet derives the same identity in both places. The tool descriptions quote it as a JSON string.
|
|
60
|
+
|
|
61
|
+
Before any key is derived, the server recovers the signer of that message from `signature_hex` and refuses the call unless it equals `address`. A signature over other text (CRLF line endings, a missing line), a signature paired with another address, a high-s copy of a valid signature or a smart-contract wallet signature is refused instead of silently deriving an identity no browser session can reproduce.
|
|
62
|
+
|
|
63
|
+
Keys are derived from all 65 bytes, the last one (`v`) included. viem, ethers and browser wallets such as MetaMask and Rabby return `v` as 27 or 28 (`1b` or `1c`). Some signers return 0 or 1 instead (raw ECDSA libraries, KMS wrappers, some hardware paths). Pass the signature exactly as your signer returned it: the identity comes from those exact bytes, as in the browser app, so rewriting `v` from 0/1 to 27/28 (or back) gives a different identity.
|
|
64
|
+
|
|
65
|
+
MPC and threshold signers return a different valid signature every time, so the keys derived from them would change between sessions. Pass a second signature of the same message as `signature_hex_repeat` the first time you use a wallet: the call fails if the two differ.
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
import { privateKeyToAccount } from 'viem/accounts'
|
|
69
|
+
|
|
70
|
+
const QSAFE_DERIVATION_MESSAGE = [
|
|
71
|
+
'QuantSafe keypair derivation v1',
|
|
72
|
+
'',
|
|
73
|
+
'This signature derives your personal post-quantum encryption keypair.',
|
|
74
|
+
'It grants no permissions and costs no gas.',
|
|
75
|
+
'',
|
|
76
|
+
'Domain: quantsafe.tech',
|
|
77
|
+
].join('\n')
|
|
78
|
+
|
|
79
|
+
const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY)
|
|
80
|
+
const signature_hex = await account.signMessage({ message: QSAFE_DERIVATION_MESSAGE })
|
|
81
|
+
const signature_hex_repeat = await account.signMessage({ message: QSAFE_DERIVATION_MESSAGE })
|
|
82
|
+
|
|
83
|
+
// signer_signature_hex, signer_address and signer_signature_hex_repeat on the encrypting tools
|
|
84
|
+
await mcp.call('qsafe_decrypt_file', {
|
|
85
|
+
blob_base64,
|
|
86
|
+
signature_hex,
|
|
87
|
+
address: account.address,
|
|
88
|
+
signature_hex_repeat,
|
|
89
|
+
expected_signer_pubkey_base64,
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The signature is root key material. Pass it over the local stdio transport only.
|
|
94
|
+
|
|
95
|
+
A wallet-derived identity is only as strong as the wallet key behind it: it rests on that key's elliptic curve. For files that must survive a break of elliptic curves, use a freshly generated key file, whose keys are random.
|
|
96
|
+
|
|
97
|
+
## Inputs and outputs
|
|
98
|
+
|
|
99
|
+
A trailing `?` marks an optional field. "Key source" is one of the forms above.
|
|
100
|
+
|
|
101
|
+
| Tool | Inputs | Outputs |
|
|
102
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------- |
|
|
103
|
+
| `qsafe_encrypt_file` | `bytes_base64`, `recipient_pubkey_base64`, signer key source? | `blob_base64`, `seal_png_base64`, `signer_pubkey_base64` |
|
|
104
|
+
| `qsafe_decrypt_file` | `blob_base64`, key source, `expected_signer_pubkey_base64`? | `plaintext_base64`, `signer_pubkey_base64`, `filename` |
|
|
105
|
+
| `qsafe_send_message` | `text`, `recipient_pubkey_base64`?, signer key source? | `url`, `mode` |
|
|
106
|
+
| `qsafe_decrypt_message` | `url`, key source? (needed unless the URL carries its own key), `expected_signer_pubkey_base64`? | `text`, `signer_pubkey_base64` |
|
|
107
|
+
| `qsafe_verify_seal` | `png_base64`, `blob_base64`? | `valid`, `metadata`, `file_hash_matches`? |
|
|
108
|
+
| `qsafe_sign_x402` | `payload_json` (a JSON object), key source | `signature_base64`, `signer_pubkey_base64` |
|
|
109
|
+
|
|
110
|
+
Every `*_base64` input is standard base64: the alphabet `A-Z a-z 0-9 + /`, `=` padding, no whitespace or line breaks. Raw text, base64url or a string with stray characters is refused with an error that names the field; it is never decoded into other bytes. Encode the bytes first (`base64 -w0` on Linux, `base64 -i file` on macOS, `Buffer.from(bytes).toString('base64')` in Node).
|
|
111
|
+
|
|
112
|
+
`seal_png_base64` is a 1x1 PNG whose text chunks carry the Quantum Seal fields (`file_hash` is sha256 of the envelope). It has no rendered card; `qsafe_verify_seal` and [/verify](https://quantsafe.tech/verify) read its fields.
|
|
113
|
+
|
|
114
|
+
## Your public key
|
|
115
|
+
|
|
116
|
+
`recipient_pubkey_base64` and `expected_signer_pubkey_base64` take the same kind of value: standard base64 of a 3168-byte hybrid public key, ML-KEM-768 (1184 bytes), then X25519 (32), then ML-DSA-65 (1952). It is the `signer_pubkey_base64` a tool returns when it signs with your key source; no tool returns it on its own.
|
|
117
|
+
|
|
118
|
+
To get yours, call `qsafe_sign_x402` with your keyfile or wallet signature and any payload, for example `{}`, and keep `signer_pubkey_base64`. Give it to anyone who should encrypt to you, or who needs it to check your signature. The same key source always returns the same value. An anonymous ephemeral signer returns a new key on every call, and nothing can be encrypted to it later.
|
|
119
|
+
|
|
120
|
+
## Reading a Seal
|
|
121
|
+
|
|
122
|
+
No Seal field is signed: every one is a claim that anyone can write into a PNG. `qsafe_verify_seal` returns the fields it finds as `metadata`, and `valid` is `true` when at least one Seal field is present. `valid` checks nothing.
|
|
123
|
+
|
|
124
|
+
Pass the envelope the Seal is for as `blob_base64` and the result adds `file_hash_matches`: `true` when sha256 of that envelope equals the Seal's `file_hash`. That is the only check the tool makes. It does not contact the chain, so anchor fields are returned as written. Who signed the envelope is checked when it is decrypted.
|
|
125
|
+
|
|
126
|
+
## Decrypting a file from someone else
|
|
127
|
+
|
|
128
|
+
`qsafe_decrypt_file` always verifies the ML-DSA-65 signature before it decrypts, and a `.qsafe` file does not carry its signer's public key. Pass the sender's `signer_pubkey_base64` (returned by `qsafe_encrypt_file`) as `expected_signer_pubkey_base64`. Leave it out only for a file you signed with the same key you decrypt with; any other file, including one encrypted with an anonymous ephemeral signer, fails with an error that names the field.
|
|
129
|
+
|
|
130
|
+
A verified signature shows which key signed the envelope, not who wrote what is inside it. The signature covers the envelope bytes only, so anyone who holds a `.qsafe` file can strip its signature and sign the same envelope with their own key.
|
|
131
|
+
|
|
132
|
+
## Messages
|
|
133
|
+
|
|
134
|
+
`qsafe_send_message` returns the URL and its `mode`:
|
|
135
|
+
|
|
136
|
+
- `recipient`: `recipient_pubkey_base64` was given, and only that key opens the message.
|
|
137
|
+
- `anyone_with_link`: `recipient_pubkey_base64` was omitted. The URL carries its own decryption key, so anyone holding the URL can read the message.
|
|
138
|
+
|
|
139
|
+
An empty or malformed `recipient_pubkey_base64` is refused, never treated as omitted.
|
|
140
|
+
|
|
141
|
+
A message URL names the key that signed it, so a valid signature proves only that whoever built the URL holds that key. `qsafe_decrypt_message` returns that key as `signer_pubkey_base64`. Pass a key you already trust as `expected_signer_pubkey_base64` and a URL signed by any other key is refused.
|
|
142
|
+
|
|
143
|
+
That pin confirms which key signed the envelope, not who wrote the text. Anyone who sees a URL can strip its signature, sign the same envelope with their own key and rebuild the URL around it: a recipient who pins that key reads the original text under the new signer. Do not take a signature as proof that the signer wrote the message.
|
|
144
|
+
|
|
145
|
+
## Signing x402 payloads
|
|
146
|
+
|
|
147
|
+
`payload_json` must be a JSON object. A JSON-encoded string, array or scalar is rejected, since a string would be signed as a quoted string literal rather than as the payload it encodes.
|
|
148
|
+
|
|
149
|
+
The signed message is the UTF-8 encoding of the payload's canonical form, signed with ML-DSA-65 under context `qsafe-v1-x402`:
|
|
150
|
+
|
|
151
|
+
- object keys sorted in JavaScript default string order (UTF-16 code units), at every level
|
|
152
|
+
- no whitespace
|
|
153
|
+
- arrays kept in order
|
|
154
|
+
- strings, numbers, booleans and `null` written as `JSON.stringify` writes them
|
|
155
|
+
|
|
156
|
+
For example, `{"b":1,"a":{"d":[true,null],"c":2.5}}` is signed as `{"a":{"c":2.5,"d":[true,null]},"b":1}`. A verifier rebuilds those bytes and checks the signature against the ML-DSA-65 part of `signer_pubkey_base64` (bytes 1216 to 3168 of the decoded key, after the ML-KEM-768 and X25519 parts).
|
|
157
|
+
|
|
158
|
+
## Errors
|
|
159
|
+
|
|
160
|
+
A failed call returns `isError: true` with a JSON body `{ "error": "..." }`. Arguments that do not match a tool's input schema are rejected before the tool runs, with `isError: true` and a plain-text message.
|
|
161
|
+
|
|
162
|
+
A blob or message whose signature verifies but which is encrypted to another key returns "not encrypted to this key source". A message URL that does not decode returns "Malformed message URL" and names the broken segment.
|
|
163
|
+
|
|
164
|
+
Docs: [quantsafe.tech/mcp](https://quantsafe.tech/mcp)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.aeadEncrypt = aeadEncrypt;
|
|
4
|
+
exports.aeadDecrypt = aeadDecrypt;
|
|
5
|
+
const constants_1 = require("./constants");
|
|
6
|
+
function getSubtle() {
|
|
7
|
+
const subtle = globalThis.crypto?.subtle;
|
|
8
|
+
if (!subtle) {
|
|
9
|
+
throw new Error('WebCrypto SubtleCrypto is not available in this environment');
|
|
10
|
+
}
|
|
11
|
+
return subtle;
|
|
12
|
+
}
|
|
13
|
+
function toArrayBuffer(u8) {
|
|
14
|
+
const buf = new ArrayBuffer(u8.byteLength);
|
|
15
|
+
new Uint8Array(buf).set(u8);
|
|
16
|
+
return buf;
|
|
17
|
+
}
|
|
18
|
+
async function importAesKey(key, usage) {
|
|
19
|
+
if (key.length !== constants_1.AES_KEY_LEN) {
|
|
20
|
+
throw new Error(`AES key must be ${constants_1.AES_KEY_LEN} bytes, got ${key.length}`);
|
|
21
|
+
}
|
|
22
|
+
return getSubtle().importKey('raw', toArrayBuffer(key), { name: 'AES-GCM' }, false, [usage]);
|
|
23
|
+
}
|
|
24
|
+
async function aeadEncrypt(key, nonce, plaintext, aad) {
|
|
25
|
+
if (nonce.length !== constants_1.NONCE_LEN) {
|
|
26
|
+
throw new Error(`Nonce must be ${constants_1.NONCE_LEN} bytes, got ${nonce.length}`);
|
|
27
|
+
}
|
|
28
|
+
const cryptoKey = await importAesKey(key, 'encrypt');
|
|
29
|
+
const ct = await getSubtle().encrypt({ name: 'AES-GCM', iv: toArrayBuffer(nonce), additionalData: toArrayBuffer(aad) }, cryptoKey, toArrayBuffer(plaintext));
|
|
30
|
+
return new Uint8Array(ct);
|
|
31
|
+
}
|
|
32
|
+
async function aeadDecrypt(key, nonce, ciphertext, aad) {
|
|
33
|
+
if (nonce.length !== constants_1.NONCE_LEN) {
|
|
34
|
+
throw new Error(`Nonce must be ${constants_1.NONCE_LEN} bytes, got ${nonce.length}`);
|
|
35
|
+
}
|
|
36
|
+
const cryptoKey = await importAesKey(key, 'decrypt');
|
|
37
|
+
const pt = await getSubtle().decrypt({ name: 'AES-GCM', iv: toArrayBuffer(nonce), additionalData: toArrayBuffer(aad) }, cryptoKey, toArrayBuffer(ciphertext));
|
|
38
|
+
return new Uint8Array(pt);
|
|
39
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildHeaderAad = buildHeaderAad;
|
|
4
|
+
exports.encode = encode;
|
|
5
|
+
exports.decode = decode;
|
|
6
|
+
const constants_1 = require("./constants");
|
|
7
|
+
const types_1 = require("./types");
|
|
8
|
+
const HEADER_FIXED_LEN = constants_1.MAGIC_LEN + 2 + 2 + constants_1.ML_KEM_CT_LEN + constants_1.X25519_PK_LEN + constants_1.NONCE_LEN + 8;
|
|
9
|
+
function writeU16LE(view, offset, value) {
|
|
10
|
+
view.setUint16(offset, value, true);
|
|
11
|
+
}
|
|
12
|
+
function writeU64LE(view, offset, value) {
|
|
13
|
+
view.setBigUint64(offset, value, true);
|
|
14
|
+
}
|
|
15
|
+
function readU16LE(view, offset) {
|
|
16
|
+
return view.getUint16(offset, true);
|
|
17
|
+
}
|
|
18
|
+
function readU64LE(view, offset) {
|
|
19
|
+
return view.getBigUint64(offset, true);
|
|
20
|
+
}
|
|
21
|
+
function buildHeaderAad(container) {
|
|
22
|
+
const out = new Uint8Array(HEADER_FIXED_LEN);
|
|
23
|
+
const view = new DataView(out.buffer, out.byteOffset, out.byteLength);
|
|
24
|
+
let o = 0;
|
|
25
|
+
out.set(constants_1.MAGIC, o);
|
|
26
|
+
o += constants_1.MAGIC_LEN;
|
|
27
|
+
writeU16LE(view, o, container.version);
|
|
28
|
+
o += 2;
|
|
29
|
+
writeU16LE(view, o, container.algoId);
|
|
30
|
+
o += 2;
|
|
31
|
+
out.set(container.kemCtPq, o);
|
|
32
|
+
o += constants_1.ML_KEM_CT_LEN;
|
|
33
|
+
out.set(container.kemCtCls, o);
|
|
34
|
+
o += constants_1.X25519_PK_LEN;
|
|
35
|
+
out.set(container.nonce, o);
|
|
36
|
+
o += constants_1.NONCE_LEN;
|
|
37
|
+
writeU64LE(view, o, container.aeadPayloadLen);
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
function encode(container) {
|
|
41
|
+
if (container.kemCtPq.length !== constants_1.ML_KEM_CT_LEN) {
|
|
42
|
+
throw new Error(`KEM PQ ciphertext must be ${constants_1.ML_KEM_CT_LEN} bytes`);
|
|
43
|
+
}
|
|
44
|
+
if (container.kemCtCls.length !== constants_1.X25519_PK_LEN) {
|
|
45
|
+
throw new Error(`KEM classical ciphertext must be ${constants_1.X25519_PK_LEN} bytes`);
|
|
46
|
+
}
|
|
47
|
+
if (container.nonce.length !== constants_1.NONCE_LEN) {
|
|
48
|
+
throw new Error(`Nonce must be ${constants_1.NONCE_LEN} bytes`);
|
|
49
|
+
}
|
|
50
|
+
if (container.signature.length > 0xffff) {
|
|
51
|
+
throw new Error('Signature too long (>64KB)');
|
|
52
|
+
}
|
|
53
|
+
const aeadLen = BigInt(container.aeadPayload.length);
|
|
54
|
+
const header = buildHeaderAad({
|
|
55
|
+
version: container.version,
|
|
56
|
+
algoId: container.algoId,
|
|
57
|
+
kemCtPq: container.kemCtPq,
|
|
58
|
+
kemCtCls: container.kemCtCls,
|
|
59
|
+
nonce: container.nonce,
|
|
60
|
+
aeadPayloadLen: aeadLen,
|
|
61
|
+
});
|
|
62
|
+
const total = header.length + container.aeadPayload.length + 2 + container.signature.length;
|
|
63
|
+
const out = new Uint8Array(total);
|
|
64
|
+
const view = new DataView(out.buffer, out.byteOffset, out.byteLength);
|
|
65
|
+
let o = 0;
|
|
66
|
+
out.set(header, o);
|
|
67
|
+
o += header.length;
|
|
68
|
+
out.set(container.aeadPayload, o);
|
|
69
|
+
o += container.aeadPayload.length;
|
|
70
|
+
writeU16LE(view, o, container.signature.length);
|
|
71
|
+
o += 2;
|
|
72
|
+
out.set(container.signature, o);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
function decode(bytes) {
|
|
76
|
+
if (bytes.length < HEADER_FIXED_LEN + 2) {
|
|
77
|
+
throw new types_1.CorruptFileError('Input too short for .qsafe header');
|
|
78
|
+
}
|
|
79
|
+
for (let i = 0; i < constants_1.MAGIC_LEN; i++) {
|
|
80
|
+
if (bytes[i] !== constants_1.MAGIC[i]) {
|
|
81
|
+
throw new types_1.CorruptFileError('Bad magic bytes (not a .qsafe file)');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
85
|
+
let o = constants_1.MAGIC_LEN;
|
|
86
|
+
const version = readU16LE(view, o);
|
|
87
|
+
o += 2;
|
|
88
|
+
if (version !== constants_1.VERSION) {
|
|
89
|
+
throw new types_1.CorruptFileError(`Unsupported version ${version} (this build expects ${constants_1.VERSION})`);
|
|
90
|
+
}
|
|
91
|
+
const algoIdNum = readU16LE(view, o);
|
|
92
|
+
o += 2;
|
|
93
|
+
if (algoIdNum !== constants_1.ALGO_ID.HYBRID_V1) {
|
|
94
|
+
throw new types_1.CorruptFileError(`Unknown algorithm suite 0x${algoIdNum.toString(16)}`);
|
|
95
|
+
}
|
|
96
|
+
const kemCtPq = bytes.slice(o, o + constants_1.ML_KEM_CT_LEN);
|
|
97
|
+
o += constants_1.ML_KEM_CT_LEN;
|
|
98
|
+
const kemCtCls = bytes.slice(o, o + constants_1.X25519_PK_LEN);
|
|
99
|
+
o += constants_1.X25519_PK_LEN;
|
|
100
|
+
const nonce = bytes.slice(o, o + constants_1.NONCE_LEN);
|
|
101
|
+
o += constants_1.NONCE_LEN;
|
|
102
|
+
const aeadLenBig = readU64LE(view, o);
|
|
103
|
+
o += 8;
|
|
104
|
+
if (aeadLenBig > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
105
|
+
throw new types_1.CorruptFileError('AEAD payload length exceeds safe range');
|
|
106
|
+
}
|
|
107
|
+
const aeadLen = Number(aeadLenBig);
|
|
108
|
+
if (bytes.length < o + aeadLen + 2) {
|
|
109
|
+
throw new types_1.CorruptFileError('Truncated AEAD payload');
|
|
110
|
+
}
|
|
111
|
+
const aeadPayload = bytes.slice(o, o + aeadLen);
|
|
112
|
+
o += aeadLen;
|
|
113
|
+
const sigLen = readU16LE(view, o);
|
|
114
|
+
o += 2;
|
|
115
|
+
if (bytes.length < o + sigLen) {
|
|
116
|
+
throw new types_1.CorruptFileError('Truncated signature');
|
|
117
|
+
}
|
|
118
|
+
const signature = bytes.slice(o, o + sigLen);
|
|
119
|
+
o += sigLen;
|
|
120
|
+
if (o !== bytes.length) {
|
|
121
|
+
throw new types_1.CorruptFileError(`Trailing bytes after signature (${bytes.length - o} bytes)`);
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
version,
|
|
125
|
+
algoId: algoIdNum,
|
|
126
|
+
kemCtPq,
|
|
127
|
+
kemCtCls,
|
|
128
|
+
nonce,
|
|
129
|
+
aeadPayload,
|
|
130
|
+
signature,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_MESSAGE_PLAINTEXT = exports.ARGON2ID_SALT_LEN = exports.ARGON2ID_PARAMS_PROD = exports.AES_GCM_TAG_LEN = exports.NONCE_LEN = exports.AES_KEY_LEN = exports.ML_DSA_SIG_LEN = exports.ML_DSA_SK_LEN = exports.ML_DSA_PK_LEN = exports.ML_DSA_SEED_LEN = exports.X25519_SK_LEN = exports.X25519_PK_LEN = exports.ML_KEM_CT_LEN = exports.ML_KEM_SK_LEN = exports.ML_KEM_PK_LEN = exports.ML_KEM_SEED_LEN = exports.WALLET_KDF_MESSAGE = exports.KEYFILE_AAD = exports.HKDF_INFO_KEYFILE_WRAP = exports.HKDF_INFO_AES_KEY = exports.HKDF_INFO_MLDSA_SEED = exports.HKDF_INFO_X25519_SEED = exports.HKDF_INFO_MLKEM_SEED = exports.HKDF_SALT_HYBRID = exports.HKDF_SALT = exports.X402_CONTEXT = exports.MESSAGE_CONTEXT = exports.FILE_CONTEXT = exports.ALGO_ID = exports.VERSION = exports.MAGIC_LEN = exports.MAGIC = void 0;
|
|
4
|
+
exports.MAGIC = new Uint8Array([0x51, 0x53, 0x41, 0x46, 0x45]);
|
|
5
|
+
exports.MAGIC_LEN = 5;
|
|
6
|
+
exports.VERSION = 1;
|
|
7
|
+
exports.ALGO_ID = {
|
|
8
|
+
HYBRID_V1: 0x0001,
|
|
9
|
+
};
|
|
10
|
+
exports.FILE_CONTEXT = 'qsafe-v1-file';
|
|
11
|
+
exports.MESSAGE_CONTEXT = 'qsafe-v1-message';
|
|
12
|
+
exports.X402_CONTEXT = 'qsafe-v1-x402';
|
|
13
|
+
exports.HKDF_SALT = 'qsafe-v1';
|
|
14
|
+
exports.HKDF_SALT_HYBRID = 'qsafe-v1-hybrid';
|
|
15
|
+
exports.HKDF_INFO_MLKEM_SEED = 'mlkem-seed';
|
|
16
|
+
exports.HKDF_INFO_X25519_SEED = 'x25519-seed';
|
|
17
|
+
exports.HKDF_INFO_MLDSA_SEED = 'mldsa-seed';
|
|
18
|
+
exports.HKDF_INFO_AES_KEY = 'aes-256-gcm';
|
|
19
|
+
exports.HKDF_INFO_KEYFILE_WRAP = 'keyfile-wrap-key';
|
|
20
|
+
exports.KEYFILE_AAD = 'qsafe-keyfile-v1';
|
|
21
|
+
exports.WALLET_KDF_MESSAGE = 'qsafe-key-derivation-v1';
|
|
22
|
+
exports.ML_KEM_SEED_LEN = 64;
|
|
23
|
+
exports.ML_KEM_PK_LEN = 1184;
|
|
24
|
+
exports.ML_KEM_SK_LEN = 2400;
|
|
25
|
+
exports.ML_KEM_CT_LEN = 1088;
|
|
26
|
+
exports.X25519_PK_LEN = 32;
|
|
27
|
+
exports.X25519_SK_LEN = 32;
|
|
28
|
+
exports.ML_DSA_SEED_LEN = 32;
|
|
29
|
+
exports.ML_DSA_PK_LEN = 1952;
|
|
30
|
+
exports.ML_DSA_SK_LEN = 4032;
|
|
31
|
+
exports.ML_DSA_SIG_LEN = 3309;
|
|
32
|
+
exports.AES_KEY_LEN = 32;
|
|
33
|
+
exports.NONCE_LEN = 12;
|
|
34
|
+
exports.AES_GCM_TAG_LEN = 16;
|
|
35
|
+
exports.ARGON2ID_PARAMS_PROD = { t: 3, m: 65536, p: 1 };
|
|
36
|
+
exports.ARGON2ID_SALT_LEN = 16;
|
|
37
|
+
exports.MAX_MESSAGE_PLAINTEXT = 64 * 1024;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hybridEncapsulate = hybridEncapsulate;
|
|
4
|
+
exports.hybridDecapsulate = hybridDecapsulate;
|
|
5
|
+
exports.sign = sign;
|
|
6
|
+
exports.verify = verify;
|
|
7
|
+
const ed25519_1 = require("@noble/curves/ed25519");
|
|
8
|
+
const utils_1 = require("@noble/hashes/utils");
|
|
9
|
+
const ml_dsa_1 = require("@noble/post-quantum/ml-dsa");
|
|
10
|
+
const ml_kem_1 = require("@noble/post-quantum/ml-kem");
|
|
11
|
+
const constants_1 = require("./constants");
|
|
12
|
+
const hkdf_1 = require("./hkdf");
|
|
13
|
+
const utf8 = new TextEncoder();
|
|
14
|
+
function combineSharedSecrets(ssPq, ssCls) {
|
|
15
|
+
const ikm = new Uint8Array(ssPq.length + ssCls.length);
|
|
16
|
+
ikm.set(ssPq, 0);
|
|
17
|
+
ikm.set(ssCls, ssPq.length);
|
|
18
|
+
return (0, hkdf_1.hkdfDerive)(ikm, constants_1.HKDF_SALT_HYBRID, constants_1.HKDF_INFO_AES_KEY, constants_1.AES_KEY_LEN);
|
|
19
|
+
}
|
|
20
|
+
function hybridEncapsulate(recipient) {
|
|
21
|
+
const pq = ml_kem_1.ml_kem768.encapsulate(recipient.mlkem);
|
|
22
|
+
const ephSk = (0, utils_1.randomBytes)(constants_1.X25519_SK_LEN);
|
|
23
|
+
const ephPk = ed25519_1.x25519.getPublicKey(ephSk);
|
|
24
|
+
const ssCls = ed25519_1.x25519.getSharedSecret(ephSk, recipient.x25519);
|
|
25
|
+
const aesKey = combineSharedSecrets(pq.sharedSecret, ssCls);
|
|
26
|
+
return {
|
|
27
|
+
kemCtPq: pq.cipherText,
|
|
28
|
+
kemCtCls: ephPk,
|
|
29
|
+
aesKey,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function hybridDecapsulate(sk, kemCtPq, kemCtCls) {
|
|
33
|
+
const ssPq = ml_kem_1.ml_kem768.decapsulate(kemCtPq, sk.mlkem);
|
|
34
|
+
const ssCls = ed25519_1.x25519.getSharedSecret(sk.x25519, kemCtCls);
|
|
35
|
+
return combineSharedSecrets(ssPq, ssCls);
|
|
36
|
+
}
|
|
37
|
+
function sign(secretKey, msg, context) {
|
|
38
|
+
const signer = ml_dsa_1.ml_dsa65.sign;
|
|
39
|
+
return signer(secretKey, msg, utf8.encode(context));
|
|
40
|
+
}
|
|
41
|
+
function verify(publicKey, msg, signature, context) {
|
|
42
|
+
const verifier = ml_dsa_1.ml_dsa65.verify;
|
|
43
|
+
return verifier(publicKey, msg, signature, utf8.encode(context));
|
|
44
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.encryptFile = encryptFile;
|
|
4
|
+
exports.decryptFile = decryptFile;
|
|
5
|
+
const utils_1 = require("@noble/hashes/utils");
|
|
6
|
+
const aead_1 = require("./aead");
|
|
7
|
+
const codec_1 = require("./codec");
|
|
8
|
+
const constants_1 = require("./constants");
|
|
9
|
+
const crypto_1 = require("./crypto");
|
|
10
|
+
const types_1 = require("./types");
|
|
11
|
+
const utf8Encoder = new TextEncoder();
|
|
12
|
+
const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
|
|
13
|
+
function packAeadPlaintext(fileBytes, filename) {
|
|
14
|
+
const filenameBytes = utf8Encoder.encode(filename);
|
|
15
|
+
if (filenameBytes.length > 0xffff) {
|
|
16
|
+
throw new Error('Filename too long (>64KB UTF-8)');
|
|
17
|
+
}
|
|
18
|
+
const out = new Uint8Array(2 + filenameBytes.length + 8 + fileBytes.length);
|
|
19
|
+
const view = new DataView(out.buffer, out.byteOffset, out.byteLength);
|
|
20
|
+
let o = 0;
|
|
21
|
+
view.setUint16(o, filenameBytes.length, true);
|
|
22
|
+
o += 2;
|
|
23
|
+
out.set(filenameBytes, o);
|
|
24
|
+
o += filenameBytes.length;
|
|
25
|
+
view.setBigUint64(o, BigInt(fileBytes.length), true);
|
|
26
|
+
o += 8;
|
|
27
|
+
out.set(fileBytes, o);
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
function unpackAeadPlaintext(plaintext) {
|
|
31
|
+
if (plaintext.length < 10) {
|
|
32
|
+
throw new types_1.SignatureError();
|
|
33
|
+
}
|
|
34
|
+
const view = new DataView(plaintext.buffer, plaintext.byteOffset, plaintext.byteLength);
|
|
35
|
+
let o = 0;
|
|
36
|
+
const filenameLen = view.getUint16(o, true);
|
|
37
|
+
o += 2;
|
|
38
|
+
if (plaintext.length < o + filenameLen + 8) {
|
|
39
|
+
throw new types_1.SignatureError();
|
|
40
|
+
}
|
|
41
|
+
const filename = utf8Decoder.decode(plaintext.slice(o, o + filenameLen));
|
|
42
|
+
o += filenameLen;
|
|
43
|
+
const originalSize = view.getBigUint64(o, true);
|
|
44
|
+
o += 8;
|
|
45
|
+
const fileBytes = plaintext.slice(o);
|
|
46
|
+
if (BigInt(fileBytes.length) !== originalSize) {
|
|
47
|
+
throw new types_1.SignatureError();
|
|
48
|
+
}
|
|
49
|
+
return { fileBytes, filename, originalSize };
|
|
50
|
+
}
|
|
51
|
+
async function encryptFile(fileBytes, filename, recipient, signer) {
|
|
52
|
+
const { kemCtPq, kemCtCls, aesKey } = (0, crypto_1.hybridEncapsulate)(recipient);
|
|
53
|
+
const nonce = (0, utils_1.randomBytes)(constants_1.NONCE_LEN);
|
|
54
|
+
const aeadPlaintext = packAeadPlaintext(fileBytes, filename);
|
|
55
|
+
const header = (0, codec_1.buildHeaderAad)({
|
|
56
|
+
version: constants_1.VERSION,
|
|
57
|
+
algoId: constants_1.ALGO_ID.HYBRID_V1,
|
|
58
|
+
kemCtPq,
|
|
59
|
+
kemCtCls,
|
|
60
|
+
nonce,
|
|
61
|
+
aeadPayloadLen: BigInt(aeadPlaintext.length + 16),
|
|
62
|
+
});
|
|
63
|
+
const aeadPayload = await (0, aead_1.aeadEncrypt)(aesKey, nonce, aeadPlaintext, header);
|
|
64
|
+
const signedBytes = new Uint8Array(header.length + aeadPayload.length);
|
|
65
|
+
signedBytes.set(header, 0);
|
|
66
|
+
signedBytes.set(aeadPayload, header.length);
|
|
67
|
+
const signature = (0, crypto_1.sign)(signer.mldsa, signedBytes, constants_1.FILE_CONTEXT);
|
|
68
|
+
return (0, codec_1.encode)({
|
|
69
|
+
version: constants_1.VERSION,
|
|
70
|
+
algoId: constants_1.ALGO_ID.HYBRID_V1,
|
|
71
|
+
kemCtPq,
|
|
72
|
+
kemCtCls,
|
|
73
|
+
nonce,
|
|
74
|
+
aeadPayload,
|
|
75
|
+
signature,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async function decryptFile(qsafeBytes, recipient, expectedSigner) {
|
|
79
|
+
const container = (0, codec_1.decode)(qsafeBytes);
|
|
80
|
+
const header = (0, codec_1.buildHeaderAad)({
|
|
81
|
+
version: container.version,
|
|
82
|
+
algoId: container.algoId,
|
|
83
|
+
kemCtPq: container.kemCtPq,
|
|
84
|
+
kemCtCls: container.kemCtCls,
|
|
85
|
+
nonce: container.nonce,
|
|
86
|
+
aeadPayloadLen: BigInt(container.aeadPayload.length),
|
|
87
|
+
});
|
|
88
|
+
const signedBytes = new Uint8Array(header.length + container.aeadPayload.length);
|
|
89
|
+
signedBytes.set(header, 0);
|
|
90
|
+
signedBytes.set(container.aeadPayload, header.length);
|
|
91
|
+
if (!(0, crypto_1.verify)(expectedSigner.mldsa, signedBytes, container.signature, constants_1.FILE_CONTEXT)) {
|
|
92
|
+
throw new types_1.SignatureError();
|
|
93
|
+
}
|
|
94
|
+
const aesKey = (0, crypto_1.hybridDecapsulate)(recipient, container.kemCtPq, container.kemCtCls);
|
|
95
|
+
const plaintext = await (0, aead_1.aeadDecrypt)(aesKey, container.nonce, container.aeadPayload, header);
|
|
96
|
+
return unpackAeadPlaintext(plaintext);
|
|
97
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hkdfDerive = hkdfDerive;
|
|
4
|
+
const hkdf_1 = require("@noble/hashes/hkdf");
|
|
5
|
+
const sha2_1 = require("@noble/hashes/sha2");
|
|
6
|
+
const utf8 = new TextEncoder();
|
|
7
|
+
function hkdfDerive(ikm, salt, info, length) {
|
|
8
|
+
return (0, hkdf_1.hkdf)(sha2_1.sha256, ikm, utf8.encode(salt), utf8.encode(info), length);
|
|
9
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.wrapKeyfile = exports.unwrapKeyfile = exports.encryptFile = exports.decryptFile = exports.encode = exports.decode = exports.buildHeaderAad = exports.verify = exports.sign = exports.hybridEncapsulate = exports.hybridDecapsulate = exports.aeadEncrypt = exports.aeadDecrypt = exports.keysFromSeeds = exports.deriveSeedsFromWalletSignature = exports.deriveSeedsFromRandomness = exports.deriveSeedsFromIkm = exports.assertEoaSignatureShape = void 0;
|
|
18
|
+
__exportStar(require("./constants"), exports);
|
|
19
|
+
__exportStar(require("./types"), exports);
|
|
20
|
+
var kdf_1 = require("./kdf");
|
|
21
|
+
Object.defineProperty(exports, "assertEoaSignatureShape", { enumerable: true, get: function () { return kdf_1.assertEoaSignatureShape; } });
|
|
22
|
+
Object.defineProperty(exports, "deriveSeedsFromIkm", { enumerable: true, get: function () { return kdf_1.deriveSeedsFromIkm; } });
|
|
23
|
+
Object.defineProperty(exports, "deriveSeedsFromRandomness", { enumerable: true, get: function () { return kdf_1.deriveSeedsFromRandomness; } });
|
|
24
|
+
Object.defineProperty(exports, "deriveSeedsFromWalletSignature", { enumerable: true, get: function () { return kdf_1.deriveSeedsFromWalletSignature; } });
|
|
25
|
+
var keys_1 = require("./keys");
|
|
26
|
+
Object.defineProperty(exports, "keysFromSeeds", { enumerable: true, get: function () { return keys_1.keysFromSeeds; } });
|
|
27
|
+
var aead_1 = require("./aead");
|
|
28
|
+
Object.defineProperty(exports, "aeadDecrypt", { enumerable: true, get: function () { return aead_1.aeadDecrypt; } });
|
|
29
|
+
Object.defineProperty(exports, "aeadEncrypt", { enumerable: true, get: function () { return aead_1.aeadEncrypt; } });
|
|
30
|
+
var crypto_1 = require("./crypto");
|
|
31
|
+
Object.defineProperty(exports, "hybridDecapsulate", { enumerable: true, get: function () { return crypto_1.hybridDecapsulate; } });
|
|
32
|
+
Object.defineProperty(exports, "hybridEncapsulate", { enumerable: true, get: function () { return crypto_1.hybridEncapsulate; } });
|
|
33
|
+
Object.defineProperty(exports, "sign", { enumerable: true, get: function () { return crypto_1.sign; } });
|
|
34
|
+
Object.defineProperty(exports, "verify", { enumerable: true, get: function () { return crypto_1.verify; } });
|
|
35
|
+
var codec_1 = require("./codec");
|
|
36
|
+
Object.defineProperty(exports, "buildHeaderAad", { enumerable: true, get: function () { return codec_1.buildHeaderAad; } });
|
|
37
|
+
Object.defineProperty(exports, "decode", { enumerable: true, get: function () { return codec_1.decode; } });
|
|
38
|
+
Object.defineProperty(exports, "encode", { enumerable: true, get: function () { return codec_1.encode; } });
|
|
39
|
+
var file_1 = require("./file");
|
|
40
|
+
Object.defineProperty(exports, "decryptFile", { enumerable: true, get: function () { return file_1.decryptFile; } });
|
|
41
|
+
Object.defineProperty(exports, "encryptFile", { enumerable: true, get: function () { return file_1.encryptFile; } });
|
|
42
|
+
var keyfile_1 = require("./keyfile");
|
|
43
|
+
Object.defineProperty(exports, "unwrapKeyfile", { enumerable: true, get: function () { return keyfile_1.unwrapKeyfile; } });
|
|
44
|
+
Object.defineProperty(exports, "wrapKeyfile", { enumerable: true, get: function () { return keyfile_1.wrapKeyfile; } });
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deriveSeedsFromIkm = deriveSeedsFromIkm;
|
|
4
|
+
exports.assertEoaSignatureShape = assertEoaSignatureShape;
|
|
5
|
+
exports.deriveSeedsFromWalletSignature = deriveSeedsFromWalletSignature;
|
|
6
|
+
exports.deriveSeedsFromRandomness = deriveSeedsFromRandomness;
|
|
7
|
+
const utils_1 = require("@noble/hashes/utils");
|
|
8
|
+
const constants_1 = require("./constants");
|
|
9
|
+
const hkdf_1 = require("./hkdf");
|
|
10
|
+
function hexToBytes(hex) {
|
|
11
|
+
const clean = hex.startsWith('0x') || hex.startsWith('0X') ? hex.slice(2) : hex;
|
|
12
|
+
if (clean.length % 2 !== 0) {
|
|
13
|
+
throw new Error('Invalid hex string: odd length');
|
|
14
|
+
}
|
|
15
|
+
const out = new Uint8Array(clean.length / 2);
|
|
16
|
+
for (let i = 0; i < out.length; i++) {
|
|
17
|
+
const byte = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
18
|
+
if (Number.isNaN(byte)) {
|
|
19
|
+
throw new Error('Invalid hex string');
|
|
20
|
+
}
|
|
21
|
+
out[i] = byte;
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
function deriveSeedsFromIkm(ikm) {
|
|
26
|
+
if (ikm.length === 0) {
|
|
27
|
+
throw new Error('IKM must be non-empty');
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
mlkem: (0, hkdf_1.hkdfDerive)(ikm, constants_1.HKDF_SALT, constants_1.HKDF_INFO_MLKEM_SEED, constants_1.ML_KEM_SEED_LEN),
|
|
31
|
+
x25519: (0, hkdf_1.hkdfDerive)(ikm, constants_1.HKDF_SALT, constants_1.HKDF_INFO_X25519_SEED, constants_1.X25519_SK_LEN),
|
|
32
|
+
mldsa: (0, hkdf_1.hkdfDerive)(ikm, constants_1.HKDF_SALT, constants_1.HKDF_INFO_MLDSA_SEED, constants_1.ML_DSA_SEED_LEN),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const EOA_SIG_HEX_RE = /^0x[0-9a-fA-F]{130}$/;
|
|
36
|
+
function assertEoaSignatureShape(signatureHex) {
|
|
37
|
+
if (!EOA_SIG_HEX_RE.test(signatureHex)) {
|
|
38
|
+
throw new Error('Wallet signature must be a 65-byte EOA signature (0x + 130 hex chars). ' +
|
|
39
|
+
'Smart-contract wallet signatures are not supported for key derivation.');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function deriveSeedsFromWalletSignature(signatureHex) {
|
|
43
|
+
assertEoaSignatureShape(signatureHex);
|
|
44
|
+
return deriveSeedsFromIkm(hexToBytes(signatureHex));
|
|
45
|
+
}
|
|
46
|
+
function deriveSeedsFromRandomness() {
|
|
47
|
+
return {
|
|
48
|
+
mlkem: (0, utils_1.randomBytes)(constants_1.ML_KEM_SEED_LEN),
|
|
49
|
+
x25519: (0, utils_1.randomBytes)(constants_1.X25519_SK_LEN),
|
|
50
|
+
mldsa: (0, utils_1.randomBytes)(constants_1.ML_DSA_SEED_LEN),
|
|
51
|
+
};
|
|
52
|
+
}
|