mirror of
https://github.com/waku-org/js-noise.git
synced 2025-02-23 00:18:27 +00:00
refactor: cipher type as parameter
This commit is contained in:
parent
932d9a0263
commit
32e64030ed
16
src/chachapoly.ts
Normal file
16
src/chachapoly.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
|
@ -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 };
|
||||
|
22
src/noise.ts
22
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),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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])],
|
||||
[
|
||||
|
Loading…
x
Reference in New Issue
Block a user