UNPKG

@simplito/privmx-webendpoint

Version:

PrivMX Web Endpoint library

173 lines (172 loc) 7.04 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DataChannelCryptorError = exports.DataChannelCryptor = void 0; const Types_1 = require("../Types"); const Logger_1 = require("./Logger"); const AES_GCM_KEY_LENGTH_BYTES = 32; const GCM_NONCE_LENGTH_BYTES = 12; const GCM_TAG_LENGTH_BITS = 128; const VERSION_LENGTH_BYTES = 1; const KEY_ID_LENGTH_BYTES = 1; const SEQUENCE_NUMBER_LENGTH_BYTES = 4; const WIRE_FORMAT_VERSION = 1; const FIXED_HEADER_LENGTH = VERSION_LENGTH_BYTES + KEY_ID_LENGTH_BYTES + SEQUENCE_NUMBER_LENGTH_BYTES + GCM_NONCE_LENGTH_BYTES; class DataChannelCryptor { keyStore; textEncoder = new TextEncoder(); textDecoder = new TextDecoder(); constructor(keyStore) { this.keyStore = keyStore; } async encryptToWireFormat(params) { const { plaintext, sequenceNumber } = params; const keyId = this.keyStore.getEncryptionKeyId(); this.assertKeyId(keyId); this.assertSequenceNumberValue(sequenceNumber); if (sequenceNumber < 0) { throw new Error("sequenceNumber must be non-negative"); } const keyIdBytes = this.textEncoder.encode(keyId); if (keyIdBytes.length > 0xffff) { throw new Error(`keyId too long: ${keyIdBytes.length}`); } const iv = crypto.getRandomValues(new Uint8Array(GCM_NONCE_LENGTH_BYTES)); const header = this.serializeHeader({ version: WIRE_FORMAT_VERSION, sequenceNumber, iv, keyIdBytes, }); const cryptoKey = await this.keyStore.getEncriptionKey(); const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData: header, tagLength: GCM_TAG_LENGTH_BITS, }, cryptoKey, plaintext); const ciphertext = new Uint8Array(encrypted); return this.concat(header, ciphertext); } async decryptFromWireFormat(params) { const parsed = this.parseEncryptedFrame(params.frame, params.lastSequenceNumber); const logger = new Logger_1.Logger(); logger.debug("decryptFromWireFormat", params, parsed); if (!this.keyStore.hasKey(parsed.keyId)) { throw new DataChannelCryptorError(Types_1.DataChannelCryptorDecryptStatus.KEY_NOT_FOUND, `Key not found: ${parsed.keyId}`); } const cryptoKey = await this.keyStore.getKey(parsed.keyId); try { const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv: parsed.iv, additionalData: parsed.header, tagLength: GCM_TAG_LENGTH_BITS, }, cryptoKey, parsed.ciphertext); return { data: new Uint8Array(decrypted), seq: parsed.sequenceNumber }; } catch { throw new DataChannelCryptorError(Types_1.DataChannelCryptorDecryptStatus.DECRYPT_AUTH_FAILED, `Decryption failed (auth error)`); } } parseEncryptedFrame(frame, lastSeq) { this.assertFrameLength(frame); const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength); let offset = 0; const version = view.getUint8(offset); offset += 1; if (version !== WIRE_FORMAT_VERSION) { throw new DataChannelCryptorError(Types_1.DataChannelCryptorDecryptStatus.UNSUPPORTED_VERSION, `Unsupported version: ${version}`); } const sequenceNumber = view.getUint32(offset, false); offset += SEQUENCE_NUMBER_LENGTH_BYTES; this.assertSequence(sequenceNumber, lastSeq); const iv = frame.slice(offset, offset + GCM_NONCE_LENGTH_BYTES); offset += GCM_NONCE_LENGTH_BYTES; this.assertIv(iv); const keyIdLength = view.getUint8(offset); offset += KEY_ID_LENGTH_BYTES; const headerLength = FIXED_HEADER_LENGTH + keyIdLength; if (frame.length < headerLength) { throw new DataChannelCryptorError(Types_1.DataChannelCryptorDecryptStatus.FRAME_TRUNCATED, `Frame truncated`); } const keyIdBytes = frame.slice(offset, offset + keyIdLength); offset += keyIdLength; const keyId = this.textDecoder.decode(keyIdBytes); this.assertKeyId(keyId); const ciphertext = frame.slice(offset); const header = frame.slice(0, headerLength); return { version, sequenceNumber, keyId, iv, ciphertext, header, }; } serializeHeader(params) { const { version, sequenceNumber, iv, keyIdBytes } = params; this.assertIv(iv); this.assertSequenceNumberValue(sequenceNumber); const header = new Uint8Array(FIXED_HEADER_LENGTH + keyIdBytes.length); const view = new DataView(header.buffer); let offset = 0; view.setUint8(offset, version); offset += VERSION_LENGTH_BYTES; view.setUint32(offset, sequenceNumber, false); offset += SEQUENCE_NUMBER_LENGTH_BYTES; header.set(iv, offset); offset += GCM_NONCE_LENGTH_BYTES; view.setUint8(offset, keyIdBytes.length); offset += KEY_ID_LENGTH_BYTES; header.set(keyIdBytes, offset); return header; } assertFrameLength(frame) { if (!frame || frame.length < FIXED_HEADER_LENGTH) { throw new DataChannelCryptorError(Types_1.DataChannelCryptorDecryptStatus.FRAME_TOO_SHORT, "Frame too short"); } } assertKeyId(keyId) { if (!keyId || keyId.trim().length === 0) { throw new DataChannelCryptorError(Types_1.DataChannelCryptorDecryptStatus.INVALID_KEY_ID, "Invalid KeyID"); } } assertSequence(msgSeq, lastSeq) { if (msgSeq <= lastSeq) { throw new DataChannelCryptorError(Types_1.DataChannelCryptorDecryptStatus.INVALID_DATA_SEQUENCE, `Invalid data sequence number: ${msgSeq}`); } } assertSequenceNumberValue(sequenceNumber) { if (!Number.isInteger(sequenceNumber)) { throw new Error(`sequenceNumber must be an integer, got: ${sequenceNumber}`); } if (sequenceNumber < 0 || sequenceNumber > 0xffffffff) { throw new Error(`sequenceNumber must fit in uint32, got: ${sequenceNumber}`); } } assertIv(iv) { if (iv.length !== GCM_NONCE_LENGTH_BYTES) { throw new DataChannelCryptorError(Types_1.DataChannelCryptorDecryptStatus.INVALID_IV_LENGTH, `Invalid IV length: ${iv.length}`); } } concat(a, b) { const out = new Uint8Array(a.length + b.length); out.set(a); out.set(b, a.length); return out; } } exports.DataChannelCryptor = DataChannelCryptor; class DataChannelCryptorError extends Error { code; constructor(code, message) { super(message); this.name = "DataChannelCryptorError"; this.code = code; } } exports.DataChannelCryptorError = DataChannelCryptorError;