You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			272 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			272 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			TypeScript
		
	
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);
 | 
						|
}
 |