Refactor crypto.js and native client interface

NB: this diff is best viewed with --ignore-whitespace

Distills crypto.js down to the hard cryptoey bones. It pulls from
webcrypto for aes and hmac, and from native client for curve25519 stuff
or potentially another object implementing the handful of needed
curve25519 functions.

Everything else formerly known as crypto, including session storage and
management, axolotl, etc.. is now protocol.js. The separation is not
quite perfect, but it's a big step.

nativeclient.js now enables talking to the native client module through
a high level interface as well as registering callbacks that will be
executed once the module is loaded. And it has tests!

Finally, this commit removes all references to the "testing_only"
object, preferring to run tests on textsecure.crypto instead.
This commit is contained in:
lilia 2014-11-04 14:59:48 -08:00
parent cd4b98d426
commit 9f676af9bb
16 changed files with 1255 additions and 1050 deletions

View file

@ -21,13 +21,15 @@
<script type="text/javascript" src="js/components.js"></script>
<script type="text/javascript" src="js/protobufs.js"></script>
<script type="text/javascript" src="js/nativeclient.js"></script>
<script type="text/javascript" src="js/helpers.js"></script>
<script type="text/javascript" src="js/storage.js"></script>
<script type="text/javascript" src="js/storage/devices.js"></script>
<script type="text/javascript" src="js/storage/groups.js"></script>
<script type="text/javascript" src="js/libphonenumber-util.js"></script>
<script type="text/javascript" src="js/webcrypto.js"></script>
<script type="text/javascript" src="js/crypto.js"></script>
<script type="text/javascript" src="js/crypto.js"></script>
<script type="text/javascript" src="js/protocol.js"></script>
<script type="text/javascript" src="js/models/messages.js"></script>
<script type="text/javascript" src="js/models/threads.js"></script>
<script type="text/javascript" src="js/api.js"></script>

View file

@ -131,13 +131,17 @@
<script type="text/javascript" src="components/bootstrap-tagsinput/dist/bootstrap-tagsinput.js"></script>
<script type="text/javascript" src="js/protobufs.js"></script>
<script type="text/javascript" src="js/nativeclient.js"></script>
<script type="text/javascript" src="js/helpers.js"></script>
<script type="text/javascript" src="js/storage.js"></script>
<script type="text/javascript" src="js/storage/devices.js"></script>
<script type="text/javascript" src="js/storage/groups.js"></script>
<script type="text/javascript" src="js/libphonenumber-util.js"></script>
<script type="text/javascript" src="js/webcrypto.js"></script>
<script type="text/javascript" src="js/crypto.js"></script>
<script type="text/javascript" src="js/protocol.js"></script>
<script type="text/javascript" src="js/models/messages.js"></script>
<script type="text/javascript" src="js/models/threads.js"></script>
<script type="text/javascript" src="js/api.js"></script>

File diff suppressed because it is too large Load diff

View file

@ -205,60 +205,21 @@ window.textsecure.throwHumanError = function(error, type, humanError) {
throw e;
}
/**********************
*** NaCL Interface ***
**********************/
window.textsecure.nacl = function() {
var self = {};
;(function() {
'use strict';
window.textsecure = window.textsecure || {};
window.textsecure.NATIVE_CLIENT = window.textsecure.NATIVE_CLIENT || true;
self.USE_NACL = true;
var onLoadCallbacks = [];
var naclLoaded = 0;
self.registerOnLoadFunction = function(func) {
return new Promise(function(resolve, reject) {
if (naclLoaded || !self.USE_NACL)
if (!textsecure.NATIVE_CLIENT) {
window.textsecure.registerOnLoadFunction = window.textsecure.nativeclient.registerOnLoadFunction;
} else {
window.textsecure.registerOnLoadFunction = function(func) {
return new Promise(function(resolve, reject) {
return resolve(func());
onLoadCallbacks[onLoadCallbacks.length] = [ func, resolve, reject ];
});
});
};
}
var naclMessageNextId = 0;
var naclMessageIdCallbackMap = {};
window.moduleDidLoad = function() {
common.hideModule();
naclLoaded = 1;
for (var i = 0; i < onLoadCallbacks.length; i++) {
try {
onLoadCallbacks[i][1](onLoadCallbacks[i][0]());
} catch (e) {
onLoadCallbacks[i][2](e);
}
}
onLoadCallbacks = [];
}
window.handleMessage = function(message) {
naclMessageIdCallbackMap[message.data.call_id](message.data);
}
self.postNaclMessage = function(message) {
if (!self.USE_NACL)
throw new Error("Attempted to make NaCL call with !USE_NACL?");
return new Promise(function(resolve) {
naclMessageIdCallbackMap[naclMessageNextId] = resolve;
message.call_id = naclMessageNextId++;
common.naclModule.postMessage(message);
});
}
return self;
}();
//TODO: Some kind of textsecure.init(use_nacl)
window.textsecure.registerOnLoadFunction = window.textsecure.nacl.registerOnLoadFunction;
})();
window.textsecure.replay = function() {
var self = {};
@ -300,13 +261,13 @@ window.textsecure.subscribeToPush = function(message_callback) {
var socket = textsecure.api.getMessageWebsocket();
socket.onmessage = function(message) {
textsecure.crypto.decryptWebsocketMessage(message.message).then(function(plaintext) {
textsecure.protocol.decryptWebsocketMessage(message.message).then(function(plaintext) {
var proto = textsecure.protobuf.IncomingPushMessageSignal.decode(plaintext);
// After this point, a) decoding errors are not the server's fault, and
// b) we should handle them gracefully and tell the user they received an invalid message
console.log("Successfully decoded message with id: " + message.id);
socket.send(JSON.stringify({type: 1, id: message.id}));
return textsecure.crypto.handleIncomingPushMessageProto(proto).then(function(decrypted) {
return textsecure.protocol.handleIncomingPushMessageProto(proto).then(function(decrypted) {
// Delivery receipt
if (decrypted === null)
//TODO: Pass to UI
@ -327,7 +288,7 @@ window.textsecure.subscribeToPush = function(message_callback) {
var handleAttachment = function(attachment) {
return textsecure.api.getAttachment(attachment.id.toString()).then(function(encryptedBin) {
return textsecure.crypto.decryptAttachment(encryptedBin, attachment.key.toArrayBuffer()).then(function(decryptedBin) {
return textsecure.protocol.decryptAttachment(encryptedBin, attachment.key.toArrayBuffer()).then(function(decryptedBin) {
attachment.decrypted = decryptedBin;
});
});
@ -421,7 +382,7 @@ window.textsecure.registerSingleDevice = function(number, verificationCode, step
textsecure.storage.putUnencrypted("regionCode", libphonenumber.util.getRegionCodeForNumber(number));
stepDone(1);
return textsecure.crypto.generateKeys().then(function(keys) {
return textsecure.protocol.generateKeys().then(function(keys) {
stepDone(2);
return textsecure.api.registerKeys(keys).then(function() {
stepDone(3);
@ -456,7 +417,7 @@ window.textsecure.registerSecondDevice = function(encodedDeviceInit, cryptoInfo,
textsecure.storage.putUnencrypted("regionCode", libphonenumber.util.getRegion(number));
stepDone(2);
return textsecure.crypto.generateKeys().then(function(keys) {
return textsecure.protocol.generateKeys().then(function(keys) {
stepDone(3);
return textsecure.api.registerKeys(keys).then(function() {
stepDone(4);

85
js/nativeclient.js Normal file
View file

@ -0,0 +1,85 @@
/* vim: ts=4:sw=4:expandtab
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
;(function() {
'use strict';
window.textsecure = window.textsecure || {};
var naclMessageNextId = 0;
var naclMessageIdCallbackMap = {};
window.handleMessage = function(message) {
naclMessageIdCallbackMap[message.data.call_id](message.data);
}
function postMessage(message) {
return new Promise(function(resolve) {
naclMessageIdCallbackMap[naclMessageNextId] = resolve;
message.call_id = naclMessageNextId++;
common.naclModule.postMessage(message);
});
};
var onLoadCallbacks = [];
var naclLoaded = false;
window.moduleDidLoad = function() {
common.hideModule();
naclLoaded = true;
for (var i = 0; i < onLoadCallbacks.length; i++) {
try {
onLoadCallbacks[i][1](onLoadCallbacks[i][0]());
} catch (e) {
onLoadCallbacks[i][2](e);
}
}
onLoadCallbacks = [];
}
window.textsecure.nativeclient = {
privToPub: function(priv) {
return postMessage({command: "bytesToPriv", priv: priv}).then(function(message) {
var priv = message.res.slice(0, 32);
return postMessage({command: "privToPub", priv: priv}).then(function(message) {
return { pubKey: message.res.slice(0, 32), privKey: priv };
});
});
},
ECDHE: function(pub, priv) {
return postMessage({command: "ECDHE", pub: pub, priv: priv}).then(function(message) {
return message.res.slice(0, 32);
});
},
Ed25519Sign: function(priv, msg) {
return postMessage({command: "Ed25519Sign", priv: priv, msg: msg}).then(function(message) {
return message.res;
});
},
Ed25519Verify: function(pub, msg, sig) {
return postMessage({command: "Ed25519Verify", pub: pub, msg: msg, sig: sig}).then(function(message) {
if (!message.res)
throw new Error("Invalid signature");
});
},
registerOnLoadFunction: function(func) {
return new Promise(function(resolve, reject) {
if (naclLoaded) {
return resolve(func());
} else {
onLoadCallbacks[onLoadCallbacks.length] = [ func, resolve, reject ];
}
});
}
};
})();

View file

@ -151,7 +151,7 @@
$('#multi-device .status').text("Connecting...");
$('#setup-qr').html('');
textsecure.crypto.prepareTempWebsocket().then(function(cryptoInfo) {
textsecure.protocol.prepareTempWebsocket().then(function(cryptoInfo) {
var qrCode = new QRCode(document.getElementById('setup-qr'));
var socket = textsecure.api.getTempWebsocket();

807
js/protocol.js Normal file
View file

@ -0,0 +1,807 @@
/* vim: ts=4:sw=4
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
;(function() {
'use strict';
window.textsecure = window.textsecure || {};
window.textsecure.protocol = function() {
var self = {};
/******************************
*** Random constants/utils ***
******************************/
// We consider messages lost after a week and might throw away keys at that point
// (also the time between signedPreKey regenerations)
var MESSAGE_LOST_THRESHOLD_MS = 1000*60*60*24*7;
var getRandomBytes = function(size) {
// At some point we might consider XORing in hashes of random
// UI events to strengthen ourselves against RNG flaws in crypto.getRandomValues
// ie maybe take a look at how Gibson does it at https://www.grc.com/r&d/js.htm
var array = new Uint8Array(size);
window.crypto.getRandomValues(array);
return array.buffer;
}
self.getRandomBytes = getRandomBytes;
function objectContainsKeys(object) {
var count = 0;
for (var key in object) {
count++;
break;
}
return count != 0;
}
/***************************
*** Key/session storage ***
***************************/
var crypto_storage = {};
crypto_storage.putKeyPair = function(keyName, keyPair) {
textsecure.storage.putEncrypted("25519Key" + keyName, keyPair);
}
crypto_storage.getNewStoredKeyPair = function(keyName) {
return textsecure.crypto.createKeyPair().then(function(keyPair) {
crypto_storage.putKeyPair(keyName, keyPair);
return keyPair;
});
}
crypto_storage.getStoredKeyPair = function(keyName) {
var res = textsecure.storage.getEncrypted("25519Key" + keyName);
if (res === undefined)
return undefined;
return { pubKey: toArrayBuffer(res.pubKey), privKey: toArrayBuffer(res.privKey) };
}
crypto_storage.removeStoredKeyPair = function(keyName) {
textsecure.storage.removeEncrypted("25519Key" + keyName);
}
crypto_storage.getIdentityKey = function() {
return this.getStoredKeyPair("identityKey");
}
crypto_storage.saveSession = function(encodedNumber, session, registrationId) {
var device = textsecure.storage.devices.getDeviceObject(encodedNumber);
if (device === undefined)
device = { sessions: {}, encodedNumber: encodedNumber };
if (registrationId !== undefined)
device.registrationId = registrationId;
crypto_storage.saveSessionAndDevice(device, session);
}
crypto_storage.saveSessionAndDevice = function(device, session) {
if (device.sessions === undefined)
device.sessions = {};
var sessions = device.sessions;
var doDeleteSession = false;
if (session.indexInfo.closed == -1 || device.identityKey === undefined)
device.identityKey = session.indexInfo.remoteIdentityKey;
if (session.indexInfo.closed != -1) {
doDeleteSession = (session.indexInfo.closed < (new Date().getTime() - MESSAGE_LOST_THRESHOLD_MS));
if (!doDeleteSession) {
var keysLeft = false;
for (var key in session) {
if (key != "indexInfo" && key != "oldRatchetList" && key != "currentRatchet") {
keysLeft = true;
break;
}
}
doDeleteSession = !keysLeft;
console.log((doDeleteSession ? "Deleting " : "Not deleting ") + "closed session which has not yet timed out");
} else
console.log("Deleting closed session due to timeout (created at " + session.indexInfo.closed + ")");
}
if (doDeleteSession)
delete sessions[getString(session.indexInfo.baseKey)];
else
sessions[getString(session.indexInfo.baseKey)] = session;
var openSessionRemaining = false;
for (var key in sessions)
if (sessions[key].indexInfo.closed == -1)
openSessionRemaining = true;
if (!openSessionRemaining)
try {
delete device['registrationId'];
} catch(_) {}
textsecure.storage.devices.saveDeviceObject(device);
}
var getSessions = function(encodedNumber) {
var device = textsecure.storage.devices.getDeviceObject(encodedNumber);
if (device === undefined || device.sessions === undefined)
return undefined;
return device.sessions;
}
crypto_storage.getOpenSession = function(encodedNumber) {
var sessions = getSessions(encodedNumber);
if (sessions === undefined)
return undefined;
for (var key in sessions)
if (sessions[key].indexInfo.closed == -1)
return sessions[key];
return undefined;
}
crypto_storage.getSessionByRemoteEphemeralKey = function(encodedNumber, remoteEphemeralKey) {
var sessions = getSessions(encodedNumber);
if (sessions === undefined)
return undefined;
var searchKey = getString(remoteEphemeralKey);
var openSession = undefined;
for (var key in sessions) {
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;
return undefined;
}
crypto_storage.getSessionOrIdentityKeyByBaseKey = function(encodedNumber, baseKey) {
var sessions = getSessions(encodedNumber);
var device = textsecure.storage.devices.getDeviceObject(encodedNumber);
if (device === undefined)
return undefined;
var preferredSession = device.sessions && device.sessions[getString(baseKey)];
if (preferredSession !== undefined)
return preferredSession;
if (device.identityKey !== undefined)
return { indexInfo: { remoteIdentityKey: device.identityKey } };
throw new Error("Datastore inconsistency: device was stored without identity key");
}
/*****************************
*** Internal Crypto stuff ***
*****************************/
var HKDF = function(input, salt, info) {
// HKDF for TextSecure has a bit of additional handling - salts always end up being 32 bytes
if (salt == '')
salt = new ArrayBuffer(32);
if (salt.byteLength != 32)
throw new Error("Got salt of incorrect length");
info = toArrayBuffer(info); // TODO: maybe convert calls?
return textsecure.crypto.HKDF(input, salt, info);
}
var verifyMAC = function(data, key, mac) {
return textsecure.crypto.sign(key, data).then(function(calculated_mac) {
if (!isEqual(calculated_mac, mac, true))
throw new Error("Bad MAC");
});
}
/******************************
*** Ratchet implementation ***
******************************/
var calculateRatchet = function(session, remoteKey, sending) {
var ratchet = session.currentRatchet;
return textsecure.crypto.ECDHE(remoteKey, toArrayBuffer(ratchet.ephemeralKeyPair.privKey)).then(function(sharedSecret) {
return HKDF(sharedSecret, toArrayBuffer(ratchet.rootKey), "WhisperRatchet").then(function(masterKey) {
if (sending)
session[getString(ratchet.ephemeralKeyPair.pubKey)] = { messageKeys: {}, chainKey: { counter: -1, key: masterKey[1] } };
else
session[getString(remoteKey)] = { messageKeys: {}, chainKey: { counter: -1, key: masterKey[1] } };
ratchet.rootKey = masterKey[0];
});
});
}
var initSession = function(isInitiator, ourEphemeralKey, ourSignedKey, encodedNumber, theirIdentityPubKey, theirEphemeralPubKey, theirSignedPubKey) {
var ourIdentityKey = crypto_storage.getIdentityKey();
if (isInitiator) {
if (ourSignedKey !== undefined)
throw new Error("Invalid call to initSession");
ourSignedKey = ourEphemeralKey;
} else {
if (theirSignedPubKey !== undefined)
throw new Error("Invalid call to initSession");
theirSignedPubKey = theirEphemeralPubKey;
}
var sharedSecret;
if (ourEphemeralKey === undefined || theirEphemeralPubKey === undefined)
sharedSecret = new Uint8Array(32 * 4);
else
sharedSecret = new Uint8Array(32 * 5);
for (var i = 0; i < 32; i++)
sharedSecret[i] = 0xff;
return textsecure.crypto.ECDHE(theirSignedPubKey, ourIdentityKey.privKey).then(function(ecRes1) {
function finishInit() {
return textsecure.crypto.ECDHE(theirSignedPubKey, ourSignedKey.privKey).then(function(ecRes) {
sharedSecret.set(new Uint8Array(ecRes), 32 * 3);
return HKDF(sharedSecret.buffer, '', "WhisperText").then(function(masterKey) {
var session = {currentRatchet: { rootKey: masterKey[0], lastRemoteEphemeralKey: theirSignedPubKey, previousCounter: 0 },
indexInfo: { remoteIdentityKey: theirIdentityPubKey, closed: -1 },
oldRatchetList: []
};
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 textsecure.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;
}
});
});
}
var promise;
if (ourEphemeralKey === undefined || theirEphemeralPubKey === undefined)
promise = Promise.resolve(new ArrayBuffer(0));
else
promise = textsecure.crypto.ECDHE(theirEphemeralPubKey, ourEphemeralKey.privKey);
return promise.then(function(ecRes4) {
sharedSecret.set(new Uint8Array(ecRes4), 32 * 4);
if (isInitiator)
return textsecure.crypto.ECDHE(theirIdentityPubKey, ourSignedKey.privKey).then(function(ecRes2) {
sharedSecret.set(new Uint8Array(ecRes1), 32);
sharedSecret.set(new Uint8Array(ecRes2), 32 * 2);
}).then(finishInit);
else
return textsecure.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) {
// 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
// and are otherwise added to the oldRatchetList, which we parse here and remove ratchets
// older than a week (we assume the message was lost and move on with our lives at that point)
var newList = [];
for (var i = 0; i < session.oldRatchetList.length; i++) {
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) && (session[ratchet].chainKey === undefined || session[ratchet].chainKey.key === undefined))
|| entry.added < new Date().getTime() - MESSAGE_LOST_THRESHOLD_MS) {
delete session[ratchet];
console.log("...deleted");
} else
newList[newList.length] = entry;
}
session.oldRatchetList = newList;
}
var closeSession = function(session, sessionClosedByRemote) {
if (session.indexInfo.closed > -1)
return;
// After this has run, we can still receive messages on ratchet chains which
// were already open (unless we know we dont need them),
// but we cannot send messages or step the ratchet
// Delete current sending ratchet
delete session[getString(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) {
if (!sessionClosedByRemote)
session.oldRatchetList[session.oldRatchetList.length] = { added: new Date().getTime(), ephemeralKey: i };
else
delete session[i].chainKey.key;
}
}
// Delete current root key and our ephemeral key pair to disallow ratchet stepping
delete session.currentRatchet['rootKey'];
delete session.currentRatchet['ephemeralKeyPair'];
session.indexInfo.closed = new Date().getTime();
removeOldChains(session);
}
self.closeOpenSessionForDevice = function(encodedNumber) {
var session = crypto_storage.getOpenSession(encodedNumber);
if (session === undefined)
return;
closeSession(session);
crypto_storage.saveSession(encodedNumber, session);
}
var initSessionFromPreKeyWhisperMessage;
var decryptWhisperMessage;
var handlePreKeyWhisperMessage = function(from, encodedMessage) {
var preKeyProto = textsecure.protobuf.PreKeyWhisperMessage.decode(encodedMessage, 'binary');
return initSessionFromPreKeyWhisperMessage(from, preKeyProto).then(function(sessions) {
return decryptWhisperMessage(from, getString(preKeyProto.message), sessions[0], preKeyProto.registrationId).then(function(result) {
if (sessions[1] !== undefined)
sessions[1]();
return result;
});
});
}
var wipeIdentityAndTryMessageAgain = function(from, encodedMessage) {
//TODO: Wipe identity key!
return handlePreKeyWhisperMessage(from, encodedMessage);
}
textsecure.replay.registerReplayFunction(wipeIdentityAndTryMessageAgain, textsecure.replay.REPLAY_FUNCS.INIT_SESSION);
initSessionFromPreKeyWhisperMessage = function(encodedNumber, message) {
var preKeyPair = crypto_storage.getStoredKeyPair("preKey" + message.preKeyId);
var signedPreKeyPair = crypto_storage.getStoredKeyPair("signedKey" + message.signedPreKeyId);
var session = crypto_storage.getSessionOrIdentityKeyByBaseKey(encodedNumber, toArrayBuffer(message.baseKey));
var open_session = crypto_storage.getOpenSession(encodedNumber);
if (signedPreKeyPair === undefined) {
// Session may or may not be the right 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, undefined]);
else
throw new Error("Missing Signed PreKey for PreKeyWhisperMessage");
}
if (session !== undefined) {
// Duplicate PreKeyMessage for session:
if (isEqual(session.indexInfo.baseKey, message.baseKey, false))
return Promise.resolve([session, undefined]);
// We already had a session/known identity key:
if (isEqual(session.indexInfo.remoteIdentityKey, message.identityKey, false)) {
// If the identity key matches the previous one, close the previous one and use the new one
if (open_session !== undefined)
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
throw new Error("Received message with unknown identity key", "The identity of the sender has changed. This may be malicious, or the sender may have simply reinstalled TextSecure.", textsecure.replay.REPLAY_FUNCS.INIT_SESSION, [encodedNumber, getString(message.encode())]);
}
}
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
return [new_session, function() {
if (open_session !== undefined)
crypto_storage.saveSession(encodedNumber, open_session);
crypto_storage.removeStoredKeyPair("preKey" + message.preKeyId);
}];
});;
}
var fillMessageKeys = function(chain, counter) {
if (chain.chainKey.counter + 1000 < counter) //TODO: maybe 1000 is too low/high in some cases?
return Promise.resolve(); // Stalker, much?
if (chain.chainKey.counter >= counter)
return Promise.resolve(); // Already calculated
if (chain.chainKey.key === undefined)
throw new Error("Got invalid request to extend chain after it was already closed");
var key = toArrayBuffer(chain.chainKey.key);
var byteArray = new Uint8Array(1);
byteArray[0] = 1;
return textsecure.crypto.sign(key, byteArray.buffer).then(function(mac) {
byteArray[0] = 2;
return textsecure.crypto.sign(key, byteArray.buffer).then(function(key) {
chain.messageKeys[chain.chainKey.counter + 1] = mac;
chain.chainKey.key = key
chain.chainKey.counter += 1;
return fillMessageKeys(chain, counter);
});
});
}
var maybeStepRatchet = function(session, remoteKey, previousCounter) {
if (session[getString(remoteKey)] !== undefined)
return Promise.resolve();
var ratchet = session.currentRatchet;
var finish = function() {
return calculateRatchet(session, remoteKey, false).then(function() {
// Now swap the ephemeral key and calculate the new sending chain
var previousRatchet = getString(ratchet.ephemeralKeyPair.pubKey);
if (session[previousRatchet] !== undefined) {
ratchet.previousCounter = session[previousRatchet].chainKey.counter;
delete session[previousRatchet];
}
return textsecure.crypto.createKeyPair().then(function(keyPair) {
ratchet.ephemeralKeyPair = keyPair;
return calculateRatchet(session, remoteKey, true).then(function() {
ratchet.lastRemoteEphemeralKey = remoteKey;
});
});
});
}
var previousRatchet = session[getString(ratchet.lastRemoteEphemeralKey)];
if (previousRatchet !== undefined) {
return fillMessageKeys(previousRatchet, previousCounter).then(function() {
delete previousRatchet.chainKey.key;
if (!objectContainsKeys(previousRatchet.messageKeys))
delete session[getString(ratchet.lastRemoteEphemeralKey)];
else
session.oldRatchetList[session.oldRatchetList.length] = { added: new Date().getTime(), ephemeralKey: ratchet.lastRemoteEphemeralKey };
}).then(finish);
} else
return finish();
}
// returns decrypted protobuf
decryptWhisperMessage = function(encodedNumber, messageBytes, session, registrationId) {
if (messageBytes[0] != String.fromCharCode((3 << 4) | 3))
throw new Error("Bad version number on WhisperMessage");
var messageProto = messageBytes.substring(1, messageBytes.length - 8);
var mac = messageBytes.substring(messageBytes.length - 8, messageBytes.length);
var message = textsecure.protobuf.WhisperMessage.decode(messageProto, 'binary');
var remoteEphemeralKey = toArrayBuffer(message.ephemeralKey);
if (session === undefined) {
var session = crypto_storage.getSessionByRemoteEphemeralKey(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() {
return HKDF(toArrayBuffer(chain.messageKeys[message.counter]), '', "WhisperMessageKeys").then(function(keys) {
delete chain.messageKeys[message.counter];
var messageProtoArray = toArrayBuffer(messageProto);
var macInput = new Uint8Array(messageProtoArray.byteLength + 33*2 + 1);
macInput.set(new Uint8Array(toArrayBuffer(session.indexInfo.remoteIdentityKey)));
macInput.set(new Uint8Array(toArrayBuffer(crypto_storage.getIdentityKey().pubKey)), 33);
macInput[33*2] = (3 << 4) | 3;
macInput.set(new Uint8Array(messageProtoArray), 33*2 + 1);
return verifyMAC(macInput.buffer, keys[1], mac).then(function() {
return window.textsecure.crypto.decrypt(keys[0], toArrayBuffer(message.ciphertext), keys[2].slice(0, 16))
.then(function(paddedPlaintext) {
paddedPlaintext = new Uint8Array(paddedPlaintext);
var plaintext;
for (var i = paddedPlaintext.length - 1; i >= 0; i--) {
if (paddedPlaintext[i] == 0x80) {
plaintext = new Uint8Array(i);
plaintext.set(paddedPlaintext.subarray(0, i));
plaintext = plaintext.buffer;
break;
} else if (paddedPlaintext[i] != 0x00)
throw new Error('Invalid padding');
}
delete session['pendingPreKey'];
var finalMessage = textsecure.protobuf.PushMessageContent.decode(plaintext);
if ((finalMessage.flags & textsecure.protobuf.PushMessageContent.Flags.END_SESSION)
== textsecure.protobuf.PushMessageContent.Flags.END_SESSION)
closeSession(session, true);
removeOldChains(session);
crypto_storage.saveSession(encodedNumber, session, registrationId);
return finalMessage;
});
});
});
});
});
}
/*************************
*** Public crypto API ***
*************************/
// Decrypts message into a raw string
self.decryptWebsocketMessage = function(message) {
var signaling_key = textsecure.storage.getEncrypted("signaling_key"); //TODO: in crypto_storage
var aes_key = toArrayBuffer(signaling_key.substring(0, 32));
var mac_key = toArrayBuffer(signaling_key.substring(32, 32 + 20));
var decodedMessage = base64DecToArr(getString(message));
if (new Uint8Array(decodedMessage)[0] != 1)
throw new Error("Got bad version number: " + decodedMessage[0]);
var iv = decodedMessage.slice(1, 1 + 16);
var ciphertext = decodedMessage.slice(1 + 16, decodedMessage.byteLength - 10);
var ivAndCiphertext = decodedMessage.slice(0, decodedMessage.byteLength - 10);
var mac = decodedMessage.slice(decodedMessage.byteLength - 10, decodedMessage.byteLength);
return verifyMAC(ivAndCiphertext, mac_key, mac).then(function() {
return window.textsecure.crypto.decrypt(aes_key, ciphertext, iv);
});
};
self.decryptAttachment = function(encryptedBin, keys) {
var aes_key = keys.slice(0, 32);
var mac_key = keys.slice(32, 64);
var iv = encryptedBin.slice(0, 16);
var ciphertext = encryptedBin.slice(16, encryptedBin.byteLength - 32);
var ivAndCiphertext = encryptedBin.slice(0, encryptedBin.byteLength - 32);
var mac = encryptedBin.slice(encryptedBin.byteLength - 32, encryptedBin.byteLength);
return verifyMAC(ivAndCiphertext, mac_key, mac).then(function() {
return window.textsecure.crypto.decrypt(aes_key, ciphertext, iv);
});
};
self.encryptAttachment = function(plaintext, keys, iv) {
var aes_key = keys.slice(0, 32);
var mac_key = keys.slice(32, 64);
return window.textsecure.crypto.encrypt(aes_key, plaintext, iv).then(function(ciphertext) {
var ivAndCiphertext = new Uint8Array(16 + ciphertext.byteLength);
ivAndCiphertext.set(new Uint8Array(iv));
ivAndCiphertext.set(new Uint8Array(ciphertext), 16);
return textsecure.crypto.sign(mac_key, ivAndCiphertext.buffer).then(function(mac) {
var encryptedBin = new Uint8Array(16 + ciphertext.byteLength + 32);
encryptedBin.set(ivAndCiphertext);
encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength);
return encryptedBin.buffer;
});
});
};
self.handleIncomingPushMessageProto = function(proto) {
switch(proto.type) {
case textsecure.protobuf.IncomingPushMessageSignal.Type.PLAINTEXT:
return Promise.resolve(textsecure.protobuf.PushMessageContent.decode(proto.message));
case textsecure.protobuf.IncomingPushMessageSignal.Type.CIPHERTEXT:
var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice);
return decryptWhisperMessage(from, getString(proto.message));
case textsecure.protobuf.IncomingPushMessageSignal.Type.PREKEY_BUNDLE:
if (proto.message.readUint8() != ((3 << 4) | 3))
throw new Error("Bad version byte");
var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice);
return handlePreKeyWhisperMessage(from, getString(proto.message));
case textsecure.protobuf.IncomingPushMessageSignal.Type.RECEIPT:
return Promise.resolve(null);
default:
return new Promise(function(resolve, reject) { reject(new Error("Unknown message type")); });
}
}
// return Promise(encoded [PreKey]WhisperMessage)
self.encryptMessageFor = function(deviceObject, pushMessageContent) {
var session = crypto_storage.getOpenSession(deviceObject.encodedNumber);
var doEncryptPushMessageContent = function() {
var msg = new textsecure.protobuf.WhisperMessage();
var plaintext = toArrayBuffer(pushMessageContent.encode());
var paddedPlaintext = new Uint8Array(Math.ceil((plaintext.byteLength + 1) / 160.0) * 160);
paddedPlaintext.set(new Uint8Array(plaintext));
paddedPlaintext[plaintext.byteLength] = 0x80;
msg.ephemeralKey = toArrayBuffer(session.currentRatchet.ephemeralKeyPair.pubKey);
var chain = session[getString(msg.ephemeralKey)];
return fillMessageKeys(chain, chain.chainKey.counter + 1).then(function() {
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 window.textsecure.crypto.encrypt(keys[0], paddedPlaintext.buffer, keys[2].slice(0, 16)).then(function(ciphertext) {
msg.ciphertext = ciphertext;
var encodedMsg = toArrayBuffer(msg.encode());
var macInput = new Uint8Array(encodedMsg.byteLength + 33*2 + 1);
macInput.set(new Uint8Array(toArrayBuffer(crypto_storage.getIdentityKey().pubKey)));
macInput.set(new Uint8Array(toArrayBuffer(session.indexInfo.remoteIdentityKey)), 33);
macInput[33*2] = (3 << 4) | 3;
macInput.set(new Uint8Array(encodedMsg), 33*2 + 1);
return textsecure.crypto.sign(keys[1], macInput.buffer).then(function(mac) {
var result = new Uint8Array(encodedMsg.byteLength + 9);
result[0] = (3 << 4) | 3;
result.set(new Uint8Array(encodedMsg), 1);
result.set(new Uint8Array(mac, 0, 8), encodedMsg.byteLength + 1);
try {
delete deviceObject['signedKey'];
delete deviceObject['signedKeyId'];
delete deviceObject['preKey'];
delete deviceObject['preKeyId'];
} catch(_) {}
removeOldChains(session);
crypto_storage.saveSessionAndDevice(deviceObject, session);
return result;
});
});
});
});
}
var preKeyMsg = new textsecure.protobuf.PreKeyWhisperMessage();
preKeyMsg.identityKey = toArrayBuffer(crypto_storage.getIdentityKey().pubKey);
preKeyMsg.registrationId = textsecure.storage.getUnencrypted("registrationId");
if (session === undefined) {
return textsecure.crypto.createKeyPair().then(function(baseKey) {
preKeyMsg.preKeyId = deviceObject.preKeyId;
preKeyMsg.signedPreKeyId = deviceObject.signedKeyId;
preKeyMsg.baseKey = toArrayBuffer(baseKey.pubKey);
return initSession(true, baseKey, undefined, deviceObject.encodedNumber,
toArrayBuffer(deviceObject.identityKey), toArrayBuffer(deviceObject.preKey), toArrayBuffer(deviceObject.signedKey))
.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) + getString(preKeyMsg.encode());
return {type: 3, body: result};
});
});
});
} else
return doEncryptPushMessageContent().then(function(message) {
if (session.pendingPreKey !== undefined) {
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) + getString(preKeyMsg.encode());
return {type: 3, body: result};
} else
return {type: 1, body: getString(message)};
});
}
var GENERATE_KEYS_KEYS_GENERATED = 100;
self.generateKeys = function() {
var identityKeyPair = crypto_storage.getIdentityKey();
var identityKeyCalculated = function(identityKeyPair) {
var firstPreKeyId = textsecure.storage.getEncrypted("maxPreKeyId", 0);
textsecure.storage.putEncrypted("maxPreKeyId", firstPreKeyId + GENERATE_KEYS_KEYS_GENERATED);
var signedKeyId = textsecure.storage.getEncrypted("signedKeyId", 0);
textsecure.storage.putEncrypted("signedKeyId", signedKeyId + 1);
var keys = {};
keys.identityKey = identityKeyPair.pubKey;
keys.preKeys = [];
var generateKey = function(keyId) {
return crypto_storage.getNewStoredKeyPair("preKey" + keyId, false).then(function(keyPair) {
keys.preKeys[keyId] = {keyId: keyId, publicKey: keyPair.pubKey};
});
};
var promises = [];
for (var i = firstPreKeyId; i < firstPreKeyId + GENERATE_KEYS_KEYS_GENERATED; i++)
promises[i] = generateKey(i);
promises[firstPreKeyId + GENERATE_KEYS_KEYS_GENERATED] = crypto_storage.getNewStoredKeyPair("signedKey" + signedKeyId).then(function(keyPair) {
return textsecure.crypto.Ed25519Sign(identityKeyPair.privKey, keyPair.pubKey).then(function(sig) {
keys.signedPreKey = {keyId: signedKeyId, publicKey: keyPair.pubKey, signature: sig};
});
});
//TODO: Process by date added and agressively call generateKeys when we get near maxPreKeyId in a message
crypto_storage.removeStoredKeyPair("signedKey" + (signedKeyId - 2));
return Promise.all(promises).then(function() {
return keys;
});
}
if (identityKeyPair === undefined)
return crypto_storage.getNewStoredKeyPair("identityKey", true).then(function(keyPair) { return identityKeyCalculated(keyPair); });
else
return identityKeyCalculated(identityKeyPair);
}
window.textsecure.registerOnLoadFunction(function() {
//TODO: Dont always update prekeys here
if (textsecure.storage.getEncrypted("lastSignedKeyUpdate", Date.now()) < Date.now() - MESSAGE_LOST_THRESHOLD_MS)
self.generateKeys();
});
self.prepareTempWebsocket = function() {
var socketInfo = {};
var keyPair;
socketInfo.decryptAndHandleDeviceInit = function(deviceInit) {
var masterEphemeral = toArrayBuffer(deviceInit.masterEphemeralPubKey);
var message = toArrayBuffer(deviceInit.identityKeyMessage);
return textsecure.crypto.ECDHE(masterEphemeral, keyPair.privKey).then(function(ecRes) {
return HKDF(ecRes, masterEphemeral, "WhisperDeviceInit").then(function(keys) {
if (new Uint8Array(message)[0] != (3 << 4) | 3)
throw new Error("Bad version number on IdentityKeyMessage");
var iv = message.slice(1, 16 + 1);
var mac = message.slice(message.length - 32, message.length);
var ivAndCiphertext = message.slice(0, message.length - 32);
var ciphertext = message.slice(16 + 1, message.length - 32);
return verifyMAC(ivAndCiphertext, ecRes[1], mac).then(function() {
window.textsecure.crypto.decrypt(ecRes[0], ciphertext, iv).then(function(plaintext) {
var identityKeyMsg = textsecure.protobuf.IdentityKey.decode(plaintext);
textsecure.crypto.createKeyPair(toArrayBuffer(identityKeyMsg.identityKey)).then(function(identityKeyPair) {
crypto_storage.putKeyPair("identityKey", identityKeyPair);
identityKeyMsg.identityKey = null;
return identityKeyMsg;
});
});
});
});
});
}
return textsecure.crypto.createKeyPair().then(function(newKeyPair) {
keyPair = newKeyPair;
socketInfo.pubKey = keyPair.pubKey;
return socketInfo;
});
}
return self;
}();
})();

View file

@ -50,7 +50,7 @@ window.textsecure.messaging = function() {
return new Promise(function() { throw new Error("Mismatched relays for number " + number); });
}
return textsecure.crypto.encryptMessageFor(deviceObjectList[i], message).then(function(encryptedMsg) {
return textsecure.protocol.encryptMessageFor(deviceObjectList[i], message).then(function(encryptedMsg) {
jsonData[i] = {
type: encryptedMsg.type,
destinationDeviceId: textsecure.utils.unencodeNumber(deviceObjectList[i].encodedNumber)[1],
@ -198,7 +198,7 @@ window.textsecure.messaging = function() {
proto.key = textsecure.crypto.getRandomBytes(64);
var iv = textsecure.crypto.getRandomBytes(16);
return textsecure.crypto.encryptAttachment(attachment.data, proto.key, iv).then(function(encryptedBin) {
return textsecure.protocol.encryptAttachment(attachment.data, proto.key, iv).then(function(encryptedBin) {
return textsecure.api.putAttachment(encryptedBin).then(function(id) {
proto.id = id;
proto.contentType = attachment.contentType;
@ -252,7 +252,7 @@ window.textsecure.messaging = function() {
return sendIndividualProto(number, proto).then(function(res) {
var devices = textsecure.storage.devices.getDeviceObjectsForNumber(number);
for (var i in devices)
textsecure.crypto.closeOpenSessionForDevice(devices[i].encodedNumber);
textsecure.protocol.closeOpenSessionForDevice(devices[i].encodedNumber);
return res;
});

View file

@ -89,22 +89,4 @@
};
})();
} // if !window.crypto.subtle
window.textsecure.subtle = {
encrypt: function(key, data, iv) {
return window.crypto.subtle.importKey('raw', key, {name: 'AES-CBC'}, false, ['encrypt']).then(function(key) {
return window.crypto.subtle.encrypt({name: 'AES-CBC', iv: new Uint8Array(iv)}, key, data);
});
},
decrypt: function(key, data, iv) {
return window.crypto.subtle.importKey('raw', key, {name: 'AES-CBC'}, false, ['decrypt']).then(function(key) {
return window.crypto.subtle.decrypt({name: 'AES-CBC', iv: new Uint8Array(iv)}, key, data);
});
},
sign: function(key, data) {
return window.crypto.subtle.importKey('raw', key, {name: 'HMAC', hash: {name: 'SHA-256'}}, false, ['sign']).then(function(key) {
return window.crypto.subtle.sign( {name: 'HMAC', hash: 'SHA-256'}, key, data);
});
},
};
})();

View file

@ -96,6 +96,7 @@
<script type="text/javascript" src="js/components.js"></script>
<script type="text/javascript" src="js/protobufs.js"></script>
<script type="text/javascript" src="js/nativeclient.js"></script>
<script type="text/javascript" src="js/helpers.js"></script>
<script type="text/javascript" src="js/storage.js"></script>
<script type="text/javascript" src="js/storage/devices.js"></script>
@ -103,6 +104,7 @@
<script type="text/javascript" src="js/libphonenumber-util.js"></script>
<script type="text/javascript" src="js/webcrypto.js"></script>
<script type="text/javascript" src="js/crypto.js"></script>
<script type="text/javascript" src="js/protocol.js"></script>
<script type="text/javascript" src="js/models/messages.js"></script>
<script type="text/javascript" src="js/models/threads.js"></script>
<script type="text/javascript" src="js/api.js"></script>

View file

@ -1,2 +1,43 @@
/*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
mocha.setup("bdd");
window.assert = chai.assert;
/*
* global helpers for tests
*/
function assertEqualArrayBuffers(ab1, ab2) {
assert.deepEqual(new Uint8Array(ab1), new Uint8Array(ab2));
};
function arrayBufferToHex(buffer) {
var array = new Uint8Array(buffer);
var s = '';
for (var i in array) {
var h = array[i].toString(16);
if (h.length < 2) { s += '0'; }
s += h;
}
return s;
};
function hexToArrayBuffer(str) {
var ret = new ArrayBuffer(str.length / 2);
var array = new Uint8Array(ret);
for (var i = 0; i < str.length/2; i++)
array[i] = parseInt(str.substr(i*2, 2), 16);
return ret;
};

View file

@ -14,8 +14,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
mocha.setup("bdd");
window.assert = chai.assert;
'use strict';
describe("ArrayBuffer->String conversion", function() {
it('works', function() {
@ -35,7 +34,7 @@ describe("Cryptographic primitives", function() {
var iv = hexToArrayBuffer('000102030405060708090a0b0c0d0e0f');
var plaintext = hexToArrayBuffer('6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710');
var ciphertext = hexToArrayBuffer('f58c4c04d6e5f1ba779eabfb5f7bfbd69cfc4e967edb808d679f777bc6702c7d39f23369a9d9bacfa530e26304231461b2eb05e2c39be9fcda6c19078c6a9d1b3f461796d6b0d6b2e0c2a72b4d80e644');
window.textsecure.subtle.encrypt(key, plaintext, iv).then(function(result) {
window.textsecure.crypto.encrypt(key, plaintext, iv).then(function(result) {
assert.strictEqual(getString(result), getString(ciphertext));
}).then(done).catch(done);
});
@ -47,7 +46,7 @@ describe("Cryptographic primitives", function() {
var iv = hexToArrayBuffer('000102030405060708090a0b0c0d0e0f');
var plaintext = hexToArrayBuffer('6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710');
var ciphertext = hexToArrayBuffer('f58c4c04d6e5f1ba779eabfb5f7bfbd69cfc4e967edb808d679f777bc6702c7d39f23369a9d9bacfa530e26304231461b2eb05e2c39be9fcda6c19078c6a9d1b3f461796d6b0d6b2e0c2a72b4d80e644');
window.textsecure.subtle.decrypt(key, ciphertext, iv).then(function(result) {
window.textsecure.crypto.decrypt(key, ciphertext, iv).then(function(result) {
assert.strictEqual(getString(result), getString(plaintext));
}).then(done).catch(done);
});
@ -58,7 +57,7 @@ describe("Cryptographic primitives", function() {
var key = hexToArrayBuffer('6f35628d65813435534b5d67fbdb54cb33403d04e843103e6399f806cb5df95febbdd61236f33245');
var input = hexToArrayBuffer('752cff52e4b90768558e5369e75d97c69643509a5e5904e0a386cbe4d0970ef73f918f675945a9aefe26daea27587e8dc909dd56fd0468805f834039b345f855cfe19c44b55af241fff3ffcd8045cd5c288e6c4e284c3720570b58e4d47b8feeedc52fd1401f698a209fccfa3b4c0d9a797b046a2759f82a54c41ccd7b5f592b');
var mac = getString(hexToArrayBuffer('05d1243e6465ed9620c9aec1c351a186'));
window.textsecure.subtle.sign(key, input).then(function(result) {
window.textsecure.crypto.sign(key, input).then(function(result) {
assert.strictEqual(getString(result).substring(0, mac.length), mac);
}).then(done).catch(done);
});
@ -79,7 +78,7 @@ describe("Cryptographic primitives", function() {
for (var i = 0; i < 10; i++)
info[i] = 240 + i;
return textsecure.crypto.testing_only.HKDF(IKM.buffer, salt.buffer, info.buffer).then(function(OKM){
return textsecure.crypto.HKDF(IKM.buffer, salt.buffer, info.buffer).then(function(OKM){
var T1 = hexToArrayBuffer("3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf");
var T2 = hexToArrayBuffer("34007208d5b887185865");
assert.equal(getString(OKM[0]), getString(T1));
@ -109,7 +108,7 @@ describe('Unencrypted PushMessageProto "decrypt"', function() {
message: text_message.encode()
};
return textsecure.crypto.handleIncomingPushMessageProto(server_message).
return textsecure.protocol.handleIncomingPushMessageProto(server_message).
then(function(message) {
assert.equal(message.body, text_message.body);
assert.equal(message.attachments.length, text_message.attachments.length);
@ -122,8 +121,8 @@ describe("Curve25519", function() {
describe("Implementation", function() {
// this is a just cute little trick to get a nice-looking note about
// which curve25519 impl we're using.
if (window.textsecure.nacl.USE_NACL) {
it("is NACL", function(done) { done(); });
if (window.textsecure.NATIVE_CLIENT) {
it("is Native Client", function(done) { done(); });
} else {
it("is JavaScript", function(done) { done(); });
}
@ -139,26 +138,26 @@ describe("Curve25519", function() {
var bob_pub = hexToArrayBuffer("05de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f");
var shared_sec = hexToArrayBuffer("4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742");
return textsecure.crypto.testing_only.privToPub(alice_priv, true).then(function(aliceKeyPair) {
return textsecure.crypto.createKeyPair(alice_priv).then(function(aliceKeyPair) {
var target = new Uint8Array(alice_priv.slice(0));
target[0] &= 248;
target[31] &= 127;
target[31] |= 64;
assert.equal(getString(aliceKeyPair.pubKey), getString(alice_pub));
assert.equal(getString(aliceKeyPair.privKey), getString(target));
return textsecure.crypto.testing_only.privToPub(bob_priv, true).then(function(bobKeyPair) {
return textsecure.crypto.createKeyPair(bob_priv).then(function(bobKeyPair) {
var target = new Uint8Array(bob_priv.slice(0));
target[0] &= 248;
target[31] &= 127;
target[31] |= 64;
assert.equal(getString(bobKeyPair.privKey), getString(target));
assert.equal(getString(aliceKeyPair.pubKey), getString(alice_pub));
assert.equal(getString(bobKeyPair.pubKey), getString(bob_pub));
return textsecure.crypto.testing_only.ECDHE(bobKeyPair.pubKey, aliceKeyPair.privKey).then(function(ss) {
return textsecure.crypto.ECDHE(bobKeyPair.pubKey, aliceKeyPair.privKey).then(function(ss) {
assert.equal(getString(ss), getString(shared_sec));
return textsecure.crypto.testing_only.ECDHE(aliceKeyPair.pubKey, bobKeyPair.privKey).then(function(ss) {
return textsecure.crypto.ECDHE(aliceKeyPair.pubKey, bobKeyPair.privKey).then(function(ss) {
assert.equal(getString(ss), getString(shared_sec));
});
});
@ -178,14 +177,14 @@ describe("Curve25519", function() {
var sig = hexToArrayBuffer("2bc06c745acb8bae10fbc607ee306084d0c28e2b3bb819133392473431291fd0"+
"dfa9c7f11479996cf520730d2901267387e08d85bbf2af941590e3035a545285");
return textsecure.crypto.testing_only.privToPub(priv, false).then(function(pubCalc) {
return textsecure.crypto.createKeyPair(priv).then(function(pubCalc) {
//if (getString(pub) != getString(pubCalc))
// return false;
return textsecure.crypto.testing_only.Ed25519Sign(priv, msg).then(function(sigCalc) {
return textsecure.crypto.Ed25519Sign(priv, msg).then(function(sigCalc) {
assert.equal(getString(sig), getString(sigCalc));
return textsecure.crypto.testing_only.Ed25519Verify(pub, msg, sig);
return textsecure.crypto.Ed25519Verify(pub, msg, sig);
});
});
}).then(done).catch(done);
@ -199,14 +198,14 @@ describe("Curve25519", function() {
it ('works', function(done) {
localStorage.clear();
return textsecure.registerOnLoadFunction(function() {
return textsecure.crypto.generateKeys().then(function() {
return textsecure.protocol.generateKeys().then(function() {
assert.isDefined(textsecure.storage.getEncrypted("25519KeyidentityKey"));
assert.isDefined(textsecure.storage.getEncrypted("25519KeysignedKey0"));
for (var i = 0; i < 100; i++) {
assert.isDefined(textsecure.storage.getEncrypted("25519KeypreKey" + i));
}
var origIdentityKey = getString(textsecure.storage.getEncrypted("25519KeyidentityKey").privKey);
return textsecure.crypto.generateKeys().then(function() {
return textsecure.protocol.generateKeys().then(function() {
assert.isDefined(textsecure.storage.getEncrypted("25519KeyidentityKey"));
assert.equal(getString(textsecure.storage.getEncrypted("25519KeyidentityKey").privKey), origIdentityKey);
@ -217,7 +216,7 @@ describe("Curve25519", function() {
assert.isDefined(textsecure.storage.getEncrypted("25519KeypreKey" + i));
}
return textsecure.crypto.generateKeys().then(function() {
return textsecure.protocol.generateKeys().then(function() {
assert.isDefined(textsecure.storage.getEncrypted("25519KeyidentityKey"));
assert.equal(getString(textsecure.storage.getEncrypted("25519KeyidentityKey").privKey), origIdentityKey);
@ -259,7 +258,7 @@ describe("Axolotl", function() {
throw new Error('Out of private keys');
else {
var privKey = privKeyQueue.shift();
return textsecure.crypto.testing_only.privToPub(privKey, false).then(function(keyPair) {
return textsecure.crypto.createKeyPair(privKey).then(function(keyPair) {
var a = btoa(getString(keyPair.privKey)); var b = btoa(getString(privKey));
if (getString(keyPair.privKey) != getString(privKey))
throw new Error('Failed to rederive private key!');
@ -286,7 +285,7 @@ describe("Axolotl", function() {
message.sourceDevice = 1;
try {
var proto = textsecure.protobuf.IncomingPushMessageSignal.decode(message.encode());
return textsecure.crypto.handleIncomingPushMessageProto(proto).then(function(res) {
return textsecure.protocol.handleIncomingPushMessageProto(proto).then(function(res) {
if (data.expectTerminateSession)
return res.flags == textsecure.protobuf.PushMessageContent.Flags.END_SESSION;
return res.body == data.expectedSmsText;
@ -303,13 +302,13 @@ describe("Axolotl", function() {
}
if (data.ourIdentityKey !== undefined)
return textsecure.crypto.testing_only.privToPub(data.ourIdentityKey, true).then(function(keyPair) {
return textsecure.crypto.createKeyPair(data.ourIdentityKey).then(function(keyPair) {
textsecure.storage.putEncrypted("25519KeyidentityKey", keyPair);
return textsecure.crypto.testing_only.privToPub(data.ourSignedPreKey, false).then(function(keyPair) {
return textsecure.crypto.createKeyPair(data.ourSignedPreKey).then(function(keyPair) {
textsecure.storage.putEncrypted("25519KeysignedKey" + data.signedPreKeyId, keyPair);
if (data.ourPreKey !== undefined)
return textsecure.crypto.testing_only.privToPub(data.ourPreKey, false).then(function(keyPair) {
return textsecure.crypto.createKeyPair(data.ourPreKey).then(function(keyPair) {
textsecure.storage.putEncrypted("25519KeypreKey" + data.preKeyId, keyPair);
return postLocalKeySetup();
});
@ -356,7 +355,7 @@ describe("Axolotl", function() {
privKeyQueue.push(data.ourEphemeralKey);
if (data.ourIdentityKey !== undefined)
return textsecure.crypto.testing_only.privToPub(data.ourIdentityKey, true).then(function(keyPair) {
return textsecure.crypto.createKeyPair(data.ourIdentityKey).then(function(keyPair) {
textsecure.storage.putEncrypted("25519KeyidentityKey", keyPair);
return postLocalKeySetup();
});

View file

@ -127,6 +127,7 @@
<script type="text/javascript" src="../js-deps/curve255.js"></script>
<script type="text/javascript" src="../js/components.js"></script>
<script type="text/javascript" src="../js/nativeclient.js"></script>
<script type="text/javascript" src="../js/protobufs.js" data-cover></script>
<script type="text/javascript" src="../js/helpers.js" data-cover></script>
<script type="text/javascript" src="../js/storage.js"></script>
@ -134,7 +135,8 @@
<script type="text/javascript" src="../js/storage/groups.js"></script>
<script type="text/javascript" src="../js/libphonenumber-util.js"></script>
<script type="text/javascript" src="../js/webcrypto.js"></script>
<script type="text/javascript" src="../js/crypto.js" data-cover></script>
<script type="text/javascript" src="../js/crypto.js"></script>
<script type="text/javascript" src="../js/protocol.js" data-cover></script>
<script type="text/javascript" src="../js/models/messages.js"></script>
<script type="text/javascript" src="../js/models/threads.js"></script>
<script type="text/javascript" src="../js/api.js"></script>
@ -155,6 +157,7 @@
<script type="text/javascript" src="fake_api.js"></script>
<script type="text/javascript" src="testvectors.js"></script>
<script type="text/javascript" src="crypto_test.js"></script>
<script type="text/javascript" src="nativeclient_test.js"></script>
<script type="text/javascript" src="views/message_view_test.js"></script>
<script type="text/javascript" src="views/list_view_test.js"></script>
<script type="text/javascript" src="views/threads_test.js"></script>

96
test/nativeclient_test.js Normal file
View file

@ -0,0 +1,96 @@
/* vim: ts=4:sw=4
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';
describe("textsecure.nativeclient", function() {
var alice_bytes = hexToArrayBuffer("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a");
var alice_priv = hexToArrayBuffer("70076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c6a");
var alice_pub = hexToArrayBuffer("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a");
var bob_bytes = hexToArrayBuffer("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb");
var bob_priv = hexToArrayBuffer("58ab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e06b");
var bob_pub = hexToArrayBuffer("de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f");
var shared_sec = hexToArrayBuffer("4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742");
describe("privToPub", function() {
it ('converts alice private keys to a keypair', function(done) {
textsecure.nativeclient.privToPub(alice_bytes).then(function(keypair) {
assertEqualArrayBuffers(keypair.privKey, alice_priv);
assertEqualArrayBuffers(keypair.pubKey, alice_pub);
done();
}).catch(done);
});
it ('converts bob private keys to a keypair', function(done) {
textsecure.nativeclient.privToPub(bob_bytes).then(function(keypair) {
assertEqualArrayBuffers(keypair.privKey, bob_priv);
assertEqualArrayBuffers(keypair.pubKey, bob_pub);
done();
}).catch(done);
});
});
describe("ECDHE", function() {
it("computes the shared secret for alice", function(done) {
textsecure.nativeclient.ECDHE(bob_pub, alice_priv).then(function(secret) {
assertEqualArrayBuffers(shared_sec, secret);
done();
}).catch(done);
});
it("computes the shared secret for bob", function(done) {
textsecure.nativeclient.ECDHE(alice_pub, bob_priv).then(function(secret) {
assertEqualArrayBuffers(shared_sec, secret);
done();
}).catch(done);
});
});
var priv = hexToArrayBuffer("48a8892cc4e49124b7b57d94fa15becfce071830d6449004685e387c62409973");
var pub = hexToArrayBuffer("55f1bfede27b6a03e0dd389478ffb01462e5c52dbbac32cf870f00af1ed9af3a");
var msg = hexToArrayBuffer("617364666173646661736466");
var sig = hexToArrayBuffer("2bc06c745acb8bae10fbc607ee306084d0c28e2b3bb819133392473431291fd0dfa9c7f11479996cf520730d2901267387e08d85bbf2af941590e3035a545285");
describe("Ed25519Sign", function() {
it("computes the signature", function(done) {
textsecure.nativeclient.Ed25519Sign(priv, msg).then(function(signature) {
assertEqualArrayBuffers(sig, signature);
done();
}).catch(done);
});
});
describe("Ed25519Verify", function() {
it("throws on bad signature", function(done) {
var badsig = sig.slice(0);
new Uint8Array(badsig).set([0], 0);
textsecure.nativeclient.Ed25519Verify(pub, msg, badsig).catch(function(e) {
if (e.message === 'Invalid signature') {
done();
} else { throw e; }
}).catch(done);
});
it("does not throw on good signature", function(done) {
textsecure.nativeclient.Ed25519Verify(pub, msg, sig).then(done).catch(done);
});
});
describe("registerOnLoadFunction", function() {
it('queues a callback til native client is loaded', function(done) {
textsecure.nativeclient.registerOnLoadFunction(done);
});
});
});

View file

@ -10872,5 +10872,46 @@ require.alias("chai/index.js", "chai/index.js");if (typeof exports == "object")
} else {
this["chai"] = require("chai");
}})();
/*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
mocha.setup("bdd");
window.assert = chai.assert;
/*
* global helpers for tests
*/
function assertEqualArrayBuffers(ab1, ab2) {
assert.deepEqual(new Uint8Array(ab1), new Uint8Array(ab2));
};
function arrayBufferToHex(buffer) {
var array = new Uint8Array(buffer);
var s = '';
for (var i in array) {
var h = array[i].toString(16);
if (h.length < 2) { s += '0'; }
s += h;
}
return s;
};
function hexToArrayBuffer(str) {
var ret = new ArrayBuffer(str.length / 2);
var array = new Uint8Array(ret);
for (var i = 0; i < str.length/2; i++)
array[i] = parseInt(str.substr(i*2, 2), 16);
return ret;
};

View file

@ -2,13 +2,6 @@
// Distributed under the X11 software license, see the accompanying
// file MIT
function hexToArrayBuffer(str) {
var ret = new ArrayBuffer(str.length / 2);
var array = new Uint8Array(ret);
for (var i = 0; i < str.length/2; i++)
array[i] = parseInt(str.substr(i*2, 2), 16);
return ret;
}
axolotlTestVectors = function() {
// We're gonna throw the finalized tests in here:
var tests = [];