mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2025-11-20 03:35:51 +00:00
Merge pull request #1507 from AlexanderBuzz/DGE-94_javascript-code-samples
Added key-derivation JavaScript examples analog to python version
This commit is contained in:
24
content/_code-samples/key-derivation/js/README.md
Normal file
24
content/_code-samples/key-derivation/js/README.md
Normal file
@@ -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
|
||||||
198
content/_code-samples/key-derivation/js/index.js
Normal file
198
content/_code-samples/key-derivation/js/index.js
Normal file
@@ -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())
|
||||||
|
}
|
||||||
12
content/_code-samples/key-derivation/js/package.json
Normal file
12
content/_code-samples/key-derivation/js/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user