diff --git a/packages/sdk/cryptographic-utils/src/dataEncryptionKey.ts b/packages/sdk/cryptographic-utils/src/dataEncryptionKey.ts index 37f12697c..6efd601e3 100644 --- a/packages/sdk/cryptographic-utils/src/dataEncryptionKey.ts +++ b/packages/sdk/cryptographic-utils/src/dataEncryptionKey.ts @@ -10,7 +10,7 @@ import { Bip39, generateKeys } from './account' * @returns {string} Corresponding compressed public key in hex encoding with '0x' leader. */ export function compressedPubKey(privateKey: Buffer): string { - return ensureLeading0x(bytesToHex(secp256k1.getPublicKey(privateKey))) + return ensureLeading0x(bytesToHex(secp256k1.getPublicKey(privateKey))).slice(1) } /** @@ -21,7 +21,7 @@ export function compressedPubKey(privateKey: Buffer): string { * @returns Decompresssed public key without prefix. */ export function decompressPublicKey(publicKey: Buffer): Buffer { - return Buffer.from(secp256k1.getSharedSecret(BigInt(1), publicKey, false)) + return Buffer.from(secp256k1.getSharedSecret(BigInt(1), publicKey, false)).slice(1) } /** diff --git a/packages/sdk/utils/package.json b/packages/sdk/utils/package.json index 8679d9a7d..1b710fb01 100644 --- a/packages/sdk/utils/package.json +++ b/packages/sdk/utils/package.json @@ -31,13 +31,13 @@ "@types/node": "^18.7.16", "bignumber.js": "^9.0.0", "bn.js": "4.11.9", + "elliptic": "^6.5.4", "ethereum-cryptography": "1.2.0", "fp-ts": "2.1.1", "io-ts": "2.0.1", "rlp": "^2.2.4", "web3-eth-abi": "1.10.0", - "web3-utils": "1.10.0", - "elliptic": "^6.5.4" + "web3-utils": "1.10.0" }, "devDependencies": { "@celo/typescript": "0.0.1" diff --git a/packages/sdk/utils/src/ecies.test.ts b/packages/sdk/utils/src/ecies.test.ts index 78be51e49..4f2b38238 100644 --- a/packages/sdk/utils/src/ecies.test.ts +++ b/packages/sdk/utils/src/ecies.test.ts @@ -1,4 +1,4 @@ -import { bytesToUtf8, utf8ToBytes } from '@noble/ciphers/utils' +import { bytesToUtf8, u8, utf8ToBytes } from '@noble/ciphers/utils' import { randomBytes } from '@noble/ciphers/webcrypto/utils' import { secp256k1 } from '@noble/curves/secp256k1' import { ECIES } from './ecies' @@ -20,15 +20,20 @@ describe('ECIES', () => { }) it('should not regress', () => { - const snapshotPrivKey = Buffer.from( - 'f353837781491b9ded31b6cb669c867e4c91f0ccfdaa85db4b1f0a814bc060c5', - 'hex' + // snapshot generated on master at commit=4861e71d0 + // with message='foo' + // and privkey='0xd2a515a64d37407f0e0e4a6a6a69a95eeb5ef8c2524ef01a6ffc6e3b39e0661b' + const snapshotPrivKey = u8( + Buffer.from('f353837781491b9ded31b6cb669c867e4c91f0ccfdaa85db4b1f0a814bc060c5', 'hex') ) - const pubKey = privateToPublic(snapshotPrivKey) - const message = Buffer.from('foo') - expect(ECIES.Encrypt(pubKey, message).toString('hex')).toMatchInlineSnapshot( - `"04f8a3e50423af34892f3a64bfa37b8c68bf4b378a5029964c563d17ab66a99bc75a101b30b95e64267219305eb5c8f65fd3b1081cfa7f549638a430d0712a58669b90d413329c70d1159664e73e1477a8a4151de90f94abeafe52cfbfb37e414df35f16a172b7d1c6b1a14d6559b7657e446f9e"` + const snapshotEncrypted = Buffer.from( + '04f8a3e50423af34892f3a64bfa37b8c68bf4b378a5029964c563d17ab66a99bc75a101b30b95e64267219305eb5c8f65fd3b1081cfa7f549638a430d0712a58669b90d413329c70d1159664e73e1477a8a4151de90f94abeafe52cfbfb37e414df35f16a172b7d1c6b1a14d6559b7657e446f9e', + 'hex' ) + // secp256k1.getPublicKey(snapshotPrivKey).slice(1) + + // const _message = Buffer.from('foo') + expect(ECIES.Decrypt(snapshotPrivKey, snapshotEncrypted).toString()).toEqual('foo') }) }) diff --git a/packages/sdk/utils/src/ecies.ts b/packages/sdk/utils/src/ecies.ts index 2ef20abdd..1776ca32e 100644 --- a/packages/sdk/utils/src/ecies.ts +++ b/packages/sdk/utils/src/ecies.ts @@ -18,6 +18,49 @@ import { randomBytes } from '@noble/hashes/utils' export const IV_LENGTH = 16 +/** + * Increments big endian uint32 + * + * @param {Buffer} ctr 32 bit unsigned big endian integer to increment. + * @returns Incremented counter. + */ +const IncCounter = (ctr: Buffer) => { + for (let i = ctr.length - 1; i >= 0; i--) { + ctr[i]++ + if (ctr[i] !== 0) { + return ctr + } + } + return ctr +} + +/** + * NIST 8000-56C Rev 1 One Step KDF with the following parameters: + * - H(x) is SHA-256(x) + * - Fixed info is null + * + * TODO: + * - Implement proper ceiling on reps. + * + * @param {Buffer} px Input keying material to derive key from. + * @param {number} kdLen Length of output in bytes + * @returns {Buffer} Output keying material of length kdLen bytes. + */ +export const ConcatKDF = (px: Buffer, kdLen: number) => { + const blockSize = 32 + const reps = ((kdLen + 7) * 8) / (blockSize * 8) + let counter = Buffer.from('00000001', 'hex') + let k = Buffer.from('00', 'hex') + for (let i = 0; i <= reps; i++) { + const hash = sha256.create() + hash.update(counter) + hash.update(px) + k = Buffer.concat([k, hash.digest()]) + counter = IncCounter(counter) + } + return k.slice(1, kdLen + 1) +} + /** * AES-128 CTR encrypt * @param {Uint8Array} encryptionKey @@ -92,7 +135,7 @@ export function AES128DecryptAndHMAC( return AES128Decrypt(encryptionKey, iv, message) } -const COMPRESSED_KEY_LENGTH = secp256k1.CURVE.Fp.BYTES + 1 // e.g. 33 for 32 +const UNCOMPRESSED_KEY_LENGTH = 65 /** * ECIES encrypt * @param {Buffer} pubKeyTo Ethereum pub key, 64 bytes. @@ -101,21 +144,22 @@ const COMPRESSED_KEY_LENGTH = secp256k1.CURVE.Fp.BYTES + 1 // e.g. 33 for 32 */ export function Encrypt(pubKeyTo: PubKey, plaintext: Uint8Array) { const ephemPrivKey = secp256k1.utils.randomPrivateKey() - const ephemPubKey = Buffer.from(secp256k1.getPublicKey(ephemPrivKey)) + const ephemPubKey = Buffer.from(secp256k1.getPublicKey(ephemPrivKey, false)) const ephemPubKeyEncoded = Buffer.from(ephemPubKey) if (typeof pubKeyTo === 'string') { pubKeyTo = secp256k1.ProjectivePoint.fromHex(pubKeyTo).toRawBytes() } const pubKeyToEncoded = Buffer.concat([Buffer.from([0x04]), pubKeyTo as Buffer]) - const px = secp256k1.getSharedSecret(ephemPrivKey, pubKeyToEncoded).slice(1) + const px = secp256k1.getSharedSecret(ephemPrivKey, pubKeyToEncoded, false).slice(1) const hash = hkdf(sha256, px, undefined, undefined, 32) + // const hash = ConcatKDF(Buffer.from(px), 32) const encryptionKey = hash.subarray(0, 16) const macKey = sha256.create().update(hash.subarray(16)).digest() const message = AES128EncryptAndHMAC(Buffer.from(encryptionKey), macKey, plaintext) const serializedCiphertext = Buffer.concat([ - ephemPubKeyEncoded, // {COMPRESSED_KEY_LENGTH} bytes + ephemPubKeyEncoded, // {UNCOMPRESSED_KEY_LENGTH} bytes message, // iv + ciphertext + mac (min 48 bytes) ]) @@ -130,11 +174,12 @@ export function Encrypt(pubKeyTo: PubKey, plaintext: Uint8Array) { */ export function Decrypt(privKey: PrivKey, encrypted: Buffer) { // Read iv, ephemPubKey, mac, ciphertext from encrypted message - const ephemPubKeyEncoded = u8(encrypted).slice(0, COMPRESSED_KEY_LENGTH) - const symmetricEncrypted = u8(encrypted).slice(COMPRESSED_KEY_LENGTH) + const ephemPubKeyEncoded = u8(encrypted).slice(0, UNCOMPRESSED_KEY_LENGTH) + const symmetricEncrypted = u8(encrypted).slice(UNCOMPRESSED_KEY_LENGTH) - const px = secp256k1.getSharedSecret(privKey, ephemPubKeyEncoded).slice(1) + const px = secp256k1.getSharedSecret(privKey, ephemPubKeyEncoded, false).slice(1) const hash = hkdf(sha256, px, undefined, undefined, 32) + // const hash = ConcatKDF(Buffer.from(px), 32) // km, ke const encryptionKey = hash.subarray(0, 16) diff --git a/yarn.lock b/yarn.lock index a51742ffa..bb6986ab0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1893,6 +1893,7 @@ __metadata: "@types/node": "npm:^18.7.16" bignumber.js: "npm:^9.0.0" bn.js: "npm:4.11.9" + elliptic: "npm:^6.5.4" ethereum-cryptography: "npm:1.2.0" fp-ts: "npm:2.1.1" io-ts: "npm:2.0.1"