diff --git a/content/_code-samples/key-derivation/js/README.md b/content/_code-samples/key-derivation/js/README.md new file mode 100644 index 0000000000..53a35247b0 --- /dev/null +++ b/content/_code-samples/key-derivation/js/README.md @@ -0,0 +1,24 @@ +# JavaScript key derivation examples + +Generates key from a given input in Ed25519 and Secp256k1 format. On first run, you +have to install the necessary node.js dedpendencies: + + npm install + +## Command-line usage: + +### Base58 formatted seed: + + npm start "snJj9fYixUfpNCBn9LzLdLv5QqUKZ" + +### Hex formatted seed: + + npm start "BB664A14F510A366404BC4352A2230A5" + +### Password like seed: + + npm start "sEdSKaCy2JT7JaM7v95H9SxkhP9wS2r" + +### Random seed + + npm start \ No newline at end of file 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..eb08b51fb6 --- /dev/null +++ b/content/_code-samples/key-derivation/js/index.js @@ -0,0 +1,198 @@ +'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()) +} \ No newline at end of file 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" + } +}