refactor: cipher type as parameter

This commit is contained in:
Richard Ramos 2023-11-21 12:38:43 -04:00
parent 85c235e7f5
commit 938789d7cd
No known key found for this signature in database
GPG Key ID: 1CE87DB518195760
6 changed files with 72 additions and 150 deletions

16
src/chachapoly.ts Normal file
View File

@ -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);
}
}

View File

@ -1,4 +1,3 @@
import { ChaCha20Poly1305 } from "@stablelib/chacha20poly1305";
import { Hash } from "@stablelib/hash"; import { Hash } from "@stablelib/hash";
import { HKDF as hkdf } from "@stablelib/hkdf"; import { HKDF as hkdf } from "@stablelib/hkdf";
import { RandomSource } from "@stablelib/random"; import { RandomSource } from "@stablelib/random";
@ -32,43 +31,6 @@ export function HKDF(
return result; 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 { export function hash(hash: new () => Hash, data: Uint8Array): bytes32 {
const h = new hash(); const h = new hash();
h.update(data); h.update(data);
@ -89,6 +51,31 @@ export function commitPublicKey(h: new () => Hash, publicKey: bytes32, r: Uint8A
return hash(h, data); 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 DiffieHellman key exchange * Represents a key uses for DiffieHellman key exchange
*/ */

View File

@ -3,7 +3,7 @@ import { randomBytes } from "@stablelib/random";
import { expect } from "chai"; import { expect } from "chai";
import { equals as uint8ArrayEquals } from "uint8arrays/equals"; import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import { chaCha20Poly1305Encrypt } from "./crypto"; import { ChaChaPoly } from "./chachapoly";
import { DH25519 } from "./dh25519"; import { DH25519 } from "./dh25519";
import { Handshake, HandshakeStepResult } from "./handshake"; import { Handshake, HandshakeStepResult } from "./handshake";
import { MessageNametagBuffer, MessageNametagLength } from "./messagenametag"; import { MessageNametagBuffer, MessageNametagLength } from "./messagenametag";
@ -11,26 +11,16 @@ import { CipherState, createEmptyKey, SymmetricState } from "./noise";
import { MAX_NONCE, Nonce } from "./nonce"; import { MAX_NONCE, Nonce } from "./nonce";
import { NoiseHandshakePatterns } from "./patterns"; import { NoiseHandshakePatterns } from "./patterns";
import { PayloadV2 } from "./payload"; import { PayloadV2 } from "./payload";
import { ChaChaPolyCipherState, NoisePublicKey } from "./publickey"; import { NoisePublicKey } from "./publickey";
function randomCipherState(rng: HMACDRBG, nonce: number = 0): CipherState { function randomCipherState(rng: HMACDRBG, nonce: number = 0): CipherState {
const randomCipherState = new CipherState(); return new CipherState(new ChaChaPoly(), rng.randomBytes(32), new Nonce(nonce));
randomCipherState.n = new Nonce(nonce);
randomCipherState.k = rng.randomBytes(32);
return randomCipherState;
} }
function c(input: Uint8Array): Uint8Array { function c(input: Uint8Array): Uint8Array {
return new Uint8Array(input); 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 { function randomNoisePublicKey(): NoisePublicKey {
const dhKey = new DH25519(); const dhKey = new DH25519();
const keypair = dhKey.generateKeyPair(); const keypair = dhKey.generateKeyPair();
@ -48,88 +38,6 @@ function randomPayloadV2(rng: HMACDRBG): PayloadV2 {
describe("js-noise", () => { describe("js-noise", () => {
const rng = new HMACDRBG(undefined); 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 () { it("PayloadV2: serialize/deserialize PayloadV2 to byte sequence", function () {
const payload2 = randomPayloadV2(rng); const payload2 = randomPayloadV2(rng);
const serializedPayload = payload2.serialize(); const serializedPayload = payload2.serialize();
@ -155,6 +63,7 @@ describe("js-noise", () => {
// We generate a random Cipher State, associated data ad and plaintext // We generate a random Cipher State, associated data ad and plaintext
let cipherState = randomCipherState(rng); let cipherState = randomCipherState(rng);
let nonceValue = Math.floor(Math.random() * MAX_NONCE); let nonceValue = Math.floor(Math.random() * MAX_NONCE);
const ad = randomBytes(128, rng); const ad = randomBytes(128, rng);
let plaintext = randomBytes(128, rng); let plaintext = randomBytes(128, rng);
let nonce = new Nonce(nonceValue); let nonce = new Nonce(nonceValue);
@ -205,7 +114,6 @@ describe("js-noise", () => {
cipherState = randomCipherState(rng); cipherState = randomCipherState(rng);
cipherState.setNonce(new Nonce(MAX_NONCE)); cipherState.setNonce(new Nonce(MAX_NONCE));
plaintext = randomBytes(128, rng); 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 // 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++) { for (let i = 0; i < 5; i++) {
try { try {
@ -225,8 +133,8 @@ describe("js-noise", () => {
cipherState.setNonce(new Nonce(MAX_NONCE)); cipherState.setNonce(new Nonce(MAX_NONCE));
plaintext = randomBytes(128, rng); plaintext = randomBytes(128, rng);
// We perform encryption using the Cipher State key, NonceMax and ad // 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 = chaCha20Poly1305Encrypt(plaintext, cipherState.getNonce().getBytes(), ad, cipherState.getKey()); 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 // 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 // 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

View File

@ -24,7 +24,7 @@ import {
PayloadV2ProtocolIDs, PayloadV2ProtocolIDs,
PreMessagePattern, PreMessagePattern,
} from "./patterns.js"; } from "./patterns.js";
import { ChaChaPolyCipherState, NoisePublicKey } from "./publickey.js"; import { NoisePublicKey } from "./publickey.js";
import { QR } from "./qr.js"; import { QR } from "./qr.js";
export { export {
@ -45,7 +45,7 @@ export {
PayloadV2ProtocolIDs, PayloadV2ProtocolIDs,
PreMessagePattern, PreMessagePattern,
}; };
export { ChaChaPolyCipherState, NoisePublicKey }; export { NoisePublicKey };
export { MessageNametagBuffer }; export { MessageNametagBuffer };
export { NoiseHandshakeDecoder, NoiseHandshakeEncoder, NoiseSecureTransferDecoder, NoiseSecureTransferEncoder }; export { NoiseHandshakeDecoder, NoiseHandshakeEncoder, NoiseSecureTransferDecoder, NoiseSecureTransferEncoder };
export { QR }; export { QR };

View File

@ -5,7 +5,7 @@ import { concat as uint8ArrayConcat } from "uint8arrays/concat";
import { equals as uint8ArrayEquals } from "uint8arrays/equals"; import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import type { bytes32 } from "./@types/basic.js"; 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 { Nonce } from "./nonce.js";
import { HandshakePattern } from "./patterns.js"; import { HandshakePattern } from "./patterns.js";
@ -66,14 +66,16 @@ export class CipherState {
// For performance reasons, the nonce is represented as a Nonce object // 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. // The nonce is treated as a uint64, even though the underlying `number` only has 52 safely-available bits.
n: Nonce; n: Nonce;
cipher: Cipher;
/** /**
* @param k encryption key * @param k encryption key
* @param n nonce * @param n nonce
*/ */
constructor(k: bytes32 = createEmptyKey(), n = new Nonce()) { constructor(cipher: Cipher, k: bytes32 = createEmptyKey(), n = new Nonce()) {
this.k = k; this.k = k;
this.n = n; this.n = n;
this.cipher = cipher;
} }
/** /**
@ -81,7 +83,7 @@ export class CipherState {
* @returns a copy of the CipherState * @returns a copy of the CipherState
*/ */
clone(): 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 (this.hasKey()) {
// If an encryption key is set in the Cipher state, we proceed with encryption // 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.increment();
this.n.assertValue(); this.n.assertValue();
@ -138,7 +140,7 @@ export class CipherState {
this.n.assertValue(); this.n.assertValue();
if (this.hasKey()) { 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) { if (!plaintext) {
throw new Error("decryptWithAd failed"); throw new Error("decryptWithAd failed");
} }
@ -220,7 +222,7 @@ export class SymmetricState {
constructor(private readonly handshakePattern: HandshakePattern) { constructor(private readonly handshakePattern: HandshakePattern) {
this.h = hashProtocol(handshakePattern.hash, handshakePattern.name); this.h = hashProtocol(handshakePattern.hash, handshakePattern.name);
this.ck = this.h; 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 // We derive two keys using HKDF
const [ck, tempK] = HKDF(this.handshakePattern.hash, this.ck, inputKeyMaterial, 32, 2); 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 // 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; this.ck = ck;
log("mixKey", this.ck, this.cs.k); log("mixKey", this.ck, this.cs.k);
} }
@ -288,7 +290,7 @@ export class SymmetricState {
this.mixHash(tmpKey1); this.mixHash(tmpKey1);
// Updates the Cipher state's key // 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." // 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); 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 // Returns a tuple of two Cipher States initialized with the derived keys
return { return {
cs1: new CipherState(tmpKey1), cs1: new CipherState(this.handshakePattern.cipher, tmpKey1),
cs2: new CipherState(tmpKey2), cs2: new CipherState(this.handshakePattern.cipher, tmpKey2),
}; };
} }

View File

@ -1,8 +1,9 @@
import { Hash } from "@stablelib/hash"; import { Hash } from "@stablelib/hash";
import { SHA256 } from "@stablelib/sha256"; import { SHA256 } from "@stablelib/sha256";
import { DHKey } from "./crypto"; import { ChaChaPoly } from "./chachapoly.js";
import { DH25519 } from "./dh25519"; import { Cipher, DHKey } from "./crypto.js";
import { DH25519 } from "./dh25519.js";
/** /**
* The Noise tokens appearing in Noise (pre)message patterns * The Noise tokens appearing in Noise (pre)message patterns
@ -75,15 +76,18 @@ export class MessagePattern {
*/ */
export class HandshakePattern { export class HandshakePattern {
public readonly dhKey: DHKey; public readonly dhKey: DHKey;
public readonly cipher: Cipher;
constructor( constructor(
public readonly name: string, public readonly name: string,
dhKeyType: new () => DHKey, dhKeyType: new () => DHKey,
cipherType: new () => Cipher,
public readonly hash: new () => Hash, public readonly hash: new () => Hash,
public readonly preMessagePatterns: Array<PreMessagePattern>, public readonly preMessagePatterns: Array<PreMessagePattern>,
public readonly messagePatterns: Array<MessagePattern> public readonly messagePatterns: Array<MessagePattern>
) { ) {
this.dhKey = new dhKeyType(); this.dhKey = new dhKeyType();
this.cipher = new cipherType();
} }
/** /**
@ -113,6 +117,7 @@ export const NoiseHandshakePatterns: Record<string, HandshakePattern> = {
Noise_K1K1_25519_ChaChaPoly_SHA256: new HandshakePattern( Noise_K1K1_25519_ChaChaPoly_SHA256: new HandshakePattern(
"Noise_K1K1_25519_ChaChaPoly_SHA256", "Noise_K1K1_25519_ChaChaPoly_SHA256",
DH25519, DH25519,
ChaChaPoly,
SHA256, SHA256,
[ [
new PreMessagePattern(MessageDirection.r, [NoiseTokens.s]), new PreMessagePattern(MessageDirection.r, [NoiseTokens.s]),
@ -127,6 +132,7 @@ export const NoiseHandshakePatterns: Record<string, HandshakePattern> = {
Noise_XK1_25519_ChaChaPoly_SHA256: new HandshakePattern( Noise_XK1_25519_ChaChaPoly_SHA256: new HandshakePattern(
"Noise_XK1_25519_ChaChaPoly_SHA256", "Noise_XK1_25519_ChaChaPoly_SHA256",
DH25519, DH25519,
ChaChaPoly,
SHA256, SHA256,
[new PreMessagePattern(MessageDirection.l, [NoiseTokens.s])], [new PreMessagePattern(MessageDirection.l, [NoiseTokens.s])],
[ [
@ -138,6 +144,7 @@ export const NoiseHandshakePatterns: Record<string, HandshakePattern> = {
Noise_XX_25519_ChaChaPoly_SHA256: new HandshakePattern( Noise_XX_25519_ChaChaPoly_SHA256: new HandshakePattern(
"Noise_XX_25519_ChaChaPoly_SHA256", "Noise_XX_25519_ChaChaPoly_SHA256",
DH25519, DH25519,
ChaChaPoly,
SHA256, SHA256,
[], [],
[ [
@ -149,6 +156,7 @@ export const NoiseHandshakePatterns: Record<string, HandshakePattern> = {
Noise_XXpsk0_25519_ChaChaPoly_SHA256: new HandshakePattern( Noise_XXpsk0_25519_ChaChaPoly_SHA256: new HandshakePattern(
"Noise_XXpsk0_25519_ChaChaPoly_SHA256", "Noise_XXpsk0_25519_ChaChaPoly_SHA256",
DH25519, DH25519,
ChaChaPoly,
SHA256, SHA256,
[], [],
[ [
@ -160,6 +168,7 @@ export const NoiseHandshakePatterns: Record<string, HandshakePattern> = {
Noise_WakuPairing_25519_ChaChaPoly_SHA256: new HandshakePattern( Noise_WakuPairing_25519_ChaChaPoly_SHA256: new HandshakePattern(
"Noise_WakuPairing_25519_ChaChaPoly_SHA256", "Noise_WakuPairing_25519_ChaChaPoly_SHA256",
DH25519, DH25519,
ChaChaPoly,
SHA256, SHA256,
[new PreMessagePattern(MessageDirection.l, [NoiseTokens.e])], [new PreMessagePattern(MessageDirection.l, [NoiseTokens.e])],
[ [