¿Cuántas rondas de hash es suficiente para un administrador de contraseñas?

15

Actualmente estoy escribiendo mi propio pequeño administrador de contraseñas que almacena la clave en un hash SHA256 , con salt. Yo creo el hash haciendo lo siguiente:

def sha256_rounds(raw, rounds=100001):
    obj = hashlib.sha256()
    for _ in xrange(rounds):
        obj.update(raw)
        raw = obj.digest()
    return obj.digest()

Una vez creado, se almacena con lo siguiente:

    key = base64.urlsafe_b64encode(provided_key)
    length = len(key)
    with open(key_file, "a+") as key_:
        front_salt, back_salt = os.urandom(16), os.urandom(16)
        key_.write("{}{}{}:{}".format(front_salt, key, back_salt, length))

Las preguntas / inquietudes que tengo son:

  • ¿Es esta una forma aceptable de almacenar una clave hash?
  • ¿Debo usar más iteraciones?
  • ¿Es mi técnica de salado suficiente? ¿O debería usar una técnica de salazón diferente?

Si esta no es una opción aceptable para almacenar una contraseña / clave con hash, ¿qué otros pasos puedo tomar para hacer esto más seguro?

ACTUALIZACIÓN:

Seguí muchos consejos de tu chico, y si quieres ver el resultado, hasta ahora , de mi pequeño administrador de contraseñas puedes encontrarlo aquí . Los consejos de todos son muy apreciados y continuaré esforzándome para que esto sea lo mejor posible. (Si ese enlace no funciona, use este enlace )

    
pregunta CertifcateJunky 28.06.2018 - 15:39
fuente

5 respuestas

80
  

Actualmente estoy escribiendo mi propio pequeño administrador de contraseñas

Ese es tu primer error. Algo en este complejo tiene muchas trampas sutiles en las que incluso los expertos a veces caen, sin mucha experiencia en esta área, no tienes la oportunidad de hacer algo que esté cerca de asegurar.

  

almacena la clave en un hash SHA256

Uh oh ...

Esto no necesariamente indica que estás haciendo algo mal, pero tengo muchas dudas de que lo harás bien. ¿Supongo que estás hablando de una contraseña maestra que está siendo bloqueada aquí? La contraseña maestra se debe convertir en una clave usando un KDF como PBKDF2, bcrypt o Argon2, luego esta tecla se usa para cifrar las contraseñas almacenadas.

Si desea tener una forma de verificar que la contraseña sea correcta, debe guardar un hash de la clave, pero NO DEBE almacenar la clave en sí ... si guarda la ¡La clave de cualquier persona que tenga acceso a su almacenamiento tiene todo lo que necesita para descifrar todas las contraseñas!

Si no está hablando de hashear una contraseña maestra y quiere decir una clave real generada al azar, no tengo idea de lo que está intentando lograr aquí, pero no debería usar un KDF lento con una gran cantidad de número de iteraciones.

Alternativamente, podría estar usando la contraseña maestra dos veces, una vez para almacenar como un hash para verificar más tarde que la contraseña que ingresa el usuario es correcta, y nuevamente para usarla como clave para el cifrado. Dependiendo de cómo se haga esto, podría ir desde una falla de diseño hasta entregar la clave por completo.

Editar: Después de ver el código completo, parece ser una cuarta opción: almacena un hash de la contraseña para verificar más tarde si la contraseña ingresada es correcta, luego hash este hash para usar como la clave, que es casi tan mala como almacenar la clave en sí misma.

  

Yo creo el hash haciendo lo siguiente:

def sha256_rounds(raw, rounds=100001):
    obj = hashlib.sha256()
    for _ in xrange(rounds):
        obj.update(raw)
        raw = obj.digest()
    return obj.digest()

Realmente no está claro qué raw está aquí, pero asumo que es la contraseña. Lo que estás haciendo es un hash sin sal usando SHA256. ¡No intentes crear tu propio KDF!

  

Una vez creado, se almacena con lo siguiente:

key = base64.urlsafe_b64encode(provided_key)
length = len(key)
with open(key_file, "a+") as key_:
    front_salt, back_salt = os.urandom(16), os.urandom(16)
    key_.write("{}{}{}:{}".format(front_salt, key, back_salt, length))

Entonces, ¿está creando la clave con la clave de hash, luego agregando un salt aleatorio al frente y atrás? No solo concatenar 2 sales diferentes al frente y atrás no es estándar, ¡no está logrando nada aquí porque se hace después de que el KDF ya ha terminado! Solo estás agregando algunos valores aleatorios para tenerlos allí.

Para mostrar qué tan malo es esto (a partir de commit 609fdb5ce976c7e5aa1832670505da60012b73bc), todo lo que se necesita para volcar todas las contraseñas almacenadas sin requiere una contraseña maestra es esto:

from encryption.aes_encryption import AESCipher
from lib.settings import store_key, MAIN_DIR, DATABASE_FILE, display_formatted_list_output
from sql.sql import create_connection, select_all_data

conn, cursor = create_connection(DATABASE_FILE)
display_formatted_list_output(select_all_data(cursor, "encrypted_data"), store_key(MAIN_DIR))

Si bien puede ser una buena experiencia de aprendizaje intentar crear un administrador de contraseñas, por favor por favor nunca lo uses para nada a distancia. importante. Como sugiere @Xenos, no parece que tenga la suficiente experiencia de que crear su propio administrador de contraseñas realmente sería beneficioso de todos modos, probablemente sería una mejor oportunidad de aprendizaje para echar un vistazo a un administrador de contraseñas de código abierto existente.

    
respondido por el AndrolGenhald 28.06.2018 - 16:38
fuente
19

Primero, permítame revelarle que trabajo para 1Password, un administrador de contraseñas, y aunque pueda parecer autoservicio, debo agregar mi voz a quienes le dicen que escribir un administrador de contraseñas seguro es más difícil de lo que parece. Por otro lado, es bueno que lo intentes y lo preguntes en público. Esta es una buena manera de aprender que es más difícil de lo que parece.

No autenticar. Cifrar en su lugar

He echado un vistazo rápido a la fuente a la que te vinculas en tu actualización, y estoy encantado de ver que has tomado en algunos consejos que se han ofrecido hasta ahora. Pero a menos que esté malinterpretando su código, todavía tiene un error de diseño fundamental que lo hace masivamente inseguro .

Parece que está tratando la entrada de la contraseña maestra como una pregunta de autenticación en lugar de como una derivación de clave de cifrado. Es decir, usted está verificando la contraseña ingresada (después del hashing) con algo que está almacenado, y si esa verificación pasa, está usando esa clave almacenada para descifrar los datos.

Lo que debe hacer es almacenar una clave de cifrado cifrada. Llamémoslo la llave maestra. Esto debe generarse de forma aleatoria cuando configura por primera vez una nueva instancia. Esta clave maestra es la que utiliza para cifrar y descifrar los datos reales.

La clave maestra nunca se almacena sin cifrar. Se debe cifrar con lo que a veces se denomina clave de cifrado de clave (KEK). Este KEK se deriva a través de su función de derivación de claves (KDF) de la contraseña maestra del usuario y un salt.

Por lo tanto, la salida de su uso de PBKDF2 (gracias por usar eso en lugar de un hashing repetido) será su KEK, y usted usa KEK para descifrar la clave maestra, y luego usa la clave maestra descifrada para descifrar sus datos .

Tal vez eso es lo que estás haciendo y he leído mal tu código. Pero si solo está comparando lo que obtiene a través de su KDF con algo almacenado y luego toma la decisión de descifrar, entonces su sistema es inseguro de manera masiva y tremendamente horrible.

Por favor, haz que la primera línea más grande y audaz del proyecto LEA el grito de que es masivamente inseguro. Y agrega eso a cada archivo fuente también.

Algunos otros puntos

Aquí hay algunas otras cosas que noté cuando miraba la fuente

  • Use un modo de cifrado autenticado como GCM en lugar de CBC.

  • Su enfoque de solo cifrar todo funcionará para pequeñas cantidades de datos, pero no se escalará una vez que tenga conjuntos de datos más grandes.

    Cuando llegue el momento de cifrar (partes de) los registros por separado, tenga en cuenta que necesitará un nonce único (para GCM) o vectores de inicialización únicos (si no cumple con el CBC) para cada registro.

  • La destrucción de sus datos después de 3 fallas es peligrosa y no agrega seguridad.

    Un atacante sofisticado simplemente copiará su archivo de datos y escribirá su propio script para intentar descifrar la contraseña. También harán su propia copia de los datos capturados. La destrucción de sus datos solo facilita que alguien destruya sus datos de forma accidental o malintencionada.

Cuántas rondas de PBKDF2

Entonces a tu pregunta original. La respuesta es que depende. Por un lado, depende de qué tan segura sea la contraseña maestra y qué tipo de recursos arrojará el atacante al problema. Y también depende de los recursos que el defensor puede lanzar. Por ejemplo, ¿ejecutará su KDF en un dispositivo móvil con una capacidad de batería limitada?

Pero a medida que sucede, en 1Password estamos tratando de calcular el costo de la fisuración por ofreciendo premios a personas o grupos que descifren una contraseña de 42,5 bits con 100.000 rondas de PBKDF2-HMAC-SHA256.

Disminución de ganancias por el hashing lento

Pero lo que es más importante que averiguar cómo sopesar todos esos factores es entender que el hashing lento, aunque es absolutamente esencial para un administrador de contraseñas KDF, produce ganancias marginales una vez que se ha sintonizado lo suficiente. .

Supongamos que está utilizando rondas de 100 K y tiene una contraseña maestra, P . Si seleccionó un dígito aleatorio, 0–9 y lo agregó a P , obtendría un aumento de diez veces en la resistencia al agrietamiento. Para obtener el mismo aumento a través de aumentar las rondas de PBKDF2, necesitaría subir hasta 1 millón de rondas. El punto es que una vez que tienes una cantidad decente de hash lento, obtienes mucha más fuerza para el esfuerzo del defensor al aumentar la fuerza de la contraseña que si aumentas el número de rondas.

Escribí sobre esto hace algunos años en Bcrypt es genial, pero ¿no es viable descifrar la contraseña?

    
respondido por el Jeffrey Goldberg 29.06.2018 - 22:57
fuente
6

No use SHA256 sin procesar, se puede acelerar usando GPU y, por lo tanto, es propenso a la fuerza bruta. No está roto, por ningún motivo, simplemente ya no es la mejor práctica. PBKDF2 trabaja para contrarrestar eso hasta cierto punto, pero debes usar bcrypt o scrypt de manera preferencial.

Reinventar la rueda (que es prácticamente lo que estás haciendo si no estás usando PBKDF2-SHA256, bcrypt o scrypt) nunca es una buena idea en criptografía.

    
respondido por el crovers 28.06.2018 - 16:43
fuente
3

Viendo el código en el enlace que publicaste (reproducido a continuación, en partes), estoy un poco confundido.

Si lo leo correctamente, main() llama a store_key() para cargar una clave de un archivo en el disco, luego usa compare() para comparar una clave dada por el usuario con esa. Ambos se ejecutan a través de sha256_rounds() que ejecuta PBKDF2 en ellos.

Luego, usas el mismo stored_key , el que está cargado desde el archivo, ¿para realizar el cifrado?

Eso es completamente y completamente al revés. Cualquier persona que tenga acceso al archivo de claves podría simplemente cargar la clave almacenada y usarla para descifrar los datos almacenados, o almacenar los nuevos datos encriptados bajo la misma clave, sin tener que pasar por el script para " verifica "la contraseña.

En el mejor de los casos, creo que estás confundido al usar la contraseña / contraseña dada por el usuario para producir una clave para el cifrado , y autenticar al usuario contra un hash de contraseña almacenada . No puede hacer ambas cosas con el mismo hash, el cifrado es completamente irrelevante si ha almacenado la clave de cifrado junto con los datos cifrados.

Claro, las contraseñas se procesan con algún tipo de hash / KDF cuando se usan para autenticación y cuando se usan para producir una clave de cifrado. Pero es más como un paso de preprocesamiento necesario solo porque los humanos no pueden memorizar claves de 256 bits, y en su lugar tenemos que confiar en contraseñas de baja entropía. Lo que haga con el hash / clave que obtuvo difiere entre los dos casos de uso.

Luego está la cosa de que tienes una sal codificada en sha256_rounds() , que en gran medida anula el propósito de una sal en general. También podría comentar sobre el código de manera más general, pero esto no es codereview.SE.

Por favor, dime que leí mal el código.

Las partes del código que miré:

def main():
    stored_key = store_key(MAIN_DIR)    
    if not compare(stored_key):
        ...
    else:
            ... # later
            password = prompt("enter the password to store: ", hide=True)
            encrypted_password = AESCipher(stored_key).encrypt(password)



def store_key(path):
    key_file = "{}/.key".format(path)
    if not os.path.exists(key_file):
        ...
    else:
        retval = open(key_file).read()
        amount = retval.split(":")[-1]
        edited = retval[16:]
        edited = edited[:int(amount)]
        return edited
    
respondido por el ilkkachu 29.06.2018 - 16:38
fuente
2

Use una herramienta mantenida que decidirá por usted y aumentará el conteo predeterminado o incluso el algoritmo con el tiempo. Yo sugeriría libsodium, pero hay otros. Libsodium tiene valores predeterminados para el hashing de contraseña y para el cifrado simétrico. Asegúrese de que su software pueda algún día "actualizar" la configuración según sea necesario. Una herramienta como libsodium cambiará sus valores predeterminados de vez en cuando para que pueda seguirlos.

Hago desarrollo para un administrador de contraseñas de código abierto y uso libsodium. También encontré el documento de seguridad de Mitro que vale la pena leer.

    
respondido por el Bufke 29.06.2018 - 16:05
fuente

Lea otras preguntas en las etiquetas