Flo-Whatsapp/app/app.js
sairajzero eaa3470cf4 Security Fix and minor UI changes
Verify privKey of groupID when creating new group
Names and Messages are now added in textContent instead of innerHTML to prevent HTML injection
Both direct message and group message use the same IDB objectStore
Converted send message input to textarea : now users can send multi-line messages
Minor UI changes and fixes
Improved Enter Key Press :
Shift+Enter key will now insert a new line
(Enter key pressed without shift key will send message as before)
Enter key event to send message will now tigger on keydown instead of keyup
2019-09-15 19:05:50 +05:30

1531 lines
49 KiB
JavaScript

window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction;
window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange;
if (!window.indexedDB) {
window.alert("Your browser doesn't support a stable version of IndexedDB.")
}
var contacts, groups;
var searchIndex = new FlexSearch();
var receiverID, selfID, recStat, modSuperNode, msgType;
var selfwebsocket, receiverWebSocket;
var privKey;
var floOpt = {
p: BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16),
exponent1: function () {
return floOpt.p.add(BigInteger.ONE).divide(BigInteger("4"))
},
calculateY: function (x) {
let p = this.p;
let exp = this.exponent1();
// x is x value of public key in BigInteger format without 02 or 03 or 04 prefix
return x.modPow(BigInteger("3"), p).add(BigInteger("7")).mod(p).modPow(exp, p)
},
// Insert a compressed public key
getUncompressedPublicKey: function (compressedPublicKey) {
const p = this.p;
// Fetch x from compressedPublicKey
let pubKeyBytes = Crypto.util.hexToBytes(compressedPublicKey);
const prefix = pubKeyBytes.shift() // remove prefix
let prefix_modulus = prefix % 2;
pubKeyBytes.unshift(0) // add prefix 0
let x = new BigInteger(pubKeyBytes)
let xDecimalValue = x.toString()
// Fetch y
let y = this.calculateY(x);
let yDecimalValue = y.toString();
// verify y value
let resultBigInt = y.mod(BigInteger("2"));
let check = resultBigInt.toString() % 2;
if (prefix_modulus !== check) {
yDecimalValue = y.negate().mod(p).toString();
}
return {
x: xDecimalValue,
y: yDecimalValue
};
},
getSenderPublicKeyString: function () {
privateKey = ellipticCurveEncryption.senderRandom();
senderPublicKeyString = ellipticCurveEncryption.senderPublicString(privateKey);
return {
privateKey: privateKey,
senderPublicKeyString: senderPublicKeyString
}
},
deriveSharedKeySender: function (receiverCompressedPublicKey, senderPrivateKey) {
try {
let receiverPublicKeyString = this.getUncompressedPublicKey(
receiverCompressedPublicKey);
var senderDerivedKey = {
XValue: "",
YValue: ""
};
senderDerivedKey = ellipticCurveEncryption.senderSharedKeyDerivation(
receiverPublicKeyString.x,
receiverPublicKeyString.y, senderPrivateKey);
return senderDerivedKey;
} catch (error) {
return new Error(error);
}
},
deriveReceiverSharedKey: function (senderPublicKeyString, receiverPrivateKey) {
return ellipticCurveEncryption.receiverSharedKeyDerivation(
senderPublicKeyString.XValuePublicString, senderPublicKeyString.YValuePublicString,
receiverPrivateKey);
},
getReceiverPublicKeyString: function (privateKey) {
return ellipticCurveEncryption.receiverPublicString(privateKey);
},
deriveSharedKeyReceiver: function (senderPublicKeyString, receiverPrivateKey) {
try {
return ellipticCurveEncryption.receiverSharedKeyDerivation(senderPublicKeyString.XValuePublicString,
senderPublicKeyString.YValuePublicString, receiverPrivateKey);
} catch (error) {
return new Error(error);
}
},
encryptData: function (data, receiverCompressedPublicKey) {
var senderECKeyData = this.getSenderPublicKeyString();
var senderDerivedKey = {
XValue: "",
YValue: ""
};
var senderPublicKeyString = {};
senderDerivedKey = this.deriveSharedKeySender(
receiverCompressedPublicKey, senderECKeyData.privateKey);
console.log("senderDerivedKey", senderDerivedKey);
let senderKey = senderDerivedKey.XValue + senderDerivedKey.YValue;
let secret = Crypto.AES.encrypt(data, senderKey);
return {
secret: secret,
pubVal: senderECKeyData.senderPublicKeyString
};
},
decryptData: function (secret, senderPublicKeyString, myPrivateKey) {
var receiverDerivedKey = {
XValue: "",
YValue: ""
};
var receiverECKeyData = {};
if (typeof myPrivateKey !== "string") throw new Error("No private key found.");
let privateKey = this.wifToDecimal(myPrivateKey, true);
if (typeof privateKey.privateKeyDecimal !== "string") throw new Error(
"Failed to detremine your private key.");
receiverECKeyData.privateKey = privateKey.privateKeyDecimal;
receiverDerivedKey = this.deriveReceiverSharedKey(senderPublicKeyString,
receiverECKeyData.privateKey);
console.log("receiverDerivedKey", receiverDerivedKey);
let receiverKey = receiverDerivedKey.XValue + receiverDerivedKey.YValue;
let decryptMsg = Crypto.AES.decrypt(secret, receiverKey);
return decryptMsg;
},
ecparams: EllipticCurve.getSECCurveByName("secp256k1"),
getPubKeyHex: function (privateKeyHex) {
var key = new Bitcoin.ECKey(privateKeyHex);
if (key.priv == null) {
alert("Invalid Private key");
return;
}
key.setCompressed(true);
var pubkeyHex = key.getPubKeyHex();
return pubkeyHex;
},
getFLOIDfromPubkeyHex: function (pubkeyHex) {
var key = new Bitcoin.ECKey().setPub(pubkeyHex);
var floID = key.getBitcoinAddress();
return floID;
},
signData: function (msg, privateKeyHex) {
var key = new Bitcoin.ECKey(privateKeyHex);
key.setCompressed(true);
var privateKeyArr = key.getBitcoinPrivateKeyByteArray();
privateKey = BigInteger.fromByteArrayUnsigned(privateKeyArr);
var messageHash = Crypto.SHA256(msg);
var messageHashBigInteger = new BigInteger(messageHash);
var messageSign = Bitcoin.ECDSA.sign(messageHashBigInteger, key.priv);
var sighex = Crypto.util.bytesToHex(messageSign);
return sighex;
},
verifyData: function (msg, signatureHex, publicKeyHex) {
var msgHash = Crypto.SHA256(msg);
var messageHashBigInteger = new BigInteger(msgHash);
var sigBytes = Crypto.util.hexToBytes(signatureHex);
var signature = Bitcoin.ECDSA.parseSig(sigBytes);
var publicKeyPoint = this.ecparams.getCurve().decodePointHex(publicKeyHex);
var verify = Bitcoin.ECDSA.verifyRaw(messageHashBigInteger, signature.r, signature.s,
publicKeyPoint);
return verify;
},
wifToDecimal: function (pk_wif, isPubKeyCompressed = false) {
let pk = Bitcoin.Base58.decode(pk_wif)
pk.shift()
pk.splice(-4, 4)
//If the private key corresponded to a compressed public key, also drop the last byte (it should be 0x01).
if (isPubKeyCompressed == true) pk.pop()
pk.unshift(0)
privateKeyDecimal = BigInteger(pk).toString()
privateKeyHex = Crypto.util.bytesToHex(pk)
return {
privateKeyDecimal: privateKeyDecimal,
privateKeyHex: privateKeyHex
}
},
genNewIDpair: function () {
try {
var key = new Bitcoin.ECKey(false);
key.setCompressed(true);
return {
floID: key.getBitcoinAddress(),
pubKey: key.getPubKeyHex(),
privKey: key.getBitcoinWalletImportFormat()
}
} catch (e) {
console.log(e);
}
},
verifyPrivKey: function (privateKeyHex, floID) {
try {
var key = new Bitcoin.ECKey(privateKeyHex);
if (key.priv == null)
return false;
key.setCompressed(true);
if (floID == key.getBitcoinAddress())
return true;
else
return false;
} catch (e) {
console.log(e);
}
}
}
//Script for AJAX, and register functions
function ajax(method, uri) {
var request = new XMLHttpRequest();
var url = `${server}/${uri}`
console.log(url)
var result;
request.open(method, url, false);
request.onload = function () {
if (request.readyState == 4 && request.status == 200)
result = this.response;
else {
console.log('error');
result = false;
}
};
request.send();
console.log(result);
return result;
}
function registerID(sender, onionAddr, wif, pubkey, username) {
var receiver = adminID;
var trx = bitjs.transaction();
var utxoAmt = 0.0;
var x = sendAmt + fee;
var response = ajax("GET", `api/addr/${sender}/utxo`);
var utxos = JSON.parse(response);
for (var x = utxos.length - 1; x >= 0; x--) {
if (utxoAmt < sendAmt + fee) {
trx.addinput(utxos[x].txid, utxos[x].vout, utxos[x].scriptPubKey);
utxoAmt += utxos[x].amount;
} else
break;
}
console.log(utxoAmt + ":" + (sendAmt + fee));
if (utxoAmt < sendAmt + fee) {
alert("Insufficient balance!");
return;
}
trx.addoutput(receiver, sendAmt);
console.log(receiver + ":" + sendAmt);
var change = utxoAmt - sendAmt - fee;
if (change > 0)
trx.addoutput(sender, change);
console.log(sender + ":" + change);
var key = new Bitcoin.ECKey(wif);
var sendFloData = JSON.stringify({
FLO_chat: {
onionAddr: onionAddr,
name: username,
pubKey: pubkey
}
});;
trx.addflodata(sendFloData);
console.log(sendFloData);
var signedTxHash = trx.sign(wif, 1);
console.log(signedTxHash);
return broadcastTx(signedTxHash);
}
function broadcastTx(signedTxHash) {
var http = new XMLHttpRequest();
var url = `${server}/api/tx/send`;
if (signedTxHash.length < 1) {
alert("Empty Signature");
return false;
}
var params = `{"rawtx":"${signedTxHash}"}`;
var result;
http.open('POST', url, false);
//Send the proper header information along with the request
http.setRequestHeader('Content-type', 'application/json');
http.onreadystatechange = () => { //Call a function when the state changes.
if (http.readyState == 4 && http.status == 200) {
console.log(http.response);
var txid = JSON.parse(http.response).txid.result;
alert("Transaction successful! txid : " + txid);
result = true;
} else {
console.log(http.responseText);
result = false;
}
}
http.send(params);
return result;
}
function userDataStartUp() {
console.log("StartUp");
document.getElementById("sendMsgInput").addEventListener("keydown", (event) => {
if (event.keyCode === 13 && !event.shiftKey) {
event.preventDefault();
sendMsg();
}
});
document.getElementById("searchContact").addEventListener("input", searchContact, true);
document.getElementById("searchList").addEventListener("input", searchChecklist, true);
getDatafromAPI().then(result => {
console.log(result);
getContactsfromIDB().then(result => {
contacts = result;
getSuperNodeListfromIDB().then(result => {
console.log(result)
superNodeList = result;
kBucketObj.launchKBucket().then(result => {
console.log(result)
getuserID().then(result => {
console.log(result);
selfID = result;
if (superNodeList.includes(selfID))
modSuperNode = true;
alert(`${selfID}\nWelcome ${contacts[selfID].name}`)
getGroupsfromIDB().then(result => {
groups = result;
readMsgfromIDB().then(result => {
console.log(result);
initselfWebSocket();
pingSuperNodeForAwayMessages();
displayContacts();
const createClock = setInterval(checkStatusInterval, 30000);
}).catch(error => {
console.log(error);
});
}).catch(error => {
console.log(error.message);
});
}).catch(error => {
console.log(error.message);
});
}).catch(error => {
console.log(error.message);
});
}).catch(error => {
console.log(error.message);
});
}).catch(error => {
console.log(error.message);
});
}).catch(error => {
console.log(error.message);
});
}
function storeContact(data) {
return new Promise((resolve, reject) => {
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
console.log("Error in opening IndexedDB!");
};
idb.onsuccess = (event) => {
var db = event.target.result;
var obs = db.transaction('contacts', "readwrite").objectStore('contacts');
objectRequest = obs.put(data);
objectRequest.onerror = (event) => {
reject(Error('Error occured: Unable to store data'));
};
objectRequest.onsuccess = (event) => {
resolve('Data saved OK');
db.close();
};
};
});
}
function storeSuperNodeData(data) {
return new Promise((resolve, reject) => {
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
reject("Error in opening IndexedDB!");
};
idb.onsuccess = (event) => {
var db = event.target.result;
var obs = db.transaction('superNodes', "readwrite").objectStore('superNodes');
if (data.addNodes)
for (var i = 0; i < data.addNodes.length; i++)
obs.add(true, data.addNodes[i])
if (data.removeNodes)
for (var i = 0; i < data.removeNodes.length; i++)
obs.delete(data.removeNodes[i])
db.close();
resolve('Updated superNodes list in IDB');
};
});
}
function getDatafromAPI() {
return new Promise((resolve, reject) => {
var addr = adminID;
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
console.log("Error in opening IndexedDB!");
};
idb.onupgradeneeded = (event) => {
var db = event.target.result;
var objectStore0 = db.createObjectStore("superNodes");
var objectStore1 = db.createObjectStore("contacts", {
keyPath: 'floID'
});
objectStore1.createIndex('onionAddr', 'onionAddr', {
unique: false
});
objectStore1.createIndex('name', 'name', {
unique: false
});
objectStore1.createIndex('pubKey', 'pubKey', {
unique: false
});
var objectStore2 = db.createObjectStore("lastTx");
var objectStore3 = db.createObjectStore("messages", {
keyPath: 'time'
});
objectStore3.createIndex('text', 'text', {
unique: false
});
objectStore3.createIndex('floID', 'floID', {
unique: false
});
objectStore3.createIndex('groupID', 'groupID', {
unique: false
});
objectStore3.createIndex('sender', 'sender', {
unique: false
});
objectStore3.createIndex('type', 'type', {
unique: false
});
var objectStore4 = db.createObjectStore("groups", {
keyPath: 'groupID'
});
objectStore4.createIndex('groupInfo', 'groupInfo', {
unique: false
});
};
idb.onsuccess = (event) => {
var db = event.target.result;
var lastTx = db.transaction('lastTx', "readwrite").objectStore('lastTx');
console.log(addr);
new Promise((res, rej) => {
var lastTxReq = lastTx.get(addr);
lastTxReq.onsuccess = (event) => {
var lasttx = event.target.result;
if (lasttx === undefined) {
lasttx = 0;
}
res(lasttx);
}
}).then(lasttx => {
var response = ajax("GET", `api/addrs/${addr}/txs`);
var nRequired = JSON.parse(response).totalItems - lasttx;
console.log(nRequired);
while (true && nRequired) {
var response = ajax("GET", `api/addrs/${addr}/txs?from=0&to=${nRequired}`);
response = JSON.parse(response);
if (nRequired + lasttx != response.totalItems) {
nRequired = response.totalItems - lasttx;
continue;
}
response.items.reverse().forEach(tx => {
try {
if (tx.vin[0].addr == addr) {
var data = JSON.parse(tx.floData).FLO_chat_SuperNode;
if (data !== undefined) {
storeSuperNodeData(data).then(response => {}).catch(error => {
console.log(error.message);
});
}
} else {
var data = JSON.parse(tx.floData).FLO_chat;
if (data !== undefined) {
if (floOpt.getFLOIDfromPubkeyHex(data.pubKey) != tx.vin[0].addr)
throw ("PublicKey doesnot match with floID")
data = {
floID: tx.vin[0].addr,
onionAddr: data.onionAddr,
name: data.name,
pubKey: data.pubKey
};
storeContact(data).then(response => {}).catch(error => {
console.log(error.message);
});
}
}
} catch (e) {
console.log(e)
}
});
var obs = db.transaction('lastTx', "readwrite").objectStore('lastTx');
obs.put(response.totalItems, addr);
break;
}
db.close();
resolve('retrived data from API');
});
};
});
}
function getuserID() {
return new Promise((resolve, reject) => {
privKey = prompt("Enter FLO Private Key : ")
var key = new Bitcoin.ECKey(privKey);
while (key.priv == null) {
privKey = prompt("Invalid FLO Private Key! Retry : ")
key = Bitcoin.ECKey(privKey);
}
key.setCompressed(true);
var userID = key.getBitcoinAddress();
if (contacts[userID] === undefined)
var reg = confirm(`${userID} is not registers to FLO chat!\nRegister FLO ID to this onion?`);
else if (contacts[userID].onionAddr == window.location.host)
resolve(userID)
else
var reg = confirm(`${userID} is registered to another onion!\nChange to this onion?`);
if (reg) {
var name = prompt("Enter your name :");
var pubKey = key.getPubKeyHex();
if (registerID(userID, window.location.host, privKey, pubKey, name)) {
contacts[userID] = {
onionAddr: window.location.host,
name: name,
pubKey: pubKey
};
resolve(userID);
}
}
reject(`Unable to bind ${userID} to this onionAddress!\nTry again later!`);
});
}
function getContactsfromIDB() {
return new Promise((resolve, reject) => {
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
reject("Error in opening IndexedDB!");
};
idb.onsuccess = (event) => {
var db = event.target.result;
var obs = db.transaction("contacts", "readwrite").objectStore("contacts");
var getReq = obs.getAll();
getReq.onsuccess = (event) => {
var result = {}
event.target.result.forEach(c => {
result[c.floID] = c;
searchIndex.add(c.floID, c.name + ' @' + c.floID);
});
resolve(result);
}
getReq.onerror = (event) => {
reject('Unable to read contacts!')
}
db.close();
};
});
}
function getGroupsfromIDB() {
return new Promise((resolve, reject) => {
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
reject("Error in opening IndexedDB!");
};
idb.onsuccess = (event) => {
var db = event.target.result;
var obs = db.transaction("groups", "readwrite").objectStore("groups");
var getReq = obs.getAll();
getReq.onsuccess = (event) => {
var result = {}
event.target.result.forEach(g => {
var gInfo = JSON.parse(g.groupInfo);
result[g.groupID] = gInfo;
searchIndex.add(g.groupID, gInfo.name + ' #' + gInfo.floID);
});
resolve(result);
}
getReq.onerror = (event) => {
reject('Unable to read groups!')
}
db.close();
};
});
}
function getSuperNodeListfromIDB() {
return new Promise((resolve, reject) => {
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
reject("Error in opening IndexedDB!");
};
idb.onsuccess = (event) => {
var db = event.target.result;
var obs = db.transaction("superNodes", "readwrite").objectStore("superNodes");
var getReq = obs.getAllKeys();
getReq.onsuccess = (event) => {
resolve(event.target.result);
}
getReq.onerror = (event) => {
reject('Unable to read superNode list!')
}
db.close();
};
});
}
function readMsgfromIDB() {
return new Promise((resolve, reject) => {
var disp = document.getElementById("conversation");
for (floID in contacts) {
var createLi = document.createElement('div');
createLi.setAttribute("id", floID);
createLi.setAttribute("class", "message-inner");
createLi.style.display = 'none';
disp.appendChild(createLi);
}
for (floID in groups) {
var createLi = document.createElement('div');
createLi.setAttribute("id", floID);
createLi.setAttribute("class", "message-inner");
createLi.style.display = 'none';
disp.appendChild(createLi);
}
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
reject("Error in opening IndexedDB!");
};
idb.onsuccess = (event) => {
var db = event.target.result;
var obs = db.transaction("messages", "readwrite").objectStore("messages");
obs.openCursor().onsuccess = (event) => {
var cursor = event.target.result;
if (cursor) {
createMsgElement(cursor.value);
cursor.continue();
} else {
resolve("Read Msg from IDB");
}
};
db.close();
};
});
}
function storeMsg(data) {
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
console.log("Error in opening IndexedDB!");
};
idb.onsuccess = (event) => {
var db = event.target.result;
var obs = db.transaction("messages", "readwrite").objectStore("messages");
obs.add(data);
db.close();
};
}
function storeSuperNodeMsg(data) {
var idb = indexedDB.open("FLO_Chat", 2);
idb.onerror = (event) => {
console.log("Error in opening IndexedDB!");
};
idb.onupgradeneeded = (event) => {
var objectStore = event.target.result.createObjectStore("superNodeMsg", {
keyPath: 'id'
});
objectStore.createIndex('from', 'from', {
unique: false
});
objectStore.createIndex('to', 'to', {
unique: false
});
objectStore.createIndex('data', 'data', {
unique: false
});
};
idb.onsuccess = (event) => {
var db = event.target.result;
var obs = db.transaction("superNodeMsg", "readwrite").objectStore("superNodeMsg");
var parsedData = JSON.parse(data);
var id = '' + parsedData.from + '_' + parsedData.to + '_' + parsedData.time;
obs.add({
id: id,
from: parsedData.from,
to: parsedData.to,
data: data
});
db.close();
};
}
function displayContacts() {
console.log('displayContacts');
var listElement = document.getElementById('contact-display');
for (floID in contacts) {
var createLi = document.createElement('div');
createLi.setAttribute("name", floID);
createLi.setAttribute("onClick", 'changeReceiver(this)');
createLi.setAttribute("class", "row sideBar-body");
createLi.innerHTML = `<div class="col-sm-11 col-xs-11 sideBar-main">
<div class="row">
<div class="col-sm-12 col-xs-12 sideBar-name">
<span class="name-meta"></span><br/>
<span class="time-meta">@${floID}</span>
</div>
</div>
</div>`
createLi.querySelector("span.name-meta").textContent = contacts[floID].name;
listElement.appendChild(createLi);
}
for (floID in groups) {
var createLi = document.createElement('div');
createLi.setAttribute("name", floID);
createLi.setAttribute("onClick", 'changeReceiver(this)');
createLi.setAttribute("class", "row sideBar-body");
createLi.innerHTML = `<div class="col-sm-11 col-xs-11 sideBar-main">
<div class="row">
<div class="col-sm-12 col-xs-12 sideBar-name">
<span class="name-meta"></span><br/>
<span class="time-meta">#${floID}</span>
</div>
</div>
</div>`
createLi.querySelector("span.name-meta").textContent = groups[floID].name;
listElement.appendChild(createLi);
}
}
function initselfWebSocket() {
var selfwebsocket = new WebSocket("ws://" + location.host + "/ws");
selfwebsocket.onopen = (evt) => {
console.log("CONNECTED");
var pass = prompt("Enter server password :")
selfwebsocket.send("$" + pass);
};
selfwebsocket.onclose = (evt) => {
console.log("DISCONNECTED");
};
selfwebsocket.onmessage = (evt) => {
console.log(evt.data);
try {
var data = JSON.parse(evt.data);
if (data.to == selfID) {
console.log('Incoming data')
processIncomingData(data);
} else if (modSuperNode) {
if (data.pingAway !== undefined)
sendStoredSuperNodeMsgs(data.pingAway)
else {
kBucketObj.determineClosestSupernode(data.to).then(result => {
console.log(result)
if (result[0].floID == selfID)
storeSuperNodeMsg(evt.data);
}).catch(e => {
console.log(e.message);
});
}
}
} catch (err) {
if (evt.data[0] == '$')
alert(evt.data);
else
console.log(err);
}
};
selfwebsocket.onerror = (evt) => {
console.log(evt);
};
}
function processIncomingData(data) {
if (data.directMsg !== undefined) {
var msg = floOpt.decryptData(data.directMsg.msgCipher.secret, data.directMsg.msgCipher.pubVal, privKey)
if (!floOpt.verifyData(msg, data.directMsg.sign, contacts[data.from].pubKey))
return
var msgInfo = {
time: Date.now(),
floID: data.from,
text: msg,
type: "R"
}
createMsgElement(msgInfo);
storeMsg(msgInfo);
} else if (data.groupMsg !== undefined && data.groupMsg.group in groups) {
if (!(groups[data.groupMsg.group].members.includes(data.from)))
return
var msg = floOpt.decryptData(data.groupMsg.msgCipher.secret, data.groupMsg.msgCipher.pubVal, groups[data.groupMsg.group].privKey);
if (!floOpt.verifyData(msg, data.groupMsg.sign, contacts[data.from].pubKey))
return
var msgInfo = {
time: Date.now(),
groupID: data.groupMsg.group,
sender: data.from,
text: msg,
type: "R"
}
createMsgElement(msgInfo);
storeMsg(msgInfo)
} else if (data.newGroup !== undefined) {
var groupInfoStr = floOpt.decryptData(data.newGroup.groupInfo.secret, data.newGroup.groupInfo.pubVal, privKey)
var groupInfo = JSON.parse(groupInfoStr);
if (floOpt.verifyData(groupInfoStr, data.newGroup.sign, contacts[groupInfo.creator].pubKey) && floOpt.verifyPrivKey(groupInfo.privKey, groupInfo.floID)) {
groups[groupInfo.floID] = groupInfo;
searchIndex.add(groupInfo.floID, groupInfo.name + ' #' + groupInfo.floID);
storeGroup(groupInfoStr, groupInfo.floID);
createGroupDisplay(groupInfo);
}
} else if (data.deleteGroup !== undefined && data.deleteGroup.group in groups) {
if (data.from != groups[data.deleteGroup.group].creator && !groups[data.deleteGroup.group].admins.includes(data.from))
return
if (floOpt.verifyData('deleteGroup:' + data.deleteGroup.group, data.deleteGroup.sign, contacts[data.from].pubKey)) {
delete groups[data.deleteGroup.group];
searchIndex.remove(data.deleteGroup.group);
deleteGroupFromIDB(data.deleteGroup.group);
}
} else if (data.addGroupMembers !== undefined && data.addGroupMembers.group in groups) {
if (data.from != groups[data.addGroupMembers.group].creator && !groups[data.addGroupMembers.group].admins.includes(data.from))
return
if (floOpt.verifyData('addGroupMembers:' + data.addGroupMembers.group + data.addGroupMembers.members.join('|'), data.addGroupMembers.sign, contacts[data.from].pubKey)) {
groups[data.addGroupMembers.group].members = groups[data.addGroupMembers.group].members.concat(data.addGroupMembers.members);
var groupInfoStr = JSON.stringify(groups[data.addGroupMembers.group]);
storeGroup(groupInfoStr, data.addGroupMembers.group);
}
} else if (data.rmGroupMembers !== undefined && data.rmGroupMembers.group in groups) {
if (data.from != groups[data.rmGroupMembers.group].creator && !groups[data.rmGroupMembers.group].admins.includes(data.from))
return
if (floOpt.verifyData('rmGroupMembers:' + data.rmGroupMembers.group + data.rmGroupMembers.members.join('|'), data.rmGroupMembers.sign, contacts[data.from].pubKey)) {
groups[data.rmGroupMembers.group].members = groups[data.rmGroupMembers.group].members.filter(x => !data.rmGroupMembers.members.includes(x)); //remove member from group
var groupInfoStr = JSON.stringify(groups[data.rmGroupMembers.group]);
storeGroup(groupInfoStr, data.rmGroupMembers.group);
}
} else if (data.addGroupAdmins !== undefined && data.addGroupAdmins.group in groups) {
if (data.from != groups[data.addGroupAdmins.group].creator)
return
if (floOpt.verifyData('addGroupAdmins:' + data.addGroupAdmins.group + data.addGroupAdmins.admins.join('|'), data.addGroupAdmins.sign, contacts[data.from].pubKey)) {
groups[data.addGroupAdmins.group].admins = groups[data.addGroupAdmins.group].admins.concat(data.addGroupAdmins.admins);
var groupInfoStr = JSON.stringify(groups[data.addGroupAdmins.group]);
storeGroup(groupInfoStr, data.addGroupAdmins.group);
}
} else if (data.rmGroupAdmins !== undefined && data.rmGroupAdmins.group in groups) {
if (data.from != groups[data.rmGroupAdmins.group].creator)
return
if (floOpt.verifyData('rmGroupAdmins:' + data.rmGroupAdmins.group + data.rmGroupAdmins.admins.join('|'), data.rmGroupAdmins.sign, contacts[data.from].pubKey)) {
groups[data.rmGroupAdmins.group].admins = groups[data.rmGroupAdmins.group].admins.filter(x => !data.rmGroupAdmins.admins.includes(x)); //remove member from group
var groupInfoStr = JSON.stringify(groups[data.rmGroupAdmins.group]);
storeGroup(groupInfoStr, data.rmGroupAdmins.group);
}
}
}
function createMsgElement(msgInfo) {
try {
const type = {
S: 'sender',
R: 'receiver'
};
if (!msgInfo.groupID) {
var msgEl = document.getElementById(msgInfo.floID);
var msghd = '';
} else {
var msgEl = document.getElementById(msgInfo.groupID);
var msghd = `<b>${msgInfo.sender}</b><br/>`;
}
if (!msgEl)
return;
var msgdiv = document.createElement('div');
msgdiv.setAttribute("class", "row message-body");
msgdiv.innerHTML = `<div class="col-sm-12 message-main-${type[msgInfo.type]}">
<div class="${type[msgInfo.type]}">
<span class="message-text">
${msghd}<pre></pre>
</span>
<span class="message-time pull-right">
${getTime(msgInfo.time)}
</span>
</div>
</div>`;
msgdiv.querySelector("pre").textContent = msgInfo.text;
msgEl.appendChild(msgdiv);
} catch (e) {
console.log(e);
}
}
function pingSuperNodeForAwayMessages() {
kBucketObj.determineClosestSupernode(selfID).then(result => {
var selfSuperNodeWS = new WebSocket("ws://" + contacts[result[0].floID].onionAddr + "/ws");
selfSuperNodeWS.onopen = (evt) => {
var data = JSON.stringify({
pingAway: selfID
});
selfSuperNodeWS.send(data)
console.log('Pinged selfSupernode for new messages')
};
selfSuperNodeWS.onerror = (ev) => {
console.log('Unable to ping superNode for new messages');
};
selfSuperNodeWS.onclose = (ev) => {
console.log('Connection with selfSupernode is closed')
};
}).catch(err => {
console.log(err.message);
});
}
function checkStatusInterval() {
try {
if (receiverWebSocket !== undefined && receiverWebSocket.readyState !== WebSocket.OPEN) {
receiverWebSocket.close()
receiverWebSocket = new WebSocket("ws://" + contacts[receiverID].onionAddr + "/ws");
receiverWebSocket.onopen = (evt) => {
receiverWebSocket.send('#')
};
receiverWebSocket.onerror = (ev) => {
receiverStatus(false);
};
receiverWebSocket.onclose = (ev) => {
receiverStatus(false);
};
receiverWebSocket.onmessage = (evt) => {
console.log(evt.data);
if (evt.data[0] == '#') {
if (evt.data[1] == '+')
receiverStatus(true);
else if (evt.data[1] == '-')
receiverStatus(false);
}
}
}
} catch (e) {
console.log(e);
}
}
function changeReceiver(param) {
if (receiverID !== undefined)
document.getElementById(receiverID).style.display = 'none';
//console.log(param.getAttribute("name"));
receiverID = param.getAttribute("name");
document.getElementById('recipient-floID').textContent = receiverID;
receiverStatus(false)
document.getElementById(receiverID).style.display = 'block';
document.getElementById("groupOptions").style.display = 'none';
if (receiverID in contacts) {
msgType = 'direct';
try {
if (receiverWebSocket !== undefined && receiverWebSocket.readyState === WebSocket.OPEN)
receiverWebSocket.close()
receiverWebSocket = new WebSocket("ws://" + contacts[receiverID].onionAddr + "/ws");
receiverWebSocket.onopen = (ev) => {
receiverWebSocket.send('#');
};
receiverWebSocket.onerror = (ev) => {
receiverStatus(false);
};
receiverWebSocket.onclose = (ev) => {
receiverStatus(false);
};
receiverWebSocket.onmessage = (evt) => {
console.log(evt.data);
if (evt.data[0] == '#') {
if (evt.data[1] == '+')
receiverStatus(true);
else if (evt.data[1] == '-')
receiverStatus(false);
}
}
} catch (e) {
console.log(e);
}
} else if (receiverID in groups) {
msgType = 'group';
if (receiverWebSocket !== undefined && receiverWebSocket.readyState === WebSocket.OPEN)
receiverWebSocket.close()
receiverWebSocket = undefined;
if (selfID == groups[receiverID].creator) {
var grpOpt = document.getElementById("groupOptions");
grpOpt.style.display = 'block';
var optList = grpOpt.querySelectorAll('li');
for (var i = 0; i < optList.length; i++)
optList[i].style.display = 'block';
} else if (groups[receiverID].admins.includes(selfID)) {
var grpOpt = document.getElementById("groupOptions");
grpOpt.style.display = 'block';
var optList = grpOpt.querySelectorAll('li');
for (var i = 0; i < 2; i++)
optList[i].style.display = 'block';
for (var i = 2; i < optList.length; i++)
optList[i].style.display = 'none';
}
}
}
function receiverStatus(status) {
if (status)
document.getElementById('recipient-status').style.color = "#4CC94C";
else
document.getElementById('recipient-status').style.color = "#CD5C5C";
recStat = status;
}
function getTime(time) {
var t = new Date(time);
var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
var fn = (n) => {
if (n < 10)
return '0' + n;
else
return n;
};
var tmp = `${months[t.getMonth()]} ${fn(t.getDate())} ${t.getFullYear()} ${fn(t.getHours())}:${fn(t.getMinutes())}`;
return tmp;
}
function sendMsg() {
if (receiverID === undefined) {
alert("Select a contact and send message");
return;
}
var inp = document.getElementById('sendMsgInput')
var msg = inp.value;
inp.value = "";
console.log(msg);
var time = Date.now();
var sign = floOpt.signData(msg, privKey);
if (msgType === 'direct')
sendDirectMsg(msg, time, sign);
else if (msgType === 'group')
sendGroupMsg(msg, time, sign);
}
function sendDirectMsg(msg, time, sign) {
var data = JSON.stringify({
from: selfID,
to: receiverID,
directMsg: {
time: time,
msgCipher: floOpt.encryptData(msg, contacts[receiverID].pubKey),
sign: sign
}
});
if (recStat)
receiverWebSocket.send(data);
else
sendDataToSuperNode(receiverID, data);
var msgInfo = {
time: time,
floID: receiverID,
text: msg,
type: "S"
}
createMsgElement(msgInfo);
storeMsg(msgInfo);
}
function sendGroupMsg(msg, time, sign) {
var data = {
from: selfID,
groupMsg: {
group: receiverID,
time: time,
msgCipher: floOpt.encryptData(msg, groups[receiverID].pubKey),
sign: sign
}
};
console.log(data);
groups[receiverID].members.forEach(floID => {
if (floID == selfID) //dont send to self
return;
data.to = floID;
sendData(floID, JSON.stringify(data));
});
var msgInfo = {
time: time,
sender: selfID,
groupID: receiverID,
text: msg,
type: "S"
}
createMsgElement(msgInfo);
storeMsg(msgInfo);
}
function sendStoredSuperNodeMsgs(floID) {
var receiverWS = new WebSocket("ws://" + contacts[floID].onionAddr + "/ws");
receiverWS.onopen = (ev) => {
var idb = indexedDB.open("FLO_Chat", 2);
idb.onerror = (event) => {
console.log("Error in opening IndexedDB!");
};
idb.onupgradeneeded = (event) => {
var objectStore = event.target.result.createObjectStore("superNodeMsg", {
keyPath: 'id'
});
objectStore.createIndex('from', 'from', {
unique: false
});
objectStore.createIndex('to', 'to', {
unique: false
});
objectStore.createIndex('data', 'data', {
unique: false
});
};
idb.onsuccess = (event) => {
var db = event.target.result;
var obs = db.transaction("superNodeMsg", "readwrite").objectStore("superNodeMsg");
obs.openCursor().onsuccess = (event) => {
var cursor = event.target.result;
if (cursor) {
if (cursor.value.to == floID) {
receiverWS.send(cursor.value.data);
cursor.delete();
}
cursor.continue();
} else {
console.log('Sent All messages to ' + floID)
}
}
db.close();
};
};
receiverWS.onerror = (ev) => {
console.log('Connection Error to ' + floID)
};
receiverWS.onclose = (ev) => {
console.log('Disconnected from ' + floID)
};
}
async function sendData(floID, data) {
try {
var recipientWS = new WebSocket("ws://" + contacts[floID].onionAddr + "/ws");
recipientWS.onopen = (ev) => {
recipientWS.send('#');
};
recipientWS.onerror = (ev) => {
sendDataToSuperNode(floID, data);
};
recipientWS.onclose = (ev) => {
console.log("Closed")
};
recipientWS.onmessage = (evt) => {
console.log(evt.data);
if (evt.data[0] == '#') {
if (evt.data[1] == '+')
recipientWS.send(data);
else if (evt.data[1] == '-')
sendDataToSuperNode(floID, data);
}
}
} catch (e) {
console.log(e);
}
}
function sendDataToSuperNode(floID, data) {
kBucketObj.determineClosestSupernode(floID).then(result => {
var superNodeWS = new WebSocket("ws://" + contacts[result[0].floID].onionAddr + "/ws");
superNodeWS.onopen = (ev) => {
console.log(`Connected to ${floID}'s SuperNode!`);
superNodeWS.send(data);
};
superNodeWS.onerror = (ev) => {
console.log(`${floID}'s SuperNode is offline!`);
};
superNodeWS.onclose = (ev) => {
console.log(`Disconnected from ${floID}'s SuperNode!`);
};
}).catch(e => {
console.log(e.message);
});
}
function createGroupDisplay(groupInfo) {
var createLi = document.createElement('div');
createLi.setAttribute("name", groupInfo.floID);
createLi.setAttribute("onClick", 'changeReceiver(this)');
createLi.setAttribute("class", "row sideBar-body");
createLi.innerHTML = `<div class="col-sm-11 col-xs-11 sideBar-main">
<div class="row">
<div class="col-sm-12 col-xs-12 sideBar-name">
<span class="name-meta"></span><br/>
<span class="time-meta">#${groupInfo.floID}</span>
</div>
</div>
</div>`;
createLi.querySelector("span.name-meta").textContent = groupInfo.name;
document.getElementById('contact-display').appendChild(createLi);
var createEl = document.createElement('div');
createEl.setAttribute("id", groupInfo.floID);
createEl.setAttribute("class", "message-inner");
createEl.style.display = 'none';
document.getElementById("conversation").appendChild(createEl);
}
function storeGroup(groupInfoStr, groupID) {
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
console.log("Error in opening IndexedDB!");
};
idb.onsuccess = (event) => {
var db = event.target.result;
console.log(groupID, groupInfoStr);
var obs = db.transaction('groups', "readwrite").objectStore('groups');
obs.put({
groupID: groupID,
groupInfo: groupInfoStr
});
db.close();
};
}
function deleteGroupFromIDB(groupID) {
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
console.log("Error in opening IndexedDB!");
};
idb.onsuccess = (event) => {
var db = event.target.result;
console.log('Delete Group:', groupID);
var obs = db.transaction('groups', "readwrite").objectStore('groups');
obs.delete(groupID);
db.close();
};
}
function createGroup() {
customCheckList(Object.keys(contacts), [selfID], 'Create Group', 'success').then(result => {
var grpInfo = floOpt.genNewIDpair();
grpInfo.name = result.grpName;
grpInfo.members = result.members;
grpInfo.members.push(selfID)
grpInfo.creator = selfID;
grpInfo.admins = [];
var grpInfoStr = JSON.stringify(grpInfo);
console.log(grpInfoStr);
var data = {
from: selfID,
newGroup: {
sign: floOpt.signData(grpInfoStr, privKey)
}
}
grpInfo.members.forEach(floID => {
data.to = floID;
data.newGroup.groupInfo = floOpt.encryptData(grpInfoStr, contacts[floID].pubKey),
sendData(floID, JSON.stringify(data));
});
}).catch(error => {
console.log(error);
})
}
function deleteGroup() {
var flag = confirm("Are you sure you want to delete this group?");
if (flag) {
var data = {
from: selfID,
deleteGroup: {
group: receiverID,
sign: floOpt.signData('deleteGroup:' + receiverID, privKey)
}
};
groups[receiverID].members.forEach(floID => {
data.to = floID;
sendData(floID, JSON.stringify(data));
});
}
}
function addGroupMembers() {
customCheckList(Object.keys(contacts), groups[receiverID].members, 'Add Members', 'success').then(result => {
var newMembers = result.members;
var data1 = {
from: selfID,
addGroupMembers: {
group: receiverID,
members: newMembers,
sign: floOpt.signData('addGroupMembers:' + receiverID + newMembers.join('|'), privKey)
}
}
groups[receiverID].members.forEach(floID => {
if (floID == selfID) //dont send to self
return;
data1.to = floID;
sendData(floID, JSON.stringify(data1));
});
groups[receiverID].members = groups[receiverID].members.concat(newMembers);
var grpInfoStr = JSON.stringify(groups[receiverID]);
console.log(grpInfoStr);
var data2 = {
from: selfID,
newGroup: {
sign: floOpt.signData(grpInfoStr, privKey)
}
}
newMembers.forEach(floID => {
data2.to = floID;
data2.newGroup.groupInfo = floOpt.encryptData(grpInfoStr, contacts[floID].pubKey),
sendData(floID, JSON.stringify(data2));
});
storeGroup(grpInfoStr, receiverID);
}).catch(error => {
console.log(error);
})
}
function rmGroupMembers() {
customCheckList(groups[receiverID].members, [], 'Remove Members', 'danger').then(result => {
var rmMembers = result.members;
var data1 = {
from: selfID,
rmGroupMembers: {
group: receiverID,
members: rmMembers,
sign: floOpt.signData('rmGroupMembers:' + receiverID + rmMembers.join('|'), privKey)
}
}
groups[receiverID].members = groups[receiverID].members.filter(x => !rmMembers.includes(x)); //remove member from group
storeGroup(JSON.stringify(groups[receiverID]), receiverID);
groups[receiverID].members.forEach(floID => {
if (floID == selfID)
return;
data1.to = floID;
sendData(floID, JSON.stringify(data1));
});
var data2 = {
from: selfID,
deleteGroup: {
group: receiverID,
sign: floOpt.signData('deleteGroup:' + receiverID, privKey)
}
};
rmMembers.forEach(floID => {
data2.to = floID;
sendData(floID, JSON.stringify(data2));
});
}).catch(error => {
console.log(error);
})
}
function addGroupAdmins() {
customCheckList(groups[receiverID].members, groups[receiverID].admins, 'Add Admins', 'success').then(result => {
var newAdmins = result.members;
var data = {
from: selfID,
addGroupAdmins: {
group: receiverID,
admins: newAdmins,
sign: floOpt.signData('addGroupAdmins:' + receiverID + newAdmins.join('|'), privKey)
}
}
groups[receiverID].members.forEach(floID => {
if (floID == selfID) //dont send to self
return;
data.to = floID;
sendData(floID, JSON.stringify(data));
});
groups[receiverID].admins = groups[receiverID].admins.concat(newAdmins);
var grpInfoStr = JSON.stringify(groups[receiverID]);
storeGroup(grpInfoStr, receiverID);
}).catch(error => {
console.log(error);
})
}
function rmGroupAdmins() {
customCheckList(groups[receiverID].admins, [], 'Remove Admins', 'danger').then(result => {
var rmAdmins = result.members;
var data = {
from: selfID,
rmGroupAdmins: {
group: receiverID,
admins: rmAdmins,
sign: floOpt.signData('rmGroupAdmins:' + receiverID + rmAdmins.join('|'), privKey)
}
}
groups[receiverID].members.forEach(floID => {
if (floID == selfID) //dont send to self
return;
data.to = floID;
sendData(floID, JSON.stringify(data));
});
groups[receiverID].admins = groups[receiverID].admins.filter(x => !rmAdmins.includes(x)); //remove admins
var grpInfoStr = JSON.stringify(groups[receiverID]);
storeGroup(grpInfoStr, receiverID);
}).catch(error => {
console.log(error);
})
}
function searchContact() {
try {
var searchKey = this.value;
if (!searchKey)
var searchResults = Object.keys(contacts).concat(Object.keys(groups));
else
var searchResults = searchIndex.search(searchKey);
var contactList = document.getElementById('contact-display').children;
for (var i = 0; i < contactList.length; i++) {
if (searchResults.includes(contactList[i].getAttribute("name")))
contactList[i].style.display = 'block';
else
contactList[i].style.display = 'none';
};
} catch (e) {
console.log(e);
}
}
function customCheckList(userList, ignoreList, okBtnVal = "Ok", okBtnType = "success") {
var dialog = document.getElementById('overlay');
dialog.style.display = "block";
var okButton = dialog.querySelector('button.ok');
var cancelButton = dialog.querySelector('button.cancel');
okButton.setAttribute("class", `ok btn btn-${okBtnType}`);
okButton.textContent = okBtnVal;
var grpNameInput = dialog.querySelector('input.grpName')
grpNameInput.style.display = (okBtnVal === "Create Group" ? "block" : "none");
grpNameInput.value = '';
var userChecklist = document.getElementById('userChecklist');
for (var i = 0; i < userList.length; i++) {
if (ignoreList.includes(userList[i]))
continue;
var listEl = document.createElement('label');
listEl.setAttribute('class', "btn btn-default listLabel");
listEl.setAttribute('name', userList[i]);
listEl.innerHTML = `
<span></span><br/>
<sub>@${userList[i]}</sub>
<input type="checkbox" class="badgebox" value="${userList[i]}">
<span class="badge">&check;</span>`;
listEl.querySelector("span").textContent = contacts[userList[i]].name;
userChecklist.appendChild(listEl);
}
return new Promise((resolve, reject) => {
dialog.addEventListener('click', function handleButtonClicks(e) {
if (e.target.tagName !== 'BUTTON') {
return;
}
dialog.removeEventListener('click', handleButtonClicks);
dialog.style.display = 'none';
if (e.target === okButton) {
var selectedList = [];
var checklist = dialog.querySelectorAll('input.badgebox');
for (var i = 0; i < checklist.length; i++)
if (checklist[i].checked)
selectedList.push(checklist[i].value);
if (selectedList.length == 0)
reject('User Didnt select Any Users!');
else
resolve({
grpName: grpNameInput.value,
members: selectedList
});
} else if (e.target === cancelButton) {
reject('User cancelled!');
} else {
reject('Some other button was clicked!');
}
});
});
}
function searchChecklist() {
try {
var searchKey = this.value;
if (!searchKey)
var searchResults = Object.keys(contacts);
else
var searchResults = searchIndex.search(searchKey);
var checklist = document.getElementById('userChecklist').children;
for (var i = 0; i < checklist.length; i++) {
if (searchResults.includes(checklist[i].getAttribute("name")))
checklist[i].style.display = 'block';
else
checklist[i].style.display = 'none';
};
} catch (e) {
console.log(e);
}
}