js-noise/src/publickey.ts
2023-01-06 13:34:32 -04:00

168 lines
5.6 KiB
TypeScript

import { concat as uint8ArrayConcat } from "uint8arrays/concat";
import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import { bytes32 } from "./@types/basic.js";
import { chaCha20Poly1305Decrypt, chaCha20Poly1305Encrypt } from "./crypto.js";
import { isEmptyKey } from "./noise.js";
/**
* A ChaChaPoly Cipher State containing key (k), nonce (nonce) and associated data (ad)
*/
export class ChaChaPolyCipherState {
k: bytes32;
nonce: bytes32;
ad: Uint8Array;
/**
* @param k 32-byte key
* @param nonce 12 byte little-endian nonce
* @param ad associated data
*/
constructor(k: bytes32 = new Uint8Array(), nonce: bytes32 = new Uint8Array(), ad: Uint8Array = new Uint8Array()) {
this.k = k;
this.nonce = nonce;
this.ad = ad;
}
/**
* Takes a Cipher State (with key, nonce, and associated data) and encrypts a plaintext.
* The cipher state in not changed
* @param plaintext data to encrypt
* @returns sealed ciphertext including authentication tag
*/
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);
}
/**
* Takes a Cipher State (with key, nonce, and associated data) and decrypts a ciphertext
* The cipher state is not changed
* @param ciphertext data to decrypt
* @returns plaintext
*/
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
*/
export class NoisePublicKey {
/**
* @param flag 1 to indicate that the public key is encrypted, 0 for unencrypted.
* Note: besides encryption, flag can be used to distinguish among multiple supported Elliptic Curves
* @param pk contains the X coordinate of the public key, if unencrypted
* or the encryption of the X coordinate concatenated with the authorization tag, if encrypted
*/
constructor(public readonly flag: number, public readonly pk: Uint8Array) {}
/**
* Create a copy of the NoisePublicKey
* @returns a copy of the NoisePublicKey
*/
clone(): NoisePublicKey {
return new NoisePublicKey(this.flag, new Uint8Array(this.pk));
}
/**
* Check NoisePublicKey equality
* @param other object to compare against
* @returns true if equal, false otherwise
*/
equals(other: NoisePublicKey): boolean {
return this.flag == other.flag && uint8ArrayEquals(this.pk, other.pk);
}
/**
* Converts a public Elliptic Curve key to an unencrypted Noise public key
* @param publicKey 32-byte public key
* @returns 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
* @returns Serialized NoisePublicKey
*/
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
* @param serializedPK Serialized NoisePublicKey
* @returns NoisePublicKey
*/
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);
}
/**
* Encrypt a NoisePublicKey using a ChaChaPolyCipherState
* @param pk NoisePublicKey to encrypt
* @param cs ChaChaPolyCipherState used to encrypt
* @returns encrypted NoisePublicKey
*/
static encrypt(pk: 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) && pk.flag == 0) {
const encPk = cs.encrypt(pk.pk);
return new NoisePublicKey(1, encPk);
}
// Otherwise we return the public key as it is
return pk.clone();
}
/**
* Decrypts a Noise public key using a ChaChaPoly Cipher State
* @param pk NoisePublicKey to decrypt
* @param cs ChaChaPolyCipherState used to decrypt
* @returns decrypted NoisePublicKey
*/
static decrypt(pk: 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) && pk.flag == 1) {
const decrypted = cs.decrypt(pk.pk);
return new NoisePublicKey(0, decrypted);
}
// Otherwise we return the public key as it is
return pk.clone();
}
}