@noble/post-quantum 0.5.4 → 0.6.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/src/_crystals.ts CHANGED
@@ -6,54 +6,118 @@
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
 
18
+ /** Extendable-output reader used by the CRYSTALS implementations. */
11
19
  export type XOF = (
12
20
  seed: Uint8Array,
13
21
  blockLen?: number
14
22
  ) => {
23
+ /**
24
+ * Read diagnostic counters for the current XOF session.
25
+ * @returns Current call and XOF block counters.
26
+ */
15
27
  stats: () => { calls: number; xofs: number };
28
+ /**
29
+ * Select one `(x, y)` coordinate pair and get a block reader for it.
30
+ * Only one coordinate stream is live at a time: a later `get(...)` call rebinds the shared
31
+ * SHAKE state and invalidates older readers.
32
+ * Each squeeze aliases one mutable internal output buffer, so callers must copy blocks they
33
+ * want to retain before the next read.
34
+ * @param x - First matrix coordinate.
35
+ * @param y - Second matrix coordinate.
36
+ * @returns Lazy block reader for that coordinate pair.
37
+ */
16
38
  get: (x: number, y: number) => () => Uint8Array; // return block aligned to blockLen and 3
39
+ /** Wipe any buffered state once the reader is no longer needed. */
17
40
  clean: () => void;
18
41
  };
19
42
 
20
43
  /** CRYSTALS (ml-kem, ml-dsa) options */
44
+ /** Shared polynomial and NTT parameters for CRYSTALS algorithms. */
21
45
  export type CrystalOpts<T extends TypedArray> = {
46
+ /**
47
+ * Allocate one zeroed polynomial/vector container.
48
+ * @param n - Number of coefficients to allocate.
49
+ * @returns Fresh typed container.
50
+ */
22
51
  newPoly: TypedCons<T>;
23
- N: number; // poly size, 256
24
- Q: number; // modulo
25
- F: number; // 256**−1 mod q for dilithium, 128**−1 mod q for kyber
52
+ /** Polynomial size, typically `256`. */
53
+ N: number;
54
+ /** Prime modulus used for all coefficient arithmetic. */
55
+ Q: number;
56
+ /** Inverse transform normalization factor:
57
+ * `256**-1 mod q` for Dilithium, `128**-1 mod q` for Kyber.
58
+ */
59
+ F: number;
60
+ /** Principal root of unity for the transform domain. */
26
61
  ROOT_OF_UNITY: number;
27
- brvBits: number; // bits for bitReversal
62
+ /** Number of bits used for bit-reversal ordering. */
63
+ brvBits: number;
64
+ /** `true` for Kyber/ML-KEM mode, `false` for Dilithium/ML-DSA mode. */
28
65
  isKyber: boolean;
29
66
  };
30
67
 
68
+ /** Constructor function for typed polynomial containers. */
31
69
  export type TypedCons<T extends TypedArray> = (n: number) => T;
32
70
 
33
- export const genCrystals = <T extends TypedArray>(
34
- opts: CrystalOpts<T>
35
- ): {
71
+ type Crystals<T extends TypedArray> = {
36
72
  mod: (a: number, modulo?: number) => number;
37
73
  smod: (a: number, modulo?: number) => number;
38
74
  nttZetas: T;
39
75
  NTT: {
76
+ /** Forward transform in place. Mutates and returns `r`. */
40
77
  encode: (r: T) => T;
78
+ /** Inverse transform in place. Mutates and returns `r`. */
41
79
  decode: (r: T) => T;
42
80
  };
43
81
  bitsCoder: (d: number, c: Coder<number, number>) => BytesCoderLen<T>;
44
- } => {
82
+ };
83
+
84
+ /**
85
+ * Creates shared modular arithmetic, NTT, and packing helpers for CRYSTALS schemes.
86
+ * @param opts - Polynomial and transform parameters. See {@link CrystalOpts}.
87
+ * @returns CRYSTALS arithmetic and encoding helpers.
88
+ * @example
89
+ * Create shared modular arithmetic and NTT helpers for a CRYSTALS parameter set.
90
+ * ```ts
91
+ * const crystals = genCrystals({
92
+ * newPoly: (n) => new Uint16Array(n),
93
+ * N: 256,
94
+ * Q: 3329,
95
+ * F: 3303,
96
+ * ROOT_OF_UNITY: 17,
97
+ * brvBits: 7,
98
+ * isKyber: true,
99
+ * });
100
+ * const reduced = crystals.mod(-1);
101
+ * ```
102
+ */
103
+ export const genCrystals = <T extends TypedArray>(opts: CrystalOpts<T>): TRet<Crystals<T>> => {
45
104
  // isKyber: true means Kyber, false means Dilithium
46
105
  const { newPoly, N, Q, F, ROOT_OF_UNITY, brvBits, isKyber } = opts;
106
+ // Normalize JS `%` into the canonical Z_m representative `[0, modulo-1]` expected by
107
+ // FIPS 203 §2.3 / FIPS 204 §2.3 before downstream mod-q arithmetic.
47
108
  const mod = (a: number, modulo = Q): number => {
48
109
  const result = a % modulo | 0;
49
110
  return (result >= 0 ? result | 0 : (modulo + result) | 0) | 0;
50
111
  };
51
- // -(Q-1)/2 < a <= (Q-1)/2
112
+ // FIPS 204 §7.4 uses the centered `mod ±` representative for low bits, keeping the
113
+ // positive midpoint when `modulo` is even.
114
+ // Center to `[-floor((modulo-1)/2), floor(modulo/2)]`.
52
115
  const smod = (a: number, modulo = Q): number => {
53
116
  const r = mod(a, modulo) | 0;
54
117
  return (r > modulo >> 1 ? (r - modulo) | 0 : r) | 0;
55
118
  };
56
- // Generate zettas (different from roots of unity, negacyclic uses phi, where acyclic uses omega)
119
+ // Kyber uses the FIPS 203 Appendix A `BitRev_7` table here via the first 128 entries, while
120
+ // Dilithium uses the FIPS 204 §7.5 / Appendix B `BitRev_8` zetas table over all 256 entries.
57
121
  function getZettas() {
58
122
  const out = newPoly(N);
59
123
  for (let i = 0; i < N; i++) {
@@ -94,44 +158,57 @@ export const genCrystals = <T extends TypedArray>(
94
158
  },
95
159
  decode: (r: T): T => {
96
160
  dit(r as any);
161
+ // The inverse-NTT normalization factor is family-specific: FIPS 203 Algorithm 10 line 14
162
+ // uses `128^-1 mod q` for Kyber, while FIPS 204 Algorithm 42 lines 21-23 use `256^-1 mod q`.
97
163
  // kyber uses 128 here, because brv && stuff
98
164
  for (let i = 0; i < r.length; i++) r[i] = mod(F * r[i]);
99
165
  return r;
100
166
  },
101
167
  };
102
- // Encode polynominal as bits
103
- const bitsCoder = (d: number, c: Coder<number, number>): BytesCoderLen<T> => {
168
+ // Pack one little-endian `d`-bit word per coefficient, matching FIPS 203 ByteEncode /
169
+ // ByteDecode and the FIPS 204 BitsToBytes-based polynomial packing helpers.
170
+ const bitsCoder = (d: number, c: Coder<number, number>): TRet<BytesCoderLen<T>> => {
104
171
  const mask = getMask(d);
105
172
  const bytesLen = d * (N / 8);
106
173
  return {
107
174
  bytesLen,
108
- encode: (poly: T): Uint8Array => {
175
+ encode: (poly_: TArg<T>): TRet<Uint8Array> => {
176
+ const poly = poly_ as T;
109
177
  const r = new Uint8Array(bytesLen);
110
178
  for (let i = 0, buf = 0, bufLen = 0, pos = 0; i < poly.length; i++) {
111
179
  buf |= (c.encode(poly[i]) & mask) << bufLen;
112
180
  bufLen += d;
113
181
  for (; bufLen >= 8; bufLen -= 8, buf >>= 8) r[pos++] = buf & getMask(bufLen);
114
182
  }
115
- return r;
183
+ return r as TRet<Uint8Array>;
116
184
  },
117
- decode: (bytes: Uint8Array): T => {
185
+ decode: (bytes: TArg<Uint8Array>): TRet<T> => {
118
186
  const r = newPoly(N);
119
187
  for (let i = 0, buf = 0, bufLen = 0, pos = 0; i < bytes.length; i++) {
120
188
  buf |= bytes[i] << bufLen;
121
189
  bufLen += 8;
122
190
  for (; bufLen >= d; bufLen -= d, buf >>= d) r[pos++] = c.decode(buf & mask);
123
191
  }
124
- return r;
192
+ return r as TRet<T>;
125
193
  },
126
- };
194
+ } as TRet<BytesCoderLen<T>>;
127
195
  };
128
196
 
129
- return { mod, smod, nttZetas, NTT, bitsCoder };
197
+ return {
198
+ mod,
199
+ smod,
200
+ nttZetas: nttZetas as TRet<T>,
201
+ NTT: {
202
+ encode: (r: TArg<T>): TRet<T> => NTT.encode(r as T) as TRet<T>,
203
+ decode: (r: TArg<T>): TRet<T> => NTT.decode(r as T) as TRet<T>,
204
+ },
205
+ bitsCoder: bitsCoder as TRet<Crystals<T>>['bitsCoder'],
206
+ };
130
207
  };
131
208
 
132
209
  const createXofShake =
133
- (shake: typeof shake128): XOF =>
134
- (seed: Uint8Array, blockLen?: number) => {
210
+ (shake: typeof shake128): TRet<XOF> =>
211
+ (seed: TArg<Uint8Array>, blockLen?: number) => {
135
212
  if (!blockLen) blockLen = shake.blockLen;
136
213
  // Optimizations that won't mater:
137
214
  // - cached seed update (two .update(), on start and on the end)
@@ -148,6 +225,8 @@ const createXofShake =
148
225
  return {
149
226
  stats: () => ({ calls, xofs }),
150
227
  get: (x: number, y: number) => {
228
+ // Rebind to `seed || x || y` so callers can implement the spec's per-coordinate
229
+ // SHAKE inputs like `rho || j || i` and `rho || IntegerToBytes(counter, 2)`.
151
230
  _seed[seedLen + 0] = x;
152
231
  _seed[seedLen + 1] = y;
153
232
  h.destroy();
@@ -155,7 +234,7 @@ const createXofShake =
155
234
  calls++;
156
235
  return () => {
157
236
  xofs++;
158
- return h.xofInto(buf);
237
+ return h.xofInto(buf) as TRet<Uint8Array>;
159
238
  };
160
239
  },
161
240
  clean: () => {
@@ -165,5 +244,37 @@ const createXofShake =
165
244
  };
166
245
  };
167
246
 
168
- export const XOF128: XOF = /* @__PURE__ */ createXofShake(shake128);
169
- export const XOF256: XOF = /* @__PURE__ */ createXofShake(shake256);
247
+ /**
248
+ * SHAKE128-based extendable-output reader factory used by ML-KEM.
249
+ * `get(x, y)` selects one coordinate pair at a time; calling it again invalidates previously
250
+ * returned readers, and each squeeze reuses one mutable internal output buffer.
251
+ * @param seed - Seed bytes for the reader.
252
+ * @param blockLen - Optional output block length.
253
+ * @returns Stateful XOF reader.
254
+ * @example
255
+ * Build the ML-KEM SHAKE128 matrix expander and read one block.
256
+ * ```ts
257
+ * import { randomBytes } from '@noble/post-quantum/utils.js';
258
+ * import { XOF128 } from '@noble/post-quantum/_crystals.js';
259
+ * const reader = XOF128(randomBytes(32));
260
+ * const block = reader.get(0, 0)();
261
+ * ```
262
+ */
263
+ export const XOF128: TRet<XOF> = /* @__PURE__ */ createXofShake(shake128);
264
+ /**
265
+ * SHAKE256-based extendable-output reader factory used by ML-DSA.
266
+ * `get(x, y)` appends raw one-byte coordinates to the seed, invalidates previously returned
267
+ * readers, and reuses one mutable internal output buffer for each squeeze.
268
+ * @param seed - Seed bytes for the reader.
269
+ * @param blockLen - Optional output block length.
270
+ * @returns Stateful XOF reader.
271
+ * @example
272
+ * Build the ML-DSA SHAKE256 coefficient expander and read one block.
273
+ * ```ts
274
+ * import { randomBytes } from '@noble/post-quantum/utils.js';
275
+ * import { XOF256 } from '@noble/post-quantum/_crystals.js';
276
+ * const reader = XOF256(randomBytes(32));
277
+ * const block = reader.get(0, 0)();
278
+ * ```
279
+ */
280
+ export const XOF256: TRet<XOF> = /* @__PURE__ */ createXofShake(shake256);