Creating a File Encryption App with JavaScript

Creating a File Encryption App with JavaScript

Security and privacy are hot topics at the moment. This is an opportunity for us to take an introspective look into the way we approach security. It is all a matter of compromise – convenience versus total lock down. Today’s tutorial is an attempt to mix a bit of both.

The app we are going to build today is an experiment that will allow people to choose files from their computers and encrypt them client-side with a pass phrase. No server-side code will be necessary, and no information will be transferred between client and server. To make this possible we will use the HTML5 FileReader API, and a JavaScript encryption library - CryptoJS.

Note that the app doesn’t encrypt the actual file, but a copy of it, so you won’t lose the original. But before we start, here are a few issues and limitations:

Issues and limitations

The 1MB limit

If you play with the demo, you will notice that it doesn’t allow you to encrypt files larger than 1mb. I placed the limit, because the HTML5 download attribute, which I use to offer the encrypted file for download, doesn’t play well with large amounts of data. Otherwise it would cause the tab to crash in Chrome, and the entire browser to crash when using Firefox. The way around this would be to use the File System API and to write the actual binary data there, but it is supported only in Chrome for now. This is not an issue with the encryption speed (which is quite fast), but with offering the file for download.

What about HTTPS?

When it comes to encrypting data and securing information, people naturally expect the page to be loaded through HTTPS. In this case I believe it is not necessary, as apart from the initial download of the HTML and assets, no data is transferred between you and the server – everything is done client-side with JavaScript.  If this bothers you, you can just download the demo and open it directly from your computer.

How secure is it?

The library that I use - CryptoJS - is open source, so I believe it to be trustworthy. I use the AES algorithm from the collection, which is known to be secure. For best results, use a long pass phrase that is difficult to guess.

JavaScript File Encryption App

JavaScript File Encryption App

The HTML

The markup of the app consists of a regular HTML5 document and a few divs that separate the app into several individual screens. You will see how these interact in the JavaScript and CSS sections of the tutorial.

index.html

<!DOCTYPE html>
<html>

	<head>
		<meta charset="utf-8"/>
		<title>JavaScript File Encryption App</title>
		<meta name="viewport" content="width=device-width, initial-scale=1" />
		<link href="http://fonts.googleapis.com/css?family=Raleway:400,700" rel="stylesheet" />
		<link href="assets/css/style.css" rel="stylesheet" />
	</head>

	<body>

		<a class="back"></a>

		<div id="stage">

			<div id="step1">
				<div class="content">
					<h1>What do you want to do?</h1>
					<a class="button encrypt green">Encrypt a file</a>
					<a class="button decrypt magenta">Decrypt a file</a>
				</div>
			</div>

			<div id="step2">

				<div class="content if-encrypt">
					<h1>Choose which file to encrypt</h1>
					<h2>An encrypted copy of the file will be generated. No data is sent to our server.</h2>
					<a class="button browse blue">Browse</a>

					<input type="file" id="encrypt-input" />
				</div>

				<div class="content if-decrypt">
					<h1>Choose which file to decrypt</h1>
					<h2>Only files encrypted by this tool are accepted.</h2>
					<a class="button browse blue">Browse</a>

					<input type="file" id="decrypt-input" />
				</div>

			</div>

			<div id="step3">

				<div class="content if-encrypt">
					<h1>Enter a pass phrase</h1>
					<h2>This phrase will be used as an encryption key. Write it down or remember it; you won't be able to restore the file without it. </h2>

					<input type="password" />
					<a class="button process red">Encrypt!</a>
				</div>

				<div class="content if-decrypt">
					<h1>Enter the pass phrase</h1>
					<h2>Enter the pass phrase that was used to encrypt this file. It is not possible to decrypt it without it.</h2>

					<input type="password" />
					<a class="button process red">Decrypt!</a>
				</div>

			</div>

			<div id="step4">

				<div class="content">
					<h1>Your file is ready!</h1>
					<a class="button download green">Download</a>
				</div>

			</div>
		</div>

	</body>

	<script src="assets/js/aes.js"></script>
	<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
	<script src="assets/js/script.js"></script>

</html>

Only one of the step divs is visible at a time. Depending on the choice of the user – to encrypt or decrypt – a class name is set on the body element. With CSS, this class name hides the elements with either the if-encrypt or if-decrypt classes. This simple gating allows us to write cleaner JavaScript that is minimally involved with the UI.

Choose File To Encrypt

Choose File To Encrypt

The JavaScript Code

As I mentioned in the intro, we are going to use the HTML5 FileReader API (support) and the CryptoJS library together. The FileReader object lets us read the contents of local files using JavaScript, but only of files that have been selected explicitly by the user through the file input’s browse dialog. You can see how this is done in the code below. Notice that most of the code handles the transitions between the different screens of the app, and the actual reading of the file happens from line 85.

assets/js/script.js

$(function(){

	var body = $('body'),
		stage = $('#stage'),
		back = $('a.back');

	/* 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];

		if(file.size > 1024*1024){
			alert('Please choose files smaller than 1mb, otherwise you may crash your browser. \nThis is a known issue. See the tutorial.');
			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('');

		if(password.length<5){
			alert('Please choose a longer password!');
			return;
		}

		// 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 = function(e){

				// Use the CryptoJS library and the AES cypher to encrypt the 
				// contents of the file, held in e.target.result, with the password

				var encrypted = CryptoJS.AES.encrypt(e.target.result, password);

				// 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('href', 'data:application/octet-stream,' + encrypted);
				a.attr('download', file.name + '.encrypted');

				step(4);
			};

			// 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 = function(e){

				var decrypted = CryptoJS.AES.decrypt(e.target.result, password)
										.toString(CryptoJS.enc.Latin1);

				if(!/^data:/.test(decrypted)){
					alert("Invalid pass phrase or file! Please try again.");
					return false;
				}

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

				step(4);
			};

			reader.readAsText(file);
		}
	});

	/* The back button */

	back.click(function(){

		// Reinitialize the hidden file inputs,
		// so that they don't hold the selection 
		// from last time

		$('#step2 input[type=file]').replaceWith(function(){
			return $(this).clone();
		});

		step(1);
	});

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

	function step(i){

		if(i == 1){
			back.fadeOut();
		}
		else{
			back.fadeIn();
		}

		// 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)+'%');
	}

});

I obtain the contents of the files as a data uri string (support). Browsers allow you to use these URIs everywhere a regular URL would go. The benefit is that they let you store the content of the resource directly in the URI, so we can, for example, place the contents of the file as the href of a link, and add the download attribute (read more) to it, to force it to download as a file when clicked.

I use the AES algorithm to encrypt the data uri with the chosen password, and to offer it as a download. The reverse happens when decrypting it. No data ever reaches the server. You don’t even need a server for that matter, you can open the HTML directly from a folder on your computer, and use it as is.

Enter a Pass Phrase

Enter a Pass Phrase

The CSS

I will present only the more interesting parts of the CSS here, you can see the rest in the stylesheet from the downloadable zip. The first thing to present, are the styles that create the layout and its ability to scroll smoothly between screens by changing the top property of the #stage element.

assets/css/styles.css

body{
	font:15px/1.3 'Raleway', sans-serif;
	color: #fff;
	width:100%;
	height:100%;
	position:absolute;
	overflow:hidden;
}

#stage{
	width:100%;
	height:100%;
	position:absolute;
	top:0;
	left:0;

	transition:top 0.4s;
}

#stage > div{  /* The step divs */
	height:100%;
	position:relative;
}

#stage h1{
	font-weight:normal;
	font-size:48px;
	text-align:center;
	color:#fff;
	margin-bottom:60px;
}

#stage h2{
	font-weight: normal;
	font-size: 14px;
	font-family: Arial, Helvetica, sans-serif;
	margin: -40px 0 45px;
	font-style: italic;
}

.content{
	position:absolute;
	text-align:center;
	left:0;
	top:50%;
	width:100%;
}

Because the step divs are set to a 100% width and height, they automatically take the full dimensions of the browser window without having to be resized.

Another interesting piece of code, are the conditional classes that greatly simplify our JavaScript:

[class*="if-"]{
	display:none;
}

body.encrypt .if-encrypt{
	display:block;
}

body.decrypt .if-decrypt{
	display:block;
}

This way, the encrypt and decrypt classes of the body control the visibility of the elements that have the respective if-* class.

We’re done!

With this our JavaScript encryption app is ready! You can use it to share pictures and documents with friends by sending them the version encrypted with a pre-agreed pass phrase. Or you can put the HTML of the app on a flash drive, along with your encrypted files, and open the index.html directly to decrypt them.

Join our newsletter and get our PSDs!18,153 people learn about HTML5, JS and more. Join them!

by Martin Angelov

Martin is a web developer with an eye for design from Bulgaria. He founded Tutorialzine in 2009 and it still is his favorite side project.

13 Comments

  1. Krypto says:

    Awesome stuff Martin! :)

  2. Dawid says:

    You can go without any limits on the file size if you read the file with arraybuffer instead of dataurl.
    Check out my example:
    http://haanto.com/client-side-file-encryption/

    (i've used GibberishAES instead of cryptojs only because it puts a newline every 65 chars so the file can also be decrypted with openssl)

    1. Martin Angelov says:

      Great experiment! The problem is not the data-url (it was quite fast), but the enormous URI that crashes the browser when hovering over the download link. I think that using the Chrome FileSystem API will solve the problem, but it will unfortunately not work anywhere else.

  3. Fantastic Martin... useful to prevent unauthorized access :)

    THanks a lot

  4. Henry says:

    Could you make the file have another extension - ie make it no longer .encrypt make it .henry

    1. Martin Angelov says:

      Yes, you only have to replace the relevant places of the code that hold the '.encrypt' string with the one you want. You can also omit the extension entirely, but I think it is easier for people to tell the two files apart.

  5. Kārlis K. says:

    This is absolutely fantastic! Thanks for this awesome tutorial, think you could do something on caching? It's a must in all things web today, but it's a bit tricky - you have to do it just right to actually have a positive effect. I think it could be interesting read knowing how well you write your tutorials.

  6. NRSP says:

    Fantastic!

    What level of encryption is used by the app by default?

    The Crypto-js page implies 256bit AES, if a passphrase is used - is that correct?

    N

    1. Martin Angelov says:

      Yes, I believe that this is correct. I haven't read the source code of the library though, so you might want to check for yourself to be certain.

  7. Destin Hubble says:

    This is awesome! This is exactly what I've been looking for! And such a beautiful design:)

  8. Ali says:

    Good Job! I really like it. How can someone save/sent encrypted file to a remote server and then on demand he/she can download from remote server to decrypt the file. Ali

  9. Regi says:

    I loved the beautiful design:)
    I'm downloading the demo just to see how the design is architected.

Add Comment

Add a Reply

HTML is escaped automatically. Surround code blocks with <pre></pre> for readability.
Perks:   **bold**   __italics__   [some text](http://example.com) for links