在 Ruby / Python 中实施 Umbraco/ASP.NET 密码验证脚本

Implement an Umbraco/ASP.NET password validation script in Ruby / Python

我已经从 Umbraco 数据库中导出了大约 1000 个用户。我们每个人都有电子邮件和 hashed_passwords。似乎没有针对每个用户的加盐。

Umbraco 项目版本为 7.4,具有以下(默认)设置:

<membership defaultProvider="UmbracoMembershipProvider" userIsOnlineTimeWindow="15">
  <providers>
    <clear />
    <add name="UmbracoMembershipProvider" type="Umbraco.Web.Security.Providers.MembersMembershipProvider, Umbraco.Web" minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="5" useLegacyEncoding="false" enablePasswordRetrieval="false" enablePasswordReset="false" requiresQuestionAndAnswer="false" defaultMemberTypeAlias="Member" passwordFormat="Hashed" allowManuallyChangingPassword="true" />
    <add name="UsersMembershipProvider" type="Umbraco.Web.Security.Providers.UsersMembershipProvider, Umbraco.Web" />
  </providers>
</membership>
<!-- Role Provider -->
<roleManager enabled="true" defaultProvider="UmbracoRoleProvider">
  <providers>
    <clear />
    <add name="UmbracoRoleProvider" type="Umbraco.Web.Security.Providers.MembersRoleProvider" />
  </providers>
</roleManager>
<machineKey validationKey="9FEB5*******************B7348B27A6C" decryptionKey="73934**************69366" validation="HMACSHA256" decryption="AES" />

Asp.net 使用的代码似乎是这个:

https://referencesource.microsoft.com/#System.Web/Security/SQLMembershipProvider.cs,f37d42faca2b921e,references

我在 Ruby 中设置了此代码以正确编码纯文本密码并将其与散列密码进行比较,以便用户无需重置其原始纯文本密码即可登录:

def encode_password(password, salt)

    require "base64"
    require "digest"

    bytes = ""
    password.each_char { |c| bytes += c + "\x00" }
    salty = Base64.decode64(salt)
    concat = salty+bytes
    sha256 = Digest::SHA256.digest(concat)
    encoded = Base64.encode64(sha256).strip()
    puts encoded
  end

但我的问题是我不知道该用哪种盐。 根据我在这里的研究:

https://shazwazza.com/post/umbraco-passwords-and-aspnet-machine-keys

Umbraco 使用从 machineKey 派生的单一盐,但不清楚具体是哪一部分。并且不清楚它是否应该成为 validationKeydecryptionKey

的一部分

盐不是基于机器密钥。博客 post 说:

The key used to hash the passwords is the generated salt we produce it is not the key part of the Machine Key

The part of the Machine Key that is used to hash the passwords is specifically the algorithm type.

所以使用的机器密钥的唯一部分是算法类型。

此方法 EncryptOrHashNewPassword is what creates the salt + password. It then calls EncryptOrHashPassword with the provided salt to do the password hashing. All of that is called from this method HashPasswordForStorage 获取完全散列的密码以及用于对密码进行散列的盐,并将它们一起存储为字符串 (base64 iirc)。

验证密码时,salt为parsed from the stored string and is used to hash the incoming password to see if they match. This unit test somewhat shows this

这几乎就是您需要移植到 Ruby/Python 的内容。

对于希望将 Umbraco 密码散列导出到备用平台的任何人,这是 Python 脚本,它使我们能够与登录时输入的纯文本密码进行比较。工作完美。

import base64
import hashlib
import hmac
import secrets
from typing import Tuple

def check_password(password: str, db_password: str) -> bool:
    """Check if password matches"""

    if not db_password.strip():
        raise ValueError("dbPassword cannot be none or empty")

    stored_hash_password, salt = stored_password(db_password)
    hashed = encrypt_or_hash_password(password, salt)
    return stored_hash_password == hashed


def stored_password(stored_string: str) -> Tuple[str, str]:
    """Return salt and password from stored_string"""
    if not stored_string.strip():
        raise ValueError("stored_string cannot be none or empty")

    salt = GenerateSalt()
    salt_len = len(salt)
    
    password = stored_string[salt_len :]
    salt = stored_string[0 : salt_len - 1]

    return password, salt


def GenerateSalt() -> str:
    """Return byte array with 24 length"""
    return secrets.token_hex(12)


def encrypt_or_hash_password(password: str, salt: str) -> str:
    """Return hashed password"""

    ##
    ## Bytes with 16 length repeated till 64 length (=== is for base64 padding)
    ##
    salt_bytes = base64.b64decode(salt + '===') * 4 

    ## WARN!!!!
    ## HACK TO MATCH C# UNICODE
    ##
    unicode_password = []
    for char in password:
        unicode_password += char
        unicode_password += chr(0)
    password_bytes = ''.join(unicode_password).encode()

    hash_bytes = hmac.new(salt_bytes, password_bytes, digestmod=hashlib.sha256).digest()
    return base64.b64encode(hash_bytes).decode()

print(
    check_password(
        "xxxxxx",
        "aOrcTkUrkb45kR6UA7Yv5Q==S++GZlit8lIxxx2rpHDwPt3dhSoEDa3HPsUg4hCpnWU=",
    )
)