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); } } } //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("keyup", (event) => { if (event.keyCode === 13) { 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); readGroupMsgfromIDB().then(result => { console.log(result); initselfWebSocket(); pingSuperNodeForAwayMessages(); displayContacts(); const createClock = setInterval(checkStatusInterval, 30000); }).catch(error => { console.log(error); }); }).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('type', 'type', { unique: false }); var objectStore4 = db.createObjectStore("groups", { keyPath: 'groupID' }); objectStore4.createIndex('groupInfo', 'groupInfo', { unique: false }); var objectStore5 = db.createObjectStore("groupMsg", { keyPath: 'time' }); objectStore5.createIndex('text', 'text', { unique: false }); objectStore5.createIndex('groupID', 'groupID', { unique: false }); objectStore5.createIndex('type', 'type', { unique: false }); objectStore5.createIndex('sender', 'sender', { 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); } 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 readGroupMsgfromIDB() { return new Promise((resolve, reject) => { var disp = document.getElementById("conversation"); 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("groupMsg", "readwrite").objectStore("groupMsg"); obs.openCursor().onsuccess = (event) => { var cursor = event.target.result; if (cursor) { createMsgElement(cursor.value); cursor.continue(); } else { resolve("Read Group Msgs 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 storeGroupMsg(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("groupMsg", "readwrite").objectStore("groupMsg"); 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 = `` 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 = `` 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); storeGroupMsg(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); storeGroupMsg(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)) { 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 = `${msgInfo.sender}`; } if (!msgEl) return; var msgdiv = document.createElement('div'); msgdiv.setAttribute("class", "row message-body"); msgdiv.innerHTML = `
${msghd}
${msgInfo.text}
${getTime(msgInfo.time)}
`; 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').innerHTML = 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); storeGroupMsg(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 = ``; 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.innerHTML = 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 = ` ${contacts[userList[i]].name}
@${userList[i]}
`; 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); } }