Files
wjones af1152b001 Refactor: standardize namespace and using directive style
Refactored all C# test files to use explicit namespace declarations and moved all using directives inside the namespace block for consistency. Updated assembly info and cache files to reflect the new build. Adjusted MvcTestingAppManifest.json to use fully qualified assembly names. No functional changes; all updates are related to code style, organization, and project metadata.
2026-02-20 16:26:53 -05:00

216 lines
6.8 KiB
C#

// <copyright file="PasswordHash.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
#pragma warning disable CS1591
using global::System;
using global::System.Collections.Generic;
using global::System.Text;
namespace MediaBrowser.Model.Cryptography;
// Defined from this hash storage spec
// https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
// $<id>[$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]]
// 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<string, string> _parameters;
private readonly byte[] _salt;
private readonly byte[] _hash;
public PasswordHash(string id, byte[] hash)
: this(id, hash, Array.Empty<byte>())
{
}
public PasswordHash(string id, byte[] hash, byte[] salt)
: this(id, hash, salt, new Dictionary<string, string>())
{
}
public PasswordHash(string id, byte[] hash, byte[] salt, Dictionary<string, string> parameters)
{
ArgumentException.ThrowIfNullOrEmpty(id);
Id = id;
_hash = hash;
_salt = salt;
_parameters = parameters;
}
/// <summary>
/// Gets the symbolic name for the function used.
/// </summary>
/// <value>Returns the symbolic name for the function used.</value>
public string Id { get; }
/// <summary>
/// Gets the additional parameters used by the hash function.
/// </summary>
public IReadOnlyDictionary<string, string> Parameters => _parameters;
/// <summary>
/// Gets the salt used for hashing the password.
/// </summary>
/// <value>Returns the salt used for hashing the password.</value>
public ReadOnlySpan<byte> Salt => _salt;
/// <summary>
/// Gets the hashed password.
/// </summary>
/// <value>Return the hashed password.</value>
public ReadOnlySpan<byte> Hash => _hash;
public static PasswordHash Parse(ReadOnlySpan<char> 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");
}
if (nextSegment == -1)
{
return new PasswordHash(hashString.ToString(), Array.Empty<byte>());
}
ReadOnlySpan<char> id = hashString[..nextSegment];
hashString = hashString[(nextSegment + 1)..];
Dictionary<string, string>? parameters = null;
nextSegment = hashString.IndexOf('$');
// Optional parameters
ReadOnlySpan<char> parametersSpan = nextSegment == -1 ? hashString : hashString[..nextSegment];
if (parametersSpan.Contains('='))
{
while (!parametersSpan.IsEmpty)
{
ReadOnlySpan<char> parameter;
int index = parametersSpan.IndexOf(',');
if (index == -1)
{
parameter = parametersSpan;
parametersSpan = ReadOnlySpan<char>.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<string, string>()).Add(
parameter[..splitIndex].ToString(),
parameter[(splitIndex + 1)..].ToString());
}
if (nextSegment == -1)
{
// parameters can't be null here
return new PasswordHash(id.ToString(), Array.Empty<byte>(), Array.Empty<byte>(), 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<byte>();
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<string, string>());
}
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;
}
/// <inheritdoc />
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();
}
}