#pragma warning disable CS1591 #nullable enable using System; using System.Collections.Generic; using System.IO; using System.Text; 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; private readonly byte[] _salt; private readonly byte[] _hash; 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) { if (id == null) { throw new ArgumentNullException(nameof(id)); } if (id.Length == 0) { throw new ArgumentException("String can't be empty", nameof(id)); } 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 ReadOnlySpan Salt => _salt; /// /// Gets the hashed password. /// /// Return the hashed password. public ReadOnlySpan Hash => _hash; public static PasswordHash Parse(ReadOnlySpan hashString) { if (hashString.IsEmpty) { throw new ArgumentException("String can't be empty", nameof(hashString)); } if (hashString[0] != '$') { throw new FormatException("Hash string must start with a $"); } // Ignore first $ hashString = hashString[1..]; int nextSegment = hashString.IndexOf('$'); if (hashString.IsEmpty || nextSegment == 0) { throw new FormatException("Hash string must contain a valid id"); } else if (nextSegment == -1) { return new PasswordHash(hashString.ToString(), Array.Empty()); } ReadOnlySpan id = hashString[..nextSegment]; hashString = hashString[(nextSegment + 1)..]; Dictionary? parameters = null; nextSegment = hashString.IndexOf('$'); // Optional parameters ReadOnlySpan parametersSpan = nextSegment == -1 ? hashString : hashString[..nextSegment]; if (parametersSpan.Contains('=')) { while (!parametersSpan.IsEmpty) { ReadOnlySpan parameter; int index = parametersSpan.IndexOf(','); if (index == -1) { parameter = parametersSpan; parametersSpan = ReadOnlySpan.Empty; } else { parameter = parametersSpan[..index]; parametersSpan = parametersSpan[(index + 1)..]; } int splitIndex = parameter.IndexOf('='); if (splitIndex == -1 || splitIndex == 0 || splitIndex == parameter.Length - 1) { throw new FormatException($"Malformed parameter in password hash string"); } (parameters ??= new Dictionary()).Add( parameter[..splitIndex].ToString(), parameter[(splitIndex + 1)..].ToString()); } if (nextSegment == -1) { // parameters can't be null here return new PasswordHash(id.ToString(), Array.Empty(), Array.Empty(), parameters!); } hashString = hashString[(nextSegment + 1)..]; nextSegment = hashString.IndexOf('$'); } if (nextSegment == 0) { throw new FormatException($"Hash string contains an empty segment"); } byte[] hash; byte[] salt; if (nextSegment == -1) { salt = Array.Empty(); hash = Convert.FromHexString(hashString); } else { salt = Convert.FromHexString(hashString[..nextSegment]); hashString = hashString[(nextSegment + 1)..]; nextSegment = hashString.IndexOf('$'); if (nextSegment != -1) { throw new FormatException("Hash string contains too many segments"); } if (hashString.IsEmpty) { throw new FormatException("Hash segment is empty"); } hash = Convert.FromHexString(hashString); } return new PasswordHash(id.ToString(), hash, salt, parameters ?? new Dictionary()); } private void SerializeParameters(StringBuilder stringBuilder) { if (_parameters.Count == 0) { return; } stringBuilder.Append('$'); foreach (var pair in _parameters) { stringBuilder.Append(pair.Key) .Append('=') .Append(pair.Value) .Append(','); } // Remove last ',' stringBuilder.Length -= 1; } /// public override string ToString() { var str = new StringBuilder() .Append('$') .Append(Id); SerializeParameters(str); if (_salt.Length != 0) { str.Append('$') .Append(Convert.ToHexString(_salt)); } if (_hash.Length != 0) { str.Append('$') .Append(Convert.ToHexString(_hash)); } return str.ToString(); } } }