mirror of
https://github.com/logos-messaging/js-noise.git
synced 2026-01-02 13:43:08 +00:00
chore: setup test
This commit is contained in:
parent
a0c6fb45ed
commit
11b97bafaf
@ -2,7 +2,7 @@
|
||||
"version": "0.1",
|
||||
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json",
|
||||
"language": "en",
|
||||
"words": ["Waku", "keypair", "nwaku"],
|
||||
"words": ["Waku", "keypair", "nwaku", "Nametag, ciphertext", "unpad", "blocksize", "Nametag", "Cipherstate", "Nametags", "HASHLEN", "ciphertext", "preshared", "libp2p"],
|
||||
"flagWords": [],
|
||||
"ignorePaths": [
|
||||
"package.json",
|
||||
|
||||
30
package-lock.json
generated
30
package-lock.json
generated
@ -1,16 +1,18 @@
|
||||
{
|
||||
"name": "js-noise",
|
||||
"name": "@waku/noise",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "js-noise",
|
||||
"name": "@waku/noise",
|
||||
"version": "0.0.1",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"dependencies": {
|
||||
"@stablelib/chacha20poly1305": "^1.0.1",
|
||||
"@stablelib/hkdf": "^1.0.1",
|
||||
"@stablelib/hmac-drbg": "^1.0.2",
|
||||
"@stablelib/random": "^1.0.2",
|
||||
"@stablelib/sha256": "^1.0.1",
|
||||
"@stablelib/x25519": "^1.0.1",
|
||||
"pkcs7-padding": "^0.1.1",
|
||||
@ -881,6 +883,18 @@
|
||||
"@stablelib/wipe": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@stablelib/hmac-drbg": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@stablelib/hmac-drbg/-/hmac-drbg-1.0.2.tgz",
|
||||
"integrity": "sha512-lZRnTm9VpuP864X7wOFv1o3t9t1DS94A3EQym9a2Eg8iUfIvp17uPP6LJKlTbD/vpUvbEDBTDoZV0uqxgcOdGw==",
|
||||
"dependencies": {
|
||||
"@stablelib/hash": "^1.0.1",
|
||||
"@stablelib/hmac": "^1.0.1",
|
||||
"@stablelib/random": "^1.0.2",
|
||||
"@stablelib/sha256": "^1.0.1",
|
||||
"@stablelib/wipe": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@stablelib/int": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@stablelib/int/-/int-1.0.1.tgz",
|
||||
@ -9647,6 +9661,18 @@
|
||||
"@stablelib/wipe": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@stablelib/hmac-drbg": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@stablelib/hmac-drbg/-/hmac-drbg-1.0.2.tgz",
|
||||
"integrity": "sha512-lZRnTm9VpuP864X7wOFv1o3t9t1DS94A3EQym9a2Eg8iUfIvp17uPP6LJKlTbD/vpUvbEDBTDoZV0uqxgcOdGw==",
|
||||
"requires": {
|
||||
"@stablelib/hash": "^1.0.1",
|
||||
"@stablelib/hmac": "^1.0.1",
|
||||
"@stablelib/random": "^1.0.2",
|
||||
"@stablelib/sha256": "^1.0.1",
|
||||
"@stablelib/wipe": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@stablelib/int": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@stablelib/int/-/int-1.0.1.tgz",
|
||||
|
||||
@ -22,8 +22,8 @@
|
||||
"fix:lint": "eslint src --ext .ts --ext .cjs --fix",
|
||||
"test": "run-s test:*",
|
||||
"test:lint": "eslint src --ext .ts",
|
||||
"test:prettier": "prettier \"src/**/*.ts\" \"./*.json\" \"*.*js\" \".github/**/*.yml\" --list-different",
|
||||
"test:spelling": "cspell \"{*.md,.github/*.md,src/**/*.ts}\"",
|
||||
"test:prettier": "prettier \"src/**/*.ts\" \"*.*js\" \".github/**/*.yml\" --list-different",
|
||||
"test:tsc": "tsc -p tsconfig.dev.json",
|
||||
"test:browser": "karma start karma.conf.cjs",
|
||||
"watch:build": "tsc -p tsconfig.json -w",
|
||||
@ -110,6 +110,8 @@
|
||||
"dependencies": {
|
||||
"@stablelib/chacha20poly1305": "^1.0.1",
|
||||
"@stablelib/hkdf": "^1.0.1",
|
||||
"@stablelib/hmac-drbg": "^1.0.2",
|
||||
"@stablelib/random": "^1.0.2",
|
||||
"@stablelib/sha256": "^1.0.1",
|
||||
"@stablelib/x25519": "^1.0.1",
|
||||
"pkcs7-padding": "^0.1.1",
|
||||
|
||||
@ -19,7 +19,7 @@ export class HandshakeStepResult {
|
||||
transportMessage: Uint8Array = new Uint8Array();
|
||||
}
|
||||
|
||||
// When a handshake is complete, the HandhshakeResult will contain the two
|
||||
// When a handshake is complete, the HandshakeResult will contain the two
|
||||
// Cipher States used to encrypt/decrypt outbound/inbound messages
|
||||
// The recipient static key rs and handshake hash values h are stored to address some possible future applications (channel-binding, session management, etc.).
|
||||
// However, are not required by Noise specifications and are thus optional
|
||||
@ -59,7 +59,7 @@ export class Handshake {
|
||||
// Advances 1 step in handshake
|
||||
// Each user in a handshake alternates writing and reading of handshake messages.
|
||||
// If the user is writing the handshake message, the transport message (if not empty) and eventually a non-empty message nametag has to be passed to transportMessage and messageNametag and readPayloadV2 can be left to its default value
|
||||
// It the user is reading the handshake message, the read payload v2 has to be passed to readPayloadV2 and the transportMessage can be left to its default values. Decryption is skipped if the payloadv2 read doesn't have a message nametag equal to messageNametag (empty input nametags are converted to all-0 MessageNametagLength bytes arrays)
|
||||
// It the user is reading the handshake message, the read payload v2 has to be passed to readPayloadV2 and the transportMessage can be left to its default values. Decryption is skipped if the PayloadV2 read doesn't have a message nametag equal to messageNametag (empty input nametags are converted to all-0 MessageNametagLength bytes arrays)
|
||||
stepHandshake(
|
||||
readPayloadV2: PayloadV2 = new PayloadV2(),
|
||||
transportMessage: Uint8Array = new Uint8Array(),
|
||||
@ -120,7 +120,7 @@ export class Handshake {
|
||||
const readHandshakeMessage = readPayloadV2.handshakeMessage;
|
||||
const readTransportMessage = readPayloadV2.transportMessage;
|
||||
|
||||
// Since we only read, nothing meanigful (i.e. public keys) is returned
|
||||
// Since we only read, nothing meaningful (i.e. public keys) is returned
|
||||
this.hs.processMessagePatternTokens(readHandshakeMessage);
|
||||
// We retrieve and store the (decrypted) received transport message by passing the messageNametag as extra additional data
|
||||
hsStepResult.transportMessage = this.hs.processMessagePatternPayload(
|
||||
|
||||
@ -27,7 +27,7 @@ export const NoisePaddingBlockSize = 248;
|
||||
// - the local and remote ephemeral/static keys e,s,re,rs (if any)
|
||||
// - the initiator flag (true if the user creating the state is the handshake initiator, false otherwise)
|
||||
// - the handshakePattern (containing the handshake protocol name, and (pre)message patterns)
|
||||
// This object is futher extended from specifications by storing:
|
||||
// This object is further extended from specifications by storing:
|
||||
// - a message pattern index msgPatternIdx indicating the next handshake message pattern to process
|
||||
// - the user's preshared psk, if any
|
||||
export class HandshakeState {
|
||||
|
||||
128
src/index.spec.ts
Normal file
128
src/index.spec.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import { HMACDRBG } from "@stablelib/hmac-drbg";
|
||||
import { randomBytes } from "@stablelib/random";
|
||||
import { expect } from "chai";
|
||||
import { equals as uint8ArrayEquals } from "uint8arrays/equals";
|
||||
|
||||
import { chaCha20Poly1305Encrypt, dh, generateX25519KeyPair } from "./crypto";
|
||||
import { CipherState } from "./noise";
|
||||
import { MAX_NONCE, Nonce } from "./nonce";
|
||||
|
||||
function randomCipherState(rng: HMACDRBG, nonce: number = 0): CipherState {
|
||||
const randomCipherState = new CipherState();
|
||||
randomCipherState.n = new Nonce(nonce);
|
||||
randomCipherState.k = rng.randomBytes(32);
|
||||
return randomCipherState;
|
||||
}
|
||||
|
||||
describe("js-noise", () => {
|
||||
it("Noise State Machine: Diffie-Hellman operation", function () {
|
||||
const aliceKey = generateX25519KeyPair();
|
||||
const bobKey = generateX25519KeyPair();
|
||||
|
||||
// A Diffie-Hellman operation between Alice's private key and Bob's public key must be equal to
|
||||
// a Diffie-hellman operation between Alice's public key and Bob's private key
|
||||
const dh1 = dh(aliceKey.privateKey, bobKey.publicKey);
|
||||
const dh2 = dh(bobKey.privateKey, aliceKey.publicKey);
|
||||
|
||||
expect(uint8ArrayEquals(dh1, dh2)).to.be.true;
|
||||
});
|
||||
|
||||
it("Noise State Machine: Cipher State primitives", function () {
|
||||
const rng = new HMACDRBG();
|
||||
|
||||
// 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);
|
||||
|
||||
// We set the random nonce generated in the cipher state
|
||||
cipherState.setNonce(nonce);
|
||||
|
||||
// We perform encryption
|
||||
let ciphertext = cipherState.encryptWithAd(ad, plaintext);
|
||||
|
||||
// After any encryption/decryption operation, the Cipher State's nonce increases by 1
|
||||
expect(cipherState.getNonce().getUint64()).to.be.equals(nonceValue + 1);
|
||||
|
||||
// We set the nonce back to its original value for decryption
|
||||
cipherState.setNonce(new Nonce(nonceValue));
|
||||
|
||||
// We decrypt (using the original nonce)
|
||||
const decrypted = cipherState.decryptWithAd(ad, ciphertext);
|
||||
|
||||
// We check if encryption and decryption are correct and that nonce correctly increased after decryption
|
||||
expect(cipherState.getNonce().getUint64()).to.be.equals(nonceValue + 1);
|
||||
expect(uint8ArrayEquals(plaintext, decrypted)).to.be.true;
|
||||
|
||||
// If a Cipher State has no key set, encryptWithAd should return the plaintext without increasing the nonce
|
||||
cipherState.setCipherStateKey(CipherState.createEmptyKey());
|
||||
nonce = cipherState.getNonce();
|
||||
nonceValue = nonce.getUint64();
|
||||
plaintext = randomBytes(128, rng);
|
||||
ciphertext = cipherState.encryptWithAd(ad, plaintext);
|
||||
|
||||
expect(uint8ArrayEquals(ciphertext, plaintext)).to.be.true;
|
||||
expect(cipherState.getNonce().getUint64()).to.be.equals(nonceValue);
|
||||
|
||||
// If a Cipher State has no key set, decryptWithAd should return the ciphertext without increasing the nonce
|
||||
cipherState.setCipherStateKey(CipherState.createEmptyKey());
|
||||
nonce = cipherState.getNonce();
|
||||
nonceValue = nonce.getUint64();
|
||||
ciphertext = randomBytes(128, rng);
|
||||
plaintext = cipherState.decryptWithAd(ad, ciphertext);
|
||||
|
||||
expect(uint8ArrayEquals(ciphertext, plaintext)).to.be.true;
|
||||
expect(cipherState.getNonce().getUint64()).to.be.equals(nonceValue);
|
||||
|
||||
// A Cipher State cannot have a nonce greater or equal 0xffffffff in this implementation (see nonce.ts for details)
|
||||
// Note that nonce is increased after each encryption and decryption operation
|
||||
|
||||
// We generate a test Cipher State with nonce set to MaxNonce
|
||||
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 {
|
||||
ciphertext = cipherState.encryptWithAd(ad, plaintext);
|
||||
expect(true).to.be.false; // Should not reach this line
|
||||
} catch (err) {
|
||||
// Do nothing
|
||||
}
|
||||
expect(cipherState.getNonce().getUint64()).to.be.equals(MAX_NONCE + 1);
|
||||
}
|
||||
|
||||
// We generate a test Cipher State
|
||||
// Since nonce is increased after decryption as well, we need to generate a proper ciphertext in order to test MaxNonceError error handling
|
||||
// We cannot call encryptWithAd to encrypt a plaintext using a nonce equal MaxNonce, since this will trigger a MaxNonceError.
|
||||
// To perform such test, we then need to encrypt a test plaintext using directly ChaChaPoly primitive
|
||||
cipherState = randomCipherState(rng);
|
||||
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()
|
||||
);
|
||||
|
||||
// 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
|
||||
// Note that decryptWithAd doesn't fail in decrypting the ciphertext (otherwise a NoiseDecryptTagError would have been triggered)
|
||||
for (let i = 0; i < 5; i++) {
|
||||
try {
|
||||
plaintext = cipherState.decryptWithAd(ad, ciphertext);
|
||||
expect(true).to.be.false; // Should not reach this line
|
||||
} catch (err) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
expect(cipherState.getNonce().getUint64()).to.be.equals(MAX_NONCE + 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
22
src/noise.ts
22
src/noise.ts
@ -30,7 +30,7 @@ import { HandshakePattern } from "./patterns.js";
|
||||
# - A preshared key psk is processed by calling MixKeyAndHash(psk);
|
||||
# - When an ephemeral public key e is read or written, the handshake hash value h is updated by calling mixHash(e); If the handshake expects a psk, MixKey(e) is further called
|
||||
# - When an encrypted static public key s or a payload message m is read, it is decrypted with decryptAndHash;
|
||||
# - When a static public key s or a payload message is writted, it is encrypted with encryptAndHash;
|
||||
# - When a static public key s or a payload message is written, it is encrypted with encryptAndHash;
|
||||
# - When any Diffie-Hellman token ee, es, se, ss is read or written, the chaining key ck is updated by calling MixKey on the computed secret;
|
||||
# - If all tokens are processed, users compute two new Cipher States by calling Split;
|
||||
# - The two Cipher States obtained from Split are used to encrypt/decrypt outbound/inbound messages.
|
||||
@ -113,6 +113,10 @@ export class CipherState {
|
||||
if (!plaintext) {
|
||||
throw "decryptWithAd failed";
|
||||
}
|
||||
|
||||
this.n.increment();
|
||||
this.n.assertValue();
|
||||
|
||||
return plaintext;
|
||||
} else {
|
||||
// Otherwise we return the input ciphertext according to specification
|
||||
@ -198,14 +202,14 @@ export class SymmetricState {
|
||||
// Combines MixKey and MixHash
|
||||
mixKeyAndHash(inputKeyMaterial: Uint8Array): void {
|
||||
// Derives 3 keys using HKDF, the chaining key and the input key material
|
||||
const [tempk0, tempk1, tempk2] = getHKDF(this.ck, inputKeyMaterial);
|
||||
const [tmpKey0, tmpKey1, tmpKey2] = getHKDF(this.ck, inputKeyMaterial);
|
||||
// Sets the chaining key
|
||||
this.ck = tempk0;
|
||||
this.ck = tmpKey0;
|
||||
// Updates the handshake hash value
|
||||
this.mixHash(tempk1);
|
||||
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(tempk2);
|
||||
this.cs = new CipherState(tmpKey2);
|
||||
}
|
||||
|
||||
// EncryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||
@ -217,7 +221,7 @@ export class SymmetricState {
|
||||
): Uint8Array {
|
||||
// The additional data
|
||||
const ad = uint8ArrayConcat([this.h, extraAd]);
|
||||
// Note that if an encryption key is not set yet in the Cipher state, ciphertext will be equal to plaintex
|
||||
// Note that if an encryption key is not set yet in the Cipher state, ciphertext will be equal to plaintext
|
||||
const ciphertext = this.cs.encryptWithAd(ad, plaintext);
|
||||
// We call mixHash over the result
|
||||
this.mixHash(ciphertext);
|
||||
@ -245,11 +249,11 @@ export class SymmetricState {
|
||||
// Once a handshake is complete, returns two Cipher States to encrypt/decrypt outbound/inbound messages
|
||||
split(): { cs1: CipherState; cs2: CipherState } {
|
||||
// Derives 2 keys using HKDF and the chaining key
|
||||
const [tempk1, tempk2] = getHKDF(this.ck, new Uint8Array(0));
|
||||
const [tmpKey1, tmpKey2] = getHKDF(this.ck, new Uint8Array(0));
|
||||
// Returns a tuple of two Cipher States initialized with the derived keys
|
||||
return {
|
||||
cs1: new CipherState(tempk1),
|
||||
cs2: new CipherState(tempk2),
|
||||
cs1: new CipherState(tmpKey1),
|
||||
cs2: new CipherState(tmpKey2),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user