@noble/post-quantum 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,197 @@
1
+ /*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */
2
+ import { unsafe } from '@noble/ciphers/aes';
3
+ import { shake128, shake256 } from '@noble/hashes/sha3';
4
+ import type { TypedArray } from '@noble/hashes/utils';
5
+ import { BytesCoderLen, Coder, getMask } from './utils.js';
6
+
7
+ export type XOF = (
8
+ seed: Uint8Array,
9
+ blockLen?: number
10
+ ) => {
11
+ stats: () => { calls: number; xofs: number };
12
+ get: (x: number, y: number) => () => Uint8Array; // return block aligned to blockLen and 3
13
+ clean: () => void;
14
+ };
15
+
16
+ export type CrystalOpts<T extends TypedArray> = {
17
+ newPoly: TypedCons<T>;
18
+ N: number; // poly size, 256
19
+ Q: number; // modulo
20
+ F: number; // 256**−1 mod q for dilithium, 128**−1 mod q for kyber
21
+ ROOT_OF_UNITY: number;
22
+ brvBits: number; // bits for bitReversal
23
+ isKyber: boolean;
24
+ };
25
+
26
+ export type TypedCons<T extends TypedArray> = (n: number) => T;
27
+
28
+ // TODO: benchmark
29
+ function bitReversal(n: number, bits: number = 8) {
30
+ const padded = n.toString(2).padStart(8, '0');
31
+ const sliced = padded.slice(-bits).padStart(7, '0');
32
+ const revrsd = sliced.split('').reverse().join('');
33
+ return Number.parseInt(revrsd, 2);
34
+ }
35
+
36
+ export const genCrystals = <T extends TypedArray>(opts: CrystalOpts<T>) => {
37
+ // isKyber: true means Kyber, false means Dilithium
38
+ const { newPoly, N, Q, F, ROOT_OF_UNITY, brvBits, isKyber } = opts;
39
+ const mod = (a: number, modulo = Q): number => {
40
+ const result = a % modulo | 0;
41
+ return (result >= 0 ? result | 0 : (modulo + result) | 0) | 0;
42
+ };
43
+ // -(Q-1)/2 < a <= (Q-1)/2
44
+ const smod = (a: number, modulo = Q): number => {
45
+ const r = mod(a, modulo) | 0;
46
+ return (r > modulo >> 1 ? (r - modulo) | 0 : r) | 0;
47
+ };
48
+ // Generate zettas
49
+ function getZettas() {
50
+ const out = newPoly(N);
51
+ for (let i = 0; i < N; i++) {
52
+ const b = bitReversal(i, brvBits);
53
+ const p = BigInt(ROOT_OF_UNITY) ** BigInt(b) % BigInt(Q);
54
+ out[i] = Number(p) | 0;
55
+ }
56
+ return out;
57
+ }
58
+ const nttZetas = getZettas();
59
+
60
+ // Number-Theoretic Transform
61
+ // Explained: https://electricdusk.com/ntt.html
62
+
63
+ // Kyber has slightly different params, since there is no 512th primitive root of unity mod q,
64
+ // only 256th primitive root of unity mod. Which also complicates MultiplyNTT.
65
+ // TODO: there should be less ugly way to define this.
66
+ const LEN1 = isKyber ? 128 : N;
67
+ const LEN2 = isKyber ? 1 : 0;
68
+ const NTT = {
69
+ encode: (r: T) => {
70
+ for (let k = 1, len = 128; len > LEN2; len >>= 1) {
71
+ for (let start = 0; start < N; start += 2 * len) {
72
+ const zeta = nttZetas[k++];
73
+ for (let j = start; j < start + len; j++) {
74
+ const t = mod(zeta * r[j + len]);
75
+ r[j + len] = mod(r[j] - t) | 0;
76
+ r[j] = mod(r[j] + t) | 0;
77
+ }
78
+ }
79
+ }
80
+ return r;
81
+ },
82
+ decode: (r: T) => {
83
+ for (let k = LEN1 - 1, len = 1 + LEN2; len < LEN1 + LEN2; len <<= 1) {
84
+ for (let start = 0; start < N; start += 2 * len) {
85
+ const zeta = nttZetas[k--];
86
+ for (let j = start; j < start + len; j++) {
87
+ const t = r[j];
88
+ r[j] = mod(t + r[j + len]);
89
+ r[j + len] = mod(zeta * (r[j + len] - t));
90
+ }
91
+ }
92
+ }
93
+ for (let i = 0; i < r.length; i++) r[i] = mod(F * r[i]);
94
+ return r;
95
+ },
96
+ };
97
+ // Encode polynominal as bits
98
+ const bitsCoder = (d: number, c: Coder<number, number>): BytesCoderLen<T> => {
99
+ const mask = getMask(d);
100
+ const bytesLen = d * (N / 8);
101
+ return {
102
+ bytesLen,
103
+ encode: (poly: T): Uint8Array => {
104
+ const r = new Uint8Array(bytesLen);
105
+ for (let i = 0, buf = 0, bufLen = 0, pos = 0; i < poly.length; i++) {
106
+ buf |= (c.encode(poly[i]) & mask) << bufLen;
107
+ bufLen += d;
108
+ for (; bufLen >= 8; bufLen -= 8, buf >>= 8) r[pos++] = buf & getMask(bufLen);
109
+ }
110
+ return r;
111
+ },
112
+ decode: (bytes: Uint8Array): T => {
113
+ const r = newPoly(N);
114
+ for (let i = 0, buf = 0, bufLen = 0, pos = 0; i < bytes.length; i++) {
115
+ buf |= bytes[i] << bufLen;
116
+ bufLen += 8;
117
+ for (; bufLen >= d; bufLen -= d, buf >>= d) r[pos++] = c.decode(buf & mask);
118
+ }
119
+ return r;
120
+ },
121
+ };
122
+ };
123
+
124
+ return { mod, smod, nttZetas, NTT, bitsCoder };
125
+ };
126
+
127
+ const createXofShake =
128
+ (shake: typeof shake128): XOF =>
129
+ (seed: Uint8Array, blockLen?: number) => {
130
+ if (!blockLen) blockLen = shake.blockLen;
131
+ // Optimizations that won't mater:
132
+ // - cached seed update (two .update(), on start and on the end)
133
+ // - another cache which cloned into working copy
134
+
135
+ // Faster than multiple updates, since seed less than blockLen
136
+ const _seed = new Uint8Array(seed.length + 2);
137
+ _seed.set(seed);
138
+ const seedLen = seed.length;
139
+ const buf = new Uint8Array(blockLen); // == shake128.blockLen
140
+ let h = shake.create({});
141
+ let calls = 0;
142
+ let xofs = 0;
143
+ return {
144
+ stats: () => ({ calls, xofs }),
145
+ get: (x: number, y: number) => {
146
+ _seed[seedLen + 0] = x;
147
+ _seed[seedLen + 1] = y;
148
+ h.destroy();
149
+ h = shake.create({}).update(_seed);
150
+ calls++;
151
+ return () => {
152
+ xofs++;
153
+ return h.xofInto(buf);
154
+ };
155
+ },
156
+ clean: () => {
157
+ h.destroy();
158
+ buf.fill(0);
159
+ _seed.fill(0);
160
+ },
161
+ };
162
+ };
163
+
164
+ export const XOF128 = /* @__PURE__ */ createXofShake(shake128);
165
+ export const XOF256 = /* @__PURE__ */ createXofShake(shake256);
166
+
167
+ const createXofAes =
168
+ (aes: typeof unsafe): XOF =>
169
+ (seed: Uint8Array, blockLen?: number) => {
170
+ if (!blockLen) blockLen = 16 * 3; // 288
171
+ const nonce = new Uint8Array(16);
172
+ const xk = aes.expandKeyLE(seed.subarray(0, 32));
173
+ const block = new Uint8Array(blockLen);
174
+ const out = block.slice();
175
+ let calls = 0;
176
+ let xofs = 0;
177
+ return {
178
+ stats: () => ({ calls, xofs }),
179
+ get: (x: number, y: number) => {
180
+ nonce.fill(0); // clean counter
181
+ nonce[0] = x;
182
+ nonce[1] = y;
183
+ calls++;
184
+ return () => {
185
+ xofs++;
186
+ return aes.ctrCounter(xk, nonce, block, out);
187
+ };
188
+ },
189
+ clean: () => {
190
+ nonce.fill(0);
191
+ xk.fill(0);
192
+ out.fill(0);
193
+ },
194
+ };
195
+ };
196
+
197
+ export const XOF_AES = /* @__PURE__ */ createXofAes(unsafe);
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ throw new Error('noble-post-quantum has no entry-point: consult README for usage');