From 4de13d3d91f1132bad8a23e4111b1bad3f285834 Mon Sep 17 00:00:00 2001 From: Nicolas Brugneaux Date: Wed, 17 Jan 2024 18:59:47 +0100 Subject: [PATCH] fix: ecies module works --- packages/sdk/utils/src/ecies.test.ts | 68 ++++-------- packages/sdk/utils/src/ecies.ts | 156 ++++++++------------------- 2 files changed, 67 insertions(+), 157 deletions(-) diff --git a/packages/sdk/utils/src/ecies.test.ts b/packages/sdk/utils/src/ecies.test.ts index 872267799..692f2957a 100644 --- a/packages/sdk/utils/src/ecies.test.ts +++ b/packages/sdk/utils/src/ecies.test.ts @@ -1,78 +1,54 @@ -import { privateToPublic } from '@ethereumjs/util' -import { ctr } from '@noble/ciphers/aes' import { bytesToUtf8, utf8ToBytes } from '@noble/ciphers/utils' -import { randomBytes as randooom } from '@noble/ciphers/webcrypto/utils' -import { randomBytes } from 'crypto' +import { randomBytes } from '@noble/ciphers/webcrypto/utils' +import { secp256k1 } from '@noble/curves/secp256k1' import { ECIES } from './ecies' describe('ECIES', () => { describe('encrypt', () => { it('should encrypt a message without error', () => { const privKey = randomBytes(32) - const pubKey = privateToPublic(privKey) + const pubKey = secp256k1.getPublicKey(privKey, false).slice(1) const message = Buffer.from('foo') - const encrypted = ECIES.Encrypt(pubKey, message) - expect(encrypted.length).toBeGreaterThanOrEqual(113) - }) + expect(() => ECIES.Encrypt(pubKey, message)).not.toThrow() + }) it('should throw an error if priv key is given', () => { const privKey = randomBytes(32) const message = Buffer.from('foo') - try { - ECIES.Encrypt(privKey, message) - expect(false).toBe(true) - } catch (error) { - // ok, encryption should not work when a priv key is given - } + + expect(() => ECIES.Encrypt(privKey, message)).toThrow() }) }) describe('roundtrip', () => { - it('should return the same plaintext after roundtrip - core', () => { - // const plaintext = utf8ToBytes('spam') - const privKey = randooom(32) - // const pubKey = privateToPublic(privKey) - // const iv = randomBytes(16) - // const encrypted = AES128Encrypt(privKey, iv, plaintext) - // const decrypted = AES128Decrypt(privKey, iv, encrypted) - - const plaintext = 'Hello, World' - const aes = ctr(privKey, randomBytes(16)) - const ciphertext_ = aes.encrypt(utf8ToBytes(plaintext)) - const plaintext_ = aes.decrypt(ciphertext_) - expect(bytesToUtf8(plaintext_)).toEqual(plaintext) - }) - it('should return the same plaintext after roundtrip', () => { - const plaintext = Buffer.from('spam') + const plaintext = 'spam' const privKey = randomBytes(32) - const pubKey = privateToPublic(privKey) - const encrypted = ECIES.Encrypt(pubKey, plaintext) - const decrypted = ECIES.Decrypt(privKey, encrypted) - expect(decrypted.toString()).toEqual(plaintext.toString()) + const pubKey = secp256k1.getPublicKey(privKey, false).slice(1) + const encrypted = ECIES.Encrypt(pubKey, utf8ToBytes(plaintext)) + const decrypted = ECIES.Decrypt(Buffer.from(privKey), encrypted) + + expect(bytesToUtf8(decrypted)).toEqual(plaintext) }) it('should only decrypt if correct priv key is given', () => { const plaintext = Buffer.from('spam') const privKey = randomBytes(32) - const pubKey = privateToPublic(privKey) + const pubKey = secp256k1.getPublicKey(privKey, false).slice(1) const fakePrivKey = randomBytes(32) - try { - ECIES.Encrypt(pubKey, plaintext) - ECIES.Decrypt(fakePrivKey, plaintext) - expect(false).toBe(true) - } catch (error) { - // ok, decryption should not work for incorrect priv key - } + const encrypted = ECIES.Encrypt(pubKey, plaintext) + + expect(() => ECIES.Decrypt(fakePrivKey, encrypted)).toThrow() }) it('should be able to encrypt and decrypt a longer message (1024 bytes)', () => { const plaintext = randomBytes(1024) const privKey = randomBytes(32) - const pubKey = privateToPublic(privKey) + const pubKey = secp256k1.getPublicKey(privKey, false).slice(1) const encrypted = ECIES.Encrypt(pubKey, plaintext) const decrypted = ECIES.Decrypt(privKey, encrypted) - expect(decrypted.toString()).toEqual(plaintext.toString()) + + expect(decrypted).toEqual(plaintext) }) }) }) @@ -95,7 +71,7 @@ describe('AES128CTR', () => { const macKey = randomBytes(16) const encrypted = ECIES.AES128EncryptAndHMAC(encKey, macKey, plaintext) const decrypted = ECIES.AES128DecryptAndHMAC(encKey, macKey, encrypted) - expect(decrypted.toString()).toEqual(plaintext.toString()) + expect(bytesToUtf8(decrypted)).toEqual(plaintext.toString()) }) it('should only decrypt if correct priv key is given', () => { @@ -115,7 +91,7 @@ describe('AES128CTR', () => { const macKey = randomBytes(16) const encrypted = ECIES.AES128EncryptAndHMAC(encKey, macKey, plaintext) const decrypted = ECIES.AES128DecryptAndHMAC(encKey, macKey, encrypted) - expect(decrypted.toString()).toEqual(plaintext.toString()) + expect(decrypted).toEqual(plaintext) }) }) diff --git a/packages/sdk/utils/src/ecies.ts b/packages/sdk/utils/src/ecies.ts index f5ce33733..2ef20abdd 100644 --- a/packages/sdk/utils/src/ecies.ts +++ b/packages/sdk/utils/src/ecies.ts @@ -7,109 +7,61 @@ 'use strict' import { ctr } from '@noble/ciphers/aes' +import { u8 } from '@noble/ciphers/utils' +import { PrivKey } from '@noble/curves/abstract/utils' +import { PubKey } from '@noble/curves/abstract/weierstrass' import { secp256k1 } from '@noble/curves/secp256k1' +import { hkdf } from '@noble/hashes/hkdf' import { hmac } from '@noble/hashes/hmac' import { sha256 } from '@noble/hashes/sha256' import { randomBytes } from '@noble/hashes/utils' export const IV_LENGTH = 16 -const Uint8ArrayEquals = (a: Uint8Array, b: Uint8Array) => { - if (a.length != b.length) return false - for (let i = 0; i < a.length; i++) { - if (a[i] != b[i]) return false - } - return true -} - -/** - * 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. - */ -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 {Buffer} encryptionKey - * @param {Buffer} iv + * @param {Uint8Array} encryptionKey + * @param {Uint8Array} iv * @param {Uint8Array} plaintext * @returns {Uint8Array} ciphertext */ export function AES128Encrypt( - encryptionKey: Buffer, + encryptionKey: Uint8Array, iv: Uint8Array, plaintext: Uint8Array ): Uint8Array { const aes = ctr(encryptionKey, iv) - return aes.encrypt(plaintext) + const message = aes.encrypt(plaintext) + return u8(Buffer.concat([iv, message])) } /** * AES-128 CTR encrypt with message authentication - * @param {Buffer} encryptionKey - * @param {Buffer} macKey + * @param {Uint8Array} encryptionKey + * @param {Uint8Array} macKey * @param {Uint8Array} plaintext - * @returns {Buffer} ciphertext + * @returns {Uint8Array} ciphertext */ export function AES128EncryptAndHMAC( - encryptionKey: Buffer, + encryptionKey: Uint8Array, macKey: Uint8Array, plaintext: Uint8Array -): Buffer { +): Uint8Array { const iv = randomBytes(IV_LENGTH) - console.log('RANDOM IV:', iv) const dataToMac = AES128Encrypt(encryptionKey, iv, plaintext) const mac = hmac(sha256, macKey, dataToMac) - - return Buffer.concat([dataToMac, mac]) + return u8(Buffer.concat([dataToMac, mac])) } /** * AES-128 CTR decrypt - * @param {Buffer} encryptionKey + * @param {Uint8Array} encryptionKey * @param {Uint8Array} iv * @param {Uint8Array} ciphertext * @returns {Uint8Array} plaintext */ export function AES128Decrypt( - encryptionKey: Buffer, + encryptionKey: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array ): Uint8Array { @@ -119,71 +71,54 @@ export function AES128Decrypt( /** * AES-128 CTR decrypt with message authentication - * @param {Buffer} encryptionKey - * @param {Buffer} macKey + * @param {Uint8Array} encryptionKey + * @param {Uint8Array} macKey * @param {Uint8Array} ciphertext * @returns {Uint8Array} plaintext */ export function AES128DecryptAndHMAC( - encryptionKey: Buffer, + encryptionKey: Uint8Array, macKey: Uint8Array, ciphertext: Uint8Array ): Uint8Array { - const iv = ciphertext.subarray(0, IV_LENGTH) - console.log('SLICED FROM CIPHER IV:', iv) - const message = ciphertext.slice(IV_LENGTH, ciphertext.length - 32) - const mac = ciphertext.slice(ciphertext.length - 32, ciphertext.length) + const iv = ciphertext.slice(0, IV_LENGTH) + const message = ciphertext.slice(IV_LENGTH, ciphertext.length - sha256.outputLen) + const mac = ciphertext.slice(ciphertext.length - sha256.outputLen, ciphertext.length) const dataToMac = Buffer.concat([iv, message]) const computedMac = hmac(sha256, macKey, dataToMac) - if (!Uint8ArrayEquals(mac, computedMac)) { + if (!Buffer.from(mac).equals(Buffer.from(computedMac))) { throw new Error('MAC mismatch') } - - return Buffer.from(AES128Decrypt(encryptionKey, iv, message)) + return AES128Decrypt(encryptionKey, iv, message) } +const COMPRESSED_KEY_LENGTH = secp256k1.CURVE.Fp.BYTES + 1 // e.g. 33 for 32 /** * ECIES encrypt * @param {Buffer} pubKeyTo Ethereum pub key, 64 bytes. * @param {Uint8Array} plaintext Plaintext to be encrypted. * @returns {Buffer} Encrypted message, serialized, 113+ bytes */ -export function Encrypt(pubKeyTo: Buffer, plaintext: Uint8Array) { +export function Encrypt(pubKeyTo: PubKey, plaintext: Uint8Array) { const ephemPrivKey = secp256k1.utils.randomPrivateKey() - const ephemPubKey = Buffer.from(secp256k1.getPublicKey(ephemPrivKey, false)) + const ephemPubKey = Buffer.from(secp256k1.getPublicKey(ephemPrivKey)) const ephemPubKeyEncoded = Buffer.from(ephemPubKey) - const pubKeyToEncoded = Buffer.concat([Buffer.from([0x04]), pubKeyTo]) - const px = secp256k1.getSharedSecret(ephemPrivKey, pubKeyToEncoded) - - // TODO: remove after I find a way to test this better - // const EC = require('elliptic').ec - // const ec = new EC('secp256k1') - // const _ephemPrivKey = ec.keyFromPrivate(ephemPrivKey) - // const _ephemPubKey = _ephemPrivKey.getPublic(false, 'hex') - // const _ephemPubKeyEncoded = Buffer.from(_ephemPubKey, 'hex') - // const _px = _ephemPrivKey.derive(ec.keyFromPublic(pubKeyToEncoded).getPublic()) + if (typeof pubKeyTo === 'string') { + pubKeyTo = secp256k1.ProjectivePoint.fromHex(pubKeyTo).toRawBytes() + } - // console.log({ - // ephemPubKeyEncoded, - // _ephemPubKeyEncoded, - // eq: ephemPubKeyEncoded.equals(_ephemPubKeyEncoded), - // }) - // console.log({ - // px: Buffer.from(px).subarray(1), // remove 0x04 prefix - // _px: _px.toArrayLike(Buffer), - // eq: Buffer.from(px).subarray(1).equals(_px.toArrayLike(Buffer)), - // }) + const pubKeyToEncoded = Buffer.concat([Buffer.from([0x04]), pubKeyTo as Buffer]) + const px = secp256k1.getSharedSecret(ephemPrivKey, pubKeyToEncoded).slice(1) - // better TODO: don't do handcrafted encryption?????? - const unprefixedPx = Buffer.from(px).subarray(1) // remove 0x04 prefix - const hash = ConcatKDF(unprefixedPx, 32) - const encryptionKey = hash.subarray(0, IV_LENGTH) + const hash = hkdf(sha256, px, undefined, undefined, 32) + const encryptionKey = hash.subarray(0, 16) const macKey = sha256.create().update(hash.subarray(16)).digest() - const message = AES128EncryptAndHMAC(encryptionKey, macKey, plaintext) + const message = AES128EncryptAndHMAC(Buffer.from(encryptionKey), macKey, plaintext) const serializedCiphertext = Buffer.concat([ - ephemPubKeyEncoded, // 65 bytes + ephemPubKeyEncoded, // {COMPRESSED_KEY_LENGTH} bytes message, // iv + ciphertext + mac (min 48 bytes) ]) + return serializedCiphertext } @@ -191,22 +126,21 @@ export function Encrypt(pubKeyTo: Buffer, plaintext: Uint8Array) { * ECIES decrypt * @param {Buffer} privKey Ethereum private key, 32 bytes. * @param {Buffer} encrypted Encrypted message, serialized, 113+ bytes - * @returns {Uint8Array} plaintext + * @returns {Buffer} plaintext */ -export function Decrypt(privKey: Buffer, encrypted: Buffer) { +export function Decrypt(privKey: PrivKey, encrypted: Buffer) { // Read iv, ephemPubKey, mac, ciphertext from encrypted message - const ephemPubKeyEncoded = encrypted.subarray(0, 65) - const symmetricEncrypted = encrypted.subarray(65) + const ephemPubKeyEncoded = u8(encrypted).slice(0, COMPRESSED_KEY_LENGTH) + const symmetricEncrypted = u8(encrypted).slice(COMPRESSED_KEY_LENGTH) - const px = secp256k1.getSharedSecret(privKey, ephemPubKeyEncoded) - const unprefixedPx = Buffer.from(px).subarray(1) // remove 0x04 prefix - const hash = ConcatKDF(unprefixedPx, 32) + const px = secp256k1.getSharedSecret(privKey, ephemPubKeyEncoded).slice(1) + const hash = hkdf(sha256, px, undefined, undefined, 32) // km, ke const encryptionKey = hash.subarray(0, 16) const macKey = sha256.create().update(hash.subarray(16)).digest() - return AES128DecryptAndHMAC(encryptionKey, macKey, symmetricEncrypted) + return AES128DecryptAndHMAC(Buffer.from(encryptionKey), macKey, symmetricEncrypted) } export const ECIES = {