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.
		
		
		
		
		
			
		
			
	
	
		
			262 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			TypeScript
		
	
		
		
			
		
	
	
			262 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			TypeScript
		
	
| 
								 
											5 years ago
										 
									 | 
							
								import { PubKey } from '../types';
							 | 
						||
| 
								 | 
							
								import * as Data from '../../../js/modules/data';
							 | 
						||
| 
								 | 
							
								import { saveSenderKeysInner } from './index';
							 | 
						||
| 
								 | 
							
								import { StringUtils } from '../utils';
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								const toHex = (buffer: ArrayBuffer) => StringUtils.decode(buffer, 'hex');
							 | 
						||
| 
								 | 
							
								const fromHex = (hex: string) => StringUtils.encode(hex, 'hex');
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const jobQueue: { [key: string]: Promise<any> } = {};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								async function queueJobForNumber(number: string, runJob: any) {
							 | 
						||
| 
								 | 
							
								  // tslint:disable-next-line no-promise-as-boolean
							 | 
						||
| 
								 | 
							
								  const runPrevious = jobQueue[number] || Promise.resolve();
							 | 
						||
| 
								 | 
							
								  const runCurrent = runPrevious.then(runJob, runJob);
							 | 
						||
| 
								 | 
							
								  jobQueue[number] = runCurrent;
							 | 
						||
| 
								 | 
							
								  // tslint:disable-next-line no-floating-promises
							 | 
						||
| 
								 | 
							
								  runCurrent.then(() => {
							 | 
						||
| 
								 | 
							
								    if (jobQueue[number] === runCurrent) {
							 | 
						||
| 
								 | 
							
								      // tslint:disable-next-line no-dynamic-delete
							 | 
						||
| 
								 | 
							
								      delete jobQueue[number];
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								  });
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								  return runCurrent;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								// This is different from the other ratchet type!
							 | 
						||
| 
								 | 
							
								interface Ratchet {
							 | 
						||
| 
								 | 
							
								  chainKey: any;
							 | 
						||
| 
								 | 
							
								  keyIdx: number;
							 | 
						||
| 
								 | 
							
								  messageKeys: any;
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								async function loadChainKey(groupId: string, senderIdentity: string) {
							 | 
						||
| 
								 | 
							
								  const senderKeyEntry = await Data.getSenderKeys(groupId, senderIdentity);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (!senderKeyEntry) {
							 | 
						||
| 
								 | 
							
								    // TODO: we should try to request the key from the sender in this case
							 | 
						||
| 
								 | 
							
								    throw Error(
							 | 
						||
| 
								 | 
							
								      `Sender key not found for group ${groupId} sender ${senderIdentity}`
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const { chainKeyHex, idx: keyIdx, messageKeys } = senderKeyEntry.ratchet;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (!chainKeyHex) {
							 | 
						||
| 
								 | 
							
								    throw Error('Chain key not found');
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // TODO: This could fail if the data is not hex, handle
							 | 
						||
| 
								 | 
							
								  // this case
							 | 
						||
| 
								 | 
							
								  const chainKey = fromHex(chainKeyHex);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return { chainKey, keyIdx, messageKeys };
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								export async function getChainKey(groupId: string, senderIdentity: string) {
							 | 
						||
| 
								 | 
							
								  const { chainKey, keyIdx } = await loadChainKey(groupId, senderIdentity);
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								  return { chainKey, keyIdx };
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								export async function encryptWithSenderKey(
							 | 
						||
| 
								 | 
							
								  plaintext: Uint8Array,
							 | 
						||
| 
								 | 
							
								  groupId: string,
							 | 
						||
| 
								 | 
							
								  ourIdentity: string
							 | 
						||
| 
								 | 
							
								) {
							 | 
						||
| 
								 | 
							
								  // We only want to serialize jobs with the same pair (groupId, ourIdentity)
							 | 
						||
| 
								 | 
							
								  const id = groupId + ourIdentity;
							 | 
						||
| 
								 | 
							
								  return queueJobForNumber(id, () =>
							 | 
						||
| 
								 | 
							
								    encryptWithSenderKeyInner(plaintext, groupId, ourIdentity)
							 | 
						||
| 
								 | 
							
								  );
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								async function encryptWithSenderKeyInner(
							 | 
						||
| 
								 | 
							
								  plaintext: Uint8Array,
							 | 
						||
| 
								 | 
							
								  groupId: string,
							 | 
						||
| 
								 | 
							
								  ourIdentity: string
							 | 
						||
| 
								 | 
							
								) {
							 | 
						||
| 
								 | 
							
								  const { messageKey, keyIdx } = await stepRatchetOnce(groupId, ourIdentity);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const ciphertext = await window.libloki.crypto.EncryptGCM(
							 | 
						||
| 
								 | 
							
								    messageKey,
							 | 
						||
| 
								 | 
							
								    plaintext
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								  );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								  return { ciphertext, keyIdx };
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								async function hmacSHA256(keybuf: any, data: any) {
							 | 
						||
| 
								 | 
							
								  // NOTE: importKey returns a 'PromiseLike'
							 | 
						||
| 
								 | 
							
								  // tslint:disable-next-line await-promise
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								  const key = await crypto.subtle.importKey(
							 | 
						||
| 
								 | 
							
								    'raw',
							 | 
						||
| 
								 | 
							
								    keybuf,
							 | 
						||
| 
								 | 
							
								    { name: 'HMAC', hash: { name: 'SHA-256' } },
							 | 
						||
| 
								 | 
							
								    false,
							 | 
						||
| 
								 | 
							
								    ['sign']
							 | 
						||
| 
								 | 
							
								  );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return crypto.subtle.sign({ name: 'HMAC', hash: 'SHA-256' }, key, data);
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								async function stepRatchet(ratchet: Ratchet) {
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								  const { chainKey, keyIdx, messageKeys } = ratchet;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const byteArray = new Uint8Array(1);
							 | 
						||
| 
								 | 
							
								  byteArray[0] = 1;
							 | 
						||
| 
								 | 
							
								  const messageKey = await hmacSHA256(chainKey, byteArray.buffer);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  byteArray[0] = 2;
							 | 
						||
| 
								 | 
							
								  const nextChainKey = await hmacSHA256(chainKey, byteArray.buffer);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const nextKeyIdx = keyIdx + 1;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return { nextChainKey, messageKey, nextKeyIdx, messageKeys };
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								async function stepRatchetOnce(
							 | 
						||
| 
								 | 
							
								  groupId: string,
							 | 
						||
| 
								 | 
							
								  senderIdentity: string
							 | 
						||
| 
								 | 
							
								): Promise<{ messageKey: any; keyIdx: any }> {
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								  const ratchet = await loadChainKey(groupId, senderIdentity);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (!ratchet) {
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								    window.log.error(
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								      `Could not find ratchet for groupId ${groupId} sender: ${senderIdentity}`
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								    throw {};
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const { nextChainKey, messageKey, nextKeyIdx } = await stepRatchet(ratchet);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Don't need to remember message keys for a sending ratchet
							 | 
						||
| 
								 | 
							
								  const messageKeys = {};
							 | 
						||
| 
								 | 
							
								  const nextChainKeyHex = toHex(nextChainKey);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  await saveSenderKeysInner(
							 | 
						||
| 
								 | 
							
								    groupId,
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								    PubKey.cast(senderIdentity),
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								    nextChainKeyHex,
							 | 
						||
| 
								 | 
							
								    nextKeyIdx,
							 | 
						||
| 
								 | 
							
								    messageKeys
							 | 
						||
| 
								 | 
							
								  );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return { messageKey, keyIdx: nextKeyIdx };
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								// Advance the ratchet until idx
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								async function advanceRatchet(
							 | 
						||
| 
								 | 
							
								  groupId: string,
							 | 
						||
| 
								 | 
							
								  senderIdentity: string,
							 | 
						||
| 
								 | 
							
								  idx: number
							 | 
						||
| 
								 | 
							
								) {
							 | 
						||
| 
								 | 
							
								  const { log } = window;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								  const ratchet = await loadChainKey(groupId, senderIdentity);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (!ratchet) {
							 | 
						||
| 
								 | 
							
								    log.error(
							 | 
						||
| 
								 | 
							
								      `Could not find ratchet for groupId ${groupId} sender: ${senderIdentity}`
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								    throw new window.textsecure.SenderKeyMissing(senderIdentity);
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Normally keyIdx will be 1 behind, in which case we stepRatchet one time only
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (idx < ratchet.keyIdx) {
							 | 
						||
| 
								 | 
							
								    // If the request is for some old index, retrieve the key generated earlier and
							 | 
						||
| 
								 | 
							
								    // remove it from the database (there is no need to advance the ratchet)
							 | 
						||
| 
								 | 
							
								    const messageKey = ratchet.messageKeys[idx];
							 | 
						||
| 
								 | 
							
								    if (messageKey) {
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								      // tslint:disable-next-line no-dynamic-delete
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								      delete ratchet.messageKeys[idx];
							 | 
						||
| 
								 | 
							
								      // TODO: just pass in the ratchet?
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								      // tslint:disable-next-line no-shadowed-variable
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								      const chainKeyHex = toHex(ratchet.chainKey);
							 | 
						||
| 
								 | 
							
								      await saveSenderKeysInner(
							 | 
						||
| 
								 | 
							
								        groupId,
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								        PubKey.cast(senderIdentity),
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								        chainKeyHex,
							 | 
						||
| 
								 | 
							
								        ratchet.keyIdx,
							 | 
						||
| 
								 | 
							
								        ratchet.messageKeys
							 | 
						||
| 
								 | 
							
								      );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      return fromHex(messageKey);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    log.error('[idx] not found key for idx: ', idx);
							 | 
						||
| 
								 | 
							
								    // I probably want a better error handling than this
							 | 
						||
| 
								 | 
							
								    return null;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const { messageKeys } = ratchet;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  let curMessageKey;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								  // tslint:disable-next-line no-constant-condition
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								  while (true) {
							 | 
						||
| 
								 | 
							
								    // eslint-disable-next-line no-await-in-loop
							 | 
						||
| 
								 | 
							
								    const { nextKeyIdx, nextChainKey, messageKey } = await stepRatchet(ratchet);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    ratchet.chainKey = nextChainKey;
							 | 
						||
| 
								 | 
							
								    ratchet.keyIdx = nextKeyIdx;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (nextKeyIdx === idx) {
							 | 
						||
| 
								 | 
							
								      curMessageKey = messageKey;
							 | 
						||
| 
								 | 
							
								      break;
							 | 
						||
| 
								 | 
							
								    } else if (nextKeyIdx > idx) {
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								      log.error(
							 | 
						||
| 
								 | 
							
								        `Could not decrypt for an older ratchet step: (${nextKeyIdx})nextKeyIdx > (${idx})idx`
							 | 
						||
| 
								 | 
							
								      );
							 | 
						||
| 
								 | 
							
								      throw new Error(`Cannot revert ratchet for group ${groupId}!`);
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      // Store keys for skipped nextKeyIdx, we might need them to decrypt
							 | 
						||
| 
								 | 
							
								      // messages that arrive out-of-order
							 | 
						||
| 
								 | 
							
								      messageKeys[nextKeyIdx] = toHex(messageKey);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const chainKeyHex = toHex(ratchet.chainKey);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  await saveSenderKeysInner(
							 | 
						||
| 
								 | 
							
								    groupId,
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								    PubKey.cast(senderIdentity),
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								    chainKeyHex,
							 | 
						||
| 
								 | 
							
								    idx,
							 | 
						||
| 
								 | 
							
								    messageKeys
							 | 
						||
| 
								 | 
							
								  );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return curMessageKey;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								export async function decryptWithSenderKey(
							 | 
						||
| 
								 | 
							
								  ciphertext: Uint8Array,
							 | 
						||
| 
								 | 
							
								  curKeyIdx: number,
							 | 
						||
| 
								 | 
							
								  groupId: string,
							 | 
						||
| 
								 | 
							
								  senderIdentity: string
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								) {
							 | 
						||
| 
								 | 
							
								  // We only want to serialize jobs with the same pair (groupId, senderIdentity)
							 | 
						||
| 
								 | 
							
								  const id = groupId + senderIdentity;
							 | 
						||
| 
								 | 
							
								  return queueJobForNumber(id, () =>
							 | 
						||
| 
								 | 
							
								    decryptWithSenderKeyInner(ciphertext, curKeyIdx, groupId, senderIdentity)
							 | 
						||
| 
								 | 
							
								  );
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								async function decryptWithSenderKeyInner(
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								  ciphertext: Uint8Array,
							 | 
						||
| 
								 | 
							
								  curKeyIdx: number,
							 | 
						||
| 
								 | 
							
								  groupId: string,
							 | 
						||
| 
								 | 
							
								  senderIdentity: string
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								) {
							 | 
						||
| 
								 | 
							
								  const messageKey = await advanceRatchet(groupId, senderIdentity, curKeyIdx);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // TODO: this might fail, handle this
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								  const plaintext = await window.libloki.crypto.DecryptGCM(
							 | 
						||
| 
								 | 
							
								    messageKey,
							 | 
						||
| 
								 | 
							
								    ciphertext
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								  );
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								
							 | 
						||
| 
								 
											5 years ago
										 
									 | 
							
								  return plaintext;
							 | 
						||
| 
								 
											6 years ago
										 
									 | 
							
								}
							 |