Last active
July 29, 2022 13:59
-
-
Save knkillname/6d4ff2e3aaedb52bc4a659083dcdd463 to your computer and use it in GitHub Desktop.
Generador de contraseñas fáciles de recordar
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Autor: Mario Abarca | |
""" | |
Aplicación que genera de contraseñas fáciles de recordar mezclando | |
palabras en español y símbolos. | |
""" | |
import abc | |
import collections | |
import itertools | |
import math | |
import pathlib | |
import random | |
import string | |
import tkinter as tk | |
from tkinter import ttk | |
from urllib import request | |
URL_DICCIONARIO = ( | |
"https://raw.githubusercontent.com/elastic/hunspell/" | |
"master/dicts/es_MX/es_MX.dic" | |
) | |
def obtener_palabras(nombre_archivo="palabras.txt"): | |
""" | |
Lee una lista de palabras desde un archivo de texto. Si el archivo | |
no existe lo crea usando las 10 000 palabras más comunes según el | |
corpus de la RAE. | |
""" | |
ruta = pathlib.Path(nombre_archivo) | |
if not ruta.exists(): | |
with request.urlopen(URL_DICCIONARIO) as respuesta: | |
contenidos = respuesta.read().decode("latin1") | |
lineas = contenidos.splitlines()[1:] | |
palabras = [linea.split("/", maxsplit=1)[0] for linea in lineas] | |
with ruta.open("wt", encoding="utf8") as archivo: | |
archivo.writelines(palabra + "\n" for palabra in palabras) | |
with ruta.open(encoding="utf8") as archivo: | |
return [linea.strip() for linea in archivo.read().splitlines()] | |
def dificultad_temporal(n_bits, intentos_por_segundo=1_000): | |
segundos = math.pow(2, n_bits) / intentos_por_segundo | |
if round(segundos) < 60: | |
return f"{round(segundos)} segundos" | |
minutos = segundos / 60 | |
if round(minutos) < 60: | |
return f"{round(segundos)} minutos" | |
horas = minutos / 60 | |
if round(horas) < 24: | |
return f"{round(horas)} horas" | |
dias = horas / 24 | |
if round(dias) < 7: | |
return f"{round(dias)} días" | |
semanas = dias / 7 | |
if round(semanas) <= 4: | |
return f"{round(semanas)} semanas" | |
meses = dias / 30.420524691358025 | |
if round(meses) < 12: | |
return f"{round(meses)} meses" | |
anios = segundos / 3.154e7 | |
if round(anios) < 100: | |
return f"{round(anios)} años" | |
siglos = anios / 100 | |
if siglos < 10: | |
return f"{round(siglos)} siglos" | |
milenios = siglos / 10 | |
if milenios < 1000: | |
return f"{round(milenios)} milenios" | |
millones = milenios / 1000 | |
return f"{round(millones)} millones de años" | |
class GeneradorDeContrasenas(abc.ABC): | |
""" | |
Clase abstracta para las estrategias de generación de contraseñas. | |
""" | |
def __init__(self): | |
self._random = random.SystemRandom() | |
def sacar_al_azar(self, elementos): | |
""" | |
Sacar y devolver al azar un elemento de una lista. | |
""" | |
if len(elementos) > 1: | |
i = self._random.randrange(len(elementos)) | |
elementos[i], elementos[-1] = elementos[-1], elementos[i] | |
return elementos.pop() | |
@abc.abstractmethod | |
def generar_contrasena(self, longitud=16): | |
""" | |
Método para generar la contraseña. | |
""" | |
raise NotImplementedError | |
class FacilDeRecordar(GeneradorDeContrasenas): | |
""" | |
Estrategia que genera contraseñas fáciles de recordar. | |
""" | |
def __init__(self, palabras, simbolos): | |
super().__init__() | |
self.palabras = tuple(palabras) # Reservorio de palabras | |
self.simbolos = tuple(simbolos) # Reservorio de símbolos | |
self.min_letras = 2 / 3 # Proporción mínima de letras en la contraseña | |
self.max_letras = 3 / 4 # Proporción máxima de letras en la contraseña | |
def generar_frase(self, longitud_min, longitud_max): | |
""" | |
Genera una lista de palabras al azar tal que la suma de sus | |
longitudes está en un intervalo determinado. | |
""" | |
if longitud_min > longitud_max: | |
raise ValueError() | |
urna = [palabra for palabra in self.palabras if len(palabra) <= longitud_max] | |
reserva, cuenta = collections.deque(), 0 # Reserva de palabras | |
while cuenta < longitud_min: | |
# Sacar una palabra de la urna y meterla a la reserva | |
palabra_entrante = self.sacar_al_azar(urna) | |
reserva.append(palabra_entrante) | |
cuenta += len(palabra_entrante) | |
# Si se excede la longitud permitida, quitar palabras | |
while cuenta > longitud_max: | |
palabra_saliente = reserva.popleft() | |
cuenta -= len(palabra_saliente) | |
return list(reserva) | |
def generar_basura(self, longitud, num_botes=1): | |
""" | |
Genera una lista de num_botes cadenas de texto con símbolos | |
elegidos al azar de manera que la suma de sus longitudes es la | |
especificada. | |
""" | |
botes = [""] * (num_botes) | |
for _ in range(longitud): | |
i = self._random.randrange(num_botes) if num_botes > 1 else 0 | |
botes[i] += self._random.choice(self.simbolos) | |
return botes | |
def generar_contrasena(self, longitud=16): | |
""" | |
Método para generar contraseñas fáciles de recordar. | |
""" | |
# Generar una lista de palabras | |
longitud_min = math.ceil(longitud * self.min_letras) | |
longitud_max = math.floor(longitud * self.max_letras) | |
frase = self.generar_frase(longitud_min=longitud_min, longitud_max=longitud_max) | |
# Poner inicial mayúscula en algunas palabras de la frase al azar | |
for i, palabra in enumerate(frase): | |
if self._random.getrandbits(1): | |
frase[i] = palabra.capitalize() | |
# Generar símbolos al azar para unificar las palabras | |
uniones = self._random.choices(self.simbolos, k=len(frase) - 1) + [""] | |
# Rellenar el resto de la contraseña con símbolos al azar | |
num_letras = sum(len(palabra) for palabra in frase) | |
num_palabras = len(frase) | |
basura = self.generar_basura( | |
longitud - num_letras - num_palabras + 1, num_palabras | |
) | |
# Juntar palabras, uniones y símbolos para formar la contraseña | |
partes = itertools.chain.from_iterable(zip(frase, uniones, basura)) | |
contrasena = "".join(partes) | |
# Calcular dificultad de contraseña | |
entropia_frase = len(frase) * math.log2( | |
sum(1 for palabra in self.palabras if len(palabra) <= longitud_max) | |
) | |
entropia_mayusculas = len(frase) # Un bit por cada palabra | |
entropia_simbolos = len(uniones) + sum(map(len, basura)) * math.log2( | |
len(self.simbolos) | |
) | |
entropia_acomodo = math.log2(len(frase)) * sum(map(len, basura)) | |
entropia_total = math.fsum( | |
[entropia_frase, entropia_mayusculas, entropia_simbolos, entropia_acomodo] | |
) | |
return contrasena, entropia_total | |
class VentanaPrincipal(ttk.Frame): | |
""" | |
Interfaz gráfica de la aplicación de generar contraseñas. | |
""" | |
def __init__(self, generador, master=None): | |
super().__init__(master) | |
if isinstance(self.master, (tk.Tk, tk.Toplevel)): | |
self.master.title("Generar contraseña") | |
self.generador = generador | |
self.etiqueta_dificultad = ttk.Label(self, text="") | |
self.etiqueta_dificultad.pack(side="bottom") | |
self.contrasena = tk.StringVar(self) | |
self.entrada_contrasena = ttk.Entry(self, textvariable=self.contrasena) | |
self.entrada_contrasena.pack( | |
side="bottom", expand=True, fill="x", padx=4, pady=4 | |
) | |
self.etiqueta_longitud = ttk.Label(self, text="Longitud:") | |
self.etiqueta_longitud.pack(side="left", padx=4, pady=4) | |
self.longitud_var = tk.IntVar(self, value=16) | |
self.selector_longitud = ttk.Spinbox( | |
self, from_=8, to=24, textvariable=self.longitud_var | |
) | |
self.selector_longitud.pack(side="left") | |
self.boton_generar = ttk.Button( | |
self, text="Generar", command=self.generar_contrasena | |
) | |
self.boton_generar.pack(side="left", padx=4, pady=4) | |
self.pack(fill="both", expand=True) | |
def generar_contrasena(self): | |
""" | |
Generar una contraseña y escribirla en el cuadro de entrada. | |
""" | |
longitud = int(self.longitud_var.get()) | |
contrasena, dificultad = self.generador.generar_contrasena(longitud) | |
bits = math.ceil(dificultad) | |
tiempo = dificultad_temporal(dificultad) | |
self.etiqueta_dificultad["text"] = f"Dificultad: {bits} bits " f"({tiempo})." | |
self.contrasena.set(contrasena) | |
if __name__ == "__main__": | |
PALABRAS = obtener_palabras() | |
SIMBOLOS = string.digits + "@#$&?!_/*-+" | |
GENERADOR = FacilDeRecordar(PALABRAS, SIMBOLOS) | |
RAIZ = tk.Tk() | |
APLICACION = VentanaPrincipal(GENERADOR, master=RAIZ) | |
APLICACION.mainloop() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import binascii | |
import os | |
import hashlib | |
def calcular_hash(contrasena): | |
secreto = contrasena.encode('utf8') | |
sal = os.urandom(16) | |
sal_hexadecimal = binascii.hexlify(sal).decode('ascii') | |
valor_hash = hashlib.sha256(sal + secreto).hexdigest() | |
return '{}:{}'.format(valor_hash, sal_hexadecimal) | |
def verificar_contrasena(hash_salado, contrasena): | |
intento = contrasena.encode('utf8') | |
valor_hash, sal_en_hexadecimal = hash_salado.split(':') | |
sal = binascii.unhexlify(sal_en_hexadecimal) | |
return valor_hash == hashlib.sha256(sal + intento).hexdigest() | |
if __name__ == '__main__': | |
while True: | |
contrasena = input('Ingrese contraseña: ') | |
if not contrasena: break | |
valor_hash = calcular_hash(contrasena) | |
print('Hash criptográfico y salado de la contraseña:') | |
print(valor_hash) | |
intento = input('Ingrese nuevamente: ') | |
if verificar_contrasena(valor_hash, intento): | |
print('Los hashes coinciden (la contraseña es correcta).') | |
else: | |
print('Los hashes no coinciden (contraseña incorrecta)') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment