@reclaimprotocol/tls
Version:
WebCrypto Based Cross Platform TLS
137 lines (136 loc) • 4.7 kB
JavaScript
import { TLS_PROTOCOL_VERSION_MAP } from "./constants.js";
/**
* Converts a buffer to a hex string with whitespace between each byte
* @returns eg. '01 02 03 04'
*/
export function toHexStringWithWhitespace(buff, whitespace = ' ') {
return [...buff]
.map(x => x.toString(16).padStart(2, '0'))
.join(whitespace);
}
export function xor(a, b) {
const result = new Uint8Array(a.length);
for (const [i, element] of a.entries()) {
result[i] = element ^ b[i];
}
return result;
}
export function concatenateUint8Arrays(arrays) {
const totalLength = arrays.reduce((acc, curr) => acc + curr.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const arr of arrays) {
result.set(arr, offset);
offset += arr.length;
}
return result;
}
export function areUint8ArraysEqual(a, b) {
if (a.length !== b.length) {
return false;
}
for (const [i, element] of a.entries()) {
if (element !== b[i]) {
return false;
}
}
return true;
}
export function uint8ArrayToDataView(arr) {
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
}
export function asciiToUint8Array(str) {
const bytes = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
const charCode = str.charCodeAt(i);
if (charCode < 0 || charCode > 255) {
throw new Error(`Invalid ASCII character at index ${i}: ${str[i]}`);
}
bytes[i] = charCode;
}
return bytes;
}
export function uint8ArrayToStr(arr) {
return new TextDecoder().decode(arr);
}
export function generateIV(iv, recordNumber) {
// make the recordNumber a buffer, so we can XOR with the main IV
// to generate the specific IV to decrypt this packet
const recordBuffer = new Uint8Array(iv.length);
const recordBufferView = new DataView(recordBuffer.buffer);
recordBufferView.setUint32(iv.length - 4, recordNumber);
return xor(iv, recordBuffer);
}
/**
* TLS has this special sort of padding where the last byte
* is the number of padding bytes, and all the padding bytes
* are the same as the last byte.
* Eg. for an 8 byte block [ 0x0a, 0x0b, 0x0c, 0xd ]
* -> [ 0x0a, 0x0b, 0x0c, 0x04, 0x04, 0x04, 0x04, 0x04 ]
*/
export function padTls(data, blockSize) {
const nextMultiple = data.length % blockSize === 0
? data.length + blockSize
: Math.ceil(data.length / blockSize) * blockSize;
const paddingLength = nextMultiple - data.length;
const paddingNum = paddingLength - 1;
const padded = new Uint8Array(nextMultiple);
padded.set(data);
padded.fill(paddingNum, data.length);
padded.fill(paddingNum, nextMultiple - 1);
return padded;
}
/**
* Unpad a TLS-spec padded buffer
*/
export function unpadTls(data) {
const paddingLength = data[data.length - 1];
for (let i = 0; i < paddingLength; i++) {
if (data[data.length - 1 - i] !== paddingLength) {
throw new Error('Invalid padding');
}
}
return data.slice(0, data.length - paddingLength);
}
export function isSymmetricCipher(cipher) {
return cipher === 'AES-128-CBC';
}
export function chunkUint8Array(arr, chunkSize) {
const result = [];
for (let i = 0; i < arr.length; i += chunkSize) {
result.push(arr.slice(i, i + chunkSize));
}
return result;
}
export function getTlsVersionFromBytes(bytes) {
const supportedV = Object.entries(TLS_PROTOCOL_VERSION_MAP)
.find(([, v]) => areUint8ArraysEqual(v, bytes));
if (!supportedV) {
throw new Error(`Unsupported TLS version '${bytes}'`);
}
return supportedV[0];
}
export const hkdfExpand = async (alg, hashLength, key, expLength, info, crypto) => {
info ||= new Uint8Array(0);
const infoLength = info.length;
const steps = Math.ceil(expLength / hashLength);
if (steps > 0xFF) {
throw new Error(`OKM length ${expLength} is too long for ${alg} hash`);
}
// use single buffer with unnecessary create/copy/move operations
const t = new Uint8Array(hashLength * steps + infoLength + 1);
for (let c = 1, start = 0, end = 0; c <= steps; ++c) {
// add info
t.set(info, end);
// add counter
t.set([c], end + infoLength);
// use view: T(C) = T(C-1) | info | C
const hmac = await crypto
.hmac(alg, key, t.slice(start, end + infoLength + 1));
// put back to the same buffer
t.set(hmac.slice(0, t.length - end), end);
start = end; // used for T(C-1) start
end += hashLength; // used for T(C-1) end & overall end
}
return t.slice(0, expLength);
};