Multi-session storage for close/regular message race conditions
This commit is contained in:
parent
cdebc8afb4
commit
3a812d4958
1 changed files with 129 additions and 25 deletions
154
js/crypto.js
154
js/crypto.js
|
@ -2,6 +2,9 @@
|
|||
var crypto_tests = {};
|
||||
|
||||
window.crypto = (function() {
|
||||
// We consider messages lost after a week and might throw away keys at that point
|
||||
var MESSAGE_LOST_THRESHOLD_MS = 1000*60*60*24*7;
|
||||
|
||||
crypto.getRandomBytes = function(size) {
|
||||
//TODO: Better random (https://www.grc.com/r&d/js.htm?)
|
||||
try {
|
||||
|
@ -98,13 +101,70 @@ window.crypto = (function() {
|
|||
}
|
||||
|
||||
crypto_storage.saveSession = function(encodedNumber, session) {
|
||||
storage.putEncrypted("session" + getEncodedNumber(encodedNumber), session);
|
||||
var sessions = storage.getEncrypted("session" + getEncodedNumber(encodedNumber));
|
||||
if (sessions === undefined)
|
||||
sessions = {};
|
||||
|
||||
var doDeleteSession = false;
|
||||
if (session.indexInfo.closed == -1)
|
||||
sessions.identityKey = session.indexInfo.remoteIdentityKey;
|
||||
else {
|
||||
doDeleteSession = (session.indexInfo.closed < (new Date().getTime() - MESSAGE_LOST_THRESHOLD_MS));
|
||||
|
||||
if (!doDeleteSession) {
|
||||
var keysLeft = false;
|
||||
for (key in session) {
|
||||
if (key != "indexInfo" && key != "indexInfo" && key != "oldRatchetList") {
|
||||
keysLeft = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
doDeleteSession = !keysLeft;
|
||||
}
|
||||
}
|
||||
|
||||
if (doDeleteSession)
|
||||
delete sessions[getString(session.indexInfo.baseKey)];
|
||||
else
|
||||
sessions[getString(session.indexInfo.baseKey)] = session;
|
||||
|
||||
storage.putEncrypted("session" + getEncodedNumber(encodedNumber), sessions);
|
||||
}
|
||||
|
||||
crypto_storage.getSession = function(encodedNumber) {
|
||||
return storage.getEncrypted("session" + getEncodedNumber(encodedNumber));
|
||||
}
|
||||
crypto_storage.getSession = function(encodedNumber, remoteEphemeralKey) {
|
||||
var sessions = storage.getEncrypted("session" + getEncodedNumber(encodedNumber));
|
||||
if (sessions === undefined)
|
||||
return undefined;
|
||||
|
||||
var searchKey = "NOTAKEY";
|
||||
if (remoteEphemeralKey !== undefined)
|
||||
searchKey = getString(remoteEphemeralKey);
|
||||
|
||||
var preferredSession = sessions[searchKey];
|
||||
if (preferredSession !== undefined)
|
||||
return preferredSession;
|
||||
|
||||
var openSession = undefined;
|
||||
for (key in sessions) {
|
||||
if (key == "identityKey")
|
||||
continue;
|
||||
|
||||
if (sessions[key].indexInfo.closed == -1) {
|
||||
if (openSession !== undefined)
|
||||
throw new Error("Datastore inconsistensy: multiple open sessions for " + encodedNumber);
|
||||
openSession = sessions[key];
|
||||
}
|
||||
if (sessions[key][searchKey] !== undefined)
|
||||
return sessions[key];
|
||||
}
|
||||
if (openSession !== undefined)
|
||||
return openSession;
|
||||
|
||||
if (sessions.identityKey !== undefined && searchKey != "NOTAKEY")
|
||||
return { indexInfo: { remoteIdentityKey: sessions.identityKey } };
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/*****************************
|
||||
*** Internal Crypto stuff ***
|
||||
|
@ -212,6 +272,7 @@ window.crypto = (function() {
|
|||
|
||||
return HKDF(sharedSecret.buffer, '', "WhisperText").then(function(masterKey) {
|
||||
var session = {currentRatchet: { rootKey: masterKey[0], lastRemoteEphemeralKey: theirEphemeralPubKey },
|
||||
indexInfo: { remoteIdentityKey: theirIdentityPubKey, closed: -1 },
|
||||
oldRatchetList: []
|
||||
};
|
||||
|
||||
|
@ -221,12 +282,12 @@ window.crypto = (function() {
|
|||
return createNewKeyPair(false).then(function(ourSendingEphemeralKey) {
|
||||
session.currentRatchet.ephemeralKeyPair = ourSendingEphemeralKey;
|
||||
return calculateRatchet(session, theirEphemeralPubKey, true).then(function() {
|
||||
crypto_storage.saveSession(encodedNumber, session);
|
||||
return session;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
session.currentRatchet.ephemeralKeyPair = ourEphemeralKey;
|
||||
crypto_storage.saveSession(encodedNumber, session);
|
||||
return session;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -245,17 +306,53 @@ window.crypto = (function() {
|
|||
});
|
||||
}
|
||||
|
||||
var initSessionFromPreKeyWhisperMessage = function(encodedNumber, message) {
|
||||
//TODO: Check remote identity key matches known-good key
|
||||
var closeSession = function(session) {
|
||||
// Clear any data which would allow session continuation:
|
||||
// Lock down current receive ratchet
|
||||
// TODO: Some kind of delete chainKey['key']
|
||||
// Delete current sending ratchet
|
||||
delete session[getString(ratchet.ephemeralKeyPair.pubKey)];
|
||||
// Delete current root key and our ephemeral key pair
|
||||
delete session.currentRatchet['rootKey'];
|
||||
delete session.currentRatchet['ephemeralKeyPair'];
|
||||
session.indexInfo.closed = new Date().getTime();
|
||||
}
|
||||
|
||||
var initSessionFromPreKeyWhisperMessage = function(encodedNumber, message) {
|
||||
var preKeyPair = crypto_storage.getAndRemovePreKeyPair(message.preKeyId);
|
||||
var session = crypto_storage.getSession(encodedNumber, toArrayBuffer(message.baseKey));
|
||||
if (preKeyPair === undefined) {
|
||||
if (crypto_storage.getSession(encodedNumber) !== undefined)
|
||||
return Promise.resolve();
|
||||
// Session may or may not be the correct one, but if its not, we can't do anything about it
|
||||
// ...fall through and let decryptWhisperMessage handle that case
|
||||
if (session !== undefined && session.currentRatchet !== undefined)
|
||||
return Promise.resolve(session);
|
||||
else
|
||||
throw new Error("Missing preKey for PreKeyWhisperMessage");
|
||||
} else
|
||||
return initSession(false, preKeyPair, encodedNumber, toArrayBuffer(message.identityKey), toArrayBuffer(message.baseKey));
|
||||
}
|
||||
if (session !== undefined) {
|
||||
// We already had a session:
|
||||
if (getString(session.indexInfo.remoteIdentityKey) == getString(message.identityKey)) {
|
||||
// If the identity key matches the previous one, close the previous one and use the new one
|
||||
if (session.currentRatchet !== undefined) { // if its a real session
|
||||
closeSession(session);
|
||||
crypto_storage.saveSession(encodedNumber, session);
|
||||
}
|
||||
} else {
|
||||
// ...otherwise create an error that the UI will pick up and ask the user if they want to re-negotiate
|
||||
// TODO: Save the message for possible later renegotiation
|
||||
var error = new Error("Received message with unknown identity key");
|
||||
error.name = "WarnTryAgainError";
|
||||
error.full_message = "The identity of the sender has changed. This may be malicious, or the sender may have simply reinstalled TextSecure.";
|
||||
throw new error;
|
||||
}
|
||||
}
|
||||
return initSession(false, preKeyPair, encodedNumber, toArrayBuffer(message.identityKey), toArrayBuffer(message.baseKey))
|
||||
.then(function(new_session) {
|
||||
// Note that the session is not actually saved until the very end of decryptWhisperMessage
|
||||
// ... to ensure that the sender actually holds the private keys for all reported pubkeys
|
||||
new_session.indexInfo.baseKey = message.baseKey;
|
||||
return new_session;
|
||||
});;
|
||||
}
|
||||
|
||||
var fillMessageKeys = function(chain, counter) {
|
||||
|
@ -292,7 +389,7 @@ window.crypto = (function() {
|
|||
var entry = session.oldRatchetList[i];
|
||||
var ratchet = getString(entry.ephemeralKey);
|
||||
console.log("Checking old chain with added time " + (entry.added/1000));
|
||||
if (!objectContainsKeys(session[ratchet].messageKeys) || entry.added < new Date().getTime() - 1000*60*60*24*7) {
|
||||
if (!objectContainsKeys(session[ratchet].messageKeys) || entry.added < new Date().getTime() - MESSAGE_LOST_THRESHOLD_MS) {
|
||||
delete session[ratchet];
|
||||
console.log("...deleted");
|
||||
} else
|
||||
|
@ -342,11 +439,7 @@ window.crypto = (function() {
|
|||
}
|
||||
|
||||
// returns decrypted protobuf
|
||||
var decryptWhisperMessage = function(encodedNumber, messageBytes) {
|
||||
var session = crypto_storage.getSession(encodedNumber);
|
||||
if (session === undefined)
|
||||
throw new Error("No session currently open with " + encodedNumber);
|
||||
|
||||
var decryptWhisperMessage = function(encodedNumber, messageBytes, session) {
|
||||
if (messageBytes[0] != String.fromCharCode((2 << 4) | 2))
|
||||
throw new Error("Bad version number on WhisperMessage");
|
||||
|
||||
|
@ -354,8 +447,15 @@ window.crypto = (function() {
|
|||
var mac = messageBytes.substring(messageBytes.length - 8, messageBytes.length);
|
||||
|
||||
var message = decodeWhisperMessageProtobuf(messageProto);
|
||||
var remoteEphemeralKey = toArrayBuffer(message.ephemeralKey);
|
||||
|
||||
return maybeStepRatchet(session, toArrayBuffer(message.ephemeralKey), message.previousCounter).then(function() {
|
||||
if (session === undefined) {
|
||||
var session = crypto_storage.getSession(encodedNumber, remoteEphemeralKey);
|
||||
if (session === undefined)
|
||||
throw new Error("No session found to decrypt message from " + encodedNumber);
|
||||
}
|
||||
|
||||
return maybeStepRatchet(session, remoteEphemeralKey, message.previousCounter).then(function() {
|
||||
var chain = session[getString(message.ephemeralKey)];
|
||||
|
||||
return fillMessageKeys(chain, message.counter).then(function() {
|
||||
|
@ -370,8 +470,13 @@ window.crypto = (function() {
|
|||
removeOldChains(session);
|
||||
delete session['pendingPreKey'];
|
||||
|
||||
var finalMessage = decodePushMessageContentProtobuf(getString(plaintext));
|
||||
|
||||
if ((finalMessage.flags & 1) == 1) // END_SESSION
|
||||
closeSession(session);
|
||||
|
||||
crypto_storage.saveSession(encodedNumber, session);
|
||||
return decodePushMessageContentProtobuf(getString(plaintext));
|
||||
return finalMessage;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -414,8 +519,8 @@ window.crypto = (function() {
|
|||
if (proto.message.readUint8() != (2 << 4 | 2))
|
||||
throw new Error("Bad version byte");
|
||||
var preKeyProto = decodePreKeyWhisperMessageProtobuf(getString(proto.message));
|
||||
return initSessionFromPreKeyWhisperMessage(proto.source, preKeyProto).then(function() {
|
||||
return decryptWhisperMessage(proto.source, getString(preKeyProto.message)).then(function(result) {
|
||||
return initSessionFromPreKeyWhisperMessage(proto.source, preKeyProto).then(function(session) {
|
||||
return decryptWhisperMessage(proto.source, getString(preKeyProto.message), session).then(function(result) {
|
||||
return {message: result, pushMessage: proto};
|
||||
});
|
||||
});
|
||||
|
@ -467,9 +572,8 @@ window.crypto = (function() {
|
|||
preKeyMsg.baseKey = toArrayBuffer(baseKey.pubKey);
|
||||
return initSession(true, baseKey, deviceObject.encodedNumber,
|
||||
toArrayBuffer(deviceObject.identityKey), toArrayBuffer(deviceObject.publicKey))
|
||||
.then(function() {
|
||||
//TODO: Delete preKey info on first message received back
|
||||
session = crypto_storage.getSession(deviceObject.encodedNumber);
|
||||
.then(function(new_session) {
|
||||
session = new_session;
|
||||
session.pendingPreKey = baseKey.pubKey;
|
||||
return doEncryptPushMessageContent().then(function(message) {
|
||||
preKeyMsg.message = message;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue