SHA 512加密在Python代码中的输出与mkpasswd工具不同

7

运行mkpasswd -m sha-512 -S salt1234 password将产生以下结果:

$6$salt1234$Zr07alHmuONZlfKILiGKKULQZaBG6Qmf5smHCNH35KnciTapZ7dItwaCv5SKZ1xH9ydG59SCgkdtsTqVWGhk81

我有一段Python代码,我认为输出应该是相同的,但实际并不是:

import hashlib, base64
print(base64.b64encode(hashlib.sha512('password' + 'salt1234').digest()))

它实际上会导致:
nOkBUt6l7zlKAfjtk1EfB0TmckXfDiA4FPLcpywOLORZ1PWQK4+PZVEiT4+9rFjqR3xnaruZBiRjDGcDpxxTig==

我不确定我做错了什么。

我还有一个问题,如何告诉sha512函数进行自定义轮数。它似乎只接受一个参数。


2
我认为passlib可能更接近你想要的 https://pythonhosted.org/passlib/lib/passlib.hash.sha512_crypt.html# hashlib和crypto(3)之间也有很大的区别。 - Padraic Cunningham
3
SHA512-crypt的规范:http://www.akkadia.org/drepper/sha-crypt.html - Martijn Pieters
如果您想使用passlib,您还可以指定轮数以匹配当前输出sha512_crypt.encrypt(password, salt=salt, rounds=5000) - Padraic Cunningham
您需要花时间阅读文档,了解哈希和密码处理的信息。安全漏洞与其他错误不同,代码可能表现正常,但仍存在不安全因素。一旦出现漏洞或误用,那么就没有安全可言了。您需要理解代码的具体执行内容及其目的。 - zaph
3个回答

11

mkpasswdcrypt()函数的前端,但我认为这里并不是一个简单的SHA512哈希。

通过一些调研,发现SHA256-crypt和SHA512-crypt的规范表明哈希函数默认会被应用5000次。您可以使用-R开关指定不同数量的轮数给mkpasswd-R 5000确实会给出相同的输出:

$ mkpasswd -m sha-512 -S salt1234 -R 5000 password
$6$rounds=5000$salt1234$Zr07alHmuONZlfKILiGKKULQZaBG6Qmf5smHCNH35KnciTapZ7dItwaCv5SKZ1xH9ydG59SCgkdtsTqVWGhk81

命令行工具提供的最小轮数为1000:

$ mkpasswd -m sha-512 -S salt1234 -R 999 password
$6$rounds=1000$salt1234$SVDFHbJXYrzjGi2fA1k3ws01/D9q0ZTAh1KfRF5.ehgjVBqfHUaKqfynXefJ4DxIWxkMAITYq9mmcBl938YQ//
$ mkpasswd -m sha-512 -S salt1234 -R 1 password
$6$rounds=1000$salt1234$SVDFHbJXYrzjGi2fA1k3ws01/D9q0ZTAh1KfRF5.ehgjVBqfHUaKqfynXefJ4DxIWxkMAITYq9mmcBl938YQ//

算法要求您创建多个摘要。相反,您可以通过crypt.crypt()函数访问C语言crypt()函数,并以与mkpasswd命令行相同的方式使用它。

如果您使用的平台支持SHA512-crypt方法,则Python 3版本的crypt模块提供了一个crypt.methods列表,告诉您平台支持哪些方法。由于这使用了与mkpasswd相同的库,您的操作系统显然也支持SHA512-crypt,Python也可以访问它。

您需要在盐值之前添加'$6$'以指定不同的方法。您可以通过在'$6$'字符串和您的盐值之间添加'rounds=<N>$'字符串来指定循环次数:

import crypt
import os
import string

try:  # 3.6 or above
    from secrets import choice as randchoice
except ImportError:
    from random import SystemRandom
    randchoice = SystemRandom().choice

def sha512_crypt(password, salt=None, rounds=None):
    if salt is None:
        salt = ''.join([randchoice(string.ascii_letters + string.digits)
                        for _ in range(8)])

    prefix = '$6$'
    if rounds is not None:
        rounds = max(1000, min(999999999, rounds or 5000))
        prefix += 'rounds={0}$'.format(rounds)
    return crypt.crypt(password, prefix + salt)

这将会产生与 mkpasswd 命令行相同的输出:

>>> sha512_crypt('password', 'salt1234')
'$6$salt1234$Zr07alHmuONZlfKILiGKKULQZaBG6Qmf5smHCNH35KnciTapZ7dItwaCv5SKZ1xH9ydG59SCgkdtsTqVWGhk81'
>>> sha512_crypt('password', 'salt1234', rounds=1000)
'$6$rounds=1000$salt1234$SVDFHbJXYrzjGi2fA1k3ws01/D9q0ZTAh1KfRF5.ehgjVBqfHUaKqfynXefJ4DxIWxkMAITYq9mmcBl938YQ//'

7

你需要使用crypt.crypt

>>> import crypt
>>> crypt.crypt('password', '$6$' + 'salt1234')
'$6$salt1234$Zr07alHmuONZlfKILiGKKULQZaBG6Qmf5smHCNH35KnciTapZ7dItwaCv5SKZ1xH9ydG59SCgkdtsTqVWGhk81'

1
但是为什么我写的代码没有按照我的期望执行呢?hashlib.sha512函数和SHA-512 crypt不一样吗?我试图从高层次的角度理解SHA-512 crypt算法。 - user1720897
@user1720897,请阅读Martijn Pieters的评论。 - falsetru

4

这里是一个基于规范的纯Python3实现的sha512_crypt函数。 这仅用于说明,始终使用crypt.crypt代替!

import hashlib, base64

SHUFFLE_SHA512_INDICES = [
  42, 21,  0,     1, 43, 22,    23,  2, 44,    45, 24,  3,     4, 46, 25,
  26,  5, 47,    48, 27,  6,     7, 49, 28,    29,  8, 50,    51, 30,  9,
  10, 52, 31,    32, 11, 53,    54, 33, 12,    13, 55, 34,    35, 14, 56,
  57, 36, 15,    16, 58, 37,    38, 17, 59,    60, 39, 18,    19, 61, 40,
  41, 20, 62,    63
]

def shuffle_sha512(data):
  return bytes(data[i] for i in SHUFFLE_SHA512_INDICES)

def extend_by_repeat(data, length):
  return (data * (length // len(data) + 1))[:length]

CUSTOM_ALPHABET = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'

'''  Base64 encode based on SECTION 22.e)
'''
def custom_b64encode(data, alphabet = CUSTOM_ALPHABET):
  buffer,count,result = 0,0,[]
  for byte in data:
    buffer |= byte << count
    count += 8
    while count >= 6:
      result.append(buffer & 0x3f)
      buffer >>= 6
      count -= 6
  if count > 0:
    result.append(buffer)
  return ''.join(alphabet[idx] for idx in result)

'''  From http://www.akkadia.org/drepper/SHA-crypt.txt
'''
def sha512_crypt(password, salt, rounds_in = None):
  rounds,rounds_defined = 5000, False
  if rounds_in is not None:
    rounds,rounds_defined = rounds_in, True

  assert 1000 <= rounds <= 999999999
  hash = hashlib.sha512
  salt_prefix = '$6$'
  password = password.encode('utf8')
  salt = salt.encode('ascii')[:16]


  A = hash()             # SECTION 1.
  A.update(password)     # SECTION 2.
  A.update(salt)         # SECTION 3.

  B = hash()             # SECTION 4.
  B.update(password)     # SECTION 5.
  B.update(salt)         # SECTION 6.
  B.update(password)     # SECTION 7.
  digestB = B.digest();  # SECTION 8.

  A.update(extend_by_repeat(digestB, len(password)))  # SECTION 9., 10.

  # SECTION 11.
  i = len(password)
  while i > 0:
    if i & 1:
      A.update(digestB)   # SECTION 11.a)
    else:
      A.update(password)  # SECTION 11.b)
    i = i >> 1

  digestA = A.digest()    # SECTION 12.

  DP = hash()             # SECTION 13.
  # SECTION 14.
  for _ in range(len(password)):
    DP.update(password)

  digestDP = DP.digest()  # SECTION 15.

  P = extend_by_repeat(digestDP, len(password))  # SECTION 16.a), 16.b)

  DS = hash()             # SECTION 17.
  # SECTION 18.
  for _ in range(16 + digestA[0]):
    DS.update(salt)

  digestDS = DS.digest()  # SECTION 19.

  S = extend_by_repeat(digestDS, len(salt))      # SECTION 20.a), 20.b)

  # SECTION 21.
  digest_iteration_AC = digestA
  for i in range(rounds):
    C = hash()                        # SECTION 21.a)
    if i % 2:
      C.update(P)                     # SECTION 21.b)
    else:
      C.update(digest_iteration_AC)   # SECTION 21.c)
    if i % 3:
      C.update(S)                     # SECTION 21.d)
    if i % 7:
      C.update(P)                     # SECTION 21.e)
    if i % 2:
      C.update(digest_iteration_AC)   # SECTION 21.f)
    else:
      C.update(P)                     # SECTION 21.g)

    digest_iteration_AC = C.digest()  # SECTION 21.h)

  shuffled_digest = shuffle_sha512(digest_iteration_AC)


  prefix = salt_prefix   # SECTION 22.a)

  # SECTION 22.b)
  if rounds_defined:
    prefix += 'rounds={0}$'.format(rounds_in)


  return (prefix
    + salt.decode('ascii')               # SECTION 22.c)
    + '$'                                # SECTION 22.d)
    + custom_b64encode(shuffled_digest)  # SECTION 22.e)
  )

actual = sha512_crypt('password', 'salt1234')
expected = '$6$salt1234$Zr07alHmuONZlfKILiGKKULQZaBG6Qmf5smHCNH35KnciTapZ7dItwaCv5SKZ1xH9ydG59SCgkdtsTqVWGhk81'

print(actual)
print(expected)
assert actual == expected

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接