SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Helpers/TwoFactor/GoogleAuthenticator.php

  1. <?php
  2. /*
  3.  * Copyright (c) 2011 Liip AG
  4.  * Copyright (c) 2018 Karmabunny Pty Ltd
  5.  *
  6.  * Permission is hereby granted, free of charge, to any person obtaining a copy
  7.  * of this software and associated documentation files (the "Software"), to deal
  8.  * in the Software without restriction, including without limitation the rights
  9.  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10.  * copies of the Software, and to permit persons to whom the Software is furnished
  11.  * to do so, subject to the following conditions:
  12.  *
  13.  * The above copyright notice and this permission notice shall be included in all
  14.  * copies or substantial portions of the Software.
  15.  *
  16.  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17.  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18.  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19.  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20.  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21.  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22.  * THE SOFTWARE.
  23.  */
  24.  
  25. namespace Sprout\Helpers\TwoFactor;
  26.  
  27. use Sprout\Helpers\Security;
  28.  
  29.  
  30. class GoogleAuthenticator
  31. {
  32. private static $PASS_CODE_LENGTH = 6;
  33. private static $SECRET_LENGTH = 20; // 20 bytes = 160 bits
  34.  
  35.  
  36.  
  37. /**
  38.   * Check a given code against a given secret
  39.   *
  40.   * Does the check three times, for current time, one period before, and one period after
  41.   * This is to allow for clock drift
  42.   *
  43.   * @param string $secret The secret key, as a base32 string
  44.   * @param string $code Numeric code entered by the user to be checked
  45.   * @return bool True if code matches, false if it does not
  46.   */
  47. public function checkCode($secret, $code)
  48. {
  49. $time = floor(time() / 30);
  50.  
  51. for ($i = -1; $i <= 1; $i++) {
  52. if ($this->getCode($secret, $time + $i) == $code) {
  53. return true;
  54. }
  55. }
  56.  
  57. return false;
  58. }
  59.  
  60.  
  61. /**
  62.   * Calculate what the code for a given secret/time should be
  63.   *
  64.   * @param string $secret The secret key, as a base32 string
  65.   * @param int $time Unix timestamp, floored to a 30-sec increment
  66.   * @return string The calculated code, numeric
  67.   */
  68. protected function getCode($secret, $time)
  69. {
  70. $base32 = new FixedBitNotation(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', TRUE, TRUE);
  71. $secret = $base32->decode($secret);
  72.  
  73. // Compute the HMAC hash H with C as the message and K as the key
  74. // K should be passed as it is, C should be passed as a raw 64-bit unsigned integer.
  75. $time = pack('N', $time);
  76. $time = str_pad($time, 8, chr(0), STR_PAD_LEFT);
  77. $hash = hash_hmac('sha1', $time, $secret, true);
  78.  
  79. // Take the least 4 significant bits of H and use it as an offset, O.
  80. $offset = ord(substr($hash, -1));
  81. $offset = $offset & 0xF;
  82.  
  83. // Take 4 bytes from H starting at O bytes MSB, discard the most significant bit
  84. // and store the rest as an (unsigned) 32-bit integer, I.
  85. $portion = substr($hash, $offset, 4);
  86. $truncatedHash = unpack('N', $portion);
  87. $truncatedHash = $truncatedHash[1] & 0x7FFFFFFF;
  88.  
  89. // The token is the lowest N digits of I in base 10.
  90. // If the result has fewer digits than N, pad it with zeroes from the left.
  91. $pinModulo = pow(10, self::$PASS_CODE_LENGTH);
  92. $pinValue = str_pad($truncatedHash % $pinModulo, 6, '0', STR_PAD_LEFT);
  93.  
  94. return $pinValue;
  95. }
  96.  
  97.  
  98. /**
  99.   * For a given set of details, return the otpauth:// url for use in a QR code
  100.   *
  101.   * @param string $issuer The name of the entity issuing the token (e.g. the website, company, etc)
  102.   * @param string $user Username who is receiving the token
  103.   * @param string $host Hostname where the token is issued
  104.   * @param string $secret Randomly-generated secret key, as a base32 string
  105.   * @return string URL with the otpauth:// scheme
  106.   */
  107. public function getQRData($issuer, $user, $host, $secret)
  108. {
  109. $params = http_build_query([
  110. 'secret' => $secret,
  111. 'issuer' => $issuer,
  112. ]);
  113. return 'otpauth://totp/' . rawurlencode($issuer . ':' . $user . '@' . $host) . '?' . $params;
  114. }
  115.  
  116.  
  117. /**
  118.   * Return a Google Charts url which generates a QR code image from some QR code data
  119.   *
  120.   * @param string $qr_data Data for the QR code
  121.   * @return string Image URL
  122.   */
  123. public function getQRImageUrl($qr_data)
  124. {
  125. return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=' . urlencode($qr_data);
  126. }
  127.  
  128.  
  129. /**
  130.   * Generate a secret key and encode as base32
  131.   *
  132.   * NOTE: Only cryptographically secure in PHP 7.0 onwards (uses "random_bytes" method)
  133.   * On earlier versions of PHP this method isn't available so rand() is used instead
  134.   *
  135.   * @return string Randomly generated secret key
  136.   */
  137. public function generateSecret()
  138. {
  139. $secret = Security::randBytes(self::$SECRET_LENGTH);
  140. $base32 = new FixedBitNotation(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', TRUE, TRUE);
  141. return $base32->encode($secret);
  142. }
  143.  
  144. }
  145.