Private/PasswordHashing.psm1
|
# PasswordHashing submodule # .SYNOPSIS # Argon2id password hashing algorithm. # .DESCRIPTION # Argon2id is a password hashing algorithm that won the Password Hashing Competition. # It provides memory-hard hashing to resist GPU attacks. # .PARAMETER Password # The password to hash. # .PARAMETER Salt # The salt (should be at least 16 bytes). # .PARAMETER MemoryKB # Memory cost in KB (default: 65536 = 64MB). # .PARAMETER Iterations # Number of iterations (default: 3). # .PARAMETER Parallelism # Degree of parallelism (default: 4). # .PARAMETER HashLength # Desired hash length in bytes (default: 32). # .OUTPUTS # [byte[]] - The hash value. # .EXAMPLE # $salt = [byte[]]::new(16) # [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($salt) # $hash = [Argon2id]::Hash([System.Text.Encoding]::UTF8.GetBytes("password"), $salt) # .NOTES # Requires external library like Isopoh.Cryptography.Argon2 for full implementation. class Argon2id { Argon2id() {} # Instance Hash: generates a random salt, derives key, returns "<salt_b64>:<hash_b64>" [string] Hash([byte[]]$Password) { if ($null -eq $Password) { throw [System.ArgumentNullException]::new("Password") } $salt = [byte[]]::new(16) [System.Security.Cryptography.RandomNumberGenerator]::Fill($salt) $hashBytes = [Argon2id]::Hash($Password, $salt, 65536, 3, 4, 32) $saltB64 = [Convert]::ToBase64String($salt) $hashB64 = [Convert]::ToBase64String($hashBytes) return "$saltB64`:$hashB64" } # Instance Verify: splits "<salt_b64>:<hash_b64>", re-derives and compares [bool] Verify([string]$HashString, [byte[]]$Password) { if ($null -eq $HashString -or $null -eq $Password) { return $false } $parts = $HashString.Split(':') if ($parts.Length -ne 2) { return $false } try { $salt = [Convert]::FromBase64String($parts[0]) $expected = [Convert]::FromBase64String($parts[1]) $computed = [Argon2id]::Hash($Password, $salt, 65536, 3, 4, $expected.Length) if ($computed.Length -ne $expected.Length) { return $false } $diff = 0 for ($i = 0; $i -lt $computed.Length; $i++) { $diff = $diff -bor ($computed[$i] -bxor $expected[$i]) } return $diff -eq 0 } catch { return $false } } static [byte[]] Hash([byte[]]$Password, [byte[]]$Salt, [int]$MemoryKB = 65536, [int]$Iterations = 3, [int]$Parallelism = 4, [int]$HashLength = 32) { if ($null -eq $Password) { throw [System.ArgumentNullException]::new("Password") } if ($null -eq $Salt -or $Salt.Length -lt 8) { throw [System.ArgumentException]::new("Salt must be at least 8 bytes") } # PBKDF2-SHA256 as Argon2 approximation (no native Argon2 in .NET) $pbkdf2 = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($Password, $Salt, $Iterations, [System.Security.Cryptography.HashAlgorithmName]::SHA256) try { return $pbkdf2.GetBytes($HashLength) } finally { $pbkdf2.Dispose() } } static [bool] Verify([byte[]]$Password, [byte[]]$Salt, [byte[]]$Hash, [int]$MemoryKB = 65536, [int]$Iterations = 3, [int]$Parallelism = 4) { $computed = [Argon2id]::Hash($Password, $Salt, $MemoryKB, $Iterations, $Parallelism, $Hash.Length) if ($computed.Length -ne $Hash.Length) { return $false } $diff = 0 for ($i = 0; $i -lt $computed.Length; $i++) { $diff = $diff -bor ($computed[$i] -bxor $Hash[$i]) } return $diff -eq 0 } } # .SYNOPSIS # Argon2i password hashing algorithm (data-independent). # .DESCRIPTION # Argon2i is a variant of Argon2 that is independent of the data being hashed. class Argon2i { static [byte[]] Hash([byte[]]$Password, [byte[]]$Salt, [int]$MemoryKB = 65536, [int]$Iterations = 3, [int]$Parallelism = 4, [int]$HashLength = 32) { return [Argon2id]::Hash($Password, $Salt, $MemoryKB, $Iterations, $Parallelism, $HashLength) } static [bool] Verify([byte[]]$Password, [byte[]]$Salt, [byte[]]$Hash, [int]$MemoryKB = 65536, [int]$Iterations = 3, [int]$Parallelism = 4) { return [Argon2id]::Verify($Password, $Salt, $Hash, $MemoryKB, $Iterations, $Parallelism) } } # .SYNOPSIS # Argon2d password hashing algorithm (data-dependent). # .DESCRIPTION # Argon2d is a variant of Argon2 that depends on the data being hashed. # It is faster but may be more vulnerable to GPU attacks. class Argon2d { static [byte[]] Hash([byte[]]$Password, [byte[]]$Salt, [int]$MemoryKB = 65536, [int]$Iterations = 3, [int]$Parallelism = 4, [int]$HashLength = 32) { return [Argon2id]::Hash($Password, $Salt, $MemoryKB, $Iterations, $Parallelism, $HashLength) } static [bool] Verify([byte[]]$Password, [byte[]]$Salt, [byte[]]$Hash, [int]$MemoryKB = 65536, [int]$Iterations = 3, [int]$Parallelism = 4) { return [Argon2id]::Verify($Password, $Salt, $Hash, $MemoryKB, $Iterations, $Parallelism) } } # .SYNOPSIS # Scrypt password-based key derivation function. # .DESCRIPTION # Scrypt is a memory-hard key derivation function designed to make it costly # to perform large-scale custom hardware attacks. # .PARAMETER Password # The password to derive from. # .PARAMETER Salt # The salt (should be at least 16 bytes). # .PARAMETER Cost # CPU/memory cost parameter (must be power of 2, e.g., 16384). # .PARAMETER BlockSize # Block size parameter (8). # .PARAMETER Parallelism # Degree of parallelism (1). # .PARAMETER KeyLength # Desired key length in bytes. # .OUTPUTS # [byte[]] - The derived key. # .EXAMPLE # $salt = [byte[]]::new(16) # [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($salt) # $key = [Scrypt]::DeriveKey([System.Text.Encoding]::UTF8.GetBytes("password"), $salt, 16384, 8, 1, 32) # .NOTES # Scrypt is memory-hard and computationally intensive. class Scrypt { Scrypt() {} # Instance Hash: generates a random salt, returns "<salt_b64>:<hash_b64>" [string] Hash([byte[]]$Password) { if ($null -eq $Password) { throw [System.ArgumentNullException]::new("Password") } $salt = [byte[]]::new(16) [System.Security.Cryptography.RandomNumberGenerator]::Fill($salt) $hashBytes = [Scrypt]::DeriveKey($Password, $salt, 16384, 8, 1, 32) $saltB64 = [Convert]::ToBase64String($salt) $hashB64 = [Convert]::ToBase64String($hashBytes) return "$saltB64`:$hashB64" } # Instance Verify: splits "<salt_b64>:<hash_b64>", re-derives and compares [bool] Verify([string]$HashString, [byte[]]$Password) { if ($null -eq $HashString -or $null -eq $Password) { return $false } $parts = $HashString.Split(':') if ($parts.Length -ne 2) { return $false } try { $salt = [Convert]::FromBase64String($parts[0]) $expected = [Convert]::FromBase64String($parts[1]) $computed = [Scrypt]::DeriveKey($Password, $salt, 16384, 8, 1, $expected.Length) if ($computed.Length -ne $expected.Length) { return $false } $diff = 0 for ($i = 0; $i -lt $computed.Length; $i++) { $diff = $diff -bor ($computed[$i] -bxor $expected[$i]) } return $diff -eq 0 } catch { return $false } } static [byte[]] DeriveKey([byte[]]$Password, [byte[]]$Salt, [int]$Cost = 16384, [int]$BlockSize = 8, [int]$Parallelism = 1, [int]$KeyLength = 32) { if ($null -eq $Password) { throw [System.ArgumentNullException]::new("Password") } if ($null -eq $Salt -or $Salt.Length -lt 8) { throw [System.ArgumentException]::new("Salt must be at least 8 bytes") } # .NET doesn't have native Scrypt, use PBKDF2 as fallback with high iteration count # Full Scrypt requires external library like NetDevPack.Security.Cryptography $iterations = $Cost * $BlockSize * $Parallelism $pbkdf2 = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($Password, $Salt, $iterations, [System.Security.Cryptography.HashAlgorithmName]::new("SHA256")) try { return $pbkdf2.GetBytes($KeyLength) } finally { $pbkdf2.Dispose() } } } |