UNPKG

pubnub

Version:

Publish & Subscribe Real-time Messaging with PubNub

305 lines (258 loc) 9.78 kB
/** * Legacy Node.js cryptography module. * * @internal */ import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto'; import { Readable, PassThrough, Transform } from 'stream'; import { Buffer } from 'buffer'; import PubNubFile, { PubNubFileParameters } from '../../file/modules/node'; import { Cryptography } from '../../core/interfaces/cryptography'; import { PubNubFileConstructor } from '../../core/types/file'; /** * Legacy cryptography implementation for Node.js-based {@link PubNub} client. * * @internal */ export default class NodeCryptography implements Cryptography<string | ArrayBuffer | Buffer | Readable> { /** * Random initialization vector size. */ static IV_LENGTH = 16; // -------------------------------------------------------- // --------------------- Encryption ----------------------- // -------------------------------------------------------- // region Encryption public async encrypt( key: string, input: string | ArrayBuffer | Buffer | Readable, ): Promise<string | ArrayBuffer | Buffer | Transform> { const bKey = this.getKey(key); if (input instanceof Buffer) return this.encryptBuffer(bKey, input); if (input instanceof Readable) return this.encryptStream(bKey, input); if (typeof input === 'string') return this.encryptString(bKey, input); throw new Error('Encryption error: unsupported input format'); } /** * Encrypt provided source {@link Buffer} using specific encryption {@link key}. * * @param key - Data encryption key. <br/>**Note:** Same key should be used to `decrypt` {@link Buffer}. * @param buffer - Source {@link Buffer} for encryption. * * @returns Encrypted data as {@link Buffer} object. */ private encryptBuffer(key: Buffer, buffer: Buffer) { const bIv = this.getIv(); const aes = createCipheriv(this.algo, key, bIv); return Buffer.concat([bIv, aes.update(buffer), aes.final()]); } /** * Encrypt provided source {@link Readable} stream using specific encryption {@link key}. * * @param key - Data encryption key. <br/>**Note:** Same key should be used to `decrypt` {@link Readable} stream. * @param stream - Source {@link Readable} stream for encryption. * * @returns Encrypted data as {@link Transform} object. */ private async encryptStream(key: Buffer, stream: Readable) { const bIv = this.getIv(); const aes = createCipheriv(this.algo, key, bIv).setAutoPadding(true); let initiated = false; return stream.pipe(aes).pipe( new Transform({ transform(chunk, _, cb) { if (!initiated) { initiated = true; this.push(Buffer.concat([bIv, chunk])); } else this.push(chunk); cb(); }, }), ); } /** * 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 encryptString(key: Buffer, text: string) { const bIv = this.getIv(); const bPlaintext = Buffer.from(text); const aes = createCipheriv(this.algo, key, bIv); return Buffer.concat([bIv, aes.update(bPlaintext), aes.final()]).toString('utf8'); } public async encryptFile( key: string, file: PubNubFile, File: PubNubFileConstructor<PubNubFile, PubNubFileParameters>, ) { const bKey = this.getKey(key); /** * Buffer type check also covers `string` which converted to the `Buffer` during file object creation. */ if (file.data instanceof Buffer) { if (file.data.byteLength <= 0) throw new Error('Encryption error: empty content.'); return File.create({ name: file.name, mimeType: file.mimeType, data: this.encryptBuffer(bKey, file.data), }); } if (file.data instanceof Readable) { if (!file.contentLength || file.contentLength === 0) throw new Error('Encryption error: empty content.'); return File.create({ name: file.name, mimeType: file.mimeType, stream: await this.encryptStream(bKey, file.data), }); } throw new Error('Cannot encrypt this file. In Node.js file encryption supports only string, Buffer or Stream.'); } // endregion // -------------------------------------------------------- // --------------------- Decryption ----------------------- // -------------------------------------------------------- // region Decryption public async decrypt(key: string, input: string | ArrayBuffer | Buffer | Readable) { const bKey = this.getKey(key); if (input instanceof ArrayBuffer) { const decryptedBuffer = this.decryptBuffer(bKey, Buffer.from(input)); return decryptedBuffer.buffer.slice( decryptedBuffer.byteOffset, decryptedBuffer.byteOffset + decryptedBuffer.length, ); } if (input instanceof Buffer) return this.decryptBuffer(bKey, input); if (input instanceof Readable) return this.decryptStream(bKey, input); if (typeof input === 'string') return this.decryptString(bKey, input); throw new Error('Decryption error: unsupported input format'); } /** * Decrypt provided encrypted {@link Buffer} using specific decryption {@link key}. * * @param key - Data decryption key. <br/>**Note:** Should be the same as used to `encrypt` {@link Buffer}. * @param buffer - Encrypted {@link Buffer} for decryption. * * @returns Decrypted data as {@link Buffer} object. */ private decryptBuffer(key: Buffer, buffer: Buffer) { const bIv = buffer.slice(0, NodeCryptography.IV_LENGTH); const bCiphertext = buffer.slice(NodeCryptography.IV_LENGTH); if (bCiphertext.byteLength <= 0) throw new Error('Decryption error: empty content'); const aes = createDecipheriv(this.algo, key, bIv); return Buffer.concat([aes.update(bCiphertext), aes.final()]); } /** * Decrypt provided encrypted {@link Readable} stream using specific decryption {@link key}. * * @param key - Data decryption key. <br/>**Note:** Should be the same as used to `encrypt` {@link Readable} stream. * @param stream - Encrypted {@link Readable} stream for decryption. * * @returns Decrypted data as {@link Readable} object. */ private decryptStream(key: Buffer, stream: Readable) { let aes: ReturnType<typeof createDecipheriv> | null = null; const output = new PassThrough(); let bIv = Buffer.alloc(0); const getIv = () => { let data = stream.read(); while (data !== null) { if (data) { const bChunk = Buffer.from(data); const sliceLen = NodeCryptography.IV_LENGTH - bIv.byteLength; if (bChunk.byteLength < sliceLen) bIv = Buffer.concat([bIv, bChunk]); else { bIv = Buffer.concat([bIv, bChunk.slice(0, sliceLen)]); aes = createDecipheriv(this.algo, key, bIv); aes.pipe(output); aes.write(bChunk.slice(sliceLen)); } } data = stream.read(); } }; stream.on('readable', getIv); stream.on('end', () => { if (aes) aes.end(); output.end(); }); return output; } /** * 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 decryptString(key: Buffer, text: string) { const ciphertext = Buffer.from(text); const bIv = ciphertext.slice(0, NodeCryptography.IV_LENGTH); const bCiphertext = ciphertext.slice(NodeCryptography.IV_LENGTH); const aes = createDecipheriv(this.algo, key, bIv); return Buffer.concat([aes.update(bCiphertext), aes.final()]).toString('utf8'); } public async decryptFile( key: string, file: PubNubFile, File: PubNubFileConstructor<PubNubFile, PubNubFileParameters>, ) { const bKey = this.getKey(key); /** * Buffer type check also covers `string` which converted to the `Buffer` during file object creation. */ if (file.data instanceof Buffer) { return File.create({ name: file.name, mimeType: file.mimeType, data: this.decryptBuffer(bKey, file.data), }); } if (file.data instanceof Readable) { return File.create({ name: file.name, mimeType: file.mimeType, stream: this.decryptStream(bKey, file.data), }); } throw new Error('Cannot decrypt this file. In Node.js file decryption supports only string, Buffer or Stream.'); } // endregion // -------------------------------------------------------- // ----------------------- Helpers ------------------------ // -------------------------------------------------------- // region Helpers /** * Cryptography algorithm. * * @returns Cryptography module algorithm. */ private get algo() { return 'aes-256-cbc'; } /** * Convert cipher key to the {@link Buffer}. * * @param key - String cipher key. * * @returns SHA256 HEX encoded cipher key {@link Buffer}. */ private getKey(key: string) { const sha = createHash('sha256'); sha.update(Buffer.from(key, 'utf8')); return Buffer.from(sha.digest('hex').slice(0, 32), 'utf8'); } /** * Generate random initialization vector. * * @returns Random initialization vector. */ private getIv() { return randomBytes(NodeCryptography.IV_LENGTH); } // endregion }