index.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. "use strict";
  2. // See: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
  3. // See: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
  4. import { ExternallyOwnedAccount } from "@ethersproject/abstract-signer";
  5. import { Base58 } from "@ethersproject/basex";
  6. import { arrayify, BytesLike, concat, hexDataSlice, hexZeroPad, hexlify } from "@ethersproject/bytes";
  7. import { BigNumber } from "@ethersproject/bignumber";
  8. import { toUtf8Bytes, UnicodeNormalizationForm } from "@ethersproject/strings";
  9. import { pbkdf2 } from "@ethersproject/pbkdf2";
  10. import { defineReadOnly } from "@ethersproject/properties";
  11. import { SigningKey } from "@ethersproject/signing-key";
  12. import { computeHmac, ripemd160, sha256, SupportedAlgorithm } from "@ethersproject/sha2";
  13. import { computeAddress } from "@ethersproject/transactions";
  14. import { Wordlist, wordlists } from "@ethersproject/wordlists";
  15. import { Logger } from "@ethersproject/logger";
  16. import { version } from "./_version";
  17. const logger = new Logger(version);
  18. const N = BigNumber.from("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141");
  19. // "Bitcoin seed"
  20. const MasterSecret = toUtf8Bytes("Bitcoin seed");
  21. const HardenedBit = 0x80000000;
  22. // Returns a byte with the MSB bits set
  23. function getUpperMask(bits: number): number {
  24. return ((1 << bits) - 1) << (8 - bits);
  25. }
  26. // Returns a byte with the LSB bits set
  27. function getLowerMask(bits: number): number {
  28. return (1 << bits) - 1;
  29. }
  30. function bytes32(value: BigNumber | Uint8Array): string {
  31. return hexZeroPad(hexlify(value), 32);
  32. }
  33. function base58check(data: Uint8Array): string {
  34. return Base58.encode(concat([ data, hexDataSlice(sha256(sha256(data)), 0, 4) ]));
  35. }
  36. function getWordlist(wordlist: string | Wordlist): Wordlist {
  37. if (wordlist == null) {
  38. return wordlists["en"];
  39. }
  40. if (typeof(wordlist) === "string") {
  41. const words = wordlists[wordlist];
  42. if (words == null) {
  43. logger.throwArgumentError("unknown locale", "wordlist", wordlist);
  44. }
  45. return words;
  46. }
  47. return wordlist;
  48. }
  49. const _constructorGuard: any = {};
  50. export const defaultPath = "m/44'/60'/0'/0/0";
  51. export interface Mnemonic {
  52. readonly phrase: string;
  53. readonly path: string;
  54. readonly locale: string;
  55. };
  56. export class HDNode implements ExternallyOwnedAccount {
  57. readonly privateKey: string;
  58. readonly publicKey: string;
  59. readonly fingerprint: string;
  60. readonly parentFingerprint: string;
  61. readonly address: string;
  62. readonly mnemonic?: Mnemonic;
  63. readonly path: string;
  64. readonly chainCode: string;
  65. readonly index: number;
  66. readonly depth: number;
  67. /**
  68. * This constructor should not be called directly.
  69. *
  70. * Please use:
  71. * - fromMnemonic
  72. * - fromSeed
  73. */
  74. constructor(constructorGuard: any, privateKey: string, publicKey: string, parentFingerprint: string, chainCode: string, index: number, depth: number, mnemonicOrPath: Mnemonic | string) {
  75. /* istanbul ignore if */
  76. if (constructorGuard !== _constructorGuard) {
  77. throw new Error("HDNode constructor cannot be called directly");
  78. }
  79. if (privateKey) {
  80. const signingKey = new SigningKey(privateKey);
  81. defineReadOnly(this, "privateKey", signingKey.privateKey);
  82. defineReadOnly(this, "publicKey", signingKey.compressedPublicKey);
  83. } else {
  84. defineReadOnly(this, "privateKey", null);
  85. defineReadOnly(this, "publicKey", hexlify(publicKey));
  86. }
  87. defineReadOnly(this, "parentFingerprint", parentFingerprint);
  88. defineReadOnly(this, "fingerprint", hexDataSlice(ripemd160(sha256(this.publicKey)), 0, 4));
  89. defineReadOnly(this, "address", computeAddress(this.publicKey));
  90. defineReadOnly(this, "chainCode", chainCode);
  91. defineReadOnly(this, "index", index);
  92. defineReadOnly(this, "depth", depth);
  93. if (mnemonicOrPath == null) {
  94. // From a source that does not preserve the path (e.g. extended keys)
  95. defineReadOnly(this, "mnemonic", null);
  96. defineReadOnly(this, "path", null);
  97. } else if (typeof(mnemonicOrPath) === "string") {
  98. // From a source that does not preserve the mnemonic (e.g. neutered)
  99. defineReadOnly(this, "mnemonic", null);
  100. defineReadOnly(this, "path", mnemonicOrPath);
  101. } else {
  102. // From a fully qualified source
  103. defineReadOnly(this, "mnemonic", mnemonicOrPath);
  104. defineReadOnly(this, "path", mnemonicOrPath.path);
  105. }
  106. }
  107. get extendedKey(): string {
  108. // We only support the mainnet values for now, but if anyone needs
  109. // testnet values, let me know. I believe current sentiment is that
  110. // we should always use mainnet, and use BIP-44 to derive the network
  111. // - Mainnet: public=0x0488B21E, private=0x0488ADE4
  112. // - Testnet: public=0x043587CF, private=0x04358394
  113. if (this.depth >= 256) { throw new Error("Depth too large!"); }
  114. return base58check(concat([
  115. ((this.privateKey != null) ? "0x0488ADE4": "0x0488B21E"),
  116. hexlify(this.depth),
  117. this.parentFingerprint,
  118. hexZeroPad(hexlify(this.index), 4),
  119. this.chainCode,
  120. ((this.privateKey != null) ? concat([ "0x00", this.privateKey ]): this.publicKey),
  121. ]));
  122. }
  123. neuter(): HDNode {
  124. return new HDNode(_constructorGuard, null, this.publicKey, this.parentFingerprint, this.chainCode, this.index, this.depth, this.path);
  125. }
  126. private _derive(index: number): HDNode {
  127. if (index > 0xffffffff) { throw new Error("invalid index - " + String(index)); }
  128. // Base path
  129. let path = this.path;
  130. if (path) { path += "/" + (index & ~HardenedBit); }
  131. const data = new Uint8Array(37);
  132. if (index & HardenedBit) {
  133. if (!this.privateKey) {
  134. throw new Error("cannot derive child of neutered node");
  135. }
  136. // Data = 0x00 || ser_256(k_par)
  137. data.set(arrayify(this.privateKey), 1);
  138. // Hardened path
  139. if (path) { path += "'"; }
  140. } else {
  141. // Data = ser_p(point(k_par))
  142. data.set(arrayify(this.publicKey));
  143. }
  144. // Data += ser_32(i)
  145. for (let i = 24; i >= 0; i -= 8) { data[33 + (i >> 3)] = ((index >> (24 - i)) & 0xff); }
  146. const I = arrayify(computeHmac(SupportedAlgorithm.sha512, this.chainCode, data));
  147. const IL = I.slice(0, 32);
  148. const IR = I.slice(32);
  149. // The private key
  150. let ki: string = null
  151. // The public key
  152. let Ki: string = null;
  153. if (this.privateKey) {
  154. ki = bytes32(BigNumber.from(IL).add(this.privateKey).mod(N));
  155. } else {
  156. const ek = new SigningKey(hexlify(IL));
  157. Ki = ek._addPoint(this.publicKey);
  158. }
  159. let mnemonicOrPath: Mnemonic | string = path;
  160. const srcMnemonic = this.mnemonic;
  161. if (srcMnemonic) {
  162. mnemonicOrPath = Object.freeze({
  163. phrase: srcMnemonic.phrase,
  164. path: path,
  165. locale: (srcMnemonic.locale || "en")
  166. });
  167. }
  168. return new HDNode(_constructorGuard, ki, Ki, this.fingerprint, bytes32(IR), index, this.depth + 1, mnemonicOrPath);
  169. }
  170. derivePath(path: string): HDNode {
  171. const components = path.split("/");
  172. if (components.length === 0 || (components[0] === "m" && this.depth !== 0)) {
  173. throw new Error("invalid path - " + path);
  174. }
  175. if (components[0] === "m") { components.shift(); }
  176. let result: HDNode = this;
  177. for (let i = 0; i < components.length; i++) {
  178. const component = components[i];
  179. if (component.match(/^[0-9]+'$/)) {
  180. const index = parseInt(component.substring(0, component.length - 1));
  181. if (index >= HardenedBit) { throw new Error("invalid path index - " + component); }
  182. result = result._derive(HardenedBit + index);
  183. } else if (component.match(/^[0-9]+$/)) {
  184. const index = parseInt(component);
  185. if (index >= HardenedBit) { throw new Error("invalid path index - " + component); }
  186. result = result._derive(index);
  187. } else {
  188. throw new Error("invalid path component - " + component);
  189. }
  190. }
  191. return result;
  192. }
  193. static _fromSeed(seed: BytesLike, mnemonic: Mnemonic): HDNode {
  194. const seedArray: Uint8Array = arrayify(seed);
  195. if (seedArray.length < 16 || seedArray.length > 64) { throw new Error("invalid seed"); }
  196. const I: Uint8Array = arrayify(computeHmac(SupportedAlgorithm.sha512, MasterSecret, seedArray));
  197. return new HDNode(_constructorGuard, bytes32(I.slice(0, 32)), null, "0x00000000", bytes32(I.slice(32)), 0, 0, mnemonic);
  198. }
  199. static fromMnemonic(mnemonic: string, password?: string, wordlist?: string | Wordlist): HDNode {
  200. // If a locale name was passed in, find the associated wordlist
  201. wordlist = getWordlist(wordlist);
  202. // Normalize the case and spacing in the mnemonic (throws if the mnemonic is invalid)
  203. mnemonic = entropyToMnemonic(mnemonicToEntropy(mnemonic, wordlist), wordlist);
  204. return HDNode._fromSeed(mnemonicToSeed(mnemonic, password), {
  205. phrase: mnemonic,
  206. path: "m",
  207. locale: wordlist.locale
  208. });
  209. }
  210. static fromSeed(seed: BytesLike): HDNode {
  211. return HDNode._fromSeed(seed, null);
  212. }
  213. static fromExtendedKey(extendedKey: string): HDNode {
  214. const bytes = Base58.decode(extendedKey);
  215. if (bytes.length !== 82 || base58check(bytes.slice(0, 78)) !== extendedKey) {
  216. logger.throwArgumentError("invalid extended key", "extendedKey", "[REDACTED]");
  217. }
  218. const depth = bytes[4];
  219. const parentFingerprint = hexlify(bytes.slice(5, 9));
  220. const index = parseInt(hexlify(bytes.slice(9, 13)).substring(2), 16);
  221. const chainCode = hexlify(bytes.slice(13, 45));
  222. const key = bytes.slice(45, 78);
  223. switch (hexlify(bytes.slice(0, 4))) {
  224. // Public Key
  225. case "0x0488b21e": case "0x043587cf":
  226. return new HDNode(_constructorGuard, null, hexlify(key), parentFingerprint, chainCode, index, depth, null);
  227. // Private Key
  228. case "0x0488ade4": case "0x04358394 ":
  229. if (key[0] !== 0) { break; }
  230. return new HDNode(_constructorGuard, hexlify(key.slice(1)), null, parentFingerprint, chainCode, index, depth, null);
  231. }
  232. return logger.throwArgumentError("invalid extended key", "extendedKey", "[REDACTED]");
  233. }
  234. }
  235. export function mnemonicToSeed(mnemonic: string, password?: string): string {
  236. if (!password) { password = ""; }
  237. const salt = toUtf8Bytes("mnemonic" + password, UnicodeNormalizationForm.NFKD);
  238. return pbkdf2(toUtf8Bytes(mnemonic, UnicodeNormalizationForm.NFKD), salt, 2048, 64, "sha512");
  239. }
  240. export function mnemonicToEntropy(mnemonic: string, wordlist?: string | Wordlist): string {
  241. wordlist = getWordlist(wordlist);
  242. logger.checkNormalize();
  243. const words = wordlist.split(mnemonic);
  244. if ((words.length % 3) !== 0) { throw new Error("invalid mnemonic"); }
  245. const entropy = arrayify(new Uint8Array(Math.ceil(11 * words.length / 8)));
  246. let offset = 0;
  247. for (let i = 0; i < words.length; i++) {
  248. let index = wordlist.getWordIndex(words[i].normalize("NFKD"));
  249. if (index === -1) { throw new Error("invalid mnemonic"); }
  250. for (let bit = 0; bit < 11; bit++) {
  251. if (index & (1 << (10 - bit))) {
  252. entropy[offset >> 3] |= (1 << (7 - (offset % 8)));
  253. }
  254. offset++;
  255. }
  256. }
  257. const entropyBits = 32 * words.length / 3;
  258. const checksumBits = words.length / 3;
  259. const checksumMask = getUpperMask(checksumBits);
  260. const checksum = arrayify(sha256(entropy.slice(0, entropyBits / 8)))[0] & checksumMask;
  261. if (checksum !== (entropy[entropy.length - 1] & checksumMask)) {
  262. throw new Error("invalid checksum");
  263. }
  264. return hexlify(entropy.slice(0, entropyBits / 8));
  265. }
  266. export function entropyToMnemonic(entropy: BytesLike, wordlist?: string | Wordlist): string {
  267. wordlist = getWordlist(wordlist);
  268. entropy = arrayify(entropy);
  269. if ((entropy.length % 4) !== 0 || entropy.length < 16 || entropy.length > 32) {
  270. throw new Error("invalid entropy");
  271. }
  272. const indices: Array<number> = [ 0 ];
  273. let remainingBits = 11;
  274. for (let i = 0; i < entropy.length; i++) {
  275. // Consume the whole byte (with still more to go)
  276. if (remainingBits > 8) {
  277. indices[indices.length - 1] <<= 8;
  278. indices[indices.length - 1] |= entropy[i];
  279. remainingBits -= 8;
  280. // This byte will complete an 11-bit index
  281. } else {
  282. indices[indices.length - 1] <<= remainingBits;
  283. indices[indices.length - 1] |= entropy[i] >> (8 - remainingBits);
  284. // Start the next word
  285. indices.push(entropy[i] & getLowerMask(8 - remainingBits));
  286. remainingBits += 3;
  287. }
  288. }
  289. // Compute the checksum bits
  290. const checksumBits = entropy.length / 4;
  291. const checksum = arrayify(sha256(entropy))[0] & getUpperMask(checksumBits);
  292. // Shift the checksum into the word indices
  293. indices[indices.length - 1] <<= checksumBits;
  294. indices[indices.length - 1] |= (checksum >> (8 - checksumBits));
  295. return wordlist.join(indices.map((index) => (<Wordlist>wordlist).getWord(index)));
  296. }
  297. export function isValidMnemonic(mnemonic: string, wordlist?: Wordlist): boolean {
  298. try {
  299. mnemonicToEntropy(mnemonic, wordlist);
  300. return true;
  301. } catch (error) { }
  302. return false;
  303. }
  304. export function getAccountPath(index: number): string {
  305. if (typeof(index) !== "number" || index < 0 || index >= HardenedBit || index % 1) {
  306. logger.throwArgumentError("invalid account index", "index", index);
  307. }
  308. return `m/44'/60'/${ index }'/0/0`;
  309. }