From eb96cbc935e4e7a76969e16c544674e0cb3b434b Mon Sep 17 00:00:00 2001 From: Hiroaki KAWAI Date: Fri, 20 Oct 2023 14:41:52 +0900 Subject: [PATCH] Add support for compressed secp256k1 publicKey (#567) * Add compressed SECP256K1 publicKey test for eciesSecp256k1Encrypt() Signed-off-by: Hiroaki KAWAI * Add compressed SECP256K1 ephemeral publicKey support in encrypted message. Signed-off-by: Hiroaki KAWAI * Convert SECP256K1 public key to compressed from uncompressed. Signed-off-by: Hiroaki KAWAI * npm install; lint and formatting; remove isCompressed option * Nit --------- Signed-off-by: Hiroaki KAWAI Co-authored-by: Diane Huxley --- README.md | 2 +- package-lock.json | 39 ++++++++++++++++++++++------------ package.json | 2 +- src/utils/encryption.ts | 31 +++++++++++++++++++++------ src/utils/secp256k1.ts | 17 ++++++++------- tests/utils/encryption.spec.ts | 16 ++++++++++++++ tests/utils/secp256k1.spec.ts | 7 ++++++ 7 files changed, 85 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 18fc3fcfb..159aae76d 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Here's to a thrilling Hacktoberfest voyage with us! 🎉 # Decentralized Web Node (DWN) SDK Code Coverage -![Statements](https://img.shields.io/badge/statements-98.47%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-95.61%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-95.68%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-98.47%25-brightgreen.svg?style=flat) +![Statements](https://img.shields.io/badge/statements-98.47%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-95.55%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-95.7%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-98.47%25-brightgreen.svg?style=flat) - [Introduction](#introduction) - [Installation](#installation) diff --git a/package-lock.json b/package-lock.json index 989e926f6..c2dd7b1bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "ajv": "8.12.0", "blockstore-core": "4.2.0", "cross-fetch": "4.0.0", - "eciesjs": "0.4.0", + "eciesjs": "0.4.5", "flat": "5.0.2", "interface-blockstore": "5.2.3", "interface-store": "5.1.2", @@ -75,7 +75,7 @@ "ms": "2.1.3", "node-stdlib-browser": "1.2.0", "playwright": "1.29.2", - "rimraf": "3.0.2", + "rimraf": "^3.0.2", "search-index": "3.4.0", "sinon": "13.0.1", "ts-sinon": "^2.0.2", @@ -695,12 +695,20 @@ "npm": ">=7.0.0" } }, + "node_modules/@noble/ciphers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.3.0.tgz", + "integrity": "sha512-ldbrnOjmNRwFdXcTM6uXDcxpMIFrbzAWNnpBPp4oTJTFF0XByGD6vf45WrehZGXRQTRVV+Zm8YP+EgEf+e4cWA==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/curves": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", - "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", "dependencies": { - "@noble/hashes": "1.3.1" + "@noble/hashes": "1.3.2" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -718,9 +726,9 @@ ] }, "node_modules/@noble/hashes": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", - "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", "engines": { "node": ">= 16" }, @@ -2468,11 +2476,16 @@ } }, "node_modules/eciesjs": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.0.tgz", - "integrity": "sha512-z4dEeaH16xxYVgtxJ8YVwpifH4Keg4gyp5F451mnDNwbAN3MgL5jcoEQGpqJrapv/zW8KwDnXG21Dw5B0hqvmw==", + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.5.tgz", + "integrity": "sha512-2zSRIygO48LpdS95Rwt9ryIkJNO37IdbkjRsnYyAn7gx7e4WPBNimnk6jGNdx2QQYr/VJRPnSVdwQpO5bycYZw==", "dependencies": { - "@noble/curves": "^1.1.0" + "@noble/ciphers": "^0.3.0", + "@noble/curves": "^1.2.0", + "@noble/hashes": "^1.3.2" + }, + "engines": { + "node": ">=16.0.0" } }, "node_modules/ee-first": { diff --git a/package.json b/package.json index 0e6addda8..c51890759 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "ajv": "8.12.0", "blockstore-core": "4.2.0", "cross-fetch": "4.0.0", - "eciesjs": "0.4.0", + "eciesjs": "0.4.5", "flat": "5.0.2", "interface-blockstore": "5.2.3", "interface-store": "5.1.2", diff --git a/src/utils/encryption.ts b/src/utils/encryption.ts index 0327d87fc..ca1d4771d 100644 --- a/src/utils/encryption.ts +++ b/src/utils/encryption.ts @@ -2,6 +2,9 @@ import * as crypto from 'crypto'; import * as eciesjs from 'eciesjs'; import { Readable } from 'readable-stream'; +// compress publicKey for message encryption +eciesjs.ECIES_CONFIG.isEphemeralKeyCompressed = true; + /** * Utility class for performing common, non-DWN specific encryption operations. */ @@ -67,18 +70,27 @@ export class Encryption { * with SECP256K1 for the asymmetric calculations, HKDF as the key-derivation function, * and AES-GCM for the symmetric encryption and MAC algorithms. */ - public static async eciesSecp256k1Encrypt(uncompressedPublicKey: Uint8Array, plaintext: Uint8Array): Promise { + public static async eciesSecp256k1Encrypt(publicKeyBytes: Uint8Array, plaintext: Uint8Array): Promise { // underlying library requires Buffer as input - const publicKey = Buffer.from(uncompressedPublicKey); + const publicKey = Buffer.from(publicKeyBytes); const plaintextBuffer = Buffer.from(plaintext); const cryptogram = eciesjs.encrypt(publicKey, plaintextBuffer); // split cryptogram returned into constituent parts - const ephemeralPublicKey = cryptogram.subarray(0, 65); - const initializationVector = cryptogram.subarray(65, 81); - const messageAuthenticationCode = cryptogram.subarray(81, 97); - const ciphertext = cryptogram.subarray(97); + let start = 0; + let end = Encryption.isEphemeralKeyCompressed ? 33 : 65; + const ephemeralPublicKey = cryptogram.subarray(start, end); + + start = end; + end += eciesjs.ECIES_CONFIG.symmetricNonceLength; + const initializationVector = cryptogram.subarray(start, end); + + start = end; + end += 16; // eciesjs.consts.AEAD_TAG_LENGTH + const messageAuthenticationCode = cryptogram.subarray(start, end); + + const ciphertext = cryptogram.subarray(end); return { ciphertext, @@ -107,6 +119,13 @@ export class Encryption { return plaintext; } + + /** + * Expose eciesjs library configuration + */ + static get isEphemeralKeyCompressed():boolean { + return eciesjs.ECIES_CONFIG.isEphemeralKeyCompressed; + } } export type EciesEncryptionOutput = { diff --git a/src/utils/secp256k1.ts b/src/utils/secp256k1.ts index 84b019ac3..fed6c2f2e 100644 --- a/src/utils/secp256k1.ts +++ b/src/utils/secp256k1.ts @@ -67,15 +67,16 @@ export class Secp256k1 { } /** - * Creates a uncompressed key in raw bytes from the given SECP256K1 JWK. + * Creates a compressed key in raw bytes from the given SECP256K1 JWK. */ public static publicJwkToBytes(publicJwk: PublicJwk): Uint8Array { const x = Encoder.base64UrlToBytes(publicJwk.x); const y = Encoder.base64UrlToBytes(publicJwk.y!); - // leading byte of 0x04 indicates that the public key is uncompressed - const publicKey = new Uint8Array([0x04, ...x, ...y]); - return publicKey; + return secp256k1.ProjectivePoint.fromAffine({ + x : secp256k1.etc.bytesToNumberBE(x), + y : secp256k1.etc.bytesToNumberBE(y) + }).toRawBytes(true); } /** @@ -129,20 +130,20 @@ export class Secp256k1 { } /** - * Generates key pair in raw bytes, where the `publicKey` is uncompressed. + * Generates key pair in raw bytes, where the `publicKey` is compressed. */ public static async generateKeyPairRaw(): Promise<{publicKey: Uint8Array, privateKey: Uint8Array}> { const privateKey = secp256k1.utils.randomPrivateKey(); - const publicKey = secp256k1.getPublicKey(privateKey, false); // `false` = uncompressed + const publicKey = secp256k1.getPublicKey(privateKey, true); // `true` = compressed return { publicKey, privateKey }; } /** - * Gets the uncompressed public key of the given private key. + * Gets the compressed public key of the given private key. */ public static async getPublicKey(privateKey: Uint8Array): Promise { - const publicKey = secp256k1.getPublicKey(privateKey, false); // `false` = uncompressed + const publicKey = secp256k1.getPublicKey(privateKey, true); // `true` = compressed return publicKey; } diff --git a/tests/utils/encryption.spec.ts b/tests/utils/encryption.spec.ts index f6341cea7..a7b2930a9 100644 --- a/tests/utils/encryption.spec.ts +++ b/tests/utils/encryption.spec.ts @@ -4,6 +4,7 @@ import { Encryption } from '../../src/utils/encryption.js'; import { expect } from 'chai'; import { Readable } from 'readable-stream'; import { Secp256k1 } from '../../src/utils/secp256k1.js'; +import { etc as Secp256k1Etc } from '@noble/secp256k1'; import { TestDataGenerator } from './test-data-generator.js'; describe('Encryption', () => { @@ -117,6 +118,21 @@ describe('Encryption', () => { expect(ArrayUtility.byteArraysEqual(originalPlaintext, decryptedPlaintext)).to.be.true; }); + + it('should be able to accept both compressed and uncompressed publicKeys', async () => { + const originalPlaintext = TestDataGenerator.randomBytes(32); + const h2b = Secp256k1Etc.hexToBytes; + // Following test vector was taken from @noble/secp256k1 test file. + // noble-secp256k1/main/test/vectors/secp256k1/privates.json + const privateKey = h2b('9c7fc36bc106fd7df5e1078d03e34b9a045892abdd053ec69bfeb22327529f6c'); + const compressed = h2b('03936cb2bd56e681d360bbce6a3a7a1ccbf72f3ab8792edbc45fb08f55b929c588'); + const uncompressed = h2b('04936cb2bd56e681d360bbce6a3a7a1ccbf72f3ab8792edbc45fb08f55b929c588529b8cee53f7eff1da5fc0e6050d952b37d4de5c3b85e952dfe9d9e9b2b3b6eb'); + for (const publicKey of [compressed, uncompressed]) { + const encrypted = await Encryption.eciesSecp256k1Encrypt(publicKey, originalPlaintext); + const decrypted = await Encryption.eciesSecp256k1Decrypt({ privateKey, ...encrypted }); + expect(ArrayUtility.byteArraysEqual(originalPlaintext, decrypted)).to.be.true; + } + }); }); }); diff --git a/tests/utils/secp256k1.spec.ts b/tests/utils/secp256k1.spec.ts index 5451c4927..b400378a4 100644 --- a/tests/utils/secp256k1.spec.ts +++ b/tests/utils/secp256k1.spec.ts @@ -6,6 +6,13 @@ import { Secp256k1 } from '../../src/utils/secp256k1.js'; import { TestDataGenerator } from './test-data-generator.js'; describe('Secp256k1', () => { + describe('generateKeyPairRaw()', () => { + it('should generate compressed publicKey', async () => { + const { publicKey } = await Secp256k1.generateKeyPairRaw(); + expect(publicKey.length).to.equal(33); + }); + }); + describe('validateKey()', () => { it('should throw if key is not a valid SECP256K1 key', async () => { const validKey = (await Secp256k1.generateKeyPair()).publicJwk;