|
|
|
@ -34148,112 +34148,54 @@ axolotlInternal.crypto = function() {
|
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
*/
|
|
|
|
|
var axolotlInternal = axolotlInternal || {};
|
|
|
|
|
|
|
|
|
|
axolotlInternal.utils = function() {
|
|
|
|
|
var util = (function() {
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
var StaticByteBufferProto = new dcodeIO.ByteBuffer().__proto__;
|
|
|
|
|
var StaticArrayBufferProto = new ArrayBuffer().__proto__;
|
|
|
|
|
var StaticUint8ArrayProto = new Uint8Array().__proto__;
|
|
|
|
|
|
|
|
|
|
function stringObject(thing) {
|
|
|
|
|
if (typeof thing === 'string') {
|
|
|
|
|
return thing;
|
|
|
|
|
}
|
|
|
|
|
if (thing === Object(thing)) {
|
|
|
|
|
if (thing.__proto__ == StaticUint8ArrayProto)
|
|
|
|
|
if (thing.__proto__ == StaticUint8ArrayProto) {
|
|
|
|
|
return String.fromCharCode.apply(null, thing);
|
|
|
|
|
if (thing.__proto__ == StaticArrayBufferProto)
|
|
|
|
|
return getString(new Uint8Array(thing));
|
|
|
|
|
if (thing.__proto__ == StaticByteBufferProto)
|
|
|
|
|
return thing.toString("binary");
|
|
|
|
|
}
|
|
|
|
|
if (thing.__proto__ == StaticArrayBufferProto) {
|
|
|
|
|
return stringObject(new Uint8Array(thing));
|
|
|
|
|
}
|
|
|
|
|
if (thing.__proto__ == StaticByteBufferProto) {
|
|
|
|
|
return thing.toString('binary');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
throw new Error("unsure how to stringify object of type " + typeof thing);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isStringable(thing) {
|
|
|
|
|
return (thing === Object(thing) &&
|
|
|
|
|
(thing.__proto__ == StaticArrayBufferProto ||
|
|
|
|
|
thing.__proto__ == StaticUint8ArrayProto ||
|
|
|
|
|
thing.__proto__ == StaticByteBufferProto));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ensureStringed(thing) {
|
|
|
|
|
if (typeof thing == "string" || typeof thing == "number" || typeof thing == "boolean")
|
|
|
|
|
return thing;
|
|
|
|
|
else if (isStringable(thing))
|
|
|
|
|
return stringObject(thing);
|
|
|
|
|
else if (thing instanceof Array) {
|
|
|
|
|
var res = [];
|
|
|
|
|
for (var i = 0; i < thing.length; i++)
|
|
|
|
|
res[i] = ensureStringed(thing[i]);
|
|
|
|
|
return res;
|
|
|
|
|
} else if (thing === Object(thing)) {
|
|
|
|
|
var res = {};
|
|
|
|
|
for (var key in thing)
|
|
|
|
|
res[key] = ensureStringed(thing[key]);
|
|
|
|
|
return res;
|
|
|
|
|
} else if (thing === null)
|
|
|
|
|
return null;
|
|
|
|
|
else
|
|
|
|
|
throw new Error("unsure of how to jsonify object of type " + typeof thing);
|
|
|
|
|
throw new Error('unsure how to stringify object of type ' + typeof thing);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
stringObject: stringObject,
|
|
|
|
|
isStringable: function (thing) {
|
|
|
|
|
return (thing === Object(thing) &&
|
|
|
|
|
(thing.__proto__ == StaticArrayBufferProto ||
|
|
|
|
|
thing.__proto__ == StaticUint8ArrayProto ||
|
|
|
|
|
thing.__proto__ == StaticByteBufferProto));
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
isEqual: function(a, b) {
|
|
|
|
|
// TODO: Special-case arraybuffers, etc
|
|
|
|
|
if (a === undefined || b === undefined)
|
|
|
|
|
return false;
|
|
|
|
|
a = getString(a);
|
|
|
|
|
b = getString(b);
|
|
|
|
|
a = stringObject(a);
|
|
|
|
|
b = stringObject(b);
|
|
|
|
|
var maxLength = Math.max(a.length, b.length);
|
|
|
|
|
if (maxLength < 5)
|
|
|
|
|
throw new Error("a/b compare too short");
|
|
|
|
|
return a.substring(0, Math.min(maxLength, a.length)) == b.substring(0, Math.min(maxLength, b.length));
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
jsonThing: function(thing) {
|
|
|
|
|
return JSON.stringify(ensureStringed(thing)); //TODO: jquery???
|
|
|
|
|
},
|
|
|
|
|
convertToString: function(thing) {
|
|
|
|
|
if (typeof thing == "string")
|
|
|
|
|
return thing;
|
|
|
|
|
else if (isStringable(thing))
|
|
|
|
|
return stringObject(thing);
|
|
|
|
|
else
|
|
|
|
|
throw new Error("Unsure how to convert object to string from type " + typeof thing);
|
|
|
|
|
},
|
|
|
|
|
convertToArrayBuffer: function(thing) {
|
|
|
|
|
if (thing === undefined)
|
|
|
|
|
return undefined;
|
|
|
|
|
if (thing === Object(thing)) {
|
|
|
|
|
if (thing.__proto__ == StaticArrayBufferProto)
|
|
|
|
|
return thing;
|
|
|
|
|
//TODO: Several more cases here...
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (thing instanceof Array) {
|
|
|
|
|
// Assuming Uint16Array from curve25519
|
|
|
|
|
//TODO: Move to convertToArrayBuffer
|
|
|
|
|
var res = new ArrayBuffer(thing.length * 2);
|
|
|
|
|
var uint = new Uint16Array(res);
|
|
|
|
|
for (var i = 0; i < thing.length; i++)
|
|
|
|
|
uint[i] = thing[i];
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var str;
|
|
|
|
|
if (isStringable(thing))
|
|
|
|
|
str = stringObject(thing);
|
|
|
|
|
else if (typeof thing == "string")
|
|
|
|
|
str = thing;
|
|
|
|
|
else
|
|
|
|
|
throw new Error("Tried to convert a non-stringable thing of type " + typeof thing + " to an array buffer");
|
|
|
|
|
var res = new ArrayBuffer(str.length);
|
|
|
|
|
var uint = new Uint8Array(res);
|
|
|
|
|
for (var i = 0; i < str.length; i++)
|
|
|
|
|
uint[i] = str.charCodeAt(i);
|
|
|
|
|
return res;
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}();
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
/* vim: ts=4:sw=4
|
|
|
|
|
*
|
|
|
|
@ -34344,6 +34286,53 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
return count != 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toString(thing) {
|
|
|
|
|
if (typeof thing == 'string') {
|
|
|
|
|
return thing;
|
|
|
|
|
} else if (util.isStringable(thing)) {
|
|
|
|
|
return util.stringObject(thing);
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error("Unsure how to convert object to string from type " + typeof thing);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var StaticArrayBufferProto = new ArrayBuffer().__proto__;
|
|
|
|
|
function toArrayBuffer(thing) {
|
|
|
|
|
if (thing === undefined) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
if (thing === Object(thing)) {
|
|
|
|
|
if (thing.__proto__ == StaticArrayBufferProto)
|
|
|
|
|
return thing;
|
|
|
|
|
//TODO: Several more cases here...
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (thing instanceof Array) {
|
|
|
|
|
// Assuming Uint16Array from curve25519
|
|
|
|
|
//TODO: Move to convertToArrayBuffer
|
|
|
|
|
var res = new ArrayBuffer(thing.length * 2);
|
|
|
|
|
var uint = new Uint16Array(res);
|
|
|
|
|
for (var i = 0; i < thing.length; i++)
|
|
|
|
|
uint[i] = thing[i];
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var str;
|
|
|
|
|
if (util.isStringable(thing)) {
|
|
|
|
|
str = util.stringObject(thing);
|
|
|
|
|
} else if (typeof thing == "string") {
|
|
|
|
|
str = thing;
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error("Tried to convert a non-stringable thing of type " + typeof thing + " to an array buffer");
|
|
|
|
|
}
|
|
|
|
|
var res = new ArrayBuffer(str.length);
|
|
|
|
|
var uint = new Uint8Array(res);
|
|
|
|
|
for (var i = 0; i < str.length; i++) {
|
|
|
|
|
uint[i] = str.charCodeAt(i);
|
|
|
|
|
}
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/***************************
|
|
|
|
|
*** Key/session storage ***
|
|
|
|
|
***************************/
|
|
|
|
@ -34364,13 +34353,13 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
if (registrationId === undefined)
|
|
|
|
|
throw new Error("Tried to save a session for an existing device that didn't exist");
|
|
|
|
|
else
|
|
|
|
|
record = new axolotlInternal.RecipientRecord(session.indexInfo.remoteIdentityKey, registrationId);
|
|
|
|
|
record = new axolotlInternal.RecipientRecord(toString(session.indexInfo.remoteIdentityKey), registrationId);
|
|
|
|
|
}
|
|
|
|
|
var sessions = record._sessions;
|
|
|
|
|
|
|
|
|
|
if (record.identityKey === null)
|
|
|
|
|
record.identityKey = session.indexInfo.remoteIdentityKey;
|
|
|
|
|
if (axolotlInternal.utils.convertToString(record.identityKey) !== axolotlInternal.utils.convertToString(session.indexInfo.remoteIdentityKey)) {
|
|
|
|
|
if (toString(record.identityKey) !== toString(session.indexInfo.remoteIdentityKey)) {
|
|
|
|
|
var e = new Error("Identity key changed at session save time");
|
|
|
|
|
e.identityKey = session.indexInfo.remoteIdentityKey.toArrayBuffer();
|
|
|
|
|
throw e;
|
|
|
|
@ -34395,9 +34384,9 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (doDeleteSession)
|
|
|
|
|
delete sessions[axolotlInternal.utils.convertToString(session.indexInfo.baseKey)];
|
|
|
|
|
delete sessions[toString(session.indexInfo.baseKey)];
|
|
|
|
|
else
|
|
|
|
|
sessions[axolotlInternal.utils.convertToString(session.indexInfo.baseKey)] = session;
|
|
|
|
|
sessions[toString(session.indexInfo.baseKey)] = session;
|
|
|
|
|
|
|
|
|
|
var openSessionRemaining = false;
|
|
|
|
|
for (var key in sessions)
|
|
|
|
@ -34411,7 +34400,7 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
throw new Error("Had open sessions on a record that had no registrationId set");
|
|
|
|
|
|
|
|
|
|
return storage_interface.getIdentityKey(encodedNumber).then(function(identityKey) {
|
|
|
|
|
if (identityKey !== undefined && axolotlInternal.utils.convertToString(identityKey) !== axolotlInternal.utils.convertToString(record.identityKey))
|
|
|
|
|
if (identityKey !== undefined && toString(identityKey) !== toString(record.identityKey))
|
|
|
|
|
throw new Error("Tried to change identity key at save time");
|
|
|
|
|
|
|
|
|
|
return storage_interface.putIdentityKey(encodedNumber, record.identityKey).then(function() {
|
|
|
|
@ -34462,7 +34451,7 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
|
|
|
|
|
detectDuplicateOpenSessions(sessions, encodedNumber);
|
|
|
|
|
|
|
|
|
|
var searchKey = axolotlInternal.utils.convertToString(remoteEphemeralKey);
|
|
|
|
|
var searchKey = toString(remoteEphemeralKey);
|
|
|
|
|
|
|
|
|
|
var openSession = undefined;
|
|
|
|
|
for (var key in sessions) {
|
|
|
|
@ -34490,7 +34479,7 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
}
|
|
|
|
|
var sessions = record._sessions;
|
|
|
|
|
|
|
|
|
|
var preferredSession = record._sessions[axolotlInternal.utils.convertToString(baseKey)];
|
|
|
|
|
var preferredSession = record._sessions[toString(baseKey)];
|
|
|
|
|
if (preferredSession !== undefined)
|
|
|
|
|
return preferredSession;
|
|
|
|
|
|
|
|
|
@ -34511,7 +34500,7 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
if (salt.byteLength != 32)
|
|
|
|
|
throw new Error("Got salt of incorrect length");
|
|
|
|
|
|
|
|
|
|
info = axolotlInternal.utils.convertToArrayBuffer(info); // TODO: maybe convert calls?
|
|
|
|
|
info = toArrayBuffer(info); // TODO: maybe convert calls?
|
|
|
|
|
|
|
|
|
|
return axolotlInternal.crypto.HKDF(input, salt, info);
|
|
|
|
|
}
|
|
|
|
@ -34539,12 +34528,19 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
var calculateRatchet = function(session, remoteKey, sending) {
|
|
|
|
|
var ratchet = session.currentRatchet;
|
|
|
|
|
|
|
|
|
|
return axolotlInternal.crypto.ECDHE(remoteKey, axolotlInternal.utils.convertToArrayBuffer(ratchet.ephemeralKeyPair.privKey)).then(function(sharedSecret) {
|
|
|
|
|
return HKDF(sharedSecret, axolotlInternal.utils.convertToArrayBuffer(ratchet.rootKey), "WhisperRatchet").then(function(masterKey) {
|
|
|
|
|
if (sending)
|
|
|
|
|
session[axolotlInternal.utils.convertToString(ratchet.ephemeralKeyPair.pubKey)] = { messageKeys: {}, chainKey: { counter: -1, key: masterKey[1] } };
|
|
|
|
|
else
|
|
|
|
|
session[axolotlInternal.utils.convertToString(remoteKey)] = { messageKeys: {}, chainKey: { counter: -1, key: masterKey[1] } };
|
|
|
|
|
return axolotlInternal.crypto.ECDHE(remoteKey, toArrayBuffer(ratchet.ephemeralKeyPair.privKey)).then(function(sharedSecret) {
|
|
|
|
|
return HKDF(sharedSecret, toArrayBuffer(ratchet.rootKey), "WhisperRatchet").then(function(masterKey) {
|
|
|
|
|
var ephemeralPublicKey;
|
|
|
|
|
if (sending) {
|
|
|
|
|
ephemeralPublicKey = ratchet.ephemeralKeyPair.pubKey;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
ephemeralPublicKey = remoteKey;
|
|
|
|
|
}
|
|
|
|
|
session[toString(ephemeralPublicKey)] = {
|
|
|
|
|
messageKeys: {},
|
|
|
|
|
chainKey: { counter: -1, key: masterKey[1] }
|
|
|
|
|
};
|
|
|
|
|
ratchet.rootKey = masterKey[0];
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
@ -34634,7 +34630,7 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
var newList = [];
|
|
|
|
|
for (var i = 0; i < session.oldRatchetList.length; i++) {
|
|
|
|
|
var entry = session.oldRatchetList[i];
|
|
|
|
|
var ratchet = axolotlInternal.utils.convertToString(entry.ephemeralKey);
|
|
|
|
|
var ratchet = toString(entry.ephemeralKey);
|
|
|
|
|
console.log("Checking old chain with added time " + (entry.added/1000));
|
|
|
|
|
if ((!objectContainsKeys(session[ratchet].messageKeys) && (session[ratchet].chainKey === undefined || session[ratchet].chainKey.key === undefined))
|
|
|
|
|
|| entry.added < Date.now() - MESSAGE_LOST_THRESHOLD_MS) {
|
|
|
|
@ -34655,7 +34651,7 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
// but we cannot send messages or step the ratchet
|
|
|
|
|
|
|
|
|
|
// Delete current sending ratchet
|
|
|
|
|
delete session[axolotlInternal.utils.convertToString(session.currentRatchet.ephemeralKeyPair.pubKey)];
|
|
|
|
|
delete session[toString(session.currentRatchet.ephemeralKeyPair.pubKey)];
|
|
|
|
|
// Move all receive ratchets to the oldRatchetList to mark them for deletion
|
|
|
|
|
for (var i in session) {
|
|
|
|
|
if (session[i].chainKey !== undefined && session[i].chainKey.key !== undefined) {
|
|
|
|
@ -34685,7 +34681,7 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
var initSessionFromPreKeyWhisperMessage = function(encodedNumber, message) {
|
|
|
|
|
return storage_interface.getPreKey(message.preKeyId).then(function(preKeyPair) {
|
|
|
|
|
return storage_interface.getSignedPreKey(message.signedPreKeyId).then(function(signedPreKeyPair) {
|
|
|
|
|
return crypto_storage.getSessionOrIdentityKeyByBaseKey(encodedNumber, axolotlInternal.utils.convertToArrayBuffer(message.baseKey)).then(function(session) {
|
|
|
|
|
return crypto_storage.getSessionOrIdentityKeyByBaseKey(encodedNumber, toArrayBuffer(message.baseKey)).then(function(session) {
|
|
|
|
|
return crypto_storage.getOpenSession(encodedNumber).then(function(open_session) {
|
|
|
|
|
if (signedPreKeyPair === undefined) {
|
|
|
|
|
// Session may or may not be the right one, but if its not, we can't do anything about it
|
|
|
|
@ -34697,13 +34693,13 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
}
|
|
|
|
|
if (session !== undefined) {
|
|
|
|
|
// Duplicate PreKeyMessage for session:
|
|
|
|
|
if (axolotlInternal.utils.isEqual(session.indexInfo.baseKey, message.baseKey))
|
|
|
|
|
if (util.isEqual(session.indexInfo.baseKey, message.baseKey))
|
|
|
|
|
return Promise.resolve([session, undefined]);
|
|
|
|
|
|
|
|
|
|
// We already had a session/known identity key:
|
|
|
|
|
if (axolotlInternal.utils.isEqual(session.indexInfo.remoteIdentityKey, message.identityKey)) {
|
|
|
|
|
if (util.isEqual(session.indexInfo.remoteIdentityKey, message.identityKey)) {
|
|
|
|
|
// If the identity key matches the previous one, close the previous one and use the new one
|
|
|
|
|
if (open_session !== undefined)
|
|
|
|
|
if (open_session !== undefin)
|
|
|
|
|
closeSession(open_session); // To be returned and saved later
|
|
|
|
|
} else {
|
|
|
|
|
// ...otherwise create an error that the UI will pick up and ask the user if they want to re-negotiate
|
|
|
|
@ -34712,7 +34708,7 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return initSession(false, preKeyPair, signedPreKeyPair, encodedNumber, axolotlInternal.utils.convertToArrayBuffer(message.identityKey), axolotlInternal.utils.convertToArrayBuffer(message.baseKey), undefined)
|
|
|
|
|
return initSession(false, preKeyPair, signedPreKeyPair, encodedNumber, toArrayBuffer(message.identityKey), toArrayBuffer(message.baseKey), undefined)
|
|
|
|
|
.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
|
|
|
|
@ -34741,7 +34737,7 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
if (chain.chainKey.key === undefined)
|
|
|
|
|
throw new Error("Got invalid request to extend chain after it was already closed");
|
|
|
|
|
|
|
|
|
|
var key = axolotlInternal.utils.convertToArrayBuffer(chain.chainKey.key);
|
|
|
|
|
var key = toArrayBuffer(chain.chainKey.key);
|
|
|
|
|
var byteArray = new Uint8Array(1);
|
|
|
|
|
byteArray[0] = 1;
|
|
|
|
|
return axolotlInternal.crypto.sign(key, byteArray.buffer).then(function(mac) {
|
|
|
|
@ -34756,7 +34752,7 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var maybeStepRatchet = function(session, remoteKey, previousCounter) {
|
|
|
|
|
if (session[axolotlInternal.utils.convertToString(remoteKey)] !== undefined)
|
|
|
|
|
if (session[toString(remoteKey)] !== undefined)
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
|
|
|
|
|
var ratchet = session.currentRatchet;
|
|
|
|
@ -34764,7 +34760,7 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
var finish = function() {
|
|
|
|
|
return calculateRatchet(session, remoteKey, false).then(function() {
|
|
|
|
|
// Now swap the ephemeral key and calculate the new sending chain
|
|
|
|
|
var previousRatchet = axolotlInternal.utils.convertToString(ratchet.ephemeralKeyPair.pubKey);
|
|
|
|
|
var previousRatchet = toString(ratchet.ephemeralKeyPair.pubKey);
|
|
|
|
|
if (session[previousRatchet] !== undefined) {
|
|
|
|
|
ratchet.previousCounter = session[previousRatchet].chainKey.counter;
|
|
|
|
|
delete session[previousRatchet];
|
|
|
|
@ -34779,12 +34775,12 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var previousRatchet = session[axolotlInternal.utils.convertToString(ratchet.lastRemoteEphemeralKey)];
|
|
|
|
|
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[axolotlInternal.utils.convertToString(ratchet.lastRemoteEphemeralKey)];
|
|
|
|
|
delete session[toString(ratchet.lastRemoteEphemeralKey)];
|
|
|
|
|
else
|
|
|
|
|
session.oldRatchetList[session.oldRatchetList.length] = { added: Date.now(), ephemeralKey: ratchet.lastRemoteEphemeralKey };
|
|
|
|
|
}).then(finish);
|
|
|
|
@ -34793,16 +34789,18 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var doDecryptWhisperMessage = function(encodedNumber, messageBytes, session, registrationId) {
|
|
|
|
|
if (messageBytes[0] != String.fromCharCode((3 << 4) | 3))
|
|
|
|
|
if (!messageBytes instanceof ArrayBuffer) {
|
|
|
|
|
throw new Error("Expected messageBytes to be an ArrayBuffer");
|
|
|
|
|
}
|
|
|
|
|
var version = (new Uint8Array(messageBytes))[0];
|
|
|
|
|
if (version !== ((3 << 4) | 3)) {
|
|
|
|
|
throw new Error("Bad version number on WhisperMessage");
|
|
|
|
|
}
|
|
|
|
|
var messageProto = messageBytes.slice(1, messageBytes.byteLength- 8);
|
|
|
|
|
var mac = messageBytes.slice(messageBytes.byteLength - 8, messageBytes.byteLength);
|
|
|
|
|
|
|
|
|
|
var messageProto = messageBytes.substring(1, messageBytes.length - 8);
|
|
|
|
|
var mac = axolotlInternal.utils.convertToArrayBuffer(
|
|
|
|
|
messageBytes.substring(messageBytes.length - 8, messageBytes.length)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
var message = axolotlInternal.protobuf.WhisperMessage.decode(messageProto, 'binary');
|
|
|
|
|
var remoteEphemeralKey = axolotlInternal.utils.convertToArrayBuffer(message.ephemeralKey);
|
|
|
|
|
var message = axolotlInternal.protobuf.WhisperMessage.decode(messageProto);
|
|
|
|
|
var remoteEphemeralKey = message.ephemeralKey.toArrayBuffer();
|
|
|
|
|
|
|
|
|
|
var promise;
|
|
|
|
|
if (session === undefined) {
|
|
|
|
@ -34817,7 +34815,7 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
|
|
|
|
|
return promise.then(function(session) {
|
|
|
|
|
return maybeStepRatchet(session, remoteEphemeralKey, message.previousCounter).then(function() {
|
|
|
|
|
var chain = session[axolotlInternal.utils.convertToString(message.ephemeralKey)];
|
|
|
|
|
var chain = session[toString(message.ephemeralKey)];
|
|
|
|
|
|
|
|
|
|
return fillMessageKeys(chain, message.counter).then(function() {
|
|
|
|
|
var messageKey = chain.messageKeys[message.counter];
|
|
|
|
@ -34826,21 +34824,22 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
e.name = 'MessageCounterError';
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
return HKDF(axolotlInternal.utils.convertToArrayBuffer(messageKey), '', "WhisperMessageKeys").then(function(keys) {
|
|
|
|
|
return HKDF(toArrayBuffer(messageKey), '', "WhisperMessageKeys").then(function(keys) {
|
|
|
|
|
return storage_interface.getMyIdentityKey().then(function(ourIdentityKey) {
|
|
|
|
|
delete chain.messageKeys[message.counter];
|
|
|
|
|
|
|
|
|
|
var messageProtoArray = axolotlInternal.utils.convertToArrayBuffer(messageProto);
|
|
|
|
|
var macInput = new Uint8Array(messageProtoArray.byteLength + 33*2 + 1);
|
|
|
|
|
macInput.set(new Uint8Array(axolotlInternal.utils.convertToArrayBuffer(session.indexInfo.remoteIdentityKey)));
|
|
|
|
|
macInput.set(new Uint8Array(axolotlInternal.utils.convertToArrayBuffer(ourIdentityKey.pubKey)), 33);
|
|
|
|
|
var macInput = new Uint8Array(messageProto.byteLength + 33*2 + 1);
|
|
|
|
|
macInput.set(new Uint8Array(toArrayBuffer(session.indexInfo.remoteIdentityKey)));
|
|
|
|
|
macInput.set(new Uint8Array(toArrayBuffer(ourIdentityKey.pubKey)), 33);
|
|
|
|
|
macInput[33*2] = (3 << 4) | 3;
|
|
|
|
|
macInput.set(new Uint8Array(messageProtoArray), 33*2 + 1);
|
|
|
|
|
macInput.set(new Uint8Array(messageProto), 33*2 + 1);
|
|
|
|
|
|
|
|
|
|
return verifyMAC(macInput.buffer, keys[1], mac, 8).then(function() {
|
|
|
|
|
return axolotlInternal.crypto.decrypt(keys[0], axolotlInternal.utils.convertToArrayBuffer(message.ciphertext), keys[2].slice(0, 16))
|
|
|
|
|
.then(function(paddedPlaintext) {
|
|
|
|
|
|
|
|
|
|
return axolotlInternal.crypto.decrypt(
|
|
|
|
|
keys[0],
|
|
|
|
|
message.ciphertext.toArrayBuffer(),
|
|
|
|
|
keys[2].slice(0, 16)
|
|
|
|
|
).then(function(paddedPlaintext) {
|
|
|
|
|
paddedPlaintext = new Uint8Array(paddedPlaintext);
|
|
|
|
|
var plaintext;
|
|
|
|
|
for (var i = paddedPlaintext.length - 1; i >= 0; i--) {
|
|
|
|
@ -34877,15 +34876,20 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
//TODO: SHARP EDGE HERE
|
|
|
|
|
//XXX: Also, you MUST call the session close function before processing another message....except its a promise...so you literally cant!
|
|
|
|
|
// returns decrypted plaintext and a function that must be called if the message indicates session close
|
|
|
|
|
self.decryptWhisperMessage = function(encodedNumber, messageBytes, session) {
|
|
|
|
|
return doDecryptWhisperMessage(encodedNumber, messageBytes, session);
|
|
|
|
|
}
|
|
|
|
|
self.decryptWhisperMessage = function(encodedNumber, messageBytes) {
|
|
|
|
|
return doDecryptWhisperMessage(encodedNumber, toArrayBuffer(messageBytes));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Inits a session (maybe) and then decrypts the message
|
|
|
|
|
self.handlePreKeyWhisperMessage = function(from, encodedMessage, encoding) {
|
|
|
|
|
var preKeyProto = axolotlInternal.protobuf.PreKeyWhisperMessage.decode(encodedMessage, encoding);
|
|
|
|
|
return initSessionFromPreKeyWhisperMessage(from, preKeyProto).then(function(sessions) {
|
|
|
|
|
return doDecryptWhisperMessage(from, axolotlInternal.utils.convertToString(preKeyProto.message), sessions[0], preKeyProto.registrationId).then(function(result) {
|
|
|
|
|
return doDecryptWhisperMessage(
|
|
|
|
|
from,
|
|
|
|
|
preKeyProto.message.toArrayBuffer(),
|
|
|
|
|
sessions[0],
|
|
|
|
|
preKeyProto.registrationId
|
|
|
|
|
).then(function(result) {
|
|
|
|
|
if (sessions[1] !== undefined)
|
|
|
|
|
return sessions[1]().then(function() { return result; });
|
|
|
|
|
return result;
|
|
|
|
@ -34921,22 +34925,22 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
paddedPlaintext.set(new Uint8Array(plaintext));
|
|
|
|
|
paddedPlaintext[plaintext.byteLength] = 0x80;
|
|
|
|
|
|
|
|
|
|
msg.ephemeralKey = axolotlInternal.utils.convertToArrayBuffer(session.currentRatchet.ephemeralKeyPair.pubKey);
|
|
|
|
|
var chain = session[axolotlInternal.utils.convertToString(msg.ephemeralKey)];
|
|
|
|
|
msg.ephemeralKey = toArrayBuffer(session.currentRatchet.ephemeralKeyPair.pubKey);
|
|
|
|
|
var chain = session[toString(msg.ephemeralKey)];
|
|
|
|
|
|
|
|
|
|
return fillMessageKeys(chain, chain.chainKey.counter + 1).then(function() {
|
|
|
|
|
return HKDF(axolotlInternal.utils.convertToArrayBuffer(chain.messageKeys[chain.chainKey.counter]), '', "WhisperMessageKeys").then(function(keys) {
|
|
|
|
|
return HKDF(toArrayBuffer(chain.messageKeys[chain.chainKey.counter]), '', "WhisperMessageKeys").then(function(keys) {
|
|
|
|
|
delete chain.messageKeys[chain.chainKey.counter];
|
|
|
|
|
msg.counter = chain.chainKey.counter;
|
|
|
|
|
msg.previousCounter = session.currentRatchet.previousCounter;
|
|
|
|
|
|
|
|
|
|
return axolotlInternal.crypto.encrypt(keys[0], paddedPlaintext.buffer, keys[2].slice(0, 16)).then(function(ciphertext) {
|
|
|
|
|
msg.ciphertext = ciphertext;
|
|
|
|
|
var encodedMsg = axolotlInternal.utils.convertToArrayBuffer(msg.encode());
|
|
|
|
|
var encodedMsg = toArrayBuffer(msg.encode());
|
|
|
|
|
|
|
|
|
|
var macInput = new Uint8Array(encodedMsg.byteLength + 33*2 + 1);
|
|
|
|
|
macInput.set(new Uint8Array(axolotlInternal.utils.convertToArrayBuffer(ourIdentityKey.pubKey)));
|
|
|
|
|
macInput.set(new Uint8Array(axolotlInternal.utils.convertToArrayBuffer(session.indexInfo.remoteIdentityKey)), 33);
|
|
|
|
|
macInput.set(new Uint8Array(toArrayBuffer(ourIdentityKey.pubKey)));
|
|
|
|
|
macInput.set(new Uint8Array(toArrayBuffer(session.indexInfo.remoteIdentityKey)), 33);
|
|
|
|
|
macInput[33*2] = (3 << 4) | 3;
|
|
|
|
|
macInput.set(new Uint8Array(encodedMsg), 33*2 + 1);
|
|
|
|
|
|
|
|
|
@ -34958,26 +34962,26 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var preKeyMsg = new axolotlInternal.protobuf.PreKeyWhisperMessage();
|
|
|
|
|
preKeyMsg.identityKey = axolotlInternal.utils.convertToArrayBuffer(ourIdentityKey.pubKey);
|
|
|
|
|
preKeyMsg.identityKey = toArrayBuffer(ourIdentityKey.pubKey);
|
|
|
|
|
preKeyMsg.registrationId = myRegistrationId;
|
|
|
|
|
|
|
|
|
|
if (session === undefined) {
|
|
|
|
|
var deviceIdentityKey = axolotlInternal.utils.convertToArrayBuffer(deviceObject.identityKey);
|
|
|
|
|
var deviceSignedKey = axolotlInternal.utils.convertToArrayBuffer(deviceObject.signedKey);
|
|
|
|
|
return axolotlInternal.crypto.Ed25519Verify(deviceIdentityKey, deviceSignedKey, axolotlInternal.utils.convertToArrayBuffer(deviceObject.signedKeySignature)).then(function() {
|
|
|
|
|
var deviceIdentityKey = toArrayBuffer(deviceObject.identityKey);
|
|
|
|
|
var deviceSignedKey = toArrayBuffer(deviceObject.signedKey);
|
|
|
|
|
return axolotlInternal.crypto.Ed25519Verify(deviceIdentityKey, deviceSignedKey, toArrayBuffer(deviceObject.signedKeySignature)).then(function() {
|
|
|
|
|
return axolotlInternal.crypto.createKeyPair().then(function(baseKey) {
|
|
|
|
|
preKeyMsg.preKeyId = deviceObject.preKeyId;
|
|
|
|
|
preKeyMsg.signedPreKeyId = deviceObject.signedKeyId;
|
|
|
|
|
preKeyMsg.baseKey = axolotlInternal.utils.convertToArrayBuffer(baseKey.pubKey);
|
|
|
|
|
preKeyMsg.baseKey = toArrayBuffer(baseKey.pubKey);
|
|
|
|
|
return initSession(true, baseKey, undefined,
|
|
|
|
|
deviceObject.encodedNumber, deviceIdentityKey,
|
|
|
|
|
axolotlInternal.utils.convertToArrayBuffer(deviceObject.preKey),
|
|
|
|
|
toArrayBuffer(deviceObject.preKey),
|
|
|
|
|
deviceSignedKey).then(function(new_session) {
|
|
|
|
|
session = new_session;
|
|
|
|
|
session.pendingPreKey = { preKeyId: deviceObject.preKeyId, signedKeyId: deviceObject.signedKeyId, baseKey: baseKey.pubKey };
|
|
|
|
|
return doEncryptPushMessageContent().then(function(message) {
|
|
|
|
|
preKeyMsg.message = message;
|
|
|
|
|
var result = String.fromCharCode((3 << 4) | 3) + axolotlInternal.utils.convertToString(preKeyMsg.encode());
|
|
|
|
|
var result = String.fromCharCode((3 << 4) | 3) + toString(preKeyMsg.encode());
|
|
|
|
|
return {type: 3, body: result};
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
@ -34986,15 +34990,15 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
} else
|
|
|
|
|
return doEncryptPushMessageContent().then(function(message) {
|
|
|
|
|
if (session.pendingPreKey !== undefined) {
|
|
|
|
|
preKeyMsg.baseKey = axolotlInternal.utils.convertToArrayBuffer(session.pendingPreKey.baseKey);
|
|
|
|
|
preKeyMsg.baseKey = toArrayBuffer(session.pendingPreKey.baseKey);
|
|
|
|
|
preKeyMsg.preKeyId = session.pendingPreKey.preKeyId;
|
|
|
|
|
preKeyMsg.signedPreKeyId = session.pendingPreKey.signedKeyId;
|
|
|
|
|
preKeyMsg.message = message;
|
|
|
|
|
|
|
|
|
|
var result = String.fromCharCode((3 << 4) | 3) + axolotlInternal.utils.convertToString(preKeyMsg.encode());
|
|
|
|
|
var result = String.fromCharCode((3 << 4) | 3) + toString(preKeyMsg.encode());
|
|
|
|
|
return {type: 3, body: result};
|
|
|
|
|
} else
|
|
|
|
|
return {type: 1, body: axolotlInternal.utils.convertToString(message)};
|
|
|
|
|
return {type: 1, body: toString(message)};
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
@ -35006,8 +35010,8 @@ window.axolotl.protocol = function(storage_interface) {
|
|
|
|
|
var keyPair;
|
|
|
|
|
|
|
|
|
|
socketInfo.decryptAndHandleDeviceInit = function(deviceInit) {
|
|
|
|
|
var masterEphemeral = axolotlInternal.utils.convertToArrayBuffer(deviceInit.publicKey);
|
|
|
|
|
var message = axolotlInternal.utils.convertToArrayBuffer(deviceInit.body);
|
|
|
|
|
var masterEphemeral = toArrayBuffer(deviceInit.publicKey);
|
|
|
|
|
var message = toArrayBuffer(deviceInit.body);
|
|
|
|
|
|
|
|
|
|
return axolotlInternal.crypto.ECDHE(masterEphemeral, keyPair.privKey).then(function(ecRes) {
|
|
|
|
|
return HKDF(ecRes, '', "TextSecure Provisioning Message").then(function(keys) {
|
|
|
|
@ -35198,19 +35202,51 @@ var axolotlInternal = axolotlInternal || {};
|
|
|
|
|
|
|
|
|
|
axolotlInternal.RecipientRecord = function() {
|
|
|
|
|
'use strict';
|
|
|
|
|
function ensureStringed(thing) {
|
|
|
|
|
if (typeof thing == "string" || typeof thing == "number" || typeof thing == "boolean")
|
|
|
|
|
return thing;
|
|
|
|
|
else if (util.isStringable(thing))
|
|
|
|
|
return util.stringObject(thing);
|
|
|
|
|
else if (thing instanceof Array) {
|
|
|
|
|
var res = [];
|
|
|
|
|
for (var i = 0; i < thing.length; i++)
|
|
|
|
|
res[i] = ensureStringed(thing[i]);
|
|
|
|
|
return res;
|
|
|
|
|
} else if (thing === Object(thing)) {
|
|
|
|
|
var res = {};
|
|
|
|
|
for (var key in thing)
|
|
|
|
|
res[key] = ensureStringed(thing[key]);
|
|
|
|
|
return res;
|
|
|
|
|
} else if (thing === null)
|
|
|
|
|
return null;
|
|
|
|
|
else
|
|
|
|
|
throw new Error("unsure of how to jsonify object of type " + typeof thing);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function jsonThing(thing) {
|
|
|
|
|
return JSON.stringify(ensureStringed(thing)); //TODO: jquery???
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var RecipientRecord = function(identityKey, registrationId) {
|
|
|
|
|
this._sessions = {};
|
|
|
|
|
this.identityKey = identityKey !== undefined ? axolotlInternal.utils.convertToString(identityKey) : null;
|
|
|
|
|
if (typeof identityKey !== 'string') {
|
|
|
|
|
throw new Error('RecipientRecord: Invalid identityKey');
|
|
|
|
|
}
|
|
|
|
|
this.identityKey = identityKey;
|
|
|
|
|
this.registrationId = registrationId;
|
|
|
|
|
|
|
|
|
|
if (this.registrationId === undefined || typeof this.registrationId !== "number")
|
|
|
|
|
if (this.registrationId === undefined || typeof this.registrationId !== 'number') {
|
|
|
|
|
this.registrationId = null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
RecipientRecord.prototype.serialize = function() {
|
|
|
|
|
return axolotlInternal.utils.jsonThing({sessions: this._sessions, registrationId: this.registrationId, identityKey: this.identityKey});
|
|
|
|
|
}
|
|
|
|
|
return jsonThing({
|
|
|
|
|
sessions : this._sessions,
|
|
|
|
|
registrationId : this.registrationId,
|
|
|
|
|
identityKey : this.identityKey
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
RecipientRecord.deserialize = function(serialized) {
|
|
|
|
|
var data = JSON.parse(serialized);
|
|
|
|
@ -35221,11 +35257,11 @@ axolotlInternal.RecipientRecord = function() {
|
|
|
|
|
if (record.identityKey === undefined || record.registrationId === undefined)
|
|
|
|
|
throw new Error("Error deserializing RecipientRecord");
|
|
|
|
|
return record;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
RecipientRecord.prototype.haveOpenSession = function() {
|
|
|
|
|
return this.registrationId !== null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return RecipientRecord;
|
|
|
|
|
}();
|
|
|
|
|