Skip to content

Commit

Permalink
Add xchacha20 as an optional encryption backend
Browse files Browse the repository at this point in the history
  • Loading branch information
kigawas committed Jul 21, 2023
1 parent 2354585 commit 6cefe4c
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 35 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).
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
},
"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 } 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()]);
}

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_poly1305, key, plainText, XCHACHA20_NONCE_LENGTH);
} else {

Check warning on line 40 in src/utils/symmetric.ts

View check run for this annotation

Codecov / codecov/patch

src/utils/symmetric.ts#L38-L40

Added lines #L38 - L40 were not covered by tests
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_poly1305, 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));
}

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 tag = ciphered.subarray(-AEAD_TAG_LENGTH);
const encrypted = ciphered.subarray(AEAD_TAG_LENGTH);
return Buffer.concat([nonce, tag, encrypted]);
}

Check warning on line 74 in src/utils/symmetric.ts

View check run for this annotation

Codecov / codecov/patch

src/utils/symmetric.ts#L66-L74

Added lines #L66 - L74 were not covered by tests
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));
}
19 changes: 19 additions & 0 deletions tests/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,23 @@ describe("test symmetric utils", () => {

ECIES_CONFIG.symmetricNonceLength = 16;
});

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

const key = Buffer.from(
decodeHex("27bd6ec46292a3b421cdaf8a3f0ca759cbc67bcbe7c5855aa0d1e0700fd0e828")
);
const nonce = Buffer.from(
decodeHex("0xfbd5dd10431af533c403d6f4fa629931e5f31872d2f7e7b6")
);
const tag = Buffer.from(decodeHex("0X5b5ccc27324af03b7ca92dd067ad6eb5"));
const encrypted = Buffer.from(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";
});
});
17 changes: 11 additions & 6 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,11 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"

"@noble/ciphers@^0.1.4":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-0.1.4.tgz#96327dca147829ed9eee0d96cfdf7c57915765f0"
integrity sha512-d3ZR8vGSpy3v/nllS+bD/OMN5UZqusWiQqkyj7AwzTnhXFH72pF5oB4Ach6DQ50g5kXxC28LdaYBEpsyv9KOUQ==

"@noble/curves@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d"
Expand Down Expand Up @@ -906,9 +911,9 @@ camelcase@^6.2.0:
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==

caniuse-lite@^1.0.30001503:
version "1.0.30001516"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001516.tgz#621b1be7d85a8843ee7d210fd9d87b52e3daab3a"
integrity sha512-Wmec9pCBY8CWbmI4HsjBeQLqDTqV91nFVR83DnZpYyRnPI1wePDsTg0bGLPC5VU/3OIZV1fmxEea1b+tFKe86g==
version "1.0.30001517"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz#90fabae294215c3495807eb24fc809e11dc2f0a8"
integrity sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==

chalk@^2.0.0:
version "2.4.2"
Expand Down Expand Up @@ -1059,9 +1064,9 @@ diff@^4.0.1:
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==

electron-to-chromium@^1.4.431:
version "1.4.461"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.461.tgz#6b14af66042732bf883ab63a4d82cac8f35eb252"
integrity sha512-1JkvV2sgEGTDXjdsaQCeSwYYuhLRphRpc+g6EHTFELJXEiznLt3/0pZ9JuAOQ5p2rI3YxKTbivtvajirIfhrEQ==
version "1.4.464"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.464.tgz#2f94bad78dff34e527aacbfc5d0b1a33cf046507"
integrity sha512-guZ84yoou4+ILNdj0XEbmGs6DEWj6zpVOWYpY09GU66yEb0DSYvP/biBPzHn0GuW/3RC/pnaYNUWlQE1fJYtgA==

emittery@^0.13.1:
version "0.13.1"
Expand Down

0 comments on commit 6cefe4c

Please sign in to comment.