/libraries/src/Encrypt/Totp.php

https://github.com/Hackwar/joomla-cms · PHP · 207 lines · 78 code · 26 blank · 103 comment · 5 complexity · e631957dc044731b4cc5b369a13889e3 MD5 · raw file

  1. <?php
  2. /**
  3. * Joomla! Content Management System
  4. *
  5. * @copyright (C) 2013 Open Source Matters, Inc. <https://www.joomla.org>
  6. * @license GNU General Public License version 2 or later; see LICENSE.txt
  7. * @note This file has been modified by the Joomla! Project and no longer reflects the original work of its author.
  8. */
  9. namespace Joomla\CMS\Encrypt;
  10. \defined('JPATH_PLATFORM') or die;
  11. /**
  12. * This class provides an RFC6238-compliant Time-based One Time Passwords,
  13. * compatible with Google Authenticator (with PassCodeLength = 6 and TimePeriod = 30).
  14. *
  15. * @since 4.0.0
  16. */
  17. class Totp
  18. {
  19. /**
  20. * Passcode length
  21. *
  22. * @var integer
  23. */
  24. private $_passCodeLength = 6;
  25. /**
  26. * Pin modulo
  27. *
  28. * @var integer
  29. */
  30. private $_pinModulo;
  31. /**
  32. * The length of the secret in bytes.
  33. * RFC 4226: "The length of the shared secret MUST be at least 128 bits. This document RECOMMENDs a shared secret length of 160 bits."
  34. * The original value was 10 bytes (80 bits) this value has been increased to 20 (160 bits) with Joomla! 3.9.25
  35. *
  36. * @var integer
  37. */
  38. private $_secretLength = 20;
  39. /**
  40. * Timestep
  41. *
  42. * @var integer
  43. */
  44. private $_timeStep = 30;
  45. /**
  46. * Base32
  47. *
  48. * @var integer
  49. */
  50. private $_base32 = null;
  51. /**
  52. * Initialises an RFC6238-compatible TOTP generator. Please note that this
  53. * class does not implement the constraint in the last paragraph of §5.2
  54. * of RFC6238. It's up to you to ensure that the same user/device does not
  55. * retry validation within the same Time Step.
  56. *
  57. * @param int $timeStep The Time Step (in seconds). Use 30 to be compatible with Google Authenticator.
  58. * @param int $passCodeLength The generated passcode length. Default: 6 digits.
  59. * @param int $secretLength The length of the secret key. Default: 10 bytes (80 bits).
  60. * @param Object $base32 The base32 en/decrypter
  61. */
  62. public function __construct($timeStep = 30, $passCodeLength = 6, $secretLength = 10, $base32=null)
  63. {
  64. $this->_timeStep = $timeStep;
  65. $this->_passCodeLength = $passCodeLength;
  66. $this->_secretLength = $secretLength;
  67. $this->_pinModulo = pow(10, $this->_passCodeLength);
  68. if (\is_null($base32))
  69. {
  70. $this->_base32 = new Base32;
  71. }
  72. else
  73. {
  74. $this->_base32 = $base32;
  75. }
  76. }
  77. /**
  78. * Get the time period based on the $time timestamp and the Time Step
  79. * defined. If $time is skipped or set to null the current timestamp will
  80. * be used.
  81. *
  82. * @param int|null $time Timestamp
  83. *
  84. * @return integer The time period since the UNIX Epoch
  85. */
  86. public function getPeriod($time = null)
  87. {
  88. if (\is_null($time))
  89. {
  90. $time = time();
  91. }
  92. $period = floor($time / $this->_timeStep);
  93. return $period;
  94. }
  95. /**
  96. * Check is the given passcode $code is a valid TOTP generated using secret
  97. * key $secret
  98. *
  99. * @param string $secret The Base32-encoded secret key
  100. * @param string $code The passcode to check
  101. *
  102. * @return boolean True if the code is valid
  103. */
  104. public function checkCode($secret, $code)
  105. {
  106. $time = $this->getPeriod();
  107. for ($i = -1; $i <= 1; $i++)
  108. {
  109. if ($this->getCode($secret, ($time + $i) * $this->_timeStep) == $code)
  110. {
  111. return true;
  112. }
  113. }
  114. return false;
  115. }
  116. /**
  117. * Gets the TOTP passcode for a given secret key $secret and a given UNIX
  118. * timestamp $time
  119. *
  120. * @param string $secret The Base32-encoded secret key
  121. * @param int $time UNIX timestamp
  122. *
  123. * @return string
  124. */
  125. public function getCode($secret, $time = null)
  126. {
  127. $period = $this->getPeriod($time);
  128. $secret = $this->_base32->decode($secret);
  129. $time = pack("N", $period);
  130. $time = str_pad($time, 8, \chr(0), STR_PAD_LEFT);
  131. $hash = hash_hmac('sha1', $time, $secret, true);
  132. $offset = \ord(substr($hash, -1));
  133. $offset = $offset & 0xF;
  134. $truncatedHash = $this->hashToInt($hash, $offset) & 0x7FFFFFFF;
  135. $pinValue = str_pad($truncatedHash % $this->_pinModulo, $this->_passCodeLength, "0", STR_PAD_LEFT);
  136. return $pinValue;
  137. }
  138. /**
  139. * Extracts a part of a hash as an integer
  140. *
  141. * @param string $bytes The hash
  142. * @param string $start The char to start from (0 = first char)
  143. *
  144. * @return string
  145. */
  146. protected function hashToInt($bytes, $start)
  147. {
  148. $input = substr($bytes, $start, \strlen($bytes) - $start);
  149. $val2 = unpack("N", substr($input, 0, 4));
  150. return $val2[1];
  151. }
  152. /**
  153. * Returns a QR code URL for easy setup of TOTP apps like Google Authenticator
  154. *
  155. * @param string $user User
  156. * @param string $hostname Hostname
  157. * @param string $secret Secret string
  158. *
  159. * @return string
  160. */
  161. public function getUrl($user, $hostname, $secret)
  162. {
  163. $url = sprintf("otpauth://totp/%s@%s?secret=%s", $user, $hostname, $secret);
  164. $encoder = "https://chart.googleapis.com/chart?chs=200x200&chld=Q|2&cht=qr&chl=";
  165. $encoderURL = $encoder . urlencode($url);
  166. return $encoderURL;
  167. }
  168. /**
  169. * Generates a (semi-)random Secret Key for TOTP generation
  170. *
  171. * @return string
  172. *
  173. * @note Since 3.9.25 we use the secure method "random_bytes" over the original insecure "rand" function.
  174. * The random_bytes function has been backported to outdated PHP versions by the core shipped library paragonie/random_compat
  175. */
  176. public function generateSecret()
  177. {
  178. $secret = random_bytes($this->_secretLength);
  179. return $this->_base32->encode($secret);
  180. }
  181. }