Private/OTPKIT.psm1

#!/usr/bin/env pwsh
using namespace System.Text
using namespace System.Security.Cryptography

using module ./Sha.psm1

class OTPKIT {
  [string] $key = ""

  static [string] CreateHOTP([string]$SECRET, [int]$Phone) {
    return [OTPKIT]::CreateHOTP($phone, [OTPKIT]::GetOtp($SECRET))
  }

  static [string] CreateHOTP([int]$phone, [string]$otp) {
    return [OTPKIT]::CreateHOTP($phone, $otp, 5)
  }
  static [string] CreateHOTP([int]$phone, [string]$otp, [int]$expiresAfter) {
    $ttl = $expiresAfter * 60 * 1000
    $expires = (Get-Date).AddMilliseconds($ttl).ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
    $bytes = [byte[]]::new(16)
    [RNGCryptoServiceProvider]::new().GetBytes($bytes)
    $salt = [BitConverter]::ToString($bytes) -replace '-'
    $data = "$phone.$otp.$expires.$salt"
    Write-Host "data: " -NoNewline -ForegroundColor Green; Write-Host "$data" -ForegroundColor Blue
    $hashBase = [FipsHmacSha256]::new().ComputeHash([Encoding]::ASCII.GetBytes($data))
    $hash = "$hashBase.$expires.$salt"
    # Import the Twilio module
    Import-Module -Name Twilio
    $phoneNumber = "+1234567890"
    $message = "Hello, this is a test message."
    [OTPKIT]::Send_TWILIO_SMS($phoneNumber, $message)
    return $hash
  }
  static [bool] Send_TWILIO_SMS ([string]$PhoneNumber, [string]$Message) {
    # Function to send SMS using Twilio
    # Twilio account SID and auth token
    $accountSid = "YOUR_TWILIO_ACCOUNT_SID"
    $authToken = "YOUR_TWILIO_AUTH_TOKEN"

    # Twilio phone number
    $twilioPhoneNumber = "YOUR_TWILIO_PHONE_NUMBER"

    # Create a new Twilio client
    $twilio = New-TwilioRestClient -AccountSid $accountSid -AuthToken $authToken

    # Send the SMS message
    $twilio.SendMessage($twilioPhoneNumber, $PhoneNumber, $Message)
    return $?
  }
  static [bool] VerifyHOTP([string]$otp, [int]$phone, [string]$hash) {
    if (!$hash -match "\.") {
      return $false
    }

    $hashValue, $expires, $salt = $hash -split "\."

    $now = (Get-Date).Ticks / 10000
    if ($now -gt [double]$expires) {
      return $false
    }
    $data = "$phone.$otp.$expires.$salt"
    Write-Host "data: " -NoNewline -ForegroundColor Green; Write-Host "$data" -ForegroundColor Blue
    $newCalculatedHash = [FipsHmacSha256]::new().ComputeHash([Encoding]::ASCII.GetBytes($data))

    if ($newCalculatedHash -eq $hashValue) {
      return $true
    }
    return $false
  }

  static [string] ParseOtpUrl([string]$otpURL) {
    # $otpURL can be decrypted text
    if (![System.Uri]::IsWellFormedUriString("$otpURL", "Absolute") -or $otpURL -notmatch "^otpauth://") { Write-Host "The decrypted text is not a valid OTP URL" "Error" ; $script:FileBrowser.Dispose() ; exit 1 }
    $parseOtpUrl = [scriptblock]::Create("[System.Web.HttpUtility]::ParseQueryString(([uri]::new('$otpURL')).Query)").Invoke()
    $otpType = $([uri]$otpURL).Host
    if ($otpType -eq "hotp") { Write-Warning "TOTP is only supported" }
    if ($otpType -eq "totp" ) { $otpType = "$otpType=" }
    $otpPeriod = if (![string]::IsNullOrEmpty($parseOtpUrl["period"])) { $parseOtpUrl["period"] } else { 30 }
    $otpDigits = if (![string]::IsNullOrEmpty($parseOtpUrl["digits"])) { $parseOtpUrl["digits"] } else { 6 }
    $otpSecret = $parseOtpUrl["secret"]
    return [OTPKIT]::GetOtp($otpSecret, $otpDigits, $otpPeriod)
  }

  static [string] GetOtp([string]$SECRET) {
    return [OTPKIT]::GetOtp($SECRET, 4, "5")
  }
  static [string] GetOtp([string]$SECRET, [int]$LENGTH, [string]$WINDOW) {
    $hmac = New-Object -TypeName System.Security.Cryptography.HMACSHA1
    $hmac.key = $([OTPKIT]::ConvertBase32ToHex($SECRET.ToUpper())) -replace '^0x', '' -split "(?<=\G\w{2})(?=\w{2})" | ForEach-Object { [Convert]::ToByte( $_, 16 ) }
    $timeSpan = $(New-TimeSpan -Start (Get-Date -Year 1970 -Month 1 -Day 1 -Hour 0 -Minute 0 -Second 0) -End (Get-Date).ToUniversalTime()).TotalSeconds
    $rndHash = $hmac.ComputeHash([byte[]][BitConverter]::GetBytes([Convert]::ToInt64([Math]::Floor($timeSpan / $WINDOW))))
    $toffset = $rndhash[($rndHash.Length - 1)] -band 0xf
    $fullOTP = ($rndhash[$toffset] -band 0x7f) * [math]::pow(2, 24)
    $fullOTP += ($rndHash[$toffset + 1] -band 0xff) * [math]::pow(2, 16)
    $fullOTP += ($rndHash[$toffset + 2] -band 0xff) * [math]::pow(2, 8)
    $fullOTP += ($rndHash[$toffset + 3] -band 0xff)

    $modNumber = [math]::pow(10, $LENGTH)
    $otp = $fullOTP % $modNumber
    $otp = $otp.ToString("0" * $LENGTH)
    return $otp
  }
  static [string] ConvertBase32ToHex([string]$base32) {
    $base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
    $bits = "";
    $hex = "";

    for ($i = 0; $i -lt $base32.Length; $i++) {
      $val = $base32chars.IndexOf($base32.Chars($i));
      $binary = [Convert]::ToString($val, 2)
      $str = $binary.ToString();
      $len = 5
      $pad = '0'
      if (($len + 1) -ge $str.Length) {
        while (($len - 1) -ge $str.Length) {
          $str = ($pad + $str)
        }
      }
      $bits += $str
    }

    for ($i = 0; $i + 4 -le $bits.Length; $i += 4) {
      $chunk = $bits.Substring($i, 4)
      # Write-Host $chunk
      $intChunk = [Convert]::ToInt32($chunk, 2)
      $hexChunk = '{0:x}' -f $([int]$intChunk)
      # Write-Host $hexChunk
      $hex = $hex + $hexChunk
    }
    return $hex;
  }
}