@zlattice/lattice-js
Version:
Lattice blockchain TypeScript SDK with dual module support (CJS + ESM)
344 lines • 13.4 kB
JavaScript
import { Curves } from "../common/index.js";
import { compressPublicKeyHex, getPublicKeyFromPrivateKey } from "../crypto/sm2/index.js";
import { sm2Curve as sm2p } from "../crypto/sm2/ec.js";
import { mod } from "@noble/curves/abstract/modular";
import { secp256k1 as secp } from "@noble/curves/secp256k1";
import { hmac } from "@noble/hashes/hmac";
import { ripemd160 } from "@noble/hashes/legacy";
import { sha256, sha512 } from "@noble/hashes/sha2";
import { abytes, bytesToHex, concatBytes, createView, hexToBytes, utf8ToBytes } from "@noble/hashes/utils";
import { createBase58check } from "@scure/base";
const Point = secp.ProjectivePoint;
const base58check = createBase58check(sha256);
function bytesToNumber(bytes) {
abytes(bytes);
const h = bytes.length === 0 ? "0" : bytesToHex(bytes);
return BigInt(`0x${h}`);
}
function numberToBytes(num) {
if (typeof num !== "bigint")
throw new Error("bigint expected");
return hexToBytes(num.toString(16).padStart(64, "0"));
}
const MASTER_SECRET = utf8ToBytes("Bitcoin seed");
const BITCOIN_VERSIONS = { private: 0x0488ade4, public: 0x0488b21e };
/** Hardened offset from Bitcoin, default */
export const HARDENED_OFFSET = 0x80000000;
const hash160 = (data) => ripemd160(sha256(data));
const fromU32 = (data) => createView(data).getUint32(0, false);
const toU32 = (n) => {
if (!Number.isSafeInteger(n) || n < 0 || n > 2 ** 32 - 1) {
throw new Error(`invalid number, should be from 0 to 2**32-1, got ${n}`);
}
const buf = new Uint8Array(4);
createView(buf).setUint32(0, n, false);
return buf;
};
/**
* HDKey from BIP32
* @example
```js
const hdkey1 = HDKey.fromMasterSeed(seed);
const hdkey2 = HDKey.fromExtendedKey(base58key);
const hdkey3 = HDKey.fromJSON({ xpriv: string });
```
*/
export class HDKey {
get fingerprint() {
if (!this.pubHash) {
throw new Error("No publicKey set!");
}
return fromU32(this.pubHash);
}
get identifier() {
return this.pubHash;
}
get pubKeyHash() {
return this.pubHash;
}
get privateKey() {
return this.privKeyBytes || null;
}
get publicKey() {
return this.pubKey || null;
}
get privateExtendedKey() {
const priv = this.privateKey;
if (!priv) {
throw new Error("No private key");
}
return base58check.encode(this.serialize(this.versions.private, concatBytes(new Uint8Array([0]), priv)));
}
get publicExtendedKey() {
if (!this.pubKey) {
throw new Error("No public key");
}
return base58check.encode(this.serialize(this.versions.public, this.pubKey));
}
static fromMasterSeed(seed, versions = BITCOIN_VERSIONS, curve = Curves.Sm2p256v1) {
abytes(seed);
if (8 * seed.length < 128 || 8 * seed.length > 512) {
throw new Error(`HDKey: seed length must be between 128 and 512 bits; 256 bits is advised, got ${seed.length}`);
}
const I = hmac(sha512, MASTER_SECRET, seed);
return new HDKey({
versions,
chainCode: I.slice(32),
privateKey: I.slice(0, 32),
curve
});
}
static fromExtendedKey(base58key, versions = BITCOIN_VERSIONS, curve = Curves.Sm2p256v1) {
// => version(4) || depth(1) || fingerprint(4) || index(4) || chain(32) || key(33)
const keyBuffer = base58check.decode(base58key);
const keyView = createView(keyBuffer);
const version = keyView.getUint32(0, false);
const opt = {
versions,
depth: keyBuffer[4],
parentFingerprint: keyView.getUint32(5, false),
index: keyView.getUint32(9, false),
chainCode: keyBuffer.slice(13, 45)
};
const key = keyBuffer.slice(45);
const isPriv = key[0] === 0;
if (version !== versions[isPriv ? "private" : "public"]) {
throw new Error("Version mismatch");
}
if (isPriv) {
return new HDKey({ ...opt, privateKey: key.slice(1), curve });
}
return new HDKey({ ...opt, publicKey: key, curve });
}
static fromJSON(json) {
return HDKey.fromExtendedKey(json.xpriv);
}
constructor(opt) {
this.depth = 0;
this.index = 0;
this.chainCode = null;
this.parentFingerprint = 0;
if (!opt || typeof opt !== "object") {
throw new Error("HDKey.constructor must not be called directly");
}
this.versions = opt.versions || BITCOIN_VERSIONS;
this.depth = opt.depth || 0;
this.chainCode = opt.chainCode || null;
this.index = opt.index || 0;
this.parentFingerprint = opt.parentFingerprint || 0;
this.curve = opt.curve || Curves.Sm2p256v1;
if (!this.depth) {
if (this.parentFingerprint || this.index) {
throw new Error("HDKey: zero depth with non-zero index/parent fingerprint");
}
}
if (opt.publicKey && opt.privateKey) {
throw new Error("HDKey: publicKey and privateKey at same time.");
}
if (opt.privateKey) {
switch (this.curve) {
case Curves.Sm2p256v1:
if (!sm2p.utils.isValidPrivateKey(bytesToHex(typeof opt.privateKey === "bigint"
? numberToBytes(opt.privateKey)
: opt.privateKey))) {
throw new Error("Invalid private key");
}
this.privKey =
typeof opt.privateKey === "bigint"
? opt.privateKey
: bytesToNumber(opt.privateKey);
this.privKeyBytes = numberToBytes(this.privKey);
this.pubKey = hexToBytes(getPublicKeyFromPrivateKey(typeof opt.privateKey === "bigint"
? bytesToHex(numberToBytes(opt.privateKey))
: bytesToHex(opt.privateKey), true));
break;
case Curves.Secp256k1:
if (!secp.utils.isValidPrivateKey(opt.privateKey)) {
throw new Error("Invalid private key");
}
this.privKey =
typeof opt.privateKey === "bigint"
? opt.privateKey
: bytesToNumber(opt.privateKey);
this.privKeyBytes = numberToBytes(this.privKey);
this.pubKey = secp.getPublicKey(opt.privateKey, true);
break;
default:
throw new Error(`Unsupported curve: ${opt.curve}`);
}
}
else if (opt.publicKey) {
if (opt.curve === Curves.Sm2p256v1) {
// compressed public key
const compressedPublicKey = compressPublicKeyHex(`0x${bytesToHex(opt.publicKey)}`);
this.pubKey = hexToBytes(compressedPublicKey);
}
else {
this.pubKey = Point.fromHex(opt.publicKey).toRawBytes(true); // force compressed point
}
}
else {
throw new Error("HDKey: no public or private key provided");
}
this.pubHash = hash160(this.pubKey);
}
derive(path) {
if (!/^[mM]'?/.test(path)) {
throw new Error('Path must start with "m" or "M"');
}
if (/^[mM]'?$/.test(path)) {
return this;
}
const parts = path.replace(/^[mM]'?\//, "").split("/");
// tslint:disable-next-line
let child = this;
for (const c of parts) {
const m = /^(\d+)('?)$/.exec(c);
const m1 = m?.[1];
if (!m || m.length !== 3 || typeof m1 !== "string") {
throw new Error(`invalid child index: ${c}`);
}
let idx = +m1;
if (!Number.isSafeInteger(idx) || idx >= HARDENED_OFFSET) {
throw new Error("Invalid index");
}
// hardened key
if (m[2] === "'") {
idx += HARDENED_OFFSET;
}
child = child.deriveChild(idx);
}
return child;
}
deriveChild(index) {
if (!this.pubKey || !this.chainCode) {
throw new Error("No publicKey or chainCode set");
}
let data = toU32(index);
if (index >= HARDENED_OFFSET) {
// Hardened
const priv = this.privateKey;
if (!priv) {
throw new Error("Could not derive hardened child key");
}
// Hardened child: 0x00 || ser256(kpar) || ser32(index)
data = concatBytes(new Uint8Array([0]), priv, data);
}
else {
// Normal child: serP(point(kpar)) || ser32(index)
data = concatBytes(this.pubKey, data);
}
const I = hmac(sha512, this.chainCode, data);
const childTweak = bytesToNumber(I.slice(0, 32));
const chainCode = I.slice(32);
if (this.curve === Curves.Sm2p256v1) {
if (!sm2p.utils.isValidPrivateKey(childTweak)) {
throw new Error("Tweak bigger than curve order");
}
}
else {
if (!secp.utils.isValidPrivateKey(childTweak)) {
throw new Error("Tweak bigger than curve order");
}
}
const opt = {
versions: this.versions,
chainCode,
depth: this.depth + 1,
parentFingerprint: this.fingerprint,
index
};
try {
// Private parent key -> private child key
if (this.privateKey) {
if (this.curve === Curves.Sm2p256v1) {
const added = mod(this.privKey + childTweak, sm2p.CURVE.n);
if (!sm2p.utils.isValidPrivateKey(bytesToHex(numberToBytes(added)))) {
throw new Error("The tweak was out of range or the resulted private key is invalid");
}
opt.privateKey = added;
}
else {
const added = mod(this.privKey + childTweak, secp.CURVE.n);
if (!secp.utils.isValidPrivateKey(added)) {
throw new Error("The tweak was out of range or the resulted private key is invalid");
}
opt.privateKey = added;
}
}
else {
if (this.curve === Curves.Sm2p256v1) {
const added = sm2p.ProjectivePoint.fromHex(this.pubKey).add(sm2p.ProjectivePoint.fromPrivateKey(childTweak));
if (added.equals(sm2p.ProjectivePoint.ZERO)) {
throw new Error("The tweak was equal to negative P, which made the result key invalid");
}
opt.publicKey = added.toRawBytes(true);
}
else {
const added = Point.fromHex(this.pubKey).add(Point.fromPrivateKey(childTweak));
// Cryptographically impossible: hmac-sha512 preimage would need to be found
if (added.equals(Point.ZERO)) {
throw new Error("The tweak was equal to negative P, which made the result key invalid");
}
opt.publicKey = added.toRawBytes(true);
}
}
return new HDKey(opt);
}
catch (err) {
return this.deriveChild(index + 1);
}
}
sign(hash) {
if (!this.privateKey) {
throw new Error("No privateKey set!");
}
abytes(hash, 32);
return this.curve === Curves.Sm2p256v1
? sm2p.sign(hash, this.privKey).toCompactRawBytes()
: secp.sign(hash, this.privKey).toCompactRawBytes();
}
verify(hash, signature) {
abytes(hash, 32);
abytes(signature, 64);
if (!this.publicKey) {
throw new Error("No publicKey set!");
}
let sig;
try {
sig =
this.curve === Curves.Sm2p256v1
? sm2p.Signature.fromCompact(signature)
: secp.Signature.fromCompact(signature);
}
catch (error) {
return false;
}
return this.curve === Curves.Sm2p256v1
? sm2p.verify(sig, hash, this.publicKey)
: secp.verify(sig, hash, this.publicKey);
}
wipePrivateData() {
this.privKey = undefined;
if (this.privKeyBytes) {
this.privKeyBytes.fill(0);
this.privKeyBytes = undefined;
}
return this;
}
toJSON() {
return {
xpriv: this.privateExtendedKey,
xpub: this.publicExtendedKey
};
}
serialize(version, key) {
if (!this.chainCode) {
throw new Error("No chainCode set");
}
abytes(key, 33);
// version(4) || depth(1) || fingerprint(4) || index(4) || chain(32) || key(33)
return concatBytes(toU32(version), new Uint8Array([this.depth]), toU32(this.parentFingerprint), toU32(this.index), this.chainCode, key);
}
}
//# sourceMappingURL=hd-key.js.map