function HermesMsg(baseUrl) { // Estados del servicio this.NoRegistrado = 0; this.Registrando = 1; this.Registrado = 2; this.Respondiendo = 3; // Estados del cliente this.Desocupado = 0; this.EnviandoMensaje = 1; this.EsperandoRespuesta = 2; // Tipos de mensajes this.MensajeCliente = 0; this.MensajeInicioSesion = 1; this.MensajeEstablecerSesion = 2; // Dirección base para los URLs que utiliza esta clase this.baseUrl = baseUrl; // Indica si hay un llamado ajax en curso this.esperandoRespuesta = false; // Identificador asignado a este servicio this.servicioId = null; // Estado del servicio this.Estado = this.NoRegistrado; // Estado del cliente this.EstadoMensaje = this.Desocupado; // Id del ultimo mensaje enviado this.MensajeId = null; // lista de mensajes this.Mensajes = new Array(); // callback que se llama cuando hay un mensaje listo this.MensajeListo = function () { }; // Este evento se llama cuando se detecta que el servicio se detuvo this.ServicioDetenido = function () { }; // Este campo tiene la información de la sesión o null si no hay una sesion segura this.sesion = null; // Objeto que contiene la funcionalidad de encripción this.subtle = null; if (window.msrCrypto) { this.subtle = window.msrCrypto.subtle; } // Especifica el tipo de mensaje que se va a enviar this.TipoMensaje = this.MensajeCliente; // Contiene las llaves RSA del servicio this.privateKey = null; this.publicKeyJwk = null; // Contiene las llaves de encripción de las sesiones this.sesiones = new Array(); // Controla si se aceptan o no mensajes sin encripción this.encryptionRequired = false; } HermesMsg.prototype.RegistrarServicio = function (callback, callbackError) { if (this.Estado != this.NoRegistrado) { safeCallback(callbackError, "No está registrado"); return false; } this.Estado = this.Registrando; var obj = this; this.HacerSolicitud( this.CreateUrl("servicio/CrearServicio"), function (respuesta) { if (respuesta.Resultado) { obj.servicioId = respuesta.Datos; obj.Estado = obj.Registrado; safeCallback(callback); // Iniciar timer para revisar mensajes para el servicio obj.PonerTimeoutRevisionMensajesPendientes(); // Crear las llaves RSA y exportar la llave publica var crypto = window.crypto || window.msCrypto; var subtle = crypto.subtle || crypto.webkitSubtle; var keySetupFailedCallback = function() { obj.privateKey = null; obj.publicKeyJwk = null; } if (crypto != null) { var result = subtle.generateKey( { name: "RSA-OAEP", modulusLength: 2048, //can be 1024, 2048, or 4096 publicExponent: new Uint8Array([0x01, 0x00, 0x01]), hash: { name: "SHA-256" } }, true, //whether the key is extractable (i.e. can be used in exportKey) ["encrypt", "decrypt"] //must contain both "encrypt" and "decrypt" ); CryptoCall( result, function (rsaKeys) { // Exportar la llave publica result = subtle.exportKey( "jwk", rsaKeys.publicKey ); CryptoCall( result, function (publicKeyJwk) { // Convertir publicKeyJwk a string y guardarlo en el campo del mismo nombre. Si es necesario convertirlo a JSON (IE) if (GetType(publicKeyJwk) != "ArrayBuffer") { obj.publicKeyJwk = JSON.stringify(publicKeyJwk); } else { // IE retorna un ArrayBuffer var s = ""; var bytes = new Int8Array(publicKeyJwk); for (var i = 0; i < bytes.length; i++) { if (bytes[i] == 0) //IE agrega un 0 al final { continue; } s += String.fromCharCode(bytes[i]); } obj.publicKeyJwk = s; } // Exportar la llave privada result = subtle.exportKey( "jwk", rsaKeys.privateKey ); CryptoCall( result, function (privateKeyJwk) { // IE retorna un ArrayBuffer if (GetType(privateKeyJwk) != "ArrayBuffer") { privateKeyJwk = JSON.stringify(privateKeyJwk); } result = obj.subtle.importKey( "jwk", toSupportedArray(privateKeyJwk), { name: "RSAES-PKCS1-v1_5" }, true, ["decrypt"] ); CryptoCall( result, function (privateKey) { obj.privateKey = privateKey; }, keySetupFailedCallback ); }, keySetupFailedCallback ); }, keySetupFailedCallback ); }, keySetupFailedCallback ); } } else { obj.servicioId = null; obj.Estado = obj.NoRegistrado; safeCallback(callbackError, respuesta.Error); } }, function (error) { obj.Estado = obj.NoRegistrado; safeCallback(callbackError,error); } ); return true; } HermesMsg.prototype.HacerSolicitud = function (url, callback, errorCallback, mensaje) { this.esperandoRespuesta = true; var obj = this; // Convertir el objeto mensaje en una hilera en formato form-data var postData = ""; if (mensaje != null) { for(var key in mensaje) { postData += key + "=" + encodeURIComponent(mensaje[key]) + "&"; } if (postData.length>0 && postData[postData.length-1] == "&") { postData = postData.substr(0, postData.length - 1); } } var result = $.ajax( { type: "POST", url: url, data: postData, dataType: "json", async: true, cache: false, error: function (error) { obj.esperandoRespuesta = false; safeCallback(errorCallback,error); }, success: function (respuesta) { obj.esperandoRespuesta = false; safeCallback(callback,respuesta); } } ); return true; } HermesMsg.prototype.ServicioId = function () { return this.servicioId; } HermesMsg.prototype.CreateUrl = function (urlPart) { var url = this.baseUrl; if (url.length == 0 || url[url.length - 1] != '/') { url += "/"; } url += urlPart; return url; } HermesMsg.prototype.EnviarMensaje = function(servicioId, contenido, callback, callbackError) { if (this.EstadoMensaje != this.Desocupado) { return false; } this.EstadoMensaje = this.EnviandoMensaje; var obj = this; var mensaje = { contenido: contenido, tipo: this.TipoMensaje }; // Si se establecio una sesión hay que encriptar el mensaje if (this.sesion != null && this.sesion.idSesion != null) { // Encriptar el mensaje var result = obj.subtle.encrypt( { name: "AES-CBC", iv: this.sesion.iv }, this.sesion.aesKey, toSupportedArray(JSON.stringify(contenido)) ); CryptoCall( result, function (cipherText) { mensaje = { contenido: _arrayBufferToBase64(cipherText), tipo: obj.TipoMensaje, sesion: obj.sesion.idSesion }; obj.EnviarMensajeInterno(servicioId, mensaje, callback, callbackError); }, function (error) { obj.Estado = obj.Desocupado; safeCallback(callbackError, error); } ); } else { this.EnviarMensajeInterno(servicioId, mensaje, callback, callbackError); } } HermesMsg.prototype.EnviarMensajeInterno = function (servicioId, mensaje, callback, callbackError) { var obj = this; var result = this.HacerSolicitud( this.CreateUrl("servicio/EnviarMensaje"), function (respuesta) { if (respuesta.Resultado) { obj.EstadoMensaje = obj.EsperandoRespuesta; var mensajeId = respuesta.Datos.Id; // Monitorear constantemente para ver si ya está lista la respuesta window.setTimeout( function () { obj.RevisarRespuesta(servicioId, mensajeId, callback, callbackError); }, 1000 ); } else { deleteFromCache("Sesion"); obj.EstadoMensaje = obj.Desocupado; safeCallback(callbackError, respuesta.Error); } }, function (error) { deleteFromCache("Sesion"); obj.Estado = obj.Desocupado; safeCallback(callbackError, error); }, { servicioId: servicioId, contenido: JSON.stringify(mensaje) } ); } HermesMsg.prototype.RevisarRespuesta = function (servicioId, mensajeId, callback, callbackError) { var obj = this; var result = this.HacerSolicitud( this.CreateUrl("servicio/RecibirRespuesta"), function (respuesta) { if (respuesta.Resultado) { if(respuesta.Datos != null) { // Esto significa que la respuesta del mensaje esta lista window.clearInterval(obj.intervalId); obj.EstadoMensaje = obj.Desocupado; // Si han una sesión entonces se debe desencriptar el mensaje if (obj.sesion != null && obj.sesion.idSesion != null) { // Desencriptar el mensaje var result = obj.subtle.decrypt( { name: "AES-CBC", iv: toSupportedArray(obj.sesion.iv) }, obj.sesion.aesKey, toSupportedArray(_base64ToArrayBuffer(respuesta.Datos)) ); CryptoCall( result, function (plainText) { var s = ""; var bytes = new Int8Array(plainText); for (var i = 0; i < bytes.length; i++) { if (bytes[i] == 0) //IE agrega un 0 al final { continue; } s += String.fromCharCode(bytes[i]); } var mensaje = JSON.parse(s); safeCallback(callback, mensaje.contenido); }, function () { safeCallback(callbackError); } ); } else { var mensaje = JSON.parse(respuesta.Datos); safeCallback(callback, mensaje.contenido); } } else { // La respuesta no está lista, volver a ejecutar timeout window.setTimeout( function () { obj.RevisarRespuesta(servicioId, mensajeId, callback, callbackError); }, 1000 ); } } else { // Esto significa que el mensaje no existe deleteFromCache("Sesion"); obj.EstadoMensaje = obj.Desocupado; safeCallback(callbackError); } }, function (error) { // Esto es un error técnico, por lo que no se cancela el timer window.setTimeout( function () { obj.RevisarRespuesta(servicioId, mensajeId, callback, callbackError); }, 1000 ); }, {servicioId : servicioId, mensajeId : mensajeId} ); } HermesMsg.prototype.ProcesarMensajesPendientes = function () { var obj = this; var result = this.HacerSolicitud( this.CreateUrl("servicio/MensajesPendientes"), function (respuesta) { obj.PonerTimeoutRevisionMensajesPendientes(); if (respuesta.Resultado) { // Procesar los mensajes recibidos var mensajesCliente = new Array(); for (var i = 0; i < respuesta.Datos.length; i++) { respuesta.Datos[i]._Respuesta = null; // Significa que el mensaje todavia no tiene una respuesta var mensaje = JSON.parse(respuesta.Datos[i].Mensaje); if (mensaje.tipo == obj.MensajeInicioSesion) { var mensajeActual = respuesta.Datos[i]; // Se debe responder con la llave publica del servicio var mensaje2 = { contenido: obj.publicKeyJwk }; // Enviar la respuesta del mensaje obj.HacerSolicitud( obj.CreateUrl("servicio/EnviarRespuesta"), function (respuesta) { // No hay nada que hacer, el mensaje sale de la lista de pendientes }, function (error) { // En este caso no se responde y el cliente debe reintentar }, { servicioId: obj.servicioId, mensajeId: mensajeActual.Id, contenido: JSON.stringify(mensaje2) } ); } else if (mensaje.tipo == obj.MensajeEstablecerSesion) { var mensajeActual = respuesta.Datos[i]; // Desencriptar la llave AES y el vector iv y guardarla en Sesiones var encryptedKey = _base64ToArrayBuffer(mensaje.contenido); // Si se produce un error se retorna -1 al cliente var errorCallback = function () { obj.HacerSolicitud( obj.CreateUrl("servicio/EnviarRespuesta"), function (respuesta) {}, function (error) {}, { servicioId: obj.servicioId, mensajeId: mensajeActual.Id, contenido: JSON.stringify({contenido:-1}) } ); }; try { var result = obj.subtle.decrypt( { name: "RSAES-PKCS1-v1_5" }, obj.privateKey, toSupportedArray(encryptedKey) ); CryptoCall( result, function (aesKeyIvBytes) { // Separar la llave AES y el vector IV var aesKeyIvBytes8 = new Uint8Array(aesKeyIvBytes); var aesKeyBytes = new Uint8Array(aesKeyIvBytes8.length - 16); var iv = new Uint8Array(16); for (var i = 0; i < aesKeyBytes.length; i++) { aesKeyBytes[i] = aesKeyIvBytes8[i]; } for (var i = 0; i < iv.length; i++) { iv[i] = aesKeyIvBytes8[i + aesKeyBytes.length]; } // Importar la llave AES var result = obj.subtle.importKey( "raw", toSupportedArray(aesKeyBytes), { name: "AES-CBC", length: 256, //can be 128, 192, or 256 }, true, ["encrypt", "decrypt"] ); CryptoCall( result, function (aesKey) { // Incluir la llave AES y el vector IV en la lista de sesiones y retornar // el numero de sesion al cliente obj.sesiones.push({ aesKey: aesKey, iv: iv }); obj.HacerSolicitud( obj.CreateUrl("servicio/EnviarRespuesta"), function (respuesta) {}, function (error) { }, { servicioId: obj.servicioId, mensajeId: mensajeActual.Id, contenido: JSON.stringify({ contenido: obj.sesiones.length }) } ); }, errorCallback ); }, errorCallback ); } catch(e) { errorCallback(); } } else if (mensaje.tipo == obj.MensajeCliente) { var mensajeActual = respuesta.Datos[i]; // Si el mensaje está encriptado es necesario desencriptarlo if (mensaje.sesion != null) { // esto permite identificar que la respuesta del mensaje debe encriptarse mensajeActual.sesion = mensaje.sesion; // Desencriptar el mensaje var iv = obj.sesiones[mensaje.sesion-1].iv; var aesKey = obj.sesiones[mensaje.sesion-1].aesKey; var result = obj.subtle.decrypt( { name: "AES-CBC", iv: toSupportedArray(iv) }, aesKey, toSupportedArray(_base64ToArrayBuffer(mensaje.contenido)) ); CryptoCall( result, function (plainText) { // Convertir los bytes a una hilera var s = ""; var bytes = new Int8Array(plainText); for (var i = 0; i < bytes.length; i++) { if (bytes[i] == 0) //IE agrega un 0 al final { continue; } s += String.fromCharCode(bytes[i]); } mensajeActual.Mensaje = JSON.parse(s); obj.Mensajes.push(mensajeActual); try { obj.MensajeListo(mensajeActual); } catch (e) { } }, function () {} ); } else { // Si se requiere encripción entonces se debe rechazar el mensaje sin encriptar if (obj.encryptionRequired) { mensajeActual._Respuesta = "Mensaje rechazado porque no está encriptado"; obj.Mensajes.push(mensajeActual); } else { mensajeActual.Mensaje = mensaje.contenido; obj.Mensajes.push(mensajeActual); mensajesCliente.push(mensajeActual); } } } } // Notificar que se recibieron nuevos mensajes if (obj.MensajeListo != null && mensajesCliente.length > 0) { try { obj.MensajeListo(mensajesCliente[0]); } catch(e) { } } // Enviar las respuestas pendientes de enviar if (obj.Mensajes.length > 0) { obj.EnviarRespuestasPendientes(); } } else { // Esto significa que el servicio esta inactivo o no existe obj.Estado = obj.NoRegistrado; safeCallback(obj.ServicioDetenido); } }, function (error) { // Esto es un error técnico, por lo que no se cancela el timer obj.PonerTimeoutRevisionMensajesPendientes(); }, { servicioId: this.servicioId } ); } HermesMsg.prototype.ResponderMensaje = function(mensaje, respuesta) { // Asegurarse que el servicio esté registrado if (this.Estado != this.Registrado) { safeCallback(callbackError, "Estado invalido"); return false; } mensaje._Respuesta = respuesta; this.EnviarRespuestasPendientes(); } HermesMsg.prototype.EnviarRespuestasPendientes = function() { // Si estamos en medio de una solicitud Ajax entonces esperar if (this.esperandoRespuesta) { return; } // Buscar el primer mensaje pendiente var pendientes = new Array(); // al final del ciclo contendrá los mensajes pendientes var mensaje = null; // al final del ciclo tendrá el mensaje que se va a responder var obj = this; for (var i = 0; i < this.Mensajes.length; i++ ) { // Si el mensaje actual no tiene una respuesta entonces // introducirlo en la lista de pendientes if (this.Mensajes[i]._Respuesta == null || mensaje != null) { pendientes.push(this.Mensajes[i]); continue; } mensaje = this.Mensajes[i]; } // Enviar el mensaje pendiente que se encontro if (mensaje != null) { this.Mensajes = pendientes; var mensaje2 = { contenido: mensaje._Respuesta }; // Si hay una sesión es necesaro encriptar la respuesta if (mensaje.sesion != null) { var iv = obj.sesiones[mensaje.sesion - 1].iv; var aesKey = obj.sesiones[mensaje.sesion - 1].aesKey; var result = obj.subtle.encrypt( { name: "AES-CBC", iv: toSupportedArray(iv) }, aesKey, toSupportedArray(JSON.stringify(mensaje2)) ); CryptoCall( result, function (encrypted) { // Enviar la respuesta del mensaje obj.HacerSolicitud( obj.CreateUrl("servicio/EnviarRespuesta"), function (respuesta) { // No hay nada que hacer, el mensaje sale de la lista de pendientes }, function (error) { // Si hay un error tecnico el mensaje vuelve a la lista de pendientes obj.Mensajes.push(mensaje); }, { servicioId: obj.servicioId, mensajeId: mensaje.Id, contenido: _arrayBufferToBase64(encrypted) } ); }, function (error) { // Si hay un error tecnico el mensaje vuelve a la lista de pendientes obj.Mensajes.push(mensaje); } ); } else { // Enviar la respuesta del mensaje this.HacerSolicitud( this.CreateUrl("servicio/EnviarRespuesta"), function (respuesta) { // No hay nada que hacer, el mensaje sale de la lista de pendientes }, function (error) { // Si hay un error tecnico el mensaje vuelve a la lista de pendientes obj.Mensajes.push(mensaje); }, { servicioId: this.servicioId, mensajeId: mensaje.Id, contenido: JSON.stringify(mensaje2) } ); } } } HermesMsg.prototype.DetenerServicio = function (callback, callbackError) { // No se puede detener si no está registrado if (this.Estado != this.Registrado) { safeCallback(callbackError,"Estado invalido"); return false; } var obj = this; this.HacerSolicitud( this.CreateUrl("servicio/DetenerServicio"), function (respuesta) { obj.servicioId = null; obj.Estado = obj.NoRegistrado; safeCallback(callback); }, function (error) { safeCallback(callbackError,error); }, { servicioId: this.servicioId } ); return true; } HermesMsg.prototype.IniciarSesion = function (servicioId, callback, callbackError) { // Verificar que se haya incluido la libreria msrCrypto if (!this.subtle) { return false; } if (this.sesion != null) { return false; } var obj = this; // Tratar de recuperar la sesion del cache var sesion = getFromCache("Sesion"); if (sesion != null) { var keyData8 = new Uint8Array(_base64ToArrayBuffer(sesion.keyData)); var result = obj.subtle.importKey( "raw", toSupportedArray(keyData8), { name: "AES-CBC", length: 256, //can be 128, 192, or 256 }, true, ["encrypt", "decrypt"] ); CryptoCall( result, function (aesKey) { obj.sesion = sesion.sesion; obj.sesion.aesKey = aesKey; obj.sesion.iv = new Uint8Array(_base64ToArrayBuffer(sesion.iv)); safeCallback(callback); }, callbackError ); return; } // Solicitar al servicio que inicie una sesión segura this.TipoMensaje = this.MensajeInicioSesion; // Si se produce un error este callback pone el campo sesion en null // para establecer que no hay una sesión var callbackError2 = function (error) { obj.TipoMensaje = obj.MensajeCliente; obj.sesion = null; safeCallback(callbackError, error); } this.EnviarMensaje( servicioId, "", function(respuesta) { obj.TipoMensaje = obj.MensajeCliente; // Si respuesta es vacio significa que hubo un error del // lado del servidor if (respuesta == null || respuesta == "") { callbackError2(); return; } obj.sesion = { publicKeyJwk: respuesta, idSesion:null }; // Importar la llave RSA recibida del servicio var result = obj.subtle.importKey( "jwk", toSupportedArray(obj.sesion.publicKeyJwk), { name: "RSAES-PKCS1-v1_5" }, true, ["encrypt"] ); CryptoCall( result, function (publicKey) { // Generar la llave AES var result = obj.subtle.generateKey( { name: "AES-CBC", length: 256, //can be 128, 192, or 256 }, true, //whether the key is extractable (i.e. can be used in exportKey) ["encrypt", "decrypt"] //can be any combination of "encrypt" and "decrypt" ); CryptoCall( result, function (aeskey) { // Es necesario guardar la llave de encripción para encryptar los mensajes // posteriores obj.sesion.aesKey = aeskey; // Exportar la llave AES para poder encriptarla luego con la llave publica del servicio result = obj.subtle.exportKey( "raw", aeskey ); CryptoCall( result, function (keyData) { // Generar el vector IV para randomizar los paquetes encriptados obj.sesion.iv = window.msrCrypto.getRandomValues(new Uint8Array(16)); // Crear un solo vector (textoPlano) que incluya la clave AES y el vector IV // para su posterior encripción. El objetivo es hace una solo operación de encripción // en lugar de dos, para disminuir el tiempo de ejecución var keyData8 = new Uint8Array(keyData); var textoPlano = new Uint8Array(keyData8.length + obj.sesion.iv.length); for (i = 0; i < keyData8.length; i++) { textoPlano[i] = keyData8[i]; } for (i = 0; i < obj.sesion.iv.length; i++) { textoPlano[keyData8.length+i] = obj.sesion.iv[i]; } // Encriptar la llave AES + el vector IV result = obj.subtle.encrypt( { name: "RSAES-PKCS1-v1_5" }, publicKey, toSupportedArray(textoPlano) ); CryptoCall( result, function (encryptedKeyData) { // Enviar la llave AES y el IV encriptada al servicio obj.TipoMensaje = obj.MensajeEstablecerSesion; var ivAndAes =_arrayBufferToBase64(encryptedKeyData); obj.EnviarMensaje( servicioId, ivAndAes, function (respuesta) { // Si respuesta es < 0 significa que se produjo un error if (respuesta == null || respuesta < 0) { callbackError2(); return; } obj.TipoMensaje = obj.MensajeCliente; obj.sesion.idSesion = respuesta addToCache( "Sesion", { sesion: obj.sesion, keyData: _arrayBufferToBase64(keyData), iv: _arrayBufferToBase64(obj.sesion.iv.buffer) } ); safeCallback(callback); }, callbackError2 ); }, callbackError2 ); } ); }, callbackError2 ); }, callbackError2 ); }, function() { obj.TipoMensaje = obj.MensajeCliente; callbackError2(); } ); return true; } HermesMsg.prototype.PonerTimeoutRevisionMensajesPendientes = function () { var obj = this; window.setTimeout( function () { obj.ProcesarMensajesPendientes(); }, 1000 ); } function safeCallback(callback, param) { if (callback == null) { return; } try { callback(param); } catch (e) { } } function toSupportedArray(data) { // does this browser support Typed Arrays? var typedArraySupport = (typeof Uint8Array !== "undefined"); // get the data type of the parameter var dataType = Object.prototype.toString.call(data); dataType = dataType.substring(8, dataType.length - 1); // determine the type switch (dataType) { // Regular JavaScript Array. Convert to Uint8Array if supported // else do nothing and return the array case "Array": return typedArraySupport ? new Uint8Array(data) : data; // ArrayBuffer. IE11 Web Crypto API returns ArrayBuffers that you have to convert // to Typed Arrays. Convert to a Uint8Arrays and return; case "ArrayBuffer": return new Uint8Array(data); // Already Uint8Array. Obviously there is support. case "Uint8Array": return data; case "Uint16Array": case "Uint32Array": return new Uint8Array(data); // String. Convert the string to a byte array using Typed Arrays if // supported. case "String": var newArray = typedArraySupport ? new Uint8Array(data.length) : new Array(data.length); for (var i = 0; i < data.length; i += 1) { newArray[i] = data.charCodeAt(i); } return newArray; // Some other type. Just return the data unchanged. default: throw new Error("toSupportedArray : unsupported data type " + dataType); } } function CryptoCall(result, successCallback, errorCallback) { if (result.then == null) { result.oncomplete = function () { successCallback(result.result); }; result.onerror = function (err) { errorCallback(err); } } else { result.then(successCallback).catch(errorCallback); } } function _arrayBufferToBase64(buffer) { var binary = ''; var bytes = new Uint8Array(buffer); var len = bytes.byteLength; for (var i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } return window.btoa(binary); } function _base64ToArrayBuffer(base64) { var binary_string = window.atob(base64); var len = binary_string.length; var bytes = new Uint8Array(len); for (var i = 0; i < len; i++) { bytes[i] = binary_string.charCodeAt(i); } return bytes.buffer; } function GetType(data) { // get the data type of the parameter var dataType = Object.prototype.toString.call(data); dataType = dataType.substring(8, dataType.length - 1); return dataType; } function addToCache(name,value) { if (typeof(window.localStorage)=="undefined") { return; } // Los items del cache expiran cada 2 horas window.localStorage.setItem(name, JSON.stringify({ value: value, expiration: (new Date()).getTime() + 7200000 })); } function getFromCache(name) { if (typeof (window.localStorage) == "undefined") { return null; } var result = window.localStorage.getItem(name); if (result == null) { return null; } result = JSON.parse(result); // Si ya expiró la entrada del cache entonces borrarla if (result.expiration < (new Date()).getTime()) { window.localStorage.removeItem(name); return null; } return result.value; } function deleteFromCache(name) { if (typeof (window.localStorage) == "undefined") { return; } window.localStorage.removeItem(name); }