From 77d6a96ee18d28ba0d19ac025ffa309eb8451601 Mon Sep 17 00:00:00 2001 From: Mohit Tejani Date: Tue, 19 Sep 2023 15:18:39 +0530 Subject: [PATCH] cryptoModule --- src/core/endpoints/file_upload/send_file.js | 19 +- .../NodeCryptoModule/NodeCryptoModule.js | 301 ++++++++++++++++++ .../modules/NodeCryptoModule/aesCbcCryptor.js | 91 ++++++ .../modules/NodeCryptoModule/legacyCryptor.js | 36 +++ src/crypto/modules/node.js | 25 +- 5 files changed, 446 insertions(+), 26 deletions(-) create mode 100644 src/crypto/modules/NodeCryptoModule/NodeCryptoModule.js create mode 100644 src/crypto/modules/NodeCryptoModule/aesCbcCryptor.js create mode 100644 src/crypto/modules/NodeCryptoModule/legacyCryptor.js diff --git a/src/core/endpoints/file_upload/send_file.js b/src/core/endpoints/file_upload/send_file.js index 6e1a08f8e..5eb8be763 100644 --- a/src/core/endpoints/file_upload/send_file.js +++ b/src/core/endpoints/file_upload/send_file.js @@ -1,18 +1,5 @@ import { PubNubError, createValidationError } from '../../components/endpoint'; -const getErrorFromResponse = (response) => - new Promise((resolve) => { - let result = ''; - - response.on('data', (data) => { - result += data.toString('utf8'); - }); - - response.on('end', () => { - resolve(result); - }); - }); - const sendFile = function ({ generateUploadUrl, publishFile, @@ -68,11 +55,9 @@ const sendFile = function ({ throw new Error('Unsupported environment'); } } catch (e) { - if (e.response) { - const errorBody = await getErrorFromResponse(e.response); - + if (e.response && typeof e.response.text === 'string') { + const errorBody = e.response.text; const reason = /(.*)<\/Message>/gi.exec(errorBody); - throw new PubNubError(reason ? `Upload to bucket failed: ${reason[1]}` : 'Upload to bucket failed.', e); } else { throw new PubNubError('Upload to bucket failed.', e); diff --git a/src/crypto/modules/NodeCryptoModule/NodeCryptoModule.js b/src/crypto/modules/NodeCryptoModule/NodeCryptoModule.js new file mode 100644 index 000000000..dbd614939 --- /dev/null +++ b/src/crypto/modules/NodeCryptoModule/NodeCryptoModule.js @@ -0,0 +1,301 @@ +import { Readable, PassThrough, Transform } from 'stream'; +import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto'; +import LegacyCryptor from './legacyCryptor'; +import AesCbcCryptor from './aesCbcCryptor'; + +export default class CryptoModule { + defaultCryptor; + cryptors = []; + + constructor({cryptoModuleConfiguration}) { + this.defaultCryptor = cryptoModuleConfiguration.default; + this.cryptors = [...this.defaultCryptor, ...cryptoModuleConfiguration.cryptors]; + } + + static legacyCryptoModule({config}) { + return new this({ + default: new LegacyCryptor({config}), + cryptors: [new AesCbcCryptor(config.cipherKey)] + }) + } + + static aesCbcCryptoModule({config}) { + return new this({ + default: new AesCbcCryptor(config.cipherKey), + cryptors: [new LegacyCryptor({config})] + }) + } + + async encrypt(data, encoding) { + let encrypted = this.default.encrypt(data); + + let header = CryptoHeader.from(this.default.identifier, encrypted.metadata); + + let payload = new Uint8Array(header.length); + let pos = 0; + payload.set(header.data, pos); + pos += header.length; + if (encrypted.metadata) { + pos -= encrypted.metadata.length; + payload.set(encrypted.metadata, pos); + } + if (encoding) { + return Buffer.concat([payload, encrypted.data]).toString(encoding); + } else { + return Buffer.concat([payload, encrypted.data]); + } + } + + async decrypt(data, encoding) { + let encryptedData = null; + if (encoding) { + encryptedData = Buffer.from(data, encoding); + } else { + encryptedData = Buffer.from(data); + } + let header = CryptoHeader.tryParse(encryptedData); + let cryptor = this._getCryptor(header); + + if (cryptor instanceof LegacyCryptor) return cryptor.decrypt(data); + + return cryptor.decrypt({ + data: encryptedData.slice(header.length), + metadata: encryptedData.slice(header.length - header.size, header.length), + }); + } + + async encryptFile(file, File) { + if (this.defaultCryptor instanceof LegacyCryptor) + return this.defaultCryptor.encryptFile(file, File) ; + if (file.data instanceof Buffer) { + return File.create({ + name: file.name, + mimeType: 'application/octet-stream', + data: await this.encrypt(file.data), + }); + } + if (file.data instanceof Readable) { + let encryptedStream = this.defaultCryptor.encryptStream(file.data); + let header = CryptoHeader.from(this.default.identifier, encryptedStream.metadata); + + let payload = new Uint8Array(header.length); + let pos = 0; + payload.set(header.data, pos); + pos += header.length; + if (encryptedStream.metadata) { + pos -= encryptedStream.metadata.length; + payload.set(encryptedStream.metadata, pos); + } + const output = new PassThrough(); + output.write(payload); + encryptedStream.data.pipe(output); + return File.create({ + name: file.name, + mimeType: 'application/octet-stream', + stream: output, + }); + } + } + + async decryptFile(file, File) { + if (file.data instanceof Buffer){ + return File.create({ + name: file.name, + mimeType: 'application/octet-stream', + data: await this.encrypt(file.data), + }); + } + + if (file.data instanceof Readable){ + let stream = file.data; + return new Promise((resolve, _) => { + stream.on('readable', () => resolve(this._decryptStream(stream,file, File))); + }); + } + } + + async _decryptStream(stream, file, File) { + let magicBytes = stream.read(4); + if (magicBytes !== null) { + if (!CryptoHeader.isSentinel(magicBytes)) { + return this._legacyFileDecrypt(magicBytes, stream); + } + let versionByte = stream.read(1); + CryptoHeader.validateVersion(versionByte[0]); + let identifier = stream.read(4); + let cryptor = this._getCryptorFromId(CryptoHeader.tryGetIdentifier(identifier)); + let headerSize = CryptoHeader.tryGetMetadataSizeFromStream(stream); + return File.create({ + name: file.name, + mimeType: 'application/octet-stream', + data: await cryptor.decryptStream({ stream: stream, metadataLength: headerSize })}); + } + } + + async _legacyFileDecrypt(bytes, stream) { + const sourceStream = new PassThrough(); + sourceStream.write(bytes); + stream.pipe(sourceStream); + stream.pause(); + sourceStream.pause(); + return this.default.decryptFile( + File.create({ + name: file.name, + mimeType: 'application/octet-stream', + data: sourceStream, + }), + ); + } + + _getCryptor(header) { + if (header === null) { + return this.legacyCryptor; + } else if (header instanceof CryptoHeaderV1) { + return this._getCryptorFromId(header.identifier); + } + } + + _getCryptorFromId(id) { + const cryptor = this.cryptors.find((c) => id === c.identifier); + if (cryptor) { + return cryptor; + } + throw Error('No registered cryptor can decrypt the data.'); + } +} + +// CryptoHeader Utility +class CryptoHeader { + static SENTINEL = 'PNED'; + static IDENTIFIER_LENGTH = 4; + static MAX_VERSION = 1; + + static from(id, headerSize) { + return new CryptoHeaderV1(id, headerSize); + } + + static isSentinel(bytes) { + if (bytes && bytes.byteLength >= 4) { + if (bytes.toString('utf8') == CryptoHeader.SENTINEL) return true; + } + } + + static validateVersion(data) { + if (data && data > CryptoHeader.MAX_VERSION) throw Error('Decrypting data created by unknown cryptor.'); + return data; + } + + static tryGetIdentifier(data) { + if (data & (data.byteLength < 4)) { + throw Error('Decrypted data header is malformed.'); + } else { + return data.toString('utf8'); + } + } + + static tryGetMetadataSizeFromStream(stream) { + let sizeBuf = stream.read(1); + if (sizeBuf && sizeBuf[0] < 255) { + return sizeBuf[0]; + } + if (sizeBuf === 255) { + let nextBuf = stream(2); + if (nextBuf & (nextBuf.byteLength >= 2)) { + return new Uint16Array([nextBuf[0], nextBuf[1]]).reduce((acc, val) => (acc << 8) + val, 0); + } + } + } + static tryParse(encryptedData) { + let sentinel = ''; + let version = null; + if (encryptedData.length >= 4) { + sentinel = encryptedData.slice(0, 4); + if (sentinel.toString('utf8') !== CryptoHeader.SENTINEL) return null; + } + + if (encryptedData.length >= 5) { + version = encryptedData[4]; + } else { + throw Error('Decrypted data header is malformed.'); + } + if (version > CryptoHeader.MAX_VERSION) throw Error('Decrypting data created by unknown cryptor.'); + + let identifier = ''; + let pos = 5 + CryptoHeader.IDENTIFIER_LENGTH; + if (encryptedData.length >= pos) { + identifier = encryptedData.slice(5, pos); + } else { + throw Error('Decrypted data header is malformed.'); + } + let headerSize = null; + if (encryptedData.length > pos + 1) { + headerSize = encryptedData[pos]; + } + pos += 1; + if (headerSize === 255 && encryptedData.length >= pos + 2) { + headerSize = new Uint16Array(encryptedData.slice(pos, pos + 3)).reduce((acc, val) => (acc << 8) + val, 0); + pos += 2; + } + return new CryptoHeaderV1(identifier.toString('utf8'), encryptedData.slice(pos, pos + headerSize)); + } +} + +// v1 cryptoHeader +class CryptoHeaderV1 { + static IDENTIFIER_LENGTH = 4; + + static SENTINEL = 'PNED'; + static VERSION = 1; + + identifier; + metadata; + + constructor(id, metadata) { + this.identifier = id; + this.metadata = metadata; + } + + get identifier() { + return this.identifier; + } + + get metadata() { + return this.metadata; + } + + get version() { + return CryptoHeaderV1.VERSION; + } + + get length() { + return ( + CryptoHeaderV1.SENTINEL.length + + 1 + + CryptoHeaderV1.IDENTIFIER_LENGTH + + (this.metadata.length < 255 ? 1 : 3) + + this.metadata.length + ); + } + + get size() { + return this.metadata.length; + } + + get data() { + let pos = 0; + const header = new Uint8Array(this.length); + header.set(Buffer.from(CryptoHeaderV1.SENTINEL)); + pos += CryptoHeaderV1.SENTINEL.length; + header[pos] = this.version; + pos++; + if (this.identifier) header.set(Buffer.from(this.identifier), pos); + pos += CryptoHeaderV1.IDENTIFIER_LENGTH; + let metadata_size = this.metadata.length; + if (metadata_size < 255) { + header[pos] = metadata_size; + } else { + header.set([255, metadata_size >> 8, metadata_size & 0xff], pos); + } + return header; + } +} diff --git a/src/crypto/modules/NodeCryptoModule/aesCbcCryptor.js b/src/crypto/modules/NodeCryptoModule/aesCbcCryptor.js new file mode 100644 index 000000000..ff24dff75 --- /dev/null +++ b/src/crypto/modules/NodeCryptoModule/aesCbcCryptor.js @@ -0,0 +1,91 @@ +import { Readable, PassThrough, Transform } from 'stream'; +import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto'; + +export default class AesCbcCryptor { + static BLOCK_SIZE = 16; + + constructor(key) { + this.cipherKey = key; + } + + get algo() { + return 'aes-256-cbc'; + } + + get identifier() { + return 'ACRH'; + } + + _getIv() { + return randomBytes(AesCbcCryptor.BLOCK_SIZE); + } + + _getKey() { + const sha = createHash('sha256'); + sha.update(Buffer.from(this.cipherKey, 'utf8')); + return Buffer.from(sha.digest(), 'utf8'); + } + + async encrypt(data) { + const iv = this._getIv(); + const key = this._getKey(); + + const bPlain = Buffer.from(data); + const aes = createCipheriv(this.algo, key, iv); + + return { + metadata: iv, + data: Buffer.concat([aes.update(bPlain), aes.final()]), + }; + } + + async decrypt(encryptedData) { + const aes = createDecipheriv(this.algo, this._getKey(), encryptedData.metadata); + return Buffer.concat([aes.update(encryptedData.data), aes.final()]); + } + + async encryptStream(stream) { + const output = new PassThrough(); + const bIv = this._getIv(); + const aes = createCipheriv(this.algo, this._getKey(), bIv); + stream.pipe(aes).pipe(output); + return { + data: output, + metadata: bIv, + metadataLength: AesCbcCryptor.BLOCK_SIZE, + }; + } + + async decryptStream(encryptedStream) { + const output = new PassThrough(); + + let bIv = Buffer.alloc(0); + let aes = null; + let data = encryptedStream.stream.read(); + while (data !== null) { + if (data) { + const bChunk = Buffer.from(data); + const sliceLen = encryptedStream.metadataLength - bIv.byteLength; + if (bChunk.byteLength < sliceLen) { + bIv = Buffer.concat([bIv, bChunk]); + } else { + bIv = Buffer.concat([bIv, bChunk.slice(0, sliceLen)]); + + aes = createDecipheriv(this.algo, this._getKey(), bIv); + + aes.pipe(output); + + aes.write(bChunk.slice(sliceLen)); + } + } + data = encryptedStream.stream.read(); + } + encryptedStream.stream.on('end', () => { + if (aes) { + aes.end(); + } + output.end(); + }); + return output; + } +} diff --git a/src/crypto/modules/NodeCryptoModule/legacyCryptor.js b/src/crypto/modules/NodeCryptoModule/legacyCryptor.js new file mode 100644 index 000000000..2b115a827 --- /dev/null +++ b/src/crypto/modules/NodeCryptoModule/legacyCryptor.js @@ -0,0 +1,36 @@ +import Crypto from '../../../core/components/cryptography/index'; +import FileCryptor from '../node'; + +export default class LegacyCryptor { + config; + cipherKey; + useRandomIVs; + + cryptor; + fileCryptor; + + constructor({ pnConfig }) { + this.config = pnConfig + this.cryptor = new Crypto({ pnConfig }); + this.fileCryptor = new FileCryptor(); + } + + get identifier() { + return ''; + } + async encrypt(data) { + return this.cryptor.encrypt(data); + } + + async decrypt(encryptedData) { + return this.cryptor.decrypt(encryptedData); + } + + async encryptFile(file, File) { + return this.fileCryptor.encryptFile(this.config.cipherKey, file, File); + } + + async decryptFile(file, File) { + return this.fileCryptor.decryptFile(this.config.cipherKey, file, File); + } +} diff --git a/src/crypto/modules/node.js b/src/crypto/modules/node.js index 27e9f4f0e..d198daba1 100644 --- a/src/crypto/modules/node.js +++ b/src/crypto/modules/node.js @@ -124,16 +124,23 @@ export default class NodeCryptography { return Buffer.concat([aes.update(bCiphertext), aes.final()]); } - encryptStream(key, stream) { - const output = new PassThrough(); + async encryptStream(key, stream) { const bIv = this.getIv(); - - const aes = createCipheriv(this.algo, key, bIv); - - output.write(bIv); - stream.pipe(aes).pipe(output); - - return output; + const aes = createCipheriv('aes-256-cbc', key, bIv).setAutoPadding(true); + let inited = false; + return stream.pipe(aes).pipe( + new Transform({ + transform(chunk, _, cb) { + if (!inited) { + inited = true; + this.push(Buffer.concat([bIv, chunk])); + } else { + this.push(chunk); + } + cb(); + }, + }), + ); } decryptStream(key, stream) {