Update libaxolotl

// FREEBIE
This commit is contained in:
lilia 2016-04-03 14:25:30 -07:00
parent 7e2c6fd6bc
commit af392c077d
2 changed files with 284 additions and 304 deletions

View file

@ -34277,15 +34277,6 @@ window.axolotl.protocol = function(storage_interface) {
// (also the time between signedPreKey regenerations) // (also the time between signedPreKey regenerations)
var MESSAGE_LOST_THRESHOLD_MS = 1000*60*60*24*7; var MESSAGE_LOST_THRESHOLD_MS = 1000*60*60*24*7;
function objectContainsKeys(object) {
var count = 0;
for (var key in object) {
count++;
break;
}
return count != 0;
}
function toString(thing) { function toString(thing) {
if (typeof thing == 'string') { if (typeof thing == 'string') {
return thing; return thing;
@ -34549,100 +34540,103 @@ window.axolotl.protocol = function(storage_interface) {
var initSession = function(isInitiator, ourEphemeralKey, ourSignedKey, encodedNumber, theirIdentityPubKey, theirEphemeralPubKey, theirSignedPubKey) { var initSession = function(isInitiator, ourEphemeralKey, ourSignedKey, encodedNumber, theirIdentityPubKey, theirEphemeralPubKey, theirSignedPubKey) {
return storage_interface.getMyIdentityKey().then(function(ourIdentityKey) { return storage_interface.getMyIdentityKey().then(function(ourIdentityKey) {
if (isInitiator) { if (isInitiator) {
if (ourSignedKey !== undefined) if (ourSignedKey !== undefined) {
throw new Error("Invalid call to initSession"); throw new Error("Invalid call to initSession");
}
ourSignedKey = ourEphemeralKey; ourSignedKey = ourEphemeralKey;
} else { } else {
if (theirSignedPubKey !== undefined) if (theirSignedPubKey !== undefined) {
throw new Error("Invalid call to initSession"); throw new Error("Invalid call to initSession");
}
theirSignedPubKey = theirEphemeralPubKey; theirSignedPubKey = theirEphemeralPubKey;
} }
var sharedSecret; var sharedSecret;
if (ourEphemeralKey === undefined || theirEphemeralPubKey === undefined) if (ourEphemeralKey === undefined || theirEphemeralPubKey === undefined) {
sharedSecret = new Uint8Array(32 * 4); sharedSecret = new Uint8Array(32 * 4);
else } else {
sharedSecret = new Uint8Array(32 * 5); sharedSecret = new Uint8Array(32 * 5);
}
for (var i = 0; i < 32; i++) for (var i = 0; i < 32; i++) {
sharedSecret[i] = 0xff; sharedSecret[i] = 0xff;
}
return axolotlInternal.crypto.ECDHE(theirSignedPubKey, ourIdentityKey.privKey).then(function(ecRes1) { return Promise.all([
function finishInit() { axolotlInternal.crypto.ECDHE(theirSignedPubKey, ourIdentityKey.privKey),
return axolotlInternal.crypto.ECDHE(theirSignedPubKey, ourSignedKey.privKey).then(function(ecRes) { axolotlInternal.crypto.ECDHE(theirIdentityPubKey, ourSignedKey.privKey),
sharedSecret.set(new Uint8Array(ecRes), 32 * 3); axolotlInternal.crypto.ECDHE(theirSignedPubKey, ourSignedKey.privKey)
]).then(function(ecRes) {
if (isInitiator) {
sharedSecret.set(new Uint8Array(ecRes[0]), 32);
sharedSecret.set(new Uint8Array(ecRes[1]), 32 * 2);
} else {
sharedSecret.set(new Uint8Array(ecRes[0]), 32 * 2);
sharedSecret.set(new Uint8Array(ecRes[1]), 32)
}
sharedSecret.set(new Uint8Array(ecRes[2]), 32 * 3);
return HKDF(sharedSecret.buffer, '', "WhisperText").then(function(masterKey) { if (ourEphemeralKey !== undefined && theirEphemeralPubKey !== undefined) {
var session = {currentRatchet: { rootKey: masterKey[0], lastRemoteEphemeralKey: theirSignedPubKey, previousCounter: 0 }, return axolotlInternal.crypto.ECDHE(
indexInfo: { remoteIdentityKey: theirIdentityPubKey, closed: -1 }, theirEphemeralPubKey, ourEphemeralKey.privKey
oldRatchetList: [] ).then(function(ecRes4) {
}; sharedSecret.set(new Uint8Array(ecRes4), 32 * 4);
if (!isInitiator)
session.indexInfo.baseKey = theirEphemeralPubKey;
else
session.indexInfo.baseKey = ourEphemeralKey.pubKey;
// If we're initiating we go ahead and set our first sending ephemeral key now,
// otherwise we figure it out when we first maybeStepRatchet with the remote's ephemeral key
if (isInitiator) {
return axolotlInternal.crypto.createKeyPair().then(function(ourSendingEphemeralKey) {
session.currentRatchet.ephemeralKeyPair = ourSendingEphemeralKey;
return calculateRatchet(session, theirSignedPubKey, true).then(function() {
return session;
});
});
} else {
session.currentRatchet.ephemeralKeyPair = ourSignedKey;
return session;
}
});
}); });
} }
}).then(function() {
return HKDF(sharedSecret.buffer, '', "WhisperText");
}).then(function(masterKey) {
var session = {
currentRatchet: {
rootKey : masterKey[0],
lastRemoteEphemeralKey : theirSignedPubKey,
previousCounter : 0
},
indexInfo: {
remoteIdentityKey : theirIdentityPubKey,
closed : -1
},
oldRatchetList: []
};
var promise; // If we're initiating we go ahead and set our first sending ephemeral key now,
if (ourEphemeralKey === undefined || theirEphemeralPubKey === undefined) // otherwise we figure it out when we first maybeStepRatchet with the remote's ephemeral key
promise = Promise.resolve(new ArrayBuffer(0)); if (isInitiator) {
else session.indexInfo.baseKey = ourEphemeralKey.pubKey;
promise = axolotlInternal.crypto.ECDHE(theirEphemeralPubKey, ourEphemeralKey.privKey); return axolotlInternal.crypto.createKeyPair().then(function(ourSendingEphemeralKey) {
return promise.then(function(ecRes4) { session.currentRatchet.ephemeralKeyPair = ourSendingEphemeralKey;
sharedSecret.set(new Uint8Array(ecRes4), 32 * 4); return calculateRatchet(session, theirSignedPubKey, true).then(function() {
return session;
if (isInitiator) });
return axolotlInternal.crypto.ECDHE(theirIdentityPubKey, ourSignedKey.privKey).then(function(ecRes2) { });
sharedSecret.set(new Uint8Array(ecRes1), 32); } else {
sharedSecret.set(new Uint8Array(ecRes2), 32 * 2); session.indexInfo.baseKey = theirEphemeralPubKey;
}).then(finishInit); session.currentRatchet.ephemeralKeyPair = ourSignedKey;
else return session;
return axolotlInternal.crypto.ECDHE(theirIdentityPubKey, ourSignedKey.privKey).then(function(ecRes2) { }
sharedSecret.set(new Uint8Array(ecRes1), 32 * 2);
sharedSecret.set(new Uint8Array(ecRes2), 32)
}).then(finishInit);
});
}); });
}); });
} }
var removeOldChains = function(session) { var removeOldChains = function(session) {
// Sending ratchets are always removed when we step because we never need them again // Sending ratchets are always removed when we step because we never need them again
// Receiving ratchets are either removed if we step with all keys used up to previousCounter // Receiving ratchets are added to the oldRatchetList, which we parse
// and are otherwise added to the oldRatchetList, which we parse here and remove ratchets // here and remove all but the last four.
// older than a week (we assume the message was lost and move on with our lives at that point) while (session.oldRatchetList.length > 4) {
var newList = []; var index = 0;
for (var i = 0; i < session.oldRatchetList.length; i++) { var oldest = session.oldRatchetList[0];
var entry = session.oldRatchetList[i]; for (var i = 0; i < session.oldRatchetList.length; i++) {
var ratchet = toString(entry.ephemeralKey); if (session.oldRatchetList[i].added < oldest.added) {
console.log("Checking old chain with added time " + (entry.added/1000)); oldest = session.oldRatchetList[i];
if ((!objectContainsKeys(session[ratchet].messageKeys) && (session[ratchet].chainKey === undefined || session[ratchet].chainKey.key === undefined)) index = i;
|| entry.added < Date.now() - MESSAGE_LOST_THRESHOLD_MS) { }
delete session[ratchet]; }
console.log("...deleted"); delete session[toString(oldest.ephemeralKey)];
} else session.oldRatchetList.splice(index, 1);
newList[newList.length] = entry;
} }
session.oldRatchetList = newList;
} }
var closeSession = function(session, sessionClosedByRemote) { var closeSession = function(session) {
if (session.indexInfo.closed > -1) if (session.indexInfo.closed > -1)
return; return;
@ -34655,10 +34649,9 @@ window.axolotl.protocol = function(storage_interface) {
// Move all receive ratchets to the oldRatchetList to mark them for deletion // Move all receive ratchets to the oldRatchetList to mark them for deletion
for (var i in session) { for (var i in session) {
if (session[i].chainKey !== undefined && session[i].chainKey.key !== undefined) { if (session[i].chainKey !== undefined && session[i].chainKey.key !== undefined) {
if (!sessionClosedByRemote) session.oldRatchetList[session.oldRatchetList.length] = {
session.oldRatchetList[session.oldRatchetList.length] = { added: Date.now(), ephemeralKey: i }; added: Date.now(), ephemeralKey: i
else };
delete session[i].chainKey.key;
} }
} }
// Delete current root key and our ephemeral key pair to disallow ratchet stepping // Delete current root key and our ephemeral key pair to disallow ratchet stepping
@ -34709,10 +34702,14 @@ window.axolotl.protocol = function(storage_interface) {
} }
} }
if (message.preKeyId && !preKeyPair) { if (message.preKeyId && !preKeyPair) {
console.log('Invalid prekey id'); console.log('Invalid prekey id');
} }
return initSession(false, preKeyPair, signedPreKeyPair, encodedNumber, toArrayBuffer(message.identityKey), toArrayBuffer(message.baseKey), undefined) return initSession(false, preKeyPair, signedPreKeyPair,
.then(function(new_session) { encodedNumber,
message.identityKey.toArrayBuffer(),
message.baseKey.toArrayBuffer(),
undefined
).then(function(new_session) {
// Note that the session is not actually saved until the very end of decryptWhisperMessage // 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 // ... to ensure that the sender actually holds the private keys for all reported pubkeys
return [new_session, function() { return [new_session, function() {
@ -34752,7 +34749,7 @@ window.axolotl.protocol = function(storage_interface) {
return fillMessageKeys(chain, counter); return fillMessageKeys(chain, counter);
}); });
}); });
} };
var maybeStepRatchet = function(session, remoteKey, previousCounter) { var maybeStepRatchet = function(session, remoteKey, previousCounter) {
if (session[toString(remoteKey)] !== undefined) if (session[toString(remoteKey)] !== undefined)
@ -34760,7 +34757,18 @@ window.axolotl.protocol = function(storage_interface) {
var ratchet = session.currentRatchet; var ratchet = session.currentRatchet;
var finish = function() { return Promise.resolve().then(function() {
var previousRatchet = session[toString(ratchet.lastRemoteEphemeralKey)];
if (previousRatchet !== undefined) {
return fillMessageKeys(previousRatchet, previousCounter).then(function() {
delete previousRatchet.chainKey.key;
session.oldRatchetList[session.oldRatchetList.length] = {
added : Date.now(),
ephemeralKey : ratchet.lastRemoteEphemeralKey
};
});
}
}).then(function() {
return calculateRatchet(session, remoteKey, false).then(function() { return calculateRatchet(session, remoteKey, false).then(function() {
// Now swap the ephemeral key and calculate the new sending chain // Now swap the ephemeral key and calculate the new sending chain
var previousRatchet = toString(ratchet.ephemeralKeyPair.pubKey); var previousRatchet = toString(ratchet.ephemeralKeyPair.pubKey);
@ -34776,20 +34784,8 @@ window.axolotl.protocol = function(storage_interface) {
}); });
}); });
}); });
} });
};
var previousRatchet = session[toString(ratchet.lastRemoteEphemeralKey)];
if (previousRatchet !== undefined) {
return fillMessageKeys(previousRatchet, previousCounter).then(function() {
delete previousRatchet.chainKey.key;
if (!objectContainsKeys(previousRatchet.messageKeys))
delete session[toString(ratchet.lastRemoteEphemeralKey)];
else
session.oldRatchetList[session.oldRatchetList.length] = { added: Date.now(), ephemeralKey: ratchet.lastRemoteEphemeralKey };
}).then(finish);
} else
return finish();
}
var doDecryptWhisperMessage = function(encodedNumber, messageBytes, session, registrationId) { var doDecryptWhisperMessage = function(encodedNumber, messageBytes, session, registrationId) {
if (!messageBytes instanceof ArrayBuffer) { if (!messageBytes instanceof ArrayBuffer) {
@ -34805,18 +34801,19 @@ window.axolotl.protocol = function(storage_interface) {
var message = axolotlInternal.protobuf.WhisperMessage.decode(messageProto); var message = axolotlInternal.protobuf.WhisperMessage.decode(messageProto);
var remoteEphemeralKey = message.ephemeralKey.toArrayBuffer(); var remoteEphemeralKey = message.ephemeralKey.toArrayBuffer();
var promise; return Promise.resolve().then(function() {
if (session === undefined) { if (session === undefined) {
promise = crypto_storage.getSessionByRemoteEphemeralKey(encodedNumber, remoteEphemeralKey).then(function(session) { return crypto_storage.getSessionByRemoteEphemeralKey(encodedNumber, remoteEphemeralKey);
if (session === undefined) } else {
throw new Error("No session found to decrypt message from " + encodedNumber);
return session; return session;
}); }
} else { }).then(function(session) {
promise = Promise.resolve(session); if (session === undefined) {
} throw new Error("No session found to decrypt message from " + encodedNumber);
}
return promise.then(function(session) { if (session.indexInfo.closed != -1) {
console.log('decrypting message for closed session');
}
return maybeStepRatchet(session, remoteEphemeralKey, message.previousCounter).then(function() { return maybeStepRatchet(session, remoteEphemeralKey, message.previousCounter).then(function() {
var chain = session[toString(message.ephemeralKey)]; var chain = session[toString(message.ephemeralKey)];
@ -34827,47 +34824,40 @@ window.axolotl.protocol = function(storage_interface) {
e.name = 'MessageCounterError'; e.name = 'MessageCounterError';
throw e; throw e;
} }
return HKDF(toArrayBuffer(messageKey), '', "WhisperMessageKeys").then(function(keys) { delete chain.messageKeys[message.counter];
return storage_interface.getMyIdentityKey().then(function(ourIdentityKey) { return HKDF(toArrayBuffer(messageKey), '', "WhisperMessageKeys");
delete chain.messageKeys[message.counter]; });
}).then(function(keys) {
return storage_interface.getMyIdentityKey().then(function(ourIdentityKey) {
var macInput = new Uint8Array(messageProto.byteLength + 33*2 + 1); var macInput = new Uint8Array(messageProto.byteLength + 33*2 + 1);
macInput.set(new Uint8Array(toArrayBuffer(session.indexInfo.remoteIdentityKey))); macInput.set(new Uint8Array(toArrayBuffer(session.indexInfo.remoteIdentityKey)));
macInput.set(new Uint8Array(toArrayBuffer(ourIdentityKey.pubKey)), 33); macInput.set(new Uint8Array(toArrayBuffer(ourIdentityKey.pubKey)), 33);
macInput[33*2] = (3 << 4) | 3; macInput[33*2] = (3 << 4) | 3;
macInput.set(new Uint8Array(messageProto), 33*2 + 1); macInput.set(new Uint8Array(messageProto), 33*2 + 1);
return verifyMAC(macInput.buffer, keys[1], mac, 8).then(function() { return verifyMAC(macInput.buffer, keys[1], mac, 8);
return axolotlInternal.crypto.decrypt( }).then(function() {
keys[0], return axolotlInternal.crypto.decrypt(keys[0], message.ciphertext.toArrayBuffer(), keys[2].slice(0, 16));
message.ciphertext.toArrayBuffer(), });
keys[2].slice(0, 16) }).then(function(paddedPlaintext) {
).then(function(paddedPlaintext) { paddedPlaintext = new Uint8Array(paddedPlaintext);
paddedPlaintext = new Uint8Array(paddedPlaintext); var plaintext;
var plaintext; for (var i = paddedPlaintext.length - 1; i >= 0; i--) {
for (var i = paddedPlaintext.length - 1; i >= 0; i--) { if (paddedPlaintext[i] == 0x80) {
if (paddedPlaintext[i] == 0x80) { plaintext = new Uint8Array(i);
plaintext = new Uint8Array(i); plaintext.set(paddedPlaintext.subarray(0, i));
plaintext.set(paddedPlaintext.subarray(0, i)); plaintext = plaintext.buffer;
plaintext = plaintext.buffer; break;
break; } else if (paddedPlaintext[i] != 0x00) {
} else if (paddedPlaintext[i] != 0x00) throw new Error('Invalid padding');
throw new Error('Invalid padding'); }
} }
delete session['pendingPreKey']; delete session['pendingPreKey'];
removeOldChains(session); removeOldChains(session);
return crypto_storage.saveSession(encodedNumber, session, registrationId).then(function() { return crypto_storage.saveSession(encodedNumber, session, registrationId).then(function() {
return [plaintext, function() { return [plaintext];
closeSession(session, true);
removeOldChains(session);
return crypto_storage.saveSession(encodedNumber, session);
}];
});
});
});
});
});
}); });
}); });
}); });
@ -34979,7 +34969,8 @@ window.axolotl.protocol = function(storage_interface) {
return initSession(true, baseKey, undefined, return initSession(true, baseKey, undefined,
deviceObject.encodedNumber, deviceIdentityKey, deviceObject.encodedNumber, deviceIdentityKey,
toArrayBuffer(deviceObject.preKey), toArrayBuffer(deviceObject.preKey),
deviceSignedKey).then(function(new_session) { deviceSignedKey
).then(function(new_session) {
session = new_session; session = new_session;
session.pendingPreKey = { preKeyId: deviceObject.preKeyId, signedKeyId: deviceObject.signedKeyId, baseKey: baseKey.pubKey }; session.pendingPreKey = { preKeyId: deviceObject.preKeyId, signedKeyId: deviceObject.signedKeyId, baseKey: baseKey.pubKey };
return doEncryptPushMessageContent().then(function(message) { return doEncryptPushMessageContent().then(function(message) {
@ -37051,20 +37042,18 @@ MessageReceiver.prototype.extend({
handleLegacyMessage: function (envelope) { handleLegacyMessage: function (envelope) {
return this.decrypt(envelope, envelope.legacyMessage).then(function(result) { return this.decrypt(envelope, envelope.legacyMessage).then(function(result) {
var plaintext = result[0]; // array buffer var plaintext = result[0]; // array buffer
var close_session = result[1]; // function
var message = textsecure.protobuf.DataMessage.decode(plaintext); var message = textsecure.protobuf.DataMessage.decode(plaintext);
return this.handleDataMessage(envelope, message, close_session); return this.handleDataMessage(envelope, message);
}.bind(this)); }.bind(this));
}, },
handleContentMessage: function (envelope) { handleContentMessage: function (envelope) {
return this.decrypt(envelope, envelope.content).then(function(result) { return this.decrypt(envelope, envelope.content).then(function(result) {
var plaintext = result[0]; // array buffer var plaintext = result[0]; // array buffer
var close_session = result[1]; // function
var content = textsecure.protobuf.Content.decode(plaintext); var content = textsecure.protobuf.Content.decode(plaintext);
if (content.syncMessage) { if (content.syncMessage) {
return this.handleSyncMessage(envelope, content.syncMessage); return this.handleSyncMessage(envelope, content.syncMessage);
} else if (content.dataMessage) { } else if (content.dataMessage) {
return this.handleDataMessage(envelope, content.dataMessage, close_session); return this.handleDataMessage(envelope, content.dataMessage);
} else { } else {
throw new Error('Got Content message with no dataMessage and no syncMessage'); throw new Error('Got Content message with no dataMessage and no syncMessage');
} }

View file

@ -34163,15 +34163,6 @@ window.axolotl.protocol = function(storage_interface) {
// (also the time between signedPreKey regenerations) // (also the time between signedPreKey regenerations)
var MESSAGE_LOST_THRESHOLD_MS = 1000*60*60*24*7; var MESSAGE_LOST_THRESHOLD_MS = 1000*60*60*24*7;
function objectContainsKeys(object) {
var count = 0;
for (var key in object) {
count++;
break;
}
return count != 0;
}
function toString(thing) { function toString(thing) {
if (typeof thing == 'string') { if (typeof thing == 'string') {
return thing; return thing;
@ -34435,100 +34426,103 @@ window.axolotl.protocol = function(storage_interface) {
var initSession = function(isInitiator, ourEphemeralKey, ourSignedKey, encodedNumber, theirIdentityPubKey, theirEphemeralPubKey, theirSignedPubKey) { var initSession = function(isInitiator, ourEphemeralKey, ourSignedKey, encodedNumber, theirIdentityPubKey, theirEphemeralPubKey, theirSignedPubKey) {
return storage_interface.getMyIdentityKey().then(function(ourIdentityKey) { return storage_interface.getMyIdentityKey().then(function(ourIdentityKey) {
if (isInitiator) { if (isInitiator) {
if (ourSignedKey !== undefined) if (ourSignedKey !== undefined) {
throw new Error("Invalid call to initSession"); throw new Error("Invalid call to initSession");
}
ourSignedKey = ourEphemeralKey; ourSignedKey = ourEphemeralKey;
} else { } else {
if (theirSignedPubKey !== undefined) if (theirSignedPubKey !== undefined) {
throw new Error("Invalid call to initSession"); throw new Error("Invalid call to initSession");
}
theirSignedPubKey = theirEphemeralPubKey; theirSignedPubKey = theirEphemeralPubKey;
} }
var sharedSecret; var sharedSecret;
if (ourEphemeralKey === undefined || theirEphemeralPubKey === undefined) if (ourEphemeralKey === undefined || theirEphemeralPubKey === undefined) {
sharedSecret = new Uint8Array(32 * 4); sharedSecret = new Uint8Array(32 * 4);
else } else {
sharedSecret = new Uint8Array(32 * 5); sharedSecret = new Uint8Array(32 * 5);
}
for (var i = 0; i < 32; i++) for (var i = 0; i < 32; i++) {
sharedSecret[i] = 0xff; sharedSecret[i] = 0xff;
}
return axolotlInternal.crypto.ECDHE(theirSignedPubKey, ourIdentityKey.privKey).then(function(ecRes1) { return Promise.all([
function finishInit() { axolotlInternal.crypto.ECDHE(theirSignedPubKey, ourIdentityKey.privKey),
return axolotlInternal.crypto.ECDHE(theirSignedPubKey, ourSignedKey.privKey).then(function(ecRes) { axolotlInternal.crypto.ECDHE(theirIdentityPubKey, ourSignedKey.privKey),
sharedSecret.set(new Uint8Array(ecRes), 32 * 3); axolotlInternal.crypto.ECDHE(theirSignedPubKey, ourSignedKey.privKey)
]).then(function(ecRes) {
if (isInitiator) {
sharedSecret.set(new Uint8Array(ecRes[0]), 32);
sharedSecret.set(new Uint8Array(ecRes[1]), 32 * 2);
} else {
sharedSecret.set(new Uint8Array(ecRes[0]), 32 * 2);
sharedSecret.set(new Uint8Array(ecRes[1]), 32)
}
sharedSecret.set(new Uint8Array(ecRes[2]), 32 * 3);
return HKDF(sharedSecret.buffer, '', "WhisperText").then(function(masterKey) { if (ourEphemeralKey !== undefined && theirEphemeralPubKey !== undefined) {
var session = {currentRatchet: { rootKey: masterKey[0], lastRemoteEphemeralKey: theirSignedPubKey, previousCounter: 0 }, return axolotlInternal.crypto.ECDHE(
indexInfo: { remoteIdentityKey: theirIdentityPubKey, closed: -1 }, theirEphemeralPubKey, ourEphemeralKey.privKey
oldRatchetList: [] ).then(function(ecRes4) {
}; sharedSecret.set(new Uint8Array(ecRes4), 32 * 4);
if (!isInitiator)
session.indexInfo.baseKey = theirEphemeralPubKey;
else
session.indexInfo.baseKey = ourEphemeralKey.pubKey;
// If we're initiating we go ahead and set our first sending ephemeral key now,
// otherwise we figure it out when we first maybeStepRatchet with the remote's ephemeral key
if (isInitiator) {
return axolotlInternal.crypto.createKeyPair().then(function(ourSendingEphemeralKey) {
session.currentRatchet.ephemeralKeyPair = ourSendingEphemeralKey;
return calculateRatchet(session, theirSignedPubKey, true).then(function() {
return session;
});
});
} else {
session.currentRatchet.ephemeralKeyPair = ourSignedKey;
return session;
}
});
}); });
} }
}).then(function() {
return HKDF(sharedSecret.buffer, '', "WhisperText");
}).then(function(masterKey) {
var session = {
currentRatchet: {
rootKey : masterKey[0],
lastRemoteEphemeralKey : theirSignedPubKey,
previousCounter : 0
},
indexInfo: {
remoteIdentityKey : theirIdentityPubKey,
closed : -1
},
oldRatchetList: []
};
var promise; // If we're initiating we go ahead and set our first sending ephemeral key now,
if (ourEphemeralKey === undefined || theirEphemeralPubKey === undefined) // otherwise we figure it out when we first maybeStepRatchet with the remote's ephemeral key
promise = Promise.resolve(new ArrayBuffer(0)); if (isInitiator) {
else session.indexInfo.baseKey = ourEphemeralKey.pubKey;
promise = axolotlInternal.crypto.ECDHE(theirEphemeralPubKey, ourEphemeralKey.privKey); return axolotlInternal.crypto.createKeyPair().then(function(ourSendingEphemeralKey) {
return promise.then(function(ecRes4) { session.currentRatchet.ephemeralKeyPair = ourSendingEphemeralKey;
sharedSecret.set(new Uint8Array(ecRes4), 32 * 4); return calculateRatchet(session, theirSignedPubKey, true).then(function() {
return session;
if (isInitiator) });
return axolotlInternal.crypto.ECDHE(theirIdentityPubKey, ourSignedKey.privKey).then(function(ecRes2) { });
sharedSecret.set(new Uint8Array(ecRes1), 32); } else {
sharedSecret.set(new Uint8Array(ecRes2), 32 * 2); session.indexInfo.baseKey = theirEphemeralPubKey;
}).then(finishInit); session.currentRatchet.ephemeralKeyPair = ourSignedKey;
else return session;
return axolotlInternal.crypto.ECDHE(theirIdentityPubKey, ourSignedKey.privKey).then(function(ecRes2) { }
sharedSecret.set(new Uint8Array(ecRes1), 32 * 2);
sharedSecret.set(new Uint8Array(ecRes2), 32)
}).then(finishInit);
});
}); });
}); });
} }
var removeOldChains = function(session) { var removeOldChains = function(session) {
// Sending ratchets are always removed when we step because we never need them again // Sending ratchets are always removed when we step because we never need them again
// Receiving ratchets are either removed if we step with all keys used up to previousCounter // Receiving ratchets are added to the oldRatchetList, which we parse
// and are otherwise added to the oldRatchetList, which we parse here and remove ratchets // here and remove all but the last four.
// older than a week (we assume the message was lost and move on with our lives at that point) while (session.oldRatchetList.length > 4) {
var newList = []; var index = 0;
for (var i = 0; i < session.oldRatchetList.length; i++) { var oldest = session.oldRatchetList[0];
var entry = session.oldRatchetList[i]; for (var i = 0; i < session.oldRatchetList.length; i++) {
var ratchet = toString(entry.ephemeralKey); if (session.oldRatchetList[i].added < oldest.added) {
console.log("Checking old chain with added time " + (entry.added/1000)); oldest = session.oldRatchetList[i];
if ((!objectContainsKeys(session[ratchet].messageKeys) && (session[ratchet].chainKey === undefined || session[ratchet].chainKey.key === undefined)) index = i;
|| entry.added < Date.now() - MESSAGE_LOST_THRESHOLD_MS) { }
delete session[ratchet]; }
console.log("...deleted"); delete session[toString(oldest.ephemeralKey)];
} else session.oldRatchetList.splice(index, 1);
newList[newList.length] = entry;
} }
session.oldRatchetList = newList;
} }
var closeSession = function(session, sessionClosedByRemote) { var closeSession = function(session) {
if (session.indexInfo.closed > -1) if (session.indexInfo.closed > -1)
return; return;
@ -34541,10 +34535,9 @@ window.axolotl.protocol = function(storage_interface) {
// Move all receive ratchets to the oldRatchetList to mark them for deletion // Move all receive ratchets to the oldRatchetList to mark them for deletion
for (var i in session) { for (var i in session) {
if (session[i].chainKey !== undefined && session[i].chainKey.key !== undefined) { if (session[i].chainKey !== undefined && session[i].chainKey.key !== undefined) {
if (!sessionClosedByRemote) session.oldRatchetList[session.oldRatchetList.length] = {
session.oldRatchetList[session.oldRatchetList.length] = { added: Date.now(), ephemeralKey: i }; added: Date.now(), ephemeralKey: i
else };
delete session[i].chainKey.key;
} }
} }
// Delete current root key and our ephemeral key pair to disallow ratchet stepping // Delete current root key and our ephemeral key pair to disallow ratchet stepping
@ -34595,10 +34588,14 @@ window.axolotl.protocol = function(storage_interface) {
} }
} }
if (message.preKeyId && !preKeyPair) { if (message.preKeyId && !preKeyPair) {
console.log('Invalid prekey id'); console.log('Invalid prekey id');
} }
return initSession(false, preKeyPair, signedPreKeyPair, encodedNumber, toArrayBuffer(message.identityKey), toArrayBuffer(message.baseKey), undefined) return initSession(false, preKeyPair, signedPreKeyPair,
.then(function(new_session) { encodedNumber,
message.identityKey.toArrayBuffer(),
message.baseKey.toArrayBuffer(),
undefined
).then(function(new_session) {
// Note that the session is not actually saved until the very end of decryptWhisperMessage // 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 // ... to ensure that the sender actually holds the private keys for all reported pubkeys
return [new_session, function() { return [new_session, function() {
@ -34638,7 +34635,7 @@ window.axolotl.protocol = function(storage_interface) {
return fillMessageKeys(chain, counter); return fillMessageKeys(chain, counter);
}); });
}); });
} };
var maybeStepRatchet = function(session, remoteKey, previousCounter) { var maybeStepRatchet = function(session, remoteKey, previousCounter) {
if (session[toString(remoteKey)] !== undefined) if (session[toString(remoteKey)] !== undefined)
@ -34646,7 +34643,18 @@ window.axolotl.protocol = function(storage_interface) {
var ratchet = session.currentRatchet; var ratchet = session.currentRatchet;
var finish = function() { return Promise.resolve().then(function() {
var previousRatchet = session[toString(ratchet.lastRemoteEphemeralKey)];
if (previousRatchet !== undefined) {
return fillMessageKeys(previousRatchet, previousCounter).then(function() {
delete previousRatchet.chainKey.key;
session.oldRatchetList[session.oldRatchetList.length] = {
added : Date.now(),
ephemeralKey : ratchet.lastRemoteEphemeralKey
};
});
}
}).then(function() {
return calculateRatchet(session, remoteKey, false).then(function() { return calculateRatchet(session, remoteKey, false).then(function() {
// Now swap the ephemeral key and calculate the new sending chain // Now swap the ephemeral key and calculate the new sending chain
var previousRatchet = toString(ratchet.ephemeralKeyPair.pubKey); var previousRatchet = toString(ratchet.ephemeralKeyPair.pubKey);
@ -34662,20 +34670,8 @@ window.axolotl.protocol = function(storage_interface) {
}); });
}); });
}); });
} });
};
var previousRatchet = session[toString(ratchet.lastRemoteEphemeralKey)];
if (previousRatchet !== undefined) {
return fillMessageKeys(previousRatchet, previousCounter).then(function() {
delete previousRatchet.chainKey.key;
if (!objectContainsKeys(previousRatchet.messageKeys))
delete session[toString(ratchet.lastRemoteEphemeralKey)];
else
session.oldRatchetList[session.oldRatchetList.length] = { added: Date.now(), ephemeralKey: ratchet.lastRemoteEphemeralKey };
}).then(finish);
} else
return finish();
}
var doDecryptWhisperMessage = function(encodedNumber, messageBytes, session, registrationId) { var doDecryptWhisperMessage = function(encodedNumber, messageBytes, session, registrationId) {
if (!messageBytes instanceof ArrayBuffer) { if (!messageBytes instanceof ArrayBuffer) {
@ -34691,18 +34687,19 @@ window.axolotl.protocol = function(storage_interface) {
var message = axolotlInternal.protobuf.WhisperMessage.decode(messageProto); var message = axolotlInternal.protobuf.WhisperMessage.decode(messageProto);
var remoteEphemeralKey = message.ephemeralKey.toArrayBuffer(); var remoteEphemeralKey = message.ephemeralKey.toArrayBuffer();
var promise; return Promise.resolve().then(function() {
if (session === undefined) { if (session === undefined) {
promise = crypto_storage.getSessionByRemoteEphemeralKey(encodedNumber, remoteEphemeralKey).then(function(session) { return crypto_storage.getSessionByRemoteEphemeralKey(encodedNumber, remoteEphemeralKey);
if (session === undefined) } else {
throw new Error("No session found to decrypt message from " + encodedNumber);
return session; return session;
}); }
} else { }).then(function(session) {
promise = Promise.resolve(session); if (session === undefined) {
} throw new Error("No session found to decrypt message from " + encodedNumber);
}
return promise.then(function(session) { if (session.indexInfo.closed != -1) {
console.log('decrypting message for closed session');
}
return maybeStepRatchet(session, remoteEphemeralKey, message.previousCounter).then(function() { return maybeStepRatchet(session, remoteEphemeralKey, message.previousCounter).then(function() {
var chain = session[toString(message.ephemeralKey)]; var chain = session[toString(message.ephemeralKey)];
@ -34713,47 +34710,40 @@ window.axolotl.protocol = function(storage_interface) {
e.name = 'MessageCounterError'; e.name = 'MessageCounterError';
throw e; throw e;
} }
return HKDF(toArrayBuffer(messageKey), '', "WhisperMessageKeys").then(function(keys) { delete chain.messageKeys[message.counter];
return storage_interface.getMyIdentityKey().then(function(ourIdentityKey) { return HKDF(toArrayBuffer(messageKey), '', "WhisperMessageKeys");
delete chain.messageKeys[message.counter]; });
}).then(function(keys) {
return storage_interface.getMyIdentityKey().then(function(ourIdentityKey) {
var macInput = new Uint8Array(messageProto.byteLength + 33*2 + 1); var macInput = new Uint8Array(messageProto.byteLength + 33*2 + 1);
macInput.set(new Uint8Array(toArrayBuffer(session.indexInfo.remoteIdentityKey))); macInput.set(new Uint8Array(toArrayBuffer(session.indexInfo.remoteIdentityKey)));
macInput.set(new Uint8Array(toArrayBuffer(ourIdentityKey.pubKey)), 33); macInput.set(new Uint8Array(toArrayBuffer(ourIdentityKey.pubKey)), 33);
macInput[33*2] = (3 << 4) | 3; macInput[33*2] = (3 << 4) | 3;
macInput.set(new Uint8Array(messageProto), 33*2 + 1); macInput.set(new Uint8Array(messageProto), 33*2 + 1);
return verifyMAC(macInput.buffer, keys[1], mac, 8).then(function() { return verifyMAC(macInput.buffer, keys[1], mac, 8);
return axolotlInternal.crypto.decrypt( }).then(function() {
keys[0], return axolotlInternal.crypto.decrypt(keys[0], message.ciphertext.toArrayBuffer(), keys[2].slice(0, 16));
message.ciphertext.toArrayBuffer(), });
keys[2].slice(0, 16) }).then(function(paddedPlaintext) {
).then(function(paddedPlaintext) { paddedPlaintext = new Uint8Array(paddedPlaintext);
paddedPlaintext = new Uint8Array(paddedPlaintext); var plaintext;
var plaintext; for (var i = paddedPlaintext.length - 1; i >= 0; i--) {
for (var i = paddedPlaintext.length - 1; i >= 0; i--) { if (paddedPlaintext[i] == 0x80) {
if (paddedPlaintext[i] == 0x80) { plaintext = new Uint8Array(i);
plaintext = new Uint8Array(i); plaintext.set(paddedPlaintext.subarray(0, i));
plaintext.set(paddedPlaintext.subarray(0, i)); plaintext = plaintext.buffer;
plaintext = plaintext.buffer; break;
break; } else if (paddedPlaintext[i] != 0x00) {
} else if (paddedPlaintext[i] != 0x00) throw new Error('Invalid padding');
throw new Error('Invalid padding'); }
} }
delete session['pendingPreKey']; delete session['pendingPreKey'];
removeOldChains(session); removeOldChains(session);
return crypto_storage.saveSession(encodedNumber, session, registrationId).then(function() { return crypto_storage.saveSession(encodedNumber, session, registrationId).then(function() {
return [plaintext, function() { return [plaintext];
closeSession(session, true);
removeOldChains(session);
return crypto_storage.saveSession(encodedNumber, session);
}];
});
});
});
});
});
}); });
}); });
}); });
@ -34865,7 +34855,8 @@ window.axolotl.protocol = function(storage_interface) {
return initSession(true, baseKey, undefined, return initSession(true, baseKey, undefined,
deviceObject.encodedNumber, deviceIdentityKey, deviceObject.encodedNumber, deviceIdentityKey,
toArrayBuffer(deviceObject.preKey), toArrayBuffer(deviceObject.preKey),
deviceSignedKey).then(function(new_session) { deviceSignedKey
).then(function(new_session) {
session = new_session; session = new_session;
session.pendingPreKey = { preKeyId: deviceObject.preKeyId, signedKeyId: deviceObject.signedKeyId, baseKey: baseKey.pubKey }; session.pendingPreKey = { preKeyId: deviceObject.preKeyId, signedKeyId: deviceObject.signedKeyId, baseKey: baseKey.pubKey };
return doEncryptPushMessageContent().then(function(message) { return doEncryptPushMessageContent().then(function(message) {