• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

daycry / auth / 22527658769

28 Feb 2026 07:41PM UTC coverage: 63.267% (-3.6%) from 66.864%
22527658769

push

github

web-flow
Merge pull request #36 from daycry/development

Implement TOTP 2FA, JWT auth, device session tracking, and docs overhaul

465 of 1168 new or added lines in 52 files covered. (39.81%)

129 existing lines in 46 files now uncovered.

3064 of 4843 relevant lines covered (63.27%)

41.53 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.86
/src/Libraries/TOTP.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of Daycry Auth.
7
 *
8
 * (c) Daycry <daycry9@proton.me>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace Daycry\Auth\Libraries;
15

16
use InvalidArgumentException;
17

18
/**
19
 * RFC 6238 TOTP (Time-based One-Time Password) implementation.
20
 *
21
 * Pure PHP, no external dependencies.
22
 * Compatible with Google Authenticator, Authy, and any RFC 6238 app.
23
 */
24
class TOTP
25
{
26
    private const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
27

28
    /**
29
     * Number of digits in the generated code.
30
     */
31
    private const DIGITS = 6;
32

33
    /**
34
     * Time step in seconds (standard: 30).
35
     */
36
    private const PERIOD = 30;
37

38
    /**
39
     * Generates a cryptographically random base32-encoded TOTP secret.
40
     *
41
     * @param int $length Number of bytes of random data (default 20 = 160-bit secret)
42
     */
43
    public static function generateSecret(int $length = 20): string
14✔
44
    {
45
        return self::base32Encode(random_bytes($length));
14✔
46
    }
47

48
    /**
49
     * Generates the current TOTP code for the given secret.
50
     *
51
     * @param string   $secret    Base32-encoded TOTP secret
52
     * @param int|null $timestamp Unix timestamp (null = now)
53
     */
54
    public static function getCode(string $secret, ?int $timestamp = null): string
6✔
55
    {
56
        $timestamp ??= time();
6✔
57
        $timeStep = (int) floor($timestamp / self::PERIOD);
6✔
58

59
        return self::computeCode($secret, $timeStep);
6✔
60
    }
61

62
    /**
63
     * Verifies a TOTP code against the given secret.
64
     * Accepts codes within ±$window time steps (default: ±1, i.e. 90-second window).
65
     *
66
     * @param string $secret    Base32-encoded TOTP secret
67
     * @param string $code      6-digit code to verify
68
     * @param int    $window    Number of adjacent time steps to accept
69
     * @param int    $timestamp Unix timestamp (useful for testing)
70
     */
71
    public static function verify(string $secret, string $code, int $window = 1, ?int $timestamp = null): bool
7✔
72
    {
73
        $timestamp ??= time();
7✔
74
        $timeStep = (int) floor($timestamp / self::PERIOD);
7✔
75

76
        for ($i = -$window; $i <= $window; $i++) {
7✔
77
            $expected = self::computeCode($secret, $timeStep + $i);
7✔
78

79
            if (hash_equals($expected, $code)) {
7✔
80
                return true;
4✔
81
            }
82
        }
83

84
        return false;
3✔
85
    }
86

87
    /**
88
     * Builds the `otpauth://` URI that can be encoded as a QR code
89
     * for scanning with Google Authenticator or any compatible app.
90
     *
91
     * @param string $secret  Base32-encoded TOTP secret
92
     * @param string $account User's email or username
93
     * @param string $issuer  Application / service name shown in the app
94
     */
95
    public static function getOtpAuthUrl(string $secret, string $account, string $issuer): string
7✔
96
    {
97
        $label = rawurlencode($issuer) . ':' . rawurlencode($account);
7✔
98

99
        return 'otpauth://totp/' . $label
7✔
100
            . '?secret=' . $secret
7✔
101
            . '&issuer=' . rawurlencode($issuer)
7✔
102
            . '&algorithm=SHA1'
7✔
103
            . '&digits=' . self::DIGITS
7✔
104
            . '&period=' . self::PERIOD;
7✔
105
    }
106

107
    /**
108
     * Returns a Google Charts QR code image URL for the given otpauth URI.
109
     * Useful for quickly rendering a QR code without a local library.
110
     */
NEW
111
    public static function getQRCodeUrl(string $otpAuthUrl, int $size = 200): string
×
112
    {
NEW
113
        return 'https://chart.googleapis.com/chart?chs=' . $size . 'x' . $size
×
NEW
114
            . '&chld=M|0'
×
NEW
115
            . '&cht=qr'
×
NEW
116
            . '&chl=' . rawurlencode($otpAuthUrl);
×
117
    }
118

119
    /**
120
     * Computes the TOTP code for a specific time step counter.
121
     */
122
    private static function computeCode(string $secret, int $counter): string
8✔
123
    {
124
        $key     = self::base32Decode(strtoupper($secret));
8✔
125
        $message = pack('J', $counter); // 8-byte big-endian counter
8✔
126

127
        $hash   = hash_hmac('sha1', $message, $key, true);
8✔
128
        $offset = ord($hash[19]) & 0x0F;
8✔
129

130
        $code = (
8✔
131
            ((ord($hash[$offset]) & 0x7F) << 24)
8✔
132
            | ((ord($hash[$offset + 1]) & 0xFF) << 16)
8✔
133
            | ((ord($hash[$offset + 2]) & 0xFF) << 8)
8✔
134
            | (ord($hash[$offset + 3]) & 0xFF)
8✔
135
        ) % (10 ** self::DIGITS);
8✔
136

137
        return str_pad((string) $code, self::DIGITS, '0', STR_PAD_LEFT);
8✔
138
    }
139

140
    /**
141
     * Encodes binary data as a base32 string (RFC 4648, no padding).
142
     */
143
    public static function base32Encode(string $data): string
15✔
144
    {
145
        $alphabet = self::BASE32_ALPHABET;
15✔
146
        $result   = '';
15✔
147
        $bits     = 0;
15✔
148
        $buffer   = 0;
15✔
149

150
        foreach (str_split($data) as $char) {
15✔
151
            $buffer = ($buffer << 8) | ord($char);
15✔
152
            $bits += 8;
15✔
153

154
            while ($bits >= 5) {
15✔
155
                $bits -= 5;
15✔
156
                $result .= $alphabet[($buffer >> $bits) & 0x1F];
15✔
157
            }
158
        }
159

160
        if ($bits > 0) {
15✔
NEW
161
            $result .= $alphabet[($buffer << (5 - $bits)) & 0x1F];
×
162
        }
163

164
        return $result;
15✔
165
    }
166

167
    /**
168
     * Decodes a base32 string (RFC 4648) into binary data.
169
     *
170
     * @throws InvalidArgumentException on invalid base32 characters
171
     */
172
    public static function base32Decode(string $base32): string
9✔
173
    {
174
        $alphabet = self::BASE32_ALPHABET;
9✔
175
        $base32   = strtoupper(rtrim($base32, '='));
9✔
176
        $result   = '';
9✔
177
        $bits     = 0;
9✔
178
        $buffer   = 0;
9✔
179

180
        foreach (str_split($base32) as $char) {
9✔
181
            $pos = strpos($alphabet, $char);
9✔
182

183
            if ($pos === false) {
9✔
NEW
184
                throw new InvalidArgumentException('Invalid base32 character: ' . $char);
×
185
            }
186

187
            $buffer = ($buffer << 5) | $pos;
9✔
188
            $bits += 5;
9✔
189

190
            if ($bits >= 8) {
9✔
191
                $bits -= 8;
9✔
192
                $result .= chr(($buffer >> $bits) & 0xFF);
9✔
193
            }
194
        }
195

196
        return $result;
9✔
197
    }
198
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc