<?php if (!defined('PmWiki')) exit();
/*  Copyright 2019-2026 Petko Yotov www.pmwiki.org/petko
    This file is part of PmWiki; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published
    by the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.  See pmwiki.php for full details.

    This helper class can create and verify 2FA codes (Authenticator apps).
    It is not included by default, include it with such a line:
    
    include_once("$FarmD/scripts/totp.php");
*/

class PmTOTP {
  protected $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
  protected $alpharev;
  protected $user;
  protected $issuer;
  protected $now;
  protected $timestep = 30;
  protected $digits = 6;
  protected $window = 1;
  function __construct($properties=[]) {
    global $WikiTitle;
    foreach ($properties as $key => $value) {
      if (property_exists($this, $key))
        $this->$key = $value;
    }
    $this->now = floor(time() / $this->timestep);
    $this->alpharev = array_flip(str_split($this->alphabet));
    if (empty($this->issuer)) $this->issuer = $WikiTitle;
  }
  
  function encode32($data) {
    $binaryString = '';
    foreach (str_split($data) as $char) {
      $binaryString .= sprintf('%08b', ord($char));
    }
    # make it multiple of 5
    $mod = strlen($binaryString) %5;
    if ($mod) $binaryString .= str_repeat('0', 5-$mod);
    $chunks5 = str_split($binaryString, 5);
    $base32 = '';
    foreach ($chunks5 as $chunk) {
      $base32 .= $this->alphabet[bindec($chunk)];
    }
    return $base32;
  }
  function decode32($base32) {
    if (empty($base32)) return '';
    
    $binaryString = '';
    for($i = 0; $i < strlen($base32); $i++)
      $binaryString .= sprintf('%05b', $this->alpharev[$base32[$i]]);
    $bytes = str_split($binaryString, 8);
    $decodedBin = '';
    foreach ($bytes as $byte) {
      if (strlen($byte)!=8) continue; # last bit leftover from encoding
      $decodedBin .= chr(bindec($byte));
    }
    return $decodedBin;
  }
  
  function mktotp($secret, $distance = 0) {
    $epoch = $this->now + $distance;
    $binsecret = $this->decode32($secret);
    $time = pack('N*', 0) . pack('N*', $epoch);
    $hash = hash_hmac('sha1', $time, $binsecret, true);
    
    $offset = ord($hash[19]) & 0x0F;
    // 4-byte chunk from the hash starting at the offset
    $slice = substr($hash, $offset, 4);
    $binary = unpack('N', $slice);
    $value = $binary[1] & 0x7FFFFFFF;
    $otp = $value % pow(10, $this->digits);
    return sprintf("%0{$this->digits}d", $otp);
  }
  
  function mksecret($length = 16) {
    $rbytes = random_bytes($length);
    return $this->encode32($rbytes);
  }
  
  function verify($secret, $usercode) {
    if (!$secret ||!$usercode) return false;
    for($i=-$this->window; $i<=$this->window; $i++) {
      $otp = $this->mktotp($secret, $i);
      if (hash_equals($otp, $usercode)) return true;
    }
    return false;
  }
}



