410 lines
11 KiB
TypeScript
Raw Normal View History

2019-10-31 17:35:18 +01:00
import {bytes32, bytes16, uint32, uint64, bytes} from './types/basic'
import { Buffer } from 'buffer';
2019-11-05 13:25:03 +01:00
import { AEAD, x25519, HKDF, SHA256 } from 'bcrypto';
2019-11-05 13:36:20 +01:00
import { BN } from 'bn.js';
2019-10-31 17:35:18 +01:00
export interface KeyPair {
2019-10-31 17:35:18 +01:00
publicKey: bytes32,
privateKey: bytes32,
}
2019-11-04 21:34:12 +01:00
interface MessageBuffer {
ne: bytes32,
ns: bytes,
ciphertext: bytes
}
2019-10-31 17:35:18 +01:00
type CipherState = {
k: bytes32,
n: uint32,
}
type SymmetricState = {
cs: CipherState,
2019-11-04 21:34:12 +01:00
ck: bytes32, // chaining key
h: bytes32, // handshake hash
2019-10-31 17:35:18 +01:00
}
type HandshakeState = {
ss: SymmetricState,
s: KeyPair,
2019-11-04 22:09:42 +01:00
e?: KeyPair,
2019-10-31 17:35:18 +01:00
rs: bytes32,
2019-11-04 22:37:43 +01:00
re: bytes32,
2019-10-31 17:35:18 +01:00
psk: bytes32,
}
type NoiseSession = {
hs: HandshakeState,
2019-11-04 22:09:42 +01:00
h?: bytes32,
cs1?: CipherState,
cs2?: CipherState,
2019-10-31 17:35:18 +01:00
mc: uint64,
i: boolean,
}
2019-11-05 13:25:03 +01:00
export type Hkdf = [bytes, bytes, bytes];
2019-10-31 17:35:18 +01:00
const minNonce = 0;
2019-11-04 15:19:40 +01:00
export class XXHandshake {
2019-11-04 15:10:14 +01:00
private createEmptyKey() : bytes32 {
return Buffer.alloc(32);
}
2019-11-04 14:31:58 +01:00
private async initializeInitiator(prologue: bytes32, s: KeyPair, rs: bytes32, psk: bytes32) : Promise<HandshakeState> {
2019-10-31 17:35:18 +01:00
const name = "Noise_XX_25519_ChaChaPoly_SHA256";
const ss = await this.initializeSymmetric(name);
this.mixHash(ss, prologue);
2019-11-04 22:37:43 +01:00
const re = Buffer.alloc(32);
2019-10-31 17:35:18 +01:00
2019-11-04 22:37:43 +01:00
return { ss, s, rs, psk, re };
2019-10-31 17:35:18 +01:00
}
2019-11-04 14:38:01 +01:00
private async initializeResponder(prologue: bytes32, s: KeyPair, rs: bytes32, psk: bytes32) : Promise<HandshakeState> {
2019-10-31 17:35:18 +01:00
const name = "Noise_XX_25519_ChaChaPoly_SHA256";
const ss = await this.initializeSymmetric(name);
this.mixHash(ss, prologue);
2019-11-04 22:37:43 +01:00
const re = Buffer.alloc(32);
2019-10-31 17:35:18 +01:00
2019-11-04 22:37:43 +01:00
return { ss, s, rs, psk, re };
2019-10-31 17:35:18 +01:00
}
2019-11-04 14:38:01 +01:00
private incrementNonce(n: uint32) : uint32 {
2019-11-01 11:30:28 +01:00
return n + 1;
}
2019-11-04 21:34:12 +01:00
private dh(privateKey: bytes32, publicKey: bytes32) : bytes32 {
2019-11-06 15:45:28 +01:00
const derived = x25519.derive(publicKey, privateKey);
2019-11-06 15:16:13 +01:00
const result = Buffer.alloc(32);
derived.copy(result);
return result;
2019-11-04 21:34:12 +01:00
}
2019-11-04 15:19:40 +01:00
private convertNonce(n: uint32) : bytes {
2019-11-04 14:31:58 +01:00
const nonce = Buffer.alloc(12);
nonce.writeUInt32LE(n, 4);
2019-11-04 15:19:40 +01:00
return nonce;
}
private encrypt(k: bytes32, n: uint32, ad: bytes, plaintext: bytes) : bytes {
const nonce = this.convertNonce(n);
2019-11-04 14:31:58 +01:00
const ctx = new AEAD();
ctx.init(k, nonce);
ctx.aad(ad);
ctx.encrypt(plaintext);
return ctx.final();
2019-11-01 11:30:28 +01:00
}
2019-11-04 15:19:40 +01:00
private decrypt(k: bytes32, n: uint32, ad: bytes, ciphertext: bytes) : bytes {
const nonce = this.convertNonce(n);
const ctx = new AEAD();
ctx.init(k, nonce);
ctx.aad(ad);
ctx.decrypt(ciphertext);
return ctx.final();
}
2019-11-04 21:34:12 +01:00
private isEmptyKey(k: bytes32) : boolean {
const emptyKey = this.createEmptyKey();
return emptyKey.equals(k);
}
2019-11-01 11:30:28 +01:00
// Cipher state related
2019-11-04 14:38:01 +01:00
private initializeKey(k: bytes32) : CipherState {
2019-10-31 17:35:18 +01:00
const n = minNonce;
return { k, n };
}
2019-11-04 21:34:12 +01:00
private hasKey(cs: CipherState) : boolean {
return !this.isEmptyKey(cs.k);
}
2019-11-04 14:38:01 +01:00
private setNonce(cs: CipherState, nonce: uint32) {
2019-11-01 11:30:28 +01:00
cs.n = nonce;
}
2019-11-04 14:38:01 +01:00
private encryptWithAd(cs: CipherState, ad: bytes, plaintext: bytes) : bytes {
2019-11-01 11:30:28 +01:00
const e = this.encrypt(cs.k, cs.n, ad, plaintext);
this.setNonce(cs, this.incrementNonce(cs.n));
return e;
}
2019-11-04 15:19:40 +01:00
private decryptWithAd(cs: CipherState, ad: bytes, ciphertext: bytes) : bytes {
const plaintext = this.decrypt(cs.k, cs.n, ad, ciphertext);
this.setNonce(cs, this.incrementNonce(cs.n));
return plaintext;
}
2019-11-01 11:30:28 +01:00
// Symmetric state related
2019-11-04 14:38:01 +01:00
private async initializeSymmetric(protocolName: string) : Promise<SymmetricState> {
2019-11-04 15:10:14 +01:00
const protocolNameBytes: bytes = Buffer.from(protocolName, 'utf-8');
const h = await this.hashProtocolName(protocolNameBytes);
2019-10-31 17:35:18 +01:00
const ck = h;
2019-11-04 15:10:14 +01:00
const key = this.createEmptyKey();
2019-11-06 15:16:13 +01:00
const cs:CipherState = this.initializeKey(key);
2019-10-31 17:35:18 +01:00
return { cs, ck, h };
}
2019-11-04 21:34:12 +01:00
private mixKey(ss: SymmetricState, ikm: bytes32) {
const [ ck, tempK ] = this.getHkdf(ss.ck, ikm);
2019-11-06 15:16:13 +01:00
ss.cs = this.initializeKey(tempK) as CipherState;
2019-11-04 21:34:12 +01:00
ss.ck = ck;
}
2019-11-04 15:10:14 +01:00
private async hashProtocolName(protocolName: bytes) : Promise<bytes32> {
2019-10-31 17:35:18 +01:00
if (protocolName.length <= 32) {
let h = Buffer.alloc(32);
2019-11-04 22:37:43 +01:00
protocolName.copy(h);
2019-11-05 13:25:03 +01:00
return h;
2019-10-31 17:35:18 +01:00
} else {
return this.getHash(protocolName, Buffer.alloc(0));
2019-10-31 17:35:18 +01:00
}
}
2019-11-05 13:25:03 +01:00
public getHkdf(ck: bytes32, ikm: bytes) : Hkdf {
2019-11-04 21:34:12 +01:00
const info = Buffer.alloc(0);
2019-11-05 13:25:03 +01:00
const prk = HKDF.extract(SHA256, ikm, ck);
const okm = HKDF.expand(SHA256, prk, info, 96);
2019-11-04 21:34:12 +01:00
2019-11-05 13:25:03 +01:00
const k1 = okm.slice(0, 32);
const k2 = okm.slice(32, 64);
const k3 = okm.slice(64, 96);
2019-11-04 21:34:12 +01:00
return [ k1, k2, k3 ];
}
private mixHash(ss: SymmetricState, data: bytes) {
ss.h = this.getHash(ss.h, data);
2019-10-31 17:35:18 +01:00
}
private getHash(a: bytes, b: bytes) : bytes32 {
return SHA256.digest(Buffer.from([...a, ...b]));
2019-10-31 17:35:18 +01:00
}
2019-11-04 21:34:12 +01:00
private async encryptAndHash(ss: SymmetricState, plaintext: bytes) : Promise<bytes> {
let ciphertext;
if (this.hasKey(ss.cs)) {
ciphertext = this.encryptWithAd(ss.cs, ss.h, plaintext);
} else {
ciphertext = plaintext;
}
this.mixHash(ss, ciphertext);
2019-11-04 21:34:12 +01:00
return ciphertext;
}
2019-11-05 09:51:24 +01:00
private async decryptAndHash(ss: SymmetricState, ciphertext: bytes) : Promise<bytes> {
let plaintext;
if (this.hasKey(ss.cs)) {
plaintext = this.decryptWithAd(ss.cs, ss.h, ciphertext);
} else {
plaintext = ciphertext;
}
this.mixHash(ss, ciphertext);
2019-11-05 09:51:24 +01:00
return plaintext;
}
2019-11-04 21:34:12 +01:00
private split (ss: SymmetricState) {
const [ tempk1, tempk2 ] = this.getHkdf(ss.ck, Buffer.alloc(0));
const cs1 = this.initializeKey(tempk1);
const cs2 = this.initializeKey(tempk2);
return { cs1, cs2 };
}
private async writeMessageA(hs: HandshakeState, payload: bytes) : Promise<MessageBuffer> {
let ns = Buffer.alloc(0);
hs.e = await this.generateKeypair();
const ne = hs.e.publicKey;
this.mixHash(hs.ss, ne);
2019-11-04 21:34:12 +01:00
const ciphertext = await this.encryptAndHash(hs.ss, payload);
2019-11-05 13:36:20 +01:00
return {ne, ns, ciphertext};
2019-11-04 21:34:12 +01:00
}
private async writeMessageB(hs: HandshakeState, payload: bytes) : Promise<MessageBuffer> {
hs.e = await this.generateKeypair();
const ne = hs.e.publicKey;
this.mixHash(hs.ss, ne);
2019-11-06 15:16:13 +01:00
this.mixKey(hs.ss, this.dh(hs.e.privateKey, hs.re));
2019-11-05 13:36:20 +01:00
const spk = Buffer.from(hs.s.publicKey);
2019-11-04 21:34:12 +01:00
const ns = await this.encryptAndHash(hs.ss, spk);
2019-11-06 15:16:13 +01:00
2019-11-04 21:34:12 +01:00
this.mixKey(hs.ss, this.dh(hs.s.privateKey, hs.re));
const ciphertext = await this.encryptAndHash(hs.ss, payload);
return { ne, ns, ciphertext };
}
private async writeMessageC(hs: HandshakeState, payload: bytes) {
2019-11-05 13:36:20 +01:00
const spk = Buffer.from(hs.s.publicKey);
2019-11-04 21:34:12 +01:00
const ns = await this.encryptAndHash(hs.ss, spk);
this.mixKey(hs.ss, this.dh(hs.s.privateKey, hs.re));
const ciphertext = await this.encryptAndHash(hs.ss, payload);
const ne = this.createEmptyKey();
const messageBuffer: MessageBuffer = {ne, ns, ciphertext};
const { cs1, cs2 } = this.split(hs.ss);
return { h: hs.ss.h, messageBuffer, cs1, cs2 };
}
private async writeMessageRegular(cs: CipherState, payload: bytes) : Promise<MessageBuffer> {
const ciphertext = this.encryptWithAd(cs, Buffer.alloc(0), payload);
const ne = this.createEmptyKey();
const ns = Buffer.alloc(0);
return { ne, ns, ciphertext };
}
2019-11-05 09:51:24 +01:00
private async readMessageA(hs: HandshakeState, message: MessageBuffer) : Promise<bytes> {
2019-11-06 15:45:28 +01:00
if (x25519.publicKeyVerify(message.ne)) {
hs.re = message.ne;
}
2019-11-05 09:51:24 +01:00
this.mixHash(hs.ss, hs.re);
2019-11-05 09:51:24 +01:00
return await this.decryptAndHash(hs.ss, message.ciphertext);
}
private async readMessageB(hs: HandshakeState, message: MessageBuffer) : Promise<bytes> {
2019-11-06 15:45:28 +01:00
if (x25519.publicKeyVerify(message.ne)) {
hs.re = message.ne;
}
2019-11-05 09:51:24 +01:00
this.mixHash(hs.ss, hs.re);
2019-11-05 09:51:24 +01:00
if (!hs.e) {
throw new Error("Handshake state `e` param is missing.");
}
this.mixKey(hs.ss, this.dh(hs.e.privateKey, hs.re));
const ns = await this.decryptAndHash(hs.ss, message.ns);
2019-11-06 15:45:28 +01:00
if (ns.length === 32 && x25519.publicKeyVerify(message.ns)) {
hs.rs = ns;
}
2019-11-05 09:51:24 +01:00
this.mixKey(hs.ss, this.dh(hs.e.privateKey, hs.rs));
return await this.decryptAndHash(hs.ss, message.ciphertext);
}
private async readMessageC(hs: HandshakeState, message: MessageBuffer) {
const ns = await this.decryptAndHash(hs.ss, message.ns);
2019-11-06 15:45:28 +01:00
if (ns.length === 32 && x25519.publicKeyVerify(message.ns)) {
hs.rs = ns;
}
2019-11-05 09:51:24 +01:00
if (!hs.e) {
throw new Error("Handshake state `e` param is missing.");
}
this.mixKey(hs.ss, this.dh(hs.e.privateKey, hs.rs));
const plaintext = await this.decryptAndHash(hs.ss, message.ciphertext);
const { cs1, cs2 } = this.split(hs.ss);
return { h: hs.ss.h, plaintext, cs1, cs2 };
}
private readMessageRegular(cs: CipherState, message: MessageBuffer) : bytes {
return this.decryptWithAd(cs, Buffer.alloc(0), message.ciphertext);
}
2019-11-04 21:34:12 +01:00
public async generateKeypair() : Promise<KeyPair> {
2019-11-06 15:16:13 +01:00
const privateKey = x25519.privateKeyGenerate();
const publicKey = x25519.publicKeyCreate(privateKey);
return {
2019-11-06 15:16:13 +01:00
publicKey,
privateKey,
}
2019-11-04 21:34:12 +01:00
}
public async initSession(initiator: boolean, prologue: bytes32, s: KeyPair, rs: bytes32) : Promise<NoiseSession> {
2019-11-04 15:10:14 +01:00
const psk = this.createEmptyKey();
2019-11-04 22:09:42 +01:00
let hs;
2019-10-31 17:35:18 +01:00
if (initiator) {
2019-11-04 22:09:42 +01:00
hs = await this.initializeInitiator(prologue, s, rs, psk);
2019-10-31 17:35:18 +01:00
} else {
2019-11-04 22:09:42 +01:00
hs = await this.initializeResponder(prologue, s, rs, psk);
2019-10-31 17:35:18 +01:00
}
2019-11-04 22:09:42 +01:00
return {
hs,
i: initiator,
2019-11-05 13:36:20 +01:00
mc: new BN(0),
2019-11-04 22:09:42 +01:00
};
2019-10-31 17:35:18 +01:00
}
2019-11-04 21:34:12 +01:00
public async sendMessage(session: NoiseSession, message: bytes) : Promise<MessageBuffer> {
2019-11-05 13:36:20 +01:00
let messageBuffer: MessageBuffer;
if (session.mc.eqn(0)) {
2019-11-04 21:34:12 +01:00
messageBuffer = await this.writeMessageA(session.hs, message);
2019-11-05 13:36:20 +01:00
} else if (session.mc.eqn(1)) {
2019-11-04 21:34:12 +01:00
messageBuffer = await this.writeMessageB(session.hs, message);
2019-11-05 13:36:20 +01:00
} else if (session.mc.eqn(2)) {
2019-11-05 09:51:24 +01:00
const { h, messageBuffer: resultingBuffer, cs1, cs2 } = await this.writeMessageC(session.hs, message);
messageBuffer = resultingBuffer;
2019-11-04 21:34:12 +01:00
session.h = h;
session.cs1 = cs1;
session.cs2 = cs2;
2019-11-05 13:36:20 +01:00
} else if (session.mc.gtn(2)) {
2019-11-04 21:34:12 +01:00
if (session.i) {
2019-11-04 22:37:43 +01:00
if (!session.cs1) {
throw new Error("CS1 (cipher state) is not defined")
}
2019-11-04 21:34:12 +01:00
messageBuffer = await this.writeMessageRegular(session.cs1, message);
} else {
2019-11-04 22:37:43 +01:00
if (!session.cs2) {
throw new Error("CS2 (cipher state) is not defined")
}
2019-11-04 21:34:12 +01:00
messageBuffer = await this.writeMessageRegular(session.cs2, message);
}
} else {
throw new Error("Session invalid.")
}
2019-11-06 15:16:13 +01:00
session.mc = session.mc.add(new BN(1));
2019-11-04 21:34:12 +01:00
return messageBuffer;
}
2019-11-05 09:51:24 +01:00
2019-11-06 15:49:20 +01:00
public async recvMessage(session: NoiseSession, message: MessageBuffer) : Promise<bytes> {
2019-11-05 09:51:24 +01:00
let plaintext: bytes;
2019-11-05 13:36:20 +01:00
if (session.mc.eqn(0)) {
2019-11-05 09:51:24 +01:00
plaintext = await this.readMessageA(session.hs, message);
2019-11-05 13:36:20 +01:00
} else if (session.mc.eqn(1)) {
2019-11-05 09:51:24 +01:00
plaintext = await this.readMessageB(session.hs, message);
2019-11-05 13:36:20 +01:00
} else if (session.mc.eqn(2)) {
2019-11-05 09:51:24 +01:00
const { h, plaintext: resultingPlaintext, cs1, cs2 } = await this.readMessageC(session.hs, message);
plaintext = resultingPlaintext;
session.h = h;
session.cs1 = cs1;
session.cs2 = cs2;
2019-11-05 13:36:20 +01:00
} else if (session.mc.gtn(2)) {
2019-11-05 09:51:24 +01:00
if (session.i) {
if (!session.cs2) {
throw new Error("CS1 (cipher state) is not defined")
}
plaintext = await this.readMessageRegular(session.cs2, message);
} else {
if (!session.cs1) {
throw new Error("CS1 (cipher state) is not defined")
}
plaintext = await this.readMessageRegular(session.cs1, message);
}
} else {
throw new Error("Session invalid.");
}
2019-11-06 15:16:13 +01:00
session.mc = session.mc.add(new BN(1));
2019-11-05 09:51:24 +01:00
return plaintext;
}
2019-10-31 17:35:18 +01:00
}