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); 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 = `