Skip to content

Instantly share code, notes, and snippets.

@zbalkan
Last active February 5, 2024 12:41
Show Gist options
  • Save zbalkan/1ba3dfa0c15c84875713281b3080c078 to your computer and use it in GitHub Desktop.
Save zbalkan/1ba3dfa0c15c84875713281b3080c078 to your computer and use it in GitHub Desktop.
TOTP (Time-based One-time Password) cmdlet
<#
.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]
}
}
}
}
@asmith3006
Copy link

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment