Skip to content

Instantly share code, notes, and snippets.

@knkillname
Last active July 29, 2022 13:59
Show Gist options
  • Save knkillname/6d4ff2e3aaedb52bc4a659083dcdd463 to your computer and use it in GitHub Desktop.
Save knkillname/6d4ff2e3aaedb52bc4a659083dcdd463 to your computer and use it in GitHub Desktop.
Generador de contraseñas fáciles de recordar
# 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()
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