UNPKG

pubnub

Version:

Publish & Subscribe Real-time Messaging with PubNub

246 lines (212 loc) 8.98 kB
/** * Legacy browser cryptography module. * * @internal */ /* global crypto */ import { PubNubFile, PubNubFileParameters } from '../../file/modules/web'; import { Cryptography } from '../../core/interfaces/cryptography'; import { PubNubFileConstructor } from '../../core/types/file'; /** * Legacy cryptography implementation for browser-based {@link PubNub} client. * * @internal */ export default class WebCryptography implements Cryptography<ArrayBuffer | string> { /** * Random initialization vector size. */ static IV_LENGTH = 16; /** * {@link string|String} to {@link ArrayBuffer} response decoder. */ static encoder = new TextEncoder(); /** * {@link ArrayBuffer} to {@link string} decoder. */ static decoder = new TextDecoder(); // -------------------------------------------------------- // --------------------- Encryption ----------------------- // -------------------------------------------------------- // region Encryption /** * Encrypt provided source data using specific encryption {@link key}. * * @param key - Data encryption key. <br/>**Note:** Same key should be used to `decrypt` data. * @param input - Source data for encryption. * * @returns Encrypted data as object or stream (depending on from source data type). * * @throws Error if unknown data type has been passed. */ public async encrypt(key: string, input: ArrayBuffer | string) { if (!(input instanceof ArrayBuffer) && typeof input !== 'string') throw new Error('Cannot encrypt this file. In browsers file encryption supports only string or ArrayBuffer'); const cKey = await this.getKey(key); return input instanceof ArrayBuffer ? this.encryptArrayBuffer(cKey, input) : this.encryptString(cKey, input); } /** * Encrypt provided source {@link Buffer} using specific encryption {@link ArrayBuffer}. * * @param key - Data encryption key. <br/>**Note:** Same key should be used to `decrypt` {@link ArrayBuffer}. * @param buffer - Source {@link ArrayBuffer} for encryption. * * @returns Encrypted data as {@link ArrayBuffer} object. */ private async encryptArrayBuffer(key: CryptoKey, buffer: ArrayBuffer) { const abIv = crypto.getRandomValues(new Uint8Array(16)); return this.concatArrayBuffer(abIv.buffer, await crypto.subtle.encrypt({ name: 'AES-CBC', iv: abIv }, key, buffer)); } /** * Encrypt provided source {@link string} using specific encryption {@link key}. * * @param key - Data encryption key. <br/>**Note:** Same key should be used to `decrypt` {@link string}. * @param text - Source {@link string} for encryption. * * @returns Encrypted data as byte {@link string}. */ private async encryptString(key: CryptoKey, text: string) { const abIv = crypto.getRandomValues(new Uint8Array(16)); const abPlaintext = WebCryptography.encoder.encode(text).buffer; const abPayload = await crypto.subtle.encrypt({ name: 'AES-CBC', iv: abIv }, key, abPlaintext); const ciphertext = this.concatArrayBuffer(abIv.buffer, abPayload); return WebCryptography.decoder.decode(ciphertext); } /** * Encrypt provided {@link PubNub} File object using specific encryption {@link key}. * * @param key - Key for {@link PubNub} File object encryption. <br/>**Note:** Same key should be * used to `decrypt` data. * @param file - Source {@link PubNub} File object for encryption. * @param File - Class constructor for {@link PubNub} File object. * * @returns Encrypted data as {@link PubNub} File object. * * @throws Error if file is empty or contains unsupported data type. */ public async encryptFile( key: string, file: PubNubFile, File: PubNubFileConstructor<PubNubFile, PubNubFileParameters>, ) { if ((file.contentLength ?? 0) <= 0) throw new Error('encryption error. empty content'); const bKey = await this.getKey(key); const abPlaindata = await file.toArrayBuffer(); const abCipherdata = await this.encryptArrayBuffer(bKey, abPlaindata); return File.create({ name: file.name, mimeType: file.mimeType ?? 'application/octet-stream', data: abCipherdata, }); } // endregion // -------------------------------------------------------- // --------------------- Decryption ----------------------- // -------------------------------------------------------- // region Decryption /** * Decrypt provided encrypted data using specific decryption {@link key}. * * @param key - Data decryption key. <br/>**Note:** Should be the same as used to `encrypt` data. * @param input - Encrypted data for decryption. * * @returns Decrypted data as object or stream (depending on from encrypted data type). * * @throws Error if unknown data type has been passed. */ public async decrypt(key: string, input: ArrayBuffer | string) { if (!(input instanceof ArrayBuffer) && typeof input !== 'string') throw new Error('Cannot decrypt this file. In browsers file decryption supports only string or ArrayBuffer'); const cKey = await this.getKey(key); return input instanceof ArrayBuffer ? this.decryptArrayBuffer(cKey, input) : this.decryptString(cKey, input); } /** * Decrypt provided encrypted {@link ArrayBuffer} using specific decryption {@link key}. * * @param key - Data decryption key. <br/>**Note:** Should be the same as used to `encrypt` {@link ArrayBuffer}. * @param buffer - Encrypted {@link ArrayBuffer} for decryption. * * @returns Decrypted data as {@link ArrayBuffer} object. */ private async decryptArrayBuffer(key: CryptoKey, buffer: ArrayBuffer) { const abIv = buffer.slice(0, 16); if (buffer.slice(WebCryptography.IV_LENGTH).byteLength <= 0) throw new Error('decryption error: empty content'); return await crypto.subtle.decrypt({ name: 'AES-CBC', iv: abIv }, key, buffer.slice(WebCryptography.IV_LENGTH)); } /** * Decrypt provided encrypted {@link string} using specific decryption {@link key}. * * @param key - Data decryption key. <br/>**Note:** Should be the same as used to `encrypt` {@link string}. * @param text - Encrypted {@link string} for decryption. * * @returns Decrypted data as byte {@link string}. */ private async decryptString(key: CryptoKey, text: string) { const abCiphertext = WebCryptography.encoder.encode(text).buffer; const abIv = abCiphertext.slice(0, 16); const abPayload = abCiphertext.slice(16); const abPlaintext = await crypto.subtle.decrypt({ name: 'AES-CBC', iv: abIv }, key, abPayload); return WebCryptography.decoder.decode(abPlaintext); } /** * Decrypt provided {@link PubNub} File object using specific decryption {@link key}. * * @param key - Key for {@link PubNub} File object decryption. <br/>**Note:** Should be the same * as used to `encrypt` data. * @param file - Encrypted {@link PubNub} File object for decryption. * @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. */ public async decryptFile( key: string, file: PubNubFile, File: PubNubFileConstructor<PubNubFile, PubNubFileParameters>, ) { const bKey = await this.getKey(key); const abCipherdata = await file.toArrayBuffer(); const abPlaindata = await this.decryptArrayBuffer(bKey, abCipherdata); return File.create({ name: file.name, mimeType: file.mimeType, data: abPlaindata, }); } // endregion // -------------------------------------------------------- // ----------------------- Helpers ------------------------ // -------------------------------------------------------- // region Helpers /** * Convert cipher key to the {@link Buffer}. * * @param key - String cipher key. * * @returns SHA256 HEX encoded cipher key {@link CryptoKey}. */ private async getKey(key: string) { const digest = await crypto.subtle.digest('SHA-256', WebCryptography.encoder.encode(key)); const hashHex = Array.from(new Uint8Array(digest)) .map((b) => b.toString(16).padStart(2, '0')) .join(''); const abKey = WebCryptography.encoder.encode(hashHex.slice(0, 32)).buffer; return crypto.subtle.importKey('raw', abKey, 'AES-CBC', true, ['encrypt', 'decrypt']); } /** * Join two `ArrayBuffer`s. * * @param ab1 - `ArrayBuffer` to which other buffer should be appended. * @param ab2 - `ArrayBuffer` which should appended to the other buffer. * * @returns Buffer which starts with `ab1` elements and appended `ab2`. */ private concatArrayBuffer(ab1: ArrayBuffer, ab2: ArrayBuffer) { const tmp = new Uint8Array(ab1.byteLength + ab2.byteLength); tmp.set(new Uint8Array(ab1), 0); tmp.set(new Uint8Array(ab2), ab1.byteLength); return tmp.buffer; } // endregion }