diff --git a/package.json b/package.json index e436271..6314fac 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@cardinal-cryptography/ecies-encryption-lib", "author": "CardinalCryptography", - "version": "0.1.1", + "version": "0.1.4", "description": "ECIES encryption library (proxy to ts/lib)", "main": "ts/lib/dist/index.js", "types": "ts/lib/dist/index.d.ts", diff --git a/ts/lib/src/index.ts b/ts/lib/src/index.ts index 3402652..0f116ea 100644 --- a/ts/lib/src/index.ts +++ b/ts/lib/src/index.ts @@ -1,28 +1,5 @@ import * as secp from "@noble/secp256k1"; -export function toHex(uint8: Uint8Array): string { - return Array.from(uint8) - .map((byte) => byte.toString(16).padStart(2, "0")) - .join(""); -} - -export function fromHex(hex: string): Uint8Array { - if (hex.length % 2 !== 0) { - throw new Error("Hex string must have an even length"); - } - const bytes = new Uint8Array(hex.length / 2); - for (let i = 0; i < hex.length; i += 2) { - bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16); - } - return bytes; -} - -export async function getCrypto(): Promise { - return typeof globalThis.crypto !== "undefined" - ? globalThis.crypto - : ((await import("node:crypto")).webcrypto as Crypto); -} - export type Keypair = { sk: Uint8Array; pk: Uint8Array }; export function generateKeypair(): Keypair { @@ -31,82 +8,34 @@ export function generateKeypair(): Keypair { return { sk, pk }; } -async function hkdf( - sharedSecret: Uint8Array, - cryptoAPI: Crypto -): Promise { - const keyMaterial = await cryptoAPI.subtle.importKey( - "raw", - sharedSecret as BufferSource, - "HKDF", - false, - ["deriveKey"] - ); - return await cryptoAPI.subtle.deriveKey( - { - name: "HKDF", - hash: "SHA-256", - salt: new Uint8Array([]), - info: new TextEncoder().encode("ecies-secp256k1-v1") as BufferSource, - }, - keyMaterial, - { name: "AES-GCM", length: 256 }, - false, - ["encrypt", "decrypt"] - ); +export function fromHex(hex: Uint8Array | string): Uint8Array { + return isBytes(hex) + ? Uint8Array.from(hex as any) + : secp.hexToBytes(removePrefix(hex as string)); } -async function _encrypt( - message: Uint8Array, - recipientPubHex: string, - cryptoAPI: Crypto -): Promise { - const recipientPub = secp.Point.fromHex(recipientPubHex); - const ephSk = secp.utils.randomPrivateKey(); - const ephPk = secp.getPublicKey(ephSk, true); - - const ephSkBigInt = BigInt("0x" + toHex(ephSk)); - const shared = recipientPub.multiply(ephSkBigInt).toRawBytes(true); - const aesKey = await hkdf(shared, cryptoAPI); +export function toHex(uint8: Uint8Array, withPrefix: boolean = false): string { + const hex = Array.from(uint8) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(""); + return withPrefix ? "0x" + hex : hex; +} - const iv = cryptoAPI.getRandomValues(new Uint8Array(12)); +export async function getCrypto(): Promise { + return typeof globalThis.crypto !== "undefined" + ? globalThis.crypto + : ((await import("node:crypto")).webcrypto as Crypto); +} - const ciphertextBuffer = await cryptoAPI.subtle.encrypt( - { name: "AES-GCM", iv }, - aesKey, - message as BufferSource +function isBytes(bytes: Uint8Array | string): boolean { + return ( + bytes instanceof Uint8Array || + (ArrayBuffer.isView(bytes) && bytes.constructor.name === "Uint8Array") ); - const ciphertext = new Uint8Array(ciphertextBuffer); - - const out = new Uint8Array(ephPk.length + iv.length + ciphertext.length); - out.set(ephPk); - out.set(iv, ephPk.length); - out.set(ciphertext, ephPk.length + iv.length); - - return out; } -async function _decrypt( - ciphertextBytes: Uint8Array, - recipientSkHex: string, - cryptoAPI: Crypto -): Promise { - const ephPk = secp.Point.fromHex(ciphertextBytes.slice(0, 33)); - const iv = ciphertextBytes.slice(33, 45); - const ciphertext = ciphertextBytes.slice(45); - - const skBytes = fromHex(recipientSkHex); - const skBigInt = BigInt("0x" + toHex(skBytes)); - const shared_point = ephPk.multiply(skBigInt); - let shared = shared_point.toRawBytes(true); - const aesKey = await hkdf(shared, cryptoAPI); - - const plaintextBuffer = await cryptoAPI.subtle.decrypt( - { name: "AES-GCM", iv }, - aesKey, - ciphertext as BufferSource - ); - return new Uint8Array(plaintextBuffer); +function removePrefix(hex: string): string { + return hex.startsWith("0x") || hex.startsWith("0X") ? hex.slice(2) : hex; } export async function encrypt( @@ -115,7 +44,7 @@ export async function encrypt( cryptoAPI: Crypto ): Promise { const encoded = new TextEncoder().encode(message); - const out = await _encrypt(encoded, recipientPubHex, cryptoAPI); + const out = await _encrypt(encoded, fromHex(recipientPubHex), cryptoAPI); return toHex(out); } @@ -126,14 +55,14 @@ export async function decrypt( ): Promise { const decrypted = await _decrypt( fromHex(ciphertextHex), - recipientSkHex, + fromHex(recipientSkHex), cryptoAPI ); return new TextDecoder().decode(decrypted); } export async function encryptPadded( - message: string | Uint8Array, + message: string, recipientPubHex: string, cryptoAPI: Crypto, paddedLength: number @@ -153,10 +82,14 @@ export async function encryptPadded( view.setUint32(0, message.length, true); encoded.set(new Uint8Array(buffer), 0); - const encodedMessage = message instanceof Uint8Array ? message : new TextEncoder().encode(message); + const encodedMessage = new TextEncoder().encode(message); encoded.set(encodedMessage, 4); - const encrypted = await _encrypt(encoded, recipientPubHex, cryptoAPI); + const encrypted = await _encrypt( + encoded, + fromHex(recipientPubHex), + cryptoAPI + ); return toHex(encrypted); } @@ -168,7 +101,7 @@ export async function decryptPadded( ): Promise { const decrypted = await _decrypt( fromHex(ciphertextHex), - recipientSkHex, + fromHex(recipientSkHex), cryptoAPI ); if (decrypted.length != paddedLength) { @@ -186,12 +119,89 @@ export async function decryptPaddedUnchecked( ): Promise { const decrypted = await _decrypt( fromHex(ciphertextHex), - recipientSkHex, + fromHex(recipientSkHex), cryptoAPI ); return await decodePadded(decrypted); } +async function hkdf( + sharedSecret: Uint8Array, + cryptoAPI: Crypto +): Promise { + const keyMaterial = await cryptoAPI.subtle.importKey( + "raw", + sharedSecret as BufferSource, + "HKDF", + false, + ["deriveKey"] + ); + return await cryptoAPI.subtle.deriveKey( + { + name: "HKDF", + hash: "SHA-256", + salt: new Uint8Array([]), + info: new TextEncoder().encode("ecies-secp256k1-v1") as BufferSource, + }, + keyMaterial, + { name: "AES-GCM", length: 256 }, + false, + ["encrypt", "decrypt"] + ); +} + +async function _encrypt( + message: Uint8Array, + recipientPk: Uint8Array, + cryptoAPI: Crypto +): Promise { + const recipientPub = secp.Point.fromHex(recipientPk); + const ephSk = secp.utils.randomPrivateKey(); + const ephPk = secp.getPublicKey(ephSk, true); + + const ephSkBigInt = BigInt(toHex(ephSk, true)); + const shared = recipientPub.multiply(ephSkBigInt).toRawBytes(true); + const aesKey = await hkdf(shared, cryptoAPI); + + const iv = cryptoAPI.getRandomValues(new Uint8Array(12)); + + const ciphertextBuffer = await cryptoAPI.subtle.encrypt( + { name: "AES-GCM", iv }, + aesKey, + message as BufferSource + ); + const ciphertext = new Uint8Array(ciphertextBuffer); + + const out = new Uint8Array(ephPk.length + iv.length + ciphertext.length); + out.set(ephPk); + out.set(iv, ephPk.length); + out.set(ciphertext, ephPk.length + iv.length); + + return out; +} + +async function _decrypt( + ciphertextBytes: Uint8Array, + recipientSkBytes: Uint8Array, + cryptoAPI: Crypto +): Promise { + const ephPk = secp.Point.fromHex(ciphertextBytes.slice(0, 33)); + const iv = ciphertextBytes.slice(33, 45); + const ciphertext = ciphertextBytes.slice(45); + + const skBigInt = BigInt(toHex(recipientSkBytes, true)); + const shared_point = ephPk.multiply(skBigInt); + let shared = shared_point.toRawBytes(true); + const aesKey = await hkdf(shared, cryptoAPI); + + const plaintextBuffer = await cryptoAPI.subtle.decrypt( + { name: "AES-GCM", iv }, + aesKey, + ciphertext as BufferSource + ); + return new Uint8Array(plaintextBuffer); +} + async function decodePadded(paddedMessage: Uint8Array): Promise { if (paddedMessage.length < 4) { throw new Error(