pubnub
Version:
Publish & Subscribe Real-time Messaging with PubNub
388 lines (336 loc) • 11.9 kB
text/typescript
/**
* Legacy cryptography module.
*
* @internal
*/
import { CryptorConfiguration } from '../../interfaces/crypto-module';
import { LoggerManager } from '../logger-manager';
import { Payload } from '../../types/api';
import { decode } from '../base64_codec';
import CryptoJS from './hmac-sha256';
/**
* Convert bytes array to words array.
*
* @param b - Bytes array (buffer) which should be converted.
*
* @returns Word sized array.
*
* @internal
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
function bufferToWordArray(b: string | any[] | Uint8ClampedArray) {
const wa: number[] = [];
let i;
for (i = 0; i < b.length; i += 1) {
wa[(i / 4) | 0] |= b[i] << (24 - 8 * i);
}
// @ts-expect-error Bundled library without types.
return CryptoJS.lib.WordArray.create(wa, b.length);
}
/**
* Legacy cryptor configuration options.
*
* @internal
*/
type CryptoConfiguration = {
encryptKey?: boolean;
keyEncoding?: 'hex' | 'utf8' | 'base64' | 'binary';
keyLength?: 128 | 256;
mode?: 'ecb' | 'cbc';
};
/**
* Legacy cryptography module for files and signature.
*
* @internal
*/
export default class {
/**
* Crypto initialization vector.
*/
private iv = '0123456789012345';
/**
* List os allowed cipher key encodings.
*/
private allowedKeyEncodings = ['hex', 'utf8', 'base64', 'binary'];
/**
* Allowed cipher key lengths.
*/
private allowedKeyLengths = [128, 256];
/**
* Allowed crypto modes.
*/
private allowedModes = ['ecb', 'cbc'];
/**
* Default cryptor configuration options.
*/
private readonly defaultOptions: Required<CryptoConfiguration>;
/**
* Registered loggers' manager.
*/
private _logger?: LoggerManager;
constructor(private readonly configuration: CryptorConfiguration) {
this.logger = configuration.logger;
this.defaultOptions = {
encryptKey: true,
keyEncoding: 'utf8',
keyLength: 256,
mode: 'cbc',
};
}
/**
* Update registered loggers' manager.
*
* @param [logger] - Logger, which crypto should use.
*/
set logger(logger: LoggerManager | undefined) {
this._logger = logger;
if (this.logger) {
this.logger.debug('Crypto', () => ({
messageType: 'object',
message: this.configuration as unknown as Record<string, unknown>,
details: 'Create with configuration:',
ignoredKeys(key: string, obj: Record<string, unknown>) {
return typeof obj[key] === 'function' || key === 'logger';
},
}));
}
}
/**
* Get loggers' manager.
*
* @returns Loggers' manager (if set).
*/
get logger() {
return this._logger;
}
/**
* Generate HMAC-SHA256 hash from input data.
*
* @param data - Data from which hash should be generated.
*
* @returns HMAC-SHA256 hash from provided `data`.
*/
public HMACSHA256(data: string): string {
// @ts-expect-error Bundled library without types.
const hash = CryptoJS.HmacSHA256(data, this.configuration.secretKey);
// @ts-expect-error Bundled library without types.
return hash.toString(CryptoJS.enc.Base64);
}
/**
* Generate SHA256 hash from input data.
*
* @param data - Data from which hash should be generated.
*
* @returns SHA256 hash from provided `data`.
*/
public SHA256(data: string): string {
// @ts-expect-error Bundled library without types.
return CryptoJS.SHA256(data).toString(CryptoJS.enc.Hex);
}
/**
* Encrypt provided data.
*
* @param data - Source data which should be encrypted.
* @param [customCipherKey] - Custom cipher key (different from defined on client level).
* @param [options] - Specific crypto configuration options.
*
* @returns Encrypted `data`.
*/
public encrypt(data: string | Payload, customCipherKey?: string, options?: CryptoConfiguration) {
if (this.configuration.customEncrypt) {
if (this.logger)
this.logger.warn('Crypto', "'customEncrypt' is deprecated. Consult docs for better alternative.");
return this.configuration.customEncrypt(data);
}
return this.pnEncrypt(data as string, customCipherKey, options);
}
/**
* Decrypt provided data.
*
* @param data - Encrypted data which should be decrypted.
* @param [customCipherKey] - Custom cipher key (different from defined on client level).
* @param [options] - Specific crypto configuration options.
*
* @returns Decrypted `data`.
*/
public decrypt(data: string, customCipherKey?: string, options?: CryptoConfiguration) {
if (this.configuration.customDecrypt) {
if (this.logger)
this.logger.warn('Crypto', "'customDecrypt' is deprecated. Consult docs for better alternative.");
return this.configuration.customDecrypt(data);
}
return this.pnDecrypt(data, customCipherKey, options);
}
/**
* Encrypt provided data.
*
* @param data - Source data which should be encrypted.
* @param [customCipherKey] - Custom cipher key (different from defined on client level).
* @param [options] - Specific crypto configuration options.
*
* @returns Encrypted `data` as string.
*/
private pnEncrypt(data: string, customCipherKey?: string, options?: CryptoConfiguration): string {
const decidedCipherKey = customCipherKey ?? this.configuration.cipherKey;
if (!decidedCipherKey) return data;
if (this.logger) {
this.logger.debug('Crypto', () => ({
messageType: 'object',
message: { data, cipherKey: decidedCipherKey, ...(options ?? {}) },
details: 'Encrypt with parameters:',
}));
}
options = this.parseOptions(options);
const mode = this.getMode(options);
const cipherKey = this.getPaddedKey(decidedCipherKey, options);
if (this.configuration.useRandomIVs) {
const waIv = this.getRandomIV();
// @ts-expect-error Bundled library without types.
const waPayload = CryptoJS.AES.encrypt(data, cipherKey, { iv: waIv, mode }).ciphertext;
// @ts-expect-error Bundled library without types.
return waIv.clone().concat(waPayload.clone()).toString(CryptoJS.enc.Base64);
}
const iv = this.getIV(options);
// @ts-expect-error Bundled library without types.
const encryptedHexArray = CryptoJS.AES.encrypt(data, cipherKey, { iv, mode }).ciphertext;
// @ts-expect-error Bundled library without types.
const base64Encrypted = encryptedHexArray.toString(CryptoJS.enc.Base64);
return base64Encrypted || data;
}
/**
* Decrypt provided data.
*
* @param data - Encrypted data which should be decrypted.
* @param [customCipherKey] - Custom cipher key (different from defined on client level).
* @param [options] - Specific crypto configuration options.
*
* @returns Decrypted `data`.
*/
private pnDecrypt(data: string, customCipherKey?: string, options?: CryptoConfiguration): Payload | null {
const decidedCipherKey = customCipherKey ?? this.configuration.cipherKey;
if (!decidedCipherKey) return data;
if (this.logger) {
this.logger.debug('Crypto', () => ({
messageType: 'object',
message: { data, cipherKey: decidedCipherKey, ...(options ?? {}) },
details: 'Decrypt with parameters:',
}));
}
options = this.parseOptions(options);
const mode = this.getMode(options);
const cipherKey = this.getPaddedKey(decidedCipherKey, options);
if (this.configuration.useRandomIVs) {
const ciphertext = new Uint8ClampedArray(decode(data));
const iv = bufferToWordArray(ciphertext.slice(0, 16));
const payload = bufferToWordArray(ciphertext.slice(16));
try {
// @ts-expect-error Bundled library without types.
const plainJSON = CryptoJS.AES.decrypt({ ciphertext: payload }, cipherKey, { iv, mode }).toString(
// @ts-expect-error Bundled library without types.
CryptoJS.enc.Utf8,
);
return JSON.parse(plainJSON);
} catch (e) {
if (this.logger) this.logger.error('Crypto', () => ({ messageType: 'error', message: e }));
return null;
}
} else {
const iv = this.getIV(options);
try {
// @ts-expect-error Bundled library without types.
const ciphertext = CryptoJS.enc.Base64.parse(data);
// @ts-expect-error Bundled library without types.
const plainJSON = CryptoJS.AES.decrypt({ ciphertext }, cipherKey, { iv, mode }).toString(CryptoJS.enc.Utf8);
return JSON.parse(plainJSON);
} catch (e) {
if (this.logger) this.logger.error('Crypto', () => ({ messageType: 'error', message: e }));
return null;
}
}
}
/**
* Pre-process provided custom crypto configuration.
*
* @param incomingOptions - Configuration which should be pre-processed before use.
*
* @returns Normalized crypto configuration options.
*/
private parseOptions(incomingOptions?: CryptoConfiguration): Required<CryptoConfiguration> {
if (!incomingOptions) return this.defaultOptions;
// Defaults
const options = {
encryptKey: incomingOptions.encryptKey ?? this.defaultOptions.encryptKey,
keyEncoding: incomingOptions.keyEncoding ?? this.defaultOptions.keyEncoding,
keyLength: incomingOptions.keyLength ?? this.defaultOptions.keyLength,
mode: incomingOptions.mode ?? this.defaultOptions.mode,
};
// Validation
if (this.allowedKeyEncodings.indexOf(options.keyEncoding!.toLowerCase()) === -1)
options.keyEncoding = this.defaultOptions.keyEncoding;
if (this.allowedKeyLengths.indexOf(options.keyLength!) === -1) options.keyLength = this.defaultOptions.keyLength;
if (this.allowedModes.indexOf(options.mode!.toLowerCase()) === -1) options.mode = this.defaultOptions.mode;
return options;
}
/**
* Decode provided cipher key.
*
* @param key - Key in `encoding` provided by `options`.
* @param options - Crypto configuration options with cipher key details.
*
* @returns Array buffer with decoded key.
*/
private decodeKey(key: string, options: CryptoConfiguration) {
// @ts-expect-error Bundled library without types.
if (options.keyEncoding === 'base64') return CryptoJS.enc.Base64.parse(key);
// @ts-expect-error Bundled library without types.
if (options.keyEncoding === 'hex') return CryptoJS.enc.Hex.parse(key);
return key;
}
/**
* Add padding to the cipher key.
*
* @param key - Key which should be padded.
* @param options - Crypto configuration options with cipher key details.
*
* @returns Properly padded cipher key.
*/
private getPaddedKey(key: string, options: CryptoConfiguration) {
key = this.decodeKey(key, options);
// @ts-expect-error Bundled library without types.
if (options.encryptKey) return CryptoJS.enc.Utf8.parse(this.SHA256(key).slice(0, 32));
return key;
}
/**
* Cipher mode.
*
* @param options - Crypto configuration with information about cipher mode.
*
* @returns Crypto cipher mode.
*/
private getMode(options: CryptoConfiguration) {
// @ts-expect-error Bundled library without types.
if (options.mode === 'ecb') return CryptoJS.mode.ECB;
// @ts-expect-error Bundled library without types.
return CryptoJS.mode.CBC;
}
/**
* Cipher initialization vector.
*
* @param options - Crypto configuration with information about cipher mode.
*
* @returns Initialization vector.
*/
private getIV(options: CryptoConfiguration) {
// @ts-expect-error Bundled library without types.
return options.mode === 'cbc' ? CryptoJS.enc.Utf8.parse(this.iv) : null;
}
/**
* Random initialization vector.
*
* @returns Generated random initialization vector.
*/
private getRandomIV() {
// @ts-expect-error Bundled library without types.
return CryptoJS.lib.WordArray.random(16);
}
}