v3 steps
This commit is contained in:
parent
66bf371aa7
commit
de83429962
5 changed files with 168 additions and 98 deletions
50
js/api.js
50
js/api.js
|
@ -22,8 +22,8 @@ window.textsecure.api = function() {
|
||||||
/************************************************
|
/************************************************
|
||||||
*** Utilities to communicate with the server ***
|
*** Utilities to communicate with the server ***
|
||||||
************************************************/
|
************************************************/
|
||||||
// WARNING: THIS SERVER LOGS KEY MATERIAL FOR TESTING
|
// Staging server
|
||||||
var URL_BASE = "http://sushiforeveryone.bluematt.me";
|
var URL_BASE = "https://textsecure-service-ca.whispersystems.org:4433";
|
||||||
|
|
||||||
// This is the real server
|
// This is the real server
|
||||||
//var URL_BASE = "https://textsecure-service.whispersystems.org";
|
//var URL_BASE = "https://textsecure-service.whispersystems.org";
|
||||||
|
@ -139,12 +139,21 @@ window.textsecure.api = function() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
self.registerKeys = function(keys) {
|
self.registerKeys = function(genKeys) {
|
||||||
//TODO: Do this conversion somewhere else?
|
var keys = {};
|
||||||
var identityKey = btoa(getString(keys.keys[0].identityKey));
|
keys.identityKey = btoa(getString(genKeys.identityKey));
|
||||||
for (var i = 0; i < keys.keys.length; i++)
|
keys.signedPreKey = {keyId: genKeys.signedPreKey.keyId, publicKey: btoa(getString(genKeys.signedPreKey.publicKey)),
|
||||||
keys.keys[i] = {keyId: i, publicKey: btoa(getString(keys.keys[i].publicKey)), identityKey: identityKey};
|
signature: btoa(getString(genKeys.signedPreKey.signature))};
|
||||||
keys.lastResortKey = {keyId: keys.lastResortKey.keyId, publicKey: btoa(getString(keys.lastResortKey.publicKey)), identityKey: identityKey};
|
|
||||||
|
keys.preKeys = [];
|
||||||
|
var j = 0;
|
||||||
|
for (i in genKeys.preKeys)
|
||||||
|
keys.preKeys[j++] = {keyId: i, publicKey: btoa(getString(genKeys.preKeys[i].publicKey))};
|
||||||
|
|
||||||
|
//TODO: This is just to make the server happy (v2 clients should choke on publicKey),
|
||||||
|
// it needs removed before release
|
||||||
|
keys.lastResortKey = {keyId: 0x7fffFFFF, publicKey: btoa("42")};
|
||||||
|
|
||||||
return doAjax({
|
return doAjax({
|
||||||
call : 'keys',
|
call : 'keys',
|
||||||
httpType : 'PUT',
|
httpType : 'PUT',
|
||||||
|
@ -159,16 +168,21 @@ window.textsecure.api = function() {
|
||||||
httpType : 'GET',
|
httpType : 'GET',
|
||||||
do_auth : true,
|
do_auth : true,
|
||||||
urlParameters : "/" + number + "/*",
|
urlParameters : "/" + number + "/*",
|
||||||
}).then(function(response) {
|
}).then(function(res) {
|
||||||
//TODO: Do this conversion somewhere else?
|
var promises = [];
|
||||||
var res = response.keys;
|
res.identityKey = base64DecToArr(res.identityKey);
|
||||||
for (var i = 0; i < res.length; i++) {
|
for (var i = 0; i < res.devices.length; i++) {
|
||||||
res[i].identityKey = base64DecToArr(res[i].identityKey);
|
res.devices[i].signedPreKey.publicKey = base64DecToArr(res.devices[i].signedPreKey.publicKey);
|
||||||
res[i].publicKey = base64DecToArr(res[i].publicKey);
|
res.devices[i].signedPreKey.signature = base64DecToArr(res.devices[i].signedPreKey.signature);
|
||||||
if (res[i].keyId === undefined)
|
promises[i] = window.textsecure.crypto.Ed25519Verify(res.identityKey, res.devices[i].signedPreKey.publicKey, res.devices[i].signedPreKey.signature);
|
||||||
res[i].keyId = 0;
|
res.devices[i].preKey.publicKey = base64DecToArr(res.devices[i].preKey.publicKey);
|
||||||
|
//TODO: Is this still needed?
|
||||||
|
//if (res.devices[i].keyId === undefined)
|
||||||
|
// res.devices[i].keyId = 0;
|
||||||
}
|
}
|
||||||
return res;
|
return Promise.all(promises).then(function() {
|
||||||
|
return res;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -265,7 +279,7 @@ window.textsecure.api = function() {
|
||||||
var password = textsecure.storage.getEncrypted("password");
|
var password = textsecure.storage.getEncrypted("password");
|
||||||
var URL = URL_BASE.replace(/^http/g, 'ws') + URL_CALLS['push'] + '/?';
|
var URL = URL_BASE.replace(/^http/g, 'ws') + URL_CALLS['push'] + '/?';
|
||||||
var params = $.param({
|
var params = $.param({
|
||||||
user: '+' + getString(user).substring(1),
|
login: '+' + getString(user).substring(1),
|
||||||
password: getString(password)
|
password: getString(password)
|
||||||
});
|
});
|
||||||
return new WebSocket(URL+params);
|
return new WebSocket(URL+params);
|
||||||
|
|
149
js/crypto.js
149
js/crypto.js
|
@ -128,14 +128,8 @@ window.textsecure.crypto = function() {
|
||||||
return { pubKey: toArrayBuffer(res.pubKey), privKey: toArrayBuffer(res.privKey) };
|
return { pubKey: toArrayBuffer(res.pubKey), privKey: toArrayBuffer(res.privKey) };
|
||||||
}
|
}
|
||||||
|
|
||||||
crypto_storage.getAndRemoveStoredKeyPair = function(keyName) {
|
crypto_storage.removeStoredKeyPair = function(keyName) {
|
||||||
var keyPair = this.getStoredKeyPair(keyName);
|
|
||||||
textsecure.storage.removeEncrypted("25519Key" + keyName);
|
textsecure.storage.removeEncrypted("25519Key" + keyName);
|
||||||
return keyPair;
|
|
||||||
}
|
|
||||||
|
|
||||||
crypto_storage.getAndRemovePreKeyPair = function(keyId) {
|
|
||||||
return this.getAndRemoveStoredKeyPair("preKey" + keyId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
crypto_storage.getIdentityPrivKey = function() {
|
crypto_storage.getIdentityPrivKey = function() {
|
||||||
|
@ -240,20 +234,30 @@ window.textsecure.crypto = function() {
|
||||||
/*****************************
|
/*****************************
|
||||||
*** Internal Crypto stuff ***
|
*** Internal Crypto stuff ***
|
||||||
*****************************/
|
*****************************/
|
||||||
var validatePubKeyFormat = function(pubKey) {
|
var validatePubKeyFormat = function(pubKey, needVersionByte) {
|
||||||
if (pubKey === undefined || ((pubKey.byteLength != 33 || new Uint8Array(pubKey)[0] != 5) && pubKey.byteLength != 32))
|
if (pubKey === undefined || ((pubKey.byteLength != 33 || new Uint8Array(pubKey)[0] != 5) && pubKey.byteLength != 32))
|
||||||
throw new Error("Invalid public key");
|
throw new Error("Invalid public key");
|
||||||
if (pubKey.byteLength == 33)
|
if (pubKey.byteLength == 33) {
|
||||||
return pubKey.slice(1);
|
if (!needVersionByte)
|
||||||
else
|
return pubKey.slice(1);
|
||||||
|
else
|
||||||
|
return pubKey;
|
||||||
|
} else {
|
||||||
console.error("WARNING: Expected pubkey of length 33, please report the ST and client that generated the pubkey");
|
console.error("WARNING: Expected pubkey of length 33, please report the ST and client that generated the pubkey");
|
||||||
|
if (!needVersionByte)
|
||||||
|
return pubKey;
|
||||||
|
var res = new Uint8Array(33);
|
||||||
|
res[0] = 5;
|
||||||
|
res.set(new Uint8Array(pubKey), 1);
|
||||||
|
return res.buffer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
testing_only.ECDHE = function(pubKey, privKey) {
|
testing_only.ECDHE = function(pubKey, privKey) {
|
||||||
if (privKey === undefined || privKey.byteLength != 32)
|
if (privKey === undefined || privKey.byteLength != 32)
|
||||||
throw new Error("Invalid private key");
|
throw new Error("Invalid private key");
|
||||||
|
|
||||||
pubKey = validatePubKeyFormat(pubKey);
|
pubKey = validatePubKeyFormat(pubKey, false);
|
||||||
|
|
||||||
return new Promise(function(resolve) {
|
return new Promise(function(resolve) {
|
||||||
if (textsecure.nacl.USE_NACL) {
|
if (textsecure.nacl.USE_NACL) {
|
||||||
|
@ -283,12 +287,12 @@ window.textsecure.crypto = function() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var Ed25519Sign = function(privKey, pubKeyToSign) {
|
var Ed25519Sign = function(privKey, pubKeyToSign) {
|
||||||
pubKeyToSign = validatePubKeyFormat(pubKeyToSign);
|
pubKeyToSign = validatePubKeyFormat(pubKeyToSign, true);
|
||||||
return testing_only.Ed25519Sign(privKey, pubKeyToSign);
|
return testing_only.Ed25519Sign(privKey, pubKeyToSign);
|
||||||
}
|
}
|
||||||
|
|
||||||
testing_only.Ed25519Verify = function(pubKey, msg, sig) {
|
testing_only.Ed25519Verify = function(pubKey, msg, sig) {
|
||||||
pubKey = validatePubKeyFormat(pubKey);
|
pubKey = validatePubKeyFormat(pubKey, false);
|
||||||
|
|
||||||
if (msg === undefined)
|
if (msg === undefined)
|
||||||
throw new Error("Invalid message");
|
throw new Error("Invalid message");
|
||||||
|
@ -306,7 +310,7 @@ window.textsecure.crypto = function() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var Ed25519Verify = function(pubKey, signedPubKey, sig) {
|
var Ed25519Verify = function(pubKey, signedPubKey, sig) {
|
||||||
signedPubKey = validatePubKeyFormat(signedPubKey);
|
signedPubKey = validatePubKeyFormat(signedPubKey, true);
|
||||||
return testing_only.Ed25519Verify(pubKey, signedPubKey, sig);
|
return testing_only.Ed25519Verify(pubKey, signedPubKey, sig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -353,14 +357,14 @@ window.textsecure.crypto = function() {
|
||||||
|
|
||||||
var verifyMACWithVersionByte = function(data, key, mac, version) {
|
var verifyMACWithVersionByte = function(data, key, mac, version) {
|
||||||
return calculateMACWithVersionByte(data, key, version).then(function(calculated_mac) {
|
return calculateMACWithVersionByte(data, key, version).then(function(calculated_mac) {
|
||||||
if (!isEqual(calculated_mac, mac, mac.byteLength))
|
if (!isEqual(calculated_mac, mac))
|
||||||
throw new Error("Bad MAC");
|
throw new Error("Bad MAC");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var verifyMAC = function(data, key, mac) {
|
var verifyMAC = function(data, key, mac) {
|
||||||
return HmacSHA256(key, data).then(function(calculated_mac) {
|
return HmacSHA256(key, data).then(function(calculated_mac) {
|
||||||
if (!isEqual(calculated_mac, mac, mac.byteLength))
|
if (!isEqual(calculated_mac, mac))
|
||||||
throw new Error("Bad MAC");
|
throw new Error("Bad MAC");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -382,48 +386,79 @@ window.textsecure.crypto = function() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var initSession = function(isInitiator, ourEphemeralKey, encodedNumber, theirIdentityPubKey, theirEphemeralPubKey) {
|
var initSession = function(isInitiator, ourEphemeralKey, ourSignedKey, encodedNumber, theirIdentityPubKey, theirEphemeralPubKey, theirSignedPubKey) {
|
||||||
var ourIdentityPrivKey = crypto_storage.getIdentityPrivKey();
|
var ourIdentityPrivKey = crypto_storage.getIdentityPrivKey();
|
||||||
|
|
||||||
var sharedSecret = new Uint8Array(32 * 3);
|
if (isInitiator) {
|
||||||
return ECDHE(theirEphemeralPubKey, ourIdentityPrivKey).then(function(ecRes1) {
|
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 ECDHE(theirSignedPubKey, ourIdentityPrivKey).then(function(ecRes1) {
|
||||||
function finishInit() {
|
function finishInit() {
|
||||||
return ECDHE(theirEphemeralPubKey, ourEphemeralKey.privKey).then(function(ecRes) {
|
return ECDHE(theirSignedPubKey, ourSignedKey.privKey).then(function(ecRes) {
|
||||||
sharedSecret.set(new Uint8Array(ecRes), 32 * 2);
|
sharedSecret.set(new Uint8Array(ecRes), 32 * 3);
|
||||||
|
|
||||||
return HKDF(sharedSecret.buffer, '', "WhisperText").then(function(masterKey) {
|
return HKDF(sharedSecret.buffer, '', "WhisperText").then(function(masterKey) {
|
||||||
var session = {currentRatchet: { rootKey: masterKey[0], lastRemoteEphemeralKey: theirEphemeralPubKey },
|
var session = {currentRatchet: { rootKey: masterKey[0], lastRemoteEphemeralKey: theirSignedPubKey },
|
||||||
indexInfo: { remoteIdentityKey: theirIdentityPubKey, closed: -1, baseKey: theirEphemeralPubKey },
|
indexInfo: { remoteIdentityKey: theirIdentityPubKey, closed: -1 },
|
||||||
oldRatchetList: []
|
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,
|
// 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
|
// otherwise we figure it out when we first maybeStepRatchet with the remote's ephemeral key
|
||||||
if (isInitiator) {
|
if (isInitiator) {
|
||||||
return createNewKeyPair(false).then(function(ourSendingEphemeralKey) {
|
return createNewKeyPair(false).then(function(ourSendingEphemeralKey) {
|
||||||
session.currentRatchet.ephemeralKeyPair = ourSendingEphemeralKey;
|
session.currentRatchet.ephemeralKeyPair = ourSendingEphemeralKey;
|
||||||
return calculateRatchet(session, theirEphemeralPubKey, true).then(function() {
|
return calculateRatchet(session, theirSignedPubKey, true).then(function() {
|
||||||
return session;
|
return session;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
session.currentRatchet.ephemeralKeyPair = ourEphemeralKey;
|
session.currentRatchet.ephemeralKeyPair = ourSignedKey;
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInitiator)
|
var promise;
|
||||||
return ECDHE(theirIdentityPubKey, ourEphemeralKey.privKey).then(function(ecRes2) {
|
if (ourEphemeralKey === undefined || theirEphemeralPubKey === undefined)
|
||||||
sharedSecret.set(new Uint8Array(ecRes1));
|
promise = Promise.resolve(new ArrayBuffer(0));
|
||||||
sharedSecret.set(new Uint8Array(ecRes2), 32);
|
|
||||||
}).then(finishInit);
|
|
||||||
else
|
else
|
||||||
return ECDHE(theirIdentityPubKey, ourEphemeralKey.privKey).then(function(ecRes2) {
|
promise = ECDHE(theirEphemeralPubKey, ourEphemeralKey.privKey);
|
||||||
sharedSecret.set(new Uint8Array(ecRes1), 32);
|
|
||||||
sharedSecret.set(new Uint8Array(ecRes2))
|
return promise.then(function(ecRes4) {
|
||||||
}).then(finishInit);
|
sharedSecret.set(new Uint8Array(ecRes4), 32 * 4);
|
||||||
|
if (isInitiator)
|
||||||
|
return ECDHE(theirIdentityPubKey, ourSignedKey.privKey).then(function(ecRes2) {
|
||||||
|
sharedSecret.set(new Uint8Array(ecRes1), 32);
|
||||||
|
sharedSecret.set(new Uint8Array(ecRes2), 32 * 2);
|
||||||
|
}).then(finishInit);
|
||||||
|
else
|
||||||
|
return ECDHE(theirIdentityPubKey, ourSignedKey.privKey).then(function(ecRes2) {
|
||||||
|
sharedSecret.set(new Uint8Array(ecRes1), 32 * 2);
|
||||||
|
sharedSecret.set(new Uint8Array(ecRes2), 32)
|
||||||
|
}).then(finishInit);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -457,7 +492,7 @@ window.textsecure.crypto = function() {
|
||||||
return initSessionFromPreKeyWhisperMessage(from, preKeyProto).then(function(sessions) {
|
return initSessionFromPreKeyWhisperMessage(from, preKeyProto).then(function(sessions) {
|
||||||
return decryptWhisperMessage(from, getString(preKeyProto.message), sessions[0], preKeyProto.registrationId).then(function(result) {
|
return decryptWhisperMessage(from, getString(preKeyProto.message), sessions[0], preKeyProto.registrationId).then(function(result) {
|
||||||
if (sessions[1] !== undefined)
|
if (sessions[1] !== undefined)
|
||||||
crypto_storage.saveSession(from, sessions[1]);
|
sessions[1]();
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -470,21 +505,22 @@ window.textsecure.crypto = function() {
|
||||||
textsecure.replay.registerReplayFunction(wipeIdentityAndTryMessageAgain, textsecure.replay.REPLAY_FUNCS.INIT_SESSION);
|
textsecure.replay.registerReplayFunction(wipeIdentityAndTryMessageAgain, textsecure.replay.REPLAY_FUNCS.INIT_SESSION);
|
||||||
|
|
||||||
initSessionFromPreKeyWhisperMessage = function(encodedNumber, message) {
|
initSessionFromPreKeyWhisperMessage = function(encodedNumber, message) {
|
||||||
var preKeyPair = crypto_storage.getAndRemovePreKeyPair(message.preKeyId);
|
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 session = crypto_storage.getSessionOrIdentityKeyByBaseKey(encodedNumber, toArrayBuffer(message.baseKey));
|
||||||
var open_session = crypto_storage.getOpenSession(encodedNumber);
|
var open_session = crypto_storage.getOpenSession(encodedNumber);
|
||||||
if (preKeyPair === undefined) {
|
if (signedPreKeyPair === undefined) {
|
||||||
// Session may or may not be the correct one, but if its not, we can't do anything about it
|
// Session may or may not be the correct one, but if its not, we can't do anything about it
|
||||||
// ...fall through and let decryptWhisperMessage handle that case
|
// ...fall through and let decryptWhisperMessage handle that case
|
||||||
if (session !== undefined && session.currentRatchet !== undefined)
|
if (session !== undefined && session.currentRatchet !== undefined)
|
||||||
return Promise.resolve([session, undefined]);
|
return Promise.resolve([session, undefined]);
|
||||||
else
|
else
|
||||||
throw new Error("Missing preKey for PreKeyWhisperMessage");
|
throw new Error("Missing signedPreKey for PreKeyWhisperMessage");
|
||||||
}
|
}
|
||||||
if (session !== undefined) {
|
if (session !== undefined) {
|
||||||
// We already had a session/known identity key:
|
// We already had a session/known identity key:
|
||||||
if (getString(session.indexInfo.remoteIdentityKey) == getString(message.identityKey)) {
|
if (isEqual(session.indexInfo.remoteIdentityKey, message.identityKey)) {
|
||||||
// If the identity key matches the previous one, close the previous one and use the new one
|
// If the identity key matches the previous one, close the previous one and use the new one
|
||||||
if (open_session !== undefined)
|
if (open_session !== undefined)
|
||||||
closeSession(open_session); // To be returned and saved later
|
closeSession(open_session); // To be returned and saved later
|
||||||
|
@ -493,11 +529,15 @@ window.textsecure.crypto = function() {
|
||||||
throw textsecure.createTryAgainError("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())]);
|
throw textsecure.createTryAgainError("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, encodedNumber, toArrayBuffer(message.identityKey), toArrayBuffer(message.baseKey))
|
return initSession(false, preKeyPair, signedPreKeyPair, encodedNumber, toArrayBuffer(message.identityKey), toArrayBuffer(message.baseKey), undefined)
|
||||||
.then(function(new_session) {
|
.then(function(new_session) {
|
||||||
// Note that the session is not actually saved until the very end of decryptWhisperMessage
|
// Note that the session is not actually saved until the very end of decryptWhisperMessage
|
||||||
// ... to ensure that the sender actually holds the private keys for all reported pubkeys
|
// ... to ensure that the sender actually holds the private keys for all reported pubkeys
|
||||||
return [new_session, open_session];
|
return [new_session, function() {
|
||||||
|
if (open_session !== undefined)
|
||||||
|
crypto_storage.saveSession(encodedNumber, open_session);
|
||||||
|
crypto_storage.removeStoredKeyPair("preKey" + message.preKeyId);
|
||||||
|
}];
|
||||||
});;
|
});;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -586,7 +626,7 @@ window.textsecure.crypto = function() {
|
||||||
|
|
||||||
// returns decrypted protobuf
|
// returns decrypted protobuf
|
||||||
decryptWhisperMessage = function(encodedNumber, messageBytes, session, registrationId) {
|
decryptWhisperMessage = function(encodedNumber, messageBytes, session, registrationId) {
|
||||||
if (messageBytes[0] != String.fromCharCode((2 << 4) | 2))
|
if (messageBytes[0] != String.fromCharCode((3 << 4) | 3))
|
||||||
throw new Error("Bad version number on WhisperMessage");
|
throw new Error("Bad version number on WhisperMessage");
|
||||||
|
|
||||||
var messageProto = messageBytes.substring(1, messageBytes.length - 8);
|
var messageProto = messageBytes.substring(1, messageBytes.length - 8);
|
||||||
|
@ -607,8 +647,7 @@ window.textsecure.crypto = function() {
|
||||||
return fillMessageKeys(chain, message.counter).then(function() {
|
return fillMessageKeys(chain, message.counter).then(function() {
|
||||||
return HKDF(toArrayBuffer(chain.messageKeys[message.counter]), '', "WhisperMessageKeys").then(function(keys) {
|
return HKDF(toArrayBuffer(chain.messageKeys[message.counter]), '', "WhisperMessageKeys").then(function(keys) {
|
||||||
delete chain.messageKeys[message.counter];
|
delete chain.messageKeys[message.counter];
|
||||||
|
return verifyMACWithVersionByte(toArrayBuffer(messageProto), keys[1], mac, (3 << 4) | 3).then(function() {
|
||||||
return verifyMACWithVersionByte(toArrayBuffer(messageProto), keys[1], mac, (2 << 4) | 2).then(function() {
|
|
||||||
var counter = intToArrayBuffer(message.counter);
|
var counter = intToArrayBuffer(message.counter);
|
||||||
return window.crypto.subtle.decrypt({name: "AES-CTR", counter: counter}, keys[0], toArrayBuffer(message.ciphertext))
|
return window.crypto.subtle.decrypt({name: "AES-CTR", counter: counter}, keys[0], toArrayBuffer(message.ciphertext))
|
||||||
.then(function(plaintext) {
|
.then(function(plaintext) {
|
||||||
|
@ -694,7 +733,7 @@ window.textsecure.crypto = function() {
|
||||||
var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice);
|
var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice);
|
||||||
return decryptWhisperMessage(from, getString(proto.message));
|
return decryptWhisperMessage(from, getString(proto.message));
|
||||||
case textsecure.protos.IncomingPushMessageProtobuf.Type.PREKEY_BUNDLE:
|
case textsecure.protos.IncomingPushMessageProtobuf.Type.PREKEY_BUNDLE:
|
||||||
if (proto.message.readUint8() != (2 << 4 | 2))
|
if (proto.message.readUint8() != ((3 << 4) | 3))
|
||||||
throw new Error("Bad version byte");
|
throw new Error("Bad version byte");
|
||||||
var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice);
|
var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice);
|
||||||
return handlePreKeyWhisperMessage(from, getString(proto.message));
|
return handlePreKeyWhisperMessage(from, getString(proto.message));
|
||||||
|
@ -725,9 +764,9 @@ window.textsecure.crypto = function() {
|
||||||
msg.ciphertext = ciphertext;
|
msg.ciphertext = ciphertext;
|
||||||
var encodedMsg = toArrayBuffer(msg.encode());
|
var encodedMsg = toArrayBuffer(msg.encode());
|
||||||
|
|
||||||
return calculateMACWithVersionByte(encodedMsg, keys[1], (2 << 4) | 2).then(function(mac) {
|
return calculateMACWithVersionByte(encodedMsg, keys[1], (3 << 4) | 3).then(function(mac) {
|
||||||
var result = new Uint8Array(encodedMsg.byteLength + 9);
|
var result = new Uint8Array(encodedMsg.byteLength + 9);
|
||||||
result[0] = (2 << 4) | 2;
|
result[0] = (3 << 4) | 3;
|
||||||
result.set(new Uint8Array(encodedMsg), 1);
|
result.set(new Uint8Array(encodedMsg), 1);
|
||||||
result.set(new Uint8Array(mac, 0, 8), encodedMsg.byteLength + 1);
|
result.set(new Uint8Array(mac, 0, 8), encodedMsg.byteLength + 1);
|
||||||
crypto_storage.saveSession(deviceObject.encodedNumber, session);
|
crypto_storage.saveSession(deviceObject.encodedNumber, session);
|
||||||
|
@ -741,19 +780,20 @@ window.textsecure.crypto = function() {
|
||||||
var preKeyMsg = new textsecure.protos.PreKeyWhisperMessageProtobuf();
|
var preKeyMsg = new textsecure.protos.PreKeyWhisperMessageProtobuf();
|
||||||
preKeyMsg.identityKey = toArrayBuffer(crypto_storage.getStoredPubKey("identityKey"));
|
preKeyMsg.identityKey = toArrayBuffer(crypto_storage.getStoredPubKey("identityKey"));
|
||||||
preKeyMsg.preKeyId = deviceObject.preKeyId;
|
preKeyMsg.preKeyId = deviceObject.preKeyId;
|
||||||
|
preKeyMsg.signedPreKeyId = deviceObject.signedKeyId;
|
||||||
preKeyMsg.registrationId = textsecure.storage.getUnencrypted("registrationId");
|
preKeyMsg.registrationId = textsecure.storage.getUnencrypted("registrationId");
|
||||||
|
|
||||||
if (session === undefined) {
|
if (session === undefined) {
|
||||||
return createNewKeyPair(false).then(function(baseKey) {
|
return createNewKeyPair(false).then(function(baseKey) {
|
||||||
preKeyMsg.baseKey = toArrayBuffer(baseKey.pubKey);
|
preKeyMsg.baseKey = toArrayBuffer(baseKey.pubKey);
|
||||||
return initSession(true, baseKey, deviceObject.encodedNumber,
|
return initSession(true, baseKey, undefined, deviceObject.encodedNumber,
|
||||||
toArrayBuffer(deviceObject.identityKey), toArrayBuffer(deviceObject.publicKey))
|
toArrayBuffer(deviceObject.identityKey), toArrayBuffer(deviceObject.preKey), toArrayBuffer(deviceObject.signedKey))
|
||||||
.then(function(new_session) {
|
.then(function(new_session) {
|
||||||
session = new_session;
|
session = new_session;
|
||||||
session.pendingPreKey = baseKey.pubKey;
|
session.pendingPreKey = baseKey.pubKey;
|
||||||
return doEncryptPushMessageContent().then(function(message) {
|
return doEncryptPushMessageContent().then(function(message) {
|
||||||
preKeyMsg.message = message;
|
preKeyMsg.message = message;
|
||||||
var result = String.fromCharCode((2 << 4) | 2) + getString(preKeyMsg.encode());
|
var result = String.fromCharCode((3 << 4) | 3) + getString(preKeyMsg.encode());
|
||||||
return {type: 3, body: result};
|
return {type: 3, body: result};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -763,7 +803,7 @@ window.textsecure.crypto = function() {
|
||||||
if (session.pendingPreKey !== undefined) {
|
if (session.pendingPreKey !== undefined) {
|
||||||
preKeyMsg.baseKey = toArrayBuffer(session.pendingPreKey);
|
preKeyMsg.baseKey = toArrayBuffer(session.pendingPreKey);
|
||||||
preKeyMsg.message = message;
|
preKeyMsg.message = message;
|
||||||
var result = String.fromCharCode((2 << 4) | 2) + getString(preKeyMsg.encode());
|
var result = String.fromCharCode((3 << 4) | 3) + getString(preKeyMsg.encode());
|
||||||
return {type: 3, body: result};
|
return {type: 3, body: result};
|
||||||
} else
|
} else
|
||||||
return {type: 1, body: getString(message)};
|
return {type: 1, body: getString(message)};
|
||||||
|
@ -800,7 +840,8 @@ window.textsecure.crypto = function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
crypto_storage.getAndRemoveStoredKeyPair("signedKey" + (signedKeyId - 2));
|
//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 Promise.all(promises).then(function() {
|
||||||
return keys;
|
return keys;
|
||||||
|
@ -818,6 +859,8 @@ window.textsecure.crypto = function() {
|
||||||
self.generateKeys();
|
self.generateKeys();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.Ed25519Verify = Ed25519Verify;
|
||||||
|
|
||||||
self.testing_only = testing_only;
|
self.testing_only = testing_only;
|
||||||
return self;
|
return self;
|
||||||
}();
|
}();
|
||||||
|
|
|
@ -127,12 +127,13 @@ function getStringable(thing) {
|
||||||
thing.__proto__ == StaticWordArrayProto)));
|
thing.__proto__ == StaticWordArrayProto)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function isEqual(a, b, maxLegnth) {
|
function isEqual(a, b) {
|
||||||
// TODO: Special-case arraybuffers, etc
|
// TODO: Special-case arraybuffers, etc
|
||||||
a = getString(a);
|
a = getString(a);
|
||||||
b = getString(b);
|
b = getString(b);
|
||||||
if (maxLength === undefined)
|
var maxLength = Math.min(a.length, b.length);
|
||||||
maxLength = Math.max(a.length, b.length);
|
if (maxLength < 5)
|
||||||
|
throw new Error("a/b compare too short");
|
||||||
return a.substring(0, Math.min(maxLength, a.length)) == b.substring(0, Math.min(maxLength, b.length));
|
return a.substring(0, Math.min(maxLength, a.length)) == b.substring(0, Math.min(maxLength, b.length));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -642,7 +643,14 @@ window.textsecure.subscribeToPush = function() {
|
||||||
};
|
};
|
||||||
socket.onopen = function(socketEvent) {
|
socket.onopen = function(socketEvent) {
|
||||||
console.log('Connected to server!');
|
console.log('Connected to server!');
|
||||||
pingInterval = setInterval(function() { console.log("Sending server ping message."); socket.send(JSON.stringify({type: 2})); }, 30000);
|
pingInterval = setInterval(function() {
|
||||||
|
console.log("Sending server ping message.");
|
||||||
|
if (socket.readyState == socket.CLOSED || socket.readyState == socket.CLOSING) {
|
||||||
|
socket.close();
|
||||||
|
socket.onclose();
|
||||||
|
} else
|
||||||
|
socket.send(JSON.stringify({type: 2}));
|
||||||
|
}, 30000);
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.onmessage = function(response) {
|
socket.onmessage = function(response) {
|
||||||
|
|
|
@ -4,19 +4,16 @@ window.textsecure.messaging = function() {
|
||||||
|
|
||||||
function getKeysForNumber(number, updateDevices) {
|
function getKeysForNumber(number, updateDevices) {
|
||||||
return textsecure.api.getKeysForNumber(number).then(function(response) {
|
return textsecure.api.getKeysForNumber(number).then(function(response) {
|
||||||
var identityKey = getString(response[0].identityKey);
|
for (i in response.devices) {
|
||||||
for (i in response)
|
if (updateDevices === undefined || updateDevices.indexOf(response.devices[i].deviceId) > -1)
|
||||||
if (getString(response[i].identityKey) != identityKey)
|
|
||||||
throw new Error("Identity key not consistent");
|
|
||||||
|
|
||||||
for (i in response) {
|
|
||||||
if (updateDevices === undefined || updateDevices.indexOf(response[i].deviceId) > -1)
|
|
||||||
textsecure.storage.devices.saveDeviceObject({
|
textsecure.storage.devices.saveDeviceObject({
|
||||||
encodedNumber: number + "." + response[i].deviceId,
|
encodedNumber: number + "." + response.devices[i].deviceId,
|
||||||
identityKey: response[i].identityKey,
|
identityKey: response.identityKey,
|
||||||
publicKey: response[i].publicKey,
|
preKey: response.devices[i].preKey.publicKey,
|
||||||
preKeyId: response[i].keyId,
|
preKeyId: response.devices[i].preKey.keyId,
|
||||||
registrationId: response[i].registrationId
|
signedKey: response.devices[i].signedPreKey.publicKey,
|
||||||
|
signedKeyId: response.devices[i].signedPreKey.keyId,
|
||||||
|
registrationId: response.devices[i].registrationId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -112,8 +109,11 @@ window.textsecure.messaging = function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var registerError = function(number, message, error) {
|
var registerError = function(number, message, error) {
|
||||||
if (error.humanError)
|
if (error) {
|
||||||
message = error.humanError;
|
if (error.humanError)
|
||||||
|
message = error.humanError;
|
||||||
|
} else
|
||||||
|
error = new Error(message);
|
||||||
errors[errors.length] = { number: number, reason: message, error: error };
|
errors[errors.length] = { number: number, reason: message, error: error };
|
||||||
numberCompleted();
|
numberCompleted();
|
||||||
}
|
}
|
||||||
|
@ -123,7 +123,7 @@ window.textsecure.messaging = function() {
|
||||||
return function() {
|
return function() {
|
||||||
var devicesForNumber = textsecure.storage.devices.getDeviceObjectsForNumber(number);
|
var devicesForNumber = textsecure.storage.devices.getDeviceObjectsForNumber(number);
|
||||||
if (devicesForNumber.length == 0)
|
if (devicesForNumber.length == 0)
|
||||||
return registerError(number, "Go empty device list when loading device keys", null);
|
return registerError(number, "Got empty device list when loading device keys", null);
|
||||||
refreshGroups(number).then(function() {
|
refreshGroups(number).then(function() {
|
||||||
doSendMessage(number, devicesForNumber, recurse);
|
doSendMessage(number, devicesForNumber, recurse);
|
||||||
});
|
});
|
||||||
|
|
21
js/test.js
21
js/test.js
|
@ -61,8 +61,11 @@ function TEST(func, name, exclusive) {
|
||||||
if (testsOutstanding[testIndex] == undefined)
|
if (testsOutstanding[testIndex] == undefined)
|
||||||
testsdiv.append('<p style="color: red;">' + funcName + ' called back multiple times</p>');
|
testsdiv.append('<p style="color: red;">' + funcName + ' called back multiple times</p>');
|
||||||
else if (error !== undefined) {
|
else if (error !== undefined) {
|
||||||
console.log(error.stack);
|
if (error && error.stack) {
|
||||||
testsdiv.append('<p style="color: red;">' + funcName + ' threw ' + error + '</p>');
|
console.log(error.stack);
|
||||||
|
testsdiv.append('<p style="color: red;">' + funcName + ' threw ' + error + '</p>');
|
||||||
|
} else
|
||||||
|
testsdiv.append('<p style="color: red;">' + funcName + ' threw non-Error: ' + error + '</p>');
|
||||||
} else if (result === true)
|
} else if (result === true)
|
||||||
testsdiv.append('<p style="color: green;">' + funcName + ' passed</p>');
|
testsdiv.append('<p style="color: green;">' + funcName + ' passed</p>');
|
||||||
else
|
else
|
||||||
|
@ -276,12 +279,14 @@ textsecure.registerOnLoadFunction(function() {
|
||||||
ourEphemeralKey: hexToArrayBuffer('f12704787bab04a3cf544ebd9d421b6fe36147519eb5afa7c90e3fb67c141e64'),
|
ourEphemeralKey: hexToArrayBuffer('f12704787bab04a3cf544ebd9d421b6fe36147519eb5afa7c90e3fb67c141e64'),
|
||||||
ourIdentityKey: hexToArrayBuffer('a05fd14abb42ff393004eee526e3167441ee51021c6d801b784720c15637747c'),
|
ourIdentityKey: hexToArrayBuffer('a05fd14abb42ff393004eee526e3167441ee51021c6d801b784720c15637747c'),
|
||||||
registrationId: 11593,
|
registrationId: 11593,
|
||||||
getKeys: [{deviceId: 0,
|
getKeys: {identityKey: hexToArrayBuffer('05276e4df34557386f67df38b708eeddb1a8924e0428b9eefdc9213c3e8927cc7d'),
|
||||||
keyId: 13845842,
|
devices: [{
|
||||||
publicKey: hexToArrayBuffer('05fee424a5b6ccb717d85ef2207e2057ab1144c40afe89cdc80e9c424dd90c146e'),
|
deviceId: 0,
|
||||||
identityKey: hexToArrayBuffer('05276e4df34557386f67df38b708eeddb1a8924e0428b9eefdc9213c3e8927cc7d'),
|
preKey: {keyId: 13845842, publicKey: hexToArrayBuffer('05fee424a5b6ccb717d85ef2207e2057ab1144c40afe89cdc80e9c424dd90c146e')},
|
||||||
registrationId: 42}
|
signedPreKey: {keyId: 13845842, publicKey: hexToArrayBuffer('05fee424a5b6ccb717d85ef2207e2057ab1144c40afe89cdc80e9c424dd90c146e')},
|
||||||
],
|
registrationId: 42
|
||||||
|
}]
|
||||||
|
},
|
||||||
//expectedPlaintext: hexToArrayBuffer('0a0e4120202020202020202020202020'),
|
//expectedPlaintext: hexToArrayBuffer('0a0e4120202020202020202020202020'),
|
||||||
//expectedCounter: 0,
|
//expectedCounter: 0,
|
||||||
expectedCiphertext: hexToArrayBuffer('2208d28acd061221059ab4e844771bfeb96382edac5f80e757a1109b5611c770b2ba9f28b363d7c2541a2105bd61aea7fa5304f4dc914892bc3795812cda8bb90b73de9920e22c609cf0ec4e2242220a21058c0c357a3a25e6da46b0186d93fec31d5b86a4ac4973742012d8e9de2346be161000180022104bd27ab87ee151d71cdfe89828050ef4b05bddfb56da491728c95a'),
|
expectedCiphertext: hexToArrayBuffer('2208d28acd061221059ab4e844771bfeb96382edac5f80e757a1109b5611c770b2ba9f28b363d7c2541a2105bd61aea7fa5304f4dc914892bc3795812cda8bb90b73de9920e22c609cf0ec4e2242220a21058c0c357a3a25e6da46b0186d93fec31d5b86a4ac4973742012d8e9de2346be161000180022104bd27ab87ee151d71cdfe89828050ef4b05bddfb56da491728c95a'),
|
||||||
|
|
Loading…
Add table
Reference in a new issue