Private/TwoFactor.ps1

function Get-Monocle2FAInterval
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [DateTime]
        $DateTime
    )

    # convert to utc
    $DateTime = $DateTime.ToUniversalTime()

    # get time interval for the date
    $secondsPerInterval = 30
    $epochTime = Get-Date "01/01/1970 00:00:00"
    $secondsSinceEpochTime = (New-TimeSpan -Start $epochTime -End $DateTime).TotalSeconds

    return [int64][math]::Floor($secondsSinceEpochTime / $secondsPerInterval)
}

function Get-Monocle2FAPin
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $Secret,

        [Parameter(Mandatory=$true)]
        [long]
        $Interval
    )

    # convert the parameters to bytes
    $secretAsBytes = Convert-Monocle2FASecretToBytes -Secret $Secret
    $timeBytes = Convert-Monocle2FAIntervalToBytes -Interval $Interval

    # do the HMAC calculation with the default SHA1
    $hmacGen = [Security.Cryptography.HMACSHA1]::new($secretAsBytes)
    $hash = $hmacGen.ComputeHash($timeBytes)

    # take half the last byte
    $offset = ($hash[$hash.Length - 1] -band 0xF)

    # use it as an index into the hash bytes and take 4 bytes from there, big-endian needed
    $fourBytes = $hash[$offset..($offset + 3)]
    if ([BitConverter]::IsLittleEndian) {
        [array]::Reverse($fourBytes)
    }

    # remove the most significant bit
    $num = ([BitConverter]::ToInt32($fourBytes, 0) -band 0x7FFFFFFF)
    return ($num % 1000000).ToString().PadLeft(6, '0')
}

function Convert-Monocle2FASecretToBytes
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $Secret
    )

    $Base32Charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'

    # convert the secret from BASE32 to a byte array via a BigInteger so we can use its bit-shifting support
    $bigInteger = [Numerics.BigInteger]::Zero
    foreach ($char in ($secret.ToUpper() -replace '[^A-Z2-7]').GetEnumerator()) {
        $bigInteger = (($bigInteger -shl 5) -bor ($Base32Charset.IndexOf($char)))
    }

    [byte[]]$secretAsBytes = $bigInteger.ToByteArray()

    # BigInteger sometimes adds a 0 byte to the end, if it happens, we need to remove it
    if ($secretAsBytes[-1] -eq 0) {
        $secretAsBytes = $secretAsBytes[0..($secretAsBytes.Count - 2)]
    }

    # BigInteger stores bytes in Little-Endian order, but we need them in Big-Endian order.
    [array]::Reverse($secretAsBytes)
    return $secretAsBytes
}

function Convert-Monocle2FAIntervalToBytes
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [long]
        $Interval
    )

    $timeBytes = [BitConverter]::GetBytes($Interval)
    if ([BitConverter]::IsLittleEndian) {
        [array]::Reverse($timeBytes)
    }

    return $timeBytes
}