up
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				Build Test Deploy / docs (push) Has been cancelled
				
			
		
			
				
	
				Build Test Deploy / deploy (push) Has been cancelled
				
			
		
			
				
	
				Build Test Deploy / build-test (push) Has been cancelled
				
			
		
			
				
	
				Build Test Deploy / authority-container (push) Has been cancelled
				
			
		
			
				
	
				Docs CI / lint-and-preview (push) Has been cancelled
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	Build Test Deploy / docs (push) Has been cancelled
				
			Build Test Deploy / deploy (push) Has been cancelled
				
			Build Test Deploy / build-test (push) Has been cancelled
				
			Build Test Deploy / authority-container (push) Has been cancelled
				
			Docs CI / lint-and-preview (push) Has been cancelled
				
			This commit is contained in:
		
							
								
								
									
										173
									
								
								src/StellaOps.Cryptography/Argon2idPasswordHasher.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								src/StellaOps.Cryptography/Argon2idPasswordHasher.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,173 @@ | ||||
| using System; | ||||
| using System.Globalization; | ||||
| using System.Security.Cryptography; | ||||
|  | ||||
| namespace StellaOps.Cryptography; | ||||
|  | ||||
| /// <summary> | ||||
| /// Argon2id password hasher that emits PHC-compliant encoded strings. | ||||
| /// </summary> | ||||
| public sealed partial class Argon2idPasswordHasher : IPasswordHasher | ||||
| { | ||||
|     private const int SaltLengthBytes = 16; | ||||
|     private const int HashLengthBytes = 32; | ||||
|  | ||||
|     public string Hash(string password, PasswordHashOptions options) | ||||
|     { | ||||
|         ArgumentException.ThrowIfNullOrEmpty(password); | ||||
|         ArgumentNullException.ThrowIfNull(options); | ||||
|  | ||||
|         options.Validate(); | ||||
|  | ||||
|         if (options.Algorithm != PasswordHashAlgorithm.Argon2id) | ||||
|         { | ||||
|             throw new InvalidOperationException("Argon2idPasswordHasher only supports the Argon2id algorithm."); | ||||
|         } | ||||
|  | ||||
|         Span<byte> salt = stackalloc byte[SaltLengthBytes]; | ||||
|         RandomNumberGenerator.Fill(salt); | ||||
|  | ||||
|         var hash = DeriveHash(password, salt, options); | ||||
|  | ||||
|         return BuildEncodedHash(salt, hash, options); | ||||
|     } | ||||
|  | ||||
|     public bool Verify(string password, string encodedHash) | ||||
|     { | ||||
|         ArgumentException.ThrowIfNullOrEmpty(password); | ||||
|         ArgumentException.ThrowIfNullOrEmpty(encodedHash); | ||||
|  | ||||
|         if (!TryParse(encodedHash, out var parsed)) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         var computed = DeriveHash(password, parsed.Salt, parsed.Options); | ||||
|         return CryptographicOperations.FixedTimeEquals(computed, parsed.Hash); | ||||
|     } | ||||
|  | ||||
|     public bool NeedsRehash(string encodedHash, PasswordHashOptions desired) | ||||
|     { | ||||
|         ArgumentNullException.ThrowIfNull(desired); | ||||
|  | ||||
|         if (!TryParse(encodedHash, out var parsed)) | ||||
|         { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         if (desired.Algorithm != PasswordHashAlgorithm.Argon2id) | ||||
|         { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         if (!parsed.Options.Algorithm.Equals(desired.Algorithm)) | ||||
|         { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         return parsed.Options.MemorySizeInKib != desired.MemorySizeInKib | ||||
|             || parsed.Options.Iterations != desired.Iterations | ||||
|             || parsed.Options.Parallelism != desired.Parallelism; | ||||
|     } | ||||
|  | ||||
|     private static byte[] DeriveHash(string password, ReadOnlySpan<byte> salt, PasswordHashOptions options) | ||||
|         => DeriveHashCore(password, salt, options); | ||||
|  | ||||
|     private static partial byte[] DeriveHashCore(string password, ReadOnlySpan<byte> salt, PasswordHashOptions options); | ||||
|  | ||||
|     private static string BuildEncodedHash(ReadOnlySpan<byte> salt, ReadOnlySpan<byte> hash, PasswordHashOptions options) | ||||
|     { | ||||
|         var saltEncoded = Convert.ToBase64String(salt); | ||||
|         var hashEncoded = Convert.ToBase64String(hash); | ||||
|  | ||||
|         return $"$argon2id$v=19$m={options.MemorySizeInKib},t={options.Iterations},p={options.Parallelism}${saltEncoded}${hashEncoded}"; | ||||
|     } | ||||
|  | ||||
|     private static bool TryParse(string encodedHash, out Argon2HashParameters parsed) | ||||
|     { | ||||
|         parsed = default; | ||||
|  | ||||
|         if (!encodedHash.StartsWith("$argon2id$", StringComparison.Ordinal)) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         var segments = encodedHash.Split('$', StringSplitOptions.RemoveEmptyEntries); | ||||
|         if (segments.Length != 5) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         // segments: 0=argon2id, 1=v=19, 2=m=...,t=...,p=..., 3=salt, 4=hash | ||||
|         if (!segments[1].StartsWith("v=19", StringComparison.Ordinal)) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         var parameterParts = segments[2].Split(',', StringSplitOptions.RemoveEmptyEntries); | ||||
|         if (parameterParts.Length != 3) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         if (!TryParseInt(parameterParts[0], "m", out var memory) || | ||||
|             !TryParseInt(parameterParts[1], "t", out var iterations) || | ||||
|             !TryParseInt(parameterParts[2], "p", out var parallelism)) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         byte[] saltBytes; | ||||
|         byte[] hashBytes; | ||||
|         try | ||||
|         { | ||||
|             saltBytes = Convert.FromBase64String(segments[3]); | ||||
|             hashBytes = Convert.FromBase64String(segments[4]); | ||||
|         } | ||||
|         catch (FormatException) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         if (saltBytes.Length != SaltLengthBytes || hashBytes.Length != HashLengthBytes) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         var options = new PasswordHashOptions | ||||
|         { | ||||
|             Algorithm = PasswordHashAlgorithm.Argon2id, | ||||
|             MemorySizeInKib = memory, | ||||
|             Iterations = iterations, | ||||
|             Parallelism = parallelism | ||||
|         }; | ||||
|  | ||||
|         parsed = new Argon2HashParameters(options, saltBytes, hashBytes); | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     private static bool TryParseInt(string component, string key, out int value) | ||||
|     { | ||||
|         value = 0; | ||||
|         if (!component.StartsWith(key + "=", StringComparison.Ordinal)) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return int.TryParse(component.AsSpan(key.Length + 1), NumberStyles.None, CultureInfo.InvariantCulture, out value); | ||||
|     } | ||||
|  | ||||
|     private readonly struct Argon2HashParameters | ||||
|     { | ||||
|         public Argon2HashParameters(PasswordHashOptions options, byte[] salt, byte[] hash) | ||||
|         { | ||||
|             Options = options; | ||||
|             Salt = salt; | ||||
|             Hash = hash; | ||||
|         } | ||||
|  | ||||
|         public PasswordHashOptions Options { get; } | ||||
|         public byte[] Salt { get; } | ||||
|         public byte[] Hash { get; } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user