Skip to content

Commit

Permalink
Add xchacha20
Browse files Browse the repository at this point in the history
  • Loading branch information
kigawas committed Jul 21, 2023
1 parent 2354585 commit f7cbc4b
Show file tree
Hide file tree
Showing 11 changed files with 172 additions and 63 deletions.
6 changes: 4 additions & 2 deletions .cspell.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
"aes",
"aes-256-gcm",
"axios",
"chacha",
"Codacy",
"Codecov",
"consts",
"ecies",
"eciespy",
"eciesjs",
"eciespy",
"eth",
"futoin-hkdf",
"helloworld",
Expand All @@ -22,7 +23,8 @@
"Npm",
"Prv",
"querystring",
"secp256k1"
"secp256k1",
"xchacha20"
],
// flagWords - list of words to be always considered incorrect
// This is useful for offensive words and common spelling errors.
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@

# Changelog

## 0.4.1
## 0.4.1 ~ 0.4.2

- Add XChacha20 as an optional encryption backend
- Add configuration for more compatibility
- Bump dependencies

Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,13 @@ readonly compressed: Buffer;
Ephemeral key format in the payload and shared key in the key derivation can be configured as compressed or uncompressed format.

```ts
export type SymmetricAlgorithm = "aes-256-gcm" | "xchacha20";
export type NonceLength = 12 | 16; // bytes. Only for aes-256-gcm

class Config {
isEphemeralKeyCompressed: boolean = false;
isHkdfKeyCompressed: boolean = false;
symmetricAlgorithm: Algorithm = "aes-256-gcm"; // currently we only support aes-256-gcm
symmetricAlgorithm: SymmetricAlgorithm = "aes-256-gcm";
symmetricNonceLength: NonceLength = 16;
}

Expand All @@ -108,10 +111,12 @@ For example, if you set `isEphemeralKeyCompressed = true`, the payload would be

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)`.

If you set `symmetricNonceLength = 12`, then the nonce of aes-256-gcm would be 12 bytes.
If you set `symmetricAlgorithm = "xchacha20"`, plaintext data will encrypted with XChacha20-Poly1305.

If you set `symmetricNonceLength = 12`, then the nonce of aes-256-gcm would be 12 bytes. XChacha20-Poly1305's nonce is always 24 bytes.

For compatibility, make sure different applications share the same configuration.

## Changelog

See [CHANGELOG.md](./CHANGELOG.md)
See [CHANGELOG.md](./CHANGELOG.md).
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
"email": "[email protected]",
"url": "https://github.com/kigawas"
},
"repository": {
"type": "git",
"url": "https://github.com/ecies/js.git"
},
"version": "0.4.2",
"engines": {
"node": ">=16.0.0"
},
Expand All @@ -28,12 +33,8 @@
"build": "npx tsc",
"test": "jest"
},
"repository": {
"type": "git",
"url": "https://github.com/ecies/js.git"
},
"version": "0.4.1",
"dependencies": {
"@noble/ciphers": "^0.1.4",
"@noble/curves": "^1.1.0"
},
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { COMPRESSED_PUBLIC_KEY_SIZE, UNCOMPRESSED_PUBLIC_KEY_SIZE } from "./consts";

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

class Config {
isEphemeralKeyCompressed: boolean = false;
Expand Down
1 change: 1 addition & 0 deletions src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export const UNCOMPRESSED_PUBLIC_KEY_SIZE = 65;
export const ETH_PUBLIC_KEY_SIZE = 64;
export const SECRET_KEY_LENGTH = 32;
export const ONE = BigInt(1);
export const XCHACHA20_NONCE_LENGTH = 24;
export const AEAD_TAG_LENGTH = 16;
84 changes: 63 additions & 21 deletions src/utils/symmetric.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
import { xchacha20_poly1305 as xchacha20 } from "@noble/ciphers/chacha";
import { Cipher } from "@noble/ciphers/utils";
import { randomBytes } from "@noble/ciphers/webcrypto/utils";
import { hkdf } from "@noble/hashes/hkdf";
import { sha256 } from "@noble/hashes/sha256";
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
import { createCipheriv, createDecipheriv } from "crypto";

import {
NonceLength,
SymmetricAlgorithm,
symmetricAlgorithm,
symmetricNonceLength,
} from "../config";
import { AEAD_TAG_LENGTH } from "../consts";
import { NonceLength, symmetricAlgorithm, symmetricNonceLength } from "../config";
import { AEAD_TAG_LENGTH, XCHACHA20_NONCE_LENGTH } from "../consts";

function _aesEncrypt(
key: Buffer,
plainText: Buffer,
algorithm: SymmetricAlgorithm,
nonceLength: NonceLength
): Buffer {
function _aesEncrypt(key: Buffer, plainText: Buffer, nonceLength: NonceLength): Buffer {
const nonce = randomBytes(nonceLength);
const cipher = createCipheriv(algorithm, key, nonce);
const cipher = createCipheriv("aes-256-gcm", key, nonce);
const encrypted = Buffer.concat([cipher.update(plainText), cipher.final()]);
const tag = cipher.getAuthTag();
return Buffer.concat([nonce, tag, encrypted]);
Expand All @@ -26,24 +19,73 @@ function _aesEncrypt(
function _aesDecrypt(
key: Buffer,
cipherText: Buffer,
algorithm: SymmetricAlgorithm,
nonceLength: NonceLength
): Buffer {
const nonceTagLength = nonceLength + AEAD_TAG_LENGTH;
const nonce = cipherText.subarray(0, nonceLength);
const tag = cipherText.subarray(nonceLength, nonceLength + AEAD_TAG_LENGTH);
const ciphered = cipherText.subarray(nonceLength + AEAD_TAG_LENGTH);
const decipher = createDecipheriv(algorithm, key, nonce);
const tag = cipherText.subarray(nonceLength, nonceTagLength);
const ciphered = cipherText.subarray(nonceTagLength);
const decipher = createDecipheriv("aes-256-gcm", key, nonce);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(ciphered), decipher.final()]);
}

function _encrypt(
func: (key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array) => Cipher,
key: Buffer,
plainText: Buffer,
nonceLength: number
) {
const nonce = randomBytes(nonceLength);
const cipher = func(key, nonce);
const ciphered = cipher.encrypt(plainText);
const encrypted = ciphered.subarray(0, ciphered.length - AEAD_TAG_LENGTH);
const tag = ciphered.subarray(-AEAD_TAG_LENGTH);
return Buffer.concat([nonce, tag, encrypted]);
}

function _decrypt(
func: (key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array) => Cipher,
key: Buffer,
cipherText: Buffer,
nonceLength: number
) {
const nonceTagLength = nonceLength + AEAD_TAG_LENGTH;
const nonce = cipherText.subarray(0, nonceLength);
const tag = cipherText.subarray(nonceLength, nonceTagLength);
const ciphered = cipherText.subarray(nonceTagLength);

const decipher = func(key, nonce);
const res = new Uint8Array(AEAD_TAG_LENGTH + ciphered.length);
res.set(ciphered);
res.set(tag, ciphered.length);
return Buffer.from(decipher.decrypt(res));
}

export function aesEncrypt(key: Buffer, plainText: Buffer): Buffer {
return _aesEncrypt(key, plainText, symmetricAlgorithm(), symmetricNonceLength());
// TODO: Rename to symEncrypt
const algorithm = symmetricAlgorithm();
if (algorithm === "aes-256-gcm") {
return _aesEncrypt(key, plainText, symmetricNonceLength());
} else if (algorithm === "xchacha20") {
return _encrypt(xchacha20, key, plainText, XCHACHA20_NONCE_LENGTH);
} else {
throw new Error("Not implemented");
}
}

export function aesDecrypt(key: Buffer, cipherText: Buffer): Buffer {
return _aesDecrypt(key, cipherText, symmetricAlgorithm(), symmetricNonceLength());
// TODO: Rename to symDecrypt
const algorithm = symmetricAlgorithm();
if (algorithm === "aes-256-gcm") {
return _aesDecrypt(key, cipherText, symmetricNonceLength());
} else if (algorithm === "xchacha20") {
return _decrypt(xchacha20, key, cipherText, XCHACHA20_NONCE_LENGTH);
} else {
throw new Error("Not implemented");
}
}

export function deriveKey(master: Buffer) {
return Buffer.from(hkdf(sha256, master, undefined, undefined, 32));
}
2 changes: 1 addition & 1 deletion tests/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe("test encrypt and decrypt", () => {
pub: prv.publicKey.toHex(),
})
);
const encryptedKnown = Buffer.from(decodeHex(res.data));
const encryptedKnown = decodeHex(res.data);
const decrypted = decrypt(prv.toHex(), encryptedKnown);

expect(decrypted.toString()).toEqual(TEXT);
Expand Down
29 changes: 16 additions & 13 deletions tests/keys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,35 @@ const PUB_HEX =
"0x98afe4f150642cd05cc9d2fa36458ce0a58567daeaf5fde7333ba9b403011140" +
"a4e28911fcf83ab1f457a30b4959efc4b9306f514a4c3711a16a80e3b47eb58b";

function checkHkdf(k1: PrivateKey, k2: PrivateKey, knownHex: string) {
const derived1 = k1.encapsulate(k2.publicKey);
const derived2 = k1.publicKey.decapsulate(k2);
const knownDerived = Buffer.from(decodeHex(knownHex));
expect(derived1.equals(knownDerived)).toBe(true);
expect(derived2.equals(knownDerived)).toBe(true);
}

const two = Buffer.from(new Uint8Array(32));
two[31] = 2;
const three = Buffer.from(new Uint8Array(32));
three[31] = 3;

describe("test keys", () => {
function checkHkdf(k1: PrivateKey, k2: PrivateKey, knownHex: string) {
const derived1 = k1.encapsulate(k2.publicKey);
const derived2 = k1.publicKey.decapsulate(k2);

const knownDerived = decodeHex(knownHex);
expect(derived1).toEqual(knownDerived);
expect(derived2).toEqual(knownDerived);
}

it("tests invalid", () => {
// 0 < private key < group order int
const ERROR = "Invalid private key";
const groupOrderInt =
"fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141";
expect(() => PrivateKey.fromHex(groupOrderInt)).toThrow(Error);
expect(() => PrivateKey.fromHex(groupOrderInt)).toThrow(ERROR);

const groupOrderIntAdd1 =
"fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364142";

expect(() => PrivateKey.fromHex(groupOrderIntAdd1)).toThrow(Error);
expect(() => PrivateKey.fromHex(groupOrderIntAdd1)).toThrow(ERROR);

expect(() => PrivateKey.fromHex("0")).toThrow(Error);
expect(() => PrivateKey.fromHex("0")).toThrow(ERROR);
expect(() => new PrivateKey(decodeHex("0"))).toThrow(ERROR);
});

it("tests equal", () => {
Expand All @@ -49,7 +52,7 @@ describe("test keys", () => {
it("tests eth key compatibility", () => {
const ethPrv = PrivateKey.fromHex(PRV_HEX);
const ethPub = PublicKey.fromHex(PUB_HEX);
expect(ethPub.equals(ethPrv.publicKey)).toBe(true);
expect(ethPub).toEqual(ethPrv.publicKey);
expect(ethPub.compressed.toString("hex")).toEqual(
"0398afe4f150642cd05cc9d2fa36458ce0a58567daeaf5fde7333ba9b403011140"
);
Expand All @@ -62,7 +65,7 @@ describe("test keys", () => {
const k1 = new PrivateKey(two);
const k2 = new PrivateKey(three);

expect(k1.multiply(k2.publicKey).equals(k2.multiply(k1.publicKey))).toBe(true);
expect(k1.multiply(k2.publicKey)).toEqual(k2.multiply(k1.publicKey));

checkHkdf(
k1,
Expand Down
67 changes: 58 additions & 9 deletions tests/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
aesDecrypt,
aesEncrypt,
decodeHex,
deriveKey,
getValidSecret,
isValidPrivateKey,
remove0x,
Expand Down Expand Up @@ -34,19 +35,32 @@ describe("test elliptic utils", () => {
});

describe("test symmetric utils", () => {
it("tests aes with random key", () => {
function testRandomKey() {
const key = randomBytes(32);
const data = Buffer.from(TEXT);
expect(data).toEqual(aesDecrypt(key, aesEncrypt(key, data)));
}

it("tests hkdf with know key", () => {
const knownKey = decodeHex(
"0x8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d"
);
expect(knownKey).toEqual(
deriveKey(decodeHex("0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"))
);
});

it("tests aes with random key", () => {
testRandomKey();
});

it("tests aes decrypt with known key", () => {
const key = Buffer.from(
decodeHex("0000000000000000000000000000000000000000000000000000000000000000")
const key = decodeHex(
"0000000000000000000000000000000000000000000000000000000000000000"
);
const nonce = Buffer.from(decodeHex("0xf3e1ba810d2c8900b11312b7c725565f"));
const tag = Buffer.from(decodeHex("0Xec3b71e17c11dbe31484da9450edcf6c"));
const encrypted = Buffer.from(decodeHex("02d2ffed93b856f148b9"));
const nonce = decodeHex("0xf3e1ba810d2c8900b11312b7c725565f");
const tag = decodeHex("0Xec3b71e17c11dbe31484da9450edcf6c");
const encrypted = decodeHex("02d2ffed93b856f148b9");

const data = Buffer.concat([nonce, tag, encrypted]);
const decrypted = aesDecrypt(key, data);
Expand All @@ -56,10 +70,45 @@ describe("test symmetric utils", () => {
it("tests aes nonce length config", () => {
ECIES_CONFIG.symmetricNonceLength = 12;

const key = randomBytes(32);
const data = Buffer.from(TEXT);
expect(data.equals(aesDecrypt(key, aesEncrypt(key, data)))).toBe(true);
testRandomKey();

ECIES_CONFIG.symmetricNonceLength = 16;
});

it("tests xchacha20 decrypt with known key", () => {
ECIES_CONFIG.symmetricAlgorithm = "xchacha20";

const key = decodeHex(
"27bd6ec46292a3b421cdaf8a3f0ca759cbc67bcbe7c5855aa0d1e0700fd0e828"
);
const nonce = decodeHex("0xfbd5dd10431af533c403d6f4fa629931e5f31872d2f7e7b6");
const tag = decodeHex("0X5b5ccc27324af03b7ca92dd067ad6eb5");
const encrypted = decodeHex("aa0664f3c00a09d098bf");
const data = Buffer.concat([nonce, tag, encrypted]);

const decrypted = aesDecrypt(key, data);
expect(decrypted.toString()).toBe("helloworld");

ECIES_CONFIG.symmetricAlgorithm = "aes-256-gcm";
});

it("tests xchacha20 with random key", () => {
ECIES_CONFIG.symmetricAlgorithm = "xchacha20";

testRandomKey();

ECIES_CONFIG.symmetricAlgorithm = "aes-256-gcm";
});

it("tests not implemented", () => {
ECIES_CONFIG.symmetricAlgorithm = "" as any;

expect(testRandomKey).toThrow("Not implemented");

expect(() => aesDecrypt(randomBytes(32), decodeHex("01010e0e"))).toThrow(
"Not implemented"
);

ECIES_CONFIG.symmetricAlgorithm = "aes-256-gcm";
});
});
Loading

0 comments on commit f7cbc4b

Please sign in to comment.