from __future__ import annotations # tuple[...] compatible con Python 3.8+ import gradio as gr import secrets import string import base64 import math import json import os import sys import tempfile from datetime import datetime from PIL import Image import io from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives import hashes from cryptography.hazmat.backends import default_backend import nacl.pwhash import nacl.signing import nacl.public VERSION = "2.1.0" PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" RNG_SOURCE = "os.urandom / secrets (CSPRNG)" # Símbolo único definido en un solo lugar — ya no duplicado SYMBOLS = "!@#$%^&*()_+-=[]{}|;:,.<>?" DICEWARE_WORDLIST = ['abaco', 'abeja', 'abierto', 'abogado', 'abono', 'abrazo', 'abrir', 'abuelo', 'abuso', 'acabar', 'academia', 'acceso', 'accion', 'aceite', 'acero', 'acertar', 'aclarar', 'acoger', 'acordar', 'acorde', 'acortar', 'acoso', 'acostar', 'acuerdo', 'acusar', 'adaptar', 'adecuar', 'adelante', 'ademas', 'adentro', 'adoptar', 'adorar', 'adquirir', 'adulto', 'adverso', 'advertir', 'afan', 'afectar', 'aficion', 'afirmar', 'afligir', 'afrontar', 'agasajar', 'agente', 'agitar', 'agonia', 'agotar', 'agradar', 'agravar', 'agregar', 'agrio', 'aguacate', 'aguantar', 'aguila', 'aguja', 'ahijado', 'ahogar', 'ahora', 'ahorro', 'aire', 'aislar', 'ajedrez', 'ajeno', 'ajustar', 'alabanza', 'alambre', 'alarma', 'alba', 'albergar', 'alcance', 'aldea', 'aleatorio', 'alegria', 'alejar', 'alerta', 'alfombra', 'alga', 'algodon', 'alguien', 'alguno', 'alianza', 'aliento', 'alinear', 'aliviar', 'allanar', 'alma', 'almendra', 'almohada', 'alojar', 'alquiler', 'altar', 'alterar', 'altivo', 'alto', 'alumno', 'alzar', 'amable', 'amante', 'amar', 'ambicion', 'ambos', 'amenaza', 'ameno', 'amigo', 'amistad', 'amor', 'amparo', 'ampolla', 'analogo', 'ancho', 'anciano', 'andar', 'anecdota', 'angel', 'angosto', 'anillo', 'animo', 'aniversario', 'anoche', 'ansia', 'antena', 'antes', 'antiguo', 'anual', 'anuncio', 'anadir', 'apagar', 'aparato', 'apartar', 'apellido', 'apenas', 'apetito', 'aplicar', 'apodo', 'aportar', 'apoyar', 'aprender', 'apretar', 'aprobado', 'aprovechar', 'aptitud', 'apuro', 'aquel', 'aquello', 'arana', 'arar', 'arbol', 'arco', 'arder', 'ardilla', 'arena', 'argucia', 'arido', 'arma', 'armario', 'aroma', 'arpon', 'arquitecto', 'arrancar', 'arreglar', 'arriba', 'arriesgar', 'arrojar', 'arroyo', 'arte', 'artico', 'artista', 'arzobispo', 'asado', 'asaltar', 'ascender', 'asco', 'asegurar', 'asentar', 'asesor', 'asfalto', 'asiento', 'asilo', 'asistir', 'asma', 'asno', 'asombro', 'aspecto', 'aspirar', 'astilla', 'astro', 'astuto', 'asumir', 'asunto', 'atacar', 'atajo', 'ataque', 'atar', 'ataud', 'atencion', 'atenerse', 'aterrizar', 'atlas', 'atmosfera', 'atomo', 'atras', 'atrever', 'atributo', 'audaz', 'audio', 'aumentar', 'aunque', 'ausente', 'autentico', 'autobus', 'autor', 'auxilio', 'avance', 'avaro', 'avena', 'aventura', 'averiguar', 'avestruz', 'avion', 'avisar', 'aviso', 'axila', 'ayer', 'ayuda', 'ayunar', 'azahar', 'azar', 'azote', 'azucar', 'azul', 'babero', 'babor', 'bache', 'bailar', 'baja', 'bajar', 'balanza', 'balcon', 'balde', 'balsa', 'banco', 'banda', 'bandera', 'bano', 'barato', 'barba', 'barco', 'barrer', 'barrio', 'barro', 'basar', 'basura', 'batalla', 'bateria', 'batir', 'bazar', 'bebe', 'beber', 'belleza', 'bendecir', 'beneficio', 'besar', 'bestia', 'biblioteca', 'bicho', 'bien', 'bigote', 'billete', 'binario', 'biologia', 'blanco', 'bloque', 'blusa', 'boa', 'bobo', 'boca', 'boda', 'bodega', 'bolero', 'bolsa', 'bomba', 'bondad', 'bonito', 'borde', 'borrar', 'bosque', 'bota', 'botella', 'boton', 'boxear', 'bravo', 'brazo', 'brecha', 'breve', 'brillo', 'brindar', 'brisa', 'brocha', 'broma', 'bronce', 'brote', 'brujula', 'bruto', 'bueno', 'buey', 'bufalo', 'bufanda', 'bugle', 'buitre', 'bulto', 'buque', 'burbuja', 'burla', 'burro', 'buscar', 'butaca', 'buzon', 'caballo', 'cabeza', 'cable', 'cabra', 'cacao', 'cacerola', 'cachorro', 'cada', 'cadena', 'caer', 'cafe', 'caja', 'calamar', 'calcular', 'caldo', 'calentar', 'calidad', 'callar', 'calma', 'calor', 'calvo', 'cama', 'cambiar', 'camello', 'camino', 'caminar', 'camisa', 'campana', 'campo', 'canal', 'cancion', 'candado', 'cangrejo', 'canoa', 'cansar', 'cantar', 'cantidad', 'capa', 'capaz', 'capitan', 'cara', 'carbon', 'carcel', 'cardinal', 'carecer', 'carga', 'cargo', 'carino', 'carne', 'carpeta', 'carrera', 'carro', 'carta', 'cartera', 'casa', 'casar', 'cascada', 'casco', 'caso', 'castigo', 'castillo', 'casual', 'catalogo', 'catarata', 'catorce', 'causa', 'cavar', 'cazar', 'cebolla', 'ceder', 'ceguera', 'ceja', 'celda', 'celebrar', 'celoso', 'celsius', 'cemento', 'cenar', 'ceniza', 'centro', 'cepillo', 'cerca', 'cerebro', 'ceremonia', 'cero', 'cerrar', 'cerro', 'certeza', 'cesar', 'champu', 'chance', 'chapa', 'charlar', 'chico', 'chile', 'chimenea', 'chino', 'chip', 'chiste', 'chocolate', 'chofer', 'choque', 'ciclon', 'cielo', 'ciencia', 'cifra', 'cima', 'cinco', 'cine', 'cinta', 'cinturon', 'circo', 'ciruela', 'cisne', 'cita', 'ciudad', 'civil', 'claro', 'clase', 'clave', 'clavar', 'clima', 'clinica', 'club', 'cobertor', 'cobrar', 'cobre', 'cocina', 'coco', 'codigo', 'coger', 'coincidir', 'cola', 'colcha', 'colegio', 'colgar', 'colina', 'collar', 'colmena', 'color', 'columna', 'comarca', 'combatir', 'comedia', 'comenzar', 'comer', 'cometa', 'comida', 'comite', 'como', 'compania', 'comparar', 'compas', 'competir', 'completo', 'comprar', 'comprender', 'computar', 'comun', 'conceder', 'concepto', 'concierto', 'conde', 'condicion', 'conducir', 'conejo', 'conexion', 'confesar', 'confianza', 'confundir', 'congelar', 'conocer', 'consejo', 'consola', 'constante', 'consumir', 'contar', 'contento', 'contigo', 'continuar', 'contra', 'control', 'convencer', 'copia', 'coquetear', 'coraje', 'corazon', 'corbata', 'corcho', 'cordero', 'cordial', 'corona', 'corral', 'correr', 'cortar', 'corte', 'cosa', 'costa', 'costumbre', 'coyote', 'crear', 'crecer', 'creer', 'crema', 'cresta', 'criar', 'crimen', 'cristal', 'criticar', 'croqueta', 'cruzar', 'cuadro', 'cual', 'cuando', 'cuanto', 'cuarto', 'cuatro', 'cubierta', 'cubo', 'cucaracha', 'cuchara', 'cuchillo', 'cuello', 'cuenta', 'cuerda', 'cuerno', 'cuerpo', 'cuesta', 'cueva', 'cuidar', 'culebra', 'culpa', 'cultivar', 'cumbre', 'cumple', 'cuna', 'cupon', 'cura', 'curioso', 'curso', 'curva', 'cuyo', 'dama', 'danar', 'danza', 'dar', 'dato', 'deber', 'decidir', 'decir', 'dedo', 'defender', 'definir', 'dejar', 'delgado', 'delito', 'demas', 'demora', 'dentro', 'departamento', 'depender', 'deporte', 'deposito', 'derrota', 'desayuno', 'descanso', 'descender', 'desear', 'desfile', 'deseo', 'desierto', 'deslizar', 'desnudo', 'despacio', 'despedir', 'despues', 'destino', 'detalle', 'detener', 'detras', 'devolver', 'dia', 'diablo', 'dialogo', 'diamante', 'diana', 'diario', 'dibujar', 'diccionario', 'diente', 'diez', 'diferente', 'dificil', 'digital', 'digno', 'dilema', 'dinero', 'dios', 'direccion', 'dirigir', 'disciplina', 'disco', 'diseno', 'disfrutar', 'disparar', 'distancia', 'distinto', 'diverso', 'dividir', 'divino', 'doblar', 'docena', 'doctor', 'dolar', 'domingo', 'donar', 'donde', 'dorado', 'dormir', 'dos', 'dosis', 'dragon', 'drama', 'droga', 'duda', 'dulce', 'duque', 'durar', 'duro', 'ebano', 'ebrio', 'echar', 'eco', 'edad', 'edificio', 'editar', 'educar', 'efecto', 'ejemplo', 'ejercito', 'elaborar', 'eleccion', 'elegir', 'elefante', 'elegante', 'elemento', 'elevar', 'eliminar', 'ella', 'ello', 'embarazo', 'emergencia', 'emisor', 'emocion', 'empatar', 'empezar', 'emplear', 'empresa', 'enano', 'encajar', 'encender', 'encima', 'encontrar', 'encuentro', 'energia', 'enfermo', 'enfrentar', 'enganar', 'enigma', 'enojar', 'ensayo', 'enseguida', 'ensuciar', 'entender', 'entero', 'entonces', 'entrada', 'entrar', 'entre', 'entregar', 'entusiasmo', 'enviar', 'envidia', 'epoca', 'equilibrio', 'equipo', 'equivocar', 'erizo', 'error', 'escalar', 'escapar', 'escena', 'escoba', 'escoger', 'esconder', 'escribir', 'escudo', 'escuela', 'ese', 'esencia', 'esfera', 'esfuerzo', 'eslabon', 'esmeralda', 'espacio', 'espalda', 'espanto', 'especial', 'especie', 'espejo', 'espera', 'esperanza', 'espia', 'espiga', 'espina', 'espiritu', 'esposa', 'esposo', 'esquina', 'establo', 'estado', 'estallar', 'estante', 'estar', 'estatua', 'este', 'estilo', 'estima', 'estornudar', 'estrella', 'estrecho', 'estructura', 'estudiar', 'eterno', 'etiqueta', 'evadir', 'evento', 'evitar', 'exacto', 'examen', 'exceso', 'excusa', 'exigir', 'exilio', 'exito', 'explicar', 'explotar', 'exponer', 'expresar', 'extender', 'extra', 'extrano', 'extremo', 'fabrica', 'fabula', 'fachada', 'facil', 'factor', 'faena', 'faja', 'falda', 'fallar', 'falso', 'falta', 'fama', 'familia', 'fanatico', 'fantasma', 'farol', 'faro', 'farsa', 'fase', 'fatal', 'fauna', 'favor', 'fax', 'febrero', 'fecha', 'felicidad', 'feliz', 'fenix', 'feo', 'feria', 'feroz', 'ferrocarril', 'festival', 'fiable', 'fibra', 'ficcion', 'ficha', 'fidelidad', 'fiebre', 'fiel', 'fiesta', 'figura', 'fijar', 'filamento', 'filosofia', 'filtro', 'fin', 'final', 'finanza', 'fingir', 'firma', 'firme', 'fiscal', 'fisica', 'flaco', 'flauta', 'flecha', 'flexible', 'flor', 'flotar', 'fluir', 'foco', 'fogata', 'fogon', 'folleto', 'fondo', 'forma', 'formar', 'foro', 'fortuna', 'forzar', 'fosa', 'foto', 'fracaso', 'fragancia', 'frase', 'fraude', 'frecuente', 'fregar', 'freno', 'frente', 'fresa', 'fresco', 'frijol', 'frontera', 'fruta', 'fuego', 'fuente', 'fuerte', 'fuerza', 'fuga', 'fumar', 'funcion', 'funda', 'fundar', 'funeral', 'furioso', 'futbol', 'futuro', 'gabinete', 'gafas', 'gala', 'galeria', 'gallina', 'gallo', 'ganar', 'ganga', 'ganso', 'garaje', 'garantia', 'garra', 'gas', 'gastar', 'gato', 'gelatina', 'gemelo', 'gema', 'genio', 'gente', 'geografia', 'gerente', 'gesto', 'gigante', 'gimnasio', 'girar', 'gitano', 'glaciar', 'globo', 'gloria', 'gobernar', 'gol', 'golondrina', 'golpear', 'goma', 'gordo', 'gorila', 'gorra', 'gota', 'gozar', 'grabadora', 'gracia', 'graduar', 'grafico', 'granada', 'grande', 'grano', 'grasa', 'gratis', 'grave', 'griego', 'grillo', 'gripe', 'gris', 'gritar', 'grito', 'grueso', 'grupo', 'guante', 'guapo', 'guardar', 'guerra', 'guia', 'guion', 'guisante', 'guitarra', 'gusano', 'gusto', 'haba', 'habil', 'habitar', 'habito', 'hablar', 'hacer', 'hacha', 'hada', 'halcon', 'hallar', 'hambre', 'harina', 'hasta', 'hebilla', 'hecho', 'helado', 'helicoptero', 'heredar', 'herida', 'hermano', 'heroe', 'herramienta', 'hervir', 'hielo', 'hierba', 'hierro', 'higo', 'hija', 'hilo', 'himno', 'hinojo', 'historia', 'hogar', 'hoja', 'hombre', 'hongo', 'honor', 'hora', 'horizonte', 'hormiga', 'horno', 'horror', 'hospital', 'hostal', 'hotel', 'hoy', 'hucha', 'hueco', 'huevo', 'huir', 'humanidad', 'humano', 'humedad', 'humilde', 'humo', 'hundir', 'huracan', 'huso', 'idea', 'idioma', 'idolo', 'iglesia', 'igual', 'ilegal', 'imagen', 'imitar', 'impedir', 'imperio', 'imponer', 'importar', 'imposible', 'impulso', 'incendio', 'incluir', 'indicar', 'indigo', 'industria', 'infantil', 'inferior', 'infinito', 'inflar', 'informe', 'ingenio', 'iniciar', 'inmenso', 'inodoro', 'insecto', 'insistir', 'instante', 'insulto', 'inteligente', 'intencion', 'interes', 'interno', 'intimar', 'introducir', 'inundar', 'invadir', 'inventar', 'invierno', 'invitar', 'inyectar', 'ir', 'ira', 'iris', 'isla', 'israel', 'italia', 'izquierda', 'jabali', 'jabon', 'jactarse', 'jade', 'jamas', 'jamon', 'jardin', 'jarra', 'jaula', 'jefe', 'jengibre', 'jinete', 'jirafa', 'jornada', 'joven', 'joya', 'jubilar', 'juez', 'jugador', 'jugar', 'jugo', 'juguete', 'juicio', 'julio', 'junio', 'juntar', 'jupiter', 'jurar', 'justicia', 'justo', 'juvenil', 'laberinto', 'labio', 'labor', 'lado', 'lagarto', 'lago', 'lagrima', 'lamentar', 'lampara', 'lana', 'langosta', 'lanza', 'lanzar', 'lapiz', 'largo', 'lastima', 'lata', 'latigo', 'latir', 'lavar', 'lazo', 'leccion', 'leche', 'lector', 'leer', 'legumbre', 'lejos', 'lengua', 'lenteja', 'lentes', 'leon', 'leopardo', 'lesion', 'letra', 'levantar', 'ley', 'leyenda', 'libertad', 'libra', 'libro', 'licor', 'lider', 'liebre', 'liga', 'ligero', 'limite', 'limon', 'limpiar', 'lindo', 'linea', 'lingote', 'lirio', 'liso', 'lista', 'listo', 'litera', 'litro', 'llama', 'llanto', 'llave', 'llegar', 'llenar', 'llevar', 'llorar', 'llover', 'lluvia', 'lobo', 'local', 'loco', 'logica', 'lograr', 'lomo', 'lona', 'longitud', 'loro', 'loteria', 'luchar', 'luciernaga', 'lugar', 'lujo', 'luna', 'lunes', 'luz', 'macho', 'madera', 'madre', 'madrugada', 'maestro', 'magia', 'magnifico', 'maiz', 'mal', 'maldad', 'maleta', 'malicia', 'malla', 'malo', 'mama', 'mamut', 'mancha', 'mandar', 'manejar', 'manga', 'mano', 'manso', 'manta', 'mantel', 'mantener', 'manzana', 'manana', 'mapa', 'maquina', 'mar', 'maravilla', 'marca', 'marcha', 'marco', 'mareo', 'margen', 'marido', 'marina', 'mariposa', 'marmol', 'marron', 'martes', 'martillo', 'mas', 'mascara', 'masivo', 'matar', 'materia', 'matrimonio', 'maximo', 'mayo', 'mayor', 'mecanico', 'medalla', 'mediano', 'medicina', 'medida', 'medio', 'medir', 'medusa', 'mejilla', 'mejor', 'melocoton', 'melodia', 'membrana', 'memoria', 'mencionar', 'mendigo', 'mensaje', 'menta', 'mente', 'mentira', 'mentor', 'menu', 'mercado', 'mercurio', 'merecer', 'merienda', 'mes', 'mesa', 'meta', 'metal', 'meter', 'metodo', 'metro', 'mezclar', 'mi', 'microbio', 'miedo', 'miel', 'miembro', 'mientras', 'migrar', 'mil', 'milagro', 'militar', 'millon', 'mimo', 'mina', 'minero', 'minimo', 'ministro', 'minuto', 'mio', 'mirar', 'mirlo', 'misa', 'misil', 'mismo', 'misterio', 'mitad', 'mito', 'moda', 'modelo', 'modesto', 'modo', 'mojar', 'moler', 'molestar', 'molino', 'momento', 'monarca', 'moneda', 'monitor', 'mono', 'montana', 'montar', 'monumento', 'morado', 'morder', 'moreno', 'morir', 'mosca', 'mostaza', 'mostrar', 'motivo', 'mover', 'movil', 'mucho', 'mudar', 'mueble', 'muerte', 'muestra', 'mujer', 'mula', 'multa', 'multiple', 'multitud', 'mundo', 'muralla', 'murcielago', 'muro', 'musculo', 'museo', 'musica', 'muslo', 'mutuo', 'nacer', 'nacion', 'nadar', 'naipe', 'naranja', 'nariz', 'nata', 'naturaleza', 'nave', 'navidad', 'neblina', 'necesitar', 'negar', 'negocio', 'negro', 'nervio', 'neto', 'neutro', 'nevar', 'nevera', 'ni', 'nido', 'niebla', 'nieto', 'ninguno', 'nino', 'nivel', 'no', 'noche', 'nombrar', 'nombre', 'noria', 'norma', 'norte', 'nota', 'notar', 'noticia', 'novato', 'novecientos', 'novela', 'noveno', 'noviembre', 'novio', 'nube', 'nuca', 'nucleo', 'nudillo', 'nudo', 'nuera', 'nueve', 'nuevo', 'numero', 'nunca', 'nutria', 'oasis', 'obedecer', 'obispo', 'objeto', 'obra', 'obrero', 'observar', 'obstaculo', 'obtener', 'ocasion', 'oceano', 'ochenta', 'ocho', 'ocio', 'ocre', 'octubre', 'ocultar', 'ocupar', 'ocurrir', 'odiar', 'odio', 'oeste', 'ofender', 'oferta', 'oficial', 'oficio', 'ogro', 'oido', 'oir', 'ojo', 'ola', 'oler', 'olfato', 'oliva', 'olvidar', 'ombligo', 'onda', 'onza', 'opaco', 'opcion', 'operar', 'opinar', 'oponer', 'optar', 'oracion', 'orar', 'orbita', 'orden', 'oreja', 'organo', 'orgullo', 'oriente', 'origen', 'orilla', 'oro', 'orquesta', 'ortiga', 'oso', 'otono', 'otro', 'ovalo', 'ovario', 'ovillo', 'oxigeno', 'oye', 'ozono', 'paciencia', 'pacto', 'padre', 'pagar', 'pagina', 'pais', 'paja', 'pajaro', 'palabra', 'palacio', 'paladar', 'palanca', 'palma', 'palmera', 'paloma', 'palpar', 'pan', 'panal', 'pantera', 'pantalla', 'pantalon', 'panza', 'papa', 'papel', 'par', 'parar', 'parasol', 'pared', 'pariente', 'parir', 'parlamento', 'paro', 'parpadear', 'parte', 'partir', 'pasado', 'pasar', 'paseo', 'pasillo', 'paso', 'pasta', 'pastel', 'pasto', 'pata', 'patio', 'patria', 'pausa', 'pavo', 'payaso', 'peaje', 'pecado', 'peces', 'pedal', 'pedir', 'pegar', 'peine', 'pelar', 'pelicula', 'peligro', 'pellejo', 'pelo', 'pelota', 'pensar', 'peon', 'peor', 'pepino', 'pequeno', 'pera', 'percha', 'perder', 'perdon', 'perfil', 'perico', 'periodo', 'perla', 'permitir', 'perro', 'persona', 'pesa', 'pescar', 'peso', 'pestana', 'pez', 'picar', 'pico', 'pie', 'piedra', 'piel', 'pierna', 'pila', 'pillar', 'pilotar', 'piloto', 'pimienta', 'pino', 'pintar', 'pintor', 'pina', 'pipa', 'pirata', 'pisar', 'piscina', 'piso', 'pista', 'pizarra', 'placer', 'plan', 'planeta', 'planta', 'plata', 'plato', 'playa', 'plaza', 'plazo', 'plegar', 'pleno', 'pluma', 'poblado', 'pobre', 'poco', 'poder', 'poema', 'poeta', 'polen', 'policia', 'pollo', 'polo', 'polvo', 'poner', 'por', 'porque', 'portar', 'posar', 'poseer', 'posible', 'postal', 'postre', 'potro', 'pozo', 'prado', 'precio', 'predicar', 'preferir', 'pregunta', 'premio', 'prenda', 'preocupar', 'preparar', 'presentar', 'preso', 'prestar', 'pretender', 'prima', 'primavera', 'primo', 'princesa', 'principal', 'prisa', 'privado', 'probar', 'proceso', 'producir', 'profesor', 'programa', 'progreso', 'promesa', 'prometer', 'pronombre', 'pronto', 'propina', 'propio', 'proteger', 'protestar', 'provecho', 'proverbio', 'provincia', 'proximo', 'proyecto', 'prueba', 'publicar', 'puchero', 'pudrir', 'pueblo', 'puente', 'puerta', 'pues', 'puesto', 'pulga', 'pulir', 'pulmon', 'pulpo', 'pulso', 'punta', 'punto', 'puno', 'pupitre', 'pure', 'quebrar', 'quedar', 'queja', 'quemar', 'querer', 'queso', 'quien', 'quieto', 'quimica', 'quince', 'quinto', 'quiosco', 'quitar', 'rabia', 'rabo', 'radiar', 'radio', 'raiz', 'rama', 'ramo', 'rana', 'rango', 'rapido', 'raptar', 'raro', 'rasgo', 'rata', 'raton', 'raya', 'rayo', 'raza', 'razon', 'reaccion', 'real', 'rebanada', 'rebelde', 'recado', 'receta', 'rechazar', 'recibir', 'recoger', 'recomendar', 'recordar', 'recorrer', 'recuerdo', 'red', 'redondo', 'reducir', 'referir', 'reflejo', 'reforma', 'refugio', 'regalar', 'regalo', 'regimen', 'regla', 'regresar', 'rehen', 'reina', 'reir', 'reja', 'relampago', 'relatar', 'reloj', 'remar', 'remedio', 'remo', 'rendir', 'renovar', 'renta', 'reparar', 'repartir', 'repetir', 'reposar', 'reptil', 'res', 'resbalar', 'rescatar', 'resfriado', 'resolver', 'respetar', 'responder', 'restar', 'resto', 'resultado', 'retirar', 'retorno', 'retrato', 'reunion', 'reunir', 'revelar', 'reventar', 'revisar', 'rey', 'rezar', 'rico', 'riego', 'rienda', 'riesgo', 'rincon', 'rio', 'riqueza', 'risa', 'ritmo', 'rito', 'rival', 'robar', 'roble', 'robot', 'roca', 'roce', 'rodar', 'rodilla', 'rogar', 'rojo', 'romper', 'roncar', 'ronda', 'ropa', 'rosa', 'rostro', 'rotar', 'roto', 'rubio', 'rueda', 'ruido', 'ruina', 'rumbo', 'rumor', 'ruta', 'sabado', 'saber', 'sabio', 'sabor', 'sacar', 'sacerdote', 'saco', 'sacudir', 'sagaz', 'sagrado', 'sal', 'sala', 'salario', 'salchicha', 'salida', 'salir', 'salmon', 'salon', 'salsa', 'saltar', 'salto', 'salud', 'saludo', 'salvaje', 'sangre', 'sano', 'santo', 'sarten', 'sastre', 'saturno', 'sauce', 'sauna', 'saxofon', 'sazon', 'secar', 'seccion', 'secreto', 'secuencia', 'sed', 'seda', 'segundo', 'seguir', 'seguridad', 'seguro', 'seis', 'sello', 'selva', 'semana', 'sembrar', 'semejante', 'semilla', 'seno', 'sensato', 'sentar', 'sentido', 'sentir', 'senal', 'senor', 'separar', 'sepultar', 'ser', 'serie', 'serio', 'serpiente', 'servir', 'sesion', 'seta', 'setenta', 'sexo', 'sexto', 'si', 'sibarita', 'sicario', 'sidra', 'siempre', 'sierra', 'siete', 'siga', 'siglo', 'significar', 'siguiente', 'silbar', 'silencio', 'silla', 'silueta', 'simbolo', 'simpatico', 'simple', 'sin', 'sino', 'sirena', 'sistema', 'sitio', 'sobrar', 'sobre', 'sobrino', 'socavar', 'socio', 'soda', 'sofocar', 'sol', 'soldado', 'soledad', 'solicitar', 'solo', 'soltar', 'solucion', 'sombrero', 'sombrilla', 'sonar', 'sonido', 'sonreir', 'sopa', 'soplar', 'soportar', 'sordo', 'sorpresa', 'sorteo', 'sospechar', 'sostener', 'suave', 'subir', 'suceder', 'sucio', 'sudar', 'suegro', 'suelo', 'sueno', 'suerte', 'sufrir', 'suizo', 'sumar', 'sumergir', 'superar', 'superior', 'suplicar', 'suponer', 'surgir', 'surtir', 'suspirar', 'sustantivo', 'susto', 'tabaco', 'tabla', 'tacto', 'tajo', 'tal', 'talar', 'talento', 'talla', 'tallo', 'tamano', 'tambor', 'tan', 'tango', 'tanque', 'tapa', 'tapar', 'tarde', 'tarea', 'tarifa', 'tarjeta', 'tarta', 'tasa', 'taza', 'teatro', 'techo', 'tecla', 'tecnica', 'tedio', 'tejer', 'tejido', 'tela', 'telefono', 'tema', 'temer', 'temor', 'temporada', 'temprano', 'tenaz', 'tender', 'tenedor', 'tener', 'tenis', 'tenso', 'teoria', 'tercer', 'terminar', 'terremoto', 'terreno', 'terrible', 'tesoro', 'testigo', 'texto', 'tiempo', 'tienda', 'tierno', 'tierra', 'tigre', 'tijera', 'timido', 'tinta', 'tio', 'tipo', 'tirar', 'tiro', 'tiza', 'toalla', 'tobillo', 'tocar', 'todavia', 'todo', 'toldo', 'tomar', 'tono', 'tonto', 'topo', 'tormenta', 'tornillo', 'toro', 'torre', 'tortuga', 'tos', 'toser', 'tostar', 'total', 'trabajar', 'trabajo', 'traducir', 'traer', 'trafico', 'tragedia', 'trago', 'traicion', 'traje', 'trama', 'trampa', 'tranquilo', 'transito', 'trapo', 'tratar', 'traves', 'trebol', 'trece', 'tregua', 'tren', 'tres', 'tribu', 'trigo', 'triste', 'triunfar', 'trofeo', 'trompeta', 'tronco', 'tropa', 'trote', 'trucha', 'tu', 'tubo', 'tumba', 'tumor', 'tunel', 'turismo', 'turno', 'tutor', 'ubicar', 'ufano', 'ultimo', 'umbral', 'unanimidad', 'uncion', 'undecimo', 'ungir', 'unico', 'unidad', 'unir', 'universo', 'uno', 'untar', 'una', 'urbano', 'urgente', 'usar', 'uso', 'usted', 'usual', 'utensilio', 'util', 'uva', 'vaca', 'vacaciones', 'vacio', 'vade', 'vagabundo', 'vagon', 'vainilla', 'valer', 'valiente', 'valle', 'valor', 'vals', 'vampiro', 'vanidad', 'vano', 'vapor', 'vara', 'variable', 'varon', 'vaso', 'vecino', 'veda', 'vegetal', 'veinte', 'vela', 'velar', 'velero', 'velocidad', 'venado', 'vender', 'veneno', 'venerar', 'venganza', 'venir', 'ventana', 'ventilar', 'ver', 'verano', 'verbo', 'verdad', 'verde', 'verdugo', 'vereda', 'vergonzoso', 'verificar', 'verso', 'verter', 'vestido', 'vestir', 'vez', 'via', 'viaje', 'vibora', 'vicio', 'victima', 'victoria', 'vid', 'vida', 'vidrio', 'viejo', 'viento', 'vientre', 'viernes', 'vigilar', 'vigor', 'villa', 'vinagre', 'vino', 'violencia', 'virgen', 'virtud', 'virus', 'visita', 'vista', 'vital', 'viuda', 'vivir', 'vocal', 'vocero', 'volador', 'volcan', 'volar', 'volcar', 'volver', 'volumen', 'voluntad', 'vomitar', 'votar', 'voz', 'vuelo', 'vuelta', 'yacer', 'yacimiento', 'yate', 'yema', 'yerno', 'yeso', 'yo', 'yodo', 'yogur', 'yugo', 'zafiro', 'zanahoria', 'zapato', 'zarza', 'zona', 'zorro', 'zumo', 'zurdo'] # ─── UTILS ─────────────────────────────────────────────────────────────────── def get_charset_size(upper, nums, syms): size = 26 if upper: size += 26 if nums: size += 10 if syms: size += len(SYMBOLS) return size def get_recommended_length(target_bits, charset_size): if charset_size <= 0: return 0 return math.ceil(target_bits / math.log2(charset_size)) def calculate_entropy(password): if not password: return 0 cs = 0 if any(c in string.ascii_lowercase for c in password): cs += 26 if any(c in string.ascii_uppercase for c in password): cs += 26 if any(c in string.digits for c in password): cs += 10 if any(c in string.punctuation for c in password): cs += 32 return round(len(password) * math.log2(cs), 2) if cs else 0 def check_strength(entropy): if entropy < 28: return "Muy debil", "#dc2626", "Segundos" elif entropy < 36: return "Debil", "#ea580c", "Minutos" elif entropy < 60: return "Media", "#ca8a04", "Horas/Dias" elif entropy < 80: return "Fuerte", "#16a34a", "Anos" elif entropy < 128: return "Muy fuerte", "#2563eb", "Decadas" elif entropy < 256: return "Extrema", "#7c3aed", "Siglos" else: return "Militar (256+)", "#581c87", "Milenios" # ─── CONTRASEÑAS ───────────────────────────────────────────────────────────── def generate_password_rigorous(length=16, use_uppercase=True, use_numbers=True, use_symbols=True): """ Genera la contraseña completa de forma aleatoria pura, luego verifica que cumpla los requisitos — sin insertar caracteres forzados que romperían la distribución uniforme. """ characters = string.ascii_lowercase if use_uppercase: characters += string.ascii_uppercase if use_numbers: characters += string.digits if use_symbols: characters += SYMBOLS for _ in range(10_000): pwd = "".join(secrets.choice(characters) for _ in range(length)) ok_lower = any(c in string.ascii_lowercase for c in pwd) ok_upper = (not use_uppercase) or any(c in string.ascii_uppercase for c in pwd) ok_num = (not use_numbers) or any(c in string.digits for c in pwd) ok_sym = (not use_symbols) or any(c in SYMBOLS for c in pwd) if ok_lower and ok_upper and ok_num and ok_sym: return pwd # fallback estadísticamente improbable return "".join(secrets.choice(characters) for _ in range(length)) def generate_password_256_bits(upper=True, nums=True, syms=True): cs = get_charset_size(upper, nums, syms) length = get_recommended_length(256, cs) return generate_password_rigorous(length, upper, nums, syms), length def generate_password_128_bits(upper=True, nums=True, syms=True): cs = get_charset_size(upper, nums, syms) length = get_recommended_length(128, cs) return generate_password_rigorous(length, upper, nums, syms), length # ─── TOKENS ────────────────────────────────────────────────────────────────── def generate_token(bits=256, format_type="hex"): num_bytes = bits // 8 rb = secrets.token_bytes(num_bytes) if format_type == "hex": return rb.hex() elif format_type == "base64": return base64.b64encode(rb).decode() elif format_type == "base64url": return base64.urlsafe_b64encode(rb).decode().rstrip("=") elif format_type == "binary": return bin(int.from_bytes(rb, "big"))[2:].zfill(bits) elif format_type == "uuid_style": h = rb.hex() if bits == 256: return f"{h[:8]}-{h[8:12]}-{h[12:16]}-{h[16:20]}-{h[20:32]}-{h[32:36]}-{h[36:48]}-{h[48:52]}-{h[52:64]}" elif bits == 128: return f"{h[:8]}-{h[8:12]}-{h[12:16]}-{h[16:20]}-{h[20:32]}" return h return rb.hex() # ─── CLAVES ────────────────────────────────────────────────────────────────── def generate_aes_key(): key = secrets.token_bytes(32) return { "hex": key.hex(), "base64": base64.b64encode(key).decode(), "base64url": base64.urlsafe_b64encode(key).decode().rstrip("="), "bytes": len(key), "bits": len(key) * 8, } def generate_hmac_key(bits=256): key = secrets.token_bytes(bits // 8) return {"hex": key.hex(), "base64": base64.b64encode(key).decode(), "bits": bits} # ─── AES-256-GCM (cifrado real) ────────────────────────────────────────────── def aes_gcm_encrypt(plaintext, password=""): try: if password: salt = secrets.token_bytes(32) kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt, iterations=600_000) key = kdf.derive(password.encode()) key_source = f"Derivada de contraseña (PBKDF2-SHA256, 600k iters)\nSalt: {salt.hex()}" else: key = secrets.token_bytes(32) key_source = "Clave aleatoria de 256 bits (CSPRNG)" aesgcm = AESGCM(key) nonce = secrets.token_bytes(12) # 96 bits — tamaño estándar GCM ciphertext = aesgcm.encrypt(nonce, plaintext.encode(), None) return { "ciphertext": base64.b64encode(nonce + ciphertext).decode(), "key_hex": key.hex(), "key_b64": base64.b64encode(key).decode(), "key_source": key_source, "nonce": nonce.hex(), "algorithm": "AES-256-GCM", "mode": "Authenticated Encryption (AEAD)", } except Exception as e: return {"error": str(e)} def aes_gcm_decrypt(ciphertext_b64, key_hex): try: key = bytes.fromhex(key_hex) data = base64.b64decode(ciphertext_b64) nonce = data[:12] ct = data[12:] aesgcm = AESGCM(key) plaintext = aesgcm.decrypt(nonce, ct, None) return {"plaintext": plaintext.decode(), "success": True} except Exception as e: return {"error": str(e), "success": False} # ─── ESTEGANOGRAFÍA AES-256-GCM + LSB ──────────────────────────────────────── END_MARKER = "1111111111111110" # 0xFFFE en binario — marcador de fin def steg_hide(image_bytes: bytes, message: str, password: str = "") -> "tuple[bytes, dict]": """ 1. Cifra el mensaje con AES-256-GCM (clave aleatoria o derivada de password) 2. Convierte el ciphertext Base64 a bits 3. Incrusta los bits en el LSB de los canales RGB del PNG Retorna (png_bytes, metadata_dict) """ # Cifrar primero enc = aes_gcm_encrypt(message, password) if "error" in enc: return None, {"error": enc["error"]} payload = enc["ciphertext"] # string Base64 key_hex = enc["key_hex"] binary = "".join(format(b, "08b") for b in payload.encode("utf-8")) binary += END_MARKER img = Image.open(io.BytesIO(image_bytes)).convert("RGB") pixels = list(img.getdata()) # Capacidad: 3 bits por pixel (R, G, B) capacity_bits = len(pixels) * 3 if len(binary) > capacity_bits: return None, {"error": f"Imagen demasiado pequeña. Necesitas {len(binary)} bits, capacidad: {capacity_bits}"} new_pixels = [] bit_idx = 0 for pixel in pixels: r, g, b = pixel if bit_idx < len(binary): r = (r & 0xFE) | int(binary[bit_idx]); bit_idx += 1 if bit_idx < len(binary): g = (g & 0xFE) | int(binary[bit_idx]); bit_idx += 1 if bit_idx < len(binary): b = (b & 0xFE) | int(binary[bit_idx]); bit_idx += 1 new_pixels.append((r, g, b)) img.putdata(new_pixels) buf = io.BytesIO() img.save(buf, format="PNG") return buf.getvalue(), { "key_hex": key_hex, "key_b64": enc["key_b64"], "nonce": enc["nonce"], "payload_bytes": len(payload.encode()), "bits_used": len(binary), "image_capacity": capacity_bits, "algorithm": "AES-256-GCM + LSB steganography", "key_source": enc["key_source"], } def steg_extract(image_bytes: bytes, key_hex: str) -> dict: """ 1. Extrae los bits LSB de los canales RGB 2. Reconstruye el payload Base64 hasta el END_MARKER 3. Descifra con AES-256-GCM usando key_hex """ img = Image.open(io.BytesIO(image_bytes)).convert("RGB") pixels = list(img.getdata()) bits = "" for pixel in pixels: for channel in pixel[:3]: bits += str(channel & 1) # Buscar END_MARKER reconstruyendo byte a byte chars = [] for i in range(0, len(bits) - 7, 8): byte = bits[i:i+8] chars.append(chr(int(byte, 2))) # Comprobar si los últimos 16 bits de chars forman el END_MARKER if len(chars) >= 2: last_two = "".join(format(ord(c), "08b") for c in chars[-2:]) if last_two == END_MARKER: chars = chars[:-2] # quitar los 2 chars del marcador break if not chars: return {"error": "No se encontró marcador de fin. ¿Imagen sin datos ocultos?"} payload_b64 = "".join(chars) # Descifrar dec = aes_gcm_decrypt(payload_b64, key_hex) if not dec.get("success"): return {"error": f"Descifrado fallido: {dec.get('error', 'clave incorrecta')}"} return {"plaintext": dec["plaintext"], "success": True} # ─── ARGON2id ──────────────────────────────────────────────────────────────── def argon2id_hash(password): try: h = nacl.pwhash.str( password.encode(), opslimit=nacl.pwhash.OPSLIMIT_MODERATE, memlimit=nacl.pwhash.MEMLIMIT_MODERATE, ) return { "hash": h.decode(), "algorithm": "Argon2id", "params": "opslimit=MODERATE, memlimit=MODERATE", "note": "Formato estándar: $argon2id$v=19$m=...$salt$hash", } except Exception as e: return {"error": str(e)} def argon2id_verify(password, hash_str): try: nacl.pwhash.verify(hash_str.encode(), password.encode()) return {"valid": True} except Exception as e: return {"valid": False, "error": str(e)} def derive_key_argon2(password, salt=None, length=32): try: if salt is None: salt = secrets.token_bytes(16) key = nacl.pwhash.kdf( length, password.encode(), salt, opslimit=nacl.pwhash.OPSLIMIT_MODERATE, memlimit=nacl.pwhash.MEMLIMIT_MODERATE, alg=nacl.pwhash.ALG_ARGON2ID13, ) return { "key_hex": key.hex(), "key_b64": base64.b64encode(key).decode(), "salt": salt.hex(), "length": length, "algorithm": "Argon2id (KDF)", } except Exception as e: return {"error": str(e)} # ─── DICEWARE ──────────────────────────────────────────────────────────────── def generate_diceware(num_words=6, separator="-"): words = [secrets.choice(DICEWARE_WORDLIST) for _ in range(num_words)] passphrase = separator.join(words) entropy = round(num_words * math.log2(len(DICEWARE_WORDLIST)), 2) return { "passphrase": passphrase, "words": num_words, "entropy": entropy, "wordlist_size": len(DICEWARE_WORDLIST), "bits_per_word": round(math.log2(len(DICEWARE_WORDLIST)), 2), } # ─── Ed25519 ───────────────────────────────────────────────────────────────── def generate_ed25519_keys(): try: priv_key = Ed25519PrivateKey.generate() pub_key = priv_key.public_key() priv_pem = priv_key.private_bytes( serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption(), ).decode() pub_pem = pub_key.public_bytes( serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo, ).decode() priv_hex = priv_key.private_bytes( serialization.Encoding.Raw, serialization.PrivateFormat.Raw, serialization.NoEncryption(), ).hex() pub_hex = pub_key.public_bytes( serialization.Encoding.Raw, serialization.PublicFormat.Raw, ).hex() return { "private_pem": priv_pem, "public_pem": pub_pem, "private_hex": priv_hex, "public_hex": pub_hex, "algorithm": "Ed25519", "key_size": "256 bits", } except Exception as e: return {"error": str(e)} def ed25519_sign(message, private_key_hex): try: priv = Ed25519PrivateKey.from_private_bytes(bytes.fromhex(private_key_hex)) sig = priv.sign(message.encode()) return { "signature": sig.hex(), "signature_b64": base64.b64encode(sig).decode(), } except Exception as e: return {"error": str(e)} def ed25519_verify(message, signature_hex, public_key_hex): try: pub = Ed25519PublicKey.from_public_bytes(bytes.fromhex(public_key_hex)) pub.verify(bytes.fromhex(signature_hex), message.encode()) return {"valid": True, "message": "✅ Firma válida"} except Exception as e: return {"valid": False, "message": f"❌ Firma inválida: {e}"} # ─── X25519 ────────────────────────────────────────────────────────────────── def generate_x25519_keys(): try: priv = X25519PrivateKey.generate() pub = priv.public_key() return { "private_hex": priv.private_bytes( serialization.Encoding.Raw, serialization.PrivateFormat.Raw, serialization.NoEncryption()).hex(), "public_hex": pub.public_bytes( serialization.Encoding.Raw, serialization.PublicFormat.Raw).hex(), "algorithm": "X25519 (ECDH)", "key_size": "256 bits", } except Exception as e: return {"error": str(e)} def x25519_shared_secret(private_hex, public_hex): try: priv = X25519PrivateKey.from_private_bytes(bytes.fromhex(private_hex)) pub = X25519PublicKey.from_public_bytes(bytes.fromhex(public_hex)) shared = priv.exchange(pub) return { "shared_secret_hex": shared.hex(), "shared_secret_b64": base64.b64encode(shared).decode(), "length": len(shared), "bits": len(shared) * 8, } except Exception as e: return {"error": str(e)} # ─── API SECRETS ───────────────────────────────────────────────────────────── def generate_jwt_secret(): s = secrets.token_urlsafe(43) return {"secret": s, "length_chars": len(s), "estimated_bits": round(len(s) * 6, 1), "type": "JWT Secret"} def generate_api_key(): prefix = "sk_" + secrets.token_hex(4) key = secrets.token_urlsafe(32) return {"api_key": f"{prefix}_{key}", "prefix": prefix, "type": "API Key"} def generate_session_token(): t = secrets.token_urlsafe(32) return {"token": t, "length": len(t), "estimated_bits": round(len(t) * 6, 1), "type": "Session Token"} def generate_oauth_secret(): s = secrets.token_urlsafe(48) return {"secret": s, "length": len(s), "estimated_bits": round(len(s) * 6, 1), "type": "OAuth 2.0 Secret"} # ─── AUDITORÍA ─────────────────────────────────────────────────────────────── def run_crypto_tests(): results = [] # T1 — CSPRNG básico try: assert len(secrets.token_bytes(32)) == 32 results.append("✅ T1 secrets.token_bytes(32) → 32 bytes OK") except Exception as e: results.append(f"❌ T1 {e}") # T2 — Contraseña con distribución uniforme rigurosa try: pwd = generate_password_rigorous(20, True, True, True) ent = calculate_entropy(pwd) # verifica que TODOS los charsets estén presentes (sin inserción forzada) assert any(c in string.ascii_lowercase for c in pwd) assert any(c in string.ascii_uppercase for c in pwd) assert any(c in string.digits for c in pwd) assert any(c in SYMBOLS for c in pwd) assert ent > 100 results.append(f"✅ T2 Password 20 chars → {ent} bits, distribución uniforme OK") except Exception as e: results.append(f"❌ T2 {e}") # T3 — AES-256-GCM roundtrip real try: key = AESGCM.generate_key(bit_length=256) aesgcm = AESGCM(key) nonce = secrets.token_bytes(12) plaintext = "Mensaje de prueba — ñoño 🔐" ct = aesgcm.encrypt(nonce, plaintext.encode(), None) pt = aesgcm.decrypt(nonce, ct, None) assert pt.decode() == plaintext results.append("✅ T3 AES-256-GCM encrypt/decrypt roundtrip OK") except Exception as e: results.append(f"❌ T3 {e}") # T4 — AES-GCM detección de tampering try: key = AESGCM.generate_key(bit_length=256) aesgcm = AESGCM(key) nonce = secrets.token_bytes(12) ct = aesgcm.encrypt(nonce, b"original", None) tampered = bytearray(ct); tampered[0] ^= 0xFF try: aesgcm.decrypt(nonce, bytes(tampered), None) results.append("❌ T4 AES-GCM debería haber rechazado datos alterados") except Exception: results.append("✅ T4 AES-GCM detecta tampering correctamente") except Exception as e: results.append(f"❌ T4 {e}") # T5 — Ed25519 sign/verify try: sk = Ed25519PrivateKey.generate() pk = sk.public_key() msg = b"test message" sig = sk.sign(msg) pk.verify(sig, msg) results.append("✅ T5 Ed25519 sign/verify OK") except Exception as e: results.append(f"❌ T5 {e}") # T6 — Ed25519 rechaza firma alterada try: sk = Ed25519PrivateKey.generate() pk = sk.public_key() sig = sk.sign(b"data") bad_sig = bytearray(sig); bad_sig[0] ^= 0xFF try: pk.verify(bytes(bad_sig), b"data") results.append("❌ T6 Ed25519 debería rechazar firma inválida") except Exception: results.append("✅ T6 Ed25519 rechaza firma alterada OK") except Exception as e: results.append(f"❌ T6 {e}") # T7 — X25519 shared secret simétrico try: sk1, pk1 = X25519PrivateKey.generate(), None sk2 = X25519PrivateKey.generate() pk1 = sk1.public_key() pk2 = sk2.public_key() assert sk1.exchange(pk2) == sk2.exchange(pk1) results.append("✅ T7 X25519 ECDH → shared secrets idénticos OK") except Exception as e: results.append(f"❌ T7 {e}") # T8 — Argon2id hash/verify try: pwd = "test_password" h = nacl.pwhash.str(pwd.encode()) nacl.pwhash.verify(h, pwd.encode()) results.append("✅ T8 Argon2id hash/verify OK") except Exception as e: results.append(f"❌ T8 {e}") # T9 — Argon2id rechaza contraseña incorrecta try: h = nacl.pwhash.str(b"correcta") try: nacl.pwhash.verify(h, b"incorrecta") results.append("❌ T9 Argon2id debería rechazar password incorrecto") except Exception: results.append("✅ T9 Argon2id rechaza password incorrecto OK") except Exception as e: results.append(f"❌ T9 {e}") # T10 — Diceware entropía mínima try: dw = generate_diceware(6) assert dw["entropy"] > 60 results.append(f"✅ T10 Diceware 6 palabras → {dw['entropy']} bits OK") except Exception as e: results.append(f"❌ T10 {e}") # T11 — Token 256 bits hex tiene longitud correcta try: t = generate_token(256, "hex") assert len(t) == 64 # 256 bits / 4 bits_per_hex_char results.append(f"✅ T11 Token hex 256 bits → {len(t)} chars OK") except Exception as e: results.append(f"❌ T11 {e}") # T12 — PBKDF2 → AES-GCM round trip con contraseña try: enc = aes_gcm_encrypt("hello world", "mi_password") dec = aes_gcm_decrypt(enc["ciphertext"], enc["key_hex"]) assert dec["success"] and dec["plaintext"] == "hello world" results.append("✅ T12 PBKDF2+AES-GCM encrypt/decrypt con password OK") except Exception as e: results.append(f"❌ T12 {e}") passed = sum(1 for r in results if r.startswith("✅")) total = len(results) results.append(f"\n{'='*48}") results.append(f"Resultado: {passed}/{total} tests pasados") if passed == total: results.append("🎉 Suite completa — sin fallos") return "\n".join(results) def get_system_info(): info = { "app_version": VERSION, "python_version": PYTHON_VERSION, "rng_source": RNG_SOURCE, "cryptography_lib": "cryptography (AES-256-GCM, Ed25519, X25519, PBKDF2)", "pynacl_version": f"PyNaCl {nacl.__version__} (Argon2id via libsodium)", "backend": str(default_backend()), "symbols_charset": f"{SYMBOLS!r} ({len(SYMBOLS)} chars)", "processing_note": "Todo se procesa en la instancia donde corre esta aplicación. Sin envío externo.", "timestamp": datetime.now().isoformat(), } return json.dumps(info, indent=2) # ─── CSS / UI ───────────────────────────────────────────────────────────────── css = """ .mono { font-family: 'Courier New', monospace; font-size: 0.9em; background: #f8f9fa; padding: 12px; border-radius: 8px; word-break: break-all; border: 1px solid #e9ecef; } .strength-badge { font-weight: bold; padding: 8px 16px; border-radius: 6px; display: inline-block; margin-top: 6px; color: white; } .info-box { background: #e7f3ff; padding: 12px; border-radius: 8px; border-left: 4px solid #2196F3; margin: 6px 0; } .ok-box { background: #e8f5e9; padding: 12px; border-radius: 8px; border-left: 4px solid #4caf50; margin: 6px 0; } .crypto-box { background: #f3e5f5; padding: 12px; border-radius: 8px; border-left: 4px solid #9c27b0; margin: 6px 0; } """ # ─── APP ───────────────────────────────────────────────────────────────────── with gr.Blocks(title="CryptoBox Pro", css=css) as demo: gr.Markdown(f""" # 🔐 CryptoBox Pro v{VERSION} Caja de herramientas criptográfica. Todo se procesa en la instancia local donde corre esta app. `secrets` (CSPRNG) · `cryptography` · `PyNaCl / libsodium` """) # ── TAB 1 — Contraseñas ────────────────────────────────────────────────── with gr.Tab("Contraseñas"): gr.Markdown("### Generador con distribución uniforme rigurosa") with gr.Row(): with gr.Column(scale=1): pwd_length = gr.Slider(8, 64, value=16, step=1, label="Longitud") pwd_upper = gr.Checkbox(value=True, label="Mayúsculas A-Z") pwd_nums = gr.Checkbox(value=True, label="Números 0-9") pwd_syms = gr.Checkbox(value=True, label=f"Símbolos ({SYMBOLS[:12]}…)") with gr.Row(): btn_pwd = gr.Button("Generar", variant="primary") btn_pwd_128 = gr.Button("~128 bits", variant="secondary") btn_pwd_256 = gr.Button("~256 bits", variant="secondary") with gr.Column(scale=2): pwd_output = gr.Textbox(label="Contraseña", interactive=False, elem_classes=["mono"]) pwd_entropy = gr.Textbox(label="Entropía real", interactive=False) pwd_strength = gr.HTML() pwd_info = gr.HTML() def _pwd_render(pwd, upper, nums, syms): entropy = calculate_entropy(pwd) strength, color, desc = check_strength(entropy) charset = get_charset_size(upper, nums, syms) badge = f'
{strength}
' info = f'
Charset: {charset} chars · {math.log2(charset):.2f} bits/char
' return pwd, f"{entropy} bits ({desc})", badge, info def on_pwd_generate(length, upper, nums, syms): return _pwd_render(generate_password_rigorous(length, upper, nums, syms), upper, nums, syms) def on_pwd_128(upper, nums, syms): pwd, length = generate_password_128_bits(upper, nums, syms) return *_pwd_render(pwd, upper, nums, syms), length def on_pwd_256(upper, nums, syms): pwd, length = generate_password_256_bits(upper, nums, syms) return *_pwd_render(pwd, upper, nums, syms), length btn_pwd.click(on_pwd_generate, [pwd_length, pwd_upper, pwd_nums, pwd_syms], [pwd_output, pwd_entropy, pwd_strength, pwd_info]) btn_pwd_128.click(on_pwd_128, [pwd_upper, pwd_nums, pwd_syms], [pwd_output, pwd_entropy, pwd_strength, pwd_info, pwd_length]) btn_pwd_256.click(on_pwd_256, [pwd_upper, pwd_nums, pwd_syms], [pwd_output, pwd_entropy, pwd_strength, pwd_info, pwd_length]) # ── TAB 2 — Tokens ─────────────────────────────────────────────────────── with gr.Tab("Tokens"): with gr.Row(): with gr.Column(): token_bits = gr.Radio([128, 256, 512], value=256, label="Bits") token_format = gr.Radio( ["hex", "base64", "base64url", "binary", "uuid_style"], value="hex", label="Formato") btn_token = gr.Button("Generar token", variant="primary") btn_token_5 = gr.Button("Generar ×5", variant="secondary") with gr.Column(): token_output = gr.Textbox(label="Token", interactive=False, lines=3, elem_classes=["mono"]) token_multiple = gr.Textbox(label="Múltiples tokens", interactive=False, lines=10, elem_classes=["mono"]) btn_token.click(generate_token, [token_bits, token_format], [token_output]) btn_token_5.click(lambda b, f: "\n".join(generate_token(b, f) for _ in range(5)), [token_bits, token_format], [token_multiple]) # ── TAB 3 — Claves ─────────────────────────────────────────────────────── with gr.Tab("Claves"): with gr.Row(): btn_aes = gr.Button("AES-256 Key", variant="primary") btn_hmac = gr.Button("HMAC-256 Key", variant="secondary") key_output = gr.Textbox(label="Clave generada", interactive=False, lines=6, elem_classes=["mono"]) def on_aes(): k = generate_aes_key() return (f"Hex: {k['hex']}\n" f"Base64: {k['base64']}\n" f"Base64URL: {k['base64url']}\n" f"Tamaño: {k['bytes']} bytes = {k['bits']} bits\n" f"Uso: AES-256-GCM, AES-256-CBC") def on_hmac(): k = generate_hmac_key(256) return (f"Hex: {k['hex']}\n" f"Base64: {k['base64']}\n" f"Tamaño: {k['bits']} bits\n" f"Uso: HMAC-SHA256, JWT HS256") btn_aes.click(on_aes, [], [key_output]) btn_hmac.click(on_hmac, [], [key_output]) # ── TAB 4 — Cifrado AES-256-GCM ────────────────────────────────────────── with gr.Tab("Cifrado AES-GCM"): gr.Markdown("### Cifrado autenticado AES-256-GCM (AEAD)") with gr.Row(): with gr.Column(): enc_msg = gr.Textbox(label="Mensaje a cifrar", lines=3) enc_pwd = gr.Textbox(label="Contraseña opcional (si vacío → clave aleatoria)", type="password") btn_encrypt = gr.Button("Cifrar", variant="primary") with gr.Column(): enc_output = gr.Textbox(label="Resultado", interactive=False, lines=9, elem_classes=["mono"]) with gr.Row(): with gr.Column(): dec_ciphertext = gr.Textbox(label="Ciphertext (Base64)", lines=2) dec_key = gr.Textbox(label="Clave (Hex)") btn_decrypt = gr.Button("Descifrar", variant="primary") with gr.Column(): dec_output = gr.Textbox(label="Mensaje descifrado", interactive=False, elem_classes=["mono"]) def on_encrypt(msg, pwd): if not msg: return "⚠️ Introduce un mensaje" r = aes_gcm_encrypt(msg, pwd) if "error" in r: return f"❌ {r['error']}" return (f"Algoritmo: {r['algorithm']} — {r['mode']}\n\n" f"Ciphertext (Base64):\n{r['ciphertext']}\n\n" f"Clave (Hex):\n{r['key_hex']}\n\n" f"Clave (Base64):\n{r['key_b64']}\n\n" f"Nonce: {r['nonce']}\n{r['key_source']}") def on_decrypt(ct, key): if not ct or not key: return "⚠️ Introduce ciphertext y clave" r = aes_gcm_decrypt(ct, key) if r.get("success"): return f"✅ {r['plaintext']}" return f"❌ {r.get('error', 'Error desconocido')}" btn_encrypt.click(on_encrypt, [enc_msg, enc_pwd], [enc_output]) btn_decrypt.click(on_decrypt, [dec_ciphertext, dec_key], [dec_output]) # ── TAB 5 — Argon2id ───────────────────────────────────────────────────── with gr.Tab("Argon2id"): gr.Markdown("### Hash de contraseñas y derivación de claves") with gr.Row(): with gr.Column(): argon_pwd = gr.Textbox(label="Contraseña", type="password") with gr.Row(): btn_argon_hash = gr.Button("Hash Argon2id", variant="primary") btn_argon_derive = gr.Button("Derivar clave", variant="secondary") gr.Markdown("**Verificar hash**") argon_verify_pwd = gr.Textbox(label="Contraseña a verificar", type="password") argon_verify_hash = gr.Textbox(label="Hash Argon2id") btn_argon_verify = gr.Button("Verificar", variant="secondary") with gr.Column(): argon_output = gr.Textbox(label="Resultado", interactive=False, lines=10, elem_classes=["mono"]) def on_argon_hash(pwd): if not pwd: return "⚠️ Introduce una contraseña" r = argon2id_hash(pwd) if "error" in r: return f"❌ {r['error']}" return f"Algoritmo: {r['algorithm']}\n{r['params']}\n{r['note']}\n\n{r['hash']}" def on_argon_derive(pwd): if not pwd: return "⚠️ Introduce una contraseña" r = derive_key_argon2(pwd) if "error" in r: return f"❌ {r['error']}" return (f"Algoritmo: {r['algorithm']}\n" f"Clave Hex: {r['key_hex']}\n" f"Clave B64: {r['key_b64']}\n" f"Salt: {r['salt']}\n" f"Longitud: {r['length']} bytes") def on_argon_verify(pwd, hash_str): if not pwd or not hash_str: return "⚠️ Ambos campos requeridos" r = argon2id_verify(pwd, hash_str) return "✅ Contraseña CORRECTA" if r["valid"] else f"❌ Contraseña INCORRECTA — {r.get('error','')}" btn_argon_hash.click(on_argon_hash, [argon_pwd], [argon_output]) btn_argon_derive.click(on_argon_derive, [argon_pwd], [argon_output]) btn_argon_verify.click(on_argon_verify, [argon_verify_pwd, argon_verify_hash], [argon_output]) # ── TAB 6 — Diceware ───────────────────────────────────────────────────── with gr.Tab("Diceware"): gr.Markdown("### Passphrase memorable de alta entropía") with gr.Row(): with gr.Column(): dice_words = gr.Slider(4, 12, value=6, step=1, label="Palabras") dice_sep = gr.Radio(["-", " ", "_", "."], value="-", label="Separador") btn_dice = gr.Button("Generar passphrase", variant="primary") with gr.Column(): dice_output = gr.Textbox(label="Passphrase", interactive=False, elem_classes=["mono"]) dice_info = gr.HTML() def on_dice(words, sep): r = generate_diceware(words, sep) info = (f'
' f'Entropía: {r["entropy"]} bits · ' f'Bits/palabra: {r["bits_per_word"]} · ' f'Lista: {r["wordlist_size"]} palabras (español)
') return r["passphrase"], info btn_dice.click(on_dice, [dice_words, dice_sep], [dice_output, dice_info]) # ── TAB 7 — Ed25519 ────────────────────────────────────────────────────── with gr.Tab("Ed25519"): gr.Markdown("### Firmas digitales") with gr.Row(): with gr.Column(): btn_gen_ed = gr.Button("Generar par de claves", variant="primary") ed_keys = gr.Textbox(label="Claves", interactive=False, lines=12, elem_classes=["mono"]) with gr.Column(): ed_msg = gr.Textbox(label="Mensaje a firmar") ed_priv = gr.Textbox(label="Clave privada (Hex)") btn_sign = gr.Button("Firmar", variant="primary") ed_sig = gr.Textbox(label="Firma", interactive=False, elem_classes=["mono"]) with gr.Row(): with gr.Column(): ed_verify_msg = gr.Textbox(label="Mensaje") ed_verify_sig = gr.Textbox(label="Firma (Hex)") ed_verify_pub = gr.Textbox(label="Clave pública (Hex)") btn_verify = gr.Button("Verificar firma", variant="primary") with gr.Column(): ed_verify_result = gr.HTML() def on_gen_ed(): k = generate_ed25519_keys() if "error" in k: return f"❌ {k['error']}" return (f"Algoritmo: {k['algorithm']} ({k['key_size']})\n\n" f"Privada (Hex):\n{k['private_hex']}\n\n" f"Pública (Hex):\n{k['public_hex']}\n\n" f"— PEM —\n{k['private_pem']}\n{k['public_pem']}") def on_sign(msg, priv): if not msg or not priv: return "⚠️ Mensaje y clave privada requeridos" r = ed25519_sign(msg, priv) if "error" in r: return f"❌ {r['error']}" return f"Hex: {r['signature']}\nBase64: {r['signature_b64']}" def on_verify(msg, sig, pub): if not all([msg, sig, pub]): return '
⚠️ Todos los campos son requeridos
' r = ed25519_verify(msg, sig, pub) color = "#4caf50" if r["valid"] else "#f44336" return f'
{r["message"]}
' btn_gen_ed.click(on_gen_ed, [], [ed_keys]) btn_sign.click(on_sign, [ed_msg, ed_priv], [ed_sig]) btn_verify.click(on_verify, [ed_verify_msg, ed_verify_sig, ed_verify_pub], [ed_verify_result]) # ── TAB 8 — X25519 ─────────────────────────────────────────────────────── with gr.Tab("X25519"): gr.Markdown("### Intercambio de claves ECDH") with gr.Row(): with gr.Column(): btn_gen_x1 = gr.Button("Generar par A", variant="primary") x1_keys = gr.Textbox(label="Par A", interactive=False, lines=4, elem_classes=["mono"]) with gr.Column(): btn_gen_x2 = gr.Button("Generar par B", variant="primary") x2_keys = gr.Textbox(label="Par B", interactive=False, lines=4, elem_classes=["mono"]) with gr.Row(): with gr.Column(): x_priv = gr.Textbox(label="Tu clave privada (Hex)") x_pub_other = gr.Textbox(label="Clave pública del otro (Hex)") btn_shared = gr.Button("Calcular secreto compartido", variant="primary") with gr.Column(): x_shared = gr.Textbox(label="Secreto compartido", interactive=False, lines=5, elem_classes=["mono"]) def on_gen_x(): k = generate_x25519_keys() if "error" in k: return f"❌ {k['error']}" return f"Privada: {k['private_hex']}\nPública: {k['public_hex']}\n{k['algorithm']} · {k['key_size']}" def on_shared(priv, pub): if not priv or not pub: return "⚠️ Ambas claves requeridas" r = x25519_shared_secret(priv, pub) if "error" in r: return f"❌ {r['error']}" return (f"Hex: {r['shared_secret_hex']}\n" f"Base64: {r['shared_secret_b64']}\n" f"Tamaño: {r['length']} bytes = {r['bits']} bits\n\n" f"Usa este valor directamente como clave AES-256.") btn_gen_x1.click(on_gen_x, [], [x1_keys]) btn_gen_x2.click(on_gen_x, [], [x2_keys]) btn_shared.click(on_shared, [x_priv, x_pub_other], [x_shared]) # ── TAB 9 — API Secrets ─────────────────────────────────────────────────── with gr.Tab("API Secrets"): with gr.Row(): btn_jwt = gr.Button("JWT Secret", variant="primary") btn_api = gr.Button("API Key", variant="primary") btn_session = gr.Button("Session Token", variant="secondary") btn_oauth = gr.Button("OAuth Secret", variant="secondary") api_output = gr.Textbox(label="Secreto generado", interactive=False, lines=5, elem_classes=["mono"]) btn_jwt.click(lambda: (lambda r: f"{r['type']}\n{r['secret']}\n\n{r['length_chars']} chars · ~{r['estimated_bits']} bits")(generate_jwt_secret()), [], [api_output]) btn_api.click(lambda: (lambda r: f"{r['type']}\n{r['api_key']}")(generate_api_key()), [], [api_output]) btn_session.click(lambda: (lambda r: f"{r['type']}\n{r['token']}\n\n{r['length']} chars · ~{r['estimated_bits']} bits")(generate_session_token()), [], [api_output]) btn_oauth.click(lambda: (lambda r: f"{r['type']}\n{r['secret']}\n\n{r['length']} chars · ~{r['estimated_bits']} bits")(generate_oauth_secret()), [], [api_output]) # ── TAB 10 — Esteganografía ─────────────────────────────────────────────── with gr.Tab("Esteganografía"): gr.Markdown(""" ### AES-256-GCM + LSB Steganography Cifra el mensaje con AES-256-GCM y esconde el ciphertext en el bit menos significativo de cada canal RGB de la imagen PNG. Sin la clave, la imagen parece normal. """) with gr.Row(): # ── OCULTAR ────────────────────────────────────────────────────── with gr.Column(): gr.Markdown("#### 🔒 Ocultar mensaje") steg_image_in = gr.Image(label="Imagen portadora (PNG recomendado)", type="filepath", sources=["upload"]) steg_msg_in = gr.Textbox(label="Mensaje a ocultar", lines=4) steg_pwd_in = gr.Textbox(label="Contraseña (opcional; si vacío → clave aleatoria)", type="password") btn_steg_hide = gr.Button("Cifrar + Ocultar", variant="primary") steg_key_out = gr.Textbox(label="🔑 Clave AES-256 (HEX) — guárdala", interactive=False, elem_classes=["mono"]) steg_info_out = gr.HTML() steg_image_out = gr.Image(label="Imagen con mensaje oculto", type="filepath", interactive=False) # ── EXTRAER ────────────────────────────────────────────────────── with gr.Column(): gr.Markdown("#### 🔓 Extraer mensaje") steg_image_ext = gr.Image(label="Imagen con datos ocultos", type="filepath", sources=["upload"]) steg_key_in = gr.Textbox(label="Clave AES-256 (HEX)") btn_steg_ext = gr.Button("Extraer + Descifrar", variant="primary") steg_result_out = gr.Textbox(label="Mensaje recuperado", interactive=False, lines=6, elem_classes=["mono"]) # ── Handlers ───────────────────────────────────────────────────────── def on_steg_hide(image_path, message, password): if not image_path: return "", '
⚠️ Sube una imagen
', None if not message: return "", '
⚠️ Escribe un mensaje
', None with open(image_path, "rb") as f: image_bytes = f.read() png_bytes, meta = steg_hide(image_bytes, message, password) if png_bytes is None: return "", f'
❌ {meta["error"]}
', None # Guardar imagen resultante en temp — compatible Linux/Windows/macOS out_path = os.path.join(tempfile.gettempdir(), "steg_output.png") with open(out_path, "wb") as f: f.write(png_bytes) info_html = ( f'
' f'Algoritmo: {meta["algorithm"]}
' f'Fuente clave: {meta["key_source"]}
' f'Nonce: {meta["nonce"]}
' f'Bits usados: {meta["bits_used"]} / {meta["image_capacity"]}
' f'Ocupación: {meta["bits_used"]/meta["image_capacity"]*100:.2f}%' f'
' ) return meta["key_hex"], info_html, out_path def on_steg_extract(image_path, key_hex): if not image_path: return "⚠️ Sube una imagen" if not key_hex: return "⚠️ Introduce la clave AES-256 en hex" with open(image_path, "rb") as f: image_bytes = f.read() result = steg_extract(image_bytes, key_hex) if not result.get("success"): return f"❌ {result.get('error', 'Error desconocido')}" return result["plaintext"] btn_steg_hide.click( on_steg_hide, [steg_image_in, steg_msg_in, steg_pwd_in], [steg_key_out, steg_info_out, steg_image_out], ) btn_steg_ext.click( on_steg_extract, [steg_image_ext, steg_key_in], [steg_result_out], ) # ── TAB 11 — Auditoría ─────────────────────────────────────────────────── with gr.Tab("Auditoría"): gr.Markdown("### Información del sistema y suite de tests criptográficos") with gr.Row(): btn_info = gr.Button("Info del sistema", variant="secondary") btn_tests = gr.Button("Ejecutar tests", variant="primary") audit_output = gr.Textbox(label="Resultado", interactive=False, lines=25, elem_classes=["mono"]) btn_info.click(get_system_info, [], [audit_output]) btn_tests.click(run_crypto_tests, [], [audit_output]) gr.Markdown(f"---\nCryptoBox Pro v{VERSION} · Python {PYTHON_VERSION} · cryptography · PyNaCl") if __name__ == "__main__": demo.launch()