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:
parent
cd4b98d426
commit
9f676af9bb
16 changed files with 1255 additions and 1050 deletions
|
@ -21,6 +21,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>
|
||||
|
@ -28,6 +29,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>
|
||||
|
|
|
@ -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>
|
||||
|
|
985
js/crypto.js
985
js/crypto.js
File diff suppressed because it is too large
Load diff
|
@ -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) {
|
||||
if (!textsecure.NATIVE_CLIENT) {
|
||||
window.textsecure.registerOnLoadFunction = window.textsecure.nativeclient.registerOnLoadFunction;
|
||||
} else {
|
||||
window.textsecure.registerOnLoadFunction = function(func) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (naclLoaded || !self.USE_NACL)
|
||||
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
85
js/nativeclient.js
Normal 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 ];
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
|
@ -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
807
js/protocol.js
Normal 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;
|
||||
}();
|
||||
|
||||
})();
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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
96
test/nativeclient_test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
41
test/test.js
41
test/test.js
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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 = [];
|
||||
|
|
Loading…
Reference in a new issue