Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add configuration & refactor utils #724

Merged
merged 1 commit into from
Jul 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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": "to.be.impressive@gmail.com",
"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