mnemonic.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import { pbkdf2, sha256 } from "../crypto/index.js";
  2. import {
  3. defineProperties, getBytes, hexlify, assertNormalize, assertPrivate, assertArgument, toUtf8Bytes
  4. } from "../utils/index.js";
  5. import { LangEn } from "../wordlists/lang-en.js";
  6. import type { BytesLike } from "../utils/index.js";
  7. import type { Wordlist } from "../wordlists/index.js";
  8. // Returns a byte with the MSB bits set
  9. function getUpperMask(bits: number): number {
  10. return ((1 << bits) - 1) << (8 - bits) & 0xff;
  11. }
  12. // Returns a byte with the LSB bits set
  13. function getLowerMask(bits: number): number {
  14. return ((1 << bits) - 1) & 0xff;
  15. }
  16. function mnemonicToEntropy(mnemonic: string, wordlist?: null | Wordlist): string {
  17. assertNormalize("NFKD");
  18. if (wordlist == null) { wordlist = LangEn.wordlist(); }
  19. const words = wordlist.split(mnemonic);
  20. assertArgument((words.length % 3) === 0 && words.length >= 12 && words.length <= 24,
  21. "invalid mnemonic length", "mnemonic", "[ REDACTED ]");
  22. const entropy = new Uint8Array(Math.ceil(11 * words.length / 8));
  23. let offset = 0;
  24. for (let i = 0; i < words.length; i++) {
  25. let index = wordlist.getWordIndex(words[i].normalize("NFKD"));
  26. assertArgument(index >= 0, `invalid mnemonic word at index ${ i }`, "mnemonic", "[ REDACTED ]");
  27. for (let bit = 0; bit < 11; bit++) {
  28. if (index & (1 << (10 - bit))) {
  29. entropy[offset >> 3] |= (1 << (7 - (offset % 8)));
  30. }
  31. offset++;
  32. }
  33. }
  34. const entropyBits = 32 * words.length / 3;
  35. const checksumBits = words.length / 3;
  36. const checksumMask = getUpperMask(checksumBits);
  37. const checksum = getBytes(sha256(entropy.slice(0, entropyBits / 8)))[0] & checksumMask;
  38. assertArgument(checksum === (entropy[entropy.length - 1] & checksumMask),
  39. "invalid mnemonic checksum", "mnemonic", "[ REDACTED ]");
  40. return hexlify(entropy.slice(0, entropyBits / 8));
  41. }
  42. function entropyToMnemonic(entropy: Uint8Array, wordlist?: null | Wordlist): string {
  43. assertArgument((entropy.length % 4) === 0 && entropy.length >= 16 && entropy.length <= 32,
  44. "invalid entropy size", "entropy", "[ REDACTED ]");
  45. if (wordlist == null) { wordlist = LangEn.wordlist(); }
  46. const indices: Array<number> = [ 0 ];
  47. let remainingBits = 11;
  48. for (let i = 0; i < entropy.length; i++) {
  49. // Consume the whole byte (with still more to go)
  50. if (remainingBits > 8) {
  51. indices[indices.length - 1] <<= 8;
  52. indices[indices.length - 1] |= entropy[i];
  53. remainingBits -= 8;
  54. // This byte will complete an 11-bit index
  55. } else {
  56. indices[indices.length - 1] <<= remainingBits;
  57. indices[indices.length - 1] |= entropy[i] >> (8 - remainingBits);
  58. // Start the next word
  59. indices.push(entropy[i] & getLowerMask(8 - remainingBits));
  60. remainingBits += 3;
  61. }
  62. }
  63. // Compute the checksum bits
  64. const checksumBits = entropy.length / 4;
  65. const checksum = parseInt(sha256(entropy).substring(2, 4), 16) & getUpperMask(checksumBits);
  66. // Shift the checksum into the word indices
  67. indices[indices.length - 1] <<= checksumBits;
  68. indices[indices.length - 1] |= (checksum >> (8 - checksumBits));
  69. return wordlist.join(indices.map((index) => (<Wordlist>wordlist).getWord(index)));
  70. }
  71. const _guard = { };
  72. /**
  73. * A **Mnemonic** wraps all properties required to compute [[link-bip-39]]
  74. * seeds and convert between phrases and entropy.
  75. */
  76. export class Mnemonic {
  77. /**
  78. * The mnemonic phrase of 12, 15, 18, 21 or 24 words.
  79. *
  80. * Use the [[wordlist]] ``split`` method to get the individual words.
  81. */
  82. readonly phrase!: string;
  83. /**
  84. * The password used for this mnemonic. If no password is used this
  85. * is the empty string (i.e. ``""``) as per the specification.
  86. */
  87. readonly password!: string;
  88. /**
  89. * The wordlist for this mnemonic.
  90. */
  91. readonly wordlist!: Wordlist;
  92. /**
  93. * The underlying entropy which the mnemonic encodes.
  94. */
  95. readonly entropy!: string;
  96. /**
  97. * @private
  98. */
  99. constructor(guard: any, entropy: string, phrase: string, password?: null | string, wordlist?: null | Wordlist) {
  100. if (password == null) { password = ""; }
  101. if (wordlist == null) { wordlist = LangEn.wordlist(); }
  102. assertPrivate(guard, _guard, "Mnemonic");
  103. defineProperties<Mnemonic>(this, { phrase, password, wordlist, entropy });
  104. }
  105. /**
  106. * Returns the seed for the mnemonic.
  107. */
  108. computeSeed(): string {
  109. const salt = toUtf8Bytes("mnemonic" + this.password, "NFKD");
  110. return pbkdf2(toUtf8Bytes(this.phrase, "NFKD"), salt, 2048, 64, "sha512");
  111. }
  112. /**
  113. * Creates a new Mnemonic for the %%phrase%%.
  114. *
  115. * The default %%password%% is the empty string and the default
  116. * wordlist is the [English wordlists](LangEn).
  117. */
  118. static fromPhrase(phrase: string, password?: null | string, wordlist?: null | Wordlist): Mnemonic {
  119. // Normalize the case and space; throws if invalid
  120. const entropy = mnemonicToEntropy(phrase, wordlist);
  121. phrase = entropyToMnemonic(getBytes(entropy), wordlist);
  122. return new Mnemonic(_guard, entropy, phrase, password, wordlist);
  123. }
  124. /**
  125. * Create a new **Mnemonic** from the %%entropy%%.
  126. *
  127. * The default %%password%% is the empty string and the default
  128. * wordlist is the [English wordlists](LangEn).
  129. */
  130. static fromEntropy(_entropy: BytesLike, password?: null | string, wordlist?: null | Wordlist): Mnemonic {
  131. const entropy = getBytes(_entropy, "entropy");
  132. const phrase = entropyToMnemonic(entropy, wordlist);
  133. return new Mnemonic(_guard, hexlify(entropy), phrase, password, wordlist);
  134. }
  135. /**
  136. * Returns the phrase for %%mnemonic%%.
  137. */
  138. static entropyToPhrase(_entropy: BytesLike, wordlist?: null | Wordlist): string {
  139. const entropy = getBytes(_entropy, "entropy");
  140. return entropyToMnemonic(entropy, wordlist);
  141. }
  142. /**
  143. * Returns the entropy for %%phrase%%.
  144. */
  145. static phraseToEntropy(phrase: string, wordlist?: null | Wordlist): string {
  146. return mnemonicToEntropy(phrase, wordlist);
  147. }
  148. /**
  149. * Returns true if %%phrase%% is a valid [[link-bip-39]] phrase.
  150. *
  151. * This checks all the provided words belong to the %%wordlist%%,
  152. * that the length is valid and the checksum is correct.
  153. */
  154. static isValidMnemonic(phrase: string, wordlist?: null | Wordlist): boolean {
  155. try {
  156. mnemonicToEntropy(phrase, wordlist);
  157. return true;
  158. } catch (error) { }
  159. return false;
  160. }
  161. }