UNPKG

immuto-backend

Version:

Immuto's JavaScript library for secure data management

1,252 lines (1,077 loc) 67.4 kB
/* COPYRIGHT 2020 under the MIT License * Immuto, Inc */ let IN_BROWSER = ("undefined" !== typeof window) var Web3 = require('web3') var sigUtil = require('eth-sig-util') var NodeHTTP = require('xmlhttprequest').XMLHttpRequest // for backend compatability var NodeForm = require('form-data') // for backend compatability var crypto = require('crypto') const NodeRSA = require('node-rsa'); const SYMMETRIC_SCHEME="aes-256-ctr" function new_HTTP() { if (IN_BROWSER) return new XMLHttpRequest() else return new NodeHTTP() } function new_Form() { // for sending files with XMLHttpRequest if (IN_BROWSER) return new FormData() else return new NodeForm() } exports.init = function(debug, debugHost) { this.host = "https://www.immuto.io" if (debug === true) { if (debugHost) this.host = debugHost else this.host = "https://dev.immuto.io" // reasonable default } try { this.web3 = new Web3("https://www.immuto.io") // Dummy provider because required on init now } catch(err) { console.error(err) throw new Error("Web3js is a required dependency of the Immuto API. Make sure it is included wherever immuto.js is present.") } // for web3 account management this.salt = "" this.encryptedKey = "" // for API acess this.authToken = "" this.email = "" if (IN_BROWSER) { let ls = window.localStorage // preserve session across pages if (ls.authToken && ls.email && ls.salt && ls.encryptedKey) { this.salt = ls.salt this.encryptedKey = ls.encryptedKey this.authToken = ls.authToken this.email = ls.email } } this.utils = { convert_unix_time: function(time_ms) { let time_seconds = time_ms * 1000 var a = new Date(time_seconds); var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; var year = a.getFullYear(); var month = months[a.getMonth()]; var date = a.getDate(); var hour = a.getHours(); var min = a.getMinutes() < 10 ? '0' + a.getMinutes() : a.getMinutes(); var sec = a.getSeconds() < 10 ? '0' + a.getSeconds() : a.getSeconds(); var time = date + ' ' + month + ' ' + year + ' ' + hour + ':' + min + ':' + sec ; return time; }, convert_js_time: function(time_js) { return this.convert_unix_time(time_js / 1000) }, emails_to_addresses: (emails) => { return new Promise((resolve, reject) => { var http = new_HTTP() let sendstring = "emails=" + emails + "&authToken=" + this.authToken http.open("POST", this.host + "/emails-to-addresses", true) http.setRequestHeader("Content-Type", "application/x-www-form-urlencoded") http.onreadystatechange = function() { if (http.readyState === 4 && http.status === 200) { try { let addresses = JSON.parse(http.responseText) resolve(addresses) } catch (err) { console.error(http.responseText) reject(err) } } else if (http.readyState === 4) { reject(http.responseText) } } http.send(sendstring) }) }, addresses_to_emails: (history) => { return new Promise((resolve, reject) => { let addresses = [] for (let event of history) { addresses.push(event.signer) } var http = new_HTTP() let sendstring = "addresses=" + JSON.stringify(addresses) + "&authToken=" + this.authToken http.open("POST", this.host + "/addresses-to-emails", true) http.setRequestHeader("Content-Type", "application/x-www-form-urlencoded") http.onreadystatechange = function() { if (http.readyState === 4 && http.status === 200) { try { let addrToEmail = JSON.parse(http.responseText) for (let i = 0; i < history.length; i++) { history[i].email = addrToEmail[history[i].signer] } resolve(history) } catch (err) { reject(err) } } else if (http.readyState === 4) { reject(http.responseText) } } http.send(sendstring) }) }, ecRecover: (message, v, r, s) => { if (v === '28') { v = Buffer.from('1c', 'hex') } else { v = Buffer.from('1b', 'hex') } r = Buffer.from(r.split('x')[1], 'hex') s = Buffer.from(s.split('x')[1], 'hex') let signature = sigUtil.concatSig(v, r, s) let address = sigUtil.recoverPersonalSignature({data: message, sig: signature}) return this.web3.utils.toChecksumAddress(address) }, parse_record_ID: (recordID) => { if (!recordID) return false; if ("string" !== typeof recordID) return false; let idLength = recordID.length if (40 === idLength || 42 === idLength) { // isAddress returns true with or without leading 0x // isAddress returns true if ID is a string and false if it's an integer if (this.web3.utils.isAddress(recordID)) { return recordID } } if (recordID.length !== 48) { throw new Error(`Invalid recordID: ${recordID}, reason: length not 50`) } return { address: recordID.substring(0, 42), shardHex: recordID.substring(42) } }, shard_to_URL: (shardIndex, forVerification) => { if (!shardIndex && shardIndex !== 0) { throw new Error("No prefix given") } if (shardIndex === 0) { if (forVerification) { return "http://localhost:8443" } return "http://localhost:8545" } if (this.host === "https://www.immuto.io") { if (forVerification) { return `https://prod${shardIndex}.immuto.io:8443` } return `https://prod${shardIndex}.immuto.io:443` } if (forVerification) { return `https://shard${shardIndex}.immuto.io:8443` } return `https://shard${shardIndex}.immuto.io:443` }, shardIndex_to_hex: (shardIndex) => { const SHARD_LENGTH = 6 let hexString = (shardIndex).toString(16) if (hexString > 16777215) { // ffffff throw new Error(`hexString: ${hexString} exceeds width of ${SHARD_LENGTH}`) } while (hexString.length < SHARD_LENGTH) { hexString = "0" + hexString } return hexString }, hex_to_shardIndex: (hexString) => { return parseInt(hexString, 16) } } this.get_registration_token = function(address) { return new Promise((resolve, reject) => { var http = new_HTTP() let sendstring = "address=" + address http.open("POST", this.host + "/get-signature-data", true) http.setRequestHeader("Content-Type", "application/x-www-form-urlencoded") http.onreadystatechange = () => { if (http.readyState === 4 && http.status === 200) { resolve(http.responseText) } else if (http.readyState === 4) { reject(http.responseText) } } http.send(sendstring) }) } this.register_user = function(email, password, orgToken) { return new Promise((resolve, reject) => { let salt = this.web3.utils.randomHex(32) let account = this.web3.eth.accounts.create() let address = account.address let encryptedKey = this.web3.eth.accounts.encrypt(account.privateKey, password + salt); this.get_registration_token(address) .then((token) => { let signature = account.sign(token) var http = new_HTTP() let sendstring = "email=" + email sendstring += "&password=" + this.web3.utils.sha3(password) sendstring += "&salt=" + salt sendstring += "&address=" + address sendstring += "&keyInfo=" + JSON.stringify(encryptedKey) sendstring += "&signature=" + JSON.stringify(signature) if (orgToken) sendstring += "&registrationCode=" + orgToken http.open("POST", this.host + "/submit-registration", true) http.setRequestHeader("Content-Type", "application/x-www-form-urlencoded") http.onreadystatechange = () => { if (http.readyState === 4 && http.status === 204) { resolve() } else if (http.readyState === 4) { reject(http.responseText) } } http.send(sendstring) }) .catch(err => reject(err)) }) } // Best for backend use only, while authenticated as organization admin this.permission_new_user = function(newUserEmail) { return new Promise((resolve, reject) => { let sendstring = "email=" + newUserEmail.toLowerCase() sendstring += "&noEmail=true" // Causes API to respond with authToken rather than emailing user sendstring += "&authToken=" + this.authToken // org admin authToken for permissioning new user registration const http = new_HTTP() http.open("POST", this.host + "/submit-org-member", true) http.setRequestHeader("Content-Type", "application/x-www-form-urlencoded") http.onreadystatechange = () => { if (http.readyState === 4 && http.status === 200) { resolve(http.responseText) } else if (http.readyState === 4) { reject(http.responseText) } } http.send(sendstring) }) } this.authenticate = function(email, password) { return new Promise((resolve, reject) => { if (this.salt && this.encryptedKey && this.authToken && this.email) { reject("User already authenticated. Call deauthenticate before authenticating a new user.") return } if (!email) { reject("Email required for authentication") return } if (!password) { reject("Password required for authentication") return } let http = new_HTTP() let sendstring = "email=" + email sendstring += "&password=" + this.web3.utils.sha3(password) // server does not see password http.open("POST", this.host + "/submit-login", true) http.setRequestHeader("Content-Type", "application/x-www-form-urlencoded") http.onreadystatechange = () => { if (http.readyState === 4 && http.status === 200) { let response = JSON.parse(http.responseText) this.salt = response.salt this.encryptedKey = response.encryptedKey this.authToken = response.authToken let account = undefined try { account = this.web3.eth.accounts.decrypt( this.encryptedKey, password + this.salt ); } catch(err) { reject("Incorrect password") return } let signature = account.sign(this.authToken) let sendstring = "address=" + account.address sendstring += "&signature=" + JSON.stringify(signature) sendstring += "&authToken=" + this.authToken sendstring += "&returnUserInfo=" + true let http2 = new_HTTP() http2.open("POST", this.host + "/prove-address", true) http2.setRequestHeader("Content-Type", "application/x-www-form-urlencoded") http2.onreadystatechange = () => { if (http2.readyState === 4 && (http2.status === 204 || http2.status === 200)) { if (IN_BROWSER) { window.localStorage.authToken = this.authToken window.localStorage.email = email window.localStorage.salt = this.salt window.localStorage.encryptedKey = this.encryptedKey } this.email = email if (http2.status === 204) { resolve(this.authToken) return } let userInfo = JSON.parse(http2.responseText) if (userInfo.publicKey) { resolve(this.authToken) return } this.generate_RSA_keypair(password) .catch(err => console.error(err)) .finally(() => resolve(this.authToken)) } else if (http2.readyState === 4) { console.error("Error on login verification") reject(http2.responseText) } } http2.send(sendstring) } else if (http.readyState === 4){ console.error("Error on login") reject(http.status + ": " + http.responseText) } } http.send(sendstring) }) } this.deauthenticate = function() { return new Promise((resolve, reject) => { let sendstring = `authToken=${this.authToken}` if (IN_BROWSER) { if (!this.authToken) { sendstring = `authToken=${window.localStorage.authToken}` } window.localStorage.authToken = "" window.localStorage.email = "" window.localStorage.salt = "" window.localStorage.encryptedKey = "" window.localStorage.password = "" // in case this is set elsewhere } this.authToken = "" this.email = "" this.salt = "" this.encryptedKey = "" var http = new_HTTP(); http.open("POST", this.host + "/logout-API", true); http.setRequestHeader("Content-Type", "application/x-www-form-urlencoded") http.onreadystatechange = () => { if (http.readyState === 4 && http.status === 204) { resolve() } else if (http.readyState === 4) { reject(http.responseText) } }; http.send(sendstring); }) } // convenience method for signing an arbitrary string // user address can be recovered from signature by utils.ecRecover this.sign_string = function(string, password) { let account = undefined; try { account = this.web3.eth.accounts.decrypt( this.encryptedKey, password + this.salt ); return account.sign(string) } catch(err) { if (!this.email) { throw new Error("User not yet authenticated."); } else { throw(err) } } } this.get_public_key = function(email) { return new Promise((resolve, reject) => { let accessURL = this.host + "/public-key-for-user?email=" + email accessURL += "&authToken=" + this.authToken var http = new_HTTP(); http.open("GET", accessURL, true); http.setRequestHeader('Accept', 'application/json, text/javascript'); http.onreadystatechange = function() { if (http.readyState === 4 && http.status === 200) { resolve(http.responseText) } else if (http.readyState === 4) { reject(http.responseText) } }; http.send() }) } this.decrypt_RSA_private_key = function(encryptedKey, rsaIv, password) { if (!password) { throw new Error("Password is required to decrypt RSA key") } const decryptKey = this.generate_key_from_password(password) const iv = this.string_to_iv(rsaIv) return this.decrypt_string(encryptedKey, decryptKey, iv, 'base64') } this.generate_RSA_keypair = function(password) { return new Promise((resolve, reject) => { if (!password) { reject("Password is required to generate RSA keypair") return; } const rsaKey = NodeRSA({b: 2048}) const pubKey = rsaKey.exportKey('public') const privKey = rsaKey.exportKey('private') const encrypted = this.encrypt_string_with_password(privKey, password, 'base64') const iv = this.iv_to_string(encrypted.iv) const encryptedPrivateKey = encrypted.ciphertext var form = new_Form() form.append('publicKey', pubKey) form.append('privateKey', encryptedPrivateKey) form.append('rsaIv', iv) form.append('authToken', this.authToken) const url = this.host + "/set-rsa-keypair" if (IN_BROWSER) { var xhr = new_HTTP() xhr.open("POST", url, true); xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 204) { resolve({pubKey, privKey}) } else if (xhr.readyState === 4) { console.error("Error on upload") reject(xhr.responseText) } }; xhr.send(form) } else { form.submit(url, function(err, res) { if (err) { reject(err) return } resolve({pubKey, privKey}) }) } }) } this.generate_key_from_password = function(password) { const salt = this.salt if (!salt) { throw new Error("User must be authenticated to generate key") } // cannot be SHA256, or Immuto backend could decrypt user data on login with current auth scheme const hash = crypto.createHash("sha512") hash.update(password + salt); return hash.digest().slice(0, 32) // key for aes-256 must be 32 bytes } this.iv_to_string = function(iv) { let str = "" for (let i = 0; i < iv.length; i ++) { str += iv[i] + "," } return str } this.string_to_iv = function(str) { let ivStr = str.split(',') let iv = new Uint8Array(16) for (let i = 0; i < ivStr.length; i++) { iv[i] = ivStr[i] } return iv } this.encrypt_with_publicKey = function(plaintext, publicKey) { const key = new NodeRSA(publicKey) return key.encrypt(plaintext, 'base64') } this.decrypt_with_privateKey = function(ciphertext, privateKey) { const key = new NodeRSA(privateKey) return key.decrypt(ciphertext, 'utf8') } this.encrypt_string_with_key = function(plaintext, key, encoding) { encoding = encoding || 'binary' const iv = crypto.randomBytes(16) const cipher = crypto.createCipheriv(SYMMETRIC_SCHEME, key, iv) let ciphertext = cipher.update(plaintext, 'binary', encoding); ciphertext += cipher.final(encoding); return { ciphertext: ciphertext, key: key, iv: iv } } this.encrypt_string_with_password = function(plaintext, password, encoding) { if (!password) throw new Error("No password provided"); let key = this.generate_key_from_password(password) return this.encrypt_string_with_key(plaintext, key, encoding) } this.encrypt_string = function(plaintext, encoding) { const key = crypto.randomBytes(32) return this.encrypt_string_with_key(plaintext, key, encoding) } this.decrypt_string = function(ciphertext, key, iv, encoding) { encoding = encoding || 'binary' let decipher = crypto.createDecipheriv(SYMMETRIC_SCHEME, key, iv) let decrypted = decipher.update(ciphertext, encoding, 'binary'); return decrypted + decipher.final('binary'); } this.str2ab = function(str) { var buf = new ArrayBuffer(str.length); // 2 bytes for each char var bufView = new Uint8Array(buf); for (var i=0, strLen=str.length; i < strLen; i++) { bufView[i] = str.charCodeAt(i); } return bufView; } this.get_file_content = function(file) { return new Promise((resolve, reject) => { var reader = new FileReader(); reader.onload = function() { resolve(this.result) } reader.onerror = function(e) { reject(e) } reader.readAsBinaryString(file); }) } // iv for aes256, optional for RSA this.store_user_key_for_record = function(userEmail, recordID, encryptedKey, iv) { return new Promise((resolve, reject) => { const url = this.host + "/set-key-for-record" let form = new_Form() form.append('userForKey', userEmail) form.append('recordID', recordID) form.append('encryptedKey', encodeURI(encryptedKey)) if (iv) form.append('iv', this.iv_to_string(iv)) form.append('authToken', this.authToken) if (IN_BROWSER) { let xhr = new_HTTP() xhr.open("POST", url, true); xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 204) { resolve() } else if (xhr.readyState === 4) { reject(xhr.responseText) } }; xhr.send(form); } else { form.submit(url, function(err, res) { if (err) { reject(err) return } resolve() }) } }) } this.upload_file_for_record = function(file, fileContent, recordID, password, projectID, handleProgress) { return new Promise((resolve, reject) => { let cipherInfo = this.encrypt_string(fileContent) let encryptedKey = this.encrypt_string_with_password( JSON.stringify({ key: cipherInfo.key, iv: this.iv_to_string(cipherInfo.iv) }), password, 'base64') let encryptedFile = new Blob([this.str2ab(cipherInfo.ciphertext)], {type: file.type}); encryptedFile.lastModifiedDate = new Date(); encryptedFile.name = file.name; let sendstring = 'fileSize=' + encryptedFile.size sendstring += '&fileName=' + file.name sendstring += '&fileType=' + file.type sendstring += '&recordID=' + recordID if (projectID) sendstring += '&projectID=' + projectID sendstring += '&authToken=' + this.authToken var xhr = new_HTTP() let url = this.host + "/prepare-file-upload" xhr.open("POST", url, true); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded") xhr.onreadystatechange = () => { if (xhr.readyState === 4 && xhr.status === 200) { let presignedPostData = JSON.parse(xhr.responseText) const formData = new FormData(); Object.keys(presignedPostData.fields).forEach(key => { formData.append(key, presignedPostData.fields[key]); }); // Actual file has to be appended last formData.append("file", encryptedFile); const http = new_HTTP(); http.open("POST", presignedPostData.url, true); http.onreadystatechange = () => { if (http.readyState === 4 && http.status === 204) { this.store_user_key_for_record(this.email, recordID, encryptedKey.ciphertext, encryptedKey.iv) .then(() => resolve("done")) .catch(err => reject(err)) } else if (http.readyState === 4){ reject(http.responseText) } }; http.upload.onprogress = function(e) { let percentComplete = Math.ceil((e.loaded / e.total) * 100); if (handleProgress) { handleProgress(percentComplete) } }; http.send(formData); } else if (xhr.readyState === 4) { console.error("Error on upload") reject(xhr.responseText) } }; xhr.send(sendstring); }) } this.upload_file = function(file, password, projectID, handleProgress) { return new Promise((resolve, reject) => { this.get_file_content(file) .then((content) => { this.create_data_management(content, file.recordName || file.name, "editable", password, "") .then((recordID) => { this.upload_file_for_record(file, content, recordID, password, projectID, handleProgress) .then(done => resolve(recordID)) .catch(err => reject(err)) }) .catch(err => reject(err)) }) .catch(err => reject(err)) }) } this.upload_file_data = function(fileContent, fileName, password, projectID, handleProgress) { return new Promise((resolve, reject) => { let file = {name: fileName, type: "text/plain"} projectID = projectID || '' this.create_data_management(fileContent, fileName, "editable", password, "") .then((recordID) => { this.upload_file_for_record(file, fileContent, recordID, password, projectID, handleProgress) .then(done => resolve(recordID)) .catch(err => reject(err)) }) .catch(err => reject(err)) }) } this.download_file_data = function(recordID, password, version) { version = version || 0 return new Promise((resolve, reject) => { this.download_file_for_recordID(recordID, password, version, true) .then(file => resolve(file.data)) .catch(err => reject(err)) }) } this.search_records_by_hash = function(hash) { return new Promise((resolve, reject) => { let accessURL = this.host + "/records-for-hash?hash=" + hash accessURL += "&authToken=" + this.authToken var http = new_HTTP(); http.open("GET", accessURL, true); http.setRequestHeader('Accept', 'application/json, text/javascript'); http.onreadystatechange = function() { if (http.readyState === 4 && http.status === 200) { resolve({records: JSON.parse(http.responseText), hash}) } else if (http.readyState === 4) { reject(http.responseText) } }; http.send() }) } this.search_records_by_content = function(fileContent) { return this.search_records_by_hash(this.web3.eth.accounts.hashMessage(fileContent)) } this.search_records_by_query = function(query) { return new Promise((resolve, reject) => { let accessURL = this.host + '/search-user-data-contracts' accessURL += "?authToken=" + this.authToken var http = new_HTTP(); http.open("POST", accessURL, true); http.setRequestHeader("Content-Type", "application/x-www-form-urlencoded") http.onreadystatechange = function() { if (http.readyState === 4 && http.status === 200) { resolve(JSON.parse(http.responseText)) } else if (http.readyState === 4) { reject(http.responseText) } }; http.send(`searchQuery=${query}`) }) } this.get_info_for_recordID = function(recordID) { return new Promise((resolve, reject) => { let accessURL = this.host + "/record-for-user?recordID=" + recordID accessURL += "&authToken=" + this.authToken var http = new_HTTP(); http.open("GET", accessURL, true); http.setRequestHeader('Accept', 'application/json, text/javascript'); http.onreadystatechange = function() { if (http.readyState === 4 && http.status === 200) { resolve(JSON.parse(http.responseText)) } else if (http.readyState === 4) { reject(http.responseText) } }; http.send() }) } this.build_full_URL = function(remoteURL) { const S3_BUCKET = this.host === "https://www.immuto.io" ? "immuto-prod" : "immuto-sandbox" if (remoteURL.includes(S3_BUCKET)) return remoteURL return "https://" + S3_BUCKET + ".s3.amazonaws.com/readable/" + remoteURL } this.decrypt_fileKey_symmetric = function(encryptedKeyInfo, password) { let decryptKey = this.generate_key_from_password(password) let encryptedKey = encryptedKeyInfo.encryptedKey let iv = this.string_to_iv(encryptedKeyInfo.iv) let fileDecryptInfo = this.decrypt_string(encryptedKey, decryptKey, iv, 'base64') let parsed = JSON.parse(fileDecryptInfo) let fileDKey = parsed.key.data let fileDiv = this.string_to_iv(parsed.iv) return { key: fileDKey, iv: fileDiv } } this.decrypt_fileKey_asymmetric = function(encryptedKeyInfo, password) { return new Promise(async (resolve, reject) => { try { const encryptedKey = encryptedKeyInfo.encryptedKey const userInfo = await this.get_user_info() const privateKey = this.decrypt_RSA_private_key(userInfo.privateKey, userInfo.rsaIv, password) let fileDecryptInfo = JSON.parse(this.decrypt_with_privateKey(encryptedKey, privateKey)) fileDecryptInfo.iv = this.string_to_iv(fileDecryptInfo.iv) resolve(fileDecryptInfo) } catch(err) { reject(err) } }) } this.key_to_string = function(keyInfo) { keyInfo.iv = this.iv_to_string(keyInfo.iv) return JSON.stringify(keyInfo) } this.download_file_for_record = function(recordInfo, password, version, asPlaintext) { return new Promise(async (resolve, reject) => { if (!recordInfo.files) { reject("No files exist for record") } if (version < 0) { reject("Version number must be non-negative integer") } if (version >= recordInfo.files.length) { reject(`Invalid version number ${version} for record with ${recordInfo.files.length} versions`) } if (version !== 0) version = version || recordInfo.files.length - 1 // default to recent const fileInfo = recordInfo.files[version] let fileURL = fileInfo.remoteURL let encryptedKeyInfo = recordInfo[this.email.toLowerCase().replace(/\./g, " ")] if (!encryptedKeyInfo) { reject(`User ${this.email} does not have key for decrypting file`); return; } let fileDecryptInfo = {} if (encryptedKeyInfo.iv) { // for symmetric, personal key, non-RSA fileDecryptInfo = this.decrypt_fileKey_symmetric(encryptedKeyInfo, password) } else { // for RSA try { fileDecryptInfo = await this.decrypt_fileKey_asymmetric(encryptedKeyInfo, password) } catch(err) { reject(err) } } let fileDKey = fileDecryptInfo.key let fileDiv = fileDecryptInfo.iv let http = new_HTTP(); http.open("GET", this.build_full_URL(fileURL), true); http.responseType = "arraybuffer" http.onreadystatechange = () => { if (http.readyState === 4 && http.status === 200) { let encryptedData = new Uint8Array(http.response) let plaintext = this.decrypt_string(encryptedData, fileDKey, fileDiv, 'base64') // this can be used for auto-verification (before splitting) let file = { type: fileInfo.type, name: fileInfo.name, data: asPlaintext ? plaintext : this.str2ab(plaintext) } resolve(file) } else if (http.readyState === 4) { reject(http.responseText) } }; http.send() }) } this.download_file_for_recordID = function(recordID, password, version, asPlaintext) { return new Promise((resolve, reject) => { if (!password) { reject("NO PASSWORD PROVIDED"); return; } this.get_info_for_recordID(recordID) .then(recordInfo => { this.download_file_for_record(recordInfo, password, version, asPlaintext) .then(file => resolve(file)) .catch(err => reject(err)) }) .catch(err => reject(err)) }) } this.get_user_info = function() { return new Promise((resolve, reject) => { var xhr = new_HTTP(); let url = this.host + '/get-user-info?authToken=' + this.authToken xhr.open("GET", url, true); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded") xhr.onreadystatechange = () => { if (xhr.readyState === 4 && xhr.status === 200) { resolve(JSON.parse(xhr.responseText)) } else if (xhr.readyState === 4) { reject(xhr.responseText) } }; xhr.send(); }) } this.share_record_access = function(recordID, shareEmail) { return new Promise((resolve, reject) => { var xhr = new_HTTP(); xhr.open("POST", this.host + '/share-record', true); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded") xhr.onreadystatechange = () => { if (xhr.readyState === 4 && xhr.status === 204) { resolve(xhr.responseText) } else if (xhr.readyState === 4) { reject(xhr.responseText) } }; let sendstring = 'recordID=' + recordID sendstring += '&shareEmail=' + shareEmail sendstring += '&authToken=' + this.authToken xhr.send(sendstring); }) } this.share_record = function(recordID, shareEmail, password) { return new Promise(async (resolve, reject) => { try { let done = await this.share_record_access(recordID, shareEmail) let userInfo = await this.get_user_info() let recordInfo = await this.get_info_for_recordID(recordID) let publicKey = await this.get_public_key(shareEmail) if (recordInfo.creator.toLowerCase() !== this.email.toLowerCase()) { reject(`Only the record creator ${recordInfo.creator} may share file content`) return } if (!recordInfo.files || (recordInfo.files && recordInfo.files.length === 0)) { reject("No files exist for record"); return; } let keyField = this.email.toLowerCase().replace(/\./g, " ") if (!recordInfo[keyField]) { reject(`No key set for user ${this.email}`); return; } const keyInfo = this.decrypt_fileKey_symmetric(recordInfo[keyField], password) const keyString = this.key_to_string(keyInfo) const encryptedKey = this.encrypt_with_publicKey(keyString, publicKey) this.store_user_key_for_record(shareEmail, recordID, encryptedKey) .catch(err => reject(err)) .finally(() => resolve()) } catch(err) { reject(err) } }) } this.create_data_management = function(content, name, type, password, desc) { return new Promise((resolve, reject) => { // Good practice to keep account encrypted unless in use (signing transaction) let account = undefined; try { account = this.web3.eth.accounts.decrypt( this.encryptedKey, password + this.salt ); } catch(err) { if (!this.email) { reject("User not yet authenticated."); } else { reject("User password invalid") } return; } let signature = account.sign(content) signature.message = "" // Maintain data privacy var xhr = new_HTTP(); let sendstring = "" xhr.open("POST", this.host + "/submit-new-data-upload", true); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded") xhr.onreadystatechange = () => { if (xhr.readyState === 4 && xhr.status === 200) { resolve(xhr.responseText) } else if (xhr.readyState === 4) { reject(xhr.responseText) } }; sendstring += 'filename=' + name sendstring += '&filedesc=' + desc sendstring += '&signature=' + JSON.stringify(signature) sendstring += '&type=' + type sendstring += '&authToken=' + this.authToken xhr.send(sendstring); }) } this.update_data_management = function(recordID, newContent, password) { return new Promise((resolve, reject) => { if (!this.utils.parse_record_ID(recordID)) { reject("Invalid recordID"); return; } let account = undefined; try { account = this.web3.eth.accounts.decrypt( this.encryptedKey, password + this.salt ); } catch(err) { reject(err); return; } this.get_data_management_history(recordID, 'editable').then((history) => { let priorHash = history.pop().hash let signature = account.sign(newContent + priorHash) signature.message = "" // Waste of space to send full message var xhr = new_HTTP(); let sendstring = "" xhr.open("POST", this.host + "/update-data-storage", true); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded") xhr.onreadystatechange = () => { if (xhr.readyState === 4 && xhr.status === 204) { resolve() } else if (xhr.readyState === 4) { reject(xhr.responseText) } }; sendstring += 'contractAddr=' + recordID // from submit-new-data-upload sendstring += '&signature=' + JSON.stringify(signature) sendstring += '&authToken=' + this.authToken sendstring += '&plainHash=' + this.web3.eth.accounts.hashMessage(newContent) xhr.send(sendstring); }).catch((err) => { reject(err) }) }) } this.get_data_management_history = function(recordID, type) { return new Promise((resolve, reject) => { let recordInfo = this.utils.parse_record_ID(recordID) if (!recordInfo) { reject("Invalid recordID"); return; } let shardIndex = this.utils.hex_to_shardIndex(recordInfo.shardHex) const web3 = new Web3(this.utils.shard_to_URL(shardIndex, true)) let contract = undefined if (type === "basic") { contract = new web3.eth.Contract(BASIC_DATA_STORAGE_ABI, recordInfo.address); } else if (type === "editable") { contract = new web3.eth.Contract(EDITABLE_DATA_STORAGE_ABI, recordInfo.address); } else { console.warn(`Unexpected type: ${type}... defaulting to editable`) contract = new web3.eth.Contract(EDITABLE_DATA_STORAGE_ABI, recordInfo.address); } contract.getPastEvents('Updated', { fromBlock: 0 }) .then((events) => { let history = [] for (let i = 0; i < events.length; i++) { let event = events[i] history.push({ timestamp: this.utils.convert_unix_time(event.returnValues.timestamp), hash: event.returnValues.hash, signer: event.returnValues.updater }) } resolve(history) }).catch((err) => { reject(err) }) }) } this.verify_data_management = function (recordID, type, verificationContent) { return new Promise((resolve, reject) => { let recordInfo = this.utils.parse_record_ID(recordID) if (!recordInfo) { reject("Invalid recordID"); return; } this.get_data_management_history(recordID, type).then((history) => { this.utils.addresses_to_emails(history).then((history) => { for (let i = 0; i < history.length; i++) { let priorHash = "" if (i > 0) priorHash = history[i - 1].hash let hash = this.web3.eth.accounts.hashMessage(verificationContent + priorHash) if (hash.toLowerCase() === history[i].hash.toLowerCase()) { resolve(history[i]) return } } resolve(false) }).catch((err) => { reject(err) }) }).catch((err) => { reject(err) }) }) } this.create_digital_agreement = function(content, name, type, password, signers, desc) { return new Promise((resolve, reject) => { let account = undefined; try { account = this.web3.eth.accounts.decrypt( this.encryptedKey, password + this.salt ); } catch(err) { if (!this.email) { reject("User not yet authenticated."); } else { reject("User password invalid") } return; } let hashedData = this.web3.utils.sha3(content) let signature = account.sign(hashedData) signature.message = "" // Unnecessary for request var xhr = new_HTTP(); let sendstring = "" xhr.open("POST", this.host + "/submit-new-agreement-upload", true); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded") xhr.onreadystatechange = () => { if (xhr.readyState === 4 && xhr.status === 200) { resolve(xhr.responseText) } else if (xhr.readyState === 4) { reject(xhr.responseText) } }; sendstring += 'filename=' + name sendstring += '&filedesc=' + desc sendstring += '&signature=' + JSON.stringify(signature) sendstring += '&type=' + type if (type === 'multisig') sendstring += '&signers=' + JSON.stringify(signers) sendstring += '&fileHash=' + hashedData sendstring += '&authToken=' + this.authToken xhr.send(sendstring); }) } this.sign_digital_agreement = function(recordID, password) { return new Promise((resolve, reject) => { if (!this.utils.parse_record_ID(recordID)) { reject("Invalid recordID"); return; } let account = undefined; try { account = this.web3.eth.accounts.decrypt( this.encryptedKey, password + this.salt ); } catch(err) { return err; } var http = new_HTTP() let sendstring = "contractAddr=" + recordID sendstring += "&authToken=" + this.authToken http.open("POST", this.host + "/get-contract-info", true) http.setRequestHeader("Content-Type", "application/x-www-form-urlencoded") http.onreadystatechange = () => { if (http.readyState === 4 && http.status === 200) { let response = JSON.parse(http.responseText) let currentHash = response.hashes.pop() let signature = account.sign(currentHash) signature.message = "" // Unnecessary for request var xhr = new_HTTP(); sendstring = "" xhr.open("POST", this.host + "/sign-agreement", true); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded") xhr.onreadystatechange = () => { if (xhr.readyState === 4 && xhr.status === 204) { resolve() } else if (xhr.readyState === 4) { r