From 59e064e7db357bde37642c44ee21581efa1d8ddf Mon Sep 17 00:00:00 2001 From: Mike Rudenko Date: Wed, 24 Jan 2024 17:00:18 +0200 Subject: [PATCH 1/3] chore: update crypto utils --- package-lock.json | 8 ++-- package.json | 2 +- src/identity/crypto-utils.ts | 48 +++++++++++-------- src/identity/derived-key-utils.ts | 12 ++--- src/identity/identity.spec.ts | 11 +++-- src/identity/identity.ts | 9 ++-- .../noble-elliptic-compat-test.spec.ts | 4 +- src/transactions/social.ts | 5 +- 8 files changed, 53 insertions(+), 46 deletions(-) diff --git a/package-lock.json b/package-lock.json index a740988..0d735ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@noble/hashes": "^1.3.0", - "@noble/secp256k1": "^1.7.1", + "@noble/secp256k1": "^2.0.0", "bs58": "^5.0.0", "ethers": "^5.6.6", "reflect-metadata": "^0.1.13" @@ -1857,9 +1857,9 @@ ] }, "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..5b0340f 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ }, "dependencies": { "@noble/hashes": "^1.3.0", - "@noble/secp256k1": "^1.7.1", + "@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..d6d554b 100644 --- a/src/identity/crypto-utils.ts +++ b/src/identity/crypto-utils.ts @@ -1,6 +1,6 @@ import { sha256 } from '@noble/hashes/sha256'; import { - Point, + ProjectivePoint as Point, sign as ecSign, utils as ecUtils, getPublicKey, @@ -10,6 +10,12 @@ 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'; // Browser friendly version of node's Buffer.concat. export function concatUint8Arrays(arrays: Uint8Array[], length?: number) { @@ -98,8 +104,8 @@ 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, @@ -110,7 +116,7 @@ export const keygen = (seed?: string | Uint8Array): KeyPair => { const normalizeSeed = (seed: string | Uint8Array): Uint8Array => { if (typeof seed === 'string') { - return ecUtils.hexToBytes(seed); + return hexToBytes(seed); } else { return seed; } @@ -122,7 +128,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)); }; @@ -158,13 +164,13 @@ 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 transactionHashHex = bytesToHex(hashedTxBytes); + const privateKey = hexToBytes(seedHex); const [signatureBytes, recoveryParam] = await sign( transactionHashHex, privateKey @@ -176,14 +182,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 ( @@ -209,8 +215,8 @@ 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) + bytesToHex(sha256(new Uint8Array(new TextEncoder().encode(jwt)))), + hexToBytes(seedHex) ); const encodedSignature = derToJoseEncoding(signature); @@ -230,7 +236,7 @@ export const encryptChatMessage = ( recipientPublicKeyBase58Check: string, message: string ) => { - const privateKey = ecUtils.hexToBytes(senderSeedHex); + const privateKey = hexToBytes(senderSeedHex); const recipientPublicKey = bs58PublicKeyToBytes( recipientPublicKeyBase58Check ); @@ -249,13 +255,13 @@ export const encrypt = async ( publicKey: Uint8Array | string, plaintext: string ): Promise => { - const ephemPrivateKey = ecUtils.randomBytes(32); + const ephemPrivateKey = randomBytes(32); const ephemPublicKey = 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( @@ -279,7 +285,7 @@ export const encrypt = async ( new Uint8Array([...iv, ...new Uint8Array(cipherBytes)]) ); - return ecUtils.bytesToHex( + return bytesToHex( new Uint8Array([ ...ephemPublicKey, ...iv, @@ -294,7 +300,7 @@ export const bs58PublicKeyToCompressedBytes = (str: string) => { return new Uint8Array(33); } const pubKeyUncompressed = bs58PublicKeyToBytes(str); - return Point.fromHex(ecUtils.bytesToHex(pubKeyUncompressed)).toRawBytes(true); + return Point.fromHex(bytesToHex(pubKeyUncompressed)).toRawBytes(true); }; export const bs58PublicKeyToBytes = (str: string) => { @@ -312,7 +318,7 @@ export const bs58PublicKeyToBytes = (str: string) => { throw new Error('Invalid checksum'); } - return Point.fromHex(ecUtils.bytesToHex(payload.slice(3))).toRawBytes(false); + return Point.fromHex(bytesToHex(payload.slice(3))).toRawBytes(false); }; const regexMainnet = /^BC[1-9A-HJ-NP-Za-km-z]{53}$/; @@ -350,7 +356,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 +366,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) { @@ -416,7 +422,7 @@ 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); }; 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..2c41bbe 100644 --- a/src/identity/identity.spec.ts +++ b/src/identity/identity.spec.ts @@ -20,11 +20,12 @@ import { TransactionNonce, } from './transaction-transcoders.js'; import { APIProvider, AsyncStorage } from './types.js'; +import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js'; function getPemEncodePublicKey(privateKey: Uint8Array): string { const publicKey = 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..fa251f5 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 { ProjectivePoint as Point } from '@noble/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) => @@ -2162,7 +2163,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..bbd8eaa 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 { getPublicKey } from '@noble/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 = 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)); } From 6c6779f08f39a6503fd2cc06ae5d9f1205fb7daf Mon Sep 17 00:00:00 2001 From: stas-kh Date: Wed, 24 Jan 2024 17:46:54 +0200 Subject: [PATCH 2/3] chore: updating sign method and small other adjustments for the updated library --- package-lock.json | 32 ++++++++++++++++++++++---------- package.json | 3 ++- src/identity/crypto-utils.ts | 34 +++++++++++++++++----------------- 3 files changed, 41 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0d735ce..9f6a91e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "2.6.5", "license": "MIT", "dependencies": { - "@noble/hashes": "^1.3.0", + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.3", "@noble/secp256k1": "^2.0.0", "bs58": "^5.0.0", "ethers": "^5.6.6", @@ -1845,16 +1846,27 @@ "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": "2.0.0", diff --git a/package.json b/package.json index 5b0340f..5f62115 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ ] }, "dependencies": { - "@noble/hashes": "^1.3.0", + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.3", "@noble/secp256k1": "^2.0.0", "bs58": "^5.0.0", "ethers": "^5.6.6", diff --git a/src/identity/crypto-utils.ts b/src/identity/crypto-utils.ts index d6d554b..0d469bc 100644 --- a/src/identity/crypto-utils.ts +++ b/src/identity/crypto-utils.ts @@ -1,11 +1,10 @@ -import { sha256 } from '@noble/hashes/sha256'; import { ProjectivePoint as Point, - sign as ecSign, - utils as ecUtils, getPublicKey, getSharedSecret as nobleGetSharedSecret, + etc, } 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'; @@ -16,6 +15,7 @@ import { hexToBytes, randomBytes, } from '@noble/hashes/utils'; +import { sha256 } from '@noble/hashes/sha256'; // Browser friendly version of node's Buffer.concat. export function concatUint8Arrays(arrays: Uint8Array[], length?: number) { @@ -149,14 +149,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 ( @@ -171,10 +173,7 @@ export const signTx = async ( const hashedTxBytes = sha256X2(transactionBytes); const transactionHashHex = bytesToHex(hashedTxBytes); const privateKey = hexToBytes(seedHex); - const [signatureBytes, recoveryParam] = await sign( - transactionHashHex, - privateKey - ); + const [signatureBytes, recoveryParam] = sign(transactionHashHex, privateKey); const signatureLength = uvarint64ToBuf(signatureBytes.length); @@ -214,7 +213,7 @@ export const getSignedJWT = async ( }); const jwt = `${urlSafeBase64(header)}.${urlSafeBase64(payload)}`; - const [signature] = await sign( + const [signature] = sign( bytesToHex(sha256(new Uint8Array(new TextEncoder().encode(jwt)))), hexToBytes(seedHex) ); @@ -280,7 +279,8 @@ export const encrypt = async ( cryptoKey, bytes ); - const hmac = await ecUtils.hmacSha256( + + const hmacSha256 = await etc.hmacSha256Async( macKey, new Uint8Array([...iv, ...new Uint8Array(cipherBytes)]) ); @@ -290,7 +290,7 @@ export const encrypt = async ( ...ephemPublicKey, ...iv, ...new Uint8Array(cipherBytes), - ...hmac, + ...hmacSha256, ]) ); }; @@ -387,7 +387,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 etc.hmacSha256Async(macKey, cipherAndIv); if (!isValidHmac(msgMac, hmacKnownGood)) { throw new Error('incorrect MAC'); From 91181ed978eb037e1e85a754b9bc4b4ac4668abc Mon Sep 17 00:00:00 2001 From: stas-kh Date: Wed, 24 Jan 2024 20:00:02 +0200 Subject: [PATCH 3/3] chore: getting rid of @noble/secp256k1 --- src/identity/crypto-utils.ts | 44 ++++++++++++------- src/identity/identity.spec.ts | 6 +-- src/identity/identity.ts | 5 ++- .../noble-elliptic-compat-test.spec.ts | 4 +- 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/identity/crypto-utils.ts b/src/identity/crypto-utils.ts index 0d469bc..add7743 100644 --- a/src/identity/crypto-utils.ts +++ b/src/identity/crypto-utils.ts @@ -1,9 +1,3 @@ -import { - ProjectivePoint as Point, - getPublicKey, - getSharedSecret as nobleGetSharedSecret, - etc, -} from '@noble/secp256k1'; import { secp256k1 } from '@noble/curves/secp256k1'; import bs58 from 'bs58'; import { PUBLIC_KEY_PREFIXES } from './constants.js'; @@ -17,6 +11,22 @@ import { } 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) { if (length === undefined) { @@ -110,7 +120,7 @@ export const keygen = (seed?: string | Uint8Array): KeyPair => { return { seedHex, private: privateKey, - public: getPublicKey(privateKey, true /* isCompressed */), + public: secp256k1.getPublicKey(privateKey, true /* isCompressed */), }; }; @@ -240,7 +250,7 @@ export const encryptChatMessage = ( recipientPublicKeyBase58Check ); const sharedPrivateKey = getSharedPrivateKey(privateKey, recipientPublicKey); - const sharedPublicKey = getPublicKey(sharedPrivateKey); + const sharedPublicKey = secp256k1.getPublicKey(sharedPrivateKey); return encrypt(sharedPublicKey, message); }; @@ -255,7 +265,7 @@ export const encrypt = async ( plaintext: string ): Promise => { const ephemPrivateKey = randomBytes(32); - const ephemPublicKey = getPublicKey(ephemPrivateKey); + const ephemPublicKey = secp256k1.getPublicKey(ephemPrivateKey); const publicKeyBytes = typeof publicKey === 'string' ? bs58PublicKeyToBytes(publicKey) : publicKey; const privKey = getSharedPrivateKey(ephemPrivateKey, publicKeyBytes); @@ -280,7 +290,7 @@ export const encrypt = async ( bytes ); - const hmacSha256 = await etc.hmacSha256Async( + const hmacSha256 = await hmacSha256Async( macKey, new Uint8Array([...iv, ...new Uint8Array(cipherBytes)]) ); @@ -300,7 +310,9 @@ export const bs58PublicKeyToCompressedBytes = (str: string) => { return new Uint8Array(33); } const pubKeyUncompressed = bs58PublicKeyToBytes(str); - return Point.fromHex(bytesToHex(pubKeyUncompressed)).toRawBytes(true); + return secp256k1.ProjectivePoint.fromHex( + bytesToHex(pubKeyUncompressed) + ).toRawBytes(true); }; export const bs58PublicKeyToBytes = (str: string) => { @@ -318,7 +330,9 @@ export const bs58PublicKeyToBytes = (str: string) => { throw new Error('Invalid checksum'); } - return Point.fromHex(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}$/; @@ -387,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 etc.hmacSha256Async(macKey, cipherAndIv); + const hmacKnownGood = await hmacSha256Async(macKey, cipherAndIv); if (!isValidHmac(msgMac, hmacKnownGood)) { throw new Error('incorrect MAC'); @@ -424,14 +438,14 @@ export const decodePublicKey = async (publicKeyBase58Check: string) => { const withPrefixRemoved = decoded.slice(3); 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/identity.spec.ts b/src/identity/identity.spec.ts index 2c41bbe..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,10 +20,10 @@ import { TransactionNonce, } from './transaction-transcoders.js'; import { APIProvider, AsyncStorage } from './types.js'; -import { bytesToHex, hexToBytes } from '@noble/hashes/utils.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(bytesToHex(publicKey), 'raw', 'pem'); } diff --git a/src/identity/identity.ts b/src/identity/identity.ts index fa251f5..931ce53 100644 --- a/src/identity/identity.ts +++ b/src/identity/identity.ts @@ -1,6 +1,6 @@ import { keccak_256 } from '@noble/hashes/sha3'; import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; -import { ProjectivePoint as Point } from '@noble/secp256k1'; +import { secp256k1 } from '@noble/curves/secp256k1'; import { ethers } from 'ethers'; import { AccessGroupEntryResponse, @@ -1420,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 }); } diff --git a/src/identity/noble-elliptic-compat-test.spec.ts b/src/identity/noble-elliptic-compat-test.spec.ts index bbd8eaa..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 { getPublicKey } 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 = getPublicKey(seedHex, true); + const noblePubKeyCompressed = secp256k1.getPublicKey(seedHex, true); const nobleDesoPubKey = new Uint8Array([ ...desoMainNetPrefix, ...noblePubKeyCompressed,