Skip to content

Commit

Permalink
Revamp symmetric encryption/decryption
Browse files Browse the repository at this point in the history
  • Loading branch information
kigawas committed Oct 26, 2024
1 parent c3958ed commit 1e12651
Show file tree
Hide file tree
Showing 16 changed files with 208 additions and 99 deletions.
3 changes: 0 additions & 3 deletions .cspell.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,10 @@
"eciesjs",
"eciespy",
"eth",
"futoin-hkdf",
"helloworld",
"hkdf",
"js",
"Npm",
"Prv",
"querystring",
"secp256k1",
"xchacha",
"xchacha20"
Expand Down
23 changes: 18 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,21 @@ jobs:
deno-version: v2.x

- run: pnpm install && pnpm build && cd example/runtime && pnpm install
- run: bun run example/runtime/main.js
- run: deno run --allow-read example/runtime/main.js
- run: node example/runtime/main.js
- run: node example/runtime/import.js
- run: node example/runtime/require.cjs

- name: check main.js
run: |
bun run example/runtime/main.js
deno run --allow-read example/runtime/main.js
node example/runtime/main.js
- name: check import.js
run: |
bun run example/runtime/import.js
deno run --allow-read example/runtime/import.js
node example/runtime/import.js
- name: check require.cjs
run: |
bun run example/runtime/require.cjs
deno run --allow-read example/runtime/require.cjs
node example/runtime/require.cjs
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@

# Changelog

## 0.4.11

- Revamp symmetric encryption/decryption

## 0.4.10

- Fix commonjs build
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ See [Configuration](#configuration) to control with more granularity.

This library is browser-friendly, check the [`example/browser`](./example/browser) directory for details. Currently it's necessary to polyfill `Buffer` for backward compatibility. From v0.5.0, it can run in browsers as is.

If you want a WASM version to run directly in modern browsers or on some blockchains, check [`ecies-wasm`](https://github.com/ecies/rs-wasm).
If you want a WASM version to run directly in modern browsers or on some blockchains, you can also try [`ecies-wasm`](https://github.com/ecies/rs-wasm).

## API

Expand Down Expand Up @@ -140,6 +140,8 @@ On `ellipticCurve = "x25519"` or `ellipticCurve = "ed25519"`, x25519 (key exchan
In this case, the payload would always be: `32 Bytes + Ciphered` regardless of `isEphemeralKeyCompressed`.

> If you don't know how to choose between x25519 and ed25519, just use the dedicated key exchange function x25519 for efficiency.
>
> Because any 32-byte data is a valid curve25519 public key, the payload would seem random. This property is excellent for circumventing censorship by adversaries.
### Secp256k1-specific configuration

Expand All @@ -153,6 +155,14 @@ On `symmetricAlgorithm = "xchacha20"`, plaintext data would be encrypted with XC

On `symmetricNonceLength = 12`, the nonce of AES-256-GCM would be 12 bytes. XChaCha20-Poly1305's nonce is always 24 bytes regardless of `symmetricNonceLength`.

### Which configuration should I choose?

For compatibility with other [ecies libraries](https://github.com/orgs/ecies/repositories), start with the default (secp256k1 with AES-256-GCM).

For speed and security, pick x25519 with XChaCha20-Poly1305.

If you know exactly what you are doing, configure as you wish or build your own ecies logic with this library.

## Security Audit

Following dependencies are audited:
Expand Down
2 changes: 1 addition & 1 deletion example/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"eciesjs": "file:../.."
},
"devDependencies": {
"vite": "^5.4.9",
"vite": "6.0.0-beta.4",
"vite-bundle-visualizer": "^1.2.1"
}
}
8 changes: 5 additions & 3 deletions example/browser/script.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { bytesToHex } from "@noble/ciphers/utils";
import { Buffer } from "buffer";
import { PrivateKey, decrypt, encrypt } from "eciesjs";

import { ECIES_CONFIG, PrivateKey, decrypt, encrypt } from "eciesjs";
import "./style.css";

globalThis.Buffer = Buffer; // polyfill manually

ECIES_CONFIG.ellipticCurve = "x25519";
ECIES_CONFIG.symmetricAlgorithm = "xchacha20";

const sk = new PrivateKey();
const encoder = new TextEncoder();
const decoder = new TextDecoder();
Expand All @@ -21,7 +23,7 @@ export function setup(encryptedElement, textElement, decryptedElement) {
const _encrypt = () => {
encrypted = encrypt(sk.publicKey.toHex(), encoder.encode(text));
encryptedElement.innerHTML = `encrypted:`;
textElement.innerHTML = `${bytesToHex(encrypted)}`;
textElement.innerHTML = `<code>${bytesToHex(encrypted)}</code>`;
decryptedElement.innerHTML = `click me to decrypt`;
};
const _decrypt = () => {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"type": "git",
"url": "git+https://github.com/ecies/js.git"
},
"version": "0.4.10",
"version": "0.4.11",
"engines": {
"node": ">=16",
"bun": ">=1",
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export { PrivateKey, PublicKey } from "./keys";

/** @deprecated - use `import utils from "eciesjs/utils"` instead. */
export const utils = {
// TODO: review these before 0.5.0
// TODO: remove these after 0.5.0
aesEncrypt,
aesDecrypt,
symEncrypt,
Expand Down
43 changes: 19 additions & 24 deletions src/utils/elliptic.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { concatBytes } from "@noble/ciphers/utils";
import { randomBytes } from "@noble/ciphers/webcrypto";
import { ed25519, x25519 } from "@noble/curves/ed25519";
import { secp256k1 } from "@noble/curves/secp256k1";

import { ellipticCurve } from "../config";
import { ETH_PUBLIC_KEY_SIZE, SECRET_KEY_LENGTH } from "../consts";
import { deriveKey } from "./hash";
import { decodeHex } from "./hex";

export const getValidSecret = (): Uint8Array => {
let key: Uint8Array;
do {
key = randomBytes(SECRET_KEY_LENGTH);
} while (!isValidPrivateKey(key));
return key;
};

export const isValidPrivateKey = (secret: Uint8Array): boolean =>
// on secp256k1: only key ∈ (0, group order) is valid
// on curve25519: any 32-byte key is valid
Expand All @@ -17,26 +23,13 @@ export const isValidPrivateKey = (secret: Uint8Array): boolean =>
() => true
);

export const getValidSecret = (): Uint8Array => {
let key: Uint8Array;
do {
key = randomBytes(SECRET_KEY_LENGTH);
} while (!isValidPrivateKey(key));
return key;
};

export const getPublicKey = (secret: Uint8Array): Uint8Array =>
_exec(
(curve) => curve.getPublicKey(secret),
(curve) => curve.getPublicKey(secret),
(curve) => curve.getPublicKey(secret)
);

export const getSharedKey = (
ephemeralPoint: Uint8Array,
sharedPoint: Uint8Array
): Uint8Array => deriveKey(concatBytes(ephemeralPoint, sharedPoint));

export const getSharedPoint = (
sk: Uint8Array,
pk: Uint8Array,
Expand Down Expand Up @@ -64,15 +57,7 @@ export const convertPublicKeyFormat = (pk: Uint8Array, compressed: boolean): Uin
export const hexToPublicKey = (hex: string): Uint8Array => {
const decoded = decodeHex(hex);
return _exec(
() => {
if (decoded.length === ETH_PUBLIC_KEY_SIZE) {
const fixed = new Uint8Array(1 + decoded.length);
fixed.set([0x04]);
fixed.set(decoded, 1);
return fixed;
}
return decoded;
},
() => compatEthPublicKey(decoded),
() => decoded,
() => decoded
);
Expand All @@ -94,3 +79,13 @@ function _exec<T>(
throw new Error("Not implemented");
}
}

const compatEthPublicKey = (pk: Uint8Array): Uint8Array => {
if (pk.length === ETH_PUBLIC_KEY_SIZE) {
const fixed = new Uint8Array(1 + pk.length);
fixed.set([0x04]);
fixed.set(pk, 1);
return fixed;
}
return pk;
};
12 changes: 10 additions & 2 deletions src/utils/hash.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { concatBytes } from "@noble/ciphers/utils";
import { hkdf } from "@noble/hashes/hkdf";
import { sha256 } from "@noble/hashes/sha256";

export const deriveKey = (master: Uint8Array): Uint8Array =>
export const deriveKey = (
master: Uint8Array,
salt?: Uint8Array,
info?: Uint8Array
): Uint8Array =>
// 32 bytes shared secret for aes256 and xchacha20 derived from HKDF-SHA256
hkdf(sha256, master, undefined, undefined, 32);
hkdf(sha256, master, salt, info, 32);

export const getSharedKey = (partyA: Uint8Array, partyB: Uint8Array): Uint8Array =>
deriveKey(concatBytes(partyA, partyB));
1 change: 0 additions & 1 deletion src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// under this folder no `Buffer`
export * from "./elliptic";
export * from "./hash";
export * from "./hex";
Expand Down
65 changes: 42 additions & 23 deletions src/utils/symmetric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ import { xchacha20 } from "@ecies/ciphers/chacha";
import { symmetricAlgorithm, symmetricNonceLength } from "../config";
import { AEAD_TAG_LENGTH, XCHACHA20_NONCE_LENGTH } from "../consts";

export const symEncrypt = (key: Uint8Array, plainText: Uint8Array): Uint8Array =>
_exec(_encrypt, key, plainText);
export const symEncrypt = (
key: Uint8Array,
plainText: Uint8Array,
AAD?: Uint8Array
): Uint8Array => _exec(_encrypt, key, plainText, AAD);

export const symDecrypt = (key: Uint8Array, cipherText: Uint8Array): Uint8Array =>
_exec(_decrypt, key, cipherText);
export const symDecrypt = (
key: Uint8Array,
cipherText: Uint8Array,
AAD?: Uint8Array
): Uint8Array => _exec(_decrypt, key, cipherText, AAD);

/** @deprecated - use `symEncrypt` instead. */
export const aesEncrypt = symEncrypt; // TODO: delete
Expand All @@ -21,13 +27,14 @@ export const aesDecrypt = symDecrypt; // TODO: delete
function _exec(
callback: typeof _encrypt | typeof _decrypt,
key: Uint8Array,
data: Uint8Array
data: Uint8Array,
AAD?: Uint8Array
): Uint8Array {
const algorithm = symmetricAlgorithm();
if (algorithm === "aes-256-gcm") {
return callback(aes256gcm, key, data, symmetricNonceLength(), AEAD_TAG_LENGTH);
return callback(aes256gcm, key, data, symmetricNonceLength(), AEAD_TAG_LENGTH, AAD);
} else if (algorithm === "xchacha20") {
return callback(xchacha20, key, data, XCHACHA20_NONCE_LENGTH, AEAD_TAG_LENGTH);
return callback(xchacha20, key, data, XCHACHA20_NONCE_LENGTH, AEAD_TAG_LENGTH, AAD);
} else if (algorithm === "aes-256-cbc") {
// NOT RECOMMENDED. There is neither AAD nor AEAD tag in cbc mode
// aes-256-cbc always uses 16 bytes iv
Expand All @@ -40,32 +47,44 @@ function _exec(
function _encrypt(
func: (key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array) => Cipher,
key: Uint8Array,
plainText: Uint8Array,
data: Uint8Array,
nonceLength: 12 | 16 | 24,
tagLength: 16 | 0
tagLength: 16 | 0,
AAD?: Uint8Array
): Uint8Array {
const nonce = randomBytes(nonceLength);
const cipher = func(key, nonce);
const ciphered = cipher.encrypt(plainText); // encrypted || tag
const cipher = func(key, nonce, AAD);
// @noble/ciphers format: cipherText || tag
const encrypted = cipher.encrypt(data);

const encrypted = ciphered.subarray(0, ciphered.length - tagLength);
const tag = ciphered.subarray(ciphered.length - tagLength);
return concatBytes(nonce, tag, encrypted);
if (tagLength === 0) {
return concatBytes(nonce, encrypted);
}

const cipherTextLength = encrypted.length - tagLength;
const cipherText = encrypted.subarray(0, cipherTextLength);
const tag = encrypted.subarray(cipherTextLength);
// ecies payload format: pk || nonce || tag || cipherText
return concatBytes(nonce, tag, cipherText);
}

function _decrypt(
func: (key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array) => Cipher,
key: Uint8Array,
cipherText: Uint8Array,
data: Uint8Array,
nonceLength: 12 | 16 | 24,
tagLength: 16 | 0
tagLength: 16 | 0,
AAD?: Uint8Array
): Uint8Array {
const nonceTagLength = nonceLength + tagLength;
const nonce = cipherText.subarray(0, nonceLength);
const tag = cipherText.subarray(nonceLength, nonceTagLength);
const encrypted = cipherText.subarray(nonceTagLength);
const nonce = data.subarray(0, nonceLength);
const cipher = func(key, Uint8Array.from(nonce), AAD); // to reset byteOffset
const encrypted = data.subarray(nonceLength);

if (tagLength === 0) {
return cipher.decrypt(encrypted);
}

const decipher = func(key, Uint8Array.from(nonce)); // to reset byteOffset
const ciphered = concatBytes(encrypted, tag);
return decipher.decrypt(ciphered);
const tag = encrypted.subarray(0, tagLength);
const cipherText = encrypted.subarray(tagLength);
return cipher.decrypt(concatBytes(cipherText, tag));
}
1 change: 1 addition & 0 deletions tests/utils/elliptic.known.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";

import { hexToBytes } from "@noble/hashes/utils";

import { ECIES_CONFIG } from "../../src";
import { decodeHex, getSharedPoint, hexToPublicKey } from "../../src/utils";

Expand Down
9 changes: 2 additions & 7 deletions tests/utils/elliptic.random.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ describe("test random elliptic", () => {
expect(isValidPrivateKey(key)).toBe(true);
}

it("tests default", () => {
it("tests secp256k1", () => {
ECIES_CONFIG.ellipticCurve = "secp256k1";
testRandom();
});

Expand All @@ -24,10 +25,4 @@ describe("test random elliptic", () => {
testRandom();
ECIES_CONFIG.ellipticCurve = "secp256k1";
});

it("tests ed25519", () => {
ECIES_CONFIG.ellipticCurve = "ed25519";
testRandom();
ECIES_CONFIG.ellipticCurve = "secp256k1";
});
});
Loading

0 comments on commit 1e12651

Please sign in to comment.