diff --git a/package-lock.json b/package-lock.json index c2ec79454..281788ac2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@noble/curves": "1.4.2", "@noble/ed25519": "2.0.0", "@noble/secp256k1": "2.0.0", + "@web5/crypto": "^1.0.5", "@web5/dids": "^1.1.3", "abstract-level": "1.0.3", "ajv": "8.12.0", @@ -1249,14 +1250,14 @@ } }, "node_modules/@web5/crypto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@web5/crypto/-/crypto-1.0.3.tgz", - "integrity": "sha512-gZJKo0scX+L53E2K/5cgEiFYxejzHP2RSg64ncF6TitOnCNxUyWjofovgufb+u3ZpGC4iuliD7V0o1C+V73Law==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@web5/crypto/-/crypto-1.0.5.tgz", + "integrity": "sha512-btVYrv6G29VQgdLVtqH4IbaH6DvvEA2UWgF3y0AMv/wAmT4EKSdwVUHhEWt16gtpN05UT5qEYmnKLZUOxk1zzw==", "dependencies": { "@noble/ciphers": "0.5.3", "@noble/curves": "1.3.0", "@noble/hashes": "1.4.0", - "@web5/common": "1.0.1" + "@web5/common": "1.0.2" }, "engines": { "node": ">=18.0.0" @@ -1296,14 +1297,14 @@ } }, "node_modules/@web5/crypto/node_modules/@web5/common": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@web5/common/-/common-1.0.1.tgz", - "integrity": "sha512-dxczXqzWt6HCwuNyOVBeakg6GgOpP74tVEVxBeKkb+D3XcSP96mYaDtky5ZnjY4iBYb16SaCgwje+sgevOL51A==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@web5/common/-/common-1.0.2.tgz", + "integrity": "sha512-SerGdrxZF47yidvhrRa8sGLEOunIlDHppxrtWYCuKMVgtQKgheEmaS4+xchGAc/mZggJX4LlwJbRuniIiSaXrw==", "dependencies": { "@isaacs/ttlcache": "1.4.1", "level": "8.0.1", "multiformats": "13.1.0", - "readable-stream": "4.4.2" + "readable-stream": "4.5.2" }, "engines": { "node": ">=18.0.0" @@ -1348,21 +1349,6 @@ "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.1.0.tgz", "integrity": "sha512-HzdtdBwxsIkzpeXzhQ5mAhhuxcHbjEHH+JQoxt7hG/2HGFjjwyolLo7hbaexcnhoEuV4e0TNJ8kkpMjiEYY4VQ==" }, - "node_modules/@web5/crypto/node_modules/readable-stream": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", - "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, "node_modules/@web5/dids": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@web5/dids/-/dids-1.1.3.tgz", @@ -1382,6 +1368,56 @@ "node": ">=18.0.0" } }, + "node_modules/@web5/dids/node_modules/@noble/curves": { + "version": "1.3.0", + "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/@web5/dids/node_modules/@web5/crypto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@web5/crypto/-/crypto-1.0.3.tgz", + "integrity": "sha512-gZJKo0scX+L53E2K/5cgEiFYxejzHP2RSg64ncF6TitOnCNxUyWjofovgufb+u3ZpGC4iuliD7V0o1C+V73Law==", + "dependencies": { + "@noble/ciphers": "0.5.3", + "@noble/curves": "1.3.0", + "@noble/hashes": "1.4.0", + "@web5/common": "1.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web5/dids/node_modules/@web5/crypto/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@web5/dids/node_modules/@web5/crypto/node_modules/@web5/common": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@web5/common/-/common-1.0.1.tgz", + "integrity": "sha512-dxczXqzWt6HCwuNyOVBeakg6GgOpP74tVEVxBeKkb+D3XcSP96mYaDtky5ZnjY4iBYb16SaCgwje+sgevOL51A==", + "dependencies": { + "@isaacs/ttlcache": "1.4.1", + "level": "8.0.1", + "multiformats": "13.1.0", + "readable-stream": "4.4.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@web5/dids/node_modules/abstract-level": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/abstract-level/-/abstract-level-1.0.4.tgz", @@ -1416,6 +1452,26 @@ "url": "https://opencollective.com/level" } }, + "node_modules/@web5/dids/node_modules/multiformats": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.1.0.tgz", + "integrity": "sha512-HzdtdBwxsIkzpeXzhQ5mAhhuxcHbjEHH+JQoxt7hG/2HGFjjwyolLo7hbaexcnhoEuV4e0TNJ8kkpMjiEYY4VQ==" + }, + "node_modules/@web5/dids/node_modules/readable-stream": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", diff --git a/package.json b/package.json index 43ba8d8af..b1735dd03 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@noble/curves": "1.4.2", "@noble/ed25519": "2.0.0", "@noble/secp256k1": "2.0.0", + "@web5/crypto": "^1.0.5", "@web5/dids": "^1.1.3", "abstract-level": "1.0.3", "ajv": "8.12.0", diff --git a/src/utils/encryption.ts b/src/utils/encryption.ts index ca1d4771d..e4d984bcf 100644 --- a/src/utils/encryption.ts +++ b/src/utils/encryption.ts @@ -1,10 +1,26 @@ -import * as crypto from 'crypto'; import * as eciesjs from 'eciesjs'; +import { AesCtr } from '@web5/crypto'; +import type { Jwk } from '@web5/crypto'; import { Readable } from 'readable-stream'; -// compress publicKey for message encryption +// Compress publicKey for message encryption eciesjs.ECIES_CONFIG.isEphemeralKeyCompressed = true; +export type EciesEncryptionOutput = { + ciphertext: Uint8Array; + ephemeralPublicKey: Uint8Array; + initializationVector: Uint8Array; + messageAuthenticationCode: Uint8Array; +}; + +export type EciesEncryptionInput = { + privateKey: Uint8Array; + ephemeralPublicKey: Uint8Array; + initializationVector: Uint8Array; + messageAuthenticationCode: Uint8Array; + ciphertext: Uint8Array; +}; + /** * Utility class for performing common, non-DWN specific encryption operations. */ @@ -12,72 +28,119 @@ export class Encryption { /** * Encrypts the given plaintext stream using AES-256-CTR algorithm. */ - public static async aes256CtrEncrypt(key: Uint8Array, initializationVector: Uint8Array, plaintextStream: Readable): Promise { - const cipher = crypto.createCipheriv('aes-256-ctr', key, initializationVector); + public static isEphemeralKeyCompressed: boolean = true; // Set default value + + private static async convertToJwk(key: Uint8Array): Promise { + // Construct the private key in JWK format using bytesToPrivateKey method. + const privateKey = await AesCtr.bytesToPrivateKey({ privateKeyBytes: key }); + + // Assign the algorithm and extractability flag. + return { + ...privateKey, + alg : 'A256CTR', + ext : 'true', + }; + } + + public static async aes256CtrEncrypt( + key: Uint8Array, + initializationVector: Uint8Array, + plaintextStream: Readable + ): Promise { + const jwkKey = await this.convertToJwk(key); + + // Create a cipher stream const cipherStream = new Readable({ - read(): void { } + read(): void {}, }); + let buffer = Buffer.alloc(0); + plaintextStream.on('data', (chunk) => { - const encryptedChunk = cipher.update(chunk); - cipherStream.push(encryptedChunk); + buffer = Buffer.concat([buffer, chunk]); }); - plaintextStream.on('end', () => { - const finalChunk = cipher.final(); - cipherStream.push(finalChunk); - cipherStream.push(null); + plaintextStream.on('end', async () => { + try { + // Encrypt the entire buffer when the stream ends + const encryptedData = await AesCtr.encrypt({ + data : buffer, + key : jwkKey, + counter : initializationVector, + length : 128, // FIX: Counter length must be between 1 and 128 + }); + + cipherStream.push(encryptedData); + cipherStream.push(null); // Signal the end of the stream + } catch (error) { + cipherStream.emit('error', error); // Emit error if encryption fails + } }); plaintextStream.on('error', (err) => { - cipherStream.emit('error', err); + cipherStream.emit('error', err); // Propagate errors }); - return cipherStream; + return cipherStream; // Return the cipher stream } /** * Decrypts the given cipher stream using AES-256-CTR algorithm. */ - public static async aes256CtrDecrypt(key: Uint8Array, initializationVector: Uint8Array, cipherStream: Readable): Promise { - const decipher = crypto.createDecipheriv('aes-256-ctr', key, initializationVector); + public static async aes256CtrDecrypt( + key: Uint8Array, + initializationVector: Uint8Array, + cipherStream: Readable + ): Promise { + const jwkKey = await this.convertToJwk(key); // Convert key to JWK format + + // Create a plaintext stream const plaintextStream = new Readable({ - read(): void { } + read(): void {}, }); + let buffer = Buffer.alloc(0); + cipherStream.on('data', (chunk) => { - const decryptedChunk = decipher.update(chunk); - plaintextStream.push(decryptedChunk); + buffer = Buffer.concat([buffer, chunk]); }); - cipherStream.on('end', () => { - const finalChunk = decipher.final(); - plaintextStream.push(finalChunk); - plaintextStream.push(null); + cipherStream.on('end', async () => { + try { + // Decrypt the entire buffer when the stream ends + const decryptedData = await AesCtr.decrypt({ + data : buffer, + key : jwkKey, + counter : initializationVector, + length : 128, + }); + + plaintextStream.push(decryptedData); + plaintextStream.push(null); // Signal the end of the stream + } catch (error) { + plaintextStream.emit('error', error); // Emit error if decryption fails + } }); cipherStream.on('error', (err) => { - plaintextStream.emit('error', err); + plaintextStream.emit('error', err); // Propagate errors }); - return plaintextStream; + return plaintextStream; // Return the plaintext stream } - /** - * Encrypts the given plaintext using ECIES (Elliptic Curve Integrated Encryption Scheme) - * 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(publicKeyBytes: Uint8Array, plaintext: Uint8Array): Promise { - // underlying library requires Buffer as input + public static async eciesSecp256k1Encrypt( + publicKeyBytes: Uint8Array, + plaintext: Uint8Array + ): Promise { const publicKey = Buffer.from(publicKeyBytes); const plaintextBuffer = Buffer.from(plaintext); const cryptogram = eciesjs.encrypt(publicKey, plaintextBuffer); - // split cryptogram returned into constituent parts + let start = 0; let end = Encryption.isEphemeralKeyCompressed ? 33 : 65; const ephemeralPublicKey = cryptogram.subarray(start, end); @@ -96,7 +159,7 @@ export class Encryption { ciphertext, ephemeralPublicKey, initializationVector, - messageAuthenticationCode + messageAuthenticationCode, }; } @@ -105,41 +168,26 @@ 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 eciesSecp256k1Decrypt(input: EciesEncryptionInput): Promise { - // underlying library requires Buffer as input + + public static async eciesSecp256k1Decrypt( + input: EciesEncryptionInput + ): Promise { const privateKeyBuffer = Buffer.from(input.privateKey); const eciesEncryptionOutput = Buffer.concat([ input.ephemeralPublicKey, input.initializationVector, input.messageAuthenticationCode, - input.ciphertext + input.ciphertext, ]); - const plaintext = eciesjs.decrypt(privateKeyBuffer, eciesEncryptionOutput); - - return plaintext; - } - - /** - * Expose eciesjs library configuration - */ - static get isEphemeralKeyCompressed():boolean { - return eciesjs.ECIES_CONFIG.isEphemeralKeyCompressed; + /** + * Expose eciesjs library configuration + */ + return eciesjs.decrypt(privateKeyBuffer, eciesEncryptionOutput); } } -export type EciesEncryptionOutput = { - initializationVector: Uint8Array; - ephemeralPublicKey: Uint8Array; - ciphertext: Uint8Array; - messageAuthenticationCode: Uint8Array; -}; - -export type EciesEncryptionInput = EciesEncryptionOutput & { - privateKey: Uint8Array; -}; - export enum EncryptionAlgorithm { Aes256Ctr = 'A256CTR', - EciesSecp256k1 = 'ECIES-ES256K' -} \ No newline at end of file + EciesSecp256k1 = 'ECIES-ES256K', +} diff --git a/src/utils/hd-key.ts b/src/utils/hd-key.ts index 6a64bd7b9..24ad49e18 100644 --- a/src/utils/hd-key.ts +++ b/src/utils/hd-key.ts @@ -1,5 +1,4 @@ import type { PrivateJwk, PublicJwk } from '../types/jose-types.js'; - import { Encoder } from './encoder.js'; import { getWebcryptoSubtle } from '@noble/ciphers/webcrypto'; import { Secp256k1 } from './secp256k1.js';