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.
		
		
		
		
		
			
		
			
				
	
	
		
			276 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			276 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			TypeScript
		
	
| import { PubKey } from '../types';
 | |
| import * as Data from '../../../js/modules/data';
 | |
| import { saveSenderKeysInner } from './index';
 | |
| import { StringUtils } from '../utils';
 | |
| 
 | |
| 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];
 | |
|     }
 | |
|   });
 | |
|   return runCurrent;
 | |
| }
 | |
| 
 | |
| // This is different from the other ratchet type!
 | |
| interface Ratchet {
 | |
|   chainKey: any;
 | |
|   keyIdx: number;
 | |
|   messageKeys: any;
 | |
| }
 | |
| 
 | |
| // TODO: change the signature to return "NO KEY" instead of throwing
 | |
| async function loadChainKey(
 | |
|   groupId: string,
 | |
|   senderIdentity: string
 | |
| ): Promise<Ratchet | null> {
 | |
|   const senderKeyEntry = await Data.getSenderKeys(groupId, senderIdentity);
 | |
| 
 | |
|   if (!senderKeyEntry) {
 | |
|     // TODO: we should try to request the key from the sender in this case
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   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 };
 | |
| }
 | |
| 
 | |
| export async function getChainKey(
 | |
|   groupId: string,
 | |
|   senderIdentity: string
 | |
| ): Promise<{ chainKey: Uint8Array; keyIdx: number } | null> {
 | |
|   const maybeKey = await loadChainKey(groupId, senderIdentity);
 | |
| 
 | |
|   if (!maybeKey) {
 | |
|     return null;
 | |
|   } else {
 | |
|     const { chainKey, keyIdx } = maybeKey;
 | |
|     return { chainKey, keyIdx };
 | |
|   }
 | |
| }
 | |
| 
 | |
| 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
 | |
|   );
 | |
| 
 | |
|   return { ciphertext, keyIdx };
 | |
| }
 | |
| 
 | |
| async function hmacSHA256(keybuf: any, data: any) {
 | |
|   // NOTE: importKey returns a 'PromiseLike'
 | |
|   // tslint:disable-next-line await-promise
 | |
|   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);
 | |
| }
 | |
| 
 | |
| async function stepRatchet(ratchet: Ratchet) {
 | |
|   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 };
 | |
| }
 | |
| 
 | |
| async function stepRatchetOnce(
 | |
|   groupId: string,
 | |
|   senderIdentity: string
 | |
| ): Promise<{ messageKey: any; keyIdx: any }> {
 | |
|   const ratchet = await loadChainKey(groupId, senderIdentity);
 | |
| 
 | |
|   if (!ratchet) {
 | |
|     window.log.error(
 | |
|       `Could not find ratchet for groupId ${groupId} sender: ${senderIdentity}`
 | |
|     );
 | |
|     throw {};
 | |
|   }
 | |
| 
 | |
|   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,
 | |
|     PubKey.cast(senderIdentity),
 | |
|     nextChainKeyHex,
 | |
|     nextKeyIdx,
 | |
|     messageKeys
 | |
|   );
 | |
| 
 | |
|   return { messageKey, keyIdx: nextKeyIdx };
 | |
| }
 | |
| 
 | |
| // Advance the ratchet until idx
 | |
| async function advanceRatchet(
 | |
|   groupId: string,
 | |
|   senderIdentity: string,
 | |
|   idx: number
 | |
| ) {
 | |
|   const { log } = window;
 | |
| 
 | |
|   const ratchet = await loadChainKey(groupId, senderIdentity);
 | |
| 
 | |
|   if (!ratchet) {
 | |
|     log.error(
 | |
|       `Could not find ratchet for groupId ${groupId} sender: ${senderIdentity}`
 | |
|     );
 | |
|     throw new window.textsecure.SenderKeyMissing(senderIdentity);
 | |
|   }
 | |
| 
 | |
|   // 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) {
 | |
|       // tslint:disable-next-line no-dynamic-delete
 | |
|       delete ratchet.messageKeys[idx];
 | |
|       // TODO: just pass in the ratchet?
 | |
|       // tslint:disable-next-line no-shadowed-variable
 | |
|       const chainKeyHex = toHex(ratchet.chainKey);
 | |
|       await saveSenderKeysInner(
 | |
|         groupId,
 | |
|         PubKey.cast(senderIdentity),
 | |
|         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;
 | |
| 
 | |
|   // tslint:disable-next-line no-constant-condition
 | |
|   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) {
 | |
|       log.error(
 | |
|         `Could not decrypt for an older ratchet step: (${nextKeyIdx})nextKeyIdx > (${idx})idx`
 | |
|       );
 | |
|       throw new Error(`Cannot revert ratchet for group ${groupId}!`);
 | |
|     } 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,
 | |
|     PubKey.cast(senderIdentity),
 | |
|     chainKeyHex,
 | |
|     idx,
 | |
|     messageKeys
 | |
|   );
 | |
| 
 | |
|   return curMessageKey;
 | |
| }
 | |
| 
 | |
| export async function decryptWithSenderKey(
 | |
|   ciphertext: Uint8Array,
 | |
|   curKeyIdx: number,
 | |
|   groupId: string,
 | |
|   senderIdentity: string
 | |
| ) {
 | |
|   // 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(
 | |
|   ciphertext: Uint8Array,
 | |
|   curKeyIdx: number,
 | |
|   groupId: string,
 | |
|   senderIdentity: string
 | |
| ) {
 | |
|   const messageKey = await advanceRatchet(groupId, senderIdentity, curKeyIdx);
 | |
| 
 | |
|   if (!messageKey) {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   // TODO: this might fail, handle this
 | |
|   const plaintext = await window.libloki.crypto.DecryptGCM(
 | |
|     messageKey,
 | |
|     ciphertext
 | |
|   );
 | |
| 
 | |
|   return plaintext;
 | |
| }
 |