UNPKG

pubnub

Version:

Publish & Subscribe Real-time Messaging with PubNub

476 lines (475 loc) 19.4 kB
"use strict"; /** * Node.js crypto module. */ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.NodeCryptoModule = exports.AesCbcCryptor = exports.LegacyCryptor = void 0; const stream_1 = require("stream"); const buffer_1 = require("buffer"); const crypto_module_1 = require("../../../core/interfaces/crypto-module"); const base64_codec_1 = require("../../../core/components/base64_codec"); const pubnub_error_1 = require("../../../errors/pubnub-error"); const aesCbcCryptor_1 = __importDefault(require("./aesCbcCryptor")); exports.AesCbcCryptor = aesCbcCryptor_1.default; const legacyCryptor_1 = __importDefault(require("./legacyCryptor")); exports.LegacyCryptor = legacyCryptor_1.default; /** * CryptoModule for Node.js platform. */ class NodeCryptoModule extends crypto_module_1.AbstractCryptoModule { /** * Assign registered loggers' manager. * * @param logger - Registered loggers' manager. * * @internal */ set logger(logger) { if (this.defaultCryptor.identifier === NodeCryptoModule.LEGACY_IDENTIFIER) this.defaultCryptor.logger = logger; else { const cryptor = this.cryptors.find((cryptor) => cryptor.identifier === NodeCryptoModule.LEGACY_IDENTIFIER); if (cryptor) cryptor.logger = logger; } } // -------------------------------------------------------- // --------------- Convenience functions ------------------ // ------------------------------------------------------- // region Convenience functions static legacyCryptoModule(config) { var _a; if (!config.cipherKey) throw new pubnub_error_1.PubNubError('Crypto module error: cipher key not set.'); return new this({ default: new legacyCryptor_1.default(Object.assign(Object.assign({}, config), { useRandomIVs: (_a = config.useRandomIVs) !== null && _a !== void 0 ? _a : true })), cryptors: [new aesCbcCryptor_1.default({ cipherKey: config.cipherKey })], }); } static aesCbcCryptoModule(config) { var _a; if (!config.cipherKey) throw new pubnub_error_1.PubNubError('Crypto module error: cipher key not set.'); return new this({ default: new aesCbcCryptor_1.default({ cipherKey: config.cipherKey }), cryptors: [ new legacyCryptor_1.default(Object.assign(Object.assign({}, config), { useRandomIVs: (_a = config.useRandomIVs) !== null && _a !== void 0 ? _a : true })), ], }); } /** * Construct crypto module with `cryptor` as default for data encryption and decryption. * * @param defaultCryptor - Default cryptor for data encryption and decryption. * * @returns Crypto module with pre-configured default cryptor. */ static withDefaultCryptor(defaultCryptor) { return new this({ default: defaultCryptor }); } // endregion // -------------------------------------------------------- // --------------------- Encryption ----------------------- // -------------------------------------------------------- // region Encryption encrypt(data) { // Encrypt data. const encrypted = data instanceof ArrayBuffer && this.defaultCryptor.identifier === NodeCryptoModule.LEGACY_IDENTIFIER ? this.defaultCryptor.encrypt(NodeCryptoModule.decoder.decode(data)) : this.defaultCryptor.encrypt(data); if (!encrypted.metadata) return encrypted.data; const headerData = this.getHeaderData(encrypted); // Write encrypted data payload content. const encryptedData = typeof encrypted.data === 'string' ? NodeCryptoModule.encoder.encode(encrypted.data).buffer : encrypted.data.buffer.slice(encrypted.data.byteOffset, encrypted.data.byteOffset + encrypted.data.length); return this.concatArrayBuffer(headerData, encryptedData); } encryptFile(file, File) { return __awaiter(this, void 0, void 0, function* () { /** * Files handled differently in case of Legacy cryptor. * (as long as we support legacy need to check on instance type) */ if (this.defaultCryptor.identifier === CryptorHeader.LEGACY_IDENTIFIER) return this.defaultCryptor.encryptFile(file, File); if (file.data instanceof buffer_1.Buffer) { const encryptedData = this.encrypt(file.data); return File.create({ name: file.name, mimeType: 'application/octet-stream', data: buffer_1.Buffer.from(typeof encryptedData === 'string' ? NodeCryptoModule.encoder.encode(encryptedData) : encryptedData), }); } if (file.data instanceof stream_1.Readable) { if (!file.contentLength || file.contentLength === 0) throw new Error('Encryption error: empty content'); const encryptedStream = yield this.defaultCryptor.encryptStream(file.data); const header = CryptorHeader.from(this.defaultCryptor.identifier, encryptedStream.metadata); const payload = new Uint8Array(header.length); let pos = 0; payload.set(header.data, pos); pos += header.length; if (encryptedStream.metadata) { const metadata = new Uint8Array(encryptedStream.metadata); pos -= encryptedStream.metadata.byteLength; payload.set(metadata, pos); } const output = new stream_1.PassThrough(); output.write(payload); encryptedStream.stream.pipe(output); return File.create({ name: file.name, mimeType: 'application/octet-stream', stream: output, }); } }); } // endregion // -------------------------------------------------------- // --------------------- Decryption ----------------------- // -------------------------------------------------------- // region Decryption decrypt(data) { const encryptedData = buffer_1.Buffer.from(typeof data === 'string' ? (0, base64_codec_1.decode)(data) : data); const header = CryptorHeader.tryParse(encryptedData.buffer.slice(encryptedData.byteOffset, encryptedData.byteOffset + encryptedData.length)); const cryptor = this.getCryptor(header); const metadata = header.length > 0 ? encryptedData.slice(header.length - header.metadataLength, header.length) : null; if (encryptedData.slice(header.length).byteLength <= 0) throw new Error('Decryption error: empty content'); return cryptor.decrypt({ data: encryptedData.slice(header.length), metadata: metadata, }); } decryptFile(file, File) { return __awaiter(this, void 0, void 0, function* () { if (file.data && file.data instanceof buffer_1.Buffer) { const header = CryptorHeader.tryParse(file.data.buffer.slice(file.data.byteOffset, file.data.byteOffset + file.data.length)); const cryptor = this.getCryptor(header); /** * If It's legacy one then redirect it. * (as long as we support legacy need to check on instance type) */ if ((cryptor === null || cryptor === void 0 ? void 0 : cryptor.identifier) === NodeCryptoModule.LEGACY_IDENTIFIER) return cryptor.decryptFile(file, File); return File.create({ name: file.name, data: buffer_1.Buffer.from(this.decrypt(file.data)), }); } if (file.data && file.data instanceof stream_1.Readable) { const stream = file.data; return new Promise((resolve) => { stream.on('readable', () => resolve(this.onStreamReadable(stream, file, File))); }); } }); } // endregion // -------------------------------------------------------- // ----------------------- Helpers ------------------------ // -------------------------------------------------------- // region Helpers /** * Retrieve registered legacy cryptor. * * @returns Previously registered {@link ILegacyCryptor|legacy} cryptor. * * @throws Error if legacy cryptor not registered. * * @internal */ getLegacyCryptor() { return this.getCryptorFromId(NodeCryptoModule.LEGACY_IDENTIFIER); } /** * Retrieve registered cryptor by its identifier. * * @param id - Unique cryptor identifier. * * @returns Registered cryptor with specified identifier. * * @throws Error if cryptor with specified {@link id} can't be found. * * @internal */ getCryptorFromId(id) { const cryptor = this.getAllCryptors().find((cryptor) => id === cryptor.identifier); if (cryptor) return cryptor; throw new Error('Unknown cryptor error'); } /** * Retrieve cryptor by its identifier. * * @param header - Header with cryptor-defined data or raw cryptor identifier. * * @returns Cryptor which correspond to provided {@link header}. * * @internal */ getCryptor(header) { if (typeof header === 'string') { const cryptor = this.getAllCryptors().find((c) => c.identifier === header); if (cryptor) return cryptor; throw new Error('Unknown cryptor error'); } else if (header instanceof CryptorHeaderV1) { return this.getCryptorFromId(header.identifier); } } /** * Create cryptor header data. * * @param encrypted - Encryption data object as source for header data. * * @returns Binary representation of the cryptor header data. * * @internal */ getHeaderData(encrypted) { if (!encrypted.metadata) return; const header = CryptorHeader.from(this.defaultCryptor.identifier, encrypted.metadata); const headerData = new Uint8Array(header.length); let pos = 0; headerData.set(header.data, pos); pos += header.length - encrypted.metadata.byteLength; headerData.set(new Uint8Array(encrypted.metadata), pos); return headerData.buffer; } /** * Merge two {@link ArrayBuffer} instances. * * @param ab1 - First {@link ArrayBuffer}. * @param ab2 - Second {@link ArrayBuffer}. * * @returns Merged data as {@link ArrayBuffer}. * * @internal */ concatArrayBuffer(ab1, ab2) { const tmp = new Uint8Array(ab1.byteLength + ab2.byteLength); tmp.set(new Uint8Array(ab1), 0); tmp.set(new Uint8Array(ab2), ab1.byteLength); return tmp.buffer; } /** * {@link Readable} stream event handler. * * @param stream - Stream which can be used to read data for decryption. * @param file - File object which has been created with {@link stream}. * @param File - Class constructor for {@link PubNub} File object. * * @returns Decrypted data as {@link PubNub} File object. * * @throws Error if file is empty or contains unsupported data type. * * @internal */ onStreamReadable(stream, file, File) { return __awaiter(this, void 0, void 0, function* () { stream.removeAllListeners('readable'); const magicBytes = stream.read(4); if (!CryptorHeader.isSentinel(magicBytes)) { if (magicBytes === null) throw new Error('Decryption error: empty content'); stream.unshift(magicBytes); return this.decryptLegacyFileStream(stream, file, File); } const versionByte = stream.read(1); CryptorHeader.validateVersion(versionByte[0]); const identifier = stream.read(4); const cryptor = this.getCryptorFromId(CryptorHeader.tryGetIdentifier(identifier)); const headerSize = CryptorHeader.tryGetMetadataSizeFromStream(stream); if (!file.contentLength || file.contentLength <= CryptorHeader.MIN_HEADER_LENGTH + headerSize) throw new Error('Decryption error: empty content'); return File.create({ name: file.name, mimeType: 'application/octet-stream', stream: (yield cryptor.decryptStream({ stream: stream, metadataLength: headerSize, })), }); }); } /** * Decrypt {@link Readable} stream using legacy cryptor. * * @param stream - Stream which can be used to read data for decryption. * @param file - File object which has been created with {@link stream}. * @param File - Class constructor for {@link PubNub} File object. * * @returns Decrypted data as {@link PubNub} File object. * * @throws Error if file is empty or contains unsupported data type. * * @internal */ decryptLegacyFileStream(stream, file, File) { return __awaiter(this, void 0, void 0, function* () { if (!file.contentLength || file.contentLength <= 16) throw new Error('Decryption error: empty content'); const cryptor = this.getLegacyCryptor(); if (cryptor) { return cryptor.decryptFile(File.create({ name: file.name, stream: stream, }), File); } else throw new Error('unknown cryptor error'); }); } } exports.NodeCryptoModule = NodeCryptoModule; /** * {@link LegacyCryptor|Legacy} cryptor identifier. */ NodeCryptoModule.LEGACY_IDENTIFIER = ''; /** * CryptorHeader Utility * * @internal */ class CryptorHeader { static from(id, metadata) { if (id === CryptorHeader.LEGACY_IDENTIFIER) return; return new CryptorHeaderV1(id, metadata.byteLength); } static isSentinel(bytes) { return bytes && bytes.byteLength >= 4 && CryptorHeader.decoder.decode(bytes) == CryptorHeader.SENTINEL; } static validateVersion(data) { if (data && data > CryptorHeader.MAX_VERSION) throw new Error('Decryption error: invalid header version'); return data; } static tryGetIdentifier(data) { if (data.byteLength < 4) throw new Error('Decryption error: unknown cryptor error'); else return CryptorHeader.decoder.decode(data); } static tryGetMetadataSizeFromStream(stream) { const sizeBuf = stream.read(1); if (sizeBuf && sizeBuf[0] < 255) return sizeBuf[0]; if (sizeBuf[0] === 255) { const nextBuf = stream.read(2); if (nextBuf.length >= 2) { return new Uint16Array([nextBuf[0], nextBuf[1]]).reduce((acc, val) => (acc << 8) + val, 0); } } throw new Error('Decryption error: invalid metadata size'); } static tryParse(encryptedData) { const encryptedDataView = new DataView(encryptedData); let sentinel; let version = null; if (encryptedData.byteLength >= 4) { sentinel = encryptedData.slice(0, 4); if (!this.isSentinel(sentinel)) return NodeCryptoModule.LEGACY_IDENTIFIER; } if (encryptedData.byteLength >= 5) version = encryptedDataView.getInt8(4); else throw new Error('Decryption error: invalid header version'); if (version > CryptorHeader.MAX_VERSION) throw new Error('unknown cryptor error'); let identifier; let pos = 5 + CryptorHeader.IDENTIFIER_LENGTH; if (encryptedData.byteLength >= pos) identifier = encryptedData.slice(5, pos); else throw new Error('Decryption error: invalid crypto identifier'); let metadataLength = null; if (encryptedData.byteLength >= pos + 1) metadataLength = encryptedDataView.getInt8(pos); else throw new Error('Decryption error: invalid metadata length'); pos += 1; if (metadataLength === 255 && encryptedData.byteLength >= pos + 2) { metadataLength = new Uint16Array(encryptedData.slice(pos, pos + 2)).reduce((acc, val) => (acc << 8) + val, 0); } return new CryptorHeaderV1(CryptorHeader.decoder.decode(identifier), metadataLength); } } CryptorHeader.decoder = new TextDecoder(); CryptorHeader.SENTINEL = 'PNED'; CryptorHeader.LEGACY_IDENTIFIER = ''; CryptorHeader.IDENTIFIER_LENGTH = 4; CryptorHeader.VERSION = 1; CryptorHeader.MAX_VERSION = 1; CryptorHeader.MIN_HEADER_LENGTH = 10; /** * Cryptor header (v1). * * @internal */ class CryptorHeaderV1 { constructor(id, metadataLength) { this._identifier = id; this._metadataLength = metadataLength; } get identifier() { return this._identifier; } set identifier(value) { this._identifier = value; } get metadataLength() { return this._metadataLength; } set metadataLength(value) { this._metadataLength = value; } get version() { return CryptorHeader.VERSION; } get length() { return (CryptorHeader.SENTINEL.length + 1 + CryptorHeader.IDENTIFIER_LENGTH + (this.metadataLength < 255 ? 1 : 3) + this.metadataLength); } get data() { let pos = 0; const header = new Uint8Array(this.length); header.set(buffer_1.Buffer.from(CryptorHeader.SENTINEL)); pos += CryptorHeader.SENTINEL.length; header[pos] = this.version; pos++; if (this.identifier) header.set(buffer_1.Buffer.from(this.identifier), pos); const metadataLength = this.metadataLength; pos += CryptorHeader.IDENTIFIER_LENGTH; if (metadataLength < 255) header[pos] = metadataLength; else header.set([255, metadataLength >> 8, metadataLength & 0xff], pos); return header; } }