eskdf.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.scrypt = scrypt;
  4. exports.pbkdf2 = pbkdf2;
  5. exports.deriveMainSeed = deriveMainSeed;
  6. exports.eskdf = eskdf;
  7. /**
  8. * Experimental KDF for AES.
  9. */
  10. const hkdf_ts_1 = require("./hkdf.js");
  11. const pbkdf2_ts_1 = require("./pbkdf2.js");
  12. const scrypt_ts_1 = require("./scrypt.js");
  13. const sha256_ts_1 = require("./sha256.js");
  14. const utils_ts_1 = require("./utils.js");
  15. // A tiny KDF for various applications like AES key-gen.
  16. // Uses HKDF in a non-standard way, so it's not "KDF-secure", only "PRF-secure".
  17. // Which is good enough: assume sha2-256 retained preimage resistance.
  18. const SCRYPT_FACTOR = 2 ** 19;
  19. const PBKDF2_FACTOR = 2 ** 17;
  20. // Scrypt KDF
  21. function scrypt(password, salt) {
  22. return (0, scrypt_ts_1.scrypt)(password, salt, { N: SCRYPT_FACTOR, r: 8, p: 1, dkLen: 32 });
  23. }
  24. // PBKDF2-HMAC-SHA256
  25. function pbkdf2(password, salt) {
  26. return (0, pbkdf2_ts_1.pbkdf2)(sha256_ts_1.sha256, password, salt, { c: PBKDF2_FACTOR, dkLen: 32 });
  27. }
  28. // Combines two 32-byte byte arrays
  29. function xor32(a, b) {
  30. (0, utils_ts_1.abytes)(a, 32);
  31. (0, utils_ts_1.abytes)(b, 32);
  32. const arr = new Uint8Array(32);
  33. for (let i = 0; i < 32; i++) {
  34. arr[i] = a[i] ^ b[i];
  35. }
  36. return arr;
  37. }
  38. function strHasLength(str, min, max) {
  39. return typeof str === 'string' && str.length >= min && str.length <= max;
  40. }
  41. /**
  42. * Derives main seed. Takes a lot of time. Prefer `eskdf` method instead.
  43. */
  44. function deriveMainSeed(username, password) {
  45. if (!strHasLength(username, 8, 255))
  46. throw new Error('invalid username');
  47. if (!strHasLength(password, 8, 255))
  48. throw new Error('invalid password');
  49. // Declared like this to throw off minifiers which auto-convert .fromCharCode(1) to actual string.
  50. // String with non-ascii may be problematic in some envs
  51. const codes = { _1: 1, _2: 2 };
  52. const sep = { s: String.fromCharCode(codes._1), p: String.fromCharCode(codes._2) };
  53. const scr = scrypt(password + sep.s, username + sep.s);
  54. const pbk = pbkdf2(password + sep.p, username + sep.p);
  55. const res = xor32(scr, pbk);
  56. (0, utils_ts_1.clean)(scr, pbk);
  57. return res;
  58. }
  59. /**
  60. * Converts protocol & accountId pair to HKDF salt & info params.
  61. */
  62. function getSaltInfo(protocol, accountId = 0) {
  63. // Note that length here also repeats two lines below
  64. // We do an additional length check here to reduce the scope of DoS attacks
  65. if (!(strHasLength(protocol, 3, 15) && /^[a-z0-9]{3,15}$/.test(protocol))) {
  66. throw new Error('invalid protocol');
  67. }
  68. // Allow string account ids for some protocols
  69. const allowsStr = /^password\d{0,3}|ssh|tor|file$/.test(protocol);
  70. let salt; // Extract salt. Default is undefined.
  71. if (typeof accountId === 'string') {
  72. if (!allowsStr)
  73. throw new Error('accountId must be a number');
  74. if (!strHasLength(accountId, 1, 255))
  75. throw new Error('accountId must be string of length 1..255');
  76. salt = (0, utils_ts_1.kdfInputToBytes)(accountId);
  77. }
  78. else if (Number.isSafeInteger(accountId)) {
  79. if (accountId < 0 || accountId > Math.pow(2, 32) - 1)
  80. throw new Error('invalid accountId');
  81. // Convert to Big Endian Uint32
  82. salt = new Uint8Array(4);
  83. (0, utils_ts_1.createView)(salt).setUint32(0, accountId, false);
  84. }
  85. else {
  86. throw new Error('accountId must be a number' + (allowsStr ? ' or string' : ''));
  87. }
  88. const info = (0, utils_ts_1.kdfInputToBytes)(protocol);
  89. return { salt, info };
  90. }
  91. function countBytes(num) {
  92. if (typeof num !== 'bigint' || num <= BigInt(128))
  93. throw new Error('invalid number');
  94. return Math.ceil(num.toString(2).length / 8);
  95. }
  96. /**
  97. * Parses keyLength and modulus options to extract length of result key.
  98. * If modulus is used, adds 64 bits to it as per FIPS 186 B.4.1 to combat modulo bias.
  99. */
  100. function getKeyLength(options) {
  101. if (!options || typeof options !== 'object')
  102. return 32;
  103. const hasLen = 'keyLength' in options;
  104. const hasMod = 'modulus' in options;
  105. if (hasLen && hasMod)
  106. throw new Error('cannot combine keyLength and modulus options');
  107. if (!hasLen && !hasMod)
  108. throw new Error('must have either keyLength or modulus option');
  109. // FIPS 186 B.4.1 requires at least 64 more bits
  110. const l = hasMod ? countBytes(options.modulus) + 8 : options.keyLength;
  111. if (!(typeof l === 'number' && l >= 16 && l <= 8192))
  112. throw new Error('invalid keyLength');
  113. return l;
  114. }
  115. /**
  116. * Converts key to bigint and divides it by modulus. Big Endian.
  117. * Implements FIPS 186 B.4.1, which removes 0 and modulo bias from output.
  118. */
  119. function modReduceKey(key, modulus) {
  120. const _1 = BigInt(1);
  121. const num = BigInt('0x' + (0, utils_ts_1.bytesToHex)(key)); // check for ui8a, then bytesToNumber()
  122. const res = (num % (modulus - _1)) + _1; // Remove 0 from output
  123. if (res < _1)
  124. throw new Error('expected positive number'); // Guard against bad values
  125. const len = key.length - 8; // FIPS requires 64 more bits = 8 bytes
  126. const hex = res.toString(16).padStart(len * 2, '0'); // numberToHex()
  127. const bytes = (0, utils_ts_1.hexToBytes)(hex);
  128. if (bytes.length !== len)
  129. throw new Error('invalid length of result key');
  130. return bytes;
  131. }
  132. /**
  133. * ESKDF
  134. * @param username - username, email, or identifier, min: 8 characters, should have enough entropy
  135. * @param password - password, min: 8 characters, should have enough entropy
  136. * @example
  137. * const kdf = await eskdf('example-university', 'beginning-new-example');
  138. * const key = kdf.deriveChildKey('aes', 0);
  139. * console.log(kdf.fingerprint);
  140. * kdf.expire();
  141. */
  142. async function eskdf(username, password) {
  143. // We are using closure + object instead of class because
  144. // we want to make `seed` non-accessible for any external function.
  145. let seed = deriveMainSeed(username, password);
  146. function deriveCK(protocol, accountId = 0, options) {
  147. (0, utils_ts_1.abytes)(seed, 32);
  148. const { salt, info } = getSaltInfo(protocol, accountId); // validate protocol & accountId
  149. const keyLength = getKeyLength(options); // validate options
  150. const key = (0, hkdf_ts_1.hkdf)(sha256_ts_1.sha256, seed, salt, info, keyLength);
  151. // Modulus has already been validated
  152. return options && 'modulus' in options ? modReduceKey(key, options.modulus) : key;
  153. }
  154. function expire() {
  155. if (seed)
  156. seed.fill(1);
  157. seed = undefined;
  158. }
  159. // prettier-ignore
  160. const fingerprint = Array.from(deriveCK('fingerprint', 0))
  161. .slice(0, 6)
  162. .map((char) => char.toString(16).padStart(2, '0').toUpperCase())
  163. .join(':');
  164. return Object.freeze({ deriveChildKey: deriveCK, expire, fingerprint });
  165. }
  166. //# sourceMappingURL=eskdf.js.map