Private/ValidationHelpers.ps1

<#
.SYNOPSIS
    Input validation functions for OATH token management
.DESCRIPTION
    Private functions for validating user input, token properties,
    and request parameters for OATH token management operations.
.NOTES
    These helpers ensure consistent validation across the module.
#>


function Test-OATHSerialNumber {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string]$SerialNumber,
        
        [Parameter()]
        [switch]$AllowEmpty,
        
        [Parameter()]
        [int]$MinLength = 1,
        
        [Parameter()]
        [int]$MaxLength = 64
    )
    
    process {
        try {
            if ([string]::IsNullOrWhiteSpace($SerialNumber)) {
                if ($AllowEmpty) {
                    return $true
                }
                Write-Warning "Serial number cannot be empty"
                return $false
            }
            
            # Check length
            if ($SerialNumber.Length -lt $MinLength -or $SerialNumber.Length -gt $MaxLength) {
                Write-Warning "Serial number must be between $MinLength and $MaxLength characters"
                return $false
            }
            
            # Typical validations for YubiKey serial numbers: alphanumeric plus some symbols
            if (-not [regex]::IsMatch($SerialNumber, '^[a-zA-Z0-9._-]+$')) {
                Write-Warning "Serial number contains invalid characters. Use only letters, numbers, periods, underscores, and hyphens."
                return $false
            }
            
            return $true
        }
        catch {
            Write-Error "Error validating serial number: $_"
            return $false
        }
    }
}

function Test-OATHSecretKey {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string]$SecretKey,
        
        [Parameter()]
        [ValidateSet('Base32', 'Hex', 'Text')]
        [string]$Format = 'Base32'
    )
    
    process {
        try {
            if ([string]::IsNullOrWhiteSpace($SecretKey)) {
                Write-Warning "Secret key cannot be empty"
                return $false
            }
            
            switch ($Format) {
                'Base32' {
                    # Base32 format: uppercase A-Z, digits 2-7, optional padding
                    if (-not [regex]::IsMatch($SecretKey, '^[A-Z2-7]+=*$')) {
                        Write-Warning "Invalid Base32 format. Must contain only A-Z, 2-7, with optional '=' padding."
                        return $false
                    }
                }
                'Hex' {
                    # Hex format: 0-9, a-f, A-F, even number of characters
                    $cleanHex = $SecretKey -replace '[-: ]', ''
                    if (-not [regex]::IsMatch($cleanHex, '^[0-9a-fA-F]+$')) {
                        Write-Warning "Invalid hexadecimal format. Must contain only 0-9, A-F."
                        return $false
                    }
                    
                    if ($cleanHex.Length % 2 -ne 0) {
                        Write-Warning "Hexadecimal string must have an even number of characters"
                        return $false
                    }
                }
                'Text' {
                    # Text format: any printable character, reasonable length
                    if ($SecretKey.Length -lt 1 -or $SecretKey.Length -gt 100) {
                        Write-Warning "Text secret must be between 1 and 100 characters"
                        return $false
                    }
                }
            }
            
            return $true
        }
        catch {
            Write-Error "Error validating secret key: $_"
            return $false
        }
    }
}

function Test-OATHTokenId {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string]$TokenId
    )
    
    process {
        try {
            if ([string]::IsNullOrWhiteSpace($TokenId)) {
                Write-Warning "Token ID cannot be empty"
                return $false
            }
            
            # GUID format validation
            if (-not [regex]::IsMatch($TokenId, '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')) {
                Write-Warning "Invalid Token ID format. Must be a valid GUID."
                return $false
            }
            
            return $true
        }
        catch {
            Write-Error "Error validating Token ID: $_"
            return $false
        }
    }
}

function Test-OATHToken {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [object]$Token,
        
        [Parameter()]
        [switch]$SkipSecretValidation
    )
    
    try {
        # Check required properties
        if (-not $Token.serialNumber) {
            Write-Warning "Token is missing required 'serialNumber' property"
            return $false
        }
        
        # Validate serial number
        if (-not (Test-OATHSerialNumber -SerialNumber $Token.serialNumber)) {
            return $false
        }
        
        # Skip secret validation if requested (for existing tokens)
        if (-not $SkipSecretValidation) {
            if (-not $Token.secretKey) {
                Write-Warning "Token is missing required 'secretKey' property"
                return $false
            }
            
            # Determine format
            $format = 'Base32'
            if ($Token.secretFormat) {
                switch ($Token.secretFormat.ToLower()) {
                    'hex' { $format = 'Hex' }
                    'text' { $format = 'Text' }
                }
            }
            
            # Validate secret key
            if (-not (Test-OATHSecretKey -SecretKey $Token.secretKey -Format $format)) {
                return $false
            }
        }
        
        # Optional property validation
        if ($Token.hashFunction -and $Token.hashFunction -notin @('hmacsha1', 'hmacsha256', 'hmacsha512')) {
            Write-Warning "Invalid 'hashFunction' value. Must be one of: 'hmacsha1', 'hmacsha256', 'hmacsha512'"
            return $false
        }
        
        if ($Token.timeIntervalInSeconds -and -not ($Token.timeIntervalInSeconds -ge 10 -and $Token.timeIntervalInSeconds -le 120)) {
            Write-Warning "Invalid 'timeIntervalInSeconds' value. Must be between 10 and 120."
            return $false
        }
        
        return $true
    }
    catch {
        Write-Error "Error validating token: $_"
        return $false
    }
}

function Test-OATHVerificationCode {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string]$VerificationCode,
        
        [Parameter()]
        [int]$ExpectedLength = 6
    )
    
    process {
        try {
            if ([string]::IsNullOrWhiteSpace($VerificationCode)) {
                Write-Warning "Verification code cannot be empty"
                return $false
            }
            
            if ($VerificationCode.Length -ne $ExpectedLength) {
                Write-Warning "Verification code must be exactly $ExpectedLength digits"
                return $false
            }
            
            if (-not [regex]::IsMatch($VerificationCode, '^\d+$')) {
                Write-Warning "Verification code must contain only digits"
                return $false
            }
            
            return $true
        }
        catch {
            Write-Error "Error validating verification code: $_"
            return $false
        }
    }
}