diff --git a/package-lock.json b/package-lock.json index a740988..9f6a91e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,9 @@ "version": "2.6.5", "license": "MIT", "dependencies": { - "@noble/hashes": "^1.3.0", - "@noble/secp256k1": "^1.7.1", + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.3", + "@noble/secp256k1": "^2.0.0", "bs58": "^5.0.0", "ethers": "^5.6.6", "reflect-metadata": "^0.1.13" @@ -1845,21 +1846,32 @@ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "dev": true }, - "node_modules/@noble/hashes": { + "node_modules/@noble/curves": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz", - "integrity": "sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", + "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "dependencies": { + "@noble/hashes": "1.3.3" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } }, "node_modules/@noble/secp256k1": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", - "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-2.0.0.tgz", + "integrity": "sha512-rUGBd95e2a45rlmFTqQJYEFA4/gdIARFfuTuTqLglz0PZ6AKyzyXsEZZq7UZn8hZsvaBgpCzKKBJizT2cJERXw==", "funding": [ { "type": "individual", diff --git a/package.json b/package.json index 1ae5fb2..5f62115 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,9 @@ ] }, "dependencies": { - "@noble/hashes": "^1.3.0", - "@noble/secp256k1": "^1.7.1", + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.3", + "@noble/secp256k1": "^2.0.0", "bs58": "^5.0.0", "ethers": "^5.6.6", "reflect-metadata": "^0.1.13" diff --git a/src/identity/crypto-utils.ts b/src/identity/crypto-utils.ts index 329c33a..add7743 100644 --- a/src/identity/crypto-utils.ts +++ b/src/identity/crypto-utils.ts @@ -1,15 +1,31 @@ -import { sha256 } from '@noble/hashes/sha256'; -import { - Point, - sign as ecSign, - utils as ecUtils, - getPublicKey, - getSharedSecret as nobleGetSharedSecret, -} from '@noble/secp256k1'; +import { secp256k1 } from '@noble/curves/secp256k1'; import bs58 from 'bs58'; import { PUBLIC_KEY_PREFIXES } from './constants.js'; import { TransactionV0 } from './transaction-transcoders.js'; import { KeyPair, Network, jwtAlgorithm } from './types.js'; +import { + bytesToHex, + concatBytes, + hexToBytes, + randomBytes, +} from '@noble/hashes/utils'; +import { sha256 } from '@noble/hashes/sha256'; + +async function hmacSha256Async( + key: Uint8Array, + message: Uint8Array +): Promise { + const encoder = new TextEncoder(); + const k = await crypto.subtle.importKey( + 'raw', + key, + { name: 'HMAC', hash: 'SHA-256' }, + true, + ['sign'] + ); + const signature = await crypto.subtle.sign('HMAC', k, message); + return new Uint8Array(signature); +} // Browser friendly version of node's Buffer.concat. export function concatUint8Arrays(arrays: Uint8Array[], length?: number) { @@ -98,19 +114,19 @@ interface Base58CheckOptions { // randomly generated 32 byte value (Uint8Array of length 32 or hex string of // length 64) export const keygen = (seed?: string | Uint8Array): KeyPair => { - const privateKey = seed ? normalizeSeed(seed) : ecUtils.randomBytes(32); - const seedHex = ecUtils.bytesToHex(privateKey); + const privateKey = seed ? normalizeSeed(seed) : randomBytes(32); + const seedHex = bytesToHex(privateKey); return { seedHex, private: privateKey, - public: getPublicKey(privateKey, true /* isCompressed */), + public: secp256k1.getPublicKey(privateKey, true /* isCompressed */), }; }; const normalizeSeed = (seed: string | Uint8Array): Uint8Array => { if (typeof seed === 'string') { - return ecUtils.hexToBytes(seed); + return hexToBytes(seed); } else { return seed; } @@ -122,7 +138,7 @@ const normalizeSeed = (seed: string | Uint8Array): Uint8Array => { * @returns */ export const sha256X2 = (data: Uint8Array | string): Uint8Array => { - const d = typeof data === 'string' ? ecUtils.hexToBytes(data) : data; + const d = typeof data === 'string' ? hexToBytes(data) : data; return sha256(sha256(d)); }; @@ -143,14 +159,16 @@ export interface SignOptions { isDerivedKey: boolean; } -export const sign = (msgHashHex: string, privateKey: Uint8Array) => { - return ecSign(msgHashHex, privateKey, { - // For details about the signing options see: https://github.com/paulmillr/noble-secp256k1#signmsghash-privatekey - canonical: true, - der: true, +export const sign = ( + msgHashHex: string, + privateKey: Uint8Array +): [Uint8Array, number] => { + const signature = secp256k1.sign(msgHashHex, privateKey, { extraEntropy: true, - recovered: true, + lowS: true, }); + + return [signature.toDERRawBytes(), signature.recovery]; }; export const signTx = async ( @@ -158,17 +176,14 @@ export const signTx = async ( seedHex: string, options?: SignOptions ): Promise => { - const transactionBytes = ecUtils.hexToBytes(txHex); + const transactionBytes = hexToBytes(txHex); const [_, v1FieldsBuffer] = TransactionV0.fromBytes(transactionBytes); const signatureIndex = transactionBytes.length - v1FieldsBuffer.length - 1; const v0FieldsWithoutSignature = transactionBytes.slice(0, signatureIndex); const hashedTxBytes = sha256X2(transactionBytes); - const transactionHashHex = ecUtils.bytesToHex(hashedTxBytes); - const privateKey = ecUtils.hexToBytes(seedHex); - const [signatureBytes, recoveryParam] = await sign( - transactionHashHex, - privateKey - ); + const transactionHashHex = bytesToHex(hashedTxBytes); + const privateKey = hexToBytes(seedHex); + const [signatureBytes, recoveryParam] = sign(transactionHashHex, privateKey); const signatureLength = uvarint64ToBuf(signatureBytes.length); @@ -176,14 +191,14 @@ export const signTx = async ( signatureBytes[0] += 1 + recoveryParam; } - const signedTransactionBytes = ecUtils.concatBytes( + const signedTransactionBytes = concatBytes( v0FieldsWithoutSignature, signatureLength, signatureBytes, v1FieldsBuffer ); - return ecUtils.bytesToHex(signedTransactionBytes); + return bytesToHex(signedTransactionBytes); }; export const getSignedJWT = async ( @@ -208,9 +223,9 @@ export const getSignedJWT = async ( }); const jwt = `${urlSafeBase64(header)}.${urlSafeBase64(payload)}`; - const [signature] = await sign( - ecUtils.bytesToHex(sha256(new Uint8Array(new TextEncoder().encode(jwt)))), - ecUtils.hexToBytes(seedHex) + const [signature] = sign( + bytesToHex(sha256(new Uint8Array(new TextEncoder().encode(jwt)))), + hexToBytes(seedHex) ); const encodedSignature = derToJoseEncoding(signature); @@ -230,12 +245,12 @@ export const encryptChatMessage = ( recipientPublicKeyBase58Check: string, message: string ) => { - const privateKey = ecUtils.hexToBytes(senderSeedHex); + const privateKey = hexToBytes(senderSeedHex); const recipientPublicKey = bs58PublicKeyToBytes( recipientPublicKeyBase58Check ); const sharedPrivateKey = getSharedPrivateKey(privateKey, recipientPublicKey); - const sharedPublicKey = getPublicKey(sharedPrivateKey); + const sharedPublicKey = secp256k1.getPublicKey(sharedPrivateKey); return encrypt(sharedPublicKey, message); }; @@ -249,13 +264,13 @@ export const encrypt = async ( publicKey: Uint8Array | string, plaintext: string ): Promise => { - const ephemPrivateKey = ecUtils.randomBytes(32); - const ephemPublicKey = getPublicKey(ephemPrivateKey); + const ephemPrivateKey = randomBytes(32); + const ephemPublicKey = secp256k1.getPublicKey(ephemPrivateKey); const publicKeyBytes = typeof publicKey === 'string' ? bs58PublicKeyToBytes(publicKey) : publicKey; const privKey = getSharedPrivateKey(ephemPrivateKey, publicKeyBytes); const encryptionKey = privKey.slice(0, 16); - const iv = ecUtils.randomBytes(16); + const iv = randomBytes(16); const macKey = sha256(privKey.slice(16)); const bytes = new TextEncoder().encode(plaintext); const cryptoKey = await globalThis.crypto.subtle.importKey( @@ -274,17 +289,18 @@ export const encrypt = async ( cryptoKey, bytes ); - const hmac = await ecUtils.hmacSha256( + + const hmacSha256 = await hmacSha256Async( macKey, new Uint8Array([...iv, ...new Uint8Array(cipherBytes)]) ); - return ecUtils.bytesToHex( + return bytesToHex( new Uint8Array([ ...ephemPublicKey, ...iv, ...new Uint8Array(cipherBytes), - ...hmac, + ...hmacSha256, ]) ); }; @@ -294,7 +310,9 @@ export const bs58PublicKeyToCompressedBytes = (str: string) => { return new Uint8Array(33); } const pubKeyUncompressed = bs58PublicKeyToBytes(str); - return Point.fromHex(ecUtils.bytesToHex(pubKeyUncompressed)).toRawBytes(true); + return secp256k1.ProjectivePoint.fromHex( + bytesToHex(pubKeyUncompressed) + ).toRawBytes(true); }; export const bs58PublicKeyToBytes = (str: string) => { @@ -312,7 +330,9 @@ export const bs58PublicKeyToBytes = (str: string) => { throw new Error('Invalid checksum'); } - return Point.fromHex(ecUtils.bytesToHex(payload.slice(3))).toRawBytes(false); + return secp256k1.ProjectivePoint.fromHex( + bytesToHex(payload.slice(3)) + ).toRawBytes(false); }; const regexMainnet = /^BC[1-9A-HJ-NP-Za-km-z]{53}$/; @@ -350,7 +370,7 @@ export const decryptChatMessage = async ( publicDecryptionKey: string, cipherTextHex: string ) => { - const privateKey = ecUtils.hexToBytes(recipientSeedHex); + const privateKey = hexToBytes(recipientSeedHex); const publicKey = await bs58PublicKeyToBytes(publicDecryptionKey); const sharedPrivateKey = await getSharedPrivateKey(privateKey, publicKey); return decrypt(sharedPrivateKey, cipherTextHex); @@ -360,7 +380,7 @@ export const decrypt = async ( privateDecryptionKey: Uint8Array | string, cipherTextHex: string ) => { - const cipherBytes = ecUtils.hexToBytes(cipherTextHex); + const cipherBytes = hexToBytes(cipherTextHex); const metaLength = 113; if (cipherBytes.length < metaLength) { @@ -381,7 +401,7 @@ export const decrypt = async ( const sharedSecretKey = await getSharedPrivateKey(privateKey, ephemPublicKey); const encryptionKey = sharedSecretKey.slice(0, 16); const macKey = sha256(sharedSecretKey.slice(16)); - const hmacKnownGood = await ecUtils.hmacSha256(macKey, cipherAndIv); + const hmacKnownGood = await hmacSha256Async(macKey, cipherAndIv); if (!isValidHmac(msgMac, hmacKnownGood)) { throw new Error('incorrect MAC'); @@ -416,16 +436,16 @@ export const getSharedPrivateKey = ( export const decodePublicKey = async (publicKeyBase58Check: string) => { const decoded = await bs58PublicKeyToBytes(publicKeyBase58Check); const withPrefixRemoved = decoded.slice(3); - const senderPubKeyHex = ecUtils.bytesToHex(withPrefixRemoved); + const senderPubKeyHex = bytesToHex(withPrefixRemoved); - return Point.fromHex(senderPubKeyHex).toRawBytes(false); + return secp256k1.ProjectivePoint.fromHex(senderPubKeyHex).toRawBytes(false); }; export const getSharedSecret = (privKey: Uint8Array, pubKey: Uint8Array) => { // passing true to compress the public key, and then slicing off the first byte // matches the implementation of derive in the elliptic package. // https://github.com/paulmillr/noble-secp256k1/issues/28#issuecomment-946538037 - return nobleGetSharedSecret(privKey, pubKey, true).slice(1); + return secp256k1.getSharedSecret(privKey, pubKey, true).slice(1); }; // taken from reference implementation in the deso chat app: diff --git a/src/identity/derived-key-utils.ts b/src/identity/derived-key-utils.ts index 2d8895a..5c6277b 100644 --- a/src/identity/derived-key-utils.ts +++ b/src/identity/derived-key-utils.ts @@ -1,4 +1,4 @@ -import { utils as ecUtils } from '@noble/secp256k1'; +import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; import { TransactionSpendingLimitResponse } from '../backend-types/index.js'; import { api, getAppState } from '../data/index.js'; import { @@ -55,14 +55,14 @@ export async function generateDerivedKeyPayload( } ); const transactionSpendingLimitBytes = TransactionSpendingLimitHex - ? ecUtils.hexToBytes(TransactionSpendingLimitHex) + ? hexToBytes(TransactionSpendingLimitHex) : []; const accessBytes = new Uint8Array([ ...derivedKeys.public, ...uint64ToBufBigEndian(expirationBlockHeight), ...transactionSpendingLimitBytes, ]); - const accessHashHex = ecUtils.bytesToHex(sha256X2(accessBytes)); + const accessHashHex = bytesToHex(sha256X2(accessBytes)); const [accessSignature] = await sign(accessHashHex, ownerKeys.private); const messagingKey = deriveAccessGroupKeyPair( ownerKeys.seedHex, @@ -72,7 +72,7 @@ export async function generateDerivedKeyPayload( messagingKey.public, { network } ); - const messagingKeyHashHex = ecUtils.bytesToHex( + const messagingKeyHashHex = bytesToHex( sha256X2( new Uint8Array([ ...messagingKey.public, @@ -100,13 +100,13 @@ export async function generateDerivedKeyPayload( ethDepositAddress: 'Not implemented yet', expirationBlock: expirationBlockHeight, network, - accessSignature: ecUtils.bytesToHex(accessSignature), + accessSignature: bytesToHex(accessSignature), jwt, derivedJwt, messagingPublicKeyBase58Check, messagingPrivateKey: messagingKey.seedHex, messagingKeyName: defaultMessagingGroupName, - messagingKeySignature: ecUtils.bytesToHex(messagingKeySignature), + messagingKeySignature: bytesToHex(messagingKeySignature), transactionSpendingLimitHex: TransactionSpendingLimitHex, signedUp: false, publicKeyAdded: ownerPublicKeyBase58, diff --git a/src/identity/identity.spec.ts b/src/identity/identity.spec.ts index 73414d4..c59e9b8 100644 --- a/src/identity/identity.spec.ts +++ b/src/identity/identity.spec.ts @@ -1,4 +1,4 @@ -import { utils as ecUtils, getPublicKey } from '@noble/secp256k1'; +import { secp256k1 } from '@noble/curves/secp256k1'; import { verify } from 'jsonwebtoken'; import KeyEncoder from 'key-encoder'; import { ChatType, NewMessageEntryResponse } from '../backend-types/index.js'; @@ -20,11 +20,12 @@ import { TransactionNonce, } from './transaction-transcoders.js'; import { APIProvider, AsyncStorage } from './types.js'; +import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; function getPemEncodePublicKey(privateKey: Uint8Array): string { - const publicKey = getPublicKey(privateKey, true); + const publicKey = secp256k1.getPublicKey(privateKey, true); const keyEncoder = new KeyEncoder('secp256k1'); - return keyEncoder.encodePublic(ecUtils.bytesToHex(publicKey), 'raw', 'pem'); + return keyEncoder.encodePublic(bytesToHex(publicKey), 'raw', 'pem'); } describe('identity', () => { @@ -93,7 +94,7 @@ describe('identity', () => { }); const txBytes = exampleTransaction.toBytes(); return Promise.resolve({ - TransactionHex: ecUtils.bytesToHex(txBytes), + TransactionHex: bytesToHex(txBytes), }); } return Promise.resolve(null); @@ -366,7 +367,7 @@ describe('identity', () => { const jwt = await identity.jwt(); const parsedAndVerifiedJwt = verify( jwt, - getPemEncodePublicKey(ecUtils.hexToBytes(testDerivedSeedHex)), + getPemEncodePublicKey(hexToBytes(testDerivedSeedHex)), { // See: https://github.com/auth0/node-jsonwebtoken/issues/862 // tl;dr: the jsonwebtoken library doesn't support the ES256K algorithm, @@ -397,7 +398,7 @@ describe('identity', () => { const jwt = await identity.jwt(); let errorMessage = ''; try { - verify(jwt, getPemEncodePublicKey(ecUtils.hexToBytes(badSeedHex)), { + verify(jwt, getPemEncodePublicKey(hexToBytes(badSeedHex)), { // See: https://github.com/auth0/node-jsonwebtoken/issues/862 // tl;dr: the jsonwebtoken library doesn't support the ES256K algorithm, // even though this is the correct algorithm for JWTs signed @@ -519,7 +520,7 @@ describe('identity', () => { 'lorem ipsum dolor sit amet, consectetur adipiscing elit'; const textEncoder = new TextEncoder(); const bytes = textEncoder.encode(plaintextMsg); - const hexEncodedMsg = ecUtils.bytesToHex(new Uint8Array(bytes)); + const hexEncodedMsg = bytesToHex(new Uint8Array(bytes)); // we only need to set the active user to the recipient, since we're not // decrypting anything. diff --git a/src/identity/identity.ts b/src/identity/identity.ts index 21d1db7..931ce53 100644 --- a/src/identity/identity.ts +++ b/src/identity/identity.ts @@ -1,5 +1,6 @@ import { keccak_256 } from '@noble/hashes/sha3'; -import { Point, utils as ecUtils } from '@noble/secp256k1'; +import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; +import { secp256k1 } from '@noble/curves/secp256k1'; import { ethers } from 'ethers'; import { AccessGroupEntryResponse, @@ -1310,9 +1311,9 @@ export class Identity { desoAddressToEthereumAddress(address: string) { const desoPKBytes = bs58PublicKeyToBytes(address).slice(1); - const ethPKHex = ecUtils.bytesToHex(keccak_256(desoPKBytes)).slice(24); + const ethPKHex = bytesToHex(keccak_256(desoPKBytes)).slice(24); // EIP-55 requires a checksum. Reference implementation: https://eips.ethereum.org/EIPS/eip-55 - const checksum = ecUtils.bytesToHex(keccak_256(ethPKHex)); + const checksum = bytesToHex(keccak_256(ethPKHex)); return Array.from(ethPKHex).reduce( (ethAddress, char, index) => @@ -1419,7 +1420,8 @@ export class Identity { ); } - const compressedEthKey = Point.fromHex(ethereumPublicKey).toRawBytes(true); + const compressedEthKey = + secp256k1.ProjectivePoint.fromHex(ethereumPublicKey).toRawBytes(true); return publicKeyToBase58Check(compressedEthKey, { network: this.#network }); } @@ -2162,7 +2164,7 @@ class DeSoCoreError extends Error { } const unencryptedHexToPlainText = (hex: string) => { - const bytes = ecUtils.hexToBytes(hex); + const bytes = hexToBytes(hex); const textDecoder = new TextDecoder(); return textDecoder.decode(bytes); }; diff --git a/src/identity/noble-elliptic-compat-test.spec.ts b/src/identity/noble-elliptic-compat-test.spec.ts index 3d70174..5977b62 100644 --- a/src/identity/noble-elliptic-compat-test.spec.ts +++ b/src/identity/noble-elliptic-compat-test.spec.ts @@ -1,4 +1,4 @@ -import * as noble from '@noble/secp256k1'; +import { secp256k1 } from '@noble/curves/secp256k1'; import * as bs58check from 'bs58check'; import { ec as EC } from 'elliptic'; @@ -19,7 +19,7 @@ describe('@noble/sepc256k1 and elliptic.ec compatibility', () => { const ecBs58CheckPubKey = bs58check.encode(ecDesoPubKey); // noble key pair - const noblePubKeyCompressed = noble.getPublicKey(seedHex, true); + const noblePubKeyCompressed = secp256k1.getPublicKey(seedHex, true); const nobleDesoPubKey = new Uint8Array([ ...desoMainNetPrefix, ...noblePubKeyCompressed, diff --git a/src/transactions/social.ts b/src/transactions/social.ts index ed94ffc..1258e8f 100644 --- a/src/transactions/social.ts +++ b/src/transactions/social.ts @@ -1,5 +1,4 @@ -import { hexToBytes } from '@noble/hashes/utils'; -import { utils as ecUtils } from '@noble/secp256k1'; +import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; import { ConstructedTransactionResponse, CreateFollowTxnStatelessRequest, @@ -880,5 +879,5 @@ export const constructUpdateGroupChatMessageTransaction = ( function hexEncodePlainText(plainText: string) { const textEncoder = new TextEncoder(); const bytes = textEncoder.encode(plainText); - return ecUtils.bytesToHex(new Uint8Array(bytes)); + return bytesToHex(new Uint8Array(bytes)); }