Bx / app.py
Scrappy-Doo's picture
Update app.py
c323fa3 verified
Raw
History Blame Contribute Delete
69.8 kB
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'<div class="strength-badge" style="background:{color};">{strength}</div>'
info = f'<div class="info-box">Charset: {charset} chars Β· {math.log2(charset):.2f} bits/char</div>'
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'<div class="info-box">'
f'<b>EntropΓ­a:</b> {r["entropy"]} bits Β· '
f'<b>Bits/palabra:</b> {r["bits_per_word"]} Β· '
f'<b>Lista:</b> {r["wordlist_size"]} palabras (espaΓ±ol)</div>')
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 '<div class="info-box">⚠️ Todos los campos son requeridos</div>'
r = ed25519_verify(msg, sig, pub)
color = "#4caf50" if r["valid"] else "#f44336"
return f'<div class="strength-badge" style="background:{color};">{r["message"]}</div>'
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 "", '<div class="info-box">⚠️ Sube una imagen</div>', None
if not message:
return "", '<div class="info-box">⚠️ Escribe un mensaje</div>', 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'<div class="info-box">❌ {meta["error"]}</div>', 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'<div class="ok-box">'
f'<b>Algoritmo:</b> {meta["algorithm"]}<br>'
f'<b>Fuente clave:</b> {meta["key_source"]}<br>'
f'<b>Nonce:</b> {meta["nonce"]}<br>'
f'<b>Bits usados:</b> {meta["bits_used"]} / {meta["image_capacity"]}<br>'
f'<b>OcupaciΓ³n:</b> {meta["bits_used"]/meta["image_capacity"]*100:.2f}%'
f'</div>'
)
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()