From 6cefe4ce2df8b919844acf876c64402d8f1d727e Mon Sep 17 00:00:00 2001 From: Weiliang Li Date: Wed, 19 Jul 2023 18:18:37 +0900 Subject: [PATCH] Add xchacha20 as an optional encryption backend --- .cspell.jsonc | 6 ++- CHANGELOG.md | 3 +- README.md | 11 ++++-- package.json | 1 + src/config.ts | 4 +- src/consts.ts | 1 + src/utils/symmetric.ts | 84 +++++++++++++++++++++++++++++++----------- tests/utils.test.ts | 19 ++++++++++ yarn.lock | 17 ++++++--- 9 files changed, 111 insertions(+), 35 deletions(-) diff --git a/.cspell.jsonc b/.cspell.jsonc index 1164590..02dbe95 100644 --- a/.cspell.jsonc +++ b/.cspell.jsonc @@ -8,12 +8,13 @@ "aes", "aes-256-gcm", "axios", + "chacha", "Codacy", "Codecov", "consts", "ecies", - "eciespy", "eciesjs", + "eciespy", "eth", "futoin-hkdf", "helloworld", @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 87902af..92a9e8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index ef7f9d0..203bcc7 100644 --- a/README.md +++ b/README.md @@ -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; } @@ -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). diff --git a/package.json b/package.json index 21bf22f..da9e87c 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ }, "version": "0.4.1", "dependencies": { + "@noble/ciphers": "^0.1.4", "@noble/curves": "^1.1.0" }, "devDependencies": { diff --git a/src/config.ts b/src/config.ts index 447217b..5111929 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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; diff --git a/src/consts.ts b/src/consts.ts index 8c81b98..6ac342c 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -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; diff --git a/src/utils/symmetric.ts b/src/utils/symmetric.ts index be94908..e1f621d 100644 --- a/src/utils/symmetric.ts +++ b/src/utils/symmetric.ts @@ -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]); @@ -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 { + 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]); +} + +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)); +} diff --git a/tests/utils.test.ts b/tests/utils.test.ts index e779d9b..4ca203c 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -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"; + }); }); diff --git a/yarn.lock b/yarn.lock index 4fa6e9a..d754109 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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" @@ -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"