pubnub
Version:
Publish & Subscribe Real-time Messaging with PubNub
246 lines (212 loc) • 8.98 kB
text/typescript
/**
* 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
}