Files
xrpl-dev-portal/content/_code-samples/key-derivation/js/index.js
2022-10-05 13:01:37 +02:00

198 lines
7.1 KiB
JavaScript

'use strict';
// Organize imports
const assert = require("assert")
const brorand = require("brorand")
const BN = require("bn.js")
const elliptic = require("elliptic");
const Ed25519 = elliptic.eddsa('ed25519');
const Secp256k1 = elliptic.ec('secp256k1');
const hashjs = require("hash.js");
const Sha512 = require("ripple-keypairs/dist/Sha512")
const { codec, encodeAccountPublic, encodeNodePublic } = require("ripple-address-codec");
const XRPL_SEED_PREFIX = 0x21
const isHex = function(value) {
const regex = new RegExp(/^[0-9a-f]+$/, 'i')
return regex.test(value)
}
const bytesToHex = function(bytes) {
return Array.from(bytes, (byteValue) => {
const hex = byteValue.toString(16).toUpperCase();
return hex.length > 1 ? hex : `0${hex}`;
}).join('');
}
const hexToBytes = function(hex) {
assert.ok(hex.length % 2 === 0);
return hex.length === 0 ? [] : new BN(hex, 16).toArray(null, hex.length / 2);
}
const encodeUTF8 = function (string) {
return encodeURIComponent(string)
}
const sha512half = function (value) {
return hashjs.sha512().update(value).digest().slice(0, 32);
}
class Seed {
constructor(seedValue = '', checkRFC1751 = false) {
this.bytes = this._initSeed(seedValue)
this.ED25519Keypair = this._deriveED25519Keypair(this.bytes)
this.Secp256K1Keypair = this._deriveSecp256K1Keypair(this.bytes)
}
_initSeed(seedValue) {
// No input given - default to random seed
if (!seedValue || seedValue === '') {
const randomBuffer = brorand(16)
return [...randomBuffer]
}
// From base58 formatted seed
try {
const base58decoded = codec.decodeChecked(seedValue)
if (base58decoded[0] === XRPL_SEED_PREFIX && base58decoded.length === 17) {
return [...base58decoded.slice(1)];
}
} catch (exception) {
// Continue probing the seed for different format
}
// From hex formatted seed
if (isHex(seedValue)) {
const decoded = hexToBytes(seedValue);
if(decoded.length === 16) {
return decoded
} else {
// Raise Error
throw new Error("incorrect decoded seed length")
}
}
// From password seed
const encoded = encodeUTF8(seedValue)
return hashjs.sha512().update(encoded).digest().slice(0, 16);
}
_deriveED25519Keypair() {
const prefix = 'ED';
const rawPrivateKey = sha512half(this.bytes);
const privateKey = prefix + bytesToHex(rawPrivateKey);
const publicKey = prefix + bytesToHex(Ed25519.keyFromSecret(rawPrivateKey).pubBytes());
return { privateKey, publicKey };
}
_deriveSecp256K1Keypair(entropy, options) {
const prefix = '00';
const privateKey = prefix + this._deriveSecp256K1PrivateKey(entropy, options).toString(16, 64).toUpperCase()
const publicKey = bytesToHex(Secp256k1.keyFromPrivate(privateKey.slice(2))
.getPublic()
.encodeCompressed());
return { privateKey, publicKey };
}
_deriveSecp256K1PrivateKey(seed, opts = {}) {
const root = opts.validator;
const order = Secp256k1.curve.n;
// This private generator represents the `root` private key, and is what's
// used by validators for signing when a keypair is generated from a seed.
const privateGen = this._deriveScalar(seed);
if (root) {
// As returned by validation_create for a given seed
return privateGen;
}
const publicGen = Secp256k1.g.mul(privateGen);
// A seed can generate many keypairs as a function of the seed and a uint32.
// Almost everyone just uses the first account, `0`.
const accountIndex = opts.accountIndex || 0;
return this._deriveScalar(publicGen.encodeCompressed(), accountIndex)
.add(privateGen)
.mod(order);
}
_deriveScalar(bytes, discrim) {
const order = Secp256k1.curve.n;
for (let i = 0; i <= 0xffffffff; i++) {
// We hash the bytes to find a 256 bit number, looping until we are sure it
// is less than the order of the curve.
const hasher = new Sha512.default().add(bytes);
// If the optional discriminator index was passed in, update the hash.
if (discrim !== undefined) {
hasher.addU32(discrim);
}
hasher.addU32(i);
const key = hasher.first256BN();
/* istanbul ignore else */
/* istanbul ignore else */
if (key.cmpn(0) > 0 && key.cmp(order) < 0) {
return key;
}
}
// This error is practically impossible to reach.
// The order of the curve describes the (finite) amount of points on the curve
// https://github.com/indutny/elliptic/blob/master/lib/elliptic/curves.js#L182
// How often will an (essentially) random number generated by Sha512 be larger than that?
// There's 2^32 chances (the for loop) to get a number smaller than the order,
// and it's rare that you'll even get past the first loop iteration.
// Note that in TypeScript we actually need the throw, otherwise the function signature would be BN | undefined
//
/* istanbul ignore next */
throw new Error('impossible unicorn ;)');
}
getBase58ED25519Account() {
const buffer = Buffer.from(this.ED25519Keypair.publicKey, "hex")
return encodeAccountPublic([...buffer])
}
getBase58ESecp256k1Account(validator = false) {
const buffer = Buffer.from(this.Secp256K1Keypair.publicKey, "hex")
if (validator) {
return encodeNodePublic([...buffer])
} else {
return encodeAccountPublic([...buffer])
}
}
toString() {
const base58Seed = codec.encodeChecked([XRPL_SEED_PREFIX].concat(this.bytes))
const hexSeed = Buffer.from(this.bytes).toString('hex').toUpperCase()
const base58ED25519Account = this.getBase58ED25519Account();
const base58Secp256k1Account = this.getBase58ESecp256k1Account();
const base58Secp256k1AccountValidator = this.getBase58ESecp256k1Account(true);
const out = `
Seed (base58): ${base58Seed}
Seed (hex): ${hexSeed}
Ed25519 Secret Key (hex): ${this.ED25519Keypair.privateKey}
Ed25519 Public Key (hex): ${this.ED25519Keypair.publicKey}
Ed25519 Public Key (base58 - Account): ${base58ED25519Account}
secp256k1 Secret Key (hex): ${this.Secp256K1Keypair.privateKey}
secp256k1 Public Key (hex): ${this.Secp256K1Keypair.publicKey}
secp256k1 Public Key (base58 - Account): ${base58Secp256k1Account}
secp256k1 Public Key (base58 - Validator): ${base58Secp256k1AccountValidator}
`
return out
}
}
if (process.argv[2] !== undefined) {
console.log(process.argv[2]);
const seed = new Seed(process.argv[2]);
console.log(seed.toString())
} else {
const seed = new Seed();
console.log(seed.toString())
}