La seguridad y la privacidad son temas candentes en la actualidad. La aplicación que vamos a desarrollar hoy es un experimento que te permitirá seleccionar archivos de tu ordenador y cifrarlos del lado del cliente mediante un código de acceso. No necesitaremos nada de código del lado del servidor, ya que no transferiremos ninguna información entre el cliente y el servidor. Para que esto sea posible vamos a utilizar la API de HTML5 FileReader, y una librería de Javascipt para la encriptación de ficheros llamada CryptoJS.
Ten en cuenta que la aplicación no cifra el archivo real, sino una copia del mismo, por lo que no perderás el original. Pero antes de empezar, tenemos que hablar sobre ciertos problemas y limitaciones:
Problemas y limitaciones
Límite de 1MB
Nuestra aplicación no permitirá encriptar archivos de más de 1MB. He establecido dicho límite ya que el atributo download de HTML5, que utilizo para servir el archivo encriptado para su descarga, no funciona bien con grandes cantidades de datos. Si obviásemos esta limitación, la pestaña del navegador se bloquearía y a la larga se cerraría con el típico mensaje del error inesperado. Resumiendo, este problema no está derivado de la acción de cifrado sino con la de descarga del fichero.
¿Qué pasa con el HTTPS?
Cuando hablamos de encriptado de datos y seguridad, los usuarios esperan que la web se cargue con el protocolo HTTPS. Para nuestro caso creo que no es necesario, ya que, aparte de la descarga inicial del HTML y los assets, todo se hace del lado del cliente con Javascript. Si no me crees, descargarte los archivos en los que hayas pegado los códigos de después y ábrelo con tu ordenador.
¿Es seguro?
La librería que utilizaremos, la CryptoJS, es open source, por lo que creo que es de confianza. Yo uso el algoritmo AES, que es conocido por su seguridad. Para mejores resultados, usa un código de acceso largo que sea difícil de averiguar.
EL HTML
La plantilla de la app consiste en un documento HTML normal con unos cuantos divs que separan la app en pantallas individuales. Podrás ver cómo interactúa todo en las posteriores secciones de Javascript y CSS del 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>
Solo uno de estos divs está visible todo el tiempo. Dependiendo de la selección del usuario, encriptar o desencriptar, se establecerá una nueva nombre de clase en el elemento body. Mediante CSS, esta nombre de clase esconderá a los elementos con las clases if-encrypt o if-decrypt. Esta simple acción nos permitirá implementar JavaScript de manera mucho más limpia.
El Javascript
Como he mencionado en la introducción, vamos a utilizar la API FileReader y la librería CryptoJS juntas. El objeto FileReader nos permite leer el contenido de archivos locales utilizando Javascript, pero solo aquellos archivos que el usuario haya seleccionado explícitamente a través de la ventana de dialogo del navegador para ello. Puedes ver cómo se hace esto en el código de más abajo. Aclarar que la mayoría del código gestiona las diferentes transiciones entre las pantallas de la app. Si solo estás interesado en la lectura del fichero, ve a la línea 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)+'%'); } });
Obtengo el contenido de dichos ficheros como un string con la URL. Los navegadores te permiten utilizar estas URLs donde se utilizan las URLs normales. La ventaja de todo esto es que permite almacenar el contenido del recurso directamente en una URL, por lo que, por ejemplos, podemos colocar el contenido del fichero en un href de un enlace, y añadirle el atributo download para forzar la descarga del fichero cuando se haga clic.
Utilizo el algoritmo AES para cifrar la URL con el código de acceso elegido, y ponerla en un enlace de descarga. Lo mismo pasa cuando hay que desencriptarlo. No necesitas ningún código del lado del servidor, es más, como te he dicho antes, puedes probar estos códigos en tu mismo ordenador.
El CSS
Vamos a ver las partes más interesantes del CSS. Lo primero que vas a ver son los estilos que crean el diseño y su capacidad para hacer scroll sin problemas entre las pantallas cambiando la propiedad top del elemento #stage.
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%; }
Debido a que los divs tienen un ancho y un alto del 100%, automáticamente cogerán las dimensiones de la ventana del navegador sin tener que cambiar de tamaño. Otra cosa interesante del CSS, son las clases condicionales, que simplifican en gran medida nuestro Javascript:
[class*="if-"]{ display:none; } body.encrypt .if-encrypt{ display:block; } body.decrypt .if-decrypt{ display:block; }
Fuente: tutorialzine.com