@noble/post-quantum 0.7.0 → 0.7.1

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/webcrypto.js ADDED
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Friendly async wrappers over ML-KEM and ML-KEM-768 + X25519 from built-in WebCrypto.
3
+ * Private keys use the same raw seed accepted by the synchronous implementations' `keygen(seed)`;
4
+ * they are not expanded decapsulation keys.
5
+ *
6
+ * # WebCrypto quirks
7
+ *
8
+ * - The algorithms are experimental: a runtime can expose `encapsulateBits` and friends while
9
+ * implementing none of them, so support is probed with a full round-trip in `isSupported()`.
10
+ * - `MLKEM768-X25519` accepts `raw-seed` on import, but has no `raw-seed` / `raw-public` export.
11
+ * Its key bytes are read out of the JWK `priv` / `pub` members instead.
12
+ * - base64url is hand-rolled: scure-base's `base64urlnopad` would do, but this module must not add
13
+ * dependencies, and importing the synchronous implementations for four byte lengths would pull
14
+ * the whole lattice math into a WebCrypto-only entrypoint.
15
+ * @module
16
+ */
17
+ /*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */
18
+ import { abytes, cleanBytes, copyBytes, equalBytes } from "./utils.js";
19
+ function _subtle() {
20
+ const cr = typeof globalThis === 'object' ? globalThis.crypto : null;
21
+ const sb = cr?.subtle;
22
+ if (typeof sb === 'object' && sb != null)
23
+ return sb;
24
+ throw new Error('crypto.subtle must be defined');
25
+ }
26
+ const PRIVATE_USAGES = ['decapsulateBits'];
27
+ const PUBLIC_USAGES = ['encapsulateBits'];
28
+ const ALL_USAGES = ['encapsulateBits', 'decapsulateBits'];
29
+ const PROBED_METHODS = ['encapsulateBits', 'decapsulateBits', 'getPublicKey'];
30
+ const SHARED_SECRET_LENGTH = 32;
31
+ const arrayBufferByteLength = Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, 'byteLength')?.get;
32
+ // Typed-array constructors also accept numbers and array-like objects, which would turn malformed
33
+ // provider results into newly allocated all-zero buffers. Use the intrinsic getter as a cross-realm
34
+ // ArrayBuffer brand check before constructing a view.
35
+ function providerBytes(value, title) {
36
+ try {
37
+ if (arrayBufferByteLength === undefined)
38
+ throw new TypeError('missing ArrayBuffer getter');
39
+ arrayBufferByteLength.call(value);
40
+ }
41
+ catch {
42
+ throw new TypeError(`WebCrypto "${title}" expected ArrayBuffer`);
43
+ }
44
+ return new Uint8Array(value);
45
+ }
46
+ function base64url(bytes) {
47
+ let binary = '';
48
+ for (let i = 0; i < bytes.length; i++)
49
+ binary += String.fromCharCode(bytes[i]);
50
+ return globalThis.btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
51
+ }
52
+ function debase64url(value, title) {
53
+ if (typeof value !== 'string')
54
+ throw new Error(`WebCrypto JWK is missing ${title}`);
55
+ const base64 = value.replace(/-/g, '+').replace(/_/g, '/');
56
+ const padding = base64.length % 4;
57
+ const binary = globalThis.atob(base64 + (padding ? '='.repeat(4 - padding) : ''));
58
+ const bytes = new Uint8Array(binary.length);
59
+ for (let i = 0; i < binary.length; i++)
60
+ bytes[i] = binary.charCodeAt(i);
61
+ return bytes;
62
+ }
63
+ /** Keys serialized through the `raw-seed` / `raw-public` WebCrypto formats. */
64
+ const rawCodec = {
65
+ importPublic: (subtle, algorithm, publicKey) => subtle.importKey('raw-public', publicKey, algorithm, false, PUBLIC_USAGES),
66
+ exportPublic: async (subtle, key, length) => abytes(new Uint8Array(await subtle.exportKey('raw-public', key)), length, 'publicKey'),
67
+ exportPrivate: async (subtle, key, length) => abytes(new Uint8Array(await subtle.exportKey('raw-seed', key)), length, 'secretKey'),
68
+ };
69
+ /** Keys serialized through the JWK `pub` / `priv` members, for algorithms without raw export. */
70
+ function jwkCodec(webCryptoName) {
71
+ const exportMember = async (subtle, key, member, length) => {
72
+ const jwk = await subtle.exportKey('jwk', key);
73
+ return abytes(debase64url(jwk?.[member], member), length, member);
74
+ };
75
+ return {
76
+ importPublic: (subtle, algorithm, publicKey) => subtle.importKey('jwk', {
77
+ kty: 'AKP',
78
+ alg: webCryptoName,
79
+ key_ops: PUBLIC_USAGES,
80
+ ext: true,
81
+ pub: base64url(publicKey),
82
+ }, algorithm, true, PUBLIC_USAGES),
83
+ exportPublic: (subtle, key, length) => exportMember(subtle, key, 'pub', length),
84
+ exportPrivate: (subtle, key, length) => exportMember(subtle, key, 'priv', length),
85
+ };
86
+ }
87
+ function createWebCryptoKEM(webCryptoName, lengths, codec) {
88
+ const algorithm = Object.freeze({ name: webCryptoName });
89
+ const frozen = Object.freeze(lengths);
90
+ let supported;
91
+ // Imports a raw seed, wiping the detached copy as soon as WebCrypto has consumed it.
92
+ const importSecret = async (subtle, secretKey) => {
93
+ const secret = copyBytes(abytes(secretKey, frozen.secretKey, 'secretKey'));
94
+ try {
95
+ return await subtle.importKey('raw-seed', secret, algorithm, false, PRIVATE_USAGES);
96
+ }
97
+ finally {
98
+ cleanBytes(secret);
99
+ }
100
+ };
101
+ const getPublicKey = async (secretKey) => {
102
+ const subtle = _subtle();
103
+ const privateKey = await importSecret(subtle, secretKey);
104
+ const publicKey = await subtle.getPublicKey(privateKey, PUBLIC_USAGES);
105
+ return codec.exportPublic(subtle, publicKey, frozen.publicKey);
106
+ };
107
+ const keygen = async (seed) => {
108
+ if (seed !== undefined) {
109
+ const secretKey = copyBytes(abytes(seed, frozen.seed, 'seed'));
110
+ try {
111
+ const publicKey = await getPublicKey(secretKey);
112
+ return { secretKey: secretKey, publicKey };
113
+ }
114
+ catch (error) {
115
+ cleanBytes(secretKey);
116
+ throw error;
117
+ }
118
+ }
119
+ const subtle = _subtle();
120
+ const keys = await subtle.generateKey(algorithm, true, ALL_USAGES);
121
+ const [secretKey, publicKey] = await Promise.all([
122
+ codec.exportPrivate(subtle, keys.privateKey, frozen.secretKey),
123
+ codec.exportPublic(subtle, keys.publicKey, frozen.publicKey),
124
+ ]);
125
+ return { secretKey, publicKey };
126
+ };
127
+ const encapsulate = async (publicKey) => {
128
+ const subtle = _subtle();
129
+ // No copy: the bytes are public and WebCrypto consumes them before the next await.
130
+ const key = await codec.importPublic(subtle, algorithm, abytes(publicKey, frozen.publicKey, 'publicKey'));
131
+ const { ciphertext, sharedKey } = await subtle.encapsulateBits(algorithm, key);
132
+ // Provider outputs cross a trust boundary: some runtimes expose experimental methods with
133
+ // incomplete implementations. Materialize the secret first so every later rejection can wipe
134
+ // it, including a malformed ciphertext result.
135
+ const sharedSecret = providerBytes(sharedKey, 'sharedKey');
136
+ try {
137
+ const cipherText = abytes(providerBytes(ciphertext, 'ciphertext'), frozen.cipherText, 'cipherText');
138
+ abytes(sharedSecret, SHARED_SECRET_LENGTH, 'sharedSecret');
139
+ return { cipherText, sharedSecret: sharedSecret };
140
+ }
141
+ catch (error) {
142
+ cleanBytes(sharedSecret);
143
+ throw error;
144
+ }
145
+ };
146
+ const decapsulate = async (cipherText, secretKey) => {
147
+ // Snapshot the ciphertext: the key import below awaits before WebCrypto reads these bytes.
148
+ const cipher = copyBytes(abytes(cipherText, frozen.cipherText, 'cipherText'));
149
+ const subtle = _subtle();
150
+ const key = await importSecret(subtle, secretKey);
151
+ const sharedSecret = providerBytes(await subtle.decapsulateBits(algorithm, key, cipher), 'sharedKey');
152
+ try {
153
+ return abytes(sharedSecret, SHARED_SECRET_LENGTH, 'sharedSecret');
154
+ }
155
+ catch (error) {
156
+ cleanBytes(sharedSecret);
157
+ throw error;
158
+ }
159
+ };
160
+ return Object.freeze({
161
+ webCryptoName,
162
+ lengths: frozen,
163
+ async isSupported() {
164
+ if (supported !== undefined)
165
+ return supported;
166
+ let secretKey;
167
+ let encapsulatedSecret;
168
+ let decapsulatedSecret;
169
+ try {
170
+ const subtle = _subtle();
171
+ for (const method of PROBED_METHODS)
172
+ if (typeof subtle[method] !== 'function')
173
+ return (supported = false);
174
+ const generated = await keygen();
175
+ secretKey = generated.secretKey;
176
+ const { publicKey } = generated;
177
+ const encapsulated = await encapsulate(publicKey);
178
+ encapsulatedSecret = encapsulated.sharedSecret;
179
+ decapsulatedSecret = await decapsulate(encapsulated.cipherText, secretKey);
180
+ const ok = equalBytes(await getPublicKey(secretKey), publicKey) &&
181
+ equalBytes(encapsulatedSecret, decapsulatedSecret);
182
+ return (supported = ok);
183
+ }
184
+ catch {
185
+ return (supported = false);
186
+ }
187
+ finally {
188
+ // A failed provider can throw at any point after producing secret material. Never let the
189
+ // support probe retain a generated seed or shared-secret output that it received.
190
+ if (secretKey !== undefined)
191
+ cleanBytes(secretKey);
192
+ if (encapsulatedSecret !== undefined)
193
+ cleanBytes(encapsulatedSecret);
194
+ if (decapsulatedSecret !== undefined)
195
+ cleanBytes(decapsulatedSecret);
196
+ }
197
+ },
198
+ keygen,
199
+ getPublicKey,
200
+ encapsulate,
201
+ decapsulate,
202
+ });
203
+ }
204
+ const mlKem = (name, publicKey, cipherText) => createWebCryptoKEM(name, { seed: 64, secretKey: 64, publicKey, cipherText }, rawCodec);
205
+ /** WebCrypto ML-KEM-512 wrapper. */
206
+ export const ml_kem512 = /* @__PURE__ */ mlKem('ML-KEM-512', 800, 768);
207
+ /** WebCrypto ML-KEM-768 wrapper. */
208
+ export const ml_kem768 = /* @__PURE__ */ mlKem('ML-KEM-768', 1184, 1088);
209
+ /** WebCrypto ML-KEM-1024 wrapper. */
210
+ export const ml_kem1024 = /* @__PURE__ */ mlKem('ML-KEM-1024', 1568, 1568);
211
+ /** WebCrypto ML-KEM-768 + X25519 (X-Wing) wrapper. */
212
+ export const ml_kem768_x25519 = /* @__PURE__ */ createWebCryptoKEM('MLKEM768-X25519', { seed: 32, secretKey: 32, publicKey: 1216, cipherText: 1120 },
213
+ /* @__PURE__ */ jwkCodec('MLKEM768-X25519'));