/*------------------------------------------------------------------------------ About : Created on : Mon Jun 26 2017 Author : Raghvendra Varma Copyright (c) 2017 Mubble Networks Private Limited. All rights reserved. ------------------------------------------------------------------------------*/ import { Mubble, ConnectionInfo, WireObject, DataLeader, Encoder, WssProviderConfig } from '@mubble/core' import { RunContextBrowser } from '../rc-browser' import { TextEncDec } from './text-enc-dec' const ASYM_ALGO = {name: 'RSA-OAEP', hash: {name: 'SHA-1'}} let arShortCode : Uint8Array let arUniqueId : Uint8Array let pwc : PakoWorkerClient export class EncryptionBrowser { private syncKey: any // can be sent as null by server private symAlgo constructor(private rc: RunContextBrowser, private ci: ConnectionInfo, private rsaPubKey: Uint8Array, private iv : Uint8Array) { rc.setupLogger(this, 'EncryptionBrowser') this.symAlgo = { name: "AES-CBC", length: 256, iv: this.iv } if (!arShortCode) this.extractShortCode(rc, ci.shortName) if (!arUniqueId) this.extractUniqueId(rc, ci.customData.jsVersion) if (!pwc) pwc = new PakoWorkerClient(rc) } async init() { this.syncKey = await crypto.subtle.generateKey(this.symAlgo, true, ['encrypt', 'decrypt']) } async encodeHeader(wsConfig: WssProviderConfig): Promise { const now = Date.now() * 1000, // microseconds tsBuffer = await this.encrypt(this.strToUnit8Ar(now.toString())), encTs = new Uint8Array(tsBuffer) as any, tsB64 = btoa(String.fromCharCode(...encTs)) const keyBuffer = await this.encryptSymKey(), encKey = new Uint8Array(keyBuffer) as any, keyB64 = btoa(String.fromCharCode(...encKey)) const configBuffer = await this.encrypt(this.strToUnit8Ar(JSON.stringify(wsConfig))), encConfig = new Uint8Array(configBuffer) as any, configB64 = btoa(String.fromCharCode(...encConfig)) return `${tsB64}${keyB64}${configB64}` } private async encryptSymKey() { /* this.ci.syncKey: Sym key that is generated by client and then changed by server this.syncKey: Public key that is used to protect this.ci.syncKey */ const buffer = await crypto.subtle.exportKey('raw', this.syncKey), key = await crypto.subtle.importKey('spki', this.rsaPubKey, ASYM_ALGO , false, ['encrypt']), encKey = await crypto.subtle.encrypt(ASYM_ALGO, key, buffer) return encKey } private async encrypt(ar: Uint8Array): Promise { return await crypto.subtle.encrypt(this.symAlgo, this.syncKey, ar) } private async decrypt(ar: Uint8Array): Promise { return await crypto.subtle.decrypt(this.symAlgo, this.syncKey, ar) } private getArrayBuffer(ar: Uint8Array) { if (ar.byteOffset === 0 && ar.byteLength === ar.buffer.byteLength) return ar.buffer as ArrayBuffer return ar.buffer.slice(ar.byteOffset, ar.byteOffset + ar.byteLength) as ArrayBuffer } async encodeBody(data: WireObject[]): Promise { const str = this.stringifyWireObjects(data) let firstPassArray, leader = -1, deflate = false if (str.length > Encoder.MIN_SIZE_TO_COMPRESS) { const ar = await pwc.deflate(str) if (ar.length < str.length) { firstPassArray = ar deflate = true } } if (!firstPassArray) { firstPassArray = this.strToUnit8Ar(str) } const secondPassArray = new Uint8Array(await this.encrypt(firstPassArray)), arOut = new Uint8Array(secondPassArray.byteLength + 1) leader = deflate ? DataLeader.ENC_DEF_JSON : DataLeader.ENC_JSON arOut.set([leader]) arOut.set(secondPassArray, 1) this.rc.isDebug() && this.rc.debug(this.rc.getName(this), 'encodeBody', { first : data[0].name, messages : data.length, json : str.length, wire : arOut.byteLength, encrypted : true, compressed : deflate, }) return arOut } private stringifyWireObjects(objects: WireObject[]) { const strArray = objects.map(wm => wm.stringify()) return `[${strArray.join(', ')}]` } async decodeBody(data: ArrayBuffer): Promise<[WireObject]> { const inAr = new Uint8Array(data, 1), ar = new Uint8Array(data, 0, 1), leader = ar[0], temp = new Uint8Array(await this.decrypt(inAr)) let arData, index, decLen, deflated = false if (leader === DataLeader.BINARY) { const newLineCode = '\n'.charCodeAt(0) for (index = 0; index < temp.length; index++) if (temp[index] === newLineCode) break const jsonStr = String.fromCharCode(...temp.slice(0, index) as any), wo = WireObject.getWireObject(JSON.parse(jsonStr)) as WireObject, outAr = temp.slice(index + 1) wo.data = outAr arData = [wo] decLen = outAr.byteLength } else { deflated = leader === DataLeader.DEF_JSON || leader === DataLeader.ENC_DEF_JSON const inJsonStr = deflated ? await pwc.inflate(temp) : this.uint8ArToStr(temp), inJson = JSON.parse(inJsonStr) decLen = inJsonStr.length arData = Array.isArray(inJson) ? inJson: [inJsonStr] for (index = 0; index < arData.length; index++) { arData[index] = WireObject.getWireObject(JSON.parse(arData[index])) } } this.rc.isDebug() && this.rc.debug(this.rc.getName(this), 'decodeBody', { first : arData[0].name, messages : arData.length, wire : data.byteLength, message : decLen, encrypted : leader === DataLeader.ENC_JSON || leader === DataLeader.ENC_BINARY || leader === DataLeader.ENC_DEF_JSON,compressed : leader === DataLeader.BINARY ? 'binary' : deflated }) return arData as [WireObject] } public async setNewKey(syncKey: string) { const arEncNewKey = this.binToUnit8Ar(atob(syncKey)) this.syncKey = await crypto.subtle.importKey('raw', arEncNewKey, this.symAlgo, true, ['encrypt', 'decrypt']) } async getSyncKeyB64(): Promise { const buffer = await crypto.subtle.exportKey('raw', this.syncKey), arr = new Uint8Array(buffer) as any return btoa(String.fromCharCode(...arr)) } binToUnit8Ar(binStr : string): Uint8Array { const cls:any = Uint8Array return cls.from(binStr, (c : any) => c.charCodeAt(0)) } strToUnit8Ar(str : string): Uint8Array { const TextEncoder = (window as any).TextEncoder return TextEncoder ? new TextEncoder('utf-8').encode(str) : new TextEncDec('utf-8').encode(str) } uint8ArToStr(uar : Uint8Array): string { const TextDecoder = (window as any).TextDecoder return TextDecoder ? new TextDecoder('utf-8').decode(uar) : new TextEncDec('utf-8').decode(uar) } private extractShortCode(rc: RunContextBrowser, code: string) { rc.isAssert() && rc.assert(rc.getName(this), code.length <= 4) arShortCode = new Uint8Array(4) for (let index = 0; index < code.length; index++) { const str = code.charAt(index) rc.isAssert() && rc.assert(rc.getName(this), str.match(/[a-zA-Z0-9]/)) arShortCode[index] = str.charCodeAt(0) - 40 } } private extractUniqueId(rc: RunContextBrowser, id: string) { let ar = id.split('.').map(i => Number(i)) if (ar.length > 1) { rc.isAssert() && rc.assert(rc.getName(this), ar.length === 3 && !isNaN(ar[0]) && !isNaN(ar[1]) && !isNaN(ar[2])) } else { let num = Number(ar[0]) rc.isAssert() && rc.assert(rc.getName(this), !isNaN(num) && num <= 999999) ar[2] = num % 100 num = Math.floor(num / 100) ar[1] = num % 100 ar[0] = Math.floor(num / 100) } arUniqueId = Uint8Array.from(ar) } } class AsyncRequest { static nextRequestId: number = 1 requestId : number promise : Promise resolve : (...params: any[]) => void reject : () => void constructor(public apiName: string) { this.requestId = AsyncRequest.nextRequestId++ this.promise = new Promise((resolve, reject) => { this.resolve = resolve this.reject = reject }) } } class PakoWorkerClient { private worker: Worker private reqMap: Mubble.uObject = {} constructor(private rc: RunContextBrowser) { const worker = this.worker = new Worker('js/pwc.js') worker.onmessage = this.onMessage.bind(this) } async inflate(inU8Array: Uint8Array): Promise { return await this.sendMessage('inflate', inU8Array, {to: 'string'}) as string } async deflate(str: string): Promise { return await this.sendMessage('deflate', str) as Uint8Array } private sendMessage(apiName : string, ...params : Array) { const asyncRequest = new AsyncRequest(apiName) this.reqMap[asyncRequest.requestId] = asyncRequest this.worker.postMessage([asyncRequest.requestId, apiName, ...params]) return asyncRequest.promise } onMessage(event : MessageEvent) { const [reqId, ...resp] = event.data const asyncRequest = this.reqMap[reqId] this.rc.isAssert() && this.rc.assert(this.rc.getName(this), asyncRequest) delete this.reqMap[reqId] asyncRequest.resolve(...resp) } }