public/Get-OTPCode.ps1
# Get-OTPCode.ps1 <# .SYNOPSIS Generates One-Time Password (OTP) codes. .DESCRIPTION This cmdlet generates TOTP (Time-based One-Time Password) or HOTP (HMAC-based One-Time Password) codes using a provided seed value. It supports both time-based and counter-based algorithms and different hash algorithms (SHA1, SHA256, SHA512). The function supports caching of decoded seeds for improved performance with TOTP codes, and provides both console and GUI interfaces for code generation. Security Note: - SHA1 is supported for compatibility but SHA256 or SHA512 are recommended - Minimum seed length is 16 Base32 characters (80 bits) for basic security - Seeds should be kept secure and never shared The Get-OTPCode function generates Time-based (TOTP) or HMAC-based (HOTP) one-time password codes using the provided secret key. It supports various hash algorithms and can display codes in a user-friendly interface. Features: - Supports both TOTP and HOTP algorithms - Multiple hash algorithms (SHA1, SHA256, SHA512) - Configurable code length (6-8 digits) - Optional console or GUI display - Tag-based organization - Pipeline support for batch processing - QR code integration The generated codes are compatible with common authenticator apps and follow RFC 4226 (HOTP) and RFC 6238 (TOTP) specifications. .PARAMETER Seed The Base32 encoded secret key used to generate the OTP code. Minimum recommended length is 16 characters (80 bits). Must contain only characters A-Z, 2-7 and optionally padding with =. .PARAMETER Algorithm The OTP algorithm to use. Valid values are 'TOTP' (default) and 'HOTP'. - TOTP: Time-based OTP, generates codes based on current time - HOTP: Counter-based OTP, generates codes based on counter value .PARAMETER Counter The counter value for HOTP algorithm. Must be a non-negative integer. Ignored for TOTP. Each code generation should increment this value. .PARAMETER HashAlgorithm The hash algorithm to use. Valid values are: - SHA1 (default, 160 bits) - Supported for compatibility - SHA256 (256 bits, more secure) - Recommended - SHA512 (512 bits, most secure) - Recommended for high security .PARAMETER ShowUI Switch parameter to display a WPF window with live OTP updates. Provides a graphical interface with auto-updating TOTP codes. .PARAMETER ForceConsole Switch parameter to force using console-based output even when WPF UI is available. Useful in environments where GUI is not desired or available. .PARAMETER Tag Optional tag to filter OTP codes. Accepts input from pipeline by property name. .PARAMETER Path Path to the QR code image file. Accepts input from pipeline by property name. This parameter is hidden from autocompletion. .PARAMETER IncludePath Switch to include the Path value in the Tag array. When used with -Path, the path will be added as a tag. .EXAMPLE New-OTPSecret | Get-OTPCode Generates a new random seed and gets its current OTP code. .EXAMPLE Get-OTPCode -Seed 'JBSWY3DPEHPK3PXP' -Algorithm 'HOTP' -Counter 1 Generates an HOTP code using a specific seed and counter. .EXAMPLE Get-OTPCode -Seed 'JBSWY3DPEHPK3PXP' -ShowUI -ForceConsole Generates a TOTP code and displays it in the console interface, even on Windows. .EXAMPLE Get-OTPCode -Seed 'JBSWY3DPEHPK3PXP' -HashAlgorithm SHA256 Generates a TOTP code using SHA256 for increased security. .EXAMPLE Get-OTPCode -Tag 'Work' Gets OTP codes for all work-related accounts. .EXAMPLE $config = Read-OTPQRCode -Path 'qrcode.png' $config | Get-OTPCode Gets OTP code for a configuration from a QR code. .NOTES The module uses secure random number generation and follows RFC 4226 (HOTP) and RFC 6238 (TOTP) specifications. It provides secure seed handling and supports modern hash algorithms for enhanced security. This version of the cmdlet was created using "Educated Prompting" by using Claude 3.5 Sonnect (by Anthropic) and Anysphere (cursor.sh). .LINK https://tools.ietf.org/html/rfc4226 https://tools.ietf.org/html/rfc6238 https://github.com/thorstenbutz/otp #> function Get-OTPCode { [CmdletBinding()] [Alias('gotp')] param( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string[]]$Seed, [Parameter()] [ValidateSet('TOTP', 'HOTP')] [string]$Algorithm = 'TOTP', [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(0, [int]::MaxValue)] [int]$Counter = 0, [Parameter(ValueFromPipelineByPropertyName)] [ValidateSet('SHA1', 'SHA256', 'SHA512')] [string]$HashAlgorithm = 'SHA1', [Parameter()] [switch]$ShowUI, [Parameter()] [switch]$ForceConsole, [Parameter(ValueFromPipelineByPropertyName)] [string[]]$Tag, [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.HiddenAttribute()] [string]$Path, [Parameter()] [switch]$IncludePath ) begin { # Cache for decoded Base32 seeds to improve performance $script:seedCache = @{} # Only show warning if SHA1 was explicitly chosen if ($PSBoundParameters.ContainsKey('HashAlgorithm') -and $HashAlgorithm -eq 'SHA1') { Write-Warning -Message 'SHA1 was explicitly selected. Consider using SHA256 or SHA512 for increased security.' } # Initialize UI if requested if ($ShowUI -or $ForceConsole) { $script:ui = New-OTPUI -ForceConsole:$ForceConsole if (-not $script:ui) { $ShowUI = $false $ForceConsole = $false return } } } process { foreach ($secretKey in $Seed) { try { # Convert to uppercase before validation $secretKey = $secretKey.ToUpperInvariant() # Add Path to Tag array if provided and IncludePath is specified if ($PSBoundParameters.ContainsKey('Path') -and $IncludePath) { if ($Tag) { $Tag = @($Tag) + $Path } else { $Tag = @($Path) } } # Enhanced Base32 validation with specific error messages if ([string]::IsNullOrWhiteSpace($secretKey)) { throw [System.ArgumentException]::new( 'Seed cannot be empty or whitespace.', 'Seed' ) } if (-not [regex]::IsMatch($secretKey, '^[A-Z2-7=]*$')) { throw [System.ArgumentException]::new( 'Invalid Base32 encoding in seed. Found invalid characters. Only A-Z, 2-7, and = are allowed.', 'Seed' ) } # Check seed length (minimum 16 bytes for security) $decodedLength = [Math]::Floor($secretKey.Replace('=', '').Length * 5 / 8) $warningMessage = "Seed length ($decodedLength bytes after Base32 decoding) is less than recommended minimum of 10 bytes. This may compromise security." if ($secretKey.Replace('=', '').Length -lt 16) { # Check Base32 character length directly Write-Warning $warningMessage } if ($VerbosePreference -eq 'Continue') { Write-Verbose "Processing seed: $secretKey" if ($Tag) { Write-Verbose "Tags: $($Tag -join ', ')" } } # Use cached decoded bytes if available $secretBytes = $null $cacheKey = "${secretKey}_${HashAlgorithm}" if (-not $script:seedCache.ContainsKey($cacheKey)) { try { $secretBytes = [OtpNet.Base32Encoding]::ToBytes($secretKey) # Only cache for TOTP as it's reused if ($Algorithm -eq 'TOTP') { $script:seedCache[$cacheKey] = $secretBytes } } catch { throw [System.ArgumentException]::new( "Failed to decode Base32 seed: $($_.Exception.Message)", 'Seed', $_.Exception ) } } else { $secretBytes = $script:seedCache[$cacheKey] } switch ($Algorithm) { 'TOTP' { try { $otp = [OtpNet.Totp]::new($secretBytes, 30, [OtpNet.OtpHashMode]::$HashAlgorithm) $code = $otp.ComputeTotp() if ($VerbosePreference -eq 'Continue') { $timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() Write-Verbose "Current Unix timestamp: $timestamp" Write-Verbose "Time step: 30 seconds" Write-Verbose "Hash algorithm: $HashAlgorithm" } } catch { throw [System.Security.SecurityException]::new( "Failed to compute TOTP code: $($_.Exception.Message)", $_.Exception ) } } 'HOTP' { try { $otp = [OtpNet.Hotp]::new($secretBytes, [OtpNet.OtpHashMode]::$HashAlgorithm) $code = $otp.ComputeHOTP($Counter) if ($VerbosePreference -eq 'Continue') { Write-Verbose "Current counter value: $Counter" Write-Verbose "Hash algorithm: $HashAlgorithm" } } catch { throw [System.Security.SecurityException]::new( "Failed to compute HOTP code: $($_.Exception.Message)", $_.Exception ) } } } $result = [PSCustomObject]@{ Code = $code Algorithm = $Algorithm HashAlgorithm = $HashAlgorithm Seed = $secretKey PSTypeName = 'OTP.Code' } # Add Tag property only if it has a value if ($Tag) { Add-Member -InputObject $result -MemberType NoteProperty -Name 'Tag' -Value $Tag if ($ShowUI -or $ForceConsole) { Add-Member -InputObject $result -MemberType NoteProperty -Name 'TagDisplay' -Value ($Tag -join ', ') } } elseif ($ShowUI -or $ForceConsole) { Add-Member -InputObject $result -MemberType NoteProperty -Name 'TagDisplay' -Value '' } if ($ShowUI -or $ForceConsole) { $script:ui.AddCode($result) } else { $result } } catch [System.ArgumentException] { Write-Error -ErrorRecord $_ } catch [System.Security.SecurityException] { Write-Error -ErrorRecord $_ } catch { $errorRecord = [System.Management.Automation.ErrorRecord]::new( $_.Exception, 'OTPGenerationError', [System.Management.Automation.ErrorCategory]::InvalidOperation, $secretKey ) Write-Error -ErrorRecord $errorRecord } } } end { if (($ShowUI -or $ForceConsole) -and $script:ui) { try { $script:ui.ShowCodes() } finally { if ($script:ui -is [System.IDisposable]) { $script:ui.Dispose() } $script:ui = $null # Clear the seed cache when done $script:seedCache.Clear() } } } } |