mirror of
https://github.com/logos-messaging/js-noise.git
synced 2026-01-03 14:13:07 +00:00
feat: noise public keys serialization
This commit is contained in:
parent
031c9e073b
commit
af283927a4
@ -393,7 +393,7 @@ export class HandshakeState {
|
||||
}
|
||||
|
||||
// We add the ephemeral public key to the Waku payload
|
||||
outHandshakeMessage.push(NoisePublicKey.to(this.e.publicKey));
|
||||
outHandshakeMessage.push(NoisePublicKey.fromPublicKey(this.e.publicKey));
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@ -5,11 +5,11 @@ import { equals as uint8ArrayEquals } from "uint8arrays/equals";
|
||||
|
||||
import { chaCha20Poly1305Encrypt, dh, generateX25519KeyPair } from "./crypto";
|
||||
import { Handshake, HandshakeStepResult } from "./handshake";
|
||||
import { CipherState, SymmetricState } from "./noise";
|
||||
import { CipherState, createEmptyKey, SymmetricState } from "./noise";
|
||||
import { MAX_NONCE, Nonce } from "./nonce";
|
||||
import { NoiseHandshakePatterns } from "./patterns";
|
||||
import { MessageNametagBuffer } from "./payload";
|
||||
import { NoisePublicKey } from "./publickey";
|
||||
import { ChaChaPolyCipherState, NoisePublicKey } from "./publickey";
|
||||
|
||||
function randomCipherState(rng: HMACDRBG, nonce: number = 0): CipherState {
|
||||
const randomCipherState = new CipherState();
|
||||
@ -22,9 +22,103 @@ 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 keypair = generateX25519KeyPair();
|
||||
return new NoisePublicKey(0, keypair.publicKey);
|
||||
}
|
||||
|
||||
describe("js-noise", () => {
|
||||
const rng = new HMACDRBG();
|
||||
|
||||
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("Noise State Machine: Diffie-Hellman operation", function () {
|
||||
const aliceKey = generateX25519KeyPair();
|
||||
const bobKey = generateX25519KeyPair();
|
||||
@ -65,7 +159,7 @@ describe("js-noise", () => {
|
||||
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());
|
||||
cipherState.setCipherStateKey(createEmptyKey());
|
||||
nonce = cipherState.getNonce();
|
||||
nonceValue = nonce.getUint64();
|
||||
plaintext = randomBytes(128, rng);
|
||||
@ -75,7 +169,7 @@ describe("js-noise", () => {
|
||||
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());
|
||||
cipherState.setCipherStateKey(createEmptyKey());
|
||||
nonce = cipherState.getNonce();
|
||||
nonceValue = nonce.getUint64();
|
||||
ciphertext = randomBytes(128, rng);
|
||||
@ -230,13 +324,13 @@ describe("js-noise", () => {
|
||||
// ==========
|
||||
|
||||
// If at least one mixKey is executed (as above), ck is non-empty
|
||||
expect(uint8ArrayEquals(symmetricState.getChainingKey(), CipherState.createEmptyKey())).to.be.false;
|
||||
expect(uint8ArrayEquals(symmetricState.getChainingKey(), createEmptyKey())).to.be.false;
|
||||
|
||||
// When a Symmetric State's ck is non-empty, we can execute split, which creates two distinct Cipher States cs1 and cs2
|
||||
// with non-empty encryption keys and nonce set to 0
|
||||
const { cs1, cs2 } = symmetricState.split();
|
||||
expect(uint8ArrayEquals(cs1.getKey(), CipherState.createEmptyKey())).to.be.false;
|
||||
expect(uint8ArrayEquals(cs2.getKey(), CipherState.createEmptyKey())).to.be.false;
|
||||
expect(uint8ArrayEquals(cs1.getKey(), createEmptyKey())).to.be.false;
|
||||
expect(uint8ArrayEquals(cs2.getKey(), createEmptyKey())).to.be.false;
|
||||
expect(cs1.getNonce().getUint64()).to.be.equals(0);
|
||||
expect(cs2.getNonce().getUint64()).to.be.equals(0);
|
||||
expect(uint8ArrayEquals(cs1.getKey(), cs2.getKey())).to.be.false;
|
||||
@ -453,7 +547,10 @@ describe("js-noise", () => {
|
||||
// <- s
|
||||
// ...
|
||||
// So we define accordingly the sequence of the pre-message public keys
|
||||
const preMessagePKs = [NoisePublicKey.to(aliceStaticKey.publicKey), NoisePublicKey.to(bobStaticKey.publicKey)];
|
||||
const preMessagePKs = [
|
||||
NoisePublicKey.fromPublicKey(aliceStaticKey.publicKey),
|
||||
NoisePublicKey.fromPublicKey(bobStaticKey.publicKey),
|
||||
];
|
||||
|
||||
const aliceHS = new Handshake({ hsPattern, staticKey: aliceStaticKey, preMessagePKs, initiator: true });
|
||||
const bobHS = new Handshake({ hsPattern, staticKey: bobStaticKey, preMessagePKs });
|
||||
@ -546,7 +643,7 @@ describe("js-noise", () => {
|
||||
// <- s
|
||||
// ...
|
||||
// So we define accordingly the sequence of the pre-message public keys
|
||||
const preMessagePKs = [NoisePublicKey.to(bobStaticKey.publicKey)];
|
||||
const preMessagePKs = [NoisePublicKey.fromPublicKey(bobStaticKey.publicKey)];
|
||||
|
||||
const aliceHS = new Handshake({ hsPattern, staticKey: aliceStaticKey, preMessagePKs, initiator: true });
|
||||
const bobHS = new Handshake({ hsPattern, staticKey: bobStaticKey, preMessagePKs });
|
||||
|
||||
22
src/noise.ts
22
src/noise.ts
@ -35,6 +35,15 @@ import { HandshakePattern } from "./patterns.js";
|
||||
#################################
|
||||
*/
|
||||
|
||||
export function createEmptyKey(): bytes32 {
|
||||
return new Uint8Array(32);
|
||||
}
|
||||
|
||||
export function isEmptyKey(k: bytes32): boolean {
|
||||
const emptyKey = createEmptyKey();
|
||||
return uint8ArrayEquals(emptyKey, k);
|
||||
}
|
||||
|
||||
// The Cipher State as in https://noiseprotocol.org/noise.html#the-cipherstate-object
|
||||
// Contains an encryption key k and a nonce n (used in Noise as a counter)
|
||||
export class CipherState {
|
||||
@ -43,7 +52,7 @@ export class CipherState {
|
||||
// The nonce is treated as a uint64, even though the underlying `number` only has 52 safely-available bits.
|
||||
n: Nonce;
|
||||
|
||||
constructor(k: bytes32 = CipherState.createEmptyKey(), n = new Nonce()) {
|
||||
constructor(k: bytes32 = createEmptyKey(), n = new Nonce()) {
|
||||
this.k = k;
|
||||
this.n = n;
|
||||
}
|
||||
@ -58,16 +67,7 @@ export class CipherState {
|
||||
|
||||
// Checks if a Cipher State has an encryption key set
|
||||
protected hasKey(): boolean {
|
||||
return !this.isEmptyKey(this.k);
|
||||
}
|
||||
|
||||
static createEmptyKey(): bytes32 {
|
||||
return new Uint8Array(32);
|
||||
}
|
||||
|
||||
protected isEmptyKey(k: bytes32): boolean {
|
||||
const emptyKey = CipherState.createEmptyKey();
|
||||
return uint8ArrayEquals(emptyKey, k);
|
||||
return !isEmptyKey(this.k);
|
||||
}
|
||||
|
||||
// Encrypts a plaintext using key material in a Noise Cipher State
|
||||
|
||||
@ -1,6 +1,48 @@
|
||||
import { concat as uint8ArrayConcat } from "uint8arrays/concat";
|
||||
import { equals as uint8ArrayEquals } from "uint8arrays/equals";
|
||||
|
||||
import { bytes32 } from "./@types/basic";
|
||||
import { chaCha20Poly1305Decrypt, chaCha20Poly1305Encrypt } from "./crypto";
|
||||
import { isEmptyKey } from "./noise";
|
||||
|
||||
// A ChaChaPoly Cipher State containing key (k), nonce (nonce) and associated data (ad)
|
||||
export class ChaChaPolyCipherState {
|
||||
k: bytes32;
|
||||
nonce: bytes32;
|
||||
ad: Uint8Array;
|
||||
constructor(k: bytes32 = new Uint8Array(), nonce: bytes32 = new Uint8Array(), ad: Uint8Array = new Uint8Array()) {
|
||||
this.k = k;
|
||||
this.nonce = nonce;
|
||||
this.ad = ad;
|
||||
}
|
||||
|
||||
// It takes a Cipher State (with key, nonce, and associated data) and encrypts a plaintext
|
||||
// The cipher state in not changed
|
||||
encrypt(plaintext: Uint8Array): Uint8Array {
|
||||
// If plaintext is empty, we raise an error
|
||||
if (plaintext.length == 0) {
|
||||
throw new Error("tried to encrypt empty plaintext");
|
||||
}
|
||||
|
||||
return chaCha20Poly1305Encrypt(plaintext, this.nonce, this.ad, this.k);
|
||||
}
|
||||
|
||||
// ChaChaPoly decryption
|
||||
// It takes a Cipher State (with key, nonce, and associated data) and decrypts a ciphertext
|
||||
// The cipher state is not changed
|
||||
decrypt(ciphertext: Uint8Array): Uint8Array {
|
||||
// If ciphertext is empty, we raise an error
|
||||
if (ciphertext.length == 0) {
|
||||
throw new Error("tried to decrypt empty ciphertext");
|
||||
}
|
||||
const plaintext = chaCha20Poly1305Decrypt(ciphertext, this.nonce, this.ad, this.k);
|
||||
if (!plaintext) {
|
||||
throw new Error("decryptWithAd failed");
|
||||
}
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
}
|
||||
|
||||
// A Noise public key is a public key exchanged during Noise handshakes (no private part)
|
||||
// This follows https://rfc.vac.dev/spec/35/#public-keys-serialization
|
||||
@ -26,7 +68,58 @@ export class NoisePublicKey {
|
||||
}
|
||||
|
||||
// Converts a public Elliptic Curve key to an unencrypted Noise public key
|
||||
static to(publicKey: bytes32): NoisePublicKey {
|
||||
static fromPublicKey(publicKey: bytes32): NoisePublicKey {
|
||||
return new NoisePublicKey(0, publicKey);
|
||||
}
|
||||
|
||||
// Converts a Noise public key to a stream of bytes as in
|
||||
// https://rfc.vac.dev/spec/35/#public-keys-serialization
|
||||
serialize(): Uint8Array {
|
||||
// Public key is serialized as (flag || pk)
|
||||
// Note that pk contains the X coordinate of the public key if unencrypted
|
||||
// or the encryption concatenated with the authorization tag if encrypted
|
||||
const serializedNoisePublicKey = new Uint8Array(uint8ArrayConcat([new Uint8Array([this.flag ? 1 : 0]), this.pk]));
|
||||
return serializedNoisePublicKey;
|
||||
}
|
||||
|
||||
// Converts a serialized Noise public key to a NoisePublicKey object as in
|
||||
// https://rfc.vac.dev/spec/35/#public-keys-serialization
|
||||
static deserialize(serializedPK: Uint8Array): NoisePublicKey {
|
||||
if (serializedPK.length == 0) throw new Error("invalid serialized key");
|
||||
|
||||
// We retrieve the encryption flag
|
||||
const flag = serializedPK[0];
|
||||
if (!(flag == 0 || flag == 1)) throw new Error("invalid flag in serialized public key");
|
||||
|
||||
const pk = serializedPK.subarray(1);
|
||||
|
||||
return new NoisePublicKey(flag, pk);
|
||||
}
|
||||
|
||||
static encrypt(ns: NoisePublicKey, cs: ChaChaPolyCipherState): NoisePublicKey {
|
||||
// We proceed with encryption only if
|
||||
// - a key is set in the cipher state
|
||||
// - the public key is unencrypted
|
||||
if (!isEmptyKey(cs.k) && ns.flag == 0) {
|
||||
const encPk = cs.encrypt(ns.pk);
|
||||
return new NoisePublicKey(1, encPk);
|
||||
}
|
||||
|
||||
// Otherwise we return the public key as it is
|
||||
return ns.clone();
|
||||
}
|
||||
|
||||
// Decrypts a Noise public key using a ChaChaPoly Cipher State
|
||||
static decrypt(ns: NoisePublicKey, cs: ChaChaPolyCipherState): NoisePublicKey {
|
||||
// We proceed with decryption only if
|
||||
// - a key is set in the cipher state
|
||||
// - the public key is encrypted
|
||||
if (!isEmptyKey(cs.k) && ns.flag == 1) {
|
||||
const decrypted = cs.decrypt(ns.pk);
|
||||
return new NoisePublicKey(0, decrypted);
|
||||
}
|
||||
|
||||
// Otherwise we return the public key as it is
|
||||
return ns.clone();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user