diff --git a/js/api.js b/js/api.js index 41272b9df8..b43dcb3d4a 100644 --- a/js/api.js +++ b/js/api.js @@ -22,8 +22,8 @@ window.textsecure.api = function() { /************************************************ *** Utilities to communicate with the server *** ************************************************/ - // WARNING: THIS SERVER LOGS KEY MATERIAL FOR TESTING - var URL_BASE = "http://sushiforeveryone.bluematt.me"; + // Staging server + var URL_BASE = "https://textsecure-service-ca.whispersystems.org:4433"; // This is the real server //var URL_BASE = "https://textsecure-service.whispersystems.org"; @@ -139,12 +139,21 @@ window.textsecure.api = function() { }); }; - self.registerKeys = function(keys) { - //TODO: Do this conversion somewhere else? - var identityKey = btoa(getString(keys.keys[0].identityKey)); - for (var i = 0; i < keys.keys.length; i++) - keys.keys[i] = {keyId: i, publicKey: btoa(getString(keys.keys[i].publicKey)), identityKey: identityKey}; - keys.lastResortKey = {keyId: keys.lastResortKey.keyId, publicKey: btoa(getString(keys.lastResortKey.publicKey)), identityKey: identityKey}; + self.registerKeys = function(genKeys) { + var keys = {}; + keys.identityKey = btoa(getString(genKeys.identityKey)); + keys.signedPreKey = {keyId: genKeys.signedPreKey.keyId, publicKey: btoa(getString(genKeys.signedPreKey.publicKey)), + signature: btoa(getString(genKeys.signedPreKey.signature))}; + + 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({ call : 'keys', httpType : 'PUT', @@ -159,16 +168,21 @@ window.textsecure.api = function() { httpType : 'GET', do_auth : true, urlParameters : "/" + number + "/*", - }).then(function(response) { - //TODO: Do this conversion somewhere else? - var res = response.keys; - for (var i = 0; i < res.length; i++) { - res[i].identityKey = base64DecToArr(res[i].identityKey); - res[i].publicKey = base64DecToArr(res[i].publicKey); - if (res[i].keyId === undefined) - res[i].keyId = 0; + }).then(function(res) { + var promises = []; + res.identityKey = base64DecToArr(res.identityKey); + for (var i = 0; i < res.devices.length; i++) { + res.devices[i].signedPreKey.publicKey = base64DecToArr(res.devices[i].signedPreKey.publicKey); + res.devices[i].signedPreKey.signature = base64DecToArr(res.devices[i].signedPreKey.signature); + promises[i] = window.textsecure.crypto.Ed25519Verify(res.identityKey, res.devices[i].signedPreKey.publicKey, res.devices[i].signedPreKey.signature); + 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 URL = URL_BASE.replace(/^http/g, 'ws') + URL_CALLS['push'] + '/?'; var params = $.param({ - user: '+' + getString(user).substring(1), + login: '+' + getString(user).substring(1), password: getString(password) }); return new WebSocket(URL+params); diff --git a/js/crypto.js b/js/crypto.js index e384c2081e..a22eaa95fa 100644 --- a/js/crypto.js +++ b/js/crypto.js @@ -128,14 +128,8 @@ window.textsecure.crypto = function() { return { pubKey: toArrayBuffer(res.pubKey), privKey: toArrayBuffer(res.privKey) }; } - crypto_storage.getAndRemoveStoredKeyPair = function(keyName) { - var keyPair = this.getStoredKeyPair(keyName); + crypto_storage.removeStoredKeyPair = function(keyName) { textsecure.storage.removeEncrypted("25519Key" + keyName); - return keyPair; - } - - crypto_storage.getAndRemovePreKeyPair = function(keyId) { - return this.getAndRemoveStoredKeyPair("preKey" + keyId); } crypto_storage.getIdentityPrivKey = function() { @@ -240,20 +234,30 @@ window.textsecure.crypto = function() { /***************************** *** 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)) throw new Error("Invalid public key"); - if (pubKey.byteLength == 33) - return pubKey.slice(1); - else + if (pubKey.byteLength == 33) { + if (!needVersionByte) + 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"); + 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) { if (privKey === undefined || privKey.byteLength != 32) throw new Error("Invalid private key"); - pubKey = validatePubKeyFormat(pubKey); + pubKey = validatePubKeyFormat(pubKey, false); return new Promise(function(resolve) { if (textsecure.nacl.USE_NACL) { @@ -283,12 +287,12 @@ window.textsecure.crypto = function() { } } var Ed25519Sign = function(privKey, pubKeyToSign) { - pubKeyToSign = validatePubKeyFormat(pubKeyToSign); + pubKeyToSign = validatePubKeyFormat(pubKeyToSign, true); return testing_only.Ed25519Sign(privKey, pubKeyToSign); } testing_only.Ed25519Verify = function(pubKey, msg, sig) { - pubKey = validatePubKeyFormat(pubKey); + pubKey = validatePubKeyFormat(pubKey, false); if (msg === undefined) throw new Error("Invalid message"); @@ -306,7 +310,7 @@ window.textsecure.crypto = function() { } } var Ed25519Verify = function(pubKey, signedPubKey, sig) { - signedPubKey = validatePubKeyFormat(signedPubKey); + signedPubKey = validatePubKeyFormat(signedPubKey, true); return testing_only.Ed25519Verify(pubKey, signedPubKey, sig); } @@ -353,14 +357,14 @@ window.textsecure.crypto = function() { var verifyMACWithVersionByte = function(data, key, mac, version) { 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"); }); } var verifyMAC = function(data, key, 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"); }); } @@ -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 sharedSecret = new Uint8Array(32 * 3); - return ECDHE(theirEphemeralPubKey, ourIdentityPrivKey).then(function(ecRes1) { + 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 ECDHE(theirSignedPubKey, ourIdentityPrivKey).then(function(ecRes1) { function finishInit() { - return ECDHE(theirEphemeralPubKey, ourEphemeralKey.privKey).then(function(ecRes) { - sharedSecret.set(new Uint8Array(ecRes), 32 * 2); + return 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: theirEphemeralPubKey }, - indexInfo: { remoteIdentityKey: theirIdentityPubKey, closed: -1, baseKey: theirEphemeralPubKey }, + var session = {currentRatchet: { rootKey: masterKey[0], lastRemoteEphemeralKey: theirSignedPubKey }, + 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 createNewKeyPair(false).then(function(ourSendingEphemeralKey) { session.currentRatchet.ephemeralKeyPair = ourSendingEphemeralKey; - return calculateRatchet(session, theirEphemeralPubKey, true).then(function() { + return calculateRatchet(session, theirSignedPubKey, true).then(function() { return session; }); }); } else { - session.currentRatchet.ephemeralKeyPair = ourEphemeralKey; + session.currentRatchet.ephemeralKeyPair = ourSignedKey; return session; } }); }); } - if (isInitiator) - return ECDHE(theirIdentityPubKey, ourEphemeralKey.privKey).then(function(ecRes2) { - sharedSecret.set(new Uint8Array(ecRes1)); - sharedSecret.set(new Uint8Array(ecRes2), 32); - }).then(finishInit); + var promise; + if (ourEphemeralKey === undefined || theirEphemeralPubKey === undefined) + promise = Promise.resolve(new ArrayBuffer(0)); else - return ECDHE(theirIdentityPubKey, ourEphemeralKey.privKey).then(function(ecRes2) { - sharedSecret.set(new Uint8Array(ecRes1), 32); - sharedSecret.set(new Uint8Array(ecRes2)) - }).then(finishInit); + promise = ECDHE(theirEphemeralPubKey, ourEphemeralKey.privKey); + + return promise.then(function(ecRes4) { + 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 decryptWhisperMessage(from, getString(preKeyProto.message), sessions[0], preKeyProto.registrationId).then(function(result) { if (sessions[1] !== undefined) - crypto_storage.saveSession(from, sessions[1]); + sessions[1](); return result; }); }); @@ -470,21 +505,22 @@ window.textsecure.crypto = function() { textsecure.replay.registerReplayFunction(wipeIdentityAndTryMessageAgain, textsecure.replay.REPLAY_FUNCS.INIT_SESSION); 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 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 // ...fall through and let decryptWhisperMessage handle that case if (session !== undefined && session.currentRatchet !== undefined) return Promise.resolve([session, undefined]); else - throw new Error("Missing preKey for PreKeyWhisperMessage"); + throw new Error("Missing signedPreKey for PreKeyWhisperMessage"); } if (session !== undefined) { // 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 (open_session !== undefined) 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())]); } } - 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) { // 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, 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 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"); var messageProto = messageBytes.substring(1, messageBytes.length - 8); @@ -607,8 +647,7 @@ window.textsecure.crypto = function() { return fillMessageKeys(chain, message.counter).then(function() { return HKDF(toArrayBuffer(chain.messageKeys[message.counter]), '', "WhisperMessageKeys").then(function(keys) { delete chain.messageKeys[message.counter]; - - return verifyMACWithVersionByte(toArrayBuffer(messageProto), keys[1], mac, (2 << 4) | 2).then(function() { + return verifyMACWithVersionByte(toArrayBuffer(messageProto), keys[1], mac, (3 << 4) | 3).then(function() { var counter = intToArrayBuffer(message.counter); return window.crypto.subtle.decrypt({name: "AES-CTR", counter: counter}, keys[0], toArrayBuffer(message.ciphertext)) .then(function(plaintext) { @@ -694,7 +733,7 @@ window.textsecure.crypto = function() { var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice); return decryptWhisperMessage(from, getString(proto.message)); 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"); var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice); return handlePreKeyWhisperMessage(from, getString(proto.message)); @@ -725,9 +764,9 @@ window.textsecure.crypto = function() { msg.ciphertext = ciphertext; 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); - result[0] = (2 << 4) | 2; + result[0] = (3 << 4) | 3; result.set(new Uint8Array(encodedMsg), 1); result.set(new Uint8Array(mac, 0, 8), encodedMsg.byteLength + 1); crypto_storage.saveSession(deviceObject.encodedNumber, session); @@ -741,19 +780,20 @@ window.textsecure.crypto = function() { var preKeyMsg = new textsecure.protos.PreKeyWhisperMessageProtobuf(); preKeyMsg.identityKey = toArrayBuffer(crypto_storage.getStoredPubKey("identityKey")); preKeyMsg.preKeyId = deviceObject.preKeyId; + preKeyMsg.signedPreKeyId = deviceObject.signedKeyId; preKeyMsg.registrationId = textsecure.storage.getUnencrypted("registrationId"); if (session === undefined) { return createNewKeyPair(false).then(function(baseKey) { preKeyMsg.baseKey = toArrayBuffer(baseKey.pubKey); - return initSession(true, baseKey, deviceObject.encodedNumber, - toArrayBuffer(deviceObject.identityKey), toArrayBuffer(deviceObject.publicKey)) + 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 = baseKey.pubKey; return doEncryptPushMessageContent().then(function(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}; }); }); @@ -763,7 +803,7 @@ window.textsecure.crypto = function() { if (session.pendingPreKey !== undefined) { preKeyMsg.baseKey = toArrayBuffer(session.pendingPreKey); 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}; } else 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 keys; @@ -818,6 +859,8 @@ window.textsecure.crypto = function() { self.generateKeys(); }); + self.Ed25519Verify = Ed25519Verify; + self.testing_only = testing_only; return self; }(); diff --git a/js/helpers.js b/js/helpers.js index 276f6d8455..80bdaddf84 100644 --- a/js/helpers.js +++ b/js/helpers.js @@ -127,12 +127,13 @@ function getStringable(thing) { thing.__proto__ == StaticWordArrayProto))); } -function isEqual(a, b, maxLegnth) { +function isEqual(a, b) { // TODO: Special-case arraybuffers, etc a = getString(a); b = getString(b); - if (maxLength === undefined) - maxLength = Math.max(a.length, b.length); + var maxLength = Math.min(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)); } @@ -642,7 +643,14 @@ window.textsecure.subscribeToPush = function() { }; socket.onopen = function(socketEvent) { 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) { diff --git a/js/sendmessage.js b/js/sendmessage.js index 00ebb0fd0f..bae55748de 100644 --- a/js/sendmessage.js +++ b/js/sendmessage.js @@ -4,19 +4,16 @@ window.textsecure.messaging = function() { function getKeysForNumber(number, updateDevices) { return textsecure.api.getKeysForNumber(number).then(function(response) { - var identityKey = getString(response[0].identityKey); - for (i in response) - 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) + for (i in response.devices) { + if (updateDevices === undefined || updateDevices.indexOf(response.devices[i].deviceId) > -1) textsecure.storage.devices.saveDeviceObject({ - encodedNumber: number + "." + response[i].deviceId, - identityKey: response[i].identityKey, - publicKey: response[i].publicKey, - preKeyId: response[i].keyId, - registrationId: response[i].registrationId + encodedNumber: number + "." + response.devices[i].deviceId, + identityKey: response.identityKey, + preKey: response.devices[i].preKey.publicKey, + preKeyId: response.devices[i].preKey.keyId, + 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) { - if (error.humanError) - message = error.humanError; + if (error) { + if (error.humanError) + message = error.humanError; + } else + error = new Error(message); errors[errors.length] = { number: number, reason: message, error: error }; numberCompleted(); } @@ -123,7 +123,7 @@ window.textsecure.messaging = function() { return function() { var devicesForNumber = textsecure.storage.devices.getDeviceObjectsForNumber(number); 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() { doSendMessage(number, devicesForNumber, recurse); }); diff --git a/js/test.js b/js/test.js index 6f67af85dd..720646e473 100644 --- a/js/test.js +++ b/js/test.js @@ -61,8 +61,11 @@ function TEST(func, name, exclusive) { if (testsOutstanding[testIndex] == undefined) testsdiv.append('
' + funcName + ' called back multiple times
'); else if (error !== undefined) { - console.log(error.stack); - testsdiv.append('' + funcName + ' threw ' + error + '
'); + if (error && error.stack) { + console.log(error.stack); + testsdiv.append('' + funcName + ' threw ' + error + '
'); + } else + testsdiv.append('' + funcName + ' threw non-Error: ' + error + '
'); } else if (result === true) testsdiv.append('' + funcName + ' passed
'); else @@ -276,12 +279,14 @@ textsecure.registerOnLoadFunction(function() { ourEphemeralKey: hexToArrayBuffer('f12704787bab04a3cf544ebd9d421b6fe36147519eb5afa7c90e3fb67c141e64'), ourIdentityKey: hexToArrayBuffer('a05fd14abb42ff393004eee526e3167441ee51021c6d801b784720c15637747c'), registrationId: 11593, - getKeys: [{deviceId: 0, - keyId: 13845842, - publicKey: hexToArrayBuffer('05fee424a5b6ccb717d85ef2207e2057ab1144c40afe89cdc80e9c424dd90c146e'), - identityKey: hexToArrayBuffer('05276e4df34557386f67df38b708eeddb1a8924e0428b9eefdc9213c3e8927cc7d'), - registrationId: 42} - ], + getKeys: {identityKey: hexToArrayBuffer('05276e4df34557386f67df38b708eeddb1a8924e0428b9eefdc9213c3e8927cc7d'), + devices: [{ + deviceId: 0, + preKey: {keyId: 13845842, publicKey: hexToArrayBuffer('05fee424a5b6ccb717d85ef2207e2057ab1144c40afe89cdc80e9c424dd90c146e')}, + signedPreKey: {keyId: 13845842, publicKey: hexToArrayBuffer('05fee424a5b6ccb717d85ef2207e2057ab1144c40afe89cdc80e9c424dd90c146e')}, + registrationId: 42 + }] + }, //expectedPlaintext: hexToArrayBuffer('0a0e4120202020202020202020202020'), //expectedCounter: 0, expectedCiphertext: hexToArrayBuffer('2208d28acd061221059ab4e844771bfeb96382edac5f80e757a1109b5611c770b2ba9f28b363d7c2541a2105bd61aea7fa5304f4dc914892bc3795812cda8bb90b73de9920e22c609cf0ec4e2242220a21058c0c357a3a25e6da46b0186d93fec31d5b86a4ac4973742012d8e9de2346be161000180022104bd27ab87ee151d71cdfe89828050ef4b05bddfb56da491728c95a'),