index.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. /*! scure-bip39 - MIT License (c) 2022 Patricio Palladino, Paul Miller (paulmillr.com) */
  2. import { bytes as assertBytes, number as assertNumber } from '@noble/hashes/_assert';
  3. import { pbkdf2, pbkdf2Async } from '@noble/hashes/pbkdf2';
  4. import { sha256 } from '@noble/hashes/sha256';
  5. import { sha512 } from '@noble/hashes/sha512';
  6. import { randomBytes } from '@noble/hashes/utils';
  7. import { utils as baseUtils } from '@scure/base';
  8. // Japanese wordlist
  9. const isJapanese = (wordlist: string[]) => wordlist[0] === '\u3042\u3044\u3053\u304f\u3057\u3093';
  10. // Normalization replaces equivalent sequences of characters
  11. // so that any two texts that are equivalent will be reduced
  12. // to the same sequence of code points, called the normal form of the original text.
  13. // https://tonsky.me/blog/unicode/#why-is-a----
  14. function nfkd(str: string) {
  15. if (typeof str !== 'string') throw new TypeError(`Invalid mnemonic type: ${typeof str}`);
  16. return str.normalize('NFKD');
  17. }
  18. function normalize(str: string) {
  19. const norm = nfkd(str);
  20. const words = norm.split(' ');
  21. if (![12, 15, 18, 21, 24].includes(words.length)) throw new Error('Invalid mnemonic');
  22. return { nfkd: norm, words };
  23. }
  24. function assertEntropy(entropy: Uint8Array) {
  25. assertBytes(entropy, 16, 20, 24, 28, 32);
  26. }
  27. /**
  28. * Generate x random words. Uses Cryptographically-Secure Random Number Generator.
  29. * @param wordlist imported wordlist for specific language
  30. * @param strength mnemonic strength 128-256 bits
  31. * @example
  32. * generateMnemonic(wordlist, 128)
  33. * // 'legal winner thank year wave sausage worth useful legal winner thank yellow'
  34. */
  35. export function generateMnemonic(wordlist: string[], strength: number = 128): string {
  36. assertNumber(strength);
  37. if (strength % 32 !== 0 || strength > 256) throw new TypeError('Invalid entropy');
  38. return entropyToMnemonic(randomBytes(strength / 8), wordlist);
  39. }
  40. const calcChecksum = (entropy: Uint8Array) => {
  41. // Checksum is ent.length/4 bits long
  42. const bitsLeft = 8 - entropy.length / 4;
  43. // Zero rightmost "bitsLeft" bits in byte
  44. // For example: bitsLeft=4 val=10111101 -> 10110000
  45. return new Uint8Array([(sha256(entropy)[0]! >> bitsLeft) << bitsLeft]);
  46. };
  47. function getCoder(wordlist: string[]) {
  48. if (!Array.isArray(wordlist) || wordlist.length !== 2048 || typeof wordlist[0] !== 'string')
  49. throw new Error('Wordlist: expected array of 2048 strings');
  50. wordlist.forEach((i) => {
  51. if (typeof i !== 'string') throw new Error(`Wordlist: non-string element: ${i}`);
  52. });
  53. return baseUtils.chain(
  54. baseUtils.checksum(1, calcChecksum),
  55. baseUtils.radix2(11, true),
  56. baseUtils.alphabet(wordlist)
  57. );
  58. }
  59. /**
  60. * Reversible: Converts mnemonic string to raw entropy in form of byte array.
  61. * @param mnemonic 12-24 words
  62. * @param wordlist imported wordlist for specific language
  63. * @example
  64. * const mnem = 'legal winner thank year wave sausage worth useful legal winner thank yellow';
  65. * mnemonicToEntropy(mnem, wordlist)
  66. * // Produces
  67. * new Uint8Array([
  68. * 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f,
  69. * 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f
  70. * ])
  71. */
  72. export function mnemonicToEntropy(mnemonic: string, wordlist: string[]): Uint8Array {
  73. const { words } = normalize(mnemonic);
  74. const entropy = getCoder(wordlist).decode(words);
  75. assertEntropy(entropy);
  76. return entropy;
  77. }
  78. /**
  79. * Reversible: Converts raw entropy in form of byte array to mnemonic string.
  80. * @param entropy byte array
  81. * @param wordlist imported wordlist for specific language
  82. * @returns 12-24 words
  83. * @example
  84. * const ent = new Uint8Array([
  85. * 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f,
  86. * 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f
  87. * ]);
  88. * entropyToMnemonic(ent, wordlist);
  89. * // 'legal winner thank year wave sausage worth useful legal winner thank yellow'
  90. */
  91. export function entropyToMnemonic(entropy: Uint8Array, wordlist: string[]): string {
  92. assertEntropy(entropy);
  93. const words = getCoder(wordlist).encode(entropy);
  94. return words.join(isJapanese(wordlist) ? '\u3000' : ' ');
  95. }
  96. /**
  97. * Validates mnemonic for being 12-24 words contained in `wordlist`.
  98. */
  99. export function validateMnemonic(mnemonic: string, wordlist: string[]): boolean {
  100. try {
  101. mnemonicToEntropy(mnemonic, wordlist);
  102. } catch (e) {
  103. return false;
  104. }
  105. return true;
  106. }
  107. const salt = (passphrase: string) => nfkd(`mnemonic${passphrase}`);
  108. /**
  109. * Irreversible: Uses KDF to derive 64 bytes of key data from mnemonic + optional password.
  110. * @param mnemonic 12-24 words
  111. * @param passphrase string that will additionally protect the key
  112. * @returns 64 bytes of key data
  113. * @example
  114. * const mnem = 'legal winner thank year wave sausage worth useful legal winner thank yellow';
  115. * await mnemonicToSeed(mnem, 'password');
  116. * // new Uint8Array([...64 bytes])
  117. */
  118. export function mnemonicToSeed(mnemonic: string, passphrase = '') {
  119. return pbkdf2Async(sha512, normalize(mnemonic).nfkd, salt(passphrase), { c: 2048, dkLen: 64 });
  120. }
  121. /**
  122. * Irreversible: Uses KDF to derive 64 bytes of key data from mnemonic + optional password.
  123. * @param mnemonic 12-24 words
  124. * @param passphrase string that will additionally protect the key
  125. * @returns 64 bytes of key data
  126. * @example
  127. * const mnem = 'legal winner thank year wave sausage worth useful legal winner thank yellow';
  128. * mnemonicToSeedSync(mnem, 'password');
  129. * // new Uint8Array([...64 bytes])
  130. */
  131. export function mnemonicToSeedSync(mnemonic: string, passphrase = '') {
  132. return pbkdf2(sha512, normalize(mnemonic).nfkd, salt(passphrase), { c: 2048, dkLen: 64 });
  133. }