UNPKG

@reclaimprotocol/tls

Version:

TLS 1.2/1.3 for any JavaScript Environment

192 lines (191 loc) 7.41 kB
import { crypto } from "../crypto/index.js"; import { AUTH_TAG_BYTE_LENGTH, SUPPORTED_CIPHER_SUITE_MAP } from "./constants.js"; import { areUint8ArraysEqual, concatenateUint8Arrays, generateIV, isSymmetricCipher, padTls, toHexStringWithWhitespace, uint8ArrayToDataView, unpadTls } from "./generics.js"; import { packPacketHeader } from "./packets.js"; const AUTH_CIPHER_LENGTH = 12; export async function decryptWrappedRecord(encryptedData, opts) { if (!('recordHeader' in opts)) { throw new Error('recordHeader is required for decrypt'); } const { key, recordNumber, cipherSuite, } = opts; const { cipher, hashLength } = SUPPORTED_CIPHER_SUITE_MAP[cipherSuite]; return isSymmetricCipher(cipher) ? doCipherDecrypt(cipher) : doAuthCipherDecrypt(cipher); async function doCipherDecrypt(cipher) { const iv = encryptedData.slice(0, 16); const ciphertext = encryptedData.slice(16); let plaintextAndMac = await crypto.decrypt(cipher, { key, iv, data: ciphertext, }); plaintextAndMac = unpadTls(plaintextAndMac); plaintextAndMac = plaintextAndMac.slice(0, -1); const mac = plaintextAndMac.slice(-hashLength); const plaintext = plaintextAndMac.slice(0, -hashLength); const macComputed = await computeMacTls12(plaintext, opts); if (!areUint8ArraysEqual(mac, macComputed)) { throw new Error(`MAC mismatch: expected ${toHexStringWithWhitespace(macComputed)}, got ${toHexStringWithWhitespace(mac)}`); } return { plaintext, iv }; } async function doAuthCipherDecrypt(cipher) { let iv = opts.iv; const recordIvLength = AUTH_CIPHER_LENGTH - iv.length; if (recordIvLength) { // const recordIv = new Uint8Array(recordIvLength) // const seqNumberView = uint8ArrayToDataView(recordIv) // seqNumberView.setUint32(recordIvLength - 4, recordNumber) const recordIv = encryptedData.slice(0, recordIvLength); encryptedData = encryptedData.slice(recordIvLength); iv = concatenateUint8Arrays([ iv, recordIv ]); } else if ( // use IV generation alg for TLS 1.3 // and ChaCha20-Poly1305 (opts.version === 'TLS1_3' || cipher === 'CHACHA20-POLY1305') && typeof recordNumber !== 'undefined') { iv = generateIV(iv, recordNumber); } const authTag = encryptedData.slice(-AUTH_TAG_BYTE_LENGTH); encryptedData = encryptedData.slice(0, -AUTH_TAG_BYTE_LENGTH); const aead = getAead(encryptedData.length, opts); const { plaintext } = await crypto.authenticatedDecrypt(cipher, { key, iv, data: encryptedData, aead, authTag, }); if (plaintext.length !== encryptedData.length) { throw new Error('Decrypted length does not match encrypted length'); } return { plaintext, iv }; } } export async function encryptWrappedRecord(plaintext, opts) { const { key, recordNumber, cipherSuite, } = opts; const { cipher } = SUPPORTED_CIPHER_SUITE_MAP[cipherSuite]; let iv = opts.iv; return isSymmetricCipher(cipher) ? doSymmetricEncrypt(cipher) : doAuthSymmetricEncrypt(cipher); async function doAuthSymmetricEncrypt(cipher) { const aead = getAead(plaintext.length, opts); // record IV is the record number as a 64-bit big-endian integer const recordIvLength = AUTH_CIPHER_LENGTH - iv.length; let recordIv; let completeIv = iv; if (recordIvLength && typeof recordNumber !== 'undefined') { recordIv = new Uint8Array(recordIvLength); const seqNumberView = uint8ArrayToDataView(recordIv); seqNumberView.setUint32(recordIvLength - 4, recordNumber); completeIv = concatenateUint8Arrays([ iv, recordIv ]); } else if ( // use IV generation alg for TLS 1.3 // and ChaCha20-Poly1305 (opts.version === 'TLS1_3' || cipher === 'CHACHA20-POLY1305') && typeof recordNumber !== 'undefined') { completeIv = generateIV(completeIv, recordNumber); } const enc = await crypto.authenticatedEncrypt(cipher, { key, iv: completeIv, data: plaintext, aead, }); if (recordIv) { enc.ciphertext = concatenateUint8Arrays([ recordIv, enc.ciphertext, ]); } return { ciphertext: concatenateUint8Arrays([ enc.ciphertext, enc.authTag, ]), iv: completeIv }; } async function doSymmetricEncrypt(cipher) { const blockSize = 16; iv = padBytes(opts.iv, 16).slice(0, 16); const mac = await computeMacTls12(plaintext, opts); const completeData = concatenateUint8Arrays([ plaintext, mac, ]); // add TLS's special padding :( const padded = padTls(completeData, blockSize); const result = await crypto.encrypt(cipher, { key, iv, data: padded }); return { ciphertext: concatenateUint8Arrays([ iv, result ]), iv, }; } function padBytes(arr, len) { const returnVal = new Uint8Array(len); returnVal.set(arr, len - arr.length); return returnVal; } } function getAead(plaintextLength, opts) { const isTls13 = opts.version === 'TLS1_3'; let aead; if (isTls13) { const dataLen = plaintextLength + AUTH_TAG_BYTE_LENGTH; const recordHeader = 'recordHeaderOpts' in opts ? packPacketHeader(dataLen, opts.recordHeaderOpts) : replaceRecordHeaderLen(opts.recordHeader, dataLen); aead = recordHeader; } else { aead = getTls12Header(plaintextLength, opts); } return aead; } function getTls12Header(plaintextLength, opts) { const { recordNumber } = opts; const recordHeader = 'recordHeaderOpts' in opts ? packPacketHeader(plaintextLength, opts.recordHeaderOpts) : replaceRecordHeaderLen(opts.recordHeader, plaintextLength); const seqNumberBytes = new Uint8Array(8); const seqNumberView = uint8ArrayToDataView(seqNumberBytes); seqNumberView.setUint32(4, recordNumber || 0); return concatenateUint8Arrays([ seqNumberBytes, recordHeader, ]); } async function computeMacTls12(plaintext, opts) { const { macKey, cipherSuite } = opts; if (!macKey) { throw new Error('macKey is required for non-AEAD cipher'); } const { hashAlgorithm } = SUPPORTED_CIPHER_SUITE_MAP[cipherSuite]; const dataToSign = concatenateUint8Arrays([ getTls12Header(plaintext.length, opts), plaintext, ]); const mac = await crypto.hmac(hashAlgorithm, macKey, dataToSign); return mac; } function replaceRecordHeaderLen(header, newLength) { const newRecordHeader = new Uint8Array(header); const dataView = uint8ArrayToDataView(newRecordHeader); dataView.setUint16(3, newLength); return newRecordHeader; }