Skip to content

Commit

Permalink
Revamp encapsulate/decapsulate (#806)
Browse files Browse the repository at this point in the history
  • Loading branch information
kigawas authored Oct 26, 2024
1 parent d50cc4c commit c3958ed
Show file tree
Hide file tree
Showing 11 changed files with 257 additions and 224 deletions.
18 changes: 8 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
},
"version": "0.4.10",
"engines": {
"node": ">=16.0.0"
"node": ">=16",
"bun": ">=1",
"deno": ">=2"
},
"keywords": [
"secp256k1",
Expand All @@ -33,23 +35,19 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": "./dist/index.js"
"default": "./dist/index.js"
},
"./config": {
"types": "./dist/config.d.ts",
"import": "./dist/config.js",
"require": "./dist/config.js"
"default": "./dist/config.js"
},
"./consts": {
"types": "./dist/consts.d.ts",
"import": "./dist/consts.js",
"require": "./dist/consts.js"
"default": "./dist/consts.js"
},
"./utils": {
"types": "./dist/utils/index.d.ts",
"import": "./dist/utils/index.js",
"require": "./dist/utils/index.js"
"default": "./dist/utils/index.js"
}
},
"scripts": {
Expand All @@ -63,7 +61,7 @@
"@noble/hashes": "^1.5.0"
},
"devDependencies": {
"@types/node": "^22.7.9",
"@types/node": "^22.8.0",
"@vitest/coverage-v8": "^2.1.3",
"typescript": "^5.6.3",
"undici": "^6.20.1",
Expand Down
68 changes: 34 additions & 34 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 33 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { concatBytes } from "@noble/ciphers/utils";

import { ephemeralKeySize, isEphemeralKeyCompressed } from "./config";
import {
ephemeralKeySize,
isEphemeralKeyCompressed,
isHkdfKeyCompressed,
} from "./config";
import { PrivateKey, PublicKey } from "./keys";
import {
aesDecrypt,
Expand All @@ -12,37 +16,52 @@ import {
symEncrypt,
} from "./utils";

/**
* Encrypts a message.
* @description From version 0.5.0, `Uint8Array` will be returned instead of `Buffer`.
* To keep the same behavior, use `Buffer.from(encrypt(...))`.
*
* @param receiverRawPK - Raw public key of the receiver, either as a hex string or a Uint8Array.
* @param msg - Message to encrypt.
* @returns Encrypted payload, format: `public key || encrypted`.
*/
export function encrypt(receiverRawPK: string | Uint8Array, msg: Uint8Array): Buffer {
const ephemeralKey = new PrivateKey();
const ephemeralSK = new PrivateKey();

const receiverPK =
receiverRawPK instanceof Uint8Array
? new PublicKey(receiverRawPK)
: PublicKey.fromHex(receiverRawPK);

const symKey = ephemeralKey.encapsulate(receiverPK);
const encrypted = symEncrypt(symKey, msg);
const sharedKey = ephemeralSK.encapsulate(receiverPK, isHkdfKeyCompressed());
const ephemeralPK = isEphemeralKeyCompressed()
? ephemeralSK.publicKey.compressed
: ephemeralSK.publicKey.uncompressed;

let pk: Uint8Array;
if (isEphemeralKeyCompressed()) {
pk = ephemeralKey.publicKey.compressed;
} else {
pk = ephemeralKey.publicKey.uncompressed;
}
return Buffer.from(concatBytes(pk, encrypted));
const encrypted = symEncrypt(sharedKey, msg);
return Buffer.from(concatBytes(ephemeralPK, encrypted));
}

/**
* Decrypts a message.
* @description From version 0.5.0, `Uint8Array` will be returned instead of `Buffer`.
* To keep the same behavior, use `Buffer.from(decrypt(...))`.
*
* @param receiverRawSK - Raw private key of the receiver, either as a hex string or a Uint8Array.
* @param msg - Message to decrypt.
* @returns Decrypted plain text.
*/
export function decrypt(receiverRawSK: string | Uint8Array, msg: Uint8Array): Buffer {
const receiverSK =
receiverRawSK instanceof Uint8Array
? new PrivateKey(receiverRawSK)
: PrivateKey.fromHex(receiverRawSK);

const keySize = ephemeralKeySize();
const senderPK = new PublicKey(msg.subarray(0, keySize));
const ephemeralPK = new PublicKey(msg.subarray(0, keySize));
const encrypted = msg.subarray(keySize);
const symKey = senderPK.decapsulate(receiverSK);
return Buffer.from(symDecrypt(symKey, encrypted));
const sharedKey = ephemeralPK.decapsulate(receiverSK, isHkdfKeyCompressed());
return Buffer.from(symDecrypt(sharedKey, encrypted));
}

export { ECIES_CONFIG } from "./config";
Expand Down
41 changes: 26 additions & 15 deletions src/keys/PrivateKey.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { bytesToHex, equalBytes } from "@noble/ciphers/utils";

import { isHkdfKeyCompressed } from "../config";
import {
decodeHex,
getPublicKey,
Expand All @@ -25,28 +24,40 @@ export class PrivateKey {
}

constructor(secret?: Uint8Array) {
const sk = secret === undefined ? getValidSecret() : secret;
if (!isValidPrivateKey(sk)) {
if (secret === undefined) {
this.data = getValidSecret();
} else if (isValidPrivateKey(secret)) {
this.data = secret;
} else {
throw new Error("Invalid private key");
}
this.data = sk;
this.publicKey = new PublicKey(getPublicKey(sk));
this.publicKey = new PublicKey(getPublicKey(this.data));
}

public toHex(): string {
return bytesToHex(this.data);
}

public encapsulate(pk: PublicKey): Uint8Array {
let senderPoint: Uint8Array;
let sharedPoint: Uint8Array;
if (isHkdfKeyCompressed()) {
senderPoint = this.publicKey.compressed;
sharedPoint = this.multiply(pk, true);
} else {
senderPoint = this.publicKey.uncompressed;
sharedPoint = this.multiply(pk, false);
}
/**
* Derives a shared secret from ephemeral private key (this) and receiver's public key (pk).
* @description The shared key is 32 bytes, derived with `HKDF-SHA256(senderPoint || sharedPoint)`. See implementation for details.
*
* There are some variations in different ECIES implementations:
* which key derivation function to use, compressed or uncompressed `senderPoint`/`sharedPoint`, whether to include `senderPoint`, etc.
*
* Because the entropy of `senderPoint`, `sharedPoint` is enough high[1], we don't need salt to derive keys.
*
* [1]: Two reasons: the public keys are "random" bytes (albeit secp256k1 public keys are **not uniformly** random), and ephemeral keys are generated in every encryption.
*
* @param pk - Receiver's public key.
* @param compressed - Whether to use compressed or uncompressed public keys in the key derivation (secp256k1 only).
* @returns Shared secret, derived with HKDF-SHA256.
*/
public encapsulate(pk: PublicKey, compressed: boolean = false): Uint8Array {
const senderPoint = compressed
? this.publicKey.compressed
: this.publicKey.uncompressed;
const sharedPoint = this.multiply(pk, compressed);
return getSharedKey(senderPoint, sharedPoint);
}

Expand Down
Loading

0 comments on commit c3958ed

Please sign in to comment.