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.
298 lines
6.9 KiB
JavaScript
298 lines
6.9 KiB
JavaScript
/* global
|
|
Signal,
|
|
libsignal,
|
|
StringView,
|
|
dcodeIO,
|
|
libloki,
|
|
log,
|
|
crypto
|
|
*/
|
|
|
|
/* 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) {
|
|
// New key, so index 0
|
|
const keyIdx = 0;
|
|
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}`
|
|
);
|
|
return null;
|
|
}
|
|
|
|
// 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('Developer error: nextKeyIdx > idx');
|
|
} 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 };
|
|
}
|
|
|
|
module.exports = {
|
|
createSenderKeyForGroup,
|
|
encryptWithSenderKey,
|
|
decryptWithSenderKey,
|
|
saveSenderKeys,
|
|
};
|