@reclaimprotocol/tls
Version:
WebCrypto Based Cross Platform TLS
168 lines (167 loc) • 7.11 kB
JavaScript
import { crypto } from "../crypto/index.js";
import { getHash } from "../utils/decryption-utils.js";
import { SUPPORTED_CIPHER_SUITE_MAP, SUPPORTED_EXTENSION_MAP, SUPPORTED_NAMED_CURVE_MAP, SUPPORTED_RECORD_TYPE_MAP, SUPPORTED_SIGNATURE_ALGS_MAP, TLS_PROTOCOL_VERSION_MAP } from "./constants.js";
import { asciiToUint8Array, concatenateUint8Arrays, uint8ArrayToDataView } from "./generics.js";
import { packWith3ByteLength, packWithLength } from "./packets.js";
const CLIENT_VERSION = new Uint8Array([0x03, 0x03]);
// no compression, as our client won't support it
// neither does TLS1.3
const COMPRESSION_MODE = new Uint8Array([0x01, 0x00]);
const RENEGOTIATION_INFO = new Uint8Array([0xff, 0x01, 0x00, 0x01, 0x00]);
export async function packClientHello({ host, sessionId = crypto.randomBytes(32), random = crypto.randomBytes(32), keysToShare, psk, cipherSuites, supportedProtocolVersions = Object
.keys(TLS_PROTOCOL_VERSION_MAP), signatureAlgorithms = Object
.keys(SUPPORTED_SIGNATURE_ALGS_MAP), applicationLayerProtocols = [] }) {
// generate random & sessionId if not provided
const packedSessionId = packWithLength(sessionId).slice(1);
const cipherSuiteList = (cipherSuites || Object.keys(SUPPORTED_CIPHER_SUITE_MAP)).map(cipherSuite => SUPPORTED_CIPHER_SUITE_MAP[cipherSuite].identifier);
const packedCipherSuites = packWithLength(concatenateUint8Arrays(cipherSuiteList));
const extensionsList = [
RENEGOTIATION_INFO,
packServerNameExtension(host),
packSupportedGroupsExtension(keysToShare.map(k => k.type)),
packSessionTicketExtension(),
packVersionsExtension(supportedProtocolVersions),
packSignatureAlgorithmsExtension(signatureAlgorithms),
packPresharedKeyModeExtension(),
await packKeyShareExtension(keysToShare),
];
if (psk) {
extensionsList.push(packPresharedKeyExtension(psk));
}
if (applicationLayerProtocols.length) {
const protocols = applicationLayerProtocols.map(alp => (
// 1 byte for length
packWithLength(asciiToUint8Array(alp)).slice(1)));
extensionsList.push(packExtension({
type: 'ALPN',
data: concatenateUint8Arrays(protocols),
}));
}
const packedExtensions = packWithLength(concatenateUint8Arrays(extensionsList));
const handshakeData = concatenateUint8Arrays([
CLIENT_VERSION,
random,
packedSessionId,
packedCipherSuites,
COMPRESSION_MODE,
packedExtensions
]);
const packedHandshake = concatenateUint8Arrays([
new Uint8Array([SUPPORTED_RECORD_TYPE_MAP.CLIENT_HELLO]),
packWith3ByteLength(handshakeData)
]);
if (psk) {
const { hashLength } = SUPPORTED_CIPHER_SUITE_MAP[psk.cipherSuite];
const prefixHandshake = packedHandshake.slice(0, -hashLength - 3);
const binder = await computeBinderSuffix(prefixHandshake, psk);
packedHandshake.set(binder, packedHandshake.length - binder.length);
}
return packedHandshake;
}
export async function computeBinderSuffix(packedHandshakePrefix, psk) {
const { hashAlgorithm } = SUPPORTED_CIPHER_SUITE_MAP[psk.cipherSuite];
const hashedHelloHandshake = await getHash([packedHandshakePrefix], psk.cipherSuite);
return crypto.hmac(hashAlgorithm, psk.finishKey, hashedHelloHandshake);
}
/**
* Packs the preshared key extension; the binder is assumed to be 0
* The empty binder is suffixed to the end of the extension
* and should be replaced with the correct binder after the full handshake is computed
*/
export function packPresharedKeyExtension({ identity, ticketAge, cipherSuite }) {
const binderLength = SUPPORTED_CIPHER_SUITE_MAP[cipherSuite].hashLength;
const packedIdentity = packWithLength(identity);
const packedTicketAge = new Uint8Array(4);
const packedTicketAgeView = uint8ArrayToDataView(packedTicketAge);
packedTicketAgeView.setUint32(0, ticketAge);
const serialisedIdentity = concatenateUint8Arrays([
packedIdentity,
packedTicketAge
]);
const identityPacked = packWithLength(serialisedIdentity);
const binderHolderBytes = new Uint8Array(binderLength + 2 + 1);
const binderHolderBytesView = uint8ArrayToDataView(binderHolderBytes);
binderHolderBytesView.setUint16(0, binderLength + 1);
binderHolderBytesView.setUint8(2, binderLength);
const total = concatenateUint8Arrays([
identityPacked,
// 2 bytes for binders
// 1 byte for each binder length
binderHolderBytes
]);
const totalPacked = packWithLength(total);
const ext = new Uint8Array(2 + totalPacked.length);
ext.set(totalPacked, 2);
const extView = uint8ArrayToDataView(ext);
extView.setUint16(0, SUPPORTED_EXTENSION_MAP.PRE_SHARED_KEY);
return ext;
}
function packPresharedKeyModeExtension() {
return packExtension({
type: 'PRE_SHARED_KEY_MODE',
data: new Uint8Array([0x00, 0x01]),
lengthBytes: 1
});
}
function packSessionTicketExtension() {
return packExtension({
type: 'SESSION_TICKET',
data: new Uint8Array(),
});
}
function packVersionsExtension(supportedVersions) {
return packExtension({
type: 'SUPPORTED_VERSIONS',
data: concatenateUint8Arrays(supportedVersions.map(v => TLS_PROTOCOL_VERSION_MAP[v])),
lengthBytes: 1
});
}
function packSignatureAlgorithmsExtension(algs) {
return packExtension({
type: 'SIGNATURE_ALGS',
data: concatenateUint8Arrays(algs.map(v => SUPPORTED_SIGNATURE_ALGS_MAP[v].identifier))
});
}
function packSupportedGroupsExtension(namedCurves) {
return packExtension({
type: 'SUPPORTED_GROUPS',
data: concatenateUint8Arrays(namedCurves
.map(n => SUPPORTED_NAMED_CURVE_MAP[n].identifier))
});
}
async function packKeyShareExtension(keys) {
const buffs = [];
for (const { key, type } of keys) {
const exportedKey = await crypto.exportKey(key);
buffs.push(SUPPORTED_NAMED_CURVE_MAP[type].identifier, packWithLength(exportedKey));
}
return packExtension({
type: 'KEY_SHARE',
data: concatenateUint8Arrays(buffs)
});
}
function packServerNameExtension(host) {
return packExtension({
type: 'SERVER_NAME',
data: concatenateUint8Arrays([
// specify that this is a server hostname
new Uint8Array([0x0]),
// pack the remaining data prefixed with length
packWithLength(asciiToUint8Array(host))
])
});
}
function packExtension({ type, data, lengthBytes }) {
lengthBytes ||= 2;
let packed = data.length ? packWithLength(data) : data;
if (lengthBytes === 1) {
packed = packed.slice(1);
}
// 2 bytes for type, 2 bytes for packed data length
const result = new Uint8Array(2 + 2 + packed.length);
const resultView = uint8ArrayToDataView(result);
resultView.setUint8(1, SUPPORTED_EXTENSION_MAP[type]);
resultView.setUint16(2, packed.length);
result.set(packed, 4);
return result;
}