@noble/post-quantum 0.6.0 → 0.7.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/src/_crystals.ts CHANGED
@@ -6,7 +6,14 @@
6
6
  import { FFTCore, reverseBits } from '@noble/curves/abstract/fft.js';
7
7
  import { shake128, shake256 } from '@noble/hashes/sha3.js';
8
8
  import type { TypedArray } from '@noble/hashes/utils.js';
9
- import { type BytesCoderLen, cleanBytes, type Coder, getMask } from './utils.ts';
9
+ import {
10
+ type BytesCoderLen,
11
+ cleanBytes,
12
+ type Coder,
13
+ getMask,
14
+ type TArg,
15
+ type TRet,
16
+ } from './utils.ts';
10
17
 
11
18
  /** Extendable-output reader used by the CRYSTALS implementations. */
12
19
  export type XOF = (
@@ -61,6 +68,25 @@ export type CrystalOpts<T extends TypedArray> = {
61
68
  /** Constructor function for typed polynomial containers. */
62
69
  export type TypedCons<T extends TypedArray> = (n: number) => T;
63
70
 
71
+ type Crystals<T extends TypedArray> = {
72
+ mod: (a: number, modulo?: number) => number;
73
+ smod: (a: number, modulo?: number) => number;
74
+ nttZetas: T;
75
+ NTT: {
76
+ /**
77
+ * Forward transform in place. Mutates and returns `r`.
78
+ * Kyber-mode input coefficients must already use canonical representatives in `[0, Q)`.
79
+ */
80
+ encode: (r: T) => T;
81
+ /**
82
+ * Inverse transform in place. Mutates and returns `r`.
83
+ * Kyber-mode input coefficients must already use canonical representatives in `[0, Q)`.
84
+ */
85
+ decode: (r: T) => T;
86
+ };
87
+ bitsCoder: (d: number, c: Coder<number, number>) => BytesCoderLen<T>;
88
+ };
89
+
64
90
  /**
65
91
  * Creates shared modular arithmetic, NTT, and packing helpers for CRYSTALS schemes.
66
92
  * @param opts - Polynomial and transform parameters. See {@link CrystalOpts}.
@@ -80,20 +106,7 @@ export type TypedCons<T extends TypedArray> = (n: number) => T;
80
106
  * const reduced = crystals.mod(-1);
81
107
  * ```
82
108
  */
83
- export const genCrystals = <T extends TypedArray>(
84
- opts: CrystalOpts<T>
85
- ): {
86
- mod: (a: number, modulo?: number) => number;
87
- smod: (a: number, modulo?: number) => number;
88
- nttZetas: T;
89
- NTT: {
90
- /** Forward transform in place. Mutates and returns `r`. */
91
- encode: (r: T) => T;
92
- /** Inverse transform in place. Mutates and returns `r`. */
93
- decode: (r: T) => T;
94
- };
95
- bitsCoder: (d: number, c: Coder<number, number>) => BytesCoderLen<T>;
96
- } => {
109
+ export const genCrystals = <T extends TypedArray>(opts: CrystalOpts<T>): TRet<Crystals<T>> => {
97
110
  // isKyber: true means Kyber, false means Dilithium
98
111
  const { newPoly, N, Q, F, ROOT_OF_UNITY, brvBits, isKyber } = opts;
99
112
  // Normalize JS `%` into the canonical Z_m representative `[0, modulo-1]` expected by
@@ -128,14 +141,34 @@ export const genCrystals = <T extends TypedArray>(
128
141
  // Kyber has slightly different params, since there is no 512th primitive root of unity mod q,
129
142
  // only 256th primitive root of unity mod. Which also complicates MultiplyNTT.
130
143
 
131
- const field = {
132
- add: (a: number, b: number) => mod((a | 0) + (b | 0)) | 0,
133
- sub: (a: number, b: number) => mod((a | 0) - (b | 0)) | 0,
134
- mul: (a: number, b: number) => mod((a | 0) * (b | 0)) | 0,
135
- inv: (_a: number) => {
136
- throw new Error('not implemented');
137
- },
144
+ const inv = (_a: number) => {
145
+ throw new Error('not implemented');
138
146
  };
147
+ // ML-KEM (Kyber) polynomials always enter the transform reduced to [0, Q), so add/sub only
148
+ // need one conditional correction instead of `%`; measured ~20% faster NTT there.
149
+ // ML-DSA keeps the generic mod() path on purpose: its first forward stage sees centered
150
+ // (negative) coefficients, and `sub(a, t)` can drop below -Q (t is a mul output in [0, Q)),
151
+ // so a single correction is not enough. A guarded fast path with mod() fallback was measured
152
+ // slower than plain `%` for the 23-bit Q (V8 int32 modulo is one div; the branches lose).
153
+ const field = isKyber
154
+ ? {
155
+ add: (a: number, b: number) => {
156
+ const r = (a + b) | 0;
157
+ return r >= Q ? (r - Q) | 0 : r;
158
+ },
159
+ sub: (a: number, b: number) => {
160
+ const r = (a - b) | 0;
161
+ return r < 0 ? (r + Q) | 0 : r;
162
+ },
163
+ mul: (a: number, b: number) => mod((a | 0) * (b | 0)) | 0,
164
+ inv,
165
+ }
166
+ : {
167
+ add: (a: number, b: number) => mod((a | 0) + (b | 0)) | 0,
168
+ sub: (a: number, b: number) => mod((a | 0) - (b | 0)) | 0,
169
+ mul: (a: number, b: number) => mod((a | 0) * (b | 0)) | 0,
170
+ inv,
171
+ };
139
172
  const nttOpts = {
140
173
  N,
141
174
  roots: nttZetas as any,
@@ -160,38 +193,56 @@ export const genCrystals = <T extends TypedArray>(
160
193
  };
161
194
  // Pack one little-endian `d`-bit word per coefficient, matching FIPS 203 ByteEncode /
162
195
  // ByteDecode and the FIPS 204 BitsToBytes-based polynomial packing helpers.
163
- const bitsCoder = (d: number, c: Coder<number, number>): BytesCoderLen<T> => {
196
+ const bitsCoder = (d: number, c: Coder<number, number>): TRet<BytesCoderLen<T>> => {
197
+ // Validate the carry shape once: JS bitwise operations silently truncate wider accumulators.
198
+ for (let i = 0, bufLen = 0; i < N; i++) {
199
+ bufLen += d;
200
+ if (bufLen > 32) getMask(bufLen);
201
+ bufLen %= 8;
202
+ }
164
203
  const mask = getMask(d);
165
204
  const bytesLen = d * (N / 8);
166
205
  return {
167
206
  bytesLen,
168
- encode: (poly: T): Uint8Array => {
207
+ encode: (poly_: TArg<T>): TRet<Uint8Array> => {
208
+ const poly = poly_ as T;
169
209
  const r = new Uint8Array(bytesLen);
170
210
  for (let i = 0, buf = 0, bufLen = 0, pos = 0; i < poly.length; i++) {
171
211
  buf |= (c.encode(poly[i]) & mask) << bufLen;
172
212
  bufLen += d;
173
- for (; bufLen >= 8; bufLen -= 8, buf >>= 8) r[pos++] = buf & getMask(bufLen);
213
+ // Take the low byte directly: `& 0xff` matches the previous getMask(bufLen) result
214
+ // after Uint8Array truncation, without a validated function call per output byte.
215
+ for (; bufLen >= 8; bufLen -= 8, buf >>= 8) r[pos++] = buf & 0xff;
174
216
  }
175
- return r;
217
+ return r as TRet<Uint8Array>;
176
218
  },
177
- decode: (bytes: Uint8Array): T => {
219
+ decode: (bytes: TArg<Uint8Array>): TRet<T> => {
178
220
  const r = newPoly(N);
179
221
  for (let i = 0, buf = 0, bufLen = 0, pos = 0; i < bytes.length; i++) {
180
222
  buf |= bytes[i] << bufLen;
181
223
  bufLen += 8;
182
224
  for (; bufLen >= d; bufLen -= d, buf >>= d) r[pos++] = c.decode(buf & mask);
183
225
  }
184
- return r;
226
+ return r as TRet<T>;
185
227
  },
186
- };
228
+ } as TRet<BytesCoderLen<T>>;
187
229
  };
188
230
 
189
- return { mod, smod, nttZetas, NTT, bitsCoder };
231
+ return {
232
+ mod,
233
+ smod,
234
+ nttZetas: nttZetas as TRet<T>,
235
+ NTT: {
236
+ encode: (r: TArg<T>): TRet<T> => NTT.encode(r as T) as TRet<T>,
237
+ decode: (r: TArg<T>): TRet<T> => NTT.decode(r as T) as TRet<T>,
238
+ },
239
+ bitsCoder: bitsCoder as TRet<Crystals<T>>['bitsCoder'],
240
+ };
190
241
  };
191
242
 
192
243
  const createXofShake =
193
- (shake: typeof shake128): XOF =>
194
- (seed: Uint8Array, blockLen?: number) => {
244
+ (shake: typeof shake128): TRet<XOF> =>
245
+ (seed: TArg<Uint8Array>, blockLen?: number) => {
195
246
  if (!blockLen) blockLen = shake.blockLen;
196
247
  // Optimizations that won't mater:
197
248
  // - cached seed update (two .update(), on start and on the end)
@@ -217,7 +268,7 @@ const createXofShake =
217
268
  calls++;
218
269
  return () => {
219
270
  xofs++;
220
- return h.xofInto(buf);
271
+ return h.xofInto(buf) as TRet<Uint8Array>;
221
272
  };
222
273
  },
223
274
  clean: () => {
@@ -243,7 +294,7 @@ const createXofShake =
243
294
  * const block = reader.get(0, 0)();
244
295
  * ```
245
296
  */
246
- export const XOF128: XOF = /* @__PURE__ */ createXofShake(shake128);
297
+ export const XOF128: TRet<XOF> = /* @__PURE__ */ createXofShake(shake128);
247
298
  /**
248
299
  * SHAKE256-based extendable-output reader factory used by ML-DSA.
249
300
  * `get(x, y)` appends raw one-byte coordinates to the seed, invalidates previously returned
@@ -260,4 +311,4 @@ export const XOF128: XOF = /* @__PURE__ */ createXofShake(shake128);
260
311
  * const block = reader.get(0, 0)();
261
312
  * ```
262
313
  */
263
- export const XOF256: XOF = /* @__PURE__ */ createXofShake(shake256);
314
+ export const XOF256: TRet<XOF> = /* @__PURE__ */ createXofShake(shake256);