From e6deb4ca633fcd16073ceb390611ad85235b73fb Mon Sep 17 00:00:00 2001 From: AlexanderBusse Date: Tue, 6 Sep 2022 23:48:59 +0200 Subject: [PATCH] Added key-derivation JavaScript examples analog to python version --- .../_code-samples/key-derivation/js/index.js | 210 ++++++++++++++++++ .../key-derivation/js/package.json | 12 + 2 files changed, 222 insertions(+) create mode 100644 content/_code-samples/key-derivation/js/index.js create mode 100644 content/_code-samples/key-derivation/js/package.json diff --git a/content/_code-samples/key-derivation/js/index.js b/content/_code-samples/key-derivation/js/index.js new file mode 100644 index 0000000000..237f25c8ef --- /dev/null +++ b/content/_code-samples/key-derivation/js/index.js @@ -0,0 +1,210 @@ +'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 { rfc1751MnemonicToKey, keyToRFC1751Mnemonic } = require("xrpl/dist/npm/Wallet/rfc1751"); +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(/^0x([0-9a-f]+(\.[0-9a-f]*)?|\.[0-9a-f]+)(p[+-]?\d+)?$/, '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) { + + //default to random seed + if (!seedValue || seedValue === '') { + const randomBuffer = brorand(16) + return [...randomBuffer] + } + + //from hex formatted seed + if (isHex(seedValue)) { + const decoded = hexToBytes(seedValue); + if(decoded.length === 16) { + return decoded + } else { + //Raise Error + console.log('Raise error!') + } + } + + //from rfc1751 Mnemonic + try { + const rippledMnemonic = rfc1751MnemonicToKey(seedValue) + if (rippledMnemonic.length === 16) { + return [...rippledMnemonic] + } + } catch (error) { + //try next seed mode + } + + //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 */ + 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 rfc1751Rippled = keyToRFC1751Mnemonic(bytesToHex(this.bytes)) + + const base58ED25519Account = this.getBase58ED25519Account(); + + const base58Secp256k1Account = this.getBase58ESecp256k1Account(); + const base58Secp256k1AccountValidator = this.getBase58ESecp256k1Account(true); + + const out = ` + Seed (base58): ${base58Seed} + Seed (hex): ${hexSeed} + Seed (rippled RFC-1751): ${rfc1751Rippled} + + Ed25519 Secret Key (hex): ${seed.ED25519Keypair.privateKey} + Ed25519 Public Key (hex): ${seed.ED25519Keypair.publicKey} + Ed25519 Public Key (base58 - Account): ${base58ED25519Account} + + secp256k1 Secret Key (hex): ${seed.Secp256K1Keypair.privateKey} + secp256k1 Public Key (hex): ${seed.Secp256K1Keypair.publicKey} + secp256k1 Public Key (base58 - Account): ${base58Secp256k1Account} + secp256k1 Public Key (base58 - Validator): ${base58Secp256k1AccountValidator} + ` + + return out + } +} + +//from empty seed +//const seed = new Seed(); +//console.log(seed.toString()); + +//from hex formatted seed +//const hexSeed = '0x21edc3dec3ef1873cf8f333381c5f360c3' +//const seed = new Seed(hexSeed); +//console.log(seed.toString()); + +//from rippled RFC1751 mnemonic +//const rippledMnemonic = 'KANT FISH GENE COST WEB JAKE CHUM HAD SOD MID KICK BOTH' +//const seed = new Seed(rippledMnemonic); +//console.log(seed.toString()); + +//from fixed seed +const string = "sEdSKaCy2JT7JaM7v95H9SxkhP9wS2r" +const seed = new Seed(string); +console.log(seed.toString()); diff --git a/content/_code-samples/key-derivation/js/package.json b/content/_code-samples/key-derivation/js/package.json new file mode 100644 index 0000000000..bbe16d16ea --- /dev/null +++ b/content/_code-samples/key-derivation/js/package.json @@ -0,0 +1,12 @@ +{ + "name": "key-derivation-examples", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "xrpl": "^2.0.0" + }, + "main": "index.js", + "scripts": { + "start": "node index.js" + } +}