import ByteBuffer from 'bytebuffer'; import { generateKeyPair, sharedKey, verify } from 'curve25519-js'; // eslint-disable-next-line import/no-named-default import { default as sodiumWrappers } from 'libsodium-wrappers-sumo'; import _ from 'lodash'; import { decryptAttachmentBufferNode as realDecryptAttachmentBufferNode, encryptAttachmentBufferNode as realEncryptAttachmentBufferNode, } from '../../../../node/encrypt_attachment_buffer'; /* eslint-disable no-console */ /* eslint-disable strict */ async function getSodiumWorker() { await sodiumWrappers.ready; return sodiumWrappers; } const functions = { arrayBufferToStringBase64, fromBase64ToArrayBuffer, fromHexToArrayBuffer, verifyAllSignatures, DecryptAESGCM, deriveSymmetricKey, encryptForPubkey, decryptAttachmentBufferNode, encryptAttachmentBufferNode, bytesFromString, }; onmessage = async (e: any) => { const [jobId, fnName, ...args] = e.data; try { const fn = (functions as any)[fnName]; if (!fn) { throw new Error(`Worker: job ${jobId} did not find function ${fnName}`); } const result = await fn(...args); postMessage([jobId, null, result]); } catch (error) { const errorForDisplay = prepareErrorForPostMessage(error); postMessage([jobId, errorForDisplay]); } }; function prepareErrorForPostMessage(error: any) { if (!error) { return null; } if (error.stack) { return error.stack; } return error.message; } function arrayBufferToStringBase64(arrayBuffer: ArrayBuffer) { return ByteBuffer.wrap(arrayBuffer).toString('base64'); } async function encryptAttachmentBufferNode(encryptingKey: Uint8Array, bufferIn: ArrayBuffer) { return realEncryptAttachmentBufferNode(encryptingKey, bufferIn, getSodiumWorker); } async function decryptAttachmentBufferNode(encryptingKey: Uint8Array, bufferIn: ArrayBuffer) { return realDecryptAttachmentBufferNode(encryptingKey, bufferIn, getSodiumWorker); } function fromBase64ToArrayBuffer(base64Str: string) { return ByteBuffer.wrap(base64Str, 'base64').toArrayBuffer(); } function fromBase64ToUint8Array(base64Str: string) { return new Uint8Array(ByteBuffer.wrap(base64Str, 'base64').toArrayBuffer()); } function fromHexToArray(hexStr: string) { return new Uint8Array(ByteBuffer.wrap(hexStr, 'hex').toArrayBuffer()); } function fromHexToArrayBuffer(hexStr: string) { return ByteBuffer.wrap(hexStr, 'hex').toArrayBuffer(); } function bytesFromString(str: string) { return ByteBuffer.wrap(str, 'utf8').toArrayBuffer(); } // hexString, base64String, base64String async function verifyAllSignatures( uncheckedSignatureMessages: Array<{ base64EncodedData: string; base64EncodedSignature: string; sender: string; }> ) { const checked = []; // keep this out of a racing (i.e. no Promise.all) for easier debugging for now // eslint-disable-next-line: prefer-for-of for (let index = 0; index < uncheckedSignatureMessages.length; index++) { const unchecked = uncheckedSignatureMessages[index]; try { // eslint-disable-next-line no-await-in-loop const valid = await verifySignature( unchecked.sender, unchecked.base64EncodedData, unchecked.base64EncodedSignature ); if (valid) { checked.push(unchecked.base64EncodedData); continue; } // eslint:disable: no-console console.info('got an opengroup message with an invalid signature'); } catch (e) { console.error(e); } } return _.compact(checked) || []; } // hexString, base64String, base64String async function verifySignature( senderPubKey: string, messageBase64: string, signatureBase64: string ) { try { if (typeof senderPubKey !== 'string') { throw new Error('senderPubKey type not correct'); } if (typeof messageBase64 !== 'string') { throw new Error('messageBase64 type not correct'); } if (typeof signatureBase64 !== 'string') { throw new Error('signatureBase64 type not correct'); } const messageData = fromBase64ToUint8Array(messageBase64); const signature = fromBase64ToUint8Array(signatureBase64); // blinded15 or blinded25 are the same for the verifySignature logic const isBlindedSender = senderPubKey.startsWith('15') || senderPubKey.startsWith('25'); const pubkeyWithoutPrefix = senderPubKey.slice(2); const pubkeyBytes = fromHexToArray(pubkeyWithoutPrefix); if (isBlindedSender) { const sodium = await getSodiumWorker(); const blindedVerifySig = sodium.crypto_sign_verify_detached( signature, messageData, pubkeyBytes ); if (!blindedVerifySig) { console.info('Invalid signature blinded'); return false; } return true; } // verify returns true if the signature is not correct const verifyRet = verify(pubkeyBytes, messageData, signature); if (!verifyRet) { console.error('Invalid signature not blinded'); return false; } return true; } catch (e) { console.error('verifySignature got an error:', e); return false; } } const NONCE_LENGTH = 12; async function deriveSymmetricKey(x25519PublicKey: Uint8Array, x25519PrivateKey: Uint8Array) { assertArrayBufferView(x25519PublicKey); assertArrayBufferView(x25519PrivateKey); const ephemeralSecret = sharedKey(x25519PrivateKey, x25519PublicKey); const salt = bytesFromString('LOKI'); const key = await crypto.subtle.importKey( 'raw', salt, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign'] ); const symmetricKey = await crypto.subtle.sign( { name: 'HMAC', hash: 'SHA-256' }, key, ephemeralSecret ); return symmetricKey; } function assertArrayBufferView(val: any) { if (!ArrayBuffer.isView(val)) { throw new Error('val type not correct'); } } // encryptForPubkey: hexString, payloadBytes: Uint8Array async function encryptForPubkey(pubkeyX25519str: string, payloadBytes: Uint8Array) { try { if (typeof pubkeyX25519str !== 'string') { throw new Error('pubkeyX25519str type not correct'); } assertArrayBufferView(payloadBytes); const ran = (await getSodiumWorker()).randombytes_buf(32); const ephemeral = generateKeyPair(ran); const pubkeyX25519Buffer = fromHexToArray(pubkeyX25519str); const symmetricKey = await deriveSymmetricKey( pubkeyX25519Buffer, new Uint8Array(ephemeral.private) ); const ciphertext = await EncryptAESGCM(symmetricKey, payloadBytes); return { ciphertext, symmetricKey, ephemeralKey: ephemeral.public }; } catch (e) { console.error('encryptForPubkey got an error:', e); return null; } } async function EncryptAESGCM(symmetricKey: ArrayBuffer, plaintext: ArrayBuffer) { const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH)); const key = await crypto.subtle.importKey('raw', symmetricKey, { name: 'AES-GCM' }, false, [ 'encrypt', ]); const ciphertext = await crypto.subtle.encrypt( { name: 'AES-GCM', iv: nonce, tagLength: 128 }, key, plaintext ); const ivAndCiphertext = new Uint8Array(NONCE_LENGTH + ciphertext.byteLength); ivAndCiphertext.set(nonce); ivAndCiphertext.set(new Uint8Array(ciphertext), nonce.byteLength); return ivAndCiphertext; } // uint8array, uint8array async function DecryptAESGCM(symmetricKey: Uint8Array, ivAndCiphertext: Uint8Array) { assertArrayBufferView(symmetricKey); assertArrayBufferView(ivAndCiphertext); const nonce = ivAndCiphertext.buffer.slice(0, NONCE_LENGTH); const ciphertext = ivAndCiphertext.buffer.slice(NONCE_LENGTH); const key = await crypto.subtle.importKey( 'raw', symmetricKey.buffer, { name: 'AES-GCM' }, false, ['decrypt'] ); return crypto.subtle.decrypt({ name: 'AES-GCM', iv: nonce }, key, ciphertext); }