Something went wrong. Please try again later.
'; 78 | } 79 | 80 | 81 | 82 | //----------------------Save&Load configuration-----------------------------// 83 | 84 | function loaddropdown (name){ 85 | if (localStorage.getItem(name) != null){ 86 | document.getElementById(name).selectedIndex = localStorage.getItem(name) 87 | } 88 | } 89 | 90 | loaddropdown("dataregion") 91 | datastoreregion= dataregion[dataregion.selectedIndex].value; 92 | loaddropdown("expiretime") 93 | 94 | function savedropdown (name, value){ 95 | localStorage.setItem(name, value); 96 | } 97 | 98 | 99 | //-------------------------Helper functions------------------------------// 100 | 101 | function uuidv4() { 102 | return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => 103 | (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) 104 | ); 105 | } 106 | 107 | 108 | function getUrlVars() { 109 | urlwithoutanchor=window.location.href.split("#")[0] 110 | var vars = {}; 111 | var parts = urlwithoutanchor.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (m, key, value) { 112 | vars[key] = value; 113 | }); 114 | return vars; 115 | } 116 | 117 | function Uint8ToString(u8a){ 118 | var CHUNK_SZ = 0x8000; 119 | var c = []; 120 | for (var i=0; i < u8a.length; i+=CHUNK_SZ) { 121 | c.push(String.fromCharCode.apply(null, u8a.subarray(i, i+CHUNK_SZ))); 122 | } 123 | return c.join(""); 124 | } 125 | 126 | function updateimgtag(extension,plaintextbytes){ 127 | var b64encoded = btoa(Uint8ToString(plaintextbytes)); 128 | divDecryptImage.style.display = "block" 129 | imgDecryptImage.src = "data:image/"+extension+";base64,"+b64encoded; 130 | } 131 | 132 | function copyURI(evt) { 133 | evt.preventDefault(); 134 | navigator.clipboard.writeText(downloadurl).then(() => { 135 | /* clipboard successfully set */ 136 | }, () => { 137 | alert("Failed to copy to clipboard! Please try manually copying it!"); 138 | /* clipboard write failed */ 139 | }); 140 | } 141 | 142 | 143 | function copytextarea() { 144 | let textarea = document.getElementById("textareaDecryptmessage"); 145 | textarea.select(); 146 | document.execCommand("copy"); 147 | } 148 | 149 | 150 | function switchdiv(t) { 151 | if (t == 'encryptfile') { 152 | divEncryptResult.style.display = 'none'; 153 | divEncrypt.style.display = 'block'; 154 | divDecrypt.style.display = 'none'; 155 | encryptemessagemode = false; 156 | 157 | divEncryptMessage.style.display = 'none'; 158 | divEncryptFile.style.display = 'block'; 159 | divFilename.style.display = ''; 160 | originalfilename = "plaintext.txt"; 161 | 162 | btnDivEncrypt.disabled = true; 163 | btnDivDecrypt.disabled = false; 164 | btnDivEncMes.disabled = false; 165 | mode = 'encrypt'; 166 | } else if (t == 'encryptmessage') { 167 | divEncryptResult.style.display = 'none'; 168 | divEncrypt.style.display = 'block'; 169 | divDecrypt.style.display = 'none'; 170 | encryptemessagemode = true; 171 | 172 | divEncryptMessage.style.display = 'block'; 173 | divEncryptFile.style.display = 'none'; 174 | divFilename.style.display = 'none'; 175 | originalfilename = "messageinbrowser.txt" 176 | 177 | btnDivEncrypt.disabled = false; 178 | btnDivDecrypt.disabled = false; 179 | btnDivEncMes.disabled = true; 180 | mode = 'encrypt'; 181 | } else if (t == 'decrypt') { 182 | divEncryptResult.style.display = 'none'; 183 | divEncrypt.style.display = 'none'; 184 | divDecrypt.style.display = 'block'; 185 | encryptemessagemode = false; 186 | originalfilename = "plaintext.txt"; 187 | if (objmetadata == null){ 188 | 189 | } 190 | 191 | btnDivEncrypt.disabled = false; 192 | btnDivDecrypt.disabled = true; 193 | btnDivEncMes.disabled = false; 194 | mode = 'decrypt'; 195 | } 196 | } 197 | 198 | 199 | function dragover_handler(ev) { 200 | console.log("dragOver"); 201 | // Prevent default select and drag behavior 202 | ev.preventDefault(); 203 | } 204 | 205 | function dragend_handler(ev) { 206 | console.log("dragEnd"); 207 | // Remove all of the drag data 208 | var dt = ev.dataTransfer; 209 | if (dt.items) { 210 | // Use DataTransferItemList interface to remove the drag data 211 | for (var i = 0; i < dt.items.length; i++) { 212 | dt.items.remove(i); 213 | } 214 | } else { 215 | // Use DataTransfer interface to remove the drag data 216 | ev.dataTransfer.clearData(); 217 | } 218 | } 219 | 220 | function selectfile(Files) { 221 | objFile = Files[0]; 222 | displayfile() 223 | } 224 | 225 | 226 | function displayfile() { 227 | originalfilename = objFile.name.replace(/[^A-Za-z0-9\-\_\.]/g, '');; 228 | txtFilename.value = objFile.name; 229 | var s; 230 | var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; 231 | var bytes = objFile.size; 232 | var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); 233 | if (i == 0) { s = bytes + ' ' + sizes[i]; } else { s = (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i]; } 234 | 235 | if (mode == 'encrypt') { 236 | spnencfilename.textContent = objFile.name + ' (' + s + ')'; 237 | } else if (mode == 'decrypt') { 238 | spndecfilename.textContent = objFile.name + ' (' + s + ')'; 239 | } 240 | btnEncrypt.disabled = false; 241 | } 242 | 243 | function readfile(file) { 244 | return new Promise((resolve, reject) => { 245 | var fr = new FileReader(); 246 | fr.onload = () => { 247 | resolve(fr.result) 248 | }; 249 | fr.readAsArrayBuffer(file); 250 | }); 251 | } 252 | 253 | 254 | function postdownloadaction(){ 255 | if (deleteondownload) { 256 | // deletefile(); 257 | return 258 | } 259 | } 260 | function buf2hex(buffer) { // buffer is an ArrayBuffer 261 | return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join(''); 262 | } 263 | 264 | async function sha1(data) { 265 | hash = await crypto.subtle.digest('SHA-1', data); 266 | return buf2hex(hash); 267 | } 268 | 269 | function showmoredecryptioninfo(){ 270 | divExtraDecResult.style.display="block"; 271 | bShowExtraInfo.style.display="none"; 272 | } 273 | 274 | //--------------------------Encryption & Uploading code----------------------------------// 275 | 276 | 277 | // get metadata for a file from lambdaurl 278 | async function getMetadata(objurl) { 279 | var body = document.body; 280 | body.classList.add("loading"); 281 | let url = lambdaurl + objurl+ "?region=" + datastoreregion; 282 | response = await fetch(url); 283 | 284 | body.classList.remove("loading"); 285 | if (response.status == 404) { 286 | bFilename.innerText = "Failed to fetch metadata - File may no longer exist."; 287 | btnDecrypt.disabled = true; 288 | return 289 | } 290 | data = await response.json(); 291 | objmetadata = data; 292 | ss = String(objmetadata.objsize) + " Bytes" 293 | if ((objmetadata.objsize / 1048576) > 1) { 294 | ss = String((objmetadata.objsize / 1048576).toFixed(0)) + " Mb"; 295 | } else if ((objmetadata.objsize / 1024) > 1) { 296 | ss = String((objmetadata.objsize / 1024).toFixed(0)) + " Kb" 297 | } 298 | originalfilename = data.objname.replace(/[^A-Za-z0-9\-\_\.]/g, ''); 299 | bFilename.innerText = originalfilename; 300 | bFilesize.innerText = ss; 301 | await decryptfile(decryptonvisit=true); 302 | body.classList.remove("loading"); 303 | } 304 | 305 | // checking for virus from decrypted blob (This perform a simple sha1 on the blob and send it to lambda function to run against virustotal API) 306 | // DONOT put just your password inside a text file... this obviously mean the sha1 hash of the password will be sent back to lamba service function 307 | async function checkforvirus(filehash) { 308 | let url = lambdaurl + "/sha1/"+filehash+ "?region=" + datastoreregion;; 309 | response = await fetch(url); 310 | data = await response.json(); 311 | console.log(data) 312 | vtlink = data.vtlink 313 | if (data.detect){ 314 | console.log("Virus total detected!"); 315 | spnDecstatus.classList.remove("greenspan"); 316 | spnDecstatus.classList.add("redspan"); 317 | spnDecstatus.innerHTML = "Failed to upload.
'; 376 | } 377 | } 378 | catch(error) { 379 | console.log("Failed to upload"); // This is where you run code if the server returns any errors 380 | console.log(err); 381 | body.classList.remove("loading"); 382 | } 383 | } 384 | 385 | 386 | // Download encrypted object from S3 - This is done before decryption 387 | async function downloadFromS3() { 388 | var url = objmetadata.signedurl 389 | const response = await fetch(url) 390 | modalstatus.innerText="Download encrypted object from S3"; 391 | if (response.status != 200) { 392 | spnDecstatus.innerText = "FAILED to download" 393 | var body = document.body; 394 | body.classList.remove("loading"); 395 | spnDecstatus.classList.remove("greenspan"); 396 | spnDecstatus.classList.add("redspan"); 397 | spnDecstatus.innerHTML = 'Cannot download file from S3..Try again?
'; 398 | return 399 | } 400 | console.log(response.headers.get("x-amz-meta-tag")) 401 | try { 402 | filemetadata = JSON.parse(response.headers.get("x-amz-meta-tag")); 403 | } catch (error) { 404 | filemetadata = {name:"plain.dec",deleteondownload:false}; 405 | } 406 | if (filemetadata.name != "") { 407 | // Just incase - remove non-alphabate characters 408 | originalfilename = filemetadata.name.replace(/[^A-Za-z0-9\-\_\.]/g, '');; 409 | } 410 | if (originalfilename == "messageinbrowser.txt") { 411 | encryptemessagemode = true; 412 | } 413 | deleteondownload = filemetadata.deleteondownload; 414 | 415 | buff = await response.arrayBuffer(); 416 | downloadedcipherbytes = new Uint8Array(buff) 417 | return downloadedcipherbytes 418 | } 419 | 420 | 421 | // Delete S3 object - Note that this operation can be done even without decrypting the file content and the delete button is not visible to the user 422 | // because the HTML display attribute. With that said, there is no real practical way of abusing this as 423 | // the objects are stored in the buckets with unique random uuid that is not possible to bruteforce. 424 | // The only one who can see the list of encrypted objects in the bucket is the owner of the bucket or AWS employee and in which case, they can delete the object via AWS console anyway. 425 | async function deletefile() { 426 | var deleteurl = lambdaurl + "delete/" + objurl+ "?region=" + datastoreregion; 427 | const response = await fetch(deleteurl) 428 | 429 | if (response.status != 200) { 430 | spnDecstatus.classList.remove("greenspan"); 431 | spnDecstatus.classList.add("redspan"); 432 | spnDecstatus.innerHTML += "Failed to delete object" 433 | return 434 | } else { 435 | spnDecstatus.innerHTML = "Deleted object" 436 | } 437 | } 438 | 439 | 440 | 441 | //drag and drop functions: 442 | //https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop 443 | function drop_handler(ev) { 444 | console.log("Drop"); 445 | ev.preventDefault(); 446 | // If dropped items aren't files, reject them 447 | var dt = ev.dataTransfer; 448 | if (dt.items) { 449 | // Use DataTransferItemList interface to access the file(s) 450 | for (var i = 0; i < dt.items.length; i++) { 451 | if (dt.items[i].kind == "file") { 452 | var f = dt.items[i].getAsFile(); 453 | console.log("... file[" + i + "].name = " + f.name); 454 | objFile = f; 455 | } 456 | } 457 | } else { 458 | // Use DataTransfer interface to access the file(s) 459 | for (var i = 0; i < dt.files.length; i++) { 460 | console.log("... file[" + i + "].name = " + dt.files[i].name); 461 | } 462 | objFile = file[0]; 463 | } 464 | displayfile() 465 | } 466 | 467 | // This is where we encrypt the file: 468 | // - Use PBKDF2 to derive passphrasekey from the anchortag(#) key and the user's provided password 469 | // - Encrypt the file/message 470 | // - Upload the file - File name is kept in plaintext as a object tag so recepient knows what they receive 471 | 472 | async function encryptfile() { 473 | var body = document.body; 474 | // Update tempkey to make sure we have fresh new key everytime. 475 | tempkey = uuidv4(); 476 | body.classList.add("loading"); 477 | modalstatus.innerText="Encrypting file with AES using tempkey and user provided password." 478 | btnEncrypt.disabled = true; 479 | var plaintextbytes = null; 480 | 481 | if (encryptemessagemode){ 482 | var plaintextbytes = new TextEncoder("utf-8").encode(textareaEncryptmessage.value) 483 | } else { 484 | var plaintextbytes = await readfile(objFile) 485 | .catch(function (err) { 486 | console.error(err); 487 | body.classList.remove("loading"); 488 | }); 489 | var plaintextbytes = new Uint8Array(plaintextbytes); 490 | } 491 | 492 | if (plaintextbytes.length == 0){ 493 | spnEncstatus.classList.add("redspan"); 494 | spnEncstatus.innerHTML = 'There is nothing to encrypt...
'; 495 | return 496 | } 497 | var pbkdf2iterations = 10000; 498 | var passphrasebytes = new TextEncoder("utf-8").encode(txtEncpassphrase.value + tempkey); 499 | var pbkdf2salt = window.crypto.getRandomValues(new Uint8Array(8)); 500 | 501 | var passphrasekey = await window.crypto.subtle.importKey('raw', passphrasebytes, { name: 'PBKDF2' }, false, ['deriveBits']) 502 | .catch(function (err) { 503 | console.error(err); 504 | body.classList.remove("loading"); 505 | }); 506 | console.log('passphrasekey imported'); 507 | 508 | var pbkdf2bytes = await window.crypto.subtle.deriveBits({ "name": 'PBKDF2', "salt": pbkdf2salt, "iterations": pbkdf2iterations, "hash": 'SHA-256' }, passphrasekey, 384) 509 | .catch(function (err) { 510 | console.error(err); 511 | body.classList.remove("loading"); 512 | }); 513 | console.log('pbkdf2bytes derived'); 514 | pbkdf2bytes = new Uint8Array(pbkdf2bytes); 515 | 516 | keybytes = pbkdf2bytes.slice(0, 32); 517 | ivbytes = pbkdf2bytes.slice(32); 518 | 519 | var key = await window.crypto.subtle.importKey('raw', keybytes, { name: 'AES-CBC', length: 256 }, false, ['encrypt']) 520 | .catch(function (err) { 521 | console.error(err); 522 | body.classList.remove("loading"); 523 | }); 524 | console.log('key imported'); 525 | 526 | var cipherbytes = await window.crypto.subtle.encrypt({ name: "AES-CBC", iv: ivbytes }, key, plaintextbytes) 527 | .catch(function (err) { 528 | console.error(err); 529 | body.classList.remove("loading"); 530 | }); 531 | 532 | if (!cipherbytes) { 533 | spnEncstatus.classList.add("redspan"); 534 | spnEncstatus.innerHTML = 'Error encrypting file. See console log.
'; 535 | return; 536 | } 537 | 538 | console.log('plaintext encrypted'); 539 | cipherbytes = new Uint8Array(cipherbytes); 540 | 541 | var resultbytes = new Uint8Array(cipherbytes.length + 16) 542 | resultbytes.set(new TextEncoder("utf-8").encode('Salted__')); 543 | resultbytes.set(pbkdf2salt, 8); 544 | resultbytes.set(cipherbytes, 16); 545 | 546 | var blob = new Blob([resultbytes], { type: 'application/download' }); 547 | var blobUrl = URL.createObjectURL(blob); 548 | var exp = expiretime[expiretime.selectedIndex].value; 549 | await uploadToS3(exp, resultbytes) 550 | body.classList.remove("loading"); 551 | } 552 | 553 | 554 | // This is where we decrypt the file: 555 | // - Download file from S3 556 | // - Use PBKDF2 to derive passphrasekey from the anchortag(#) key and the user's provided password 557 | // - Decrypt the blob 558 | // - Generate SHA1 of the plaintext and send back for virustotal api check, if return infected - update UI and stop user from downloading 559 | // - Determine if it's a message or file to display appropriately to the user 560 | 561 | async function decryptfile(decryptonvisit=false) { 562 | var body = document.body; 563 | body.classList.add("loading"); 564 | var cipherbytes = downloadedcipherbytes; 565 | if (downloadedcipherbytes == null){ 566 | modalstatus.innerText="Downloading from S3"; 567 | var cipherbytes = await downloadFromS3(); 568 | } 569 | modalstatus.innerText="Decrypting file using anchor key and user provided key"; 570 | var pbkdf2iterations = 10000; 571 | var passphrasebytes = new TextEncoder("utf-8").encode(txtDecpassphrase.value + anchorkey); 572 | var pbkdf2salt = cipherbytes.slice(8, 16); 573 | 574 | var passphrasekey = await window.crypto.subtle.importKey('raw', passphrasebytes, { name: 'PBKDF2' }, false, ['deriveBits']) 575 | .catch(function (err) { 576 | console.error(err); 577 | body.classList.remove("loading"); 578 | 579 | }); 580 | console.log('passphrasekey imported'); 581 | 582 | var pbkdf2bytes = await window.crypto.subtle.deriveBits({ "name": 'PBKDF2', "salt": pbkdf2salt, "iterations": pbkdf2iterations, "hash": 'SHA-256' }, passphrasekey, 384) 583 | .catch(function (err) { 584 | console.error(err); 585 | body.classList.remove("loading"); 586 | }); 587 | console.log('pbkdf2bytes derived'); 588 | pbkdf2bytes = new Uint8Array(pbkdf2bytes); 589 | 590 | keybytes = pbkdf2bytes.slice(0, 32); 591 | ivbytes = pbkdf2bytes.slice(32); 592 | cipherbytes = cipherbytes.slice(16); 593 | 594 | var key = await window.crypto.subtle.importKey('raw', keybytes, { name: 'AES-CBC', length: 256 }, false, ['decrypt']) 595 | .catch(function (err) { 596 | console.error(err); 597 | body.classList.remove("loading"); 598 | }); 599 | console.log('key imported'); 600 | 601 | var plaintextbytes = await window.crypto.subtle.decrypt({ name: "AES-CBC", iv: ivbytes }, key, cipherbytes) 602 | .catch(function (err) { 603 | console.error(err); 604 | body.classList.remove("loading"); 605 | }); 606 | 607 | if (decryptonvisit && !plaintextbytes ){ 608 | console.log("Could not decrypt the file without user key") 609 | body.classList.remove("loading"); 610 | return 611 | } 612 | 613 | if (!plaintextbytes) { 614 | spnDecstatus.classList.remove("greenspan"); 615 | spnDecstatus.classList.add("redspan"); 616 | spnDecstatus.innerHTML = 'Error decrypting file. Password may be incorrect.
'; 617 | return; 618 | } 619 | 620 | console.log('ciphertext decrypted'); 621 | plaintextbytes = new Uint8Array(plaintextbytes); 622 | 623 | var blob = new Blob([plaintextbytes], { type: 'application/download' }); 624 | var blobUrl = URL.createObjectURL(blob); 625 | aDecsavefile.href = blobUrl; 626 | aDecsavefile.download = originalfilename; 627 | spnDecstatus.classList.remove("redspan"); 628 | spnDecstatus.classList.add("greenspan"); 629 | spnDecstatus.innerHTML = 'File decrypted.
'; 630 | divDecsavefile.hidden = false; 631 | aDeleteFile.hidden = false; 632 | modalstatus.innerText="Checking SHA1 hash of the file with Virustotal"; 633 | filehash= await sha1(plaintextbytes); 634 | await checkforvirus(filehash); 635 | // If this is a message send in browser, show it. 636 | body.classList.remove("loading"); 637 | divDecryptInfo.style.display = "none"; 638 | divDecryptResult.style.display = "" 639 | if (encryptemessagemode) 640 | { 641 | textareaDecryptmessage.value = new TextDecoder("utf-8").decode(plaintextbytes); 642 | bCopyText.hidden = false; 643 | divDecryptmessage.style.display = ""; 644 | } 645 | 646 | fileextension = originalfilename.substr(-4) 647 | 648 | switch (fileextension) { 649 | case ".png": 650 | updateimgtag("png",plaintextbytes); 651 | break; 652 | case ".jpg": 653 | updateimgtag("jpg",plaintextbytes); 654 | break; 655 | case "jpeg": 656 | updateimgtag("jpeg",plaintextbytes); 657 | break; 658 | case ".gif": 659 | updateimgtag("gif",plaintextbytes); 660 | break; 661 | 662 | } 663 | 664 | if (deleteondownload) { 665 | bDownloadDecFile.innerText = "Download or Lose it" 666 | deletefile(); 667 | } else { 668 | aDeleteFile.hidden = false; 669 | } 670 | } 671 | 672 | -------------------------------------------------------------------------------- /frontend/assets/tooltip.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Tooltip.js styles 3 | */ 4 | 5 | #tooltip { 6 | position:absolute; 7 | background:#0b80d0; 8 | color:#ffffff; 9 | padding:8px; 10 | z-index:999; 11 | font-size: medium; 12 | } 13 | 14 | #tooltip.alt-tooltip { 15 | background:#490B22; 16 | } -------------------------------------------------------------------------------- /frontend/assets/tooltip.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tooltip.js 3 | * A basic script that applies a mouseover tooltip functionality to all elements of a page that have a data-tooltip attribute 4 | * Matthias Schuetz, http://matthiasschuetz.com 5 | * 6 | * Copyright (C) Matthias Schuetz 7 | * Free to use under the MIT license 8 | */ 9 | 10 | (function (root, factory) { 11 | if (typeof define === "function" && define.amd) { 12 | // AMD. Register as an anonymous module. 13 | define(factory); 14 | } else if (!root.tooltip) { 15 | // Browser globals 16 | root.tooltip = factory(root); 17 | } 18 | }(this, function() { 19 | var _options = { 20 | tooltipId: "tooltip", 21 | offsetDefault: 15 22 | }; 23 | 24 | var _tooltips = []; 25 | var _tooltipsTemp = null; 26 | 27 | function _bindTooltips(resetTooltips) { 28 | if (resetTooltips) { 29 | _tooltipsTemp = _tooltips.concat(); 30 | _tooltips = []; 31 | } 32 | 33 | Array.prototype.forEach.call(document.querySelectorAll("[data-tooltip]"), function(elm, idx) { 34 | var tooltipText = elm.getAttribute("title").trim(); 35 | var options; 36 | 37 | if (resetTooltips && _tooltipsTemp.length && _tooltipsTemp[idx] && _tooltipsTemp[idx].text) { 38 | if (tooltipText.length === 0) { 39 | elm.setAttribute("title", _tooltipsTemp[idx].text); 40 | tooltipText = _tooltipsTemp[idx].text; 41 | } 42 | 43 | elm.removeEventListener("mousemove", _onElementMouseMove); 44 | elm.removeEventListener("mouseout", _onElementMouseOut); 45 | elm.removeEventListener("mouseover", _onElementMouseOver); 46 | } 47 | 48 | if (tooltipText) { 49 | elm.setAttribute("title", ""); 50 | elm.setAttribute("data-tooltip-id", idx); 51 | options = _parseOptions(elm.getAttribute("data-tooltip")); 52 | 53 | _tooltips[idx] = { 54 | text: tooltipText, 55 | options: options 56 | }; 57 | 58 | elm.addEventListener("mousemove", _onElementMouseMove); 59 | elm.addEventListener("mouseout", _onElementMouseOut); 60 | elm.addEventListener("mouseover", _onElementMouseOver); 61 | } 62 | }); 63 | 64 | if (resetTooltips) { 65 | _tooltipsTemp = null; 66 | } 67 | } 68 | 69 | function _createTooltip(text, tooltipId) { 70 | var tooltipElm = document.createElement("pre"); 71 | var tooltipText = document.createTextNode(text); 72 | var options = tooltipId && _tooltips[tooltipId] && _tooltips[tooltipId].options; 73 | 74 | if (options && options["class"]) { 75 | tooltipElm.setAttribute("class", options["class"]); 76 | } 77 | 78 | tooltipElm.setAttribute("id", _options.tooltipId); 79 | tooltipElm.appendChild(tooltipText); 80 | console.log(tooltipElm) 81 | document.querySelector("body").appendChild(tooltipElm); 82 | } 83 | 84 | function _getTooltipElm() { 85 | return document.querySelector("#" + _options.tooltipId); 86 | } 87 | 88 | function _onElementMouseMove(evt) { 89 | var tooltipId = this.getAttribute("data-tooltip-id"); 90 | var tooltipElm = _getTooltipElm(); 91 | var options = tooltipId && _tooltips[tooltipId] && _tooltips[tooltipId].options; 92 | var offset = options && options.offset || _options.offsetDefault; 93 | var scrollY = window.scrollY || window.pageYOffset; 94 | var scrollX = window.scrollX || window.pageXOffset; 95 | var tooltipTop = evt.pageY + offset; 96 | var tooltipLeft = evt.pageX + offset; 97 | 98 | if (tooltipElm) { 99 | tooltipTop = (tooltipTop - scrollY + tooltipElm.offsetHeight + 20 >= window.innerHeight ? (tooltipTop - tooltipElm.offsetHeight - 20) : tooltipTop); 100 | tooltipLeft = (tooltipLeft - scrollX + tooltipElm.offsetWidth + 20 >= window.innerWidth ? (tooltipLeft - tooltipElm.offsetWidth - 20) : tooltipLeft); 101 | 102 | tooltipElm.style.top = tooltipTop + "px"; 103 | tooltipElm.style.left = tooltipLeft + "px"; 104 | } 105 | } 106 | 107 | function _onElementMouseOut(evt) { 108 | var tooltipElm = _getTooltipElm(); 109 | 110 | if (tooltipElm) { 111 | document.querySelector("body").removeChild(tooltipElm); 112 | } 113 | } 114 | 115 | function _onElementMouseOver(evt) { 116 | var tooltipId = this.getAttribute("data-tooltip-id"); 117 | var tooltipText = tooltipId && _tooltips[tooltipId] && _tooltips[tooltipId].text; 118 | if (tooltipText) { 119 | _createTooltip(tooltipText, tooltipId); 120 | } 121 | } 122 | 123 | function _parseOptions(options) { 124 | var optionsObj; 125 | 126 | if (options.length) { 127 | try { 128 | optionsObj = JSON.parse(options.replace(/'/ig, "\"")); 129 | } catch(err) { 130 | console.log(err); 131 | } 132 | } 133 | 134 | return optionsObj; 135 | } 136 | 137 | function _init() { 138 | window.addEventListener("load", _bindTooltips); 139 | } 140 | 141 | _init(); 142 | 143 | return { 144 | setOptions: function(options) { 145 | for (var option in options) { 146 | if (_options.hasOwnProperty(option)) { 147 | _options[option] = options[option]; 148 | } 149 | } 150 | }, 151 | refresh: function() { 152 | _bindTooltips(true); 153 | } 154 | }; 155 | })); -------------------------------------------------------------------------------- /frontend/clipboard/assets/main.js: -------------------------------------------------------------------------------- 1 | // Get encrypted cloudflare KV stored clipboard data to local clipboard 2 | var currentclipboard = ""; 3 | const getButton = document.getElementById('get'); 4 | txtEncpassphrase.onmouseover = function(){this.type="text"}; 5 | txtEncpassphrase.onmouseout = function(){this.type="password"}; 6 | 7 | getButton.addEventListener('click', async () => { 8 | var body = document.body; 9 | body.classList.add("loading"); 10 | document.getElementById("status").innerText = "Getting latest clipboard ..."; 11 | getcurrentclipboard().then(plaintext => { 12 | if (plaintext.startsWith("ClipboardError:")) { 13 | document.getElementById("status").innerText = "Failed to get latest clipboard." + plaintext; 14 | body.classList.remove("loading"); 15 | return 16 | } 17 | navigator.clipboard.writeText(plaintext).then(() => { 18 | document.getElementById("status").innerText = "Successfully pull latest clipboard value"; 19 | body.classList.remove("loading"); 20 | }).catch(err => { 21 | document.getElementById("status").innerText = "Failed to get latest clipboard due to: " + err.toString(); 22 | body.classList.remove("loading"); 23 | }) 24 | 25 | }).catch(err => { 26 | document.getElementById("status").innerText = "Failed to get latest clipboard due to: " + err.toString(); 27 | body.classList.remove("loading"); 28 | }) 29 | 30 | 31 | 32 | }) 33 | 34 | // Update cloudflare KV stored clipboard's content 35 | const updateButton = document.getElementById('update') 36 | updateButton.addEventListener('click', async () => { 37 | var body = document.body; 38 | body.classList.add("loading"); 39 | document.getElementById("status").innerText = "Updating ..."; 40 | navigator.clipboard.readText().then(text => { 41 | updateclipboard(text).then(encryptedbytes => { 42 | body.classList.remove("loading"); 43 | }).catch(err => { 44 | document.getElementById("status").innerText = "Failed updating clipboard due to: " + err.toString(); 45 | body.classList.remove("loading"); 46 | }) 47 | 48 | }).catch(err => { 49 | document.getElementById("status").innerText = "Failed updating clipboard due to: " + err.toString(); 50 | body.classList.remove("loading"); 51 | }) 52 | 53 | 54 | }) 55 | 56 | 57 | // The encryption potion of the code was written by meixler, you can find it here https://github.com/meixler/web-browser-based-file-encryption-decryption 58 | // All cryptography operations are implemented using using the Web Crypto API. Files are encrypted using AES-CBC 256-bit symmetric encryption. The encryption key is derived from the password and a random salt using PBKDF2 derivation with 10000 iterations of SHA256 hashing. 59 | 60 | var downloadedcipherbytes = {}; 61 | var anchorkey = window.location.hash.substring(1); 62 | // In tunnel mode tempkey is anchorkey derived from clipboardid 63 | var tempkey = anchorkey; 64 | var body = document.body; 65 | 66 | 67 | /*---------------------------------CREATE/LOAD A TUNNEL------------------------------------*/ 68 | var clipboardid = ""; 69 | clipboardid = getUrlVars()["clipboardid"] 70 | if (clipboardid == undefined) { 71 | clipboardid = "" 72 | while (clipboardid.length < 8) { 73 | clipboardid = prompt("Enter tunnel name (Min 8 characters)") 74 | if (clipboardid == null) { 75 | body.classList.add("loading"); 76 | modalstatus.innerHTML = "This code uses Clipboard()
API to retrieve or update user's clipboard. Clipboard data is AES encrypted in the browser before being sent to Cloudflare worker, you can inspect this in the network tab and review both frontend & backend code. The aim of the project is for me to play with cloudflare KV storage and build something useful.
22 |
23 |
24 | To use this application, simply copy something, enter a password, click "Update clipboard" (You will need to grant this app access to clipboard API) then on another machine, visit this url, enter the same tunnelid & password, click "Get Clipboard" to retrieve it and vice versa.
25 |
26 | Now you can retrieve and update clipboard between multiple machines!
27 |
Upload file successfully. Check Decrypt tab.
'; 278 | refreshfilelist() 279 | } else { 280 | spandownloadurl.innerText = "Failed to upload the file to S3"; 281 | spnEncstatus.classList.remove("greenspan"); 282 | spnEncstatus.classList.add("redspan"); 283 | spnEncstatus.innerHTML = 'Failed to upload.
'; 284 | } 285 | } 286 | catch(error) { 287 | console.log("Failed to upload"); // This is where you run code if the server returns any errors 288 | console.log(err); 289 | body.classList.remove("loading"); 290 | } 291 | } 292 | 293 | function Uint8ToString(u8a){ 294 | var CHUNK_SZ = 0x8000; 295 | var c = []; 296 | for (var i=0; i < u8a.length; i+=CHUNK_SZ) { 297 | c.push(String.fromCharCode.apply(null, u8a.subarray(i, i+CHUNK_SZ))); 298 | } 299 | return c.join(""); 300 | } 301 | 302 | function updateimgtag(extension,plaintextbytes){ 303 | var b64encoded = btoa(Uint8ToString(plaintextbytes)); 304 | divDecryptImage.style.display = "block"; 305 | imgDecryptImage.src = "data:image/"+extension+";base64,"+b64encoded; 306 | } 307 | 308 | async function downloadFromS3(objkey) { 309 | var objmetadata = await getMetadata(objkey); 310 | console.log(objmetadata) 311 | var url = objmetadata.signedurl; 312 | const response = await fetch(url); 313 | 314 | if (response.status != 200) { 315 | spnDecstatus.innerText = "FAILED to download" 316 | return 317 | } 318 | console.log(response.headers.get("x-amz-meta-tag")) 319 | try { 320 | filemetadata = JSON.parse(response.headers.get("x-amz-meta-tag")); 321 | } catch (error) { 322 | filemetadata = {name:"plain.dec",deleteondownload:false}; 323 | } 324 | if (filemetadata.name != "") { 325 | originalfilename = filemetadata.name.replace(/[^A-Za-z0-9\-\_\.]/g, '');; 326 | } 327 | deleteondownload = filemetadata.deleteondownload; 328 | 329 | buff = await response.arrayBuffer(); 330 | downloadedcipherbytes[objkey] = new Uint8Array(buff) 331 | modalstatus.innerText="Decrypting binary blob"; 332 | return [downloadedcipherbytes[objkey],deleteondownload] 333 | } 334 | 335 | async function deletefile(objkey) { 336 | var deleteurl = lambdaurl + "delete/" + objkey 337 | const response = await fetch(deleteurl) 338 | 339 | if (response.status != 200) { 340 | spnDecstatus.classList.remove("greenspan"); 341 | spnDecstatus.classList.add("redspan"); 342 | spnDecstatus.innerHTML += "Failed to delete object"; 343 | return 344 | } else { 345 | spnDecstatus.innerHTML = "Deleted object"; 346 | } 347 | refreshfilelist(); 348 | } 349 | 350 | 351 | //drag and drop functions: 352 | //https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop 353 | function drop_handler(ev) { 354 | console.log("Drop"); 355 | ev.preventDefault(); 356 | // If dropped items aren't files, reject them 357 | var dt = ev.dataTransfer; 358 | if (dt.items) { 359 | // Use DataTransferItemList interface to access the file(s) 360 | for (var i = 0; i < dt.items.length; i++) { 361 | if (dt.items[i].kind == "file") { 362 | var f = dt.items[i].getAsFile(); 363 | console.log("... file[" + i + "].name = " + f.name); 364 | objFile = f; 365 | } 366 | } 367 | } else { 368 | // Use DataTransfer interface to access the file(s) 369 | for (var i = 0; i < dt.files.length; i++) { 370 | console.log("... file[" + i + "].name = " + dt.files[i].name); 371 | } 372 | objFile = file[0]; 373 | } 374 | displayfile() 375 | } 376 | 377 | function dragover_handler(ev) { 378 | console.log("dragOver"); 379 | // Prevent default select and drag behavior 380 | ev.preventDefault(); 381 | } 382 | 383 | function dragend_handler(ev) { 384 | console.log("dragEnd"); 385 | // Remove all of the drag data 386 | var dt = ev.dataTransfer; 387 | if (dt.items) { 388 | // Use DataTransferItemList interface to remove the drag data 389 | for (var i = 0; i < dt.items.length; i++) { 390 | dt.items.remove(i); 391 | } 392 | } else { 393 | // Use DataTransfer interface to remove the drag data 394 | ev.dataTransfer.clearData(); 395 | } 396 | } 397 | 398 | function selectfile(Files) { 399 | objFile = Files[0]; 400 | displayfile() 401 | } 402 | 403 | function displayfile() { 404 | originalfilename = objFile.name.replace(/[^A-Za-z0-9\-\_\.]/g, '');; 405 | txtFilename.value = objFile.name; 406 | var s; 407 | var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; 408 | var bytes = objFile.size; 409 | var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); 410 | if (i == 0) { s = bytes + ' ' + sizes[i]; } else { s = (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i]; } 411 | 412 | if (mode == 'encrypt') { 413 | spnencfilename.textContent = objFile.name + ' (' + s + ')'; 414 | } else if (mode == 'decrypt') { 415 | spndecfilename.textContent = objFile.name + ' (' + s + ')'; 416 | } 417 | btnEncrypt.disabled = false; 418 | } 419 | 420 | function readfile(file) { 421 | return new Promise((resolve, reject) => { 422 | var fr = new FileReader(); 423 | fr.onload = () => { 424 | resolve(fr.result) 425 | }; 426 | fr.readAsArrayBuffer(file); 427 | }); 428 | } 429 | 430 | /*----------------------------------ENCRYPT FILE-------------------------------------*/ 431 | async function encryptfile() { 432 | var body = document.body; 433 | // In 434 | tempkey = anchorkey 435 | body.classList.add("loading"); 436 | modalstatus.innerText="Encrypting file with AES using tempkey and user provided password." 437 | btnEncrypt.disabled = true; 438 | var plaintextbytes = null; 439 | 440 | if (encryptemessagemode){ 441 | var plaintextbytes = new TextEncoder("utf-8").encode(textareaEncryptmessage.value) 442 | if (msgtitle.value != "") { 443 | originalfilename = msgtitle.value.replace(/[^A-Za-z0-9\-\_\.]/g)+".txt" 444 | } 445 | } else { 446 | var plaintextbytes = await readfile(objFile) 447 | .catch(function (err) { 448 | console.error(err); 449 | body.classList.remove("loading"); 450 | }); 451 | var plaintextbytes = new Uint8Array(plaintextbytes); 452 | } 453 | 454 | if (plaintextbytes.length == 0){ 455 | spnEncstatus.classList.add("redspan"); 456 | spnEncstatus.innerHTML = 'There is nothing to encrypt...
'; 457 | return 458 | } 459 | var pbkdf2iterations = 10000; 460 | var passphrasebytes = new TextEncoder("utf-8").encode(txtEncpassphrase.value + tempkey); 461 | var pbkdf2salt = window.crypto.getRandomValues(new Uint8Array(8)); 462 | 463 | var passphrasekey = await window.crypto.subtle.importKey('raw', passphrasebytes, { name: 'PBKDF2' }, false, ['deriveBits']) 464 | .catch(function (err) { 465 | console.error(err); 466 | body.classList.remove("loading"); 467 | }); 468 | console.log('passphrasekey imported'); 469 | 470 | var pbkdf2bytes = await window.crypto.subtle.deriveBits({ "name": 'PBKDF2', "salt": pbkdf2salt, "iterations": pbkdf2iterations, "hash": 'SHA-256' }, passphrasekey, 384) 471 | .catch(function (err) { 472 | console.error(err); 473 | body.classList.remove("loading"); 474 | }); 475 | console.log('pbkdf2bytes derived'); 476 | pbkdf2bytes = new Uint8Array(pbkdf2bytes); 477 | 478 | keybytes = pbkdf2bytes.slice(0, 32); 479 | ivbytes = pbkdf2bytes.slice(32); 480 | 481 | var key = await window.crypto.subtle.importKey('raw', keybytes, { name: 'AES-CBC', length: 256 }, false, ['encrypt']) 482 | .catch(function (err) { 483 | console.error(err); 484 | body.classList.remove("loading"); 485 | }); 486 | console.log('key imported'); 487 | 488 | var cipherbytes = await window.crypto.subtle.encrypt({ name: "AES-CBC", iv: ivbytes }, key, plaintextbytes) 489 | .catch(function (err) { 490 | console.error(err); 491 | body.classList.remove("loading"); 492 | }); 493 | 494 | if (!cipherbytes) { 495 | spnEncstatus.classList.add("redspan"); 496 | spnEncstatus.innerHTML = 'Error encrypting file. See console log.
'; 497 | return; 498 | } 499 | 500 | console.log('plaintext encrypted'); 501 | cipherbytes = new Uint8Array(cipherbytes); 502 | 503 | var resultbytes = new Uint8Array(cipherbytes.length + 16) 504 | resultbytes.set(new TextEncoder("utf-8").encode('Salted__')); 505 | resultbytes.set(pbkdf2salt, 8); 506 | resultbytes.set(cipherbytes, 16); 507 | 508 | var blob = new Blob([resultbytes], { type: 'application/download' }); 509 | var blobUrl = URL.createObjectURL(blob); 510 | var exp = 1; 511 | await uploadToS3(exp, resultbytes) 512 | body.classList.remove("loading"); 513 | // aEncsavefile.href = blobUrl; 514 | // aEncsavefile.download = objFile.name + '.enc'; 515 | // aEncsavefile.hidden = false; 516 | } 517 | 518 | 519 | /*----------------------------------DECRYPT FILE-------------------------------------*/ 520 | 521 | async function decryptfile(objkey,filename) { 522 | var body = document.body; 523 | body.classList.add("loading"); 524 | if (downloadedcipherbytes[objkey] != undefined){ 525 | var cipherbytes = downloadedcipherbytes[objkey]; 526 | } else { 527 | modalstatus.innerText="Downloading from S3"; 528 | var [cipherbytes,deleteondownload] = await downloadFromS3(objkey); 529 | } 530 | modalstatus.innerText="Decrypting file using anchor key and user provided key"; 531 | var pbkdf2iterations = 10000; 532 | var passphrasebytes = new TextEncoder("utf-8").encode(txtDecpassphrase.value + anchorkey); 533 | var pbkdf2salt = cipherbytes.slice(8, 16); 534 | 535 | var passphrasekey = await window.crypto.subtle.importKey('raw', passphrasebytes, { name: 'PBKDF2' }, false, ['deriveBits']) 536 | .catch(function (err) { 537 | console.error(err); 538 | body.classList.remove("loading"); 539 | 540 | }); 541 | console.log('passphrasekey imported'); 542 | 543 | var pbkdf2bytes = await window.crypto.subtle.deriveBits({ "name": 'PBKDF2', "salt": pbkdf2salt, "iterations": pbkdf2iterations, "hash": 'SHA-256' }, passphrasekey, 384) 544 | .catch(function (err) { 545 | console.error(err); 546 | body.classList.remove("loading"); 547 | }); 548 | console.log('pbkdf2bytes derived'); 549 | pbkdf2bytes = new Uint8Array(pbkdf2bytes); 550 | 551 | keybytes = pbkdf2bytes.slice(0, 32); 552 | ivbytes = pbkdf2bytes.slice(32); 553 | cipherbytes = cipherbytes.slice(16); 554 | 555 | var key = await window.crypto.subtle.importKey('raw', keybytes, { name: 'AES-CBC', length: 256 }, false, ['decrypt']) 556 | .catch(function (err) { 557 | console.error(err); 558 | body.classList.remove("loading"); 559 | }); 560 | console.log('key imported'); 561 | 562 | var plaintextbytes = await window.crypto.subtle.decrypt({ name: "AES-CBC", iv: ivbytes }, key, cipherbytes) 563 | .catch(function (err) { 564 | // console.error(err); 565 | body.classList.remove("loading"); 566 | }); 567 | 568 | if (!plaintextbytes) { 569 | spnDecstatus.classList.remove("greenspan"); 570 | spnDecstatus.classList.add("redspan"); 571 | spnDecstatus.innerHTML = 'Error decrypting file. Password may be incorrect.
'; 572 | return; 573 | } 574 | 575 | console.log('ciphertext decrypted'); 576 | plaintextbytes = new Uint8Array(plaintextbytes); 577 | divDecryptImage.style.display = "none"; 578 | divDecryptmessage.style.display = "none"; 579 | 580 | var blob = new Blob([plaintextbytes], { type: 'application/download' }); 581 | var blobUrl = URL.createObjectURL(blob); 582 | aDecsavefile.href = blobUrl; 583 | aDecsavefile.download = originalfilename; 584 | spnDecstatus.classList.remove("redspan"); 585 | spnDecstatus.classList.add("greenspan"); 586 | spnDecstatus.innerHTML = 'File decrypted.
'; 587 | divDecsavefile.hidden = false; 588 | modalstatus.innerText="Checking SHA1 hash of the file with Virustotal"; 589 | filehash= await sha1(plaintextbytes); 590 | await checkforvirus(filehash); 591 | // If this is a message send in browser, show it. 592 | body.classList.remove("loading"); 593 | divDecryptInfo.style.display = "none"; 594 | divDecryptResult.style.display = "" 595 | bDeleteFile.onclick = function(){deletefile(objkey)}; 596 | if (filename.endsWith("txt")) 597 | { 598 | textareaDecryptmessage.value = new TextDecoder("utf-8").decode(plaintextbytes); 599 | bCopyText.hidden = false; 600 | divDecryptmessage.style.display = ""; 601 | } 602 | 603 | fileextension = originalfilename.substr(-4) 604 | 605 | switch (fileextension) { 606 | case ".png": 607 | updateimgtag("png",plaintextbytes); 608 | break; 609 | case ".jpg": 610 | updateimgtag("jpg",plaintextbytes); 611 | break; 612 | case "jpeg": 613 | updateimgtag("jpeg",plaintextbytes); 614 | break; 615 | case ".gif": 616 | updateimgtag("gif",plaintextbytes); 617 | break; 618 | 619 | } 620 | if (deleteondownload){ 621 | deletefile(objkey); 622 | } 623 | } 624 | 625 | 626 | 627 | -------------------------------------------------------------------------------- /frontend/tunnel/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |