
Adds Backbone-based Whisper.Messages model/collection with local storage extension. Saves sent and received messages in Whisper.Messages instead of message map. This will assign a unique id to the message and save it to localStorage. Adds Backbone-based view to popup.html Automatically updates itself when new messages are saved to Whisper.Messages db from the background page. Added some shiny new styles, and started splitting up css into multiple files for sanity's sake.
596 lines
18 KiB
JavaScript
596 lines
18 KiB
JavaScript
/* 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/>.
|
|
*/
|
|
|
|
/* START CRAP TO BE DELETED */
|
|
//TODO: Stolen from MDN (copyright...)
|
|
function b64ToUint6 (nChr) {
|
|
|
|
return nChr > 64 && nChr < 91 ?
|
|
nChr - 65
|
|
: nChr > 96 && nChr < 123 ?
|
|
nChr - 71
|
|
: nChr > 47 && nChr < 58 ?
|
|
nChr + 4
|
|
: nChr === 43 ?
|
|
62
|
|
: nChr === 47 ?
|
|
63
|
|
:
|
|
0;
|
|
|
|
}
|
|
|
|
function base64DecToArr (sBase64, nBlocksSize) {
|
|
var
|
|
sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length,
|
|
nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize : nInLen * 3 + 1 >> 2;
|
|
var aBBytes = new ArrayBuffer(nOutLen);
|
|
var taBytes = new Uint8Array(aBBytes);
|
|
|
|
for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
|
|
nMod4 = nInIdx & 3;
|
|
nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
|
|
if (nMod4 === 3 || nInLen - nInIdx === 1) {
|
|
for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
|
|
taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
|
|
}
|
|
nUint24 = 0;
|
|
}
|
|
}
|
|
return aBBytes;
|
|
}
|
|
|
|
/* Base64 string to array encoding */
|
|
|
|
function uint6ToB64 (nUint6) {
|
|
|
|
return nUint6 < 26 ?
|
|
nUint6 + 65
|
|
: nUint6 < 52 ?
|
|
nUint6 + 71
|
|
: nUint6 < 62 ?
|
|
nUint6 - 4
|
|
: nUint6 === 62 ?
|
|
43
|
|
: nUint6 === 63 ?
|
|
47
|
|
:
|
|
65;
|
|
|
|
}
|
|
|
|
function base64EncArr (aBytes) {
|
|
|
|
var nMod3, sB64Enc = "";
|
|
|
|
for (var nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) {
|
|
nMod3 = nIdx % 3;
|
|
//if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) { sB64Enc += "\r\n"; }
|
|
nUint24 |= aBytes[nIdx] << (16 >>> nMod3 & 24);
|
|
if (nMod3 === 2 || aBytes.length - nIdx === 1) {
|
|
sB64Enc += String.fromCharCode(uint6ToB64(nUint24 >>> 18 & 63), uint6ToB64(nUint24 >>> 12 & 63), uint6ToB64(nUint24 >>> 6 & 63), uint6ToB64(nUint24 & 63));
|
|
nUint24 = 0;
|
|
}
|
|
}
|
|
|
|
return sB64Enc.replace(/A(?=A$|$)/g, "=");
|
|
|
|
}
|
|
|
|
/* END CRAP TO BE DELETED */
|
|
|
|
window.textsecure = window.textsecure || {};
|
|
|
|
/*********************************
|
|
*** Type conversion utilities ***
|
|
*********************************/
|
|
// Strings/arrays
|
|
//TODO: Throw all this shit in favor of consistent types
|
|
var StaticByteBufferProto = new dcodeIO.ByteBuffer().__proto__;
|
|
var StaticArrayBufferProto = new ArrayBuffer().__proto__;
|
|
var StaticUint8ArrayProto = new Uint8Array().__proto__;
|
|
var StaticWordArrayProto = CryptoJS.lib.WordArray.create('').__proto__;
|
|
function getString(thing) {
|
|
if (thing === Object(thing)) {
|
|
if (thing.__proto__ == StaticUint8ArrayProto)
|
|
return String.fromCharCode.apply(null, thing);
|
|
if (thing.__proto__ == StaticArrayBufferProto)
|
|
return getString(new Uint8Array(thing));
|
|
if (thing.__proto__ == StaticByteBufferProto)
|
|
return thing.toString("binary");
|
|
if (thing.__proto__ == StaticWordArrayProto)
|
|
return thing.toString(CryptoJS.enc.Latin1);
|
|
}
|
|
return thing;
|
|
}
|
|
|
|
function getStringable(thing) {
|
|
return (typeof thing == "string" || typeof thing == "number" || typeof thing == "boolean" ||
|
|
(thing === Object(thing) &&
|
|
(thing.__proto__ == StaticArrayBufferProto ||
|
|
thing.__proto__ == StaticUint8ArrayProto ||
|
|
thing.__proto__ == StaticByteBufferProto ||
|
|
thing.__proto__ == StaticWordArrayProto)));
|
|
}
|
|
|
|
function toArrayBuffer(thing) {
|
|
//TODO: Optimize this for specific cases
|
|
if (thing === undefined)
|
|
return undefined;
|
|
if (thing === Object(thing) && thing.__proto__ == StaticArrayBufferProto)
|
|
return thing;
|
|
|
|
if (thing instanceof Array) {
|
|
// Assuming Uint16Array from curve25519
|
|
var res = new ArrayBuffer(thing.length * 2);
|
|
var uint = new Uint16Array(res);
|
|
for (var i = 0; i < thing.length; i++)
|
|
uint[i] = thing[i];
|
|
return res;
|
|
}
|
|
|
|
if (!getStringable(thing))
|
|
throw new Error("Tried to convert a non-stringable thing of type " + typeof thing + " to an array buffer");
|
|
var str = getString(thing);
|
|
var res = new ArrayBuffer(str.length);
|
|
var uint = new Uint8Array(res);
|
|
for (var i = 0; i < str.length; i++)
|
|
uint[i] = str.charCodeAt(i);
|
|
return res;
|
|
}
|
|
|
|
function ensureStringed(thing) {
|
|
if (getStringable(thing))
|
|
return getString(thing);
|
|
else if (thing instanceof Array) {
|
|
var res = [];
|
|
for (var i = 0; i < thing.length; i++)
|
|
res[i] = ensureStringed(thing[i]);
|
|
return res;
|
|
} else if (thing === Object(thing)) {
|
|
var res = {};
|
|
for (key in thing)
|
|
res[key] = ensureStringed(thing[key]);
|
|
return res;
|
|
}
|
|
throw new Error("unsure of how to jsonify object of type " + typeof thing);
|
|
|
|
}
|
|
|
|
function jsonThing(thing) {
|
|
return JSON.stringify(ensureStringed(thing));
|
|
}
|
|
|
|
function base64ToArrayBuffer(string) {
|
|
return base64DecToArr(string);
|
|
}
|
|
|
|
// Protobuf decoding
|
|
//TODO: throw on missing fields everywhere
|
|
var IncomingPushMessageProtobuf = dcodeIO.ProtoBuf.loadProtoFile("protos/IncomingPushMessageSignal.proto").build("textsecure.IncomingPushMessageSignal");
|
|
function decodeIncomingPushMessageProtobuf(string) {
|
|
return IncomingPushMessageProtobuf.decode(btoa(string));
|
|
}
|
|
|
|
var PushMessageContentProtobuf = dcodeIO.ProtoBuf.loadProtoFile("protos/IncomingPushMessageSignal.proto").build("textsecure.PushMessageContent");
|
|
function decodePushMessageContentProtobuf(string) {
|
|
return PushMessageContentProtobuf.decode(btoa(string));
|
|
}
|
|
|
|
var WhisperMessageProtobuf = dcodeIO.ProtoBuf.loadProtoFile("protos/WhisperTextProtocol.proto").build("textsecure.WhisperMessage");
|
|
function decodeWhisperMessageProtobuf(string) {
|
|
return WhisperMessageProtobuf.decode(btoa(string));
|
|
}
|
|
|
|
var PreKeyWhisperMessageProtobuf = dcodeIO.ProtoBuf.loadProtoFile("protos/WhisperTextProtocol.proto").build("textsecure.PreKeyWhisperMessage");
|
|
function decodePreKeyWhisperMessageProtobuf(string) {
|
|
return PreKeyWhisperMessageProtobuf.decode(btoa(string));
|
|
}
|
|
|
|
var KeyExchangeMessageProtobuf = dcodeIO.ProtoBuf.loadProtoFile("protos/WhisperTextProtocol.proto").build("textsecure.KeyExchangeMessage");
|
|
function decodeKeyExchangeMessageProtobuf(string) {
|
|
return KeyExchangeMessageProtobuf.decode(btoa(string));
|
|
}
|
|
|
|
// Number formatting
|
|
function getNumberFromString(string) {
|
|
return string.split(".")[0];
|
|
}
|
|
|
|
function getEncodedNumber(number) {
|
|
var split = number.split(".");
|
|
if (split.length > 1) {
|
|
if (split[1] == 1)
|
|
return split[0];
|
|
else
|
|
return number;
|
|
} else
|
|
return number;
|
|
}
|
|
|
|
function verifyNumber(string) {
|
|
//TODO: fancy country-code guessing and number verification
|
|
return getEncodedNumber(string.trim());
|
|
}
|
|
|
|
// Other
|
|
|
|
function timestampToHumanReadable(timestamp) {
|
|
var date = new Date();
|
|
date.setTime(timestamp*1000);
|
|
return date.toUTCString();
|
|
}
|
|
|
|
function objectContainsKeys(object) {
|
|
var count = 0;
|
|
for (key in object) {
|
|
count++;
|
|
break;
|
|
}
|
|
return count != 0;
|
|
}
|
|
|
|
/************************************************
|
|
*** Utilities to store data in local storage ***
|
|
************************************************/
|
|
//TODO: textsecure.storage
|
|
window.storage = function() {
|
|
var self = {};
|
|
|
|
/*****************************
|
|
*** Base Storage Routines ***
|
|
*****************************/
|
|
self.putEncrypted = function(key, value) {
|
|
//TODO
|
|
if (value === undefined)
|
|
throw new Error("Tried to store undefined");
|
|
localStorage.setItem("e" + key, jsonThing(value));
|
|
}
|
|
|
|
self.getEncrypted = function(key, defaultValue) {
|
|
//TODO
|
|
var value = localStorage.getItem("e" + key);
|
|
if (value === null)
|
|
return defaultValue;
|
|
return JSON.parse(value);
|
|
}
|
|
|
|
self.removeEncrypted = function(key) {
|
|
localStorage.removeItem("e" + key);
|
|
}
|
|
|
|
self.putUnencrypted = function(key, value) {
|
|
if (value === undefined)
|
|
throw new Error("Tried to store undefined");
|
|
localStorage.setItem("u" + key, jsonThing(value));
|
|
}
|
|
|
|
self.getUnencrypted = function(key, defaultValue) {
|
|
var value = localStorage.getItem("u" + key);
|
|
if (value === null)
|
|
return defaultValue;
|
|
return JSON.parse(value);
|
|
}
|
|
|
|
self.removeUnencrypted = function(key) {
|
|
localStorage.removeItem("u" + key);
|
|
}
|
|
|
|
/**********************
|
|
*** Device Storage ***
|
|
**********************/
|
|
self.devices = function() {
|
|
var self = {};
|
|
|
|
self.getDeviceObject = function(encodedNumber) {
|
|
return storage.getEncrypted("deviceObject" + getEncodedNumber(encodedNumber));
|
|
}
|
|
|
|
self.getDeviceIdListFromNumber = function(number) {
|
|
return storage.getEncrypted("deviceIdList" + getNumberFromString(number), []);
|
|
}
|
|
|
|
self.addDeviceIdForNumber = function(number, deviceId) {
|
|
var deviceIdList = this.getDeviceIdListFromNumber(getNumberFromString(number));
|
|
for (var i = 0; i < deviceIdList.length; i++) {
|
|
if (deviceIdList[i] == deviceId)
|
|
return;
|
|
}
|
|
deviceIdList[deviceIdList.length] = deviceId;
|
|
storage.putEncrypted("deviceIdList" + getNumberFromString(number), deviceIdList);
|
|
}
|
|
|
|
var getDeviceId = function(encodedNumber) {
|
|
var split = encodedNumber.split(".");
|
|
if (split.length > 1)
|
|
return split[1];
|
|
return 1;
|
|
}
|
|
|
|
// throws "Identity key mismatch"
|
|
self.saveDeviceObject = function(deviceObject) {
|
|
var existing = this.getDeviceObject(deviceObject.encodedNumber);
|
|
if (existing === undefined)
|
|
existing = {encodedNumber: getEncodedNumber(deviceObject.encodedNumber)};
|
|
for (key in deviceObject) {
|
|
if (key == "encodedNumber")
|
|
continue;
|
|
|
|
if (key == "identityKey" && deviceObject.identityKey != deviceObject.identityKey)
|
|
throw new Error("Identity key mismatch");
|
|
|
|
existing[key] = deviceObject[key];
|
|
}
|
|
storage.putEncrypted("deviceObject" + getEncodedNumber(deviceObject.encodedNumber), existing);
|
|
this.addDeviceIdForNumber(deviceObject.encodedNumber, getDeviceId(deviceObject.encodedNumber));
|
|
}
|
|
|
|
self.getDeviceObjectListFromNumber = function(number) {
|
|
var deviceObjectList = [];
|
|
var deviceIdList = this.getDeviceIdListFromNumber(number);
|
|
for (var i = 0; i < deviceIdList.length; i++)
|
|
deviceObjectList[deviceObjectList.length] = this.getDeviceObject(getNumberFromString(number) + "." + deviceIdList[i]);
|
|
return deviceObjectList;
|
|
}
|
|
|
|
return self;
|
|
}();
|
|
|
|
return self;
|
|
}();
|
|
|
|
function registrationDone() {
|
|
storage.putUnencrypted("registration_done", "");
|
|
//TODO: Fix dirty hack:
|
|
chrome.runtime.reload();
|
|
}
|
|
|
|
function isRegistrationDone() {
|
|
return storage.getUnencrypted("registration_done") !== undefined;
|
|
}
|
|
|
|
/**********************
|
|
*** NaCL Interface ***
|
|
**********************/
|
|
window.textsecure.nacl = function() {
|
|
var self = {};
|
|
|
|
self.USE_NACL = false;
|
|
|
|
var onLoadCallbacks = [];
|
|
var naclLoaded = 0;
|
|
self.registerOnLoadFunction = function(func) {
|
|
if (naclLoaded || !self.USE_NACL) {
|
|
func();
|
|
return;
|
|
}
|
|
onLoadCallbacks[onLoadCallbacks.length] = func;
|
|
}
|
|
|
|
var naclMessageNextId = 0;
|
|
var naclMessageIdCallbackMap = {};
|
|
window.moduleDidLoad = function() {
|
|
common.hideModule();
|
|
naclLoaded = 1;
|
|
for (var i = 0; i < onLoadCallbacks.length; i++)
|
|
onLoadCallbacks[i]();
|
|
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;
|
|
|
|
// message_callback({message: decryptedMessage, pushMessage: server-providedPushMessage})
|
|
window.textsecure.subscribeToPush = function() {
|
|
var subscribeToPushMessageSemaphore = 0;
|
|
return function(message_callback) {
|
|
subscribeToPushMessageSemaphore++;
|
|
if (subscribeToPushMessageSemaphore <= 0)
|
|
return;
|
|
|
|
var socket = textsecure.api.getWebsocket();
|
|
|
|
var pingInterval;
|
|
|
|
//TODO: GUI
|
|
socket.onerror = function(socketEvent) {
|
|
console.log('Server is down :(');
|
|
clearInterval(pingInterval);
|
|
subscribeToPushMessageSemaphore--;
|
|
setTimeout(function() { textsecure.subscribeToPush(message_callback); }, 60000);
|
|
};
|
|
socket.onclose = function(socketEvent) {
|
|
console.log('Server closed :(');
|
|
clearInterval(pingInterval);
|
|
subscribeToPushMessageSemaphore--;
|
|
setTimeout(function() { textsecure.subscribeToPush(message_callback); }, 60000);
|
|
};
|
|
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);
|
|
};
|
|
|
|
socket.onmessage = function(response) {
|
|
try {
|
|
var message = JSON.parse(response.data);
|
|
} catch (e) {
|
|
console.log('Error parsing server JSON message: ' + response.responseBody.split("|")[1]);
|
|
return;
|
|
}
|
|
|
|
if (message.type == 3) {
|
|
console.log("Got pong message");
|
|
} else if (message.type === undefined && message.id !== undefined) {
|
|
textsecure.crypto.decryptWebsocketMessage(message.message).then(function(plaintext) {
|
|
var proto = decodeIncomingPushMessageProtobuf(getString(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) {
|
|
var handleAttachment = function(attachment) {
|
|
return textsecure.api.getAttachment(attachment.id).then(function(encryptedBin) {
|
|
return textsecure.crypto.decryptAttachment(encryptedBin, toArrayBuffer(attachment.key)).then(function(decryptedBin) {
|
|
attachment.decrypted = decryptedBin;
|
|
});
|
|
});
|
|
};
|
|
|
|
var promises = [];
|
|
for (var i = 0; i < decrypted.message.attachments.length; i++) {
|
|
promises[i] = handleAttachment(decrypted.message.attachments[i]);
|
|
}
|
|
return Promise.all(promises).then(function() {
|
|
Whisper.Messages.addIncomingMessage(decrypted);
|
|
message_callback(decrypted);
|
|
});
|
|
})
|
|
}).catch(function(e) {
|
|
console.log("Error handling incoming message: ");
|
|
console.log(e);
|
|
});
|
|
}
|
|
};
|
|
}
|
|
}();
|
|
|
|
// sendMessage(numbers = [], message = PushMessageContentProto, callback(success/failure map))
|
|
window.textsecure.sendMessage = function() {
|
|
function getKeysForNumber(number) {
|
|
return textsecure.api.getKeysForNumber(number).then(function(response) {
|
|
for (var i = 0; i < response.length; i++) {
|
|
storage.devices.saveDeviceObject({
|
|
encodedNumber: number + "." + response[i].deviceId,
|
|
identityKey: response[i].identityKey,
|
|
publicKey: response[i].publicKey,
|
|
preKeyId: response[i].keyId,
|
|
registrationId: response[i].registrationId
|
|
});
|
|
}
|
|
return response[0].identityKey;
|
|
});
|
|
}
|
|
|
|
// success_callback(server success/failure map), error_callback(error_msg)
|
|
// message == PushMessageContentProto (NOT STRING)
|
|
function sendMessageToDevices(number, deviceObjectList, message, success_callback, error_callback) {
|
|
var jsonData = [];
|
|
var relay = undefined;
|
|
var promises = [];
|
|
|
|
var addEncryptionFor = function(i) {
|
|
return textsecure.crypto.encryptMessageFor(deviceObjectList[i], message).then(function(encryptedMsg) {
|
|
jsonData[i] = {
|
|
type: encryptedMsg.type,
|
|
destination: deviceObjectList[i].encodedNumber,
|
|
destinationRegistrationId: deviceObjectList[i].registrationId,
|
|
body: encryptedMsg.body,
|
|
timestamp: new Date().getTime()
|
|
};
|
|
|
|
if (deviceObjectList[i].relay !== undefined) {
|
|
jsonData[i].relay = deviceObjectList[i].relay;
|
|
if (relay === undefined)
|
|
relay = jsonData[i].relay;
|
|
else if (relay != jsonData[i].relay)
|
|
throw new Error("Mismatched relays for number " + number);
|
|
} else {
|
|
if (relay === undefined)
|
|
relay = "";
|
|
else if (relay != "")
|
|
throw new Error("Mismatched relays for number " + number);
|
|
}
|
|
});
|
|
}
|
|
for (var i = 0; i < deviceObjectList.length; i++)
|
|
promises[i] = addEncryptionFor(i);
|
|
return Promise.all(promises).then(function() {
|
|
return textsecure.api.sendMessages(number, jsonData);
|
|
});
|
|
}
|
|
|
|
return function(numbers, message, callback) {
|
|
var numbersCompleted = 0;
|
|
var errors = [];
|
|
var successfulNumbers = [];
|
|
|
|
var numberCompleted = function() {
|
|
numbersCompleted++;
|
|
if (numbersCompleted >= numbers.length)
|
|
callback({success: successfulNumbers, failure: errors});
|
|
}
|
|
|
|
var registerError = function(number, message, error) {
|
|
errors[errors.length] = { number: number, reason: message, error: error };
|
|
numberCompleted();
|
|
}
|
|
|
|
var doSendMessage = function(number, devicesForNumber, message) {
|
|
return sendMessageToDevices(number, devicesForNumber, message).then(function(result) {
|
|
successfulNumbers[successfulNumbers.length] = number;
|
|
numberCompleted();
|
|
}).catch(function(error) {
|
|
if (error instanceof Error && error.name == "HTTPError" && (error.message == 410 || error.message == 409)) {
|
|
//TODO: Re-request keys for number here
|
|
}
|
|
registerError(number, "Failed to create or send message", error);
|
|
});
|
|
}
|
|
|
|
for (var i = 0; i < numbers.length; i++) {
|
|
var number = numbers[i];
|
|
var devicesForNumber = storage.devices.getDeviceObjectListFromNumber(number);
|
|
|
|
if (devicesForNumber.length == 0) {
|
|
getKeysForNumber(number).then(function(identity_key) {
|
|
devicesForNumber = storage.devices.getDeviceObjectListFromNumber(number);
|
|
if (devicesForNumber.length == 0)
|
|
registerError(number, "Failed to retreive new device keys for number " + number, null);
|
|
else
|
|
doSendMessage(number, devicesForNumber, message);
|
|
}).catch(function(error) {
|
|
registerError(number, "Failed to retreive new device keys for number " + number, error);
|
|
});
|
|
} else
|
|
doSendMessage(number, devicesForNumber, message);
|
|
}
|
|
}
|
|
}();
|
|
|
|
function requestIdentityPrivKeyFromMasterDevice(number, identityKey) {
|
|
sendMessageToDevices([storage.devices.getDeviceObject(getNumberFromString(number)) + ".1"],
|
|
{message: "Identity Key request"}, function() {}, function() {});//TODO
|
|
}
|
|
|