assets/js/script.js

Last modified Mon Aug 9 11:20:20 UTC 2021



// script.js
// original by Mark Angelov
// modified by Petri Kutvonen

$(function(){

	async function wrapper() {

	/* Are we about where we should be? */

	let myUrl = window.location.href;
	if (myUrl.match(/^https:\/\/crypt-app\.net/) != "https://crypt-app.net"){
		window.location.href = "https://crypt-app.net/";
	}

	/* Preparations */

	var body = $('body'),
		stage = $('#stage'),
		back = $('a.back');
		priv = $('a.privacy');
		disc = $('a.disclaimer');
		info = $('a.info');
		crypt = $('p.cryptapp');
		content = $('div.content');

	var appHeader = (window.location.href).
		replace(/https\x3a\x2f\x2f/,'').
		replace(/\x3f.*$/, '').
		replace(/\x2f$/, '').
		replace(/\x2findex.html/, '');	

	document.getElementById("cryptapp").innerHTML = appHeader;

	var testMode = appHeader.match(/\x2ftest/);

	if (iPad && safari){ 
		makeRoomForKeyboard(); // for iPad virtual keyboard
	}

	/* Step 1 */

	$('#step1 .encrypt').click(function(){
		body.attr('class', 'encrypt');

		// Go to step 2
		step(2);
	});

	$('#step1 .decrypt').click(function(){
		body.attr('class', 'decrypt');
		step(2);
	});

	/* Step 2 */

	$('#step2 .button').click(function(){

		// Trigger the file browser dialog
		$(this).parent().find('input').click();
	});

	// Set up events for the file inputs

	var file = null;

	$('#step2').on('change', '#encrypt-input', function(e){

		// Has a file been selected?

		if(e.target.files.length!=1){
			alert('Please select a file to encrypt!');
			return false;
		}

		file = e.target.files[0];

		var sizeLimit;

		// Limit file size, performance.memory is Chromium specific, and 42 is an arbitrary constant 

		if (!!performance.memory){
			sizeLimit = Math.floor((performance.memory.jsHeapSizeLimit-performance.memory.totalJSHeapSize)/1024/1024/42);
		}else{
			if(window.navigator.userAgent.match(/Linux/i) || window.navigator.userAgent.match(/Windows/i) ||
			   window.navigator.userAgent.match(/OS X/i)){
				sizeLimit = 20;
			}else{
				sizeLimit = 10;
			}
		}
		if (testMode) console.log('sizeLimit: '+sizeLimit+' Mbytes');

		if(file.size > sizeLimit*1024*1024){
			alert('Please choose files smaller than '+sizeLimit+' Mbytes. \nThis is a limitation caused by your browser or system.');
			return;
		}

		step(3);
	});

	$('#step2').on('change', '#decrypt-input', function(e){

		if(e.target.files.length!=1){
			alert('Please select a file to decrypt!');
			return false;
		}

		file = e.target.files[0];

		step(3);
	});

	/* Step 3 */

	$('a.button.process').click(function(){

		var input = $(this).parent().find('input[type=password]'),
			a = $('#step4 a.download');
		
		password = input.val();

		input.val('');

		// Passphrase must be at least 16 characters long - however, allow a fixed shorter test passphrase

		if(password.length<16 &&
				Array.from(password).reduce((hash, char) => 0 | (31*hash+char.charCodeAt(0)),0) != 114548376){
			if (body.hasClass('encrypt')){
				alert('Please choose a longer passphrase!');
			}else{
				alert('Passphrase is too short!');
			}
			return false;
		}

		content.css('top', '37.5%'); // Restore display layout (if changed)

		imBusy(true);

		// The HTML5 FileReader object will allow us to read the 
		// contents of the selected file.

		var reader = new FileReader();

		if(body.hasClass('encrypt')){

			// Encrypt the file!

			reader.onload = async function(e){

				// Use the Web Cryptography API through the aes4js wrapper to
				// encrypt the contents of the file, held in e.target.result, 
				// using AES-256-GCM with the password. The aes4js wrapper
				// includes key derivation with 1000000 passes through PBKDF2  
				// with SHA256.

				var encrypted = await aes4js.encrypt(password, e.target.result);
				password = null;

				// The download attribute will cause the contents of the href
				// attribute to be downloaded when clicked. The download attribute
				// also holds the name of the file that is offered for download.

				a.attr('download', file.name + '.encrypted');
				a.attr('href', 'data:application/octet-stream,' + JSON.stringify(encrypted));

				imBusy(false);

				if(testMode) console.timeEnd("encryption");

				step(4);
			};

			if (testMode) console.time("encryption");

			// This will encode the contents of the file into a data-uri.
			// It will trigger the onload handler above, with the result

			reader.readAsDataURL(file);
		}
		else {

			// Decrypt it!

			reader.onload = async function(e){

				var ok = true;
				var decrypted;

				if (e.target.result.slice(0, 19) != '{"encrypted":"data:') { // obviously invalid file
					ok = false;
				} else {
					decrypted = await aes4js.decrypt(password, e.target.result).then({}, x => { ok = false; });
				}
				password = null;

				imBusy(false);

				if (! ok) {
					if (testMode) console.timeEnd("decryption");

					alert("Invalid passphrase or file! Please try again.");

					return false;
				}

				if(testMode) console.timeEnd("decryption");

				// For plain text files we display a preview window

				if (decrypted.slice(0, 10) == "data:text/") { 

					var previewHTML = 
						'<!DOCTYPE html><html><head>\
							<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />\
							<meta name="viewport" content="width=device-width, initial-scale=1" />\
							<style>body {font-family: Roboto, "Proxima Nova", "Segoe UI", "Nimbus Sans L", Ubuntu, Arial, sans-serif; background-color: #fff; color: #000; width: 94%; margin: 0 3% 0 3%; } h2 { font-weight: normal; } p { font-size: 18px; } pre { font-size: 14px; } #previewText { display: none; }</style>\
							<title>Safe Crypt App | Preview</title>\
						</head>\
						<body>\
							<h2>Preview of &ldquo;' + file.name.replace('.encrypted','') + '&rdquo; is ready!</h2>\
							<button id="previewButton">Show preview</button> &nbsp; \
							<button id="closeButton">Close window</button>\
							<p>Close this window to see the <strong>SAVE FILE</strong> dialog.<br />This window will be closed automatically <strong>after 2 minutes</strong>.</p>\
							<div id="previewText"><hr /><pre>' +
							      atob(decrypted.slice(decrypted.indexOf(",")+1)).replace(/&/g,"&amp;").replace(/</g,"<&zwj;") + 
							'</pre><hr /></div>\
							<script src="assets/js/preview.js"></script>\
						</body>\
						</html>';

					var newHTML;

					// Decode two byte UTF-8 coded Unicode charaters if there are any - else display as is

					try {					
						newHTML = decodeURIComponent(escape(previewHTML));
					} catch(e) {
						newHTML = previewHTML;
					}

					previewHTML = null;

					var w = window.open("","_blank","resizable=yes,scrollbars=yes,height=600,width=600");

					// Not all browsers or browser settings support this - be prepared to fail

					if(w == null) {alert('Your browser prevented Safe Crypt App opening the text file preview. ' + 
						      	     'Please unblock Pop-ups in the settings. '+ 
						      	     'If you see just a blank page, your browser prevents the loading ' +
						      	     'of the text content.');
					} else {
						w.document.open();
						w.document.write(newHTML);
						w.document.close();

						// For security reasons close the preview after 2 minutes

						setTimeout( function() { w.close(); }, 120000);
					}
					
					newHTML = null;
				}

				// Normal download

				a.attr('download', file.name.replace('.encrypted',''));
				a.attr('href', decrypted);

				imBusy(false);

				step(4);
			};

			reader.oner&zwsp;ror = function(){
				alert("Er&zwsp;ror while processing file: " + reader.er&zwsp;ror.message);
			};

			if (testMode) console.time("decryption");

			// This will download the decrypted file

			reader.readAsText(file);

		}
	});

	/* The back button */

	back.click(function(){

		encrypted = null;
		decrypted = null;

		step(1);
	});

	// Helper function that moves the viewport to the correct step div

	function step(i){

		if(i == 1){
			if(safari || brave){
				window.location.reload();
			}
			back.hide();
			priv.show();
			disc.show();
			info.show();
			crypt.show();
		}
		else{
			back.show();
			priv.hide();
			disc.hide();
			info.hide();
			crypt.hide();
		}

		// Move the #stage div. Changing the top property will trigger
		// a css transition on the element. i-1 because we want the
		// steps to start from 1:

		stage.css('top',(-(i-1)*100)+'%');

	}

	// Helper function to show an animated "busy" png image	

	function imBusy(s){

		if(s){
			document.getElementById("busy").style.visibility = "visible";
		}
		else{			
			document.getElementById("busy").style.visibility = "hidden";
		}
	}

	// Helper function to prevent iPad virtual keyboard to mask the Encrypt/Decrypt button
	// An annoyance: there is no way for the browser to detect if an external bluetooth keyboard is connected

	function makeRoomForKeyboard(){

		var encFocus = false;
		var decFocus = false;

		document.querySelector('input[id="i-enc"]').addEventListener('focus', () =>
			{ if (!encFocus){
				encFocus = true;
				if (window.outerWidth > window.outerHeight){
					back.hide();
					content.css('top', '26.5%');
				}
			} });
		document.querySelector('input[id="i-dec"]').addEventListener('focus', () =>
			{ if (!decFocus){
				decFocus = true;
				if (window.outerWidth > window.outerHeight){
					back.hide();
					content.css('top', '26.5%');
				}
			} });
	}

	}	// end of aync wrapper

	wrapper();

});