using System; using System.Collections.Generic; using System.IO; using System.Text; using static MediaBrowser.Common.HexHelper; namespace MediaBrowser.Common.Cryptography { // Defined from this hash storage spec // https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md // $[$=(,=)*][$[$]] // with one slight amendment to ease the transition, we're writing out the bytes in hex // rather than making them a BASE64 string with stripped padding public class PasswordHash { private readonly Dictionary _parameters; public PasswordHash(string id, byte[] hash) : this(id, hash, Array.Empty()) { } public PasswordHash(string id, byte[] hash, byte[] salt) : this(id, hash, salt, new Dictionary()) { } public PasswordHash(string id, byte[] hash, byte[] salt, Dictionary parameters) { Id = id; Hash = hash; Salt = salt; _parameters = parameters; } /// /// Gets the symbolic name for the function used. /// /// Returns the symbolic name for the function used. public string Id { get; } /// /// Gets the additional parameters used by the hash function. /// /// public IReadOnlyDictionary Parameters => _parameters; /// /// Gets the salt used for hashing the password. /// /// Returns the salt used for hashing the password. public byte[] Salt { get; } /// /// Gets the hashed password. /// /// Return the hashed password. public byte[] Hash { get; } public static PasswordHash Parse(string storageString) { string[] splitted = storageString.Split('$'); // The string should at least contain the hash function and the hash itself if (splitted.Length < 3) { throw new ArgumentException("String doesn't contain enough segments", nameof(storageString)); } // Start at 1, the first index shouldn't contain any data int index = 1; // Name of the hash function string id = splitted[index++]; // Optional parameters Dictionary parameters = new Dictionary(); if (splitted[index].IndexOf('=') != -1) { foreach (string paramset in splitted[index++].Split(',')) { if (string.IsNullOrEmpty(paramset)) { continue; } string[] fields = paramset.Split('='); if (fields.Length != 2) { throw new InvalidDataException($"Malformed parameter in password hash string {paramset}"); } parameters.Add(fields[0], fields[1]); } } byte[] hash; byte[] salt; // Check if the string also contains a salt if (splitted.Length - index == 2) { salt = FromHexString(splitted[index++]); hash = FromHexString(splitted[index++]); } else { salt = Array.Empty(); hash = FromHexString(splitted[index++]); } return new PasswordHash(id, hash, salt, parameters); } private void SerializeParameters(StringBuilder stringBuilder) { if (_parameters.Count == 0) { return; } stringBuilder.Append('$'); foreach (var pair in _parameters) { stringBuilder.Append(pair.Key); stringBuilder.Append('='); stringBuilder.Append(pair.Value); stringBuilder.Append(','); } // Remove last ',' stringBuilder.Length -= 1; } /// public override string ToString() { var str = new StringBuilder(); str.Append('$'); str.Append(Id); SerializeParameters(str); if (Salt.Length != 0) { str.Append('$'); str.Append(ToHexString(Salt)); } str.Append('$'); str.Append(ToHexString(Hash)); return str.ToString(); } } }