pocket-messaging
Version:
A small cryptographic messaging library written in TypeScript both for browser and nodejs supporting TCP and WebSockets
728 lines (592 loc) • 27 kB
text/typescript
/**
* A four way client-server handshake, as excellently described in
* https://ssbc.github.io/scuttlebutt-protocol-guide/ with a few additions:
*
* This protocols has one added version byte, one added difficulty byte, 4 byte added nonce bytes,
* and a 6 byte added clock timestamp and added optional client/server data exchange for
* swapping application parameters of default maximum 2048 bytes.
*
* The added difficulty byte can be used to force the client to calculate a nonce to match the
* difficulty level set by the server for the handshake to complete.
*/
import sodium from "libsodium-wrappers";
import {ClientInterface, ByteSize} from "pocket-sockets";
import {
HandshakeResult,
} from "./types";
type KeyPair = {
publicKey: Buffer,
secretKey: Buffer
};
// Single byte depicting the version of the handshake protocol.
const Version = Buffer.from([1]);
export function writeUInt64BE(target: Buffer, nr: bigint) {
if (typeof(nr) !== "bigint") {
throw new Error("expecting nr type bigint");
}
if (nr < BigInt(0) || nr > 0xffffffffffffffffn) {
throw new Error("64 bit integer out of bounds");
}
const binary = nr.toString(2).padStart(64, "0");
const msb32 = parseInt(binary.slice(0, 32), 2);
const lsb32 = parseInt(binary.slice(32), 2);
target.writeUInt32BE(msb32, 0);
target.writeUInt32BE(lsb32, 4);
}
export function readUInt64BE(source: Buffer): bigint {
const high32b = source.readUInt32BE(0);
const low32b = source.readUInt32BE(4);
const binary = high32b.toString(2).padStart(32, "0") + low32b.toString(2).padStart(32, "0");
const value = BigInt("0b" + binary);
return value;
}
// Compare two buffers in constant time
function Equals(a: Buffer, b: Buffer): boolean {
if (a.length !== b.length) {
// Cannot compare buffers of different lengths in constant time
return false;
}
let result = 0; // == buffers are equal.
for (let i=0; i<a.length; i++) {
result |= a[i] ^ b[i];
}
return result === 0; // true if buffers are equal
}
function createEphemeralKeys(): KeyPair {
const keyPair = sodium.crypto_box_keypair();
return {
publicKey: Buffer.from(keyPair.publicKey),
secretKey: Buffer.from(keyPair.privateKey)
};
}
function hmac(msg: Buffer, key: Buffer): Buffer {
const hmac = sodium.crypto_auth(msg, key);
return Buffer.from(hmac);
}
function assertHmac(clientHmac: Buffer, msg: Buffer, key: Buffer): boolean {
const hmac2 = hmac(msg, key);
return Equals(hmac2, clientHmac);
}
function clientSharedSecret_ab(clientEphemeralSk: Buffer, serverEphemeralPk: Buffer): Buffer {
return Buffer.from(sodium.crypto_scalarmult(clientEphemeralSk, serverEphemeralPk));
}
function serverSharedSecret_ab(serverEphemeralSk: Buffer, clientEphemeralPk: Buffer): Buffer {
return Buffer.from(sodium.crypto_scalarmult(serverEphemeralSk, clientEphemeralPk));
}
function clientSharedSecret_aB(clientEphemeralPk: Buffer, serverLongtermPk: Buffer): Buffer {
return Buffer.from(sodium.crypto_scalarmult(clientEphemeralPk, sodium.crypto_sign_ed25519_pk_to_curve25519(serverLongtermPk)));
}
function serverSharedSecret_aB(serverLongtermSk: Buffer, clientEphemeralPk: Buffer): Buffer {
return Buffer.from(sodium.crypto_scalarmult(sodium.crypto_sign_ed25519_sk_to_curve25519(serverLongtermSk), clientEphemeralPk));
}
function clientSharedSecret_Ab(clientLongtermSk: Buffer, serverEphemeralPk: Buffer): Buffer {
return Buffer.from(sodium.crypto_scalarmult(sodium.crypto_sign_ed25519_sk_to_curve25519(clientLongtermSk), serverEphemeralPk));
}
function serverSharedSecret_Ab(serverEphemeralSk: Buffer, clientLongtermPk: Buffer): Buffer {
return Buffer.from(sodium.crypto_scalarmult(serverEphemeralSk, sodium.crypto_sign_ed25519_pk_to_curve25519(clientLongtermPk)));
}
function signDetached(msg: Buffer, secretKey: Buffer): Buffer {
return Buffer.from(sodium.crypto_sign_detached(msg, secretKey));
}
function signVerifyDetached(msg: Buffer, sig: Buffer, publicKey: Buffer): boolean {
return sodium.crypto_sign_verify_detached(sig, msg, publicKey);
}
function secretBox(msg: Buffer, nonce: Buffer, key: Buffer): Buffer {
return Buffer.from(sodium.crypto_secretbox_easy(msg, nonce, key));
}
function secretBoxOpen(ciphertext: Buffer, nonce: Buffer, key: Buffer): Buffer {
const unboxed = sodium.crypto_secretbox_open_easy(ciphertext, nonce, key);
if (!unboxed) {
throw new Error("Could not open box");
}
return Buffer.from(unboxed);
}
function calcClientToServerKey(discriminator: Buffer, sharedSecret_ab: Buffer, sharedSecret_aB: Buffer, sharedSecret_Ab: Buffer, serverLongtermPk: Buffer, serverEphemeralPk: Buffer): [Buffer, Buffer] {
const inner = hashFn(hashFn(Buffer.concat([discriminator, sharedSecret_ab, sharedSecret_aB, sharedSecret_Ab])));
const clientToServerKey = hashFn(Buffer.concat([inner, serverLongtermPk]));
const clientNonce = hmac(serverEphemeralPk, discriminator).slice(0, 24);
return [clientToServerKey, clientNonce];
}
function calcServerToClientKey(discriminator: Buffer, sharedSecret_ab: Buffer, sharedSecret_aB: Buffer, sharedSecret_Ab: Buffer, clientLongtermPk: Buffer, clientEphemeralPk: Buffer): [Buffer, Buffer] {
const inner = hashFn(hashFn(Buffer.concat([discriminator, sharedSecret_ab, sharedSecret_aB, sharedSecret_Ab])));
const serverToClientKey = hashFn(Buffer.concat([inner, clientLongtermPk]));
const serverNonce = hmac(clientEphemeralPk, discriminator).slice(0, 24);
return [serverToClientKey, serverNonce];
}
function hashFn(message: Buffer): Buffer {
const digest = sodium.crypto_generichash(32, message);
return Buffer.from(digest);
}
/**
* Client creates message 1 (65 bytes).
*
* @return 1 byte version + 32 bytes hmac + 32 bytes clientEphemeralPk
*/
function message1(clientEphemeralPk: Buffer, discriminator: Buffer): Buffer {
if (clientEphemeralPk.length !== 32) {
throw new Error("clientEphemeralPk must be 32 bytes");
}
if (discriminator.length !== 32) {
throw new Error("Discriminator must be 32 bytes");
}
const clientHmac = hmac(clientEphemeralPk, discriminator);
return Buffer.concat([Version, clientHmac, clientEphemeralPk]);
}
/**
* Server verifies client message 1.
* @return client ephemeral public key on success, else throw exception.
* @throws
*/
function verifyMessage1(msg1: Buffer, discriminator: Buffer): Buffer {
if (msg1.length !== 65) {
throw new Error("Incoming message 1 must be 65 bytes long");
}
if (discriminator.length !== 32) {
throw new Error("Discriminator must be 32 bytes");
}
const version = msg1.slice(0, 1);
// Check so versions match.
if (!Equals(version, Version)) {
throw new Error("Mismatching version of the handshake");
}
const hmac = msg1.slice(1, 1+32);
const clientEphemeralPk = msg1.slice(1+32, 1+32+32);
if (assertHmac(hmac, clientEphemeralPk, discriminator)) {
return clientEphemeralPk;
}
throw new Error("Non matching discriminators");
}
/**
* Server creates message 2 (65 bytes).
* @return msg2: Buffer
*/
function message2(difficulty: Buffer, serverEphemeralPk: Buffer, discriminator: Buffer): Buffer {
if (difficulty.length !== 1) {
throw new Error("Difficulty must be of length 1 bytes");
}
if (serverEphemeralPk.length !== 32) {
throw new Error("ServerEphemeralPk must be of length 32 bytes");
}
if (discriminator.length !== 32) {
throw new Error("Discriminator must be 32 bytes");
}
const serverHmac = hmac(Buffer.concat([difficulty, serverEphemeralPk]), discriminator);
return Buffer.concat([serverHmac, difficulty, serverEphemeralPk]);
}
/**
* Client verifies first server message (message 2: 65 bytes).
* Return server ephemeral public key on success.
* Throws on error.
* @return serverEphemeralPk
* @throws
*/
function verifyMessage2(msg2: Buffer, discriminator: Buffer): [Buffer, Buffer] {
if (msg2.length !== 65) {
throw new Error("Incoming message 2 must be of 65 bytes");
}
if (discriminator.length !== 32) {
throw new Error("Discriminator must be 32 bytes");
}
const hmac = msg2.slice(0, 32);
const difficulty = msg2.slice(32, 33);
const serverEphemeralPk = msg2.slice(33, 65);
if (assertHmac(hmac, Buffer.concat([difficulty, serverEphemeralPk]), discriminator)) {
return [difficulty, serverEphemeralPk];
}
throw new Error("Non matching discriminators");
}
/**
* Client creates its second message (message 3: variable length).
* @return ciphertext: Buffer
*/
function message3(detachedSigA: Buffer, nonce: Buffer, discriminator: Buffer,
clientLongtermPk: Buffer, sharedSecret_ab: Buffer, sharedSecret_aB: Buffer, clientClock: number,
clientData?: Buffer): Buffer
{
if (detachedSigA.length !== 64) {
throw new Error("detachedSigA must be 64 bytes");
}
if (nonce.length !== 4) {
throw new Error("Nonce must be 4 bytes");
}
if (discriminator.length !== 32) {
throw new Error("Discriminator must be 32 bytes");
}
if (clientLongtermPk.length !== 32) {
throw new Error("ServerEphemeralPk must be of length 32 bytes");
}
if (sharedSecret_ab.length !== 32) {
throw new Error("sharedSecret_ab must be of length 32 bytes");
}
if (sharedSecret_aB.length !== 32) {
throw new Error("sharedSecret_aB must be of length 32 bytes");
}
if (clientClock < 0) {
throw new Error("clientClock must be zero or positive");
}
if (!clientData) {
clientData = Buffer.alloc(0);
}
if (clientData.length > 1024*60) {
throw new Error("Client data cannot exceed 60 KiB");
}
let packedClientClock = Buffer.alloc(8);
writeUInt64BE(packedClientClock, BigInt(clientClock));
packedClientClock = packedClientClock.slice(2); // remove first two empty bytes
const message = Buffer.concat([detachedSigA, nonce, clientLongtermPk, packedClientClock, clientData]);
const boxNonce = Buffer.alloc(24).fill(0);
const key = hashFn(Buffer.concat([discriminator, sharedSecret_ab, sharedSecret_aB]));
const ciphertext = Buffer.from(secretBox(message, boxNonce, key));
const length = Buffer.alloc(2); // Prepend ciphertext with two bytes describing length
length.writeUInt16BE(ciphertext.length, 0);
return Buffer.concat([length, ciphertext]);
}
/**
* Server verifies message 3.
* Return client longterm public key, the detachedSigA, and the arbitrary variable length client data on success.
* Throws exception on error.
* @return [nonce, clientLongtermPk, detachedSigA, clientClock, clientData]
* @throws
*/
function verifyMessage3(msg3: Buffer, serverLongtermPk: Buffer, discriminator: Buffer,
sharedSecret_ab: Buffer, sharedSecret_aB: Buffer): [Buffer, Buffer, Buffer, number, Buffer]
{
if (serverLongtermPk.length !== 32) {
throw new Error("serverLongtermPk must be of length 32 bytes");
}
if (discriminator.length !== 32) {
throw new Error("Discriminator must be 32 bytes");
}
if (sharedSecret_ab.length !== 32) {
throw new Error("sharedSecret_ab must be of length 32 bytes");
}
if (sharedSecret_aB.length !== 32) {
throw new Error("sharedSecret_aB must be of length 32 bytes");
}
const length = msg3.readUInt16BE(0);
const ciphertext = msg3.slice(2);
if (ciphertext.length !== length) {
throw new Error("Mismatching expected length of message 3");
}
const boxNonce = Buffer.alloc(24).fill(0);
const key = hashFn(Buffer.concat([discriminator, sharedSecret_ab, sharedSecret_aB]));
const unboxed = secretBoxOpen(ciphertext, boxNonce, key);
const detachedSigA = unboxed.slice(0, 64);
const nonce = unboxed.slice(64, 64+4);
const clientLongtermPk = unboxed.slice(64+4, 64+4+32);
const packedClientClock = unboxed.slice(64+4+32, 64+4+32+6);
const clientData = unboxed.slice(64+4+32+6);
const msg = Buffer.concat([nonce, discriminator, serverLongtermPk, hashFn(sharedSecret_ab)]);
if (!signVerifyDetached(msg, detachedSigA, clientLongtermPk)) {
throw new Error("Signature does not match");
}
const clientClock = Number(readUInt64BE(Buffer.concat([Buffer.alloc(2), packedClientClock])));
return [nonce, clientLongtermPk, detachedSigA, clientClock, clientData];
}
/**
* Server creates its second message (message 4) (176 bytes).
*/
function message4(discriminator: Buffer, detachedSigA: Buffer, clientLongtermPk: Buffer,
sharedSecret_ab: Buffer, sharedSecret_aB: Buffer, sharedSecret_Ab: Buffer,
serverLongtermSk: Buffer, serverClock: number, serverData?: Buffer): Buffer
{
if (discriminator.length !== 32) {
throw new Error("Discriminator must be 32 bytes");
}
if (detachedSigA.length !== 64) {
throw new Error("detachedSigA must be 64 bytes");
}
if (clientLongtermPk.length !== 32) {
throw new Error("clientLongtermPk must be of length 32 bytes");
}
if (sharedSecret_ab.length !== 32) {
throw new Error("sharedSecret_ab must be of length 32 bytes");
}
if (sharedSecret_aB.length !== 32) {
throw new Error("sharedSecret_aB must be of length 32 bytes");
}
if (sharedSecret_Ab.length !== 32) {
throw new Error("sharedSecret_Ab must be of length 32 bytes");
}
if (serverLongtermSk.length !== 64) {
throw new Error("serverLongtermSk must be of length 64 bytes");
}
if (serverClock < 0) {
throw new Error("serverClock must be zero or positive");
}
if (!serverData) {
serverData = Buffer.alloc(0);
}
let packedServerClock = Buffer.alloc(8);
writeUInt64BE(packedServerClock, BigInt(serverClock));
packedServerClock = packedServerClock.slice(2); // remove first two empty bytes
const detachedSigB = signDetached(Buffer.concat([discriminator, detachedSigA, clientLongtermPk, hashFn(sharedSecret_ab)]), serverLongtermSk);
const boxNonce = Buffer.alloc(24).fill(0);
const key = hashFn(Buffer.concat([discriminator, sharedSecret_ab, sharedSecret_aB, sharedSecret_Ab]));
const ciphertext = secretBox(Buffer.concat([detachedSigB, packedServerClock, serverData]), boxNonce, key);
const length = Buffer.alloc(2); // Prepend ciphertext with two bytes describing length
length.writeUInt16BE(ciphertext.length, 0);
return Buffer.concat([length, ciphertext]);
}
/**
* Client verifies server message 2 (message 4).
* @return [serverClock: number, serverData: Buffer]
* serverClock in milliseconds
* @throws on error
*/
function verifyMessage4(msg4: Buffer, detachedSigA: Buffer, clientLongtermPk: Buffer,
serverLongtermPk: Buffer, discriminator: Buffer, sharedSecret_ab: Buffer,
sharedSecret_aB: Buffer, sharedSecret_Ab: Buffer): [number, Buffer]
{
if (detachedSigA.length !== 64) {
throw new Error("detachedSigA must be 64 bytes");
}
if (clientLongtermPk.length !== 32) {
throw new Error("clientLongtermPk must be of length 32 bytes");
}
if (serverLongtermPk.length !== 32) {
throw new Error("serverLongtermPk must be of length 32 bytes");
}
if (discriminator.length !== 32) {
throw new Error("Discriminator must be 32 bytes");
}
if (sharedSecret_ab.length !== 32) {
throw new Error("sharedSecret_ab must be of length 32 bytes");
}
if (sharedSecret_aB.length !== 32) {
throw new Error("sharedSecret_aB must be of length 32 bytes");
}
if (sharedSecret_Ab.length !== 32) {
throw new Error("sharedSecret_Ab must be of length 32 bytes");
}
const length = msg4.readUInt16BE(0);
const ciphertext = msg4.slice(2);
if (ciphertext.length !== length) {
throw new Error("Mismatching expected length of message 4");
}
const boxNonce = Buffer.alloc(24).fill(0);
const key = hashFn(Buffer.concat([discriminator, sharedSecret_ab, sharedSecret_aB, sharedSecret_Ab]));
const unboxed = secretBoxOpen(ciphertext, boxNonce, key);
const detachedSigB = unboxed.slice(0, 64);
const packedServerClock = unboxed.slice(64, 70);
const serverData = unboxed.slice(70);
const msg = Buffer.concat([discriminator, detachedSigA, clientLongtermPk, hashFn(sharedSecret_ab)]);
if (!signVerifyDetached(msg, detachedSigB, serverLongtermPk)) {
throw new Error("Signature does not match");
}
const serverClock = Number(readUInt64BE(Buffer.concat([Buffer.alloc(2), packedServerClock])));
return [serverClock, serverData];
}
/**
* @param difficulty number of nibbles to solve for
*/
function CalculateNonce(difficulty: number, serverEphemeralPk: Buffer): Buffer {
const target = Buffer.from(serverEphemeralPk.toString("hex").slice(0, difficulty));
let n = 0;
let nonce = Buffer.alloc(4);
const b = Buffer.alloc(4);
while (!Equals(target, Buffer.from(nonce.toString("hex").slice(0, difficulty)))) {
n++;
b.writeUInt32BE(n);
nonce = hashFn(b).slice(0, 4);
if (n>=0xffffffff) {
throw new Error("Nonce overflow");
}
}
return nonce;
}
function VerifyNonce(difficulty: number, serverEphemeralPk: Buffer, nonce: Buffer): boolean {
const target = Buffer.from(serverEphemeralPk.toString("hex").slice(0, difficulty));
return Equals(target, Buffer.from(nonce.toString("hex").slice(0, difficulty)));
}
/**
* On successful handshake return a populated HandshakeResult object.
* On unsuccessful throw exception.
* @return Promise <HandshakeResult>
* @throws
*/
export async function HandshakeAsClient(
client: ClientInterface,
clientLongtermSk: Buffer,
clientLongtermPk: Buffer,
serverLongtermPk: Buffer,
discriminator: Buffer,
clientData?: Buffer,
clock?: number,
maxServerDataSize: number = 2048,
timeout: number = 3000): Promise<HandshakeResult>
{
clock = clock ?? Date.now();
// We need this to take a delta later to adjust the clock.
//
const initialTimestamp = Date.now();
await sodium.ready;
// Make sure the discriminator is constant length
discriminator = hashFn(discriminator);
const clientEphemeralKeys = createEphemeralKeys();
const clientEphemeralPk = clientEphemeralKeys.publicKey;
const clientEphemeralSk = clientEphemeralKeys.secretKey;
// First message from client (message 1)
const msg1 = message1(clientEphemeralPk, discriminator);
client.send(msg1);
// First response from server (message 2)
const msg2 = await new ByteSize(client).read(65, timeout);
// Before spending any more time, take the delta.
//
const delta = Date.now() - initialTimestamp;
const [difficulty, serverEphemeralPk] = verifyMessage2(msg2, discriminator);
const nonce = CalculateNonce(difficulty.readUInt8(0), serverEphemeralPk);
const sharedSecret_ab = clientSharedSecret_ab(clientEphemeralSk, serverEphemeralPk);
const sharedSecret_aB = clientSharedSecret_aB(clientEphemeralSk, serverLongtermPk);
// Second message from client (message 3)
//
const clientClock = clock + delta;
const detachedSigA = signDetached(Buffer.concat([nonce, discriminator, serverLongtermPk,
hashFn(sharedSecret_ab)]), clientLongtermSk);
const msg3 = message3(detachedSigA, nonce, discriminator, clientLongtermPk, sharedSecret_ab,
sharedSecret_aB, clientClock, clientData);
client.send(msg3);
const sharedSecret_Ab = clientSharedSecret_Ab(clientLongtermSk, serverEphemeralPk);
// Wait for second response from server (message 4)
const lengthPrefix = await new ByteSize(client).read(2, timeout);
const length = lengthPrefix.readUInt16BE(0);
const FIXED_LENGTH = 70; // 64 byte signature + 6 byte clock
if (length - FIXED_LENGTH > maxServerDataSize) {
throw new Error("Server data length too big");
}
const msg4_ciphertext = await new ByteSize(client).read(length, timeout);
const msg4 = Buffer.concat([lengthPrefix, msg4_ciphertext]);
const [serverClock, serverData] = verifyMessage4(msg4, detachedSigA, clientLongtermPk,
serverLongtermPk, discriminator, sharedSecret_ab, sharedSecret_aB, sharedSecret_Ab);
const [clientToServerKey, clientNonce] = calcClientToServerKey(discriminator, sharedSecret_ab, sharedSecret_aB, sharedSecret_Ab, serverLongtermPk, serverEphemeralPk);
const [serverToClientKey, serverNonce] = calcServerToClientKey(discriminator, sharedSecret_ab, sharedSecret_aB, sharedSecret_Ab, clientLongtermPk, clientEphemeralPk);
const handshakeParams = {
longtermPk: clientLongtermPk,
peerLongtermPk: serverLongtermPk,
clientToServerKey,
clientNonce,
serverToClientKey,
serverNonce,
clockDiff: clientClock - serverClock,
peerData: serverData,
};
return handshakeParams;
}
/**
* On successful handshake return a populated HandshakeResult object.
*
* On failed handshake throw exception.
*
* @param client to send and retrieve data on
* @param serverLongtermSk this side's long term secret key
* @param serverLongtermPk this side's long term public key
* @param discriminator arbitrary data that must exactly match the client discriminator data
* @param allowedClientKeys if Buffer array it contains all client public keys allowed to handshake.
* If a function the function has to return true for the client to be allowed.
* If undefined then allow all clients to handshake.
* @param serverData optional data to pass to the client upon successful handshake. Its length cannot
* exceed the client's maximum allowed length.
* @param clock timestamp in milliseconds for when starting the handshake
* This value will be adjusted upwards to be as near as possible for when the clock was sent
* @param difficulty is the number of nibbles the client is required to calculate to mitigate ddos
* attacks. Difficulty 6 is a lot. 8 is max.
* If the client does not provide a requested nonce then the handshake is aborted after the clients
* second message is retrieved.
* @param maxClientDataSize the maximum length allowed for the client to pass its optional data.
* @param timeout in milliseconds to wait for each message before aborting.
* @return Promise<HandshakeResult>
* @throws on error
*/
export async function HandshakeAsServer(
client: ClientInterface,
serverLongtermSk: Buffer,
serverLongtermPk: Buffer,
discriminator: Buffer,
allowedClientKeys?: ((clientLongtermPk: Buffer) => boolean) | Buffer[],
serverData?: Buffer,
clock?: number,
difficulty: number = 0,
maxClientDataSize: number = 2048,
timeout: number = 3000): Promise<HandshakeResult>
{
clock = clock ?? Date.now();
// We need this to take a delta later to adjust the clock.
//
const initialTimestamp = Date.now();
if (difficulty > 8) {
// We support 8 nibbles of nonce.
throw new Error("Too high difficulty requested, max 8");
}
await sodium.ready;
// Make sure the discriminator is constant length
discriminator = hashFn(discriminator);
const serverEphemeralKeys = createEphemeralKeys();
const serverEphemeralPk = serverEphemeralKeys.publicKey;
const serverEphemeralSk = serverEphemeralKeys.secretKey;
// Wait for first message from client (message 1)
const msg1 = await new ByteSize(client).read(65, timeout);
const clientEphemeralPk = verifyMessage1(msg1, discriminator);
// Send first message from server (message 2)
const msg2 = message2(Buffer.from([difficulty]), serverEphemeralPk, discriminator);
client.send(msg2);
const sharedSecret_ab = serverSharedSecret_ab(serverEphemeralSk, clientEphemeralPk);
const sharedSecret_aB = serverSharedSecret_aB(serverLongtermSk, clientEphemeralPk);
// Wait for second message from client (message 3)
const lengthPrefix = await new ByteSize(client).read(2, timeout + difficulty * 30000);
const length = lengthPrefix.readUInt16BE(0);
const FIXED_LENGTH = 106; // incl. 6 byte clock
if (length - FIXED_LENGTH > maxClientDataSize) {
throw new Error("Client data length too big");
}
const msg3_ciphertext = await new ByteSize(client).read(length, timeout);
// Before spending any more time, take the delta.
//
const delta = Date.now() - initialTimestamp;
const msg3 = Buffer.concat([lengthPrefix, msg3_ciphertext]);
const [nonce, clientLongtermPk, detachedSigA, clientClock, clientData] = verifyMessage3(msg3,
serverLongtermPk, discriminator, sharedSecret_ab, sharedSecret_aB);
if (!VerifyNonce(difficulty, serverEphemeralPk, nonce)) {
throw new Error("Nonce does not verify");
}
// Verify permissioned handshake for client longterm pk
if (allowedClientKeys) {
if (typeof(allowedClientKeys) === "function") {
if (!allowedClientKeys(clientLongtermPk)) {
throw new Error(`Client longterm pk (${clientLongtermPk.toString("hex")} not allowed by function, IP: ${client.getRemoteAddress()}`);
}
}
else if (Array.isArray(allowedClientKeys)) {
if (!allowedClientKeys.find( (pk) => Equals(pk, clientLongtermPk) )) {
throw new Error(`Client longterm pk (${clientLongtermPk.toString("hex")}) not in list of allowed public keys, IP: ${client.getRemoteAddress()}`);
}
}
else {
throw new Error("Unknown client longterm pk validator");
}
}
else {
// WARNING: no allowedClientKeys means to allow all clients connecting
// Fall through
}
const sharedSecret_Ab = serverSharedSecret_Ab(serverEphemeralSk, clientLongtermPk);
// Send second message from server (message 4)
//
const serverClock = clock + delta;
const msg4 = message4(discriminator, detachedSigA, clientLongtermPk, sharedSecret_ab,
sharedSecret_aB, sharedSecret_Ab, serverLongtermSk, serverClock, serverData);
client.send(msg4);
const [clientToServerKey, clientNonce] = calcClientToServerKey(discriminator, sharedSecret_ab, sharedSecret_aB, sharedSecret_Ab, serverLongtermPk, serverEphemeralPk);
const [serverToClientKey, serverNonce] = calcServerToClientKey(discriminator, sharedSecret_ab, sharedSecret_aB, sharedSecret_Ab, clientLongtermPk, clientEphemeralPk);
const handshakeParams = {
longtermPk: serverLongtermPk,
peerLongtermPk: clientLongtermPk,
clientToServerKey,
clientNonce,
serverToClientKey,
clockDiff: serverClock - clientClock,
serverNonce,
peerData: clientData,
};
// Done
return handshakeParams;
}