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.
		
		
		
		
		
			
		
			
				
	
	
		
			307 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			JavaScript
		
	
			
		
		
	
	
			307 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			JavaScript
		
	
| /* global
 | |
|   Signal,
 | |
|   libsignal,
 | |
|   StringView,
 | |
|   dcodeIO,
 | |
|   libloki,
 | |
|   log,
 | |
|   crypto,
 | |
|   textsecure
 | |
| */
 | |
| 
 | |
| /* eslint-disable more/no-then */
 | |
| 
 | |
| const toHex = buffer => StringView.arrayBufferToHex(buffer);
 | |
| 
 | |
| const fromHex = hex => dcodeIO.ByteBuffer.wrap(hex, 'hex').toArrayBuffer();
 | |
| 
 | |
| async function saveSenderKeysInner(
 | |
|   groupId,
 | |
|   senderIdentity,
 | |
|   chainKey,
 | |
|   keyIdx,
 | |
|   messageKeys
 | |
| ) {
 | |
|   const ratchet = {
 | |
|     chainKey,
 | |
|     messageKeys,
 | |
|     idx: keyIdx,
 | |
|   };
 | |
| 
 | |
|   await Signal.Data.createOrUpdateSenderKeys({
 | |
|     groupId,
 | |
|     senderIdentity,
 | |
|     ratchet,
 | |
|   });
 | |
| 
 | |
|   log.debug(
 | |
|     `Saving sender keys for groupId ${groupId}, sender ${senderIdentity}`
 | |
|   );
 | |
| }
 | |
| 
 | |
| // Save somebody else's key
 | |
| async function saveSenderKeys(groupId, senderIdentity, chainKey, keyIdx) {
 | |
|   const messageKeys = {};
 | |
|   await saveSenderKeysInner(
 | |
|     groupId,
 | |
|     senderIdentity,
 | |
|     chainKey,
 | |
|     keyIdx,
 | |
|     messageKeys
 | |
|   );
 | |
| }
 | |
| 
 | |
| async function createSenderKeyForGroup(groupId, senderIdentity) {
 | |
|   // Generate Chain Key (32 random bytes)
 | |
|   const rootChainKey = await libsignal.crypto.getRandomBytes(32);
 | |
|   const rootChainKeyHex = toHex(rootChainKey);
 | |
| 
 | |
|   const keyIdx = 0;
 | |
|   const messageKeys = {};
 | |
| 
 | |
|   await saveSenderKeysInner(
 | |
|     groupId,
 | |
|     senderIdentity,
 | |
|     rootChainKeyHex,
 | |
|     keyIdx,
 | |
|     messageKeys
 | |
|   );
 | |
| 
 | |
|   return rootChainKeyHex;
 | |
| }
 | |
| 
 | |
| async function hmacSHA256(keybuf, data) {
 | |
|   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) {
 | |
|   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, senderIdentity) {
 | |
|   const ratchet = await loadChainKey(groupId, senderIdentity);
 | |
| 
 | |
|   if (!ratchet) {
 | |
|     log.error(
 | |
|       `Could not find ratchet for groupId ${groupId} sender: ${senderIdentity}`
 | |
|     );
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   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,
 | |
|     senderIdentity,
 | |
|     nextChainKeyHex,
 | |
|     nextKeyIdx,
 | |
|     messageKeys
 | |
|   );
 | |
| 
 | |
|   return { messageKey, keyIdx: nextKeyIdx };
 | |
| }
 | |
| 
 | |
| // Advance the ratchet until idx
 | |
| async function advanceRatchet(groupId, senderIdentity, idx) {
 | |
|   const ratchet = await loadChainKey(groupId, senderIdentity);
 | |
| 
 | |
|   if (!ratchet) {
 | |
|     log.error(
 | |
|       `Could not find ratchet for groupId ${groupId} sender: ${senderIdentity}`
 | |
|     );
 | |
|     throw new 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) {
 | |
|       delete ratchet.messageKeys[idx];
 | |
|       // TODO: just pass in the ratchet?
 | |
|       const chainKeyHex = toHex(ratchet.chainKey);
 | |
|       await saveSenderKeysInner(
 | |
|         groupId,
 | |
|         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;
 | |
| 
 | |
|   // eslint-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,
 | |
|     senderIdentity,
 | |
|     chainKeyHex,
 | |
|     idx,
 | |
|     messageKeys
 | |
|   );
 | |
| 
 | |
|   return curMessageKey;
 | |
| }
 | |
| 
 | |
| async function loadChainKey(groupId, senderIdentity) {
 | |
|   const senderKeyEntry = await Signal.Data.getSenderKeys(
 | |
|     groupId,
 | |
|     senderIdentity
 | |
|   );
 | |
| 
 | |
|   if (!senderKeyEntry) {
 | |
|     // TODO: we should try to request the key from the sender in this case
 | |
|     log.error(
 | |
|       `Sender key not found for group ${groupId} sender ${senderIdentity}`
 | |
|     );
 | |
|     // TODO: throw instead?
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   const {
 | |
|     chainKey: chainKeyHex,
 | |
|     idx: keyIdx,
 | |
|     messageKeys,
 | |
|   } = senderKeyEntry.ratchet;
 | |
| 
 | |
|   if (!chainKeyHex) {
 | |
|     log.error('Chain key not found');
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   // TODO: This could fail if the data is not hex, handle
 | |
|   // this case
 | |
|   const chainKey = fromHex(chainKeyHex);
 | |
| 
 | |
|   return { chainKey, keyIdx, messageKeys };
 | |
| }
 | |
| 
 | |
| const jobQueue = {};
 | |
| 
 | |
| function queueJobForNumber(number, runJob) {
 | |
|   const runPrevious = jobQueue[number] || Promise.resolve();
 | |
|   const runCurrent = runPrevious.then(runJob, runJob);
 | |
|   jobQueue[number] = runCurrent;
 | |
|   runCurrent.then(() => {
 | |
|     if (jobQueue[number] === runCurrent) {
 | |
|       delete jobQueue[number];
 | |
|     }
 | |
|   });
 | |
|   return runCurrent;
 | |
| }
 | |
| 
 | |
| async function decryptWithSenderKey(
 | |
|   ciphertext,
 | |
|   curKeyIdx,
 | |
|   groupId,
 | |
|   senderIdentity
 | |
| ) {
 | |
|   // 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,
 | |
|   curKeyIdx,
 | |
|   groupId,
 | |
|   senderIdentity
 | |
| ) {
 | |
|   const messageKey = await advanceRatchet(groupId, senderIdentity, curKeyIdx);
 | |
| 
 | |
|   // TODO: this might fail, handle this
 | |
|   const plaintext = await libloki.crypto.DecryptGCM(messageKey, ciphertext);
 | |
| 
 | |
|   return plaintext;
 | |
| }
 | |
| 
 | |
| async function encryptWithSenderKey(plaintext, groupId, ourIdentity) {
 | |
|   // 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, groupId, ourIdentity) {
 | |
|   const { messageKey, keyIdx } = await stepRatchetOnce(groupId, ourIdentity);
 | |
| 
 | |
|   const ciphertext = await libloki.crypto.EncryptGCM(messageKey, plaintext);
 | |
| 
 | |
|   return { ciphertext, keyIdx };
 | |
| }
 | |
| 
 | |
| async function getSenderKeys(groupId, senderIdentity) {
 | |
|   const { chainKey, keyIdx } = await loadChainKey(groupId, senderIdentity);
 | |
| 
 | |
|   return { chainKey, keyIdx };
 | |
| }
 | |
| 
 | |
| module.exports = {
 | |
|   createSenderKeyForGroup,
 | |
|   encryptWithSenderKey,
 | |
|   decryptWithSenderKey,
 | |
|   saveSenderKeys,
 | |
|   getSenderKeys,
 | |
| };
 |