diff --git a/src/chachapoly.ts b/src/chachapoly.ts new file mode 100644 index 0000000..9294c0d --- /dev/null +++ b/src/chachapoly.ts @@ -0,0 +1,16 @@ +import { ChaCha20Poly1305 } from "@stablelib/chacha20poly1305"; + +import { bytes32 } from "./@types/basic.js"; +import { Cipher } from "./crypto.js"; + +export class ChaChaPoly implements Cipher { + encrypt(k: bytes32, n: Uint8Array, ad: Uint8Array, plaintext: Uint8Array): Uint8Array { + const ctx = new ChaCha20Poly1305(k); + return ctx.seal(n, plaintext, ad); + } + + decrypt(k: bytes32, n: Uint8Array, ad: Uint8Array, ciphertext: Uint8Array): Uint8Array | null { + const ctx = new ChaCha20Poly1305(k); + return ctx.open(n, ciphertext, ad); + } +} diff --git a/src/crypto.ts b/src/crypto.ts index 21df8af..5c2eff5 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -1,4 +1,3 @@ -import { ChaCha20Poly1305 } from "@stablelib/chacha20poly1305"; import { Hash } from "@stablelib/hash"; import { HKDF as hkdf } from "@stablelib/hkdf"; import { RandomSource } from "@stablelib/random"; @@ -32,43 +31,6 @@ export function HKDF( return result; } -/** - * Encrypt and authenticate data using ChaCha20-Poly1305 - * @param plaintext data to encrypt - * @param nonce 12 byte little-endian nonce - * @param ad associated data - * @param k 32-byte key - * @returns sealed ciphertext including authentication tag - */ -export function chaCha20Poly1305Encrypt( - plaintext: Uint8Array, - nonce: Uint8Array, - ad: Uint8Array, - k: bytes32 -): Uint8Array { - const ctx = new ChaCha20Poly1305(k); - return ctx.seal(nonce, plaintext, ad); -} - -/** - * Authenticate and decrypt data using ChaCha20-Poly1305 - * @param ciphertext data to decrypt - * @param nonce 12 byte little-endian nonce - * @param ad associated data - * @param k 32-byte key - * @returns plaintext if decryption was successful, `null` otherwise - */ -export function chaCha20Poly1305Decrypt( - ciphertext: Uint8Array, - nonce: Uint8Array, - ad: Uint8Array, - k: bytes32 -): Uint8Array | null { - const ctx = new ChaCha20Poly1305(k); - - return ctx.open(nonce, ciphertext, ad); -} - export function hash(hash: new () => Hash, data: Uint8Array): bytes32 { const h = new hash(); h.update(data); @@ -89,6 +51,31 @@ export function commitPublicKey(h: new () => Hash, publicKey: bytes32, r: Uint8A return hash(h, data); } +/** + * Represents a Cipher + */ +export interface Cipher { + /** + * Encrypt and authenticate data + * @param k 32-byte key + * @param n 12 byte little-endian nonce + * @param ad associated data + * @param plaintext data to encrypt + * @returns sealed ciphertext including authentication tag + */ + encrypt(k: bytes32, n: Uint8Array, ad: Uint8Array, plaintext: Uint8Array): Uint8Array; + + /** + * Authenticate and decrypt data + * @param k 32-byte key + * @param n 12 byte little-endian nonce + * @param ad associated data + * @param ciphertext data to decrypt + * @returns plaintext if decryption was successful, `null` otherwise + */ + decrypt(k: bytes32, n: Uint8Array, ad: Uint8Array, ciphertext: Uint8Array): Uint8Array | null; +} + /** * Represents a key uses for Diffie–Hellman key exchange */ diff --git a/src/index.spec.ts b/src/index.spec.ts index 6efae03..1864e24 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -3,7 +3,7 @@ import { randomBytes } from "@stablelib/random"; import { expect } from "chai"; import { equals as uint8ArrayEquals } from "uint8arrays/equals"; -import { chaCha20Poly1305Encrypt } from "./crypto"; +import { ChaChaPoly } from "./chachapoly"; import { DH25519 } from "./dh25519"; import { Handshake, HandshakeStepResult } from "./handshake"; import { MessageNametagBuffer, MessageNametagLength } from "./messagenametag"; @@ -11,26 +11,16 @@ import { CipherState, createEmptyKey, SymmetricState } from "./noise"; import { MAX_NONCE, Nonce } from "./nonce"; import { NoiseHandshakePatterns } from "./patterns"; import { PayloadV2 } from "./payload"; -import { ChaChaPolyCipherState, NoisePublicKey } from "./publickey"; +import { NoisePublicKey } from "./publickey"; function randomCipherState(rng: HMACDRBG, nonce: number = 0): CipherState { - const randomCipherState = new CipherState(); - randomCipherState.n = new Nonce(nonce); - randomCipherState.k = rng.randomBytes(32); - return randomCipherState; + return new CipherState(new ChaChaPoly(), rng.randomBytes(32), new Nonce(nonce)); } function c(input: Uint8Array): Uint8Array { return new Uint8Array(input); } -function randomChaChaPolyCipherState(rng: HMACDRBG): ChaChaPolyCipherState { - const k = rng.randomBytes(32); - const n = rng.randomBytes(16); - const ad = rng.randomBytes(32); - return new ChaChaPolyCipherState(k, n, ad); -} - function randomNoisePublicKey(): NoisePublicKey { const dhKey = new DH25519(); const keypair = dhKey.generateKeyPair(); @@ -48,88 +38,6 @@ function randomPayloadV2(rng: HMACDRBG): PayloadV2 { describe("js-noise", () => { const rng = new HMACDRBG(undefined); - it("ChaChaPoly Encryption/Decryption: random byte sequences", function () { - const cipherState = randomChaChaPolyCipherState(rng); - - // We encrypt/decrypt random byte sequences - const plaintext = rng.randomBytes(128); - const ciphertext = cipherState.encrypt(plaintext); - const decrypted = cipherState.decrypt(ciphertext); - - expect(uint8ArrayEquals(decrypted, plaintext)).to.be.true; - }); - - it("ChaChaPoly Encryption/Decryption: random byte sequences", function () { - const cipherState = randomChaChaPolyCipherState(rng); - - // We encrypt/decrypt random byte sequences - const plaintext = rng.randomBytes(128); - const ciphertext = cipherState.encrypt(plaintext); - const decrypted = cipherState.decrypt(ciphertext); - - expect(uint8ArrayEquals(decrypted, plaintext)).to.be.true; - }); - - it("Noise public keys: encrypt and decrypt a public key", function () { - const noisePublicKey = randomNoisePublicKey(); - const cipherState = randomChaChaPolyCipherState(rng); - - const encryptedPK = NoisePublicKey.encrypt(noisePublicKey, cipherState); - const decryptedPK = NoisePublicKey.decrypt(encryptedPK, cipherState); - - expect(noisePublicKey.equals(decryptedPK)).to.be.true; - }); - - it("Noise public keys: decrypt an unencrypted public key", function () { - const noisePublicKey = randomNoisePublicKey(); - const cipherState = randomChaChaPolyCipherState(rng); - - const decryptedPK = NoisePublicKey.decrypt(noisePublicKey, cipherState); - - expect(noisePublicKey.equals(decryptedPK)).to.be.true; - }); - - it("Noise public keys: encrypt an encrypted public key", function () { - const noisePublicKey = randomNoisePublicKey(); - const cipherState = randomChaChaPolyCipherState(rng); - - const encryptedPK = NoisePublicKey.encrypt(noisePublicKey, cipherState); - const encryptedPK2 = NoisePublicKey.encrypt(encryptedPK, cipherState); - - expect(encryptedPK.equals(encryptedPK2)).to.be.true; - }); - - it("Noise public keys: encrypt, decrypt and decrypt a public key", function () { - const noisePublicKey = randomNoisePublicKey(); - const cipherState = randomChaChaPolyCipherState(rng); - - const encryptedPK = NoisePublicKey.encrypt(noisePublicKey, cipherState); - const decryptedPK = NoisePublicKey.decrypt(encryptedPK, cipherState); - const decryptedPK2 = NoisePublicKey.decrypt(decryptedPK, cipherState); - - expect(decryptedPK.equals(decryptedPK2)).to.be.true; - }); - - it("Noise public keys: serialize and deserialize an unencrypted public key", function () { - const noisePublicKey = randomNoisePublicKey(); - const serializedNoisePublicKey = noisePublicKey.serialize(); - const deserializedNoisePublicKey = NoisePublicKey.deserialize(serializedNoisePublicKey); - - expect(noisePublicKey.equals(deserializedNoisePublicKey)).to.be.true; - }); - - it("Noise public keys: encrypt, serialize, deserialize and decrypt a public key", function () { - const noisePublicKey = randomNoisePublicKey(); - const cipherState = randomChaChaPolyCipherState(rng); - - const encryptedPK = NoisePublicKey.encrypt(noisePublicKey, cipherState); - const serializedNoisePublicKey = encryptedPK.serialize(); - const deserializedNoisePublicKey = NoisePublicKey.deserialize(serializedNoisePublicKey); - const decryptedPK = NoisePublicKey.decrypt(deserializedNoisePublicKey, cipherState); - - expect(noisePublicKey.equals(decryptedPK)).to.be.true; - }); - it("PayloadV2: serialize/deserialize PayloadV2 to byte sequence", function () { const payload2 = randomPayloadV2(rng); const serializedPayload = payload2.serialize(); @@ -155,6 +63,7 @@ describe("js-noise", () => { // We generate a random Cipher State, associated data ad and plaintext let cipherState = randomCipherState(rng); let nonceValue = Math.floor(Math.random() * MAX_NONCE); + const ad = randomBytes(128, rng); let plaintext = randomBytes(128, rng); let nonce = new Nonce(nonceValue); @@ -205,7 +114,6 @@ describe("js-noise", () => { cipherState = randomCipherState(rng); cipherState.setNonce(new Nonce(MAX_NONCE)); plaintext = randomBytes(128, rng); - // We test if encryption fails. Any subsequent encryption call over the Cipher State should fail similarly and leave the nonce unchanged for (let i = 0; i < 5; i++) { try { @@ -225,8 +133,8 @@ describe("js-noise", () => { cipherState.setNonce(new Nonce(MAX_NONCE)); plaintext = randomBytes(128, rng); - // We perform encryption using the Cipher State key, NonceMax and ad - ciphertext = chaCha20Poly1305Encrypt(plaintext, cipherState.getNonce().getBytes(), ad, cipherState.getKey()); + // We perform encryption using the Cipher State key, NonceMax and ad (not using the cypher state directly so it does not trigger the max nonce error) + ciphertext = cipherState.cipher.encrypt(cipherState.getKey(), cipherState.getNonce().getBytes(), ad, plaintext); // At this point ciphertext is a proper encryption of the original plaintext obtained with nonce equal to NonceMax // We can now test if decryption fails with a NoiseNonceMaxError error. Any subsequent decryption call over the Cipher State should fail similarly and leave the nonce unchanged diff --git a/src/index.ts b/src/index.ts index 125764b..f7b2081 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,7 +24,7 @@ import { PayloadV2ProtocolIDs, PreMessagePattern, } from "./patterns.js"; -import { ChaChaPolyCipherState, NoisePublicKey } from "./publickey.js"; +import { NoisePublicKey } from "./publickey.js"; import { QR } from "./qr.js"; export { @@ -45,7 +45,7 @@ export { PayloadV2ProtocolIDs, PreMessagePattern, }; -export { ChaChaPolyCipherState, NoisePublicKey }; +export { NoisePublicKey }; export { MessageNametagBuffer }; export { NoiseHandshakeDecoder, NoiseHandshakeEncoder, NoiseSecureTransferDecoder, NoiseSecureTransferEncoder }; export { QR }; diff --git a/src/noise.ts b/src/noise.ts index f814082..c58b35a 100644 --- a/src/noise.ts +++ b/src/noise.ts @@ -5,7 +5,7 @@ import { concat as uint8ArrayConcat } from "uint8arrays/concat"; import { equals as uint8ArrayEquals } from "uint8arrays/equals"; import type { bytes32 } from "./@types/basic.js"; -import { chaCha20Poly1305Decrypt, chaCha20Poly1305Encrypt, hash, HKDF } from "./crypto.js"; +import { Cipher, hash, HKDF } from "./crypto.js"; import { Nonce } from "./nonce.js"; import { HandshakePattern } from "./patterns.js"; @@ -66,14 +66,16 @@ export class CipherState { // For performance reasons, the nonce is represented as a Nonce object // The nonce is treated as a uint64, even though the underlying `number` only has 52 safely-available bits. n: Nonce; + cipher: Cipher; /** * @param k encryption key * @param n nonce */ - constructor(k: bytes32 = createEmptyKey(), n = new Nonce()) { + constructor(cipher: Cipher, k: bytes32 = createEmptyKey(), n = new Nonce()) { this.k = k; this.n = n; + this.cipher = cipher; } /** @@ -81,7 +83,7 @@ export class CipherState { * @returns a copy of the CipherState */ clone(): CipherState { - return new CipherState(new Uint8Array(this.k), new Nonce(this.n.getUint64())); + return new CipherState(this.cipher, new Uint8Array(this.k), new Nonce(this.n.getUint64())); } /** @@ -114,7 +116,7 @@ export class CipherState { if (this.hasKey()) { // If an encryption key is set in the Cipher state, we proceed with encryption - ciphertext = chaCha20Poly1305Encrypt(plaintext, this.n.getBytes(), ad, this.k); + ciphertext = this.cipher.encrypt(this.k, this.n.getBytes(), ad, plaintext); this.n.increment(); this.n.assertValue(); @@ -138,7 +140,7 @@ export class CipherState { this.n.assertValue(); if (this.hasKey()) { - const plaintext = chaCha20Poly1305Decrypt(ciphertext, this.n.getBytes(), ad, this.k); + const plaintext = this.cipher.decrypt(this.k, this.n.getBytes(), ad, ciphertext); if (!plaintext) { throw new Error("decryptWithAd failed"); } @@ -220,7 +222,7 @@ export class SymmetricState { constructor(private readonly handshakePattern: HandshakePattern) { this.h = hashProtocol(handshakePattern.hash, handshakePattern.name); this.ck = this.h; - this.cs = new CipherState(); + this.cs = new CipherState(handshakePattern.cipher); } /** @@ -258,7 +260,7 @@ export class SymmetricState { // We derive two keys using HKDF const [ck, tempK] = HKDF(this.handshakePattern.hash, this.ck, inputKeyMaterial, 32, 2); // We update ck and the Cipher state's key k using the output of HDKF - this.cs = new CipherState(tempK); + this.cs = new CipherState(this.handshakePattern.cipher, tempK); this.ck = ck; log("mixKey", this.ck, this.cs.k); } @@ -288,7 +290,7 @@ export class SymmetricState { this.mixHash(tmpKey1); // Updates the Cipher state's key // Note for later support of 512 bits hash functions: "If HASHLEN is 64, then truncates tempKeys[2] to 32 bytes." - this.cs = new CipherState(tmpKey2); + this.cs = new CipherState(this.handshakePattern.cipher, tmpKey2); } /** @@ -337,8 +339,8 @@ export class SymmetricState { const [tmpKey1, tmpKey2] = HKDF(this.handshakePattern.hash, this.ck, new Uint8Array(0), 32, 2); // Returns a tuple of two Cipher States initialized with the derived keys return { - cs1: new CipherState(tmpKey1), - cs2: new CipherState(tmpKey2), + cs1: new CipherState(this.handshakePattern.cipher, tmpKey1), + cs2: new CipherState(this.handshakePattern.cipher, tmpKey2), }; } diff --git a/src/patterns.ts b/src/patterns.ts index 5cbc6c0..508595a 100644 --- a/src/patterns.ts +++ b/src/patterns.ts @@ -1,8 +1,9 @@ import { Hash } from "@stablelib/hash"; import { SHA256 } from "@stablelib/sha256"; -import { DHKey } from "./crypto"; -import { DH25519 } from "./dh25519"; +import { ChaChaPoly } from "./chachapoly.js"; +import { Cipher, DHKey } from "./crypto.js"; +import { DH25519 } from "./dh25519.js"; /** * The Noise tokens appearing in Noise (pre)message patterns @@ -75,15 +76,18 @@ export class MessagePattern { */ export class HandshakePattern { public readonly dhKey: DHKey; + public readonly cipher: Cipher; constructor( public readonly name: string, dhKeyType: new () => DHKey, + cipherType: new () => Cipher, public readonly hash: new () => Hash, public readonly preMessagePatterns: Array, public readonly messagePatterns: Array ) { this.dhKey = new dhKeyType(); + this.cipher = new cipherType(); } /** @@ -113,6 +117,7 @@ export const NoiseHandshakePatterns: Record = { Noise_K1K1_25519_ChaChaPoly_SHA256: new HandshakePattern( "Noise_K1K1_25519_ChaChaPoly_SHA256", DH25519, + ChaChaPoly, SHA256, [ new PreMessagePattern(MessageDirection.r, [NoiseTokens.s]), @@ -127,6 +132,7 @@ export const NoiseHandshakePatterns: Record = { Noise_XK1_25519_ChaChaPoly_SHA256: new HandshakePattern( "Noise_XK1_25519_ChaChaPoly_SHA256", DH25519, + ChaChaPoly, SHA256, [new PreMessagePattern(MessageDirection.l, [NoiseTokens.s])], [ @@ -138,6 +144,7 @@ export const NoiseHandshakePatterns: Record = { Noise_XX_25519_ChaChaPoly_SHA256: new HandshakePattern( "Noise_XX_25519_ChaChaPoly_SHA256", DH25519, + ChaChaPoly, SHA256, [], [ @@ -149,6 +156,7 @@ export const NoiseHandshakePatterns: Record = { Noise_XXpsk0_25519_ChaChaPoly_SHA256: new HandshakePattern( "Noise_XXpsk0_25519_ChaChaPoly_SHA256", DH25519, + ChaChaPoly, SHA256, [], [ @@ -160,6 +168,7 @@ export const NoiseHandshakePatterns: Record = { Noise_WakuPairing_25519_ChaChaPoly_SHA256: new HandshakePattern( "Noise_WakuPairing_25519_ChaChaPoly_SHA256", DH25519, + ChaChaPoly, SHA256, [new PreMessagePattern(MessageDirection.l, [NoiseTokens.e])], [