scrypt.ts 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. /**
  2. * RFC 7914 Scrypt KDF. Can be used to create a key from password and salt.
  3. * @module
  4. */
  5. import { pbkdf2 } from './pbkdf2.ts';
  6. import { sha256 } from './sha2.ts';
  7. // prettier-ignore
  8. import {
  9. anumber, asyncLoop,
  10. checkOpts, clean,
  11. type KDFInput, rotl,
  12. swap32IfBE,
  13. u32
  14. } from './utils.ts';
  15. // The main Scrypt loop: uses Salsa extensively.
  16. // Six versions of the function were tried, this is the fastest one.
  17. // prettier-ignore
  18. function XorAndSalsa(
  19. prev: Uint32Array,
  20. pi: number,
  21. input: Uint32Array,
  22. ii: number,
  23. out: Uint32Array,
  24. oi: number
  25. ) {
  26. // Based on https://cr.yp.to/salsa20.html
  27. // Xor blocks
  28. let y00 = prev[pi++] ^ input[ii++], y01 = prev[pi++] ^ input[ii++];
  29. let y02 = prev[pi++] ^ input[ii++], y03 = prev[pi++] ^ input[ii++];
  30. let y04 = prev[pi++] ^ input[ii++], y05 = prev[pi++] ^ input[ii++];
  31. let y06 = prev[pi++] ^ input[ii++], y07 = prev[pi++] ^ input[ii++];
  32. let y08 = prev[pi++] ^ input[ii++], y09 = prev[pi++] ^ input[ii++];
  33. let y10 = prev[pi++] ^ input[ii++], y11 = prev[pi++] ^ input[ii++];
  34. let y12 = prev[pi++] ^ input[ii++], y13 = prev[pi++] ^ input[ii++];
  35. let y14 = prev[pi++] ^ input[ii++], y15 = prev[pi++] ^ input[ii++];
  36. // Save state to temporary variables (salsa)
  37. let x00 = y00, x01 = y01, x02 = y02, x03 = y03,
  38. x04 = y04, x05 = y05, x06 = y06, x07 = y07,
  39. x08 = y08, x09 = y09, x10 = y10, x11 = y11,
  40. x12 = y12, x13 = y13, x14 = y14, x15 = y15;
  41. // Main loop (salsa)
  42. for (let i = 0; i < 8; i += 2) {
  43. x04 ^= rotl(x00 + x12 | 0, 7); x08 ^= rotl(x04 + x00 | 0, 9);
  44. x12 ^= rotl(x08 + x04 | 0, 13); x00 ^= rotl(x12 + x08 | 0, 18);
  45. x09 ^= rotl(x05 + x01 | 0, 7); x13 ^= rotl(x09 + x05 | 0, 9);
  46. x01 ^= rotl(x13 + x09 | 0, 13); x05 ^= rotl(x01 + x13 | 0, 18);
  47. x14 ^= rotl(x10 + x06 | 0, 7); x02 ^= rotl(x14 + x10 | 0, 9);
  48. x06 ^= rotl(x02 + x14 | 0, 13); x10 ^= rotl(x06 + x02 | 0, 18);
  49. x03 ^= rotl(x15 + x11 | 0, 7); x07 ^= rotl(x03 + x15 | 0, 9);
  50. x11 ^= rotl(x07 + x03 | 0, 13); x15 ^= rotl(x11 + x07 | 0, 18);
  51. x01 ^= rotl(x00 + x03 | 0, 7); x02 ^= rotl(x01 + x00 | 0, 9);
  52. x03 ^= rotl(x02 + x01 | 0, 13); x00 ^= rotl(x03 + x02 | 0, 18);
  53. x06 ^= rotl(x05 + x04 | 0, 7); x07 ^= rotl(x06 + x05 | 0, 9);
  54. x04 ^= rotl(x07 + x06 | 0, 13); x05 ^= rotl(x04 + x07 | 0, 18);
  55. x11 ^= rotl(x10 + x09 | 0, 7); x08 ^= rotl(x11 + x10 | 0, 9);
  56. x09 ^= rotl(x08 + x11 | 0, 13); x10 ^= rotl(x09 + x08 | 0, 18);
  57. x12 ^= rotl(x15 + x14 | 0, 7); x13 ^= rotl(x12 + x15 | 0, 9);
  58. x14 ^= rotl(x13 + x12 | 0, 13); x15 ^= rotl(x14 + x13 | 0, 18);
  59. }
  60. // Write output (salsa)
  61. out[oi++] = (y00 + x00) | 0; out[oi++] = (y01 + x01) | 0;
  62. out[oi++] = (y02 + x02) | 0; out[oi++] = (y03 + x03) | 0;
  63. out[oi++] = (y04 + x04) | 0; out[oi++] = (y05 + x05) | 0;
  64. out[oi++] = (y06 + x06) | 0; out[oi++] = (y07 + x07) | 0;
  65. out[oi++] = (y08 + x08) | 0; out[oi++] = (y09 + x09) | 0;
  66. out[oi++] = (y10 + x10) | 0; out[oi++] = (y11 + x11) | 0;
  67. out[oi++] = (y12 + x12) | 0; out[oi++] = (y13 + x13) | 0;
  68. out[oi++] = (y14 + x14) | 0; out[oi++] = (y15 + x15) | 0;
  69. }
  70. function BlockMix(input: Uint32Array, ii: number, out: Uint32Array, oi: number, r: number) {
  71. // The block B is r 128-byte chunks (which is equivalent of 2r 64-byte chunks)
  72. let head = oi + 0;
  73. let tail = oi + 16 * r;
  74. for (let i = 0; i < 16; i++) out[tail + i] = input[ii + (2 * r - 1) * 16 + i]; // X ← B[2r−1]
  75. for (let i = 0; i < r; i++, head += 16, ii += 16) {
  76. // We write odd & even Yi at same time. Even: 0bXXXXX0 Odd: 0bXXXXX1
  77. XorAndSalsa(out, tail, input, ii, out, head); // head[i] = Salsa(blockIn[2*i] ^ tail[i-1])
  78. if (i > 0) tail += 16; // First iteration overwrites tmp value in tail
  79. XorAndSalsa(out, head, input, (ii += 16), out, tail); // tail[i] = Salsa(blockIn[2*i+1] ^ head[i])
  80. }
  81. }
  82. export type ScryptOpts = {
  83. N: number; // cost factor
  84. r: number; // block size
  85. p: number; // parallelization
  86. dkLen?: number; // key length
  87. asyncTick?: number; // block execution max time
  88. maxmem?: number;
  89. onProgress?: (progress: number) => void;
  90. };
  91. // Common prologue and epilogue for sync/async functions
  92. function scryptInit(password: KDFInput, salt: KDFInput, _opts?: ScryptOpts) {
  93. // Maxmem - 1GB+1KB by default
  94. const opts = checkOpts(
  95. {
  96. dkLen: 32,
  97. asyncTick: 10,
  98. maxmem: 1024 ** 3 + 1024,
  99. },
  100. _opts
  101. );
  102. const { N, r, p, dkLen, asyncTick, maxmem, onProgress } = opts;
  103. anumber(N);
  104. anumber(r);
  105. anumber(p);
  106. anumber(dkLen);
  107. anumber(asyncTick);
  108. anumber(maxmem);
  109. if (onProgress !== undefined && typeof onProgress !== 'function')
  110. throw new Error('progressCb should be function');
  111. const blockSize = 128 * r;
  112. const blockSize32 = blockSize / 4;
  113. // Max N is 2^32 (Integrify is 32-bit). Real limit is 2^22: JS engines Uint8Array limit is 4GB in 2024.
  114. // Spec check `N >= 2^(blockSize / 8)` is not done for compat with popular libs,
  115. // which used incorrect r: 1, p: 8. Also, the check seems to be a spec error:
  116. // https://www.rfc-editor.org/errata_search.php?rfc=7914
  117. const pow32 = Math.pow(2, 32);
  118. if (N <= 1 || (N & (N - 1)) !== 0 || N > pow32) {
  119. throw new Error('Scrypt: N must be larger than 1, a power of 2, and less than 2^32');
  120. }
  121. if (p < 0 || p > ((pow32 - 1) * 32) / blockSize) {
  122. throw new Error(
  123. 'Scrypt: p must be a positive integer less than or equal to ((2^32 - 1) * 32) / (128 * r)'
  124. );
  125. }
  126. if (dkLen < 0 || dkLen > (pow32 - 1) * 32) {
  127. throw new Error(
  128. 'Scrypt: dkLen should be positive integer less than or equal to (2^32 - 1) * 32'
  129. );
  130. }
  131. const memUsed = blockSize * (N + p);
  132. if (memUsed > maxmem) {
  133. throw new Error(
  134. 'Scrypt: memused is bigger than maxMem. Expected 128 * r * (N + p) > maxmem of ' + maxmem
  135. );
  136. }
  137. // [B0...Bp−1] ← PBKDF2HMAC-SHA256(Passphrase, Salt, 1, blockSize*ParallelizationFactor)
  138. // Since it has only one iteration there is no reason to use async variant
  139. const B = pbkdf2(sha256, password, salt, { c: 1, dkLen: blockSize * p });
  140. const B32 = u32(B);
  141. // Re-used between parallel iterations. Array(iterations) of B
  142. const V = u32(new Uint8Array(blockSize * N));
  143. const tmp = u32(new Uint8Array(blockSize));
  144. let blockMixCb = () => {};
  145. if (onProgress) {
  146. const totalBlockMix = 2 * N * p;
  147. // Invoke callback if progress changes from 10.01 to 10.02
  148. // Allows to draw smooth progress bar on up to 8K screen
  149. const callbackPer = Math.max(Math.floor(totalBlockMix / 10000), 1);
  150. let blockMixCnt = 0;
  151. blockMixCb = () => {
  152. blockMixCnt++;
  153. if (onProgress && (!(blockMixCnt % callbackPer) || blockMixCnt === totalBlockMix))
  154. onProgress(blockMixCnt / totalBlockMix);
  155. };
  156. }
  157. return { N, r, p, dkLen, blockSize32, V, B32, B, tmp, blockMixCb, asyncTick };
  158. }
  159. function scryptOutput(
  160. password: KDFInput,
  161. dkLen: number,
  162. B: Uint8Array,
  163. V: Uint32Array,
  164. tmp: Uint32Array
  165. ) {
  166. const res = pbkdf2(sha256, password, B, { c: 1, dkLen });
  167. clean(B, V, tmp);
  168. return res;
  169. }
  170. /**
  171. * Scrypt KDF from RFC 7914.
  172. * @param password - pass
  173. * @param salt - salt
  174. * @param opts - parameters
  175. * - `N` is cpu/mem work factor (power of 2 e.g. 2**18)
  176. * - `r` is block size (8 is common), fine-tunes sequential memory read size and performance
  177. * - `p` is parallelization factor (1 is common)
  178. * - `dkLen` is output key length in bytes e.g. 32.
  179. * - `asyncTick` - (default: 10) max time in ms for which async function can block execution
  180. * - `maxmem` - (default: `1024 ** 3 + 1024` aka 1GB+1KB). A limit that the app could use for scrypt
  181. * - `onProgress` - callback function that would be executed for progress report
  182. * @returns Derived key
  183. * @example
  184. * scrypt('password', 'salt', { N: 2**18, r: 8, p: 1, dkLen: 32 });
  185. */
  186. export function scrypt(password: KDFInput, salt: KDFInput, opts: ScryptOpts): Uint8Array {
  187. const { N, r, p, dkLen, blockSize32, V, B32, B, tmp, blockMixCb } = scryptInit(
  188. password,
  189. salt,
  190. opts
  191. );
  192. swap32IfBE(B32);
  193. for (let pi = 0; pi < p; pi++) {
  194. const Pi = blockSize32 * pi;
  195. for (let i = 0; i < blockSize32; i++) V[i] = B32[Pi + i]; // V[0] = B[i]
  196. for (let i = 0, pos = 0; i < N - 1; i++) {
  197. BlockMix(V, pos, V, (pos += blockSize32), r); // V[i] = BlockMix(V[i-1]);
  198. blockMixCb();
  199. }
  200. BlockMix(V, (N - 1) * blockSize32, B32, Pi, r); // Process last element
  201. blockMixCb();
  202. for (let i = 0; i < N; i++) {
  203. // First u32 of the last 64-byte block (u32 is LE)
  204. const j = B32[Pi + blockSize32 - 16] % N; // j = Integrify(X) % iterations
  205. for (let k = 0; k < blockSize32; k++) tmp[k] = B32[Pi + k] ^ V[j * blockSize32 + k]; // tmp = B ^ V[j]
  206. BlockMix(tmp, 0, B32, Pi, r); // B = BlockMix(B ^ V[j])
  207. blockMixCb();
  208. }
  209. }
  210. swap32IfBE(B32);
  211. return scryptOutput(password, dkLen, B, V, tmp);
  212. }
  213. /**
  214. * Scrypt KDF from RFC 7914. Async version.
  215. * @example
  216. * await scryptAsync('password', 'salt', { N: 2**18, r: 8, p: 1, dkLen: 32 });
  217. */
  218. export async function scryptAsync(
  219. password: KDFInput,
  220. salt: KDFInput,
  221. opts: ScryptOpts
  222. ): Promise<Uint8Array> {
  223. const { N, r, p, dkLen, blockSize32, V, B32, B, tmp, blockMixCb, asyncTick } = scryptInit(
  224. password,
  225. salt,
  226. opts
  227. );
  228. swap32IfBE(B32);
  229. for (let pi = 0; pi < p; pi++) {
  230. const Pi = blockSize32 * pi;
  231. for (let i = 0; i < blockSize32; i++) V[i] = B32[Pi + i]; // V[0] = B[i]
  232. let pos = 0;
  233. await asyncLoop(N - 1, asyncTick, () => {
  234. BlockMix(V, pos, V, (pos += blockSize32), r); // V[i] = BlockMix(V[i-1]);
  235. blockMixCb();
  236. });
  237. BlockMix(V, (N - 1) * blockSize32, B32, Pi, r); // Process last element
  238. blockMixCb();
  239. await asyncLoop(N, asyncTick, () => {
  240. // First u32 of the last 64-byte block (u32 is LE)
  241. const j = B32[Pi + blockSize32 - 16] % N; // j = Integrify(X) % iterations
  242. for (let k = 0; k < blockSize32; k++) tmp[k] = B32[Pi + k] ^ V[j * blockSize32 + k]; // tmp = B ^ V[j]
  243. BlockMix(tmp, 0, B32, Pi, r); // B = BlockMix(B ^ V[j])
  244. blockMixCb();
  245. });
  246. }
  247. swap32IfBE(B32);
  248. return scryptOutput(password, dkLen, B, V, tmp);
  249. }