refactor: cipher type as parameter

This commit is contained in:
Richard Ramos 2023-11-21 12:38:43 -04:00 committed by richΛrd
parent 932d9a0263
commit 32e64030ed
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 { 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 DiffieHellman key exchange
*/

View File

@ -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

View File

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

View File

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

View File

@ -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<PreMessagePattern>,
public readonly messagePatterns: Array<MessagePattern>
) {
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",
DH25519,
ChaChaPoly,
SHA256,
[
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",
DH25519,
ChaChaPoly,
SHA256,
[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",
DH25519,
ChaChaPoly,
SHA256,
[],
[
@ -149,6 +156,7 @@ export const NoiseHandshakePatterns: Record<string, HandshakePattern> = {
Noise_XXpsk0_25519_ChaChaPoly_SHA256: new HandshakePattern(
"Noise_XXpsk0_25519_ChaChaPoly_SHA256",
DH25519,
ChaChaPoly,
SHA256,
[],
[
@ -160,6 +168,7 @@ export const NoiseHandshakePatterns: Record<string, HandshakePattern> = {
Noise_WakuPairing_25519_ChaChaPoly_SHA256: new HandshakePattern(
"Noise_WakuPairing_25519_ChaChaPoly_SHA256",
DH25519,
ChaChaPoly,
SHA256,
[new PreMessagePattern(MessageDirection.l, [NoiseTokens.e])],
[