I have exported about 1K users from an Umbraco database. We have emails and hashed_passwords for each. There appears to be no per-user salt.
Umbraco project version is 7.4 with the following (default) settings:
<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 code used seems to be this one:
I have setup this code in Ruby to properly encode the plain text password and compare it with the hashed passwords, so that users can login without having to reset their original plain text passwords:
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
But my problem is that I don’t know which salt to use.
From what I’ve been able to research here:
https://shazwazza.com/post/umbraco-passwords-and-aspnet-machine-keys
Umbraco uses a single salt derived from the machineKey, but it’s unclear which part exactly. And it’s unclear if it should be part of the validationKey or the decryptionKey
Answers:
Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.
Method 1
the salt is not based on the machine key. The blog post says:
The key used to hash the passwords is the generated salt we produce it is not the key part of the Machine Key
and
The part of the Machine Key that is used to hash the passwords is specifically the algorithm type.
So the only part of the machine key that is used is the algorithm type.
This method 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 which takes the fully hashed password along with the salt used to hash the password and stores both of them together as a string (base64 iirc).
When verifying the password, the salt is parsed from the stored string and is used to hash the incoming password to see if they match. This unit test somewhat shows this
That’s pretty much what you would need to port to Ruby/Python.
Method 2
For anyone who is looking to export their Umbraco passwords hashes to an alternate platform, this is the Python script that made it possible for us to do the comparison with plain text passwords entered at login time. Working flawlessly.
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=",
)
)
All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0