| | 1 | | #pragma warning disable CS1591 |
| | 2 | |
|
| | 3 | | using System; |
| | 4 | | using System.Collections.Generic; |
| | 5 | | using System.Text; |
| | 6 | |
|
| | 7 | | namespace MediaBrowser.Model.Cryptography |
| | 8 | | { |
| | 9 | | // Defined from this hash storage spec |
| | 10 | | // https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md |
| | 11 | | // $<id>[$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]] |
| | 12 | | // with one slight amendment to ease the transition, we're writing out the bytes in hex |
| | 13 | | // rather than making them a BASE64 string with stripped padding |
| | 14 | | public class PasswordHash |
| | 15 | | { |
| | 16 | | private readonly Dictionary<string, string> _parameters; |
| | 17 | | private readonly byte[] _salt; |
| | 18 | | private readonly byte[] _hash; |
| | 19 | |
|
| | 20 | | public PasswordHash(string id, byte[] hash) |
| 6 | 21 | | : this(id, hash, Array.Empty<byte>()) |
| | 22 | | { |
| 4 | 23 | | } |
| | 24 | |
|
| | 25 | | public PasswordHash(string id, byte[] hash, byte[] salt) |
| 6 | 26 | | : this(id, hash, salt, new Dictionary<string, string>()) |
| | 27 | | { |
| 4 | 28 | | } |
| | 29 | |
|
| | 30 | | public PasswordHash(string id, byte[] hash, byte[] salt, Dictionary<string, string> parameters) |
| | 31 | | { |
| 35 | 32 | | ArgumentException.ThrowIfNullOrEmpty(id); |
| | 33 | |
|
| | 34 | | Id = id; |
| 33 | 35 | | _hash = hash; |
| 33 | 36 | | _salt = salt; |
| 33 | 37 | | _parameters = parameters; |
| 33 | 38 | | } |
| | 39 | |
|
| | 40 | | /// <summary> |
| | 41 | | /// Gets the symbolic name for the function used. |
| | 42 | | /// </summary> |
| | 43 | | /// <value>Returns the symbolic name for the function used.</value> |
| | 44 | | public string Id { get; } |
| | 45 | |
|
| | 46 | | /// <summary> |
| | 47 | | /// Gets the additional parameters used by the hash function. |
| | 48 | | /// </summary> |
| 16 | 49 | | public IReadOnlyDictionary<string, string> Parameters => _parameters; |
| | 50 | |
|
| | 51 | | /// <summary> |
| | 52 | | /// Gets the salt used for hashing the password. |
| | 53 | | /// </summary> |
| | 54 | | /// <value>Returns the salt used for hashing the password.</value> |
| 16 | 55 | | public ReadOnlySpan<byte> Salt => _salt; |
| | 56 | |
|
| | 57 | | /// <summary> |
| | 58 | | /// Gets the hashed password. |
| | 59 | | /// </summary> |
| | 60 | | /// <value>Return the hashed password.</value> |
| 16 | 61 | | public ReadOnlySpan<byte> Hash => _hash; |
| | 62 | |
|
| | 63 | | public static PasswordHash Parse(ReadOnlySpan<char> hashString) |
| | 64 | | { |
| 32 | 65 | | if (hashString.IsEmpty) |
| | 66 | | { |
| 2 | 67 | | throw new ArgumentException("String can't be empty", nameof(hashString)); |
| | 68 | | } |
| | 69 | |
|
| 30 | 70 | | if (hashString[0] != '$') |
| | 71 | | { |
| 1 | 72 | | throw new FormatException("Hash string must start with a $"); |
| | 73 | | } |
| | 74 | |
|
| | 75 | | // Ignore first $ |
| 29 | 76 | | hashString = hashString[1..]; |
| | 77 | |
|
| 29 | 78 | | int nextSegment = hashString.IndexOf('$'); |
| 29 | 79 | | if (hashString.IsEmpty || nextSegment == 0) |
| | 80 | | { |
| 2 | 81 | | throw new FormatException("Hash string must contain a valid id"); |
| | 82 | | } |
| | 83 | |
|
| 27 | 84 | | if (nextSegment == -1) |
| | 85 | | { |
| 2 | 86 | | return new PasswordHash(hashString.ToString(), Array.Empty<byte>()); |
| | 87 | | } |
| | 88 | |
|
| 25 | 89 | | ReadOnlySpan<char> id = hashString[..nextSegment]; |
| 25 | 90 | | hashString = hashString[(nextSegment + 1)..]; |
| 25 | 91 | | Dictionary<string, string>? parameters = null; |
| | 92 | |
|
| 25 | 93 | | nextSegment = hashString.IndexOf('$'); |
| | 94 | |
|
| | 95 | | // Optional parameters |
| 25 | 96 | | ReadOnlySpan<char> parametersSpan = nextSegment == -1 ? hashString : hashString[..nextSegment]; |
| 25 | 97 | | if (parametersSpan.Contains('=')) |
| | 98 | | { |
| 41 | 99 | | while (!parametersSpan.IsEmpty) |
| | 100 | | { |
| | 101 | | ReadOnlySpan<char> parameter; |
| 25 | 102 | | int index = parametersSpan.IndexOf(','); |
| 25 | 103 | | if (index == -1) |
| | 104 | | { |
| 19 | 105 | | parameter = parametersSpan; |
| 19 | 106 | | parametersSpan = ReadOnlySpan<char>.Empty; |
| | 107 | | } |
| | 108 | | else |
| | 109 | | { |
| 6 | 110 | | parameter = parametersSpan[..index]; |
| 6 | 111 | | parametersSpan = parametersSpan[(index + 1)..]; |
| | 112 | | } |
| | 113 | |
|
| 25 | 114 | | int splitIndex = parameter.IndexOf('='); |
| 25 | 115 | | if (splitIndex == -1 || splitIndex == 0 || splitIndex == parameter.Length - 1) |
| | 116 | | { |
| 3 | 117 | | throw new FormatException("Malformed parameter in password hash string"); |
| | 118 | | } |
| | 119 | |
|
| 22 | 120 | | (parameters ??= new Dictionary<string, string>()).Add( |
| 22 | 121 | | parameter[..splitIndex].ToString(), |
| 22 | 122 | | parameter[(splitIndex + 1)..].ToString()); |
| | 123 | | } |
| | 124 | |
|
| 16 | 125 | | if (nextSegment == -1) |
| | 126 | | { |
| | 127 | | // parameters can't be null here |
| 3 | 128 | | return new PasswordHash(id.ToString(), Array.Empty<byte>(), Array.Empty<byte>(), parameters!); |
| | 129 | | } |
| | 130 | |
|
| 13 | 131 | | hashString = hashString[(nextSegment + 1)..]; |
| 13 | 132 | | nextSegment = hashString.IndexOf('$'); |
| | 133 | | } |
| | 134 | |
|
| 19 | 135 | | if (nextSegment == 0) |
| | 136 | | { |
| 2 | 137 | | throw new FormatException("Hash string contains an empty segment"); |
| | 138 | | } |
| | 139 | |
|
| | 140 | | byte[] hash; |
| | 141 | | byte[] salt; |
| | 142 | |
|
| 17 | 143 | | if (nextSegment == -1) |
| | 144 | | { |
| 6 | 145 | | salt = Array.Empty<byte>(); |
| 6 | 146 | | hash = Convert.FromHexString(hashString); |
| | 147 | | } |
| | 148 | | else |
| | 149 | | { |
| 11 | 150 | | salt = Convert.FromHexString(hashString[..nextSegment]); |
| 10 | 151 | | hashString = hashString[(nextSegment + 1)..]; |
| 10 | 152 | | nextSegment = hashString.IndexOf('$'); |
| 10 | 153 | | if (nextSegment != -1) |
| | 154 | | { |
| 2 | 155 | | throw new FormatException("Hash string contains too many segments"); |
| | 156 | | } |
| | 157 | |
|
| 8 | 158 | | if (hashString.IsEmpty) |
| | 159 | | { |
| 3 | 160 | | throw new FormatException("Hash segment is empty"); |
| | 161 | | } |
| | 162 | |
|
| 5 | 163 | | hash = Convert.FromHexString(hashString); |
| | 164 | | } |
| | 165 | |
|
| 10 | 166 | | return new PasswordHash(id.ToString(), hash, salt, parameters ?? new Dictionary<string, string>()); |
| | 167 | | } |
| | 168 | |
|
| | 169 | | private void SerializeParameters(StringBuilder stringBuilder) |
| | 170 | | { |
| 33 | 171 | | if (_parameters.Count == 0) |
| | 172 | | { |
| 12 | 173 | | return; |
| | 174 | | } |
| | 175 | |
|
| 21 | 176 | | stringBuilder.Append('$'); |
| 108 | 177 | | foreach (var pair in _parameters) |
| | 178 | | { |
| 33 | 179 | | stringBuilder.Append(pair.Key) |
| 33 | 180 | | .Append('=') |
| 33 | 181 | | .Append(pair.Value) |
| 33 | 182 | | .Append(','); |
| | 183 | | } |
| | 184 | |
|
| | 185 | | // Remove last ',' |
| 21 | 186 | | stringBuilder.Length -= 1; |
| 21 | 187 | | } |
| | 188 | |
|
| | 189 | | /// <inheritdoc /> |
| | 190 | | public override string ToString() |
| | 191 | | { |
| 33 | 192 | | var str = new StringBuilder() |
| 33 | 193 | | .Append('$') |
| 33 | 194 | | .Append(Id); |
| 33 | 195 | | SerializeParameters(str); |
| | 196 | |
|
| 33 | 197 | | if (_salt.Length != 0) |
| | 198 | | { |
| 10 | 199 | | str.Append('$') |
| 10 | 200 | | .Append(Convert.ToHexString(_salt)); |
| | 201 | | } |
| | 202 | |
|
| 33 | 203 | | if (_hash.Length != 0) |
| | 204 | | { |
| 22 | 205 | | str.Append('$') |
| 22 | 206 | | .Append(Convert.ToHexString(_hash)); |
| | 207 | | } |
| | 208 | |
|
| 33 | 209 | | return str.ToString(); |
| | 210 | | } |
| | 211 | | } |
| | 212 | | } |