Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(crypto)!: libsodium crypto implementation #574

Merged
merged 4 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/continuous-integration-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ env:
TL_LEVEL: ${{ github.event.pull_request.head.repo.fork && 'info' || vars.TL_LEVEL }}
# -----------------------------------------------------------------------------------------
KEY_MANAGEMENT_PROVIDER: 'inMemory'
KEY_MANAGEMENT_PARAMS: '{"bip32Ed25519": "CML", "accountIndex": 0, "chainId":{"networkId": 0, "networkMagic": 888}, "passphrase":"some_passphrase","mnemonic":"vacant violin soft weird deliver render brief always monitor general maid smart jelly core drastic erode echo there clump dizzy card filter option defense"}'
KEY_MANAGEMENT_PARAMS: '{"bip32Ed25519": "Sodium", "accountIndex": 0, "chainId":{"networkId": 0, "networkMagic": 888}, "passphrase":"some_passphrase","mnemonic":"vacant violin soft weird deliver render brief always monitor general maid smart jelly core drastic erode echo there clump dizzy card filter option defense"}'
ASSET_PROVIDER: 'http'
ASSET_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:4000/"}'
CHAIN_HISTORY_PROVIDER: 'http'
Expand Down
9 changes: 9 additions & 0 deletions packages/crypto/AUDIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Security audit

The security audit of the libsodium crypto provider is still ongoing, list of current open topics:

Form PR: https://github.com/input-output-hk/cardano-js-sdk/pull/574

- https://github.com/input-output-hk/cardano-js-sdk/pull/574#discussion_r1086409503
- https://github.com/input-output-hk/cardano-js-sdk/pull/574#discussion_r1085173470
- https://github.com/input-output-hk/cardano-js-sdk/pull/574#discussion_r1085128396
3 changes: 3 additions & 0 deletions packages/crypto/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@

This package provides a set of high level primitives to perform hashing, signature generation/verification, import private keys from BIP39
mnemonics, and derive BIP32-Ed25519 extended signing keys.

> **Warning**
> The libsodium crypto provider has not yet been audited. Use at this stage is at own risk
2 changes: 1 addition & 1 deletion packages/crypto/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@cardano-sdk/crypto",
"version": "0.1.12",
"description": "Cryptographic types and functions for Cardano",
"description": "Cryptographic types and functions for Cardano. Warning: The libsodium crypto provider has not yet been audited. Use at this stage is at own risk",
"engines": {
"node": ">=16.20.1"
},
Expand Down
176 changes: 176 additions & 0 deletions packages/crypto/src/Bip32/Bip32KeyDerivation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { BN } from 'bn.js';
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.
*
* @param index The index to verify.
* @returns true if hardened; otherwise; false.
*/
const isHardenedDerivation = (index: number) => index >= 0x80_00_00_00;

/**
* Derives the private key with a hardened index.
*
* @param index The derivation index.
* @param scalar Ed25519 curve scalar.
* @param iv Ed25519 binary blob used as IV for signing.
* @param chainCode The chain code.
*/
const deriveHardened = (
index: number,
scalar: Buffer,
iv: Buffer,
chainCode: Buffer
): { zMac: Uint8Array; ccMac: Uint8Array } => {
const data = Buffer.allocUnsafe(1 + 64 + 4);
data.writeUInt32LE(index, 1 + 64);
scalar.copy(data, 1);
iv.copy(data, 1 + 32);

data[0] = 0x00;
const zMac = crypto_auth_hmacsha512(data, chainCode);
data[0] = 0x01;
const ccMac = crypto_auth_hmacsha512(data, chainCode);

return { ccMac, zMac };
};

/**
* Derives the private key with a 'soft' index.
*
* @param index The derivation index.
* @param scalar Ed25519 curve scalar.
* @param chainCode The chain code.
*/
const deriveSoft = (index: number, scalar: Buffer, chainCode: Buffer): { zMac: Uint8Array; ccMac: Uint8Array } => {
const data = Buffer.allocUnsafe(1 + 32 + 4);
data.writeUInt32LE(index, 1 + 32);

const vk = Buffer.from(crypto_scalarmult_ed25519_base_noclamp(scalar));
rhyslbw marked this conversation as resolved.
Show resolved Hide resolved

vk.copy(data, 1);

data[0] = 0x02;
const zMac = crypto_auth_hmacsha512(data, chainCode);
data[0] = 0x03;
const ccMac = crypto_auth_hmacsha512(data, chainCode);

return { ccMac, zMac };
};

/**
* Adds the left hand side to 28 bytes of the right hand side and multiplies the result by 8.
*
* @param lhs Left hand side Little-Endian big number.
* @param rhs Right hand side Little-Endian big number.
*/
const truc28Mul8 = (lhs: Uint8Array, rhs: Uint8Array): Buffer =>
new BN(lhs, 16, 'le').add(new BN(rhs.slice(0, 28), 16, 'le').mul(new BN(8))).toArrayLike(Buffer, 'le', 32);

/**
* Computes `(8 * sk[:28])*G` where `sk` is a little-endian encoded int and `G` is the curve's base point.
*
* @param sk The secret key.
*/
const pointOfTrunc28Mul8 = (sk: Uint8Array) => {
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);
};

/**
* Adds the left hand side to the right hand side.
*
* @param lhs Left hand side Little-Endian big number.
* @param rhs Right hand side Little-Endian big number.
*/
const add = (lhs: Uint8Array, rhs: Uint8Array): Buffer => {
let r = new BN(lhs, 16, 'le').add(new BN(rhs, 16, 'le')).toArrayLike(Buffer, 'le').subarray(0, 32);

if (r.length !== 32) {
r = Buffer.from(r.toString('hex').padEnd(32, '0'), 'hex');
}
rhyslbw marked this conversation as resolved.
Show resolved Hide resolved

return r;
};

/**
* Derive the given private key with the given index.
*
* # 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 key The parent key to be derived.
* @param index The derivation index.
* @returns The child BIP32 key.
*/
export const derivePrivate = (key: Buffer, index: number): Buffer => {
const kl = key.subarray(0, 32);
const kr = key.subarray(32, 64);
const cc = key.subarray(64, 96);

const { ccMac, zMac } = isHardenedDerivation(index) ? deriveHardened(index, kl, kr, cc) : deriveSoft(index, kl, cc);

const chainCode = ccMac.slice(32, 64);
const zl = zMac.slice(0, 32);
const zr = zMac.slice(32, 64);

const left = truc28Mul8(kl, zl);
const right = add(kr, zr);

return Buffer.concat([left, right, chainCode]);
};

/**
* Derive the given public key with the given index.
*
* Public key derivation is only possible with non-hardened indices.
*
* @param key The parent key to be derived.
* @param index The derivation index.
* @returns The child BIP32 key.
*/
export const derivePublic = (key: Buffer, index: number): Buffer => {
const pk = key.subarray(0, 32);
const cc = key.subarray(32, 64);

const data = Buffer.allocUnsafe(1 + 32 + 4);
data.writeUInt32LE(index, 1 + 32);

if (isHardenedDerivation(index))
throw new InvalidArgumentError('index', 'Public key can not be derived from a hardened index.');

pk.copy(data, 1);
data[0] = 0x02;
const z = crypto_auth_hmacsha512(data, cc);
data[0] = 0x03;
const c = crypto_auth_hmacsha512(data, cc);

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

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

const p = pointOfTrunc28Mul8(zl);

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;
rhyslbw marked this conversation as resolved.
Show resolved Hide resolved
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 {
mkazlauskas marked this conversation as resolved.
Show resolved Hide resolved
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'));
}
}
Loading