/* 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, };