Creación de una aplicación de cifrado de archivos con JavaScript

La seguridad y la privacidad son temas candentes en este momento. Esta es una oportunidad para que echemos un vistazo introspectivo a la forma en que abordamos la seguridad. Todo es una cuestión de compromiso:conveniencia versus bloqueo total. El tutorial de hoy es un intento de mezclar un poco de ambos.

La aplicación que vamos a construir hoy es un experimento que permitirá a las personas elegir archivos de sus computadoras y cifrarlos en el lado del cliente con una frase de contraseña. No será necesario ningún código del lado del servidor y no se transferirá información entre el cliente y el servidor. Para que esto sea posible, utilizaremos la API FileReader de HTML5 y una biblioteca de cifrado de JavaScript:CryptoJS.

Tenga en cuenta que la aplicación no cifra el archivo real, sino una copia del mismo, por lo que no perderá el original. Pero antes de comenzar, aquí hay algunos problemas y limitaciones:

Problemas y limitaciones

El límite de 1 MB

Si juegas con la demostración, notarás que no te permite encriptar archivos de más de 1 mb. Puse el límite, porque el HTML5 download atributo, que utilizo para ofrecer el archivo cifrado para descargar, no funciona bien con grandes cantidades de datos. De lo contrario, la pestaña fallaría en Chrome y todo el navegador fallaría al usar Firefox. La forma de evitar esto sería usar la API del sistema de archivos y escribir los datos binarios reales allí, pero por ahora solo es compatible con Chrome. Esto no es un problema con la velocidad de encriptación (que es bastante rápida), sino con ofrecer el archivo para descargar.

¿Qué pasa con HTTPS?

Cuando se trata de cifrar datos y proteger la información, la gente espera naturalmente que la página se cargue a través de HTTPS. En este caso, creo que no es necesario, ya que aparte de la descarga inicial del HTML y los activos, no se transfieren datos entre usted y el servidor; todo se hace del lado del cliente con JavaScript. Si esto le molesta, puede descargar la demostración y abrirla directamente desde su computadora.

¿Qué tan seguro es?

La biblioteca que uso, CryptoJS, es de código abierto, por lo que creo que es confiable. Uso el algoritmo AES de la colección, que se sabe que es seguro. Para obtener los mejores resultados, utilice una frase de contraseña larga que sea difícil de adivinar.

El HTML

El marcado de la aplicación consiste en un documento HTML5 regular y algunos divs que separan la aplicación en varias pantallas individuales. Verá cómo interactúan en las 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 los divs de paso es visible a la vez. Dependiendo de la elección del usuario, para cifrar o descifrar, se establece un nombre de clase en el elemento del cuerpo. Con CSS, este nombre de clase oculta los elementos con if-encrypt o if-decrypt clases Esta puerta simple nos permite escribir JavaScript más limpio que está mínimamente involucrado con la interfaz de usuario.

El Código JavaScript

Como mencioné en la introducción, vamos a usar la API FileReader de HTML5 (soporte) y la biblioteca CryptoJS juntas. El objeto FileReader nos permite leer el contenido de los archivos locales usando JavaScript, pero solo de los archivos que el usuario ha seleccionado explícitamente a través del cuadro de diálogo de exploración de la entrada del archivo. Puedes ver cómo se hace esto en el siguiente código. Tenga en cuenta que la mayor parte del código maneja las transiciones entre las diferentes pantallas de la aplicación y la lectura real del archivo ocurre desde la línea 85.

activos/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 los archivos como una cadena de datos uri (soporte). Los navegadores le permiten usar estos URI en todos los lugares donde iría una URL normal. El beneficio es que te permiten almacenar el contenido del recurso directamente en la URI, por lo que podemos, por ejemplo, colocar el contenido del archivo como href de un enlace y agrega el download atributo (leer más) a él, para obligarlo a descargar como un archivo cuando se hace clic.

Utilizo el algoritmo AES para cifrar el uri de datos con la contraseña elegida, y ofrecerlo como descarga. Lo contrario sucede al descifrarlo. Ningún dato llega al servidor. Ni siquiera necesita un servidor para el caso, puede abrir el HTML directamente desde una carpeta en su computadora y usarlo tal como está.

El CSS

Presentaré solo las partes más interesantes del CSS aquí, puedes ver el resto en la hoja de estilo del zip descargable. Lo primero que hay que presentar son los estilos que crean el diseño y su capacidad para desplazarse sin problemas entre pantallas cambiando el top propiedad del #stage elemento.

activos/css/estilos.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 de paso están configurados al 100 % de ancho y alto, automáticamente toman las dimensiones completas de la ventana del navegador sin tener que cambiar el tamaño.

Otra pieza de código interesante son las clases condicionales que simplifican enormemente nuestro JavaScript:

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

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

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

De esta forma, las clases de encriptar y desencriptar del cuerpo controlan la visibilidad de los elementos que tienen el respectivo if-* clase.

¡Hemos terminado!

¡Con esto, nuestra aplicación de cifrado de JavaScript está lista! Puede usarlo para compartir imágenes y documentos con amigos enviándoles la versión encriptada con una frase de contraseña acordada previamente. O puede colocar el HTML de la aplicación en una unidad flash, junto con sus archivos cifrados, y abrir index.html directamente para descifrarlos.