Skip to content

Commit

Permalink
Add configuration & refactor utils (#724)
Browse files Browse the repository at this point in the history
  • Loading branch information
kigawas authored Jul 16, 2023
1 parent 8ebcc80 commit 2354585
Show file tree
Hide file tree
Showing 19 changed files with 787 additions and 588 deletions.
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@

# Changelog

## 0.4.1

- Add configuration for more compatibility
- Bump dependencies

## 0.4.0

- Change secp256k1 library to [noble-curves](https://github.com/paulmillr/noble-curves), which is [audited](https://github.com/paulmillr/noble-curves/tree/main/audit)
- Change hash library to [noble-hashes](https://github.com/paulmillr/noble-hashes)
- Change test library to [jest](https://jestjs.io/)
- Bump dependencies
- Drop Node 14 support

## 0.3.1 ~ 0.3.17

- Support Node 18, 20
- Drop Node 10, 12 support
- Bump dependencies
- Update documentation
- Extract constant variables and rename some parameters

## 0.3.0

- API change: `encrypt/decrypt` now can take both hex `string` and `Buffer`

## 0.2.0

- API change: use `HKDF-sha256` to derive shared keys instead of `sha256`
- Bump dependencies
- Update documentation

## 0.1.1 ~ 0.1.5

- Bump dependencies
- Update documentation

## 0.1.0

- First beta version release
44 changes: 17 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,39 +89,29 @@ readonly uncompressed: Buffer;
readonly compressed: Buffer;
```

## Release Notes
## Configuration

### 0.4.0
Ephemeral key format in the payload and shared key in the key derivation can be configured as compressed or uncompressed format.

- Change secp256k1 library to [noble-curves](https://github.com/paulmillr/noble-curves), which is [audited](https://github.com/paulmillr/noble-curves/tree/main/audit)
- Change hash library to [noble-hashes](https://github.com/paulmillr/noble-hashes)
- Change test library to [jest](https://jestjs.io/)
- Bump dependencies
- Drop Node 14 support
```ts
class Config {
isEphemeralKeyCompressed: boolean = false;
isHkdfKeyCompressed: boolean = false;
symmetricAlgorithm: Algorithm = "aes-256-gcm"; // currently we only support aes-256-gcm
symmetricNonceLength: NonceLength = 16;
}

### 0.3.1 ~ 0.3.17

- Support Node 18, 20
- Drop Node 10, 12 support
- Bump dependencies
- Update documentation
- Extract constant variables and rename some parameters

### 0.3.0

- API change: `encrypt/decrypt` now can take both hex `string` and `Buffer`
export const ECIES_CONFIG = new Config();
```

### 0.2.0
For example, if you set `isEphemeralKeyCompressed = true`, the payload would be like: `33 Bytes + AES` instead of `65 Bytes + AES`.

- API change: use `HKDF-sha256` to derive shared keys instead of `sha256`
- Bump dependencies
- Update documentation
If you set `isHkdfKeyCompressed = true`, the hkdf key would be derived from `ephemeral public key (compressed) + shared public key (compressed)` instead of `ephemeral public key (uncompressed) + shared public key (uncompressed)`.

### 0.1.1 ~ 0.1.5
If you set `symmetricNonceLength = 12`, then the nonce of aes-256-gcm would be 12 bytes.

- Bump dependencies
- Update documentation
For compatibility, make sure different applications share the same configuration.

### 0.1.0
## Changelog

- First beta version release
See [CHANGELOG.md](./CHANGELOG.md)
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
"email": "[email protected]",
"url": "https://github.com/kigawas"
},
"engines": {
"node": ">=16.0.0"
},
"keywords": [
"secp256k1",
"crypto",
Expand All @@ -29,15 +32,15 @@
"type": "git",
"url": "https://github.com/ecies/js.git"
},
"version": "0.4.0",
"version": "0.4.1",
"dependencies": {
"@noble/curves": "^1.1.0"
},
"devDependencies": {
"@types/jest": "^29.5.2",
"@types/node": "^20.3.2",
"@types/node": "^20.4.2",
"axios": "^1.4.0",
"jest": "^29.5.0",
"jest": "^29.6.1",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"typescript": "^5.1.5"
Expand Down
22 changes: 22 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { COMPRESSED_PUBLIC_KEY_SIZE, UNCOMPRESSED_PUBLIC_KEY_SIZE } from "./consts";

export type SymmetricAlgorithm = "aes-256-gcm";
export type NonceLength = 12 | 16 | 24; // bytes

class Config {
isEphemeralKeyCompressed: boolean = false;
isHkdfKeyCompressed: boolean = false;
symmetricAlgorithm: SymmetricAlgorithm = "aes-256-gcm";
symmetricNonceLength: NonceLength = 16;
}

export const ECIES_CONFIG = new Config();

export const isEphemeralKeyCompressed = () => ECIES_CONFIG.isEphemeralKeyCompressed;
export const isHkdfKeyCompressed = () => ECIES_CONFIG.isHkdfKeyCompressed;
export const ephemeralKeySize = () =>
ECIES_CONFIG.isEphemeralKeyCompressed
? COMPRESSED_PUBLIC_KEY_SIZE
: UNCOMPRESSED_PUBLIC_KEY_SIZE;
export const symmetricAlgorithm = () => ECIES_CONFIG.symmetricAlgorithm;
export const symmetricNonceLength = () => ECIES_CONFIG.symmetricNonceLength;
6 changes: 3 additions & 3 deletions src/consts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const COMPRESSED_PUBLIC_KEY_SIZE = 33;
export const UNCOMPRESSED_PUBLIC_KEY_SIZE = 65;
export const AES_IV_LENGTH = 16;
export const AES_TAG_LENGTH = 16;
export const AES_IV_PLUS_TAG_LENGTH = AES_IV_LENGTH + AES_TAG_LENGTH;
export const ETH_PUBLIC_KEY_SIZE = 64;
export const SECRET_KEY_LENGTH = 32;
export const ONE = BigInt(1);
export const AEAD_TAG_LENGTH = 16;
14 changes: 10 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UNCOMPRESSED_PUBLIC_KEY_SIZE } from "./consts";
import { ephemeralKeySize, isEphemeralKeyCompressed } from "./config";
import { PrivateKey, PublicKey } from "./keys";
import { aesDecrypt, aesEncrypt, decodeHex, getValidSecret, remove0x } from "./utils";

Expand All @@ -12,7 +12,12 @@ export function encrypt(receiverRawPK: string | Buffer, msg: Buffer): Buffer {

const aesKey = ephemeralKey.encapsulate(receiverPK);
const encrypted = aesEncrypt(aesKey, msg);
return Buffer.concat([ephemeralKey.publicKey.uncompressed, encrypted]);

if (isEphemeralKeyCompressed()) {
return Buffer.concat([ephemeralKey.publicKey.compressed, encrypted]);
} else {
return Buffer.concat([ephemeralKey.publicKey.uncompressed, encrypted]);
}
}

export function decrypt(receiverRawSK: string | Buffer, msg: Buffer): Buffer {
Expand All @@ -21,8 +26,9 @@ export function decrypt(receiverRawSK: string | Buffer, msg: Buffer): Buffer {
? new PrivateKey(receiverRawSK)
: PrivateKey.fromHex(receiverRawSK);

const senderPubkey = new PublicKey(msg.subarray(0, UNCOMPRESSED_PUBLIC_KEY_SIZE));
const encrypted = msg.subarray(UNCOMPRESSED_PUBLIC_KEY_SIZE);
const keySize = ephemeralKeySize();
const senderPubkey = new PublicKey(msg.subarray(0, keySize));
const encrypted = msg.subarray(keySize);
const aesKey = senderPubkey.decapsulate(receiverSK);
return aesDecrypt(aesKey, encrypted);
}
Expand Down
33 changes: 22 additions & 11 deletions src/keys/PrivateKey.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { secp256k1 } from "@noble/curves/secp256k1";
import { hkdf } from "@noble/hashes/hkdf";
import { sha256 } from "@noble/hashes/sha256";
import { decodeHex, getValidSecret } from "../utils";
import { isHkdfKeyCompressed } from "../config";
import {
decodeHex,
deriveKey,
getPublicKey,
getSharedPoint,
getValidSecret,
isValidPrivateKey,
} from "../utils";
import PublicKey from "./PublicKey";

export default class PrivateKey {
Expand All @@ -13,24 +18,30 @@ export default class PrivateKey {
public readonly publicKey: PublicKey;

constructor(secret?: Buffer) {
this.secret = secret || getValidSecret();
if (!secp256k1.utils.isValidPrivateKey(this.secret)) {
this.secret = secret === undefined ? getValidSecret() : secret;
if (!isValidPrivateKey(this.secret)) {
throw new Error("Invalid private key");
}
this.publicKey = new PublicKey(Buffer.from(secp256k1.getPublicKey(this.secret)));
this.publicKey = new PublicKey(getPublicKey(this.secret));
}

public toHex(): string {
return this.secret.toString("hex");
}

public encapsulate(pub: PublicKey): Buffer {
const master = Buffer.concat([this.publicKey.uncompressed, this.multiply(pub)]);
return Buffer.from(hkdf(sha256, master, undefined, undefined, 32));
let master: Buffer;

if (isHkdfKeyCompressed()) {
master = Buffer.concat([this.publicKey.compressed, this.multiply(pub, true)]);
} else {
master = Buffer.concat([this.publicKey.uncompressed, this.multiply(pub, false)]);
}
return deriveKey(master);
}

public multiply(pub: PublicKey): Buffer {
return Buffer.from(secp256k1.getSharedSecret(this.secret, pub.compressed, false));
public multiply(pub: PublicKey, compressed: boolean = false): Buffer {
return getSharedPoint(this.secret, pub.compressed, compressed);
}

public equals(other: PrivateKey): boolean {
Expand Down
24 changes: 14 additions & 10 deletions src/keys/PublicKey.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { secp256k1 } from "@noble/curves/secp256k1";
import { hkdf } from "@noble/hashes/hkdf";
import { sha256 } from "@noble/hashes/sha256";
import { ONE, UNCOMPRESSED_PUBLIC_KEY_SIZE } from "../consts";
import { decodeHex } from "../utils";
import { isHkdfKeyCompressed } from "../config";
import { ETH_PUBLIC_KEY_SIZE, ONE } from "../consts";
import { decodeHex, deriveKey, getSharedPoint } from "../utils";
import PrivateKey from "./PrivateKey";

export default class PublicKey {
public static fromHex(hex: string): PublicKey {
const decoded = decodeHex(hex);
if (decoded.length === UNCOMPRESSED_PUBLIC_KEY_SIZE - 1) {
if (decoded.length === ETH_PUBLIC_KEY_SIZE) {
// eth public key
const prefix: Buffer = Buffer.from([0x04]);
const fixed: Buffer = Buffer.concat([prefix, decoded]);
Expand All @@ -21,8 +19,8 @@ export default class PublicKey {
public readonly compressed: Buffer;

constructor(buffer: Buffer) {
this.uncompressed = Buffer.from(secp256k1.getSharedSecret(ONE, buffer, false));
this.compressed = Buffer.from(secp256k1.getSharedSecret(ONE, buffer, true));
this.uncompressed = getSharedPoint(ONE, buffer, false);
this.compressed = getSharedPoint(ONE, buffer, true);
}

public toHex(compressed: boolean = true): string {
Expand All @@ -34,8 +32,14 @@ export default class PublicKey {
}

public decapsulate(priv: PrivateKey): Buffer {
const master = Buffer.concat([this.uncompressed, priv.multiply(this)]);
return Buffer.from(hkdf(sha256, master, undefined, undefined, 32));
let master: Buffer;

if (isHkdfKeyCompressed()) {
master = Buffer.concat([this.compressed, priv.multiply(this, true)]);
} else {
master = Buffer.concat([this.uncompressed, priv.multiply(this, false)]);
}
return deriveKey(master);
}

public equals(other: PublicKey): boolean {
Expand Down
40 changes: 0 additions & 40 deletions src/utils.ts

This file was deleted.

28 changes: 28 additions & 0 deletions src/utils/elliptic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { secp256k1 } from "@noble/curves/secp256k1";
import { randomBytes } from "crypto";

import { SECRET_KEY_LENGTH } from "../consts";

export function isValidPrivateKey(secret: Buffer) {
return secp256k1.utils.isValidPrivateKey(secret);
}

export function getValidSecret(): Buffer {
let key: Buffer;
do {
key = randomBytes(SECRET_KEY_LENGTH);
} while (!isValidPrivateKey(key));
return key;
}

export function getPublicKey(secret: Buffer): Buffer {
return Buffer.from(secp256k1.getPublicKey(secret));
}

export function getSharedPoint(
skRaw: Buffer | bigint,
pkRaw: Buffer,
compressed: boolean
): Buffer {
return Buffer.from(secp256k1.getSharedSecret(skRaw, pkRaw, compressed));
}
10 changes: 10 additions & 0 deletions src/utils/hex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function remove0x(hex: string): string {
if (hex.startsWith("0x") || hex.startsWith("0X")) {
return hex.slice(2);
}
return hex;
}

export function decodeHex(hex: string): Buffer {
return Buffer.from(remove0x(hex), "hex");
}
3 changes: 3 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./elliptic";
export * from "./hex";
export * from "./symmetric";
Loading

0 comments on commit 2354585

Please sign in to comment.