|
|
|
import ByteBuffer from 'bytebuffer';
|
|
|
|
import { generateKeyPair, sharedKey, verify } from 'curve25519-js';
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
// tslint:disable: function-name
|
|
|
|
//tslint-disable no-console
|
|
|
|
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
|
|
|
|
// tslint:disable-next-line: prefer-for-of
|
|
|
|
for (let index = 0; index < uncheckedSignatureMessages.length; index++) {
|
|
|
|
const unchecked = uncheckedSignatureMessages[index];
|
|
|
|
try {
|
|
|
|
const valid = await verifySignature(
|
|
|
|
unchecked.sender,
|
|
|
|
unchecked.base64EncodedData,
|
|
|
|
unchecked.base64EncodedSignature
|
|
|
|
);
|
|
|
|
if (valid) {
|
|
|
|
checked.push(unchecked.base64EncodedData);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// tslint: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);
|
|
|
|
const isBlindedSender = senderPubKey.startsWith('15');
|
|
|
|
|
|
|
|
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
|
|
|
|
);
|
|
|
|
|
|
|
|
// tslint:disable-next-line: restrict-plus-operands
|
|
|
|
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);
|
|
|
|
}
|