Skip to content

Commit

Permalink
Merge pull request #1492 from kadena-community/refactor/hd-wallet-sign
Browse files Browse the repository at this point in the history
Refactor/hd wallet sign
  • Loading branch information
javadkh2 authored Jan 18, 2024
2 parents 0a0aec6 + c383d1c commit 3f65a7d
Show file tree
Hide file tree
Showing 12 changed files with 116 additions and 95 deletions.
5 changes: 5 additions & 0 deletions .changeset/great-meals-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@kadena/hd-wallet': minor
---

Removed dependency to @kadena/client and refactor sign functions to sign hash
1 change: 0 additions & 1 deletion packages/libs/hd-wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
"test": "vitest run"
},
"dependencies": {
"@kadena/client": "workspace:*",
"@kadena/cryptography-utils": "workspace:*",
"@scure/bip39": "^1.2.1",
"debug": "~4.3.4",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { BinaryLike } from 'crypto';
import type { EncryptedString } from '../utils/kadenaEncryption';
import { kadenaDecrypt, kadenaEncrypt } from '../utils/kadenaEncryption';
import { deriveKeyPair } from './utils/sign';

function genKeypairFromSeed(
password: string,
password: BinaryLike,
seedBuffer: Uint8Array,
index: number,
derivationPathTemplate: string,
Expand All @@ -24,14 +25,14 @@ function genKeypairFromSeed(
}

export function kadenaGenKeypairFromSeed(
password: string,
password: BinaryLike,
seed: EncryptedString,
index: number,
derivationPathTemplate?: string,
): [string, EncryptedString];

export function kadenaGenKeypairFromSeed(
password: string,
password: BinaryLike,
seed: EncryptedString,
indexRange: [number, number],
derivationPathTemplate?: string,
Expand All @@ -48,7 +49,7 @@ export function kadenaGenKeypairFromSeed(
* @throws {Error} Throws an error if the seed buffer is not provided, if the indices are invalid, or if encryption fails.
*/
export function kadenaGenKeypairFromSeed(
password: string,
password: BinaryLike,
seed: EncryptedString,
indexOrRange: number | [number, number],
derivationPathTemplate: string = `m'/44'/626'/<index>'`,
Expand Down
14 changes: 7 additions & 7 deletions packages/libs/hd-wallet/src/SLIP10/kadenaGetPublic.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { EncryptedString } from '../utils/kadenaEncryption';
import type { BinaryLike } from 'crypto';
import { kadenaDecrypt } from '../utils/kadenaEncryption';
import { deriveKeyPair } from './utils/sign';

Expand All @@ -18,15 +18,15 @@ function genPublicKeyFromSeed(
}

export function kadenaGetPublic(
password: string,
seed: EncryptedString,
password: BinaryLike,
seed: BinaryLike,
index: number,
derivationPathTemplate?: string,
): string;

export function kadenaGetPublic(
password: string,
seed: EncryptedString,
password: BinaryLike,
seed: BinaryLike,
indexRange: [number, number],
derivationPathTemplate?: string,
): string[];
Expand All @@ -42,8 +42,8 @@ export function kadenaGetPublic(
* @throws {Error} Throws an error if the seed buffer is not provided, if the indices are invalid, or if encryption fails.
*/
export function kadenaGetPublic(
password: string,
seed: EncryptedString,
password: BinaryLike,
seed: BinaryLike,
indexOrRange: number | [number, number],
derivationPathTemplate: string = `m'/44'/626'/<index>'`,
): string | string[] {
Expand Down
13 changes: 8 additions & 5 deletions packages/libs/hd-wallet/src/SLIP10/kadenaMnemonic.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as bip39 from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import type { EncryptedString } from '../utils/kadenaEncryption';
import type { BinaryLike } from 'crypto';
import { kadenaEncrypt } from '../utils/kadenaEncryption';
/**
* Generates a mnemonic phrase using the BIP39 protocol with a specified wordlist.
Expand All @@ -20,16 +20,19 @@ export function kadenaGenMnemonic(): string {
* @throws {Error} Throws an error if the provided mnemonic is not valid.
* @returns {Promise<{ seedBuffer: Uint8Array, seed: string }>} - Returns the seed buffer and processed seed.
*/
export async function kadenaMnemonicToSeed(
password: string,
export async function kadenaMnemonicToSeed<
TEncode extends 'base64' | 'buffer' = 'base64',
>(
password: BinaryLike,
mnemonic: string,
encode: TEncode = 'base64' as TEncode,
// wordList: string[] = wordlist,
): Promise<EncryptedString> {
) {
if (bip39.validateMnemonic(mnemonic, wordlist) === false) {
throw Error('Invalid mnemonic.');
}

const seedBuffer = await bip39.mnemonicToSeed(mnemonic);

return kadenaEncrypt(password, seedBuffer);
return kadenaEncrypt(password, seedBuffer, encode);
}
53 changes: 41 additions & 12 deletions packages/libs/hd-wallet/src/SLIP10/kadenaSign.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { IUnsignedCommand } from '@kadena/client';
import { verifySig } from '@kadena/cryptography-utils';
import type { BinaryLike } from 'crypto';
import type { EncryptedString } from '../utils/kadenaEncryption';
import { kadenaDecrypt } from '../utils/kadenaEncryption';
import type { ISignatureWithPublicKey } from './utils/sign';
import { signWithKeyPair, signWithSeed } from './utils/sign';

/**
Expand All @@ -12,16 +13,30 @@ import { signWithKeyPair, signWithSeed } from './utils/sign';
* @returns {Function} A function that takes an unsigned command (`IUnsignedCommand`) and returns an object with an array of signatures.
*/
export function kadenaSignWithKeyPair(
password: string,
password: BinaryLike,
publicKey: string,
encryptedPrivateKey: EncryptedString,
): (tx: IUnsignedCommand) => { sigs: { sig: string }[] } {
): (hash: string) => ISignatureWithPublicKey {
return signWithKeyPair(
publicKey,
Buffer.from(kadenaDecrypt(password, encryptedPrivateKey)).toString('hex'),
);
}

export function kadenaSignWithSeed(
password: BinaryLike,
seed: BinaryLike,
index: number,
derivationPathTemplate?: string,
): (hash: string) => ISignatureWithPublicKey;

export function kadenaSignWithSeed(
password: BinaryLike,
seed: BinaryLike,
index: number[],
derivationPathTemplate?: string,
): (hash: string) => ISignatureWithPublicKey[];

/**
* Signs a Kadena transaction with a seed and index.
*
Expand All @@ -30,15 +45,26 @@ export function kadenaSignWithKeyPair(
* @returns {Function} A function that takes an unsigned command (`IUnsignedCommand`) and returns an object with an array of signatures.
*/
export function kadenaSignWithSeed(
password: string,
seed: EncryptedString,
index: number,
password: BinaryLike,
seed: BinaryLike,
index: number | number[],
derivationPathTemplate: string = `m'/44'/626'/<index>'`,
): (tx: IUnsignedCommand) => { sigs: { sig: string }[] } {
return signWithSeed(
kadenaDecrypt(password, seed),
derivationPathTemplate.replace('<index>', index.toString()),
): (hash: string) => ISignatureWithPublicKey | ISignatureWithPublicKey[] {
const decryptedSeed = kadenaDecrypt(password, seed);
if (typeof index === 'number') {
return signWithSeed(
decryptedSeed,
derivationPathTemplate.replace('<index>', index.toString()),
);
}
const signers = index.map((i) =>
signWithSeed(
decryptedSeed,
derivationPathTemplate.replace('<index>', i.toString()),
),
);

return (hash: string) => signers.map((signer) => signer(hash));
}

/**
Expand All @@ -50,12 +76,15 @@ export function kadenaSignWithSeed(
* @returns {boolean} - Returns true if verification succeeded or false if it failed.
*/
export function kadenaVerify(
message: string,
message: BinaryLike,
publicKey: string,
signature: string,
): boolean {
// Convert the message, public key, and signature from hex string to Uint8Array
const msgUint8Array = Uint8Array.from(Buffer.from(message, 'hex'));
const msgUint8Array =
typeof message === 'string'
? Uint8Array.from(Buffer.from(message, 'hex'))
: new Uint8Array(message.buffer);
const publicKeyUint8Array = Uint8Array.from(Buffer.from(publicKey, 'hex'));
const signatureUint8Array = Uint8Array.from(Buffer.from(signature, 'hex'));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,19 @@ import {
kadenaSignWithKeyPair,
} from '..';

import type { IUnsignedCommand } from '@kadena/client';

describe('kadenaSignWithKeyPair', async () => {
const password = 'password';
const mnemonic = kadenaGenMnemonic();
const seed = await kadenaMnemonicToSeed(password, mnemonic);

const [publicKey, privateKey] = kadenaGenKeypairFromSeed(password, seed, 0);

const mockUnsignedCommand: IUnsignedCommand = {
cmd: '{"command":"value"}',
hash: 'kadena-hash',
sigs: [],
};
const txHash: string = 'tx-hash';

it('should sign a transaction with a public and private key ans password', () => {
const signer = kadenaSignWithKeyPair(password, publicKey, privateKey);

const signedTx = signer(mockUnsignedCommand);

expect(signedTx).toHaveProperty('sigs');
expect(signedTx.sigs).toBeInstanceOf(Array);
expect(signedTx.sigs.length).toBeGreaterThan(0);
expect(signedTx.sigs[0]).toHaveProperty('sig');
expect(signedTx.sigs[0].sig).toBeTruthy();
const signature = signer(txHash);
expect(signature).toBeTruthy();
expect(signature.sig.length > 0).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,17 @@ import {
kadenaSignWithSeed,
} from '..';

import type { IUnsignedCommand } from '@kadena/client';

describe('kadenaSignWithSeed', async () => {
const password = 'password';
const mnemonic = kadenaGenMnemonic();
const seed = await kadenaMnemonicToSeed(password, mnemonic);
const index = 0;
const mockUnsignedCommand: IUnsignedCommand = {
cmd: '{"commands":"value"}',
hash: 'kadena-hash',
sigs: [],
};
const hash = 'transaction-hash';

it('should sign a transaction with a seed and index', () => {
const signer = kadenaSignWithSeed(password, seed, index);
const signedTx = signer(mockUnsignedCommand);
expect(signedTx).toHaveProperty('sigs');
expect(signedTx.sigs).toBeInstanceOf(Array);
expect(signedTx.sigs[0]).toHaveProperty('sig');
expect(signedTx.sigs[0].sig).toBeTruthy();
const signature = signer(hash);
expect(signature).toBeTruthy();
expect(signature.sig.length > 0).toBeTruthy();
});
});
25 changes: 11 additions & 14 deletions packages/libs/hd-wallet/src/SLIP10/utils/sign.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import type { IUnsignedCommand } from '@kadena/client';
import { sign } from '@kadena/cryptography-utils';
import { signHash } from '@kadena/cryptography-utils';

import { HDKey } from 'ed25519-keygen/hdkey';
import { uint8ArrayToHex } from '../../utils/buffer-helpers';

export interface ISignatureWithPublicKey {
sig: string;
pubKey: string;
}

/**
* Derive a key pair using a seed and an index.
* @param {Uint8Array} seed - The seed for key derivation.
Expand Down Expand Up @@ -36,21 +40,14 @@ export const deriveKeyPair = (
*
* @throws {Error} Throws an error if the signature is undefined.
*/
export const signWithKeyPair = (
publicKey: string,
secretKey: string,
): ((tx: IUnsignedCommand) => { sigs: { sig: string }[] }) => {
return (tx: IUnsignedCommand) => {
const { sig } = sign(tx.cmd, { publicKey, secretKey });
export const signWithKeyPair =
(publicKey: string, secretKey: string) => (hash: string) => {
const { sig } = signHash(hash, { publicKey, secretKey });
if (sig === undefined) {
throw new Error('Signature is undefined');
}
return {
...tx,
sigs: [{ sig }],
};
return { sig, pubKey: publicKey };
};
};

/**
* Generate a signer function using a seed and an index.
Expand All @@ -61,7 +58,7 @@ export const signWithKeyPair = (
export const signWithSeed = (
seed: Uint8Array,
derivationPath: string,
): ((tx: IUnsignedCommand) => { sigs: { sig: string }[] }) => {
): ((hash: string) => ISignatureWithPublicKey) => {
const { publicKey, privateKey } = deriveKeyPair(seed, derivationPath);
return signWithKeyPair(publicKey, privateKey);
};
6 changes: 3 additions & 3 deletions packages/libs/hd-wallet/src/utils/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type BinaryLike = string | NodeJS.ArrayBufferView;
* @param {string} password - User's password.
* @returns {Buffer} - Returns the derived cryptographic key.
*/
function deriveKey(password: string, salt: BinaryLike): Buffer {
function deriveKey(password: BinaryLike, salt: BinaryLike): Buffer {
return pbkdf2Sync(password, salt, 1000, 32, 'sha256');
}

Expand All @@ -24,7 +24,7 @@ function deriveKey(password: string, salt: BinaryLike): Buffer {
*/
export function encrypt(
text: Buffer,
password: string,
password: BinaryLike,
salt: BinaryLike,
): { cipherText: Buffer; iv: Buffer; tag: Buffer } {
const key = deriveKey(password, salt);
Expand Down Expand Up @@ -52,7 +52,7 @@ export function decrypt(
iv: Buffer;
tag: Buffer;
},
password: string,
password: BinaryLike,
salt: BinaryLike,
): Buffer | undefined {
const key = deriveKey(password, salt);
Expand Down
Loading

0 comments on commit 3f65a7d

Please sign in to comment.