Last active
February 5, 2024 12:41
-
-
Save zbalkan/1ba3dfa0c15c84875713281b3080c078 to your computer and use it in GitHub Desktop.
TOTP (Time-based One-time Password) cmdlet
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.Synopsis | |
Time-based One-Time Password Algorithm (RFC 6238) | |
.DESCRIPTION | |
Based on the script of Jon Friesen - https://gist.github.com/jonfriesen/234c7471c3e3199f97d5 | |
.EXAMPLE | |
Get-OTP -Secret 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' # Default OTP length is 6 digits and period is 30 seconds | |
.EXAMPLE | |
totp -Secret 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' # you can use totp or otp alias | |
.EXAMPLE | |
Get-OTP -Secret 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' -Digits 8 -Period 60 | |
.EXAMPLE | |
totp -KeyURI 'otpauth://totp/ACME%20Co:[email protected]?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=8&period=60' # You can use Google Authenticator scheme by providing KeyURI parameter | |
#> | |
function Get-OTP | |
{ | |
[CmdletBinding(DefaultParameterSetName='Use Secret', | |
SupportsShouldProcess=$true, | |
PositionalBinding=$false, | |
ConfirmImpact='Low')] | |
[Alias('totp')] | |
[OutputType([String])] | |
Param | |
( | |
# OTP Secret as string | |
[Parameter(Mandatory=$true, | |
ValueFromPipeline=$true, | |
ValueFromPipelineByPropertyName=$true, | |
ValueFromRemainingArguments=$false, | |
HelpMessage="Enter the OTP Secret", | |
ParameterSetName='Use Secret', | |
Position=0)] | |
[ValidateNotNull()] | |
[ValidateNotNullOrEmpty()] | |
[ValidateCount(8,128)] | |
[ValidateScript({ | |
If ( ($_ -match '^([ABCDEFGHIJKLMNOPQRSTUVWXYZ234567]*)$') -and (($_ * 8) % 5 -eq 0)){ | |
$True | |
} | |
else { | |
Throw "The secret $_ is not valid." | |
} | |
})] | |
[Alias("s")] | |
[string] | |
$Secret, | |
# OTP Secret as string | |
[Parameter(Mandatory=$true, | |
ValueFromPipeline=$true, | |
ValueFromPipelineByPropertyName=$true, | |
ValueFromRemainingArguments=$false, | |
HelpMessage="Enter the OTP Secret", | |
ParameterSetName='Use KeyURI', | |
Position=0)] | |
[ValidateNotNull()] | |
[ValidateNotNullOrEmpty()] | |
[ValidateScript({ | |
If ($_ -match '^(otpauth\:\/\/totp\/[a-zA-Z_0-9\%]*\:[a-zA-Z_0-9\@\.\%]*\?secret\=[ABCDEFGHIJKLMNOPQRSTUVWXYZ234567]*.*)$') { | |
$True | |
} | |
else { | |
Throw "$_ is not valid" | |
} | |
})] | |
[Alias("k")] | |
[string] | |
$KeyURI, | |
# OTP length | |
[Parameter(Mandatory=$false, | |
ValueFromPipeline=$false, | |
ValueFromPipelineByPropertyName=$false, | |
ValueFromRemainingArguments=$false, | |
HelpMessage="Enter the OTP Length", | |
Position=1)] | |
[ValidateRange(6,40)] | |
[Alias("d")] | |
[int] | |
$Digits = 6, | |
# OTP Time-Step in seconds | |
[Parameter(Mandatory=$false, | |
ValueFromPipeline=$false, | |
ValueFromPipelineByPropertyName=$false, | |
ValueFromRemainingArguments=$false, | |
HelpMessage="Enter the OTP period in seconds", | |
Position=2)] | |
[ValidateRange(15,300)] | |
[Alias("p")] | |
[int] | |
$Period = 30 | |
) | |
Begin | |
{ | |
function Get-TimeByteArray($WINDOW) { | |
$span = (New-TimeSpan -Start (Get-Date -Year 1970 -Month 1 -Day 1 -Hour 0 -Minute 0 -Second 0) -End (Get-Date).ToUniversalTime()).TotalSeconds | |
$unixTime = [Convert]::ToInt64([Math]::Floor($span/$WINDOW)) | |
$byteArray = [BitConverter]::GetBytes($unixTime) | |
[array]::Reverse($byteArray) | |
return $byteArray | |
} | |
function Convert-HexToByteArray($hexString) { | |
$byteArray = $hexString -replace '^0x', '' -split "(?<=\G\w{2})(?=\w{2})" | ForEach-Object { [Convert]::ToByte( $_, 16 ) } | |
return $byteArray | |
} | |
function Convert-Base32ToHex($base32) { | |
$base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; | |
$bits = ""; | |
$hex = ""; | |
for ($i = 0; $i -lt $base32.Length; $i++) { | |
$val = $base32chars.IndexOf($base32.Chars($i)); | |
$binary = [Convert]::ToString($val, 2) | |
$staticLen = 5 | |
$padder = '0' | |
# Write-Host $binary | |
$bits += Add-LeftPad $binary.ToString() $staticLen $padder | |
} | |
for ($i = 0; $i+4 -le $bits.Length; $i+=4) { | |
$chunk = $bits.Substring($i, 4) | |
# Write-Host $chunk | |
$intChunk = [Convert]::ToInt32($chunk, 2) | |
$hexChunk = Convert-IntToHex($intChunk) | |
# Write-Host $hexChunk | |
$hex = $hex + $hexChunk | |
} | |
return $hex; | |
} | |
function Convert-IntToHex([int]$num) { | |
return ('{0:x}' -f $num) | |
} | |
function Add-LeftPad($str, $len, $pad) { | |
if(($len + 1) -ge $str.Length) { | |
while (($len - 1) -ge $str.Length) { | |
$str = ($pad + $str) | |
} | |
} | |
return $str; | |
} | |
} | |
Process | |
{ | |
if ($pscmdlet.ShouldProcess($Secret, "OTP ($Digits digits) calculation for $Period seconds")) | |
{ | |
try | |
{ | |
if("Use KeyURI" -eq $PSCmdlet.ParameterSetName) | |
{ | |
$matchSecret = $KeyURI -match 'secret=([ABCDEFGHIJKLMNOPQRSTUVWXYZ234567]*)' | |
if ($matchSecret) { $Secret = $Matches[1] } | |
else { Write-Error "Invalid secret."; return } | |
$validBase32 = (($Secret * 8) % 5) -eq 0 | |
if ($validBase32 -eq $false) { Write-Error "Invalid secret."; return } | |
$matchDigit = $KeyURI -match 'digits=([0-9]*)' | |
if ($matchDigit) { | |
$d = [int]::Parse($Matches[1]) | |
if ($d -lt 6) { Write-Error "Cannot validate argument on parameter 'Digits'. The $d argument is less than the minimum allowed range of 6. Supply an argument that is greater than or equal to 6 and then try the command again."; return } | |
if ($d -gt 40) { Write-Error "Cannot validate argument on parameter 'Digits'. The $d argument is greater than the maximum allowed range of 40. Supply an argument that is less than or equal to 40 and then try the command again."; return } | |
$Digits = $d | |
} | |
$matchPeriod = $KeyURI -match 'period=([0-9]*)' | |
if ($matchPeriod) { | |
$p = [int]::Parse($Matches[1]) | |
if ($p -lt 15) { Write-Error "Cannot validate argument on parameter 'Period'. The $p argument is less than the minimum allowed range of 15. Supply an argument that is greater than or equal to 15 and then try the command again."; return } | |
if ($p -gt 300) { Write-Error "Cannot validate argument on parameter 'Period'. The $p argument is greater than the maximum allowed range of 300. Supply an argument that is less than or equal to 300 and then try the command again."; return } | |
$Period = $p | |
} | |
} | |
$hmac = New-Object -TypeName System.Security.Cryptography.HMACSHA1 | |
$hmac.key = Convert-HexToByteArray(Convert-Base32ToHex(($Secret.ToUpper()))) | |
$timeBytes = Get-TimeByteArray $Period | |
$randHash = $hmac.ComputeHash($timeBytes) | |
$offset = $randhash[($randHash.Length - 1)] -band 0xf | |
$fullOTP = ($randhash[$offset] -band 0x7f) * [math]::pow(2, 24) | |
$fullOTP += ($randHash[$offset + 1] -band 0xff) * [math]::pow(2, 16) | |
$fullOTP += ($randHash[$offset + 2] -band 0xff) * [math]::pow(2, 8) | |
$fullOTP += ($randHash[$offset + 3] -band 0xff) | |
$modNumber = [math]::pow(10, $Digits) | |
$otp = $fullOTP % $modNumber | |
$otp = $otp.ToString("0" * $Digits) | |
return $otp | |
} | |
catch | |
{ | |
Write-Error $Error[0] | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Line 37
-and (($_ * 8) % 5 -eq 0)
is causing the script to crash for me (error converting to Int). Removing this makes it work.Love it!