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

daycry / auth / 25518434194

07 May 2026 07:49PM UTC coverage: 58.608% (-6.4%) from 64.989%
25518434194

push

github

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

Implement security enhancements and new account features

277 of 1030 new or added lines in 55 files covered. (26.89%)

11 existing lines in 6 files now uncovered.

3544 of 6047 relevant lines covered (58.61%)

47.97 hits per line

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

14.29
/src/Models/TotpBackupCodeModel.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\Models;
15

16
use CodeIgniter\I18n\Time;
17
use Daycry\Auth\Entities\User;
18

19
/**
20
 * Stores one-time backup codes that authenticate a user when their
21
 * TOTP authenticator is unavailable.
22
 *
23
 * Codes are SHA-256 hashed at rest; raw codes are shown to the user
24
 * exactly once during enrollment.
25
 */
26
class TotpBackupCodeModel extends BaseModel
27
{
28
    protected $primaryKey     = 'id';
29
    protected $returnType     = 'array';
30
    protected $useSoftDeletes = false;
31
    protected $allowedFields  = [
32
        'user_id',
33
        'code_hash',
34
        'used_at',
35
        'created_at',
36
    ];
37
    protected $useTimestamps = false;
38

39
    protected function initialize(): void
1✔
40
    {
41
        parent::initialize();
1✔
42

43
        $this->table = $this->tables['totp_backup_codes'] ?? 'auth_totp_backup_codes';
1✔
44
    }
45

46
    /**
47
     * Replaces this user's backup codes with a fresh set and returns the
48
     * plain-text codes (only shown to the user this once).
49
     *
50
     * @return list<string>
51
     */
NEW
52
    public function regenerateForUser(User $user, int $count = 10, int $bytes = 5): array
×
53
    {
NEW
54
        $this->where('user_id', $user->id)->delete();
×
55

NEW
56
        $now   = Time::now()->toDateTimeString();
×
NEW
57
        $codes = [];
×
NEW
58
        $rows  = [];
×
59

NEW
60
        while (count($codes) < $count) {
×
NEW
61
            $candidate = strtolower(bin2hex(random_bytes($bytes)));
×
62

NEW
63
            if (in_array($candidate, $codes, true)) {
×
NEW
64
                continue; // collision — extremely unlikely but cheap to handle
×
65
            }
66

NEW
67
            $codes[] = $candidate;
×
NEW
68
            $rows[]  = [
×
NEW
69
                'user_id'    => (int) $user->id,
×
NEW
70
                'code_hash'  => hash('sha256', $candidate),
×
NEW
71
                'used_at'    => null,
×
NEW
72
                'created_at' => $now,
×
NEW
73
            ];
×
74
        }
75

NEW
76
        $this->insertBatch($rows);
×
77

NEW
78
        return $codes;
×
79
    }
80

81
    /**
82
     * Returns true and marks the row as used when the supplied plain-text
83
     * code matches an unused backup code for the user.
84
     */
NEW
85
    public function consume(User $user, string $rawCode): bool
×
86
    {
NEW
87
        $hash = hash('sha256', strtolower(trim($rawCode)));
×
88

89
        // Atomic: only succeeds when there is an unused matching row, and
90
        // marks it as used in the same statement (no SELECT-then-UPDATE race).
NEW
91
        $this->where('user_id', $user->id)
×
NEW
92
            ->where('code_hash', $hash)
×
NEW
93
            ->where('used_at')
×
NEW
94
            ->set('used_at', Time::now()->toDateTimeString())
×
NEW
95
            ->update();
×
96

NEW
97
        return $this->db->affectedRows() > 0;
×
98
    }
99

100
    /**
101
     * Counts the user's remaining (unused) backup codes.
102
     */
NEW
103
    public function remainingCount(User $user): int
×
104
    {
NEW
105
        return $this->where('user_id', $user->id)
×
NEW
106
            ->where('used_at')
×
NEW
107
            ->countAllResults();
×
108
    }
109

110
    /**
111
     * Removes all backup codes for the user (e.g. when TOTP is disabled).
112
     */
113
    public function purgeForUser(User $user): void
1✔
114
    {
115
        $this->where('user_id', $user->id)->delete();
1✔
116
    }
117
}
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