TOTP.ps1

#requires -version 2
<#
.SYNOPSIS
  Time-base One-Time Password Algorithm (RFC 6238)

.DESCRIPTION
  This is an implementation of the RFC 6238 Time-Based One-Time Password Algorithm draft based upon the HMAC-based One-Time Password (HOTP) algorithm (RFC 4226). This is a time based variant of the HOTP algorithm providing short-lived OTP values.

.NOTES
  Version: 1.0
  Author: Jon Friesen
  Creation Date: May 7, 2015
  Purpose/Change: Provide an easy way of generating OTPs

#>


function Get-Otp() {
    param(
        [Parameter(Mandatory = $true)]$TokenSecret,
        $TokenLength = 6,
        $TokenWindow = 30
    )
    $hmac = New-Object -TypeName System.Security.Cryptography.HMACSHA1
    $hmac.key = Convert-HexToByteArray(Convert-Base32ToHex(($TokenSecret.ToUpper())))
    $timeBytes = Get-TimeByteArray $TokenWindow
    $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, $TokenLength)
    $otp = $fullOTP % $modNumber
    $otp = $otp.ToString('0' * $TokenLength)
    return $otp
}

# Get-OTPRemainingSecondsLeft returns how many seconds are left in the current TOTP window. In a script that needs to wait until the next code is generated, use like $RetryDelayInSeconds = Get-OTPRemainingSecondsLeft; Start-Sleep -Seconds $RetryDelayInSeconds
function Get-OTPRemainingSecondsLeft ([int32]$TokenWindow = 30) {
    $span = New-TimeSpan -Start ( Get-Date -Year 1970 -Month 1 -Day 1 -Hour 0 -Minute 0 -Second 0) -End (Get-Date).ToUniversalTime()
    $seconds = [math]::floor($span.TotalSeconds)
    $counter = [math]::floor($seconds / $TokenWindow)
    $nextTimeStep = ($counter + 1) * $TokenWindow
    $difference = $nextTimeStep - $seconds
    return $difference
}

function Get-TimeByteArray($TokenWindow) {
    $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 / $TokenWindow))
    $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;
}

New-Alias -Name otp -Value Get-Otp