index.ts 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. /*! scure-bip32 - MIT License (c) 2022 Patricio Palladino, Paul Miller (paulmillr.com) */
  2. import { hmac } from '@noble/hashes/hmac';
  3. import { ripemd160 } from '@noble/hashes/ripemd160';
  4. import { sha256 } from '@noble/hashes/sha256';
  5. import { sha512 } from '@noble/hashes/sha512';
  6. import { bytes as assertBytes } from '@noble/hashes/_assert';
  7. import { bytesToHex, concatBytes, createView, hexToBytes, utf8ToBytes } from '@noble/hashes/utils';
  8. import { secp256k1 as secp } from '@noble/curves/secp256k1';
  9. import { mod } from '@noble/curves/abstract/modular';
  10. import { createBase58check } from '@scure/base';
  11. const Point = secp.ProjectivePoint;
  12. const base58check = createBase58check(sha256);
  13. function bytesToNumber(bytes: Uint8Array): bigint {
  14. return BigInt(`0x${bytesToHex(bytes)}`);
  15. }
  16. function numberToBytes(num: bigint): Uint8Array {
  17. return hexToBytes(num.toString(16).padStart(64, '0'));
  18. }
  19. const MASTER_SECRET = utf8ToBytes('Bitcoin seed');
  20. // Bitcoin hardcoded by default
  21. const BITCOIN_VERSIONS: Versions = { private: 0x0488ade4, public: 0x0488b21e };
  22. export const HARDENED_OFFSET: number = 0x80000000;
  23. export interface Versions {
  24. private: number;
  25. public: number;
  26. }
  27. const hash160 = (data: Uint8Array) => ripemd160(sha256(data));
  28. const fromU32 = (data: Uint8Array) => createView(data).getUint32(0, false);
  29. const toU32 = (n: number) => {
  30. if (!Number.isSafeInteger(n) || n < 0 || n > 2 ** 32 - 1) {
  31. throw new Error(`Invalid number=${n}. Should be from 0 to 2 ** 32 - 1`);
  32. }
  33. const buf = new Uint8Array(4);
  34. createView(buf).setUint32(0, n, false);
  35. return buf;
  36. };
  37. interface HDKeyOpt {
  38. versions?: Versions;
  39. depth?: number;
  40. index?: number;
  41. parentFingerprint?: number;
  42. chainCode?: Uint8Array;
  43. publicKey?: Uint8Array;
  44. privateKey?: Uint8Array | bigint;
  45. }
  46. export class HDKey {
  47. get fingerprint(): number {
  48. if (!this.pubHash) {
  49. throw new Error('No publicKey set!');
  50. }
  51. return fromU32(this.pubHash);
  52. }
  53. get identifier(): Uint8Array | undefined {
  54. return this.pubHash;
  55. }
  56. get pubKeyHash(): Uint8Array | undefined {
  57. return this.pubHash;
  58. }
  59. get privateKey(): Uint8Array | null {
  60. return this.privKeyBytes || null;
  61. }
  62. get publicKey(): Uint8Array | null {
  63. return this.pubKey || null;
  64. }
  65. get privateExtendedKey(): string {
  66. const priv = this.privateKey;
  67. if (!priv) {
  68. throw new Error('No private key');
  69. }
  70. return base58check.encode(
  71. this.serialize(this.versions.private, concatBytes(new Uint8Array([0]), priv))
  72. );
  73. }
  74. get publicExtendedKey(): string {
  75. if (!this.pubKey) {
  76. throw new Error('No public key');
  77. }
  78. return base58check.encode(this.serialize(this.versions.public, this.pubKey));
  79. }
  80. public static fromMasterSeed(seed: Uint8Array, versions: Versions = BITCOIN_VERSIONS): HDKey {
  81. assertBytes(seed);
  82. if (8 * seed.length < 128 || 8 * seed.length > 512) {
  83. throw new Error(
  84. `HDKey: wrong seed length=${seed.length}. Should be between 128 and 512 bits; 256 bits is advised)`
  85. );
  86. }
  87. const I = hmac(sha512, MASTER_SECRET, seed);
  88. return new HDKey({
  89. versions,
  90. chainCode: I.slice(32),
  91. privateKey: I.slice(0, 32),
  92. });
  93. }
  94. public static fromExtendedKey(base58key: string, versions: Versions = BITCOIN_VERSIONS): HDKey {
  95. // => version(4) || depth(1) || fingerprint(4) || index(4) || chain(32) || key(33)
  96. const keyBuffer: Uint8Array = base58check.decode(base58key);
  97. const keyView = createView(keyBuffer);
  98. const version = keyView.getUint32(0, false);
  99. const opt = {
  100. versions,
  101. depth: keyBuffer[4],
  102. parentFingerprint: keyView.getUint32(5, false),
  103. index: keyView.getUint32(9, false),
  104. chainCode: keyBuffer.slice(13, 45),
  105. };
  106. const key = keyBuffer.slice(45);
  107. const isPriv = key[0] === 0;
  108. if (version !== versions[isPriv ? 'private' : 'public']) {
  109. throw new Error('Version mismatch');
  110. }
  111. if (isPriv) {
  112. return new HDKey({ ...opt, privateKey: key.slice(1) });
  113. } else {
  114. return new HDKey({ ...opt, publicKey: key });
  115. }
  116. }
  117. public static fromJSON(json: { xpriv: string }): HDKey {
  118. return HDKey.fromExtendedKey(json.xpriv);
  119. }
  120. public readonly versions: Versions;
  121. public readonly depth: number = 0;
  122. public readonly index: number = 0;
  123. public readonly chainCode: Uint8Array | null = null;
  124. public readonly parentFingerprint: number = 0;
  125. private privKey?: bigint;
  126. private privKeyBytes?: Uint8Array;
  127. private pubKey?: Uint8Array;
  128. private pubHash: Uint8Array | undefined;
  129. constructor(opt: HDKeyOpt) {
  130. if (!opt || typeof opt !== 'object') {
  131. throw new Error('HDKey.constructor must not be called directly');
  132. }
  133. this.versions = opt.versions || BITCOIN_VERSIONS;
  134. this.depth = opt.depth || 0;
  135. this.chainCode = opt.chainCode || null;
  136. this.index = opt.index || 0;
  137. this.parentFingerprint = opt.parentFingerprint || 0;
  138. if (!this.depth) {
  139. if (this.parentFingerprint || this.index) {
  140. throw new Error('HDKey: zero depth with non-zero index/parent fingerprint');
  141. }
  142. }
  143. if (opt.publicKey && opt.privateKey) {
  144. throw new Error('HDKey: publicKey and privateKey at same time.');
  145. }
  146. if (opt.privateKey) {
  147. if (!secp.utils.isValidPrivateKey(opt.privateKey)) {
  148. throw new Error('Invalid private key');
  149. }
  150. this.privKey =
  151. typeof opt.privateKey === 'bigint' ? opt.privateKey : bytesToNumber(opt.privateKey);
  152. this.privKeyBytes = numberToBytes(this.privKey);
  153. this.pubKey = secp.getPublicKey(opt.privateKey, true);
  154. } else if (opt.publicKey) {
  155. this.pubKey = Point.fromHex(opt.publicKey).toRawBytes(true); // force compressed point
  156. } else {
  157. throw new Error('HDKey: no public or private key provided');
  158. }
  159. this.pubHash = hash160(this.pubKey);
  160. }
  161. public derive(path: string): HDKey {
  162. if (!/^[mM]'?/.test(path)) {
  163. throw new Error('Path must start with "m" or "M"');
  164. }
  165. if (/^[mM]'?$/.test(path)) {
  166. return this;
  167. }
  168. const parts = path.replace(/^[mM]'?\//, '').split('/');
  169. // tslint:disable-next-line
  170. let child: HDKey = this;
  171. for (const c of parts) {
  172. const m = /^(\d+)('?)$/.exec(c);
  173. const m1 = m && m[1];
  174. if (!m || m.length !== 3 || typeof m1 !== 'string') {
  175. throw new Error(`Invalid child index: ${c}`);
  176. }
  177. let idx = +m1;
  178. if (!Number.isSafeInteger(idx) || idx >= HARDENED_OFFSET) {
  179. throw new Error('Invalid index');
  180. }
  181. // hardened key
  182. if (m[2] === "'") {
  183. idx += HARDENED_OFFSET;
  184. }
  185. child = child.deriveChild(idx);
  186. }
  187. return child;
  188. }
  189. public deriveChild(index: number): HDKey {
  190. if (!this.pubKey || !this.chainCode) {
  191. throw new Error('No publicKey or chainCode set');
  192. }
  193. let data = toU32(index);
  194. if (index >= HARDENED_OFFSET) {
  195. // Hardened
  196. const priv = this.privateKey;
  197. if (!priv) {
  198. throw new Error('Could not derive hardened child key');
  199. }
  200. // Hardened child: 0x00 || ser256(kpar) || ser32(index)
  201. data = concatBytes(new Uint8Array([0]), priv, data);
  202. } else {
  203. // Normal child: serP(point(kpar)) || ser32(index)
  204. data = concatBytes(this.pubKey, data);
  205. }
  206. const I = hmac(sha512, this.chainCode, data);
  207. const childTweak = bytesToNumber(I.slice(0, 32));
  208. const chainCode = I.slice(32);
  209. if (!secp.utils.isValidPrivateKey(childTweak)) {
  210. throw new Error('Tweak bigger than curve order');
  211. }
  212. const opt: HDKeyOpt = {
  213. versions: this.versions,
  214. chainCode,
  215. depth: this.depth + 1,
  216. parentFingerprint: this.fingerprint,
  217. index,
  218. };
  219. try {
  220. // Private parent key -> private child key
  221. if (this.privateKey) {
  222. const added = mod(this.privKey! + childTweak, secp.CURVE.n);
  223. if (!secp.utils.isValidPrivateKey(added)) {
  224. throw new Error('The tweak was out of range or the resulted private key is invalid');
  225. }
  226. opt.privateKey = added;
  227. } else {
  228. const added = Point.fromHex(this.pubKey).add(Point.fromPrivateKey(childTweak));
  229. // Cryptographically impossible: hmac-sha512 preimage would need to be found
  230. if (added.equals(Point.ZERO)) {
  231. throw new Error('The tweak was equal to negative P, which made the result key invalid');
  232. }
  233. opt.publicKey = added.toRawBytes(true);
  234. }
  235. return new HDKey(opt);
  236. } catch (err) {
  237. return this.deriveChild(index + 1);
  238. }
  239. }
  240. public sign(hash: Uint8Array): Uint8Array {
  241. if (!this.privateKey) {
  242. throw new Error('No privateKey set!');
  243. }
  244. assertBytes(hash, 32);
  245. return secp.sign(hash, this.privKey!).toCompactRawBytes();
  246. }
  247. public verify(hash: Uint8Array, signature: Uint8Array): boolean {
  248. assertBytes(hash, 32);
  249. assertBytes(signature, 64);
  250. if (!this.publicKey) {
  251. throw new Error('No publicKey set!');
  252. }
  253. let sig;
  254. try {
  255. sig = secp.Signature.fromCompact(signature);
  256. } catch (error) {
  257. return false;
  258. }
  259. return secp.verify(sig, hash, this.publicKey);
  260. }
  261. public wipePrivateData(): this {
  262. this.privKey = undefined;
  263. if (this.privKeyBytes) {
  264. this.privKeyBytes.fill(0);
  265. this.privKeyBytes = undefined;
  266. }
  267. return this;
  268. }
  269. public toJSON(): { xpriv: string; xpub: string } {
  270. return {
  271. xpriv: this.privateExtendedKey,
  272. xpub: this.publicExtendedKey,
  273. };
  274. }
  275. private serialize(version: number, key: Uint8Array) {
  276. if (!this.chainCode) {
  277. throw new Error('No chainCode set');
  278. }
  279. assertBytes(key, 33);
  280. // version(4) || depth(1) || fingerprint(4) || index(4) || chain(32) || key(33)
  281. return concatBytes(
  282. toU32(version),
  283. new Uint8Array([this.depth]),
  284. toU32(this.parentFingerprint),
  285. toU32(this.index),
  286. this.chainCode,
  287. key
  288. );
  289. }
  290. }