Gracias, Wikipedia.
Proyectos::Encriptación con JavaScript
Permalink: http://www.treeweb.es/u/1126/ 18/06/2011

Encriptación con JavaScript

Esta nota viene a raíz de un artículo leído en genbeta.com: http://www.genbeta.com/seguridad/encipherit-oculta-cualquier-texto-y-anade-una-capa-extra-de-seguridad-usando-tecnicas-criptograficas#c348299

En él se referencia a https://encipher.it/ donde describe cómo encriptar un texto con el navegador mediante javascript, y una sencilla forma de instalarlo añadiendo un marcador (o favorito) con código javascript en el href.

El único problema que le veo es que cada vez que encriptamos algo, el minijavascript del enlace descarga un archivo de https://encipher.it/ que es el que realmente tiene los algoritmos. Si los de https://encipher.it/ decidieran algún día leer nuestros mensajes encriptados, sólo tendrían que cambiar el javascript que se descarga y listo. Concretamente el archivo es https://encipher.it/javascripts/inject.v2.js .

En la web oficial de https://encipher.it/ nos proponen añadir a nuestros marcadores un enlace con el siguiente código:
javascript:(function(){document.body.appendChild(document.createElement('script')).src='https://encipher.it/javascripts/inject.v2.js';})();
Lo que hace exactamente es escribir en el html de la página cargada una etiqueta 'script' para que se descargue el javascript con el algoritmo.

El contenido del archivo lo vemos a continuación.


https://encipher.it/javascripts/inject.v2.js:
(function() { var BASE_URL, CRYPTO_FOOTER, CRYPTO_HEADER, HELP, Popup, main, show; var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, __indexOf = Array.prototype.indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (this[i] === item) return i; } return -1; }; BASE_URL = "https://encipher.it"; HELP = "This message is encrypted. Visit " + BASE_URL + " to learn how to deal with it.nn"; CRYPTO_HEADER = "EnCt2"; CRYPTO_FOOTER = "IwEmS"; Popup = (function() { function Popup() { var body; this.cache = {}; if (this.parse()) { body = "<input type='text' style='position: absolute; display: block; top: 4px; left: 4px; right: 4px; bottom: 32px; width: 97%;' id='crypt-key'/>"; if (this.encrypted) { this.show("Enter decryption key", "Decrypt", body); } else { if (this.text) { this.show("Enter encryption key", "Encrypt", body); } else { this.show("Message is empty", "Cancel", "Please type the message first"); } } } else { this.show("Message not found", "Cancel", "Please select the input area"); } jQuery('#crypt-key').focus().keyup(__bind(function(e) { var enabled; enabled = this.password() !== ''; jQuery('#crypt-btn').attr('disabled', !enabled); if (e.which === 27) { return this.hide(); } if (e.which === 13 && enabled) { return this.run(); } }, this)); } Popup.prototype.show = function(title, action, body) { this.frame = jQuery(" <div style='position: fixed; z-index: 9999; background: #355664; border: solid gray 1px; -moz-border-radius: 10px; -webkit-border-radius: 10px; border-radius: 10px'> <div style='position: absolute; left: 0; right: 0; color: white; margin: 4px; height: 32px;'> <b style='padding: 8px; float: left;'>" + title + "</b> <img style='border: none; float: right;' id='crypt-close' src='" + BASE_URL + "/close.png'/> </div> <div style='position: absolute; bottom: 0; top: 32px; margin: 4px; padding: 10px; left: 0; right: 0;'> " + body + " <b style='position: absolute; display: block; left: 4px; bottom: 4px;' id='crypt-message''></b> <input disabled='true' style='position: absolute; display: block; right: 4px; bottom: 4px;' id='crypt-btn' type='button' value='" + action + "'/> </div> </div> "); jQuery('body').append(this.frame); if (action === "Cancel") { jQuery('#crypt-btn').attr('disabled', false).click(__bind(function() { return this.hide(); }, this)).keyup(__bind(function(e) { if (e.which === 27) { return this.hide(); } }, this)).focus(); } else { jQuery('#crypt-btn').click(__bind(function() { return this.run(); }, this)); } jQuery(window).resize(__bind(function() { return this.layout(); }, this)); jQuery('#crypt-close').click(__bind(function() { return this.hide(); }, this)); return this.layout(); }; Popup.prototype.alert = function(msg) { return jQuery('#crypt-message').html(msg); }; Popup.prototype.hide = function() { this.frame.remove(); return window.CRYPT_GUI = void 0; }; Popup.prototype.layout = function() { var height, width, _ref; _ref = [400, 105], width = _ref[0], height = _ref[1]; return this.frame.css({ 'top': (jQuery(window).height() - height) / 2 + 'px', 'left': (jQuery(window).width() - width) / 2 + 'px', 'width': width + 'px', 'height': height + 'px' }); }; Popup.prototype.password = function() { return jQuery('#crypt-key').attr('value'); }; Popup.prototype.run = function() { var callback; callback = __bind(function(res) { if (res) { return this.hide(); } else { return this.alert("Invalid password"); } }, this); if (this.encrypted) { return this.decrypt(this.password(), callback); } else { return this.encrypt(this.password(), callback); } }; Popup.prototype.derive = function(password, salt, callback) { var cacheKey, pbkdf2; cacheKey = password + salt; if (this.cache[cacheKey]) { return callback(this.cache[cacheKey]); } pbkdf2 = new PBKDF2(password, salt, 1000, 32); return pbkdf2.deriveKey(__bind(function(per) { return this.alert("Generating key: " + (Math.floor(per)) + "%"); }, this), __bind(function(key) { this.cache[cacheKey] = key; return callback(key); }, this)); }; Popup.prototype.decryptNode = function(node, text, password, callback) { var hash, salt; hash = text.slice(0, 64); salt = text.slice(64, 72); text = text.slice(72); return this.derive(password, salt, __bind(function(key) { var newHash; text = Aes.Ctr.decrypt(text, key, 256); newHash = Sha256.hash(text); if (hash === newHash) { this.updateNode(node, text); return callback(true); } else { return callback(false); } }, this)); }; Popup.prototype.decrypt = function(password, callback) { var i, next, success; i = 0; success = false; next = __bind(function() { if (this.nodes.length > i) { return this.decryptNode(this.nodes[i], this.texts[i], password, __bind(function(res) { i += 1; success || (success = res); return next(); }, this)); } else { return callback(success); } }, this); return next(); }; Popup.prototype.encrypt = function(password, callback) { var hash, salt; hash = Sha256.hash(this.text); salt = Base64.random(8); return this.derive(password, salt, __bind(function(key) { this.updateNode(this.node, HELP + this.dump(hash + salt + Aes.Ctr.encrypt(this.text, key, 256))); return callback(true); }, this)); }; Popup.prototype.dump = function(text) { var i, out, _ref; text = CRYPTO_HEADER + text + CRYPTO_FOOTER; i = 0; out = ""; for (i = 0, _ref = text.length; 0 <= _ref ? i < _ref : i > _ref; 0 <= _ref ? i++ : i--) { out += text.charAt(i); if ((i % 80) === 79) { out += 'n'; } } return out; }; Popup.prototype.updateNode = function(node, value) { if (node.is('textarea')) { return node.val(value); } else { return node.html(value.replace(/n/g, '<br/>')); } }; Popup.prototype.findEncrypted = function() { var found, nodes, texts, traverse, traverseBody, _ref; _ref = [[], []], nodes = _ref[0], texts = _ref[1]; found = function(elem, txt) { var ftr, hdr; txt = txt.replace(/[n> ]/g, ''); hdr = txt.indexOf(CRYPTO_HEADER); ftr = txt.indexOf(CRYPTO_FOOTER); if (hdr >= 0 && ftr >= 0) { txt = txt.slice(hdr + CRYPTO_HEADER.length, ftr); nodes.push(elem); texts.push(txt); return 1; } return 0; }; traverse = function(node) { var elem, i, skip, _ref2; skip = 0; if (node.nodeType === 3 && node.data.indexOf(CRYPTO_HEADER) >= 0) { elem = jQuery(node.parentNode); skip = found(elem, elem.text()); } else { if (node.nodeType === 1 && !/(script|style)/i.test(node.tagName)) { if (/(input|textarea)/i.test(node.tagName)) { elem = jQuery(node); found(elem, elem.val()); } else { if (node.childNodes) { for (i = 0, _ref2 = node.childNodes.length; 0 <= _ref2 ? i < _ref2 : i > _ref2; 0 <= _ref2 ? i++ : i--) { i += traverse(node.childNodes[i]); } } } } } return skip; }; traverseBody = function(body) { body.each(function() { return traverse(this); }); return body.find("iframe").each(function() { try { return traverseBody(jQuery(this).contents().find('body')); } catch (e) { } }); }; traverseBody(jQuery('body')); return [nodes, texts]; }; Popup.prototype.findInput = function() { var node; node = jQuery('#canvas_frame').contents().find('textarea[name=body]:visible'); if (node.length) { return [node, node.val()]; } node = jQuery('#canvas_frame').contents().find('iframe.editable').contents().find('body'); if (node.length) { return [node, node.html()]; } if (jQuery('#canvas_frame').length) { return [void 0, void 0]; } node = jQuery('textarea[name=txtbdy]'); if (node.length === 1) { return [node, node.val()]; } node = jQuery('textarea'); if (node.length === 1) { return [node, node.val()]; } if (node.length > 1) { node = jQuery('textarea:focus'); } if (node.length === 1) { return [node, node.val()]; } return [void 0, void 0]; }; Popup.prototype.parse = function() { var _ref, _ref2; _ref = this.findEncrypted(), this.nodes = _ref[0], this.texts = _ref[1]; _ref2 = this.findInput(), this.node = _ref2[0], this.text = _ref2[1]; this.encrypted = this.nodes.length > 0; return this.encrypted || this.node !== void 0; }; return Popup; })(); show = function() { if (window.CRYPT_GUI) { return window.CRYPT_GUI.hide(); } else { return window.CRYPT_GUI = new Popup(); } }; main = function() { var count, ready, script, script_tag, scripts, _i, _len, _results; if (window.CRYPT_LOADED) { return show(); } else { scripts = ['AES.js', 'sha1.js', 'pbkdf2.js', 'base64.js', 'utf8.js']; if (typeof jQuery === "undefined") { scripts.push('jquery.min.js'); } count = scripts.length; ready = function() { count -= 1; if (count === 0) { window.CRYPT_LOADED = true; if (__indexOf.call(scripts, 'jquery.min.js') >= 0) { $.noConflict(); } jQuery.expr[':'].focus = function(elem) { return elem === document.activeElement && (elem.type || elem.href); }; return show(); } }; _results = []; for (_i = 0, _len = scripts.length; _i < _len; _i++) { script = scripts[_i]; script_tag = document.createElement('script'); script_tag.setAttribute("type", "text/javascript"); script_tag.setAttribute("src", BASE_URL + "/javascripts/" + script); script_tag.onload = ready; script_tag.onreadystatechange = function() { if (this.readyState === 'complete' || this.readyState === 'loaded') { return ready(); } }; _results.push(document.getElementsByTagName("head")[0].appendChild(script_tag)); } return _results; } }; main(); }).call(this);
Usándolo de forma normal sin analizar el código y viendo el tráfico con HttpFox se puede observar que la única petición es la del archivo js, por lo que nuestros datos parecen estar seguros.

Vamos a probar a meter el propio fichero inject.v2.js dentro de un enlace, para ello limpio un poco el código original quitando saltos de línea, espacios duplicados, elimino comillas dobles y escapo las comillas simples del interior de las subcadenas envueltas en comillas dobles:




Y hacemos un enlace con todo ese código que será el que guardaremos en nuestros marcadores:




Arrastrando este enlace a marcadores o haciendo clic en él, ya podemos encriptar o desencriptar cosas. Voy a meter un textarea para hacer la prueba:




Todavía faltan algunos detalles que se pueden arreglar, por ejemplo, uno de los más llamativos es que el cuadro de texto en el que se introduce la clave no oculta las letras con asterisco. Así que vamos a intentar mejorarlo:



Todavía se pueden mejorar algunas cosillas, hacer el código más ligero y corregir el algoritmo que busca los cuadros de texto, ya que si hay muchos o algunos están ocultos, se hace un lío.