Skip to content

Commit

Permalink
feat(crypto): added BIP-32 key management and derivation to crypto pa…
Browse files Browse the repository at this point in the history
…ckage
  • Loading branch information
AngelCastilloB committed Feb 7, 2023
1 parent 59600e6 commit 1d09ee9
Show file tree
Hide file tree
Showing 7 changed files with 405 additions and 10 deletions.
21 changes: 11 additions & 10 deletions packages/crypto/src/Bip32/Bip32KeyDerivation.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { BN } from 'bn.js';
import { InvalidArgumentError } from '../errors';
import { crypto_auth_hmacsha512, crypto_scalarmult_ed25519_base_noclamp } from 'libsodium-wrappers-sumo';
import { eddsa } from 'elliptic';

const ed25519 = new eddsa('ed25519');
import { InvalidArgumentError } from '@cardano-sdk/util';
import {
crypto_auth_hmacsha512,
crypto_core_ed25519_add,
crypto_scalarmult_ed25519_base_noclamp
} from 'libsodium-wrappers-sumo';

/**
* Check if the index is hardened.
Expand Down Expand Up @@ -78,8 +79,9 @@ const truc28Mul8 = (lhs: Uint8Array, rhs: Uint8Array): Buffer =>
* @param sk The secret key.
*/
const pointOfTrunc28Mul8 = (sk: Uint8Array) => {
const left = new BN(sk.slice(0, 28), 16, 'le').mul(new BN(8));
return ed25519.curve.g.mul(left);
const left = new BN(sk.slice(0, 28), 16, 'le').mul(new BN(8)).toArrayLike(Buffer, 'le', 32);

return crypto_scalarmult_ed25519_base_noclamp(left);
};

/**
Expand Down Expand Up @@ -165,11 +167,10 @@ export const derivePublic = (key: Buffer, index: number): Buffer => {
const c = crypto_auth_hmacsha512(data, cc);

const chainCode = c.slice(32, 64);

const zl = z.slice(0, 32);

const p = pointOfTrunc28Mul8(zl);
const pp = ed25519.decodePoint(pk.toString('hex'));
const point = pp.add(p);

return Buffer.concat([Buffer.from(ed25519.encodePoint(point)), chainCode]);
return Buffer.concat([crypto_core_ed25519_add(p, pk), chainCode]);
};
183 changes: 183 additions & 0 deletions packages/crypto/src/Bip32/Bip32PrivateKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/* eslint-disable no-bitwise */
import * as Bip32KeyDerivation from './Bip32KeyDerivation';
import { Bip32PrivateKeyHex } from '../hexTypes';
import { Bip32PublicKey } from './Bip32PublicKey';
import { EXTENDED_ED25519_PRIVATE_KEY_LENGTH, Ed25519PrivateKey, NORMAL_ED25519_PRIVATE_KEY_LENGTH } from '../Ed25519e';
import { InvalidArgumentError } from '@cardano-sdk/util';
import { crypto_scalarmult_ed25519_base_noclamp, ready } from 'libsodium-wrappers-sumo';
import { pbkdf2 } from 'pbkdf2';

const SCALAR_INDEX = 0;
const SCALAR_SIZE = 32;
const PBKDF2_ITERATIONS = 4096;
const PBKDF2_KEY_SIZE = 96;
const PBKDF2_DIGEST_ALGORITHM = 'sha512';
const CHAIN_CODE_INDEX = 64;
const CHAIN_CODE_SIZE = 32;

/**
* clamp the scalar by:
*
* 1. clearing the 3 lower bits.
* 2. clearing the three highest bits.
* 3. setting the second-highest bit.
*
* @param scalar The clamped scalar.
*/
const clampScalar = (scalar: Buffer): Buffer => {
scalar[0] &= 0b1111_1000;
scalar[31] &= 0b0001_1111;
scalar[31] |= 0b0100_0000;
return scalar;
};

/**
* Extract the scalar part (first 32 bytes) from the extended key.
*
* @param extendedKey The extended key.
* @returns the scalar part of the extended key.
*/
const extendedScalar = (extendedKey: Uint8Array) => extendedKey.slice(SCALAR_INDEX, SCALAR_SIZE);

export const BIP32_ED25519_PRIVATE_KEY_LENGTH = 96;

/**
* Bip32PrivateKey private key. This type of key have the ability to derive additional keys from them
* following the BIP-32 derivation scheme variant called BIP32-Ed25519.
*
* @see <a href="https://input-output-hk.github.io/adrestia/static/Ed25519_BIP.pdf">
* BIP32-Ed25519: Hierarchical Deterministic Keys over a Non-linear Keyspace
* </a>
*/
export class Bip32PrivateKey {
readonly #key: Uint8Array;

/**
* Initializes a new instance of the Bip32PrivateKey class.
*
* @param key The BIP-32 private key.
*/
constructor(key: Uint8Array) {
this.#key = key;
}

/**
* Turns an initial entropy into a secure cryptographic master key.
*
* To generate a BIP32PrivateKey from a BIP39 recovery phrase it must be first converted to entropy following
* the <a href="https://en.bitcoin.it/wiki/BIP_0039">BIP39 protocol</a>.
*
* The resulting extended Ed25519 secret key composed of:
* - 32 bytes: Ed25519 curve scalar from which few bits have been tweaked according to ED25519-BIP32
* - 32 bytes: Ed25519 binary blob used as IV for signing
*
* @param entropy Random stream of bytes generated from a BIP39 seed phrase.
* @param password The second factor authentication password for the mnemonic phrase.
* @returns The secret extended key.
*/
static fromBip39Entropy(entropy: Buffer, password: string): Promise<Bip32PrivateKey> {
return new Promise((resolve, reject) => {
pbkdf2(password, entropy, PBKDF2_ITERATIONS, PBKDF2_KEY_SIZE, PBKDF2_DIGEST_ALGORITHM, (err, xprv) => {
if (err) {
reject(err);
}

xprv = clampScalar(xprv);
resolve(Bip32PrivateKey.fromBytes(xprv));
});
});
}

/**
* Initializes a new Bip32PrivateKey provided as a byte array.
*
* @param key The BIP-32 private key.
*/
static fromBytes(key: Uint8Array) {
if (key.length !== BIP32_ED25519_PRIVATE_KEY_LENGTH)
throw new InvalidArgumentError(
'key',
`Key should be ${NORMAL_ED25519_PRIVATE_KEY_LENGTH} bytes; however ${key.length} bytes were provided.`
);
return new Bip32PrivateKey(key);
}

/**
* Initializes a new instance of the Bip32PrivateKey class from its key material provided as a hex string.
*
* @param key The key as a hex string.
*/
static fromHex(key: Bip32PrivateKeyHex) {
return Bip32PrivateKey.fromBytes(Buffer.from(key, 'hex'));
}

/**
* Given a set of indices, this function computes the corresponding child extended key.
*
* # Security considerations
*
* hard derivation index cannot be soft derived with the public key.
*
* # Hard derivation vs Soft derivation
*
* If you pass an index below 0x80000000 then it is a soft derivation.
* The advantage of soft derivation is that it is possible to derive the
* public key too. I.e. derivation the private key with a soft derivation
* index and then retrieving the associated public key is equivalent to
* deriving the public key associated to the parent private key.
*
* Hard derivation index does not allow public key derivation.
*
* This is why deriving the private key should not fail while deriving
* the public key may fail (if the derivation index is invalid).
*
* @param derivationIndices The derivation indices.
* @returns The child BIP-32 key.
*/
async derive(derivationIndices: number[]): Promise<Bip32PrivateKey> {
await ready;
let key = Buffer.from(this.#key);

for (const index of derivationIndices) {
key = Bip32KeyDerivation.derivePrivate(key, index);
}

return Bip32PrivateKey.fromBytes(key);
}

/**
* Gets the Ed25519 raw private key. This key can be used for cryptographically signing messages.
*/
toRawKey(): Ed25519PrivateKey {
return Ed25519PrivateKey.fromExtendedBytes(this.#key.slice(0, EXTENDED_ED25519_PRIVATE_KEY_LENGTH));
}

/**
* Computes the BIP-32 public key from this BIP-32 private key.
*
* @returns the public key.
*/
async toPublic(): Promise<Bip32PublicKey> {
await ready;
const scalar = extendedScalar(this.#key.slice(0, EXTENDED_ED25519_PRIVATE_KEY_LENGTH));
const publicKey = crypto_scalarmult_ed25519_base_noclamp(scalar);

return Bip32PublicKey.fromBytes(
Buffer.concat([publicKey, this.#key.slice(CHAIN_CODE_INDEX, CHAIN_CODE_INDEX + CHAIN_CODE_SIZE)])
);
}

/**
* Gets the BIP-32 private key as a byte array.
*/
bytes(): Uint8Array {
return this.#key;
}

/**
* Gets the BIP-32 private key as a hex string.
*/
hex(): Bip32PrivateKeyHex {
return Bip32PrivateKeyHex(Buffer.from(this.#key).toString('hex'));
}
}
85 changes: 85 additions & 0 deletions packages/crypto/src/Bip32/Bip32PublicKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import * as Bip32KeyDerivation from './Bip32KeyDerivation';
import { Bip32PublicKeyHex } from '../hexTypes';
import { ED25519_PUBLIC_KEY_LENGTH, Ed25519PublicKey } from '../Ed25519e';
import { InvalidArgumentError } from '@cardano-sdk/util';
import { ready } from 'libsodium-wrappers-sumo';

export const BIP32_ED25519_PUBLIC_KEY_LENGTH = 64;

/**
* BIP32 public key.
*/
export class Bip32PublicKey {
readonly #key: Uint8Array;

/**
* Initializes a new instance of the Bip32PublicKey class.
*
* @param key The BIP32 public key.
*/
private constructor(key: Uint8Array) {
this.#key = key;
}

/**
* Initializes a new Bip32PublicKey provided as a byte array.
*
* @param key The BIP32 public key.
*/
static fromBytes(key: Uint8Array): Bip32PublicKey {
if (key.length !== BIP32_ED25519_PUBLIC_KEY_LENGTH)
throw new InvalidArgumentError(
'key',
`Key should be ${BIP32_ED25519_PUBLIC_KEY_LENGTH} bytes; however ${key.length} bytes were provided.`
);
return new Bip32PublicKey(key);
}

/**
* Initializes a new instance of the Bip32PublicKey class from its key material provided as a hex string.
*
* @param key The key as a hex string.
*/
static fromHex(key: Bip32PublicKeyHex): Bip32PublicKey {
return Bip32PublicKey.fromBytes(Buffer.from(key, 'hex'));
}

/**
* Gets the Ed25519 raw public key. This key can be used for cryptographically verifying messages
* previously signed with the matching Ed25519 raw private key.
*/
toRawKey(): Ed25519PublicKey {
return Ed25519PublicKey.fromBytes(this.#key.slice(0, ED25519_PUBLIC_KEY_LENGTH));
}

/**
* Given a set of indices, this function computes the corresponding child extended key.
*
* @param derivationIndices The list of derivation indices.
* @returns The child extended private key.
*/
async derive(derivationIndices: number[]): Promise<Bip32PublicKey> {
await ready;
let key = Buffer.from(this.#key);

for (const index of derivationIndices) {
key = Bip32KeyDerivation.derivePublic(key, index);
}

return Bip32PublicKey.fromBytes(key);
}

/**
* Gets the Bip32PublicKey as a byte array.
*/
bytes(): Uint8Array {
return this.#key;
}

/**
* Gets the Bip32PublicKey as a hex string.
*/
hex(): Bip32PublicKeyHex {
return Bip32PublicKeyHex(Buffer.from(this.#key).toString('hex'));
}
}
2 changes: 2 additions & 0 deletions packages/crypto/src/Bip32/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './Bip32PrivateKey';
export * from './Bip32PublicKey';
2 changes: 2 additions & 0 deletions packages/crypto/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export * from './Bip32';
export * from './Bip32Ed25519';
export * from './Ed25519e';
export * from './strategies';
export * from './hexTypes';
export * from './types';
72 changes: 72 additions & 0 deletions packages/crypto/test/bip32/Bip32PrivateKey.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import * as Crypto from '../../src';
import { InvalidStringError } from '@cardano-sdk/util';
import { bip32TestVectorMessageOneLength, extendedVectors } from '../ed25519e/Ed25519TestVectors';

describe('Bip32PrivateKey', () => {
it('can create an instance from a valid normal BIP-32 private key hex representation', async () => {
const privateKey = Crypto.Bip32PrivateKey.fromHex(
Crypto.Bip32PrivateKeyHex(bip32TestVectorMessageOneLength.rootKey)
);
expect(privateKey.hex()).toBe(bip32TestVectorMessageOneLength.rootKey);
});

it('can create an instance from a valid normal BIP-32 private key raw binary representation', () => {
const bytes = Buffer.from(bip32TestVectorMessageOneLength.rootKey, 'hex');
const privateKey = Crypto.Bip32PrivateKey.fromBytes(bytes);

expect(privateKey.bytes()).toBe(bytes);
});

it('throws if a BIP-32 private key of invalid size is given.', () => {
expect(() => Crypto.Bip32PrivateKey.fromHex(Crypto.Bip32PrivateKeyHex('1f'))).toThrow(InvalidStringError);
expect(() =>
Crypto.Bip32PrivateKey.fromHex(Crypto.Bip32PrivateKeyHex(`${bip32TestVectorMessageOneLength.rootKey}1f2f3f`))
).toThrow(InvalidStringError);
});

it('can create the correct BIP-32 key given the right bip39 entropy and password.', async () => {
expect.assertions(extendedVectors.length);

for (const vector of extendedVectors) {
const bip32Key = await Crypto.Bip32PrivateKey.fromBip39Entropy(
Buffer.from(vector.bip39Entropy, 'hex'),
vector.password
);

expect(bip32Key.hex()).toBe(vector.rootKey);
}
});

it('can derive the correct child BIP-32 private key given a derivation path.', async () => {
expect.assertions(extendedVectors.length);

for (const vector of extendedVectors) {
const rootKey = await Crypto.Bip32PrivateKey.fromHex(Crypto.Bip32PrivateKeyHex(vector.rootKey));
const childKey = await rootKey.derive(vector.derivationPath);

expect(childKey.hex()).toBe(vector.childPrivateKey);
}
});

it('can compute the matching BIP-32 public key.', async () => {
expect.assertions(extendedVectors.length);

for (const vector of extendedVectors) {
const rootKey = await Crypto.Bip32PrivateKey.fromHex(Crypto.Bip32PrivateKeyHex(vector.rootKey));
const publicKey = await rootKey.toPublic();

expect(publicKey.hex()).toBe(vector.publicKey);
}
});

it('can compute the correct ED25519e raw private key.', async () => {
expect.assertions(extendedVectors.length);

for (const vector of extendedVectors) {
const rootKey = await Crypto.Bip32PrivateKey.fromHex(Crypto.Bip32PrivateKeyHex(vector.rootKey));
const rawKey = await rootKey.toRawKey();

expect(rawKey.hex()).toBe(vector.ed25519eVector.secretKey);
}
});
});
Loading

0 comments on commit 1d09ee9

Please sign in to comment.