/** * HMAC-Based One-Time Password Algorithm * @see RFC 4226 * @param $key shared secret, treated as binary * @param $counter 8-byte counter * [@param $digits = 6] Length of the output code * [@param $algorithm = 'sha1'] HMAC algorithm - sha1, sha256, and sha512 permitted * @return string n-character numeric code */ function HOTP(Secret $key, int $counter, int $digits = 6, string $algorithm = 'sha1') : string { if (!in_array($algorithm, ['sha1', 'sha256', 'sha512'])) { throw new OutOfRangeException('Unexpected algorithm'); } if ($digits < 6 || $digits > 8) { // "Implementations MUST extract a 6-digit code at a minimum and // possibly 7 and 8-digit code." throw new LengthException('RFC4226 requires a 6 to 8-digit output'); } if (strlen($key->reveal()) < 128 / 8) { throw new LengthException('Key must be at least 128 bits long (160+ recommended)'); } $counter = pack('J', $counter); // Convert to 8-byte string $hash = hash_hmac($algorithm, $counter, $key->reveal(), true); // Determine the offset: get the last nibble of the hash output $offset = ord(substr($hash, -1)) & 0xf; // Index into the hash output by $offset bytes, take four bytes $dbc1 = unpack('N', substr($hash, $offset, 4))[1]; // Mask out the high bit (per the spec, avoids signed/unsigned issues) $dbc2 = $dbc1 & 0x7fffffff; // Use the last $digits by using modulo 10^digits $code = (string) ($dbc2 % pow(10, $digits)); // Finally, prepend zeroes to match the string length return str_pad($code, $digits, '0', \STR_PAD_LEFT); }
private function sign(Secret $key) { $alg = $this->headers['alg']; // DEFAULT? $payload = self::b64encode($this->headers) . '.' . self::b64encode($this->claims); switch ($alg) { case Algorithm::NONE: $data = ''; break; case Algorithm::HMAC_SHA_256: $data = hash_hmac('SHA256', $payload, $key->reveal(), true); break; case Algorithm::HMAC_SHA_384: $data = hash_hmac('SHA384', $payload, $key->reveal(), true); break; case Algorithm::HMAC_SHA_512: $data = hash_hmac('SHA512', $payload, $key->reveal(), true); break; default: throw new Exception("Unsupported algorithm"); // use openssl_sign and friends to do the signing } return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); }