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
										 | } |