/********************************************************************************** * Copyright by Safevia.net 2020 **********************************************************************************/ var App = window.App = window.App || {}; App.encrypt = App.encrypt || {}; App.encrypt.MAX_MESSAGE_LENGTH = 50000; App.encrypt.PASSWORD_ALGORITHM = { name: "PBKDF2", iterations: 150000, hash: "SHA-256" }; App.encrypt.ENCRYPTION_ALGORITHM = { name: "AES-GCM", length: 256, tagLength: 128 }; App.encrypt.MAX_FILE_SIZE = 10485760; App.encrypt.MAX_TOTAL_SIZE = 10485760; App.encrypt.file = []; App.encrypt.attachmentIDFactory = 0; App.encrypt.generateRandomBytes = function(len) { var bytes = new Uint8Array(len); if (App.crypto && App.crypto.getRandomValues) { App.crypto.getRandomValues(bytes); //use (theoretically) cryptographically strong random values (but it's browser-specific, we don't know the internals) } //XORing cryptographic source of random values with other sources (even non-cryptographic ones) should not make the result weaker, but potentially even stronger, just in case browser implementation is vulnerable for (var i = 0; i < bytes.length; i++) { var b = bytes[i]; var r = Date.now(); //milliseconds from UNIX epoch if (window.performance) { r += window.performance.now(); //current nanoseconds from browsing start } r *= Math.random(); //multiply with some random between 0 and 1 b ^= r % 255; //XOR with random byte using byte modulo bytes[i] = b; } return bytes; } App.encrypt.generateRandomTextToken = function(len) { //this is URL-safe text using Base-58 alphabet to ensure no misunderstanding when dictating var alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; var output = ''; var randomBytes = App.encrypt.generateRandomBytes(len); for (var i = 0; i < len; i++) { var alphabetIndex = randomBytes[i] % alphabet.length; output += alphabet[alphabetIndex]; } return output; } App.encrypt.fail = function(e) { App.fail(e); App.GUI.screen('screen-failure'); } App.encrypt.togglePasswordProtection = function() { var isEnabled = App.GUI.element('prepare-password-protection-enabler').checked; var elPassword = App.GUI.element('prepare-password-protection-input'); var elNotice = App.GUI.element('prepare-password-protection-notice'); if (isEnabled) { App.GUI.show(elPassword); App.GUI.show(elNotice); elPassword.setAttribute('required', true); elPassword.focus(); } else { App.GUI.hide(elPassword); App.GUI.hide(elNotice); elPassword.setAttribute('required', false); } } App.encrypt.doPrepareChooseFile = function() { App.GUI.element('prepare-choose-file-input').click(); } App.encrypt.doPrepareResetFile = function(attachmentID) { var files = App.encrypt.file; for (var i = 0; i < files.length; i++) { if (files[i].attachmentID == attachmentID) { files.splice(i, 1); } } var toRemove = App.GUI.element('prepare-file-info-view-template' + attachmentID); toRemove.parentNode.removeChild(toRemove); if (App.encrypt.file.length == 0) { App.GUI.hide('prepare-file-info-view'); } } App.encrypt.onPrepareMessageChange = function() { var textarea = App.GUI.element('prepare-message-textarea'); var text = textarea.value; if (text.length > App.MAX_MESSAGE_LENGTH) { text = text.substr(0, App.MAX_MESSAGE_LENGTH); textarea.value = text; } App.GUI.element('prepare-message-len').textContent = text.length; } App.encrypt.onPrepareFileChange = function() { var input = App.GUI.element('prepare-choose-file-input'); App.encrypt.collectFile(input.files); } App.encrypt.verifyFileLimit = function() { var files = App.encrypt.file; if (files) { var sizeCorrect = true; var totalSize = 0; for (var i = 0; i < files.length; i++) { var file = files[i]; totalSize += file.size; if (App.encrypt.MAX_FILE_SIZE && file.size > App.encrypt.MAX_FILE_SIZE) { sizeCorrect = false; App.GUI.show('prepare-file-info-parameter-too-big' + file.attachmentID); } else { App.GUI.hide('prepare-file-info-parameter-too-big' + file.attachmentID); } } //Let's show total size warning only if each single file is small enough if (sizeCorrect && App.encrypt.MAX_TOTAL_SIZE && totalSize > App.encrypt.MAX_TOTAL_SIZE) { sizeCorrect = false; App.GUI.show('prepare-files-info-parameter-too-big'); } else { App.GUI.hide('prepare-files-info-parameter-too-big'); } if (sizeCorrect) { App.GUI.show('prepare-encrypt-btn'); } else { App.GUI.hide('prepare-encrypt-btn'); } } } App.encrypt.collectFile = function(files) { var input = App.GUI.element('prepare-choose-file-input'); if (files && files.length > 0) { var file = files[0]; App.encrypt.file.push(file); App.GUI.show('prepare-file-info-view'); App.encrypt.attachmentIDFactory++; var newGUIRow = App.duplicateHTML(App.GUI.element('prepare-file-info-view-template'), App.GUI.element('prepare-file-info-view'), App.encrypt.attachmentIDFactory); var prepareRemoveButtonWithID = function(attachmentID) { return function(clickInfo) { App.encrypt.doPrepareResetFile(attachmentID); App.encrypt.verifyFileLimit(); } } App.GUI.click("prepare-file-reset-btn" + App.encrypt.attachmentIDFactory, prepareRemoveButtonWithID(App.encrypt.attachmentIDFactory)); file.attachmentID = App.encrypt.attachmentIDFactory; App.GUI.show(newGUIRow.id); App.GUI.element('prepare-file-name' + App.encrypt.attachmentIDFactory).textContent = file.name; App.GUI.element('prepare-file-size' + App.encrypt.attachmentIDFactory).textContent = file.size; App.encrypt.verifyFileLimit(); } } App.encrypt.doPrepareEncrypt = function() { var text = App.GUI.element('prepare-message-textarea').value; text = text.substr(0, App.encrypt.MAX_MESSAGE_LENGTH); if (text == "" && App.encrypt.file.length == 0) { App.GUI.show('provide-something-msg'); return; } var password = ""; if (App.GUI.element('prepare-password-protection-enabler').checked) { var elPassword = App.GUI.element('prepare-password-protection-input'); var password = elPassword.value; if (password.length == 0) { elPassword.focus(); return; } } var usePassword = password.length > 0; App.GUI.screen('screen-progress'); var limitTimeSeconds = App.GUI.element('prepare-limit-time-seconds').value; if (isNaN(limitTimeSeconds)) limitTimeSeconds = 0; limitTimeSeconds = Math.max(0, Math.min(604800, limitTimeSeconds)); var userToken = App.encrypt.generateRandomTextToken(10); var keyBase64URL; var passordCheckBase64URL; var idMessage; var saltPasswordCheck; var saltContentKey; var encrypt = function (key, data, generateRandomIV) { var iv; if (generateRandomIV) { iv = App.encrypt.generateRandomBytes(12); } else { iv = new Uint8Array(12); //automatically filled by 0 - fine for AES GCM to use one time (nonce) } var ivHexString = App.convert.Uint8ArrayToHexString(iv); var algo = App.util.clone(App.encrypt.ENCRYPTION_ALGORITHM); algo.iv = iv; algo.additionalData = new Uint8Array(1); var algoSerialized = { iv: ivHexString }; return App.crypto.subtle.encrypt(algo, key, data) .then(function(encryptedData) { return { algo: algoSerialized, encryptedData: encryptedData } }); } var encryptAndUpload = function (idMessage, idBlob, fileNumber, blobDescriptors, encryptionKey, data, generateRandomIV, callbackSuccess, callbackFailure) { encrypt(encryptionKey, data, generateRandomIV) .then(function (ret) { var data = ret.encryptedData; var parameters = { "msgid": idMessage, "blob": idBlob }; if (blobDescriptors != null) { var blobChunks = blobDescriptors.chunks; var blobDescriptor = blobChunks; if (Array.isArray(blobChunks)) { blobDescriptor = {}; blobChunks.push(blobDescriptor); } blobDescriptor.id = idBlob; blobDescriptor.algo = ret.algo; blobDescriptor.len = data.byteLength; parameters.fileNumber = fileNumber; } var request = { service: "upload", parameters: parameters, body: new Blob([data]), success: function(response) { if (response && response.status == 'OK') { callbackSuccess(ret); } else { callbackFailure(); } }, failure: callbackFailure }; App.callService(request); }).catch(App.encrypt.fail); } var processPreparationResponse = function(response) { idMessage = response.m; serverIndicator = response.s; var promiseKey; if (usePassword) { saltContentKey = App.encrypt.generateRandomBytes(16); promiseKey = App.deriveKeyFromPassword(password, App.encrypt.PASSWORD_ALGORITHM, App.encrypt.ENCRYPTION_ALGORITHM, saltContentKey); } else { var keyRandomBytes = App.encrypt.generateRandomBytes(App.encrypt.ENCRYPTION_ALGORITHM.length / 8); keyBase64URL = App.util.encodeBase64URL(keyRandomBytes); promiseKey = App.crypto.subtle.importKey('raw', keyRandomBytes.buffer, App.encrypt.ENCRYPTION_ALGORITHM, false, [ "encrypt", "decrypt" ] ); //not using generateKey here as we can't be sure about web browser's implementation of random key } promiseKey.then(function (key) { var meta = {}; meta.text = text; meta.fs = []; var totalUploads = 1; //include META as the last one var countUploaded = 0; var uploadedMetaCallbackFund = function() { App.runNext(function() { var url = App.BASE_WEB_URL + '-/?m=' + idMessage + "&s=" + serverIndicator + '#'; if (!usePassword) { url += 'k=' + keyBase64URL + '&'; } else { url += 'sc=' + App.util.encodeBase64URL(saltPasswordCheck) + '&'; url += 'sk=' + App.util.encodeBase64URL(saltContentKey) + '&'; } url += 'u=' + userToken; var elLink = App.GUI.element('ready-share-link'); elLink.setAttribute('href', url); elLink.textContent = url; App.GUI.screen('screen-ready'); }); } var uploadMeta = function() { encryptAndUpload(idMessage, "META", -1, null, key, App.util.encodeUTF8(JSON.stringify(meta)), false, uploadedMetaCallbackFund, App.fail); } var uploadedBlobCallbackFunc = function() { countUploaded++; if (countUploaded === totalUploads - 1) { //when all uploads are finished (except META) - gather meta-data, encode it and also upload uploadMeta(); } } var file = App.encrypt.file; if (file != null && file.length > 0) { totalUploads += file.length; var fileUploadingProcess = function(fileNumber) { var fileReader = new FileReader(); var totalSize = 0; //TODO: [feature] support file slicing (multiple chunks on bigger files) fileReader.onload = function (e) { totalSize += file[fileNumber].size; if (totalSize > App.encrypt.MAX_FILE_SIZE) { App.encrypt.fail("Attempted upload of files bigger than the limit!"); throw Error("Attempted upload of files bigger than the limit!") } var data = e.target.result; var fileName = file[fileNumber].name; if (!fileName) { fileName = "file_" + fileNumber; } var fileDesc = { "fileName" : fileName, "len": file[fileNumber].size, "chunks": [] } meta.fs.push(fileDesc); encryptAndUpload(idMessage, "FILE", fileNumber, meta.fs[fileNumber], key, data, true, uploadedBlobCallbackFunc, App.fail); }; fileReader.readAsArrayBuffer(file[fileNumber]); } App.runNext(function() { for (var fileNumber = 0; fileNumber < file.length; fileNumber++) { fileUploadingProcess(fileNumber); } }); } else { uploadMeta(); } }) .catch(App.encrypt.fail); }; var doRequest = function() { App.callService({ service: "prepare", body: { pwdAlgo: usePassword ? App.encrypt.PASSWORD_ALGORITHM : null, pwdCheck: usePassword ? passordCheckBase64URL : null, encAlgo: App.encrypt.ENCRYPTION_ALGORITHM, limitTimeSeconds: limitTimeSeconds, userTokens: [ userToken ], noOfAttachments: App.encrypt.file.length }, success: function(response) { if (response) { try { processPreparationResponse(response); } catch (e) { App.encrypt.fail(e); } } else { App.encrypt.fail("Not valid response from message creator"); } }, failure: App.encrypt.fail }); } if (usePassword) { saltPasswordCheck = App.encrypt.generateRandomBytes(16); promiseKey = App.deriveKeyFromPassword(password, App.encrypt.PASSWORD_ALGORITHM, App.encrypt.ENCRYPTION_ALGORITHM, saltPasswordCheck) .then(function(passwordCheckCryptoKey) { return App.crypto.subtle.exportKey('raw', passwordCheckCryptoKey); }) .then(function(passwordCheckExportedKey) { passordCheckBase64URL = App.util.encodeBase64URL(passwordCheckExportedKey); doRequest(); }); } else { doRequest(); } } App.encrypt.doReadyCopyToClipboard = function() { App.GUI.hideAll('ready-clipboard-result'); var url = App.GUI.element('ready-share-link').getAttribute('href'); App.util.copyToClipboard(url, function() { App.GUI.hideAll('ready-clipboard-result'); App.GUI.show('ready-clipboard-done'); }, function(status) { App.GUI.hideAll('ready-clipboard-result'); if (status == App.FAIL_NO_PERMISSION) App.GUI.show('ready-clipboard-fail-not-supported'); else if (status == App.FAIL_NEED_PERMISSION) App.GUI.show('ready-clipboard-fail-need-permission'); else App.GUI.show('ready-clipboard-fail-not-supported'); }); } App.encrypt.doShowLink = function() { App.GUI.hide('ready-show-link-btn'); App.GUI.show('ready-hide-link-btn'); App.GUI.show('ready-link-box'); } App.encrypt.doHideLink = function() { App.GUI.hide('ready-hide-link-btn'); App.GUI.show('ready-show-link-btn'); App.GUI.hide('ready-link-box'); } App.encrypt.doShowQRCode = function() { App.GUI.hide('ready-show-qrcode-btn'); App.GUI.show('ready-hide-qrcode-btn'); var elLink = App.GUI.element('ready-share-link'); var link = elLink.getAttribute('href'); var qrCodeEl = document.getElementById('qrcode-div'); qrCodeEl.innerHTML = ""; new QRCode(qrCodeEl, link); App.GUI.show('qrcode-div'); } App.encrypt.doHideQRCode = function() { App.GUI.hide('ready-hide-qrcode-btn'); App.GUI.show('ready-show-qrcode-btn'); App.GUI.hide('qrcode-div'); } App.encrypt.onAcceptRegAct = function() { var encryptBtn = App.GUI.element('prepare-encrypt-btn'); var regActCheckbox = App.GUI.element('accept-regulation-act'); encryptBtn.disabled = !regActCheckbox.checked; if (App.util.isCookieAccepted("privacy")) { App.util.setCookie("regActAccepted", regActCheckbox.checked); } } App.encrypt.onDropAction = function(ev) { if (ev.dataTransfer.items) { // Use DataTransferItemList interface to access the file(s) for (var i = 0; i < ev.dataTransfer.items.length; i++) { // If dropped items aren't files, reject them if (ev.dataTransfer.items[i].kind === 'file') { var file = ev.dataTransfer.items[i].getAsFile(); App.encrypt.collectFile([file]); } } } else { App.encrypt.collectFile(ev.dataTransfer.files); } App.encrypt.onDragEnd(); } App.encrypt.onDragAction = function() { if (!App.GUI.shown('drop-file-info')) App.GUI.show('drop-file-info'); return true; } App.encrypt.onDragEnd = function() { App.GUI.hide('drop-file-info'); return true; } App.addInitListener(function() { if (!App.GUI.exist('app-encrypt')) return; App.GUI.click("prepare-choose-file-btn", App.encrypt.doPrepareChooseFile); App.GUI.click("prepare-encrypt-btn", App.encrypt.doPrepareEncrypt); App.GUI.click("ready-clipboard-btn", App.encrypt.doReadyCopyToClipboard); App.GUI.click("ready-show-link-btn", App.encrypt.doShowLink); App.GUI.click("ready-hide-link-btn", App.encrypt.doHideLink); App.GUI.click("ready-show-qrcode-btn", App.encrypt.doShowQRCode); App.GUI.click("ready-hide-qrcode-btn", App.encrypt.doHideQRCode); App.GUI.setFunction("change", "accept-regulation-act", App.encrypt.onAcceptRegAct); App.GUI.ondrop("drop-file-info", App.encrypt.onDropAction); App.GUI.ondragover("screen-prepare", App.encrypt.onDragAction); App.GUI.ondragend("drop-file-info", App.encrypt.onDragEnd); var cookie = App.util.readCookie("regActAccepted"); var regActCheckbox = App.GUI.element('accept-regulation-act'); var regActVisible = (window.getComputedStyle(regActCheckbox) != 'none'); App.GUI.element('accept-regulation-act').checked = regActVisible && (cookie != "" && cookie != null && cookie == "true"); App.GUI.element('prepare-encrypt-btn').disabled = regActVisible && !App.GUI.element('accept-regulation-act').checked; App.GUI.element("prepare-password-protection-enabler").onchange = App.encrypt.togglePasswordProtection; App.GUI.element("prepare-choose-file-input").onchange = App.encrypt.onPrepareFileChange; App.GUI.hide('prepare-file-info-view'); App.GUI.hide('prepare-file-info-view-template'); var textarea = App.GUI.element('prepare-message-textarea'); textarea.onkeyup = textarea.onblur = textarea.onpaste = textarea.onchange = App.encrypt.onPrepareMessageChange; textarea.setAttribute('maxlength', App.encrypt.MAX_MESSAGE_LENGTH); App.GUI.element('prepare-message-maxlen').textContent = App.encrypt.MAX_MESSAGE_LENGTH; App.encrypt.onPrepareMessageChange(); App.encrypt.togglePasswordProtection(); textarea.focus(); });