diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index bc994ea22..fe57fa3c9 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -1,6 +1,5 @@ import { bytesToHex, randomBytes } from '@noble/hashes/utils'; import { CashuMint } from './CashuMint.js'; -import * as dhke from './DHKE.js'; import { BlindedMessage } from './model/BlindedMessage.js'; import { type AmountPreference, @@ -20,7 +19,8 @@ import { type SplitPayload, type Token, type TokenEntry, - CheckStateEnum + CheckStateEnum, + SerializedBlindedSignature } from './model/types/index.js'; import { bytesToNumber, @@ -29,13 +29,21 @@ import { getDefaultAmountPreference, splitAmount } from './utils.js'; -import { deriveBlindingFactor, deriveSecret, deriveSeedFromMnemonic } from './secrets.js'; import { validateMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; +import { hashToCurve, pointFromHex } from '@cashu/crypto/modules/common'; +import { + blindMessage, + constructProofFromPromise, + serializeProof +} from '@cashu/crypto/modules/client'; +import { + deriveBlindingFactor, + deriveSecret, + deriveSeedFromMnemonic +} from '@cashu/crypto/modules/client/NUT09'; import { createP2PKsecret, getSignedProofs } from '@cashu/crypto/modules/client/NUT11'; import { type Proof as NUT11Proof } from '@cashu/crypto/modules/common/index'; -import { serializeProof } from '@cashu/crypto/modules/client'; -import { pointFromHex } from './DHKE'; /** * Class that represents a Cashu wallet. @@ -189,7 +197,7 @@ class CashuWallet { options?.privkey ); const { signatures } = await CashuMint.split(tokenEntry.mint, payload); - const newProofs = dhke.constructProofs( + const newProofs = this.constructProofs( signatures, blindedMessages.rs, blindedMessages.secrets, @@ -260,7 +268,7 @@ class CashuWallet { options?.privkey ); const { signatures } = await this.mint.split(payload); - const proofs = dhke.constructProofs( + const proofs = this.constructProofs( signatures, blindedMessages.rs, blindedMessages.secrets, @@ -316,7 +324,7 @@ class CashuWallet { ); return { - proofs: dhke.constructProofs(promises, validRs, validSecrets, keys) + proofs: this.constructProofs(promises, validRs, validSecrets, keys) }; } @@ -387,7 +395,7 @@ class CashuWallet { }; const { signatures } = await this.mint.mint(mintPayload); return { - proofs: dhke.constructProofs(signatures, rs, secrets, keyset) + proofs: this.constructProofs(signatures, rs, secrets, keyset) }; } @@ -435,7 +443,7 @@ class CashuWallet { isPaid: meltResponse.paid ?? false, preimage: meltResponse.payment_preimage, change: meltResponse?.change - ? dhke.constructProofs(meltResponse.change, rs, secrets, keys) + ? this.constructProofs(meltResponse.change, rs, secrets, keys) : [] }; } @@ -572,7 +580,7 @@ class CashuWallet { */ async checkProofsSpent(proofs: Array): Promise> { const enc = new TextEncoder(); - const Ys = proofs.map((p) => dhke.hashToCurve(enc.encode(p.secret)).toHex(true)); + const Ys = proofs.map((p) => hashToCurve(enc.encode(p.secret)).toHex(true)); const payload = { // array of Ys of proofs to check Ys: Ys @@ -652,7 +660,7 @@ class CashuWallet { secretBytes = new TextEncoder().encode(secretHex); } secrets.push(secretBytes); - const { B_, r } = dhke.blindMessage(secretBytes, deterministicR); + const { B_, r } = blindMessage(secretBytes, deterministicR); rs.push(r); const blindedMessage = new BlindedMessage(amounts[i], B_, keysetId); blindedMessages.push(blindedMessage.getSerializedBlindedMessage()); @@ -682,6 +690,31 @@ class CashuWallet { const { blindedMessages, rs, secrets } = this.createBlindedMessages(amounts, keysetId, counter); return { blindedMessages, secrets, rs }; } + + /** + * construct proofs from @params promises, @params rs, @params secrets, and @params keyset + * @param promises array of serialized blinded signatures + * @param rs arrays of binding factors + * @param secrets array of secrets + * @param keyset mint keyset + * @returns array of serialized proofs + */ + private constructProofs( + promises: Array, + rs: Array, + secrets: Array, + keyset: MintKeys + ): Array { + return promises + .map((p: SerializedBlindedSignature, i: number) => { + const blindSignature = { id: p.id, amount: p.amount, C_: pointFromHex(p.C_) }; + const r = rs[i]; + const secret = secrets[i]; + const A = pointFromHex(keyset.keys[p.amount]); + return constructProofFromPromise(blindSignature, r, secret, A); + }) + .map((p) => serializeProof(p) as Proof); + } } export { CashuWallet }; diff --git a/src/DHKE.ts b/src/DHKE.ts deleted file mode 100644 index da1dfe6b1..000000000 --- a/src/DHKE.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { ProjPointType } from '@noble/curves/abstract/weierstrass'; -import { secp256k1 } from '@noble/curves/secp256k1'; -import { MintKeys, Proof, SerializedBlindedSignature } from './model/types/index.js'; -import { bytesToNumber } from './utils.js'; -import { sha256 } from '@noble/hashes/sha256'; -import { bytesToHex, hexToBytes } from '@noble/curves/abstract/utils'; -import { Buffer } from 'buffer/'; - -const DOMAIN_SEPARATOR = hexToBytes('536563703235366b315f48617368546f43757276655f43617368755f'); - -function hashToCurve(secret: Uint8Array): ProjPointType { - const msgToHash = sha256(Buffer.concat([DOMAIN_SEPARATOR, secret])); - const counter = new Uint32Array(1); - const maxIterations = 2 ** 16; - for (let i = 0; i < maxIterations; i++) { - const counterBytes = new Uint8Array(counter.buffer); - const hash = sha256(Buffer.concat([msgToHash, counterBytes])); - try { - return pointFromHex(bytesToHex(Buffer.concat([new Uint8Array([0x02]), hash]))); - } catch (error) { - counter[0]++; - } - } - throw new Error('No valid point found'); -} - -export function pointFromHex(hex: string) { - return secp256k1.ProjectivePoint.fromHex(hex); -} -/* export function h2cToPoint(h2c: H2CPoint): ProjPointType { - return secp256k1.ProjectivePoint.fromAffine(h2c.toAffine()); -} */ -function blindMessage(secret: Uint8Array, r?: bigint): { B_: ProjPointType; r: bigint } { - const Y = hashToCurve(secret); - if (!r) { - r = bytesToNumber(secp256k1.utils.randomPrivateKey()); - } - const rG = secp256k1.ProjectivePoint.BASE.multiply(r); - const B_ = Y.add(rG); - return { B_, r }; -} - -function unblindSignature( - C_: ProjPointType, - r: bigint, - A: ProjPointType -): ProjPointType { - const C = C_.subtract(A.multiply(r)); - return C; -} - -function constructProofs( - promises: Array, - rs: Array, - secrets: Array, - keyset: MintKeys -): Array { - return promises.map((p: SerializedBlindedSignature, i: number) => { - const C_ = pointFromHex(p.C_); - const A = pointFromHex(keyset.keys[p.amount]); - const C = unblindSignature(C_, rs[i], A); - const proof = { - id: p.id, - amount: p.amount, - secret: new TextDecoder().decode(secrets[i]), - C: C.toHex(true) - }; - return proof; - }); -} - -export { hashToCurve, blindMessage, unblindSignature, constructProofs }; diff --git a/src/index.ts b/src/index.ts index f1c49f0b0..efa9c23f3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { CashuMint } from './CashuMint.js'; import { CashuWallet } from './CashuWallet.js'; import { setGlobalRequestOptions } from './request.js'; -import { generateNewMnemonic, deriveSeedFromMnemonic } from './secrets.js'; +import { generateNewMnemonic, deriveSeedFromMnemonic } from '@cashu/crypto/modules/client/NUT09'; import { getEncodedToken, getDecodedToken, deriveKeysetId } from './utils.js'; export * from './model/types/index.js'; diff --git a/src/secrets.ts b/src/secrets.ts deleted file mode 100644 index 48e4fbb69..000000000 --- a/src/secrets.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { HDKey } from '@scure/bip32'; -import { generateMnemonic, mnemonicToSeedSync } from '@scure/bip39'; -import { wordlist } from '@scure/bip39/wordlists/english'; -import { encodeBase64toUint8 } from './base64'; -import { bytesToNumber } from './utils'; -import { hexToNumber } from '@noble/curves/abstract/utils'; - -const STANDARD_DERIVATION_PATH = `m/129372'/0'`; - -enum DerivationType { - SECRET = 0, - BLINDING_FACTOR = 1 -} - -export const generateNewMnemonic = (): string => { - const mnemonic = generateMnemonic(wordlist, 128); - return mnemonic; -}; - -export const deriveSeedFromMnemonic = (mnemonic: string): Uint8Array => { - const seed = mnemonicToSeedSync(mnemonic); - return seed; -}; - -export const deriveSecret = (seed: Uint8Array, keysetId: string, counter: number): Uint8Array => { - return derive(seed, keysetId, counter, DerivationType.SECRET); -}; - -export const deriveBlindingFactor = ( - seed: Uint8Array, - keysetId: string, - counter: number -): Uint8Array => { - return derive(seed, keysetId, counter, DerivationType.BLINDING_FACTOR); -}; - -const derive = ( - seed: Uint8Array, - keysetId: string, - counter: number, - secretOrBlinding: DerivationType -): Uint8Array => { - const hdkey = HDKey.fromMasterSeed(seed); - const keysetIdInt = getKeysetIdInt(keysetId); - const derivationPath = `${STANDARD_DERIVATION_PATH}/${keysetIdInt}'/${counter}'/${secretOrBlinding}`; - const derived = hdkey.derive(derivationPath); - if (derived.privateKey === null) { - throw new Error('Could not derive private key'); - } - return derived.privateKey; -}; - -const getKeysetIdInt = (keysetId: string): bigint => { - let keysetIdInt: bigint; - if (/^[a-fA-F0-9]+$/.test(keysetId)) { - keysetIdInt = hexToNumber(keysetId) % BigInt(2 ** 31 - 1); - } else { - //legacy keyset compatibility - keysetIdInt = bytesToNumber(encodeBase64toUint8(keysetId)) % BigInt(2 ** 31 - 1); - } - return keysetIdInt; -}; diff --git a/test/crypto.scheme.test.ts b/test/crypto.scheme.test.ts deleted file mode 100644 index 16ca68a94..000000000 --- a/test/crypto.scheme.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { secp256k1 } from '@noble/curves/secp256k1'; -import { hashToCurve } from '../src/DHKE.js'; -import { bytesToNumber } from '../src/utils.js'; -import { ProjPointType } from '@noble/curves/abstract/weierstrass'; - -describe('test crypto bdhke', () => { - test('bdhke', async () => { - //Mint(Alice) - const mint: Mint = new Mint(); - - //Wallet(Bob) - const wallet: Wallet = new Wallet(); - const B_ = await wallet.createBlindedMessage('secret'); - - //Mint - const C_ = mint.createBlindSignature(B_); - - //Wallet - const { C, secret } = wallet.unblindSignature(C_, mint.publicKey); - - //Mint - const aY = await mint.calculateCVerify(secret); - expect(aY.equals(C)).toBe(true); - }); -}); - -class Mint { - private privateKey: Uint8Array; - publicKey: ProjPointType; - constructor() { - this.privateKey = secp256k1.utils.randomPrivateKey(); - this.publicKey = secp256k1.ProjectivePoint.BASE.multiply(bytesToNumber(this.privateKey)); - } - - createBlindSignature(B_: ProjPointType): ProjPointType { - const C_ = B_.multiply(bytesToNumber(this.privateKey)); - return C_; - } - - async calculateCVerify(secret: Uint8Array): Promise> { - const Y = hashToCurve(secret); - const aY = Y.multiply(bytesToNumber(this.privateKey)); - return aY; - } -} - -class Wallet { - private Y: ProjPointType | undefined; - private r = BigInt(0); - private rG: ProjPointType | undefined; - private B_: ProjPointType | undefined; - private secret = new Uint8Array(); - constructor() {} - - async createBlindedMessage(message: string): Promise> { - const enc = new TextEncoder(); - this.secret = enc.encode(message); - this.Y = hashToCurve(this.secret); - this.r = bytesToNumber(secp256k1.utils.randomPrivateKey()); - this.rG = secp256k1.ProjectivePoint.BASE.multiply(this.r); - this.B_ = this.Y.add(this.rG); - return this.B_; - } - - unblindSignature( - C_: ProjPointType, - mintPubK: ProjPointType - ): { C: ProjPointType; secret: Uint8Array } { - const C = C_.subtract(mintPubK.multiply(this.r)); - return { C, secret: this.secret }; - } -} diff --git a/test/dhke.test.ts b/test/dhke.test.ts deleted file mode 100644 index d85e7d627..000000000 --- a/test/dhke.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { hexToBytes } from '@noble/curves/abstract/utils'; -import * as dhke from '../src/DHKE.js'; -import { bytesToNumber } from '../src/utils.js'; - -const SECRET_MESSAGE = 'test_message'; - -describe('testing hash to curve', () => { - test('testing string 0000....00', async () => { - let secret = hexToBytes('0000000000000000000000000000000000000000000000000000000000000000'); - let Y = dhke.hashToCurve(secret); - let hexY = Y.toHex(true); - expect(hexY).toBe('024cce997d3b518f739663b757deaec95bcd9473c30a14ac2fd04023a739d1a725'); - }); - - test('testing string 0000....01', async () => { - let secret = hexToBytes('0000000000000000000000000000000000000000000000000000000000000001'); - let Y = dhke.hashToCurve(secret); - let hexY = Y.toHex(true); - expect(hexY).toBe('022e7158e11c9506f1aa4248bf531298daa7febd6194f003edcd9b93ade6253acf'); - }); -}); - -describe('test blinding message', () => { - test('testing string 0000....01', async () => { - let secretUInt8 = new TextEncoder().encode(SECRET_MESSAGE); - expect(secretUInt8).toStrictEqual( - new Uint8Array([116, 101, 115, 116, 95, 109, 101, 115, 115, 97, 103, 101]) - ); - const r = bytesToNumber( - hexToBytes('0000000000000000000000000000000000000000000000000000000000000001') - ); - let { B_ } = await dhke.blindMessage(secretUInt8, r); - expect(B_.toHex(true)).toBe( - '025cc16fe33b953e2ace39653efb3e7a7049711ae1d8a2f7a9108753f1cdea742b' - ); - }); -}); - -describe('test unblinding signature', () => { - test('testing string 0000....01', async () => { - let C_ = dhke.pointFromHex( - '02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2' - ); - let r = bytesToNumber( - hexToBytes('0000000000000000000000000000000000000000000000000000000000000001') - ); - let A = dhke.pointFromHex('020000000000000000000000000000000000000000000000000000000000000001'); - let C = dhke.unblindSignature(C_, r, A); - expect(C.toHex(true)).toBe( - '03c724d7e6a5443b39ac8acf11f40420adc4f99a02e7cc1b57703d9391f6d129cd' - ); - }); -}); diff --git a/test/secrets.test.ts b/test/secrets.test.ts deleted file mode 100644 index c561f2a8d..000000000 --- a/test/secrets.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { bytesToHex } from '@noble/curves/abstract/utils'; -import { deriveSeedFromMnemonic } from '../src/secrets'; -import { deriveBlindingFactor, deriveSecret } from '../src/secrets'; -import { blindMessage } from '../src/DHKE'; -import { HDKey } from '@scure/bip32'; - -const mnemonic = 'half depart obvious quality work element tank gorilla view sugar picture humble'; -const seed = deriveSeedFromMnemonic(mnemonic); - -describe('testing hdkey from seed', () => { - test('hdkey from seed', async () => { - const hdkey = HDKey.fromMasterSeed(seed); - expect(hdkey).not.toBeNull(); - }); - - test('hdkey to uint8array', async () => { - const hdkey = HDKey.fromMasterSeed(seed); - const privateKey = hdkey.privateKey; - expect(privateKey).not.toBeNull(); - - const seed_expected = - 'dd44ee516b0647e80b488e8dcc56d736a148f15276bef588b37057476d4b2b25780d3688a32b37353d6995997842c0fd8b412475c891c16310471fbc86dcbda8'; - const seed_uint8_array_expected = Uint8Array.from(Buffer.from(seed_expected, 'hex')); - expect(seed).toEqual(seed_uint8_array_expected); - }); -}); - -describe('testing deterministic secrets', () => { - const secrets = [ - '485875df74771877439ac06339e284c3acfcd9be7abf3bc20b516faeadfe77ae', - '8f2b39e8e594a4056eb1e6dbb4b0c38ef13b1b2c751f64f810ec04ee35b77270', - 'bc628c79accd2364fd31511216a0fab62afd4a18ff77a20deded7b858c9860c8', - '59284fd1650ea9fa17db2b3acf59ecd0f2d52ec3261dd4152785813ff27a33bf', - '576c23393a8b31cc8da6688d9c9a96394ec74b40fdaf1f693a6bb84284334ea0' - ]; - test('derive Secret', async () => { - const secret1 = deriveSecret(seed, '009a1f293253e41e', 0); - const secret2 = deriveSecret(seed, '009a1f293253e41e', 1); - const secret3 = deriveSecret(seed, '009a1f293253e41e', 2); - const secret4 = deriveSecret(seed, '009a1f293253e41e', 3); - const secret5 = deriveSecret(seed, '009a1f293253e41e', 4); - - const bf1 = deriveBlindingFactor(seed, '009a1f293253e41e', 0); - const bf2 = deriveBlindingFactor(seed, '009a1f293253e41e', 1); - const bf3 = deriveBlindingFactor(seed, '009a1f293253e41e', 2); - const bf4 = deriveBlindingFactor(seed, '009a1f293253e41e', 3); - const bf5 = deriveBlindingFactor(seed, '009a1f293253e41e', 4); - - expect(bytesToHex(secret1)).toBe(secrets[0]); - expect(bytesToHex(secret2)).toBe(secrets[1]); - expect(bytesToHex(secret3)).toBe(secrets[2]); - expect(bytesToHex(secret4)).toBe(secrets[3]); - expect(bytesToHex(secret5)).toBe(secrets[4]); - }); -}); - -describe('testing deterministic blindedMessage', () => { - const secrets = ['485875df74771877439ac06339e284c3acfcd9be7abf3bc20b516faeadfe77ae']; - test('derive Secret', async () => { - const secret1 = deriveSecret(seed, '009a1f293253e41e', 0); - - const bf1 = deriveBlindingFactor(seed, '009a1f293253e41e', 0); - - expect(bytesToHex(secret1)).toBe(secrets[0]); - - // blindMessage() - }); -}); - -describe('test private key derivation from derivation path', () => { - const seed = - 'dd44ee516b0647e80b488e8dcc56d736a148f15276bef588b37057476d4b2b25780d3688a32b37353d6995997842c0fd8b412475c891c16310471fbc86dcbda8'; - const seed_uint8_array = Uint8Array.from(Buffer.from(seed, 'hex')); - const hdkey = HDKey.fromMasterSeed(seed_uint8_array); - const expected_privatekey = '9d32fc57e6fa2942d05ee475d28ba6a56839b8cb8a3f174b05ed0ed9d3a420f6'; - const derivation_path = "m/129372'/0'/2004500376'/0'/0"; - const derived = hdkey.derive(derivation_path); - test('derive Secret', async () => { - expect(derived.privateKey).not.toBeNull(); - const privateKey = derived.privateKey || new Uint8Array(); - expect(bytesToHex(privateKey)).toBe(expected_privatekey); - }); -}); diff --git a/test/utils.test.ts b/test/utils.test.ts index ccc341607..898ae22bb 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -304,7 +304,6 @@ describe('test keyset derivation', () => { test('derive', () => { const keys = PUBKEYS; const keysetId = utils.deriveKeysetId(keys); - console.log(keysetId); expect(keysetId).toBe('009a1f293253e41e'); }); }); diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 27fcb7224..e32b300d3 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -170,7 +170,7 @@ describe('receive', () => { expect(/[0-9a-f]{64}/.test(t.token[0].proofs[0].secret)).toBe(true); }); test('test receive could not verify proofs', async () => { - nock(mintUrl).post('/v1/split').reply(200, { code: 0, error: 'could not verify proofs.' }); + nock(mintUrl).post('/v1/swap').reply(200, { code: 0, error: 'could not verify proofs.' }); const wallet = new CashuWallet(mint, { unit }); const { tokensWithErrors } = await wallet.receive(tokenInput);