Passed
Pull Request — master (#4822)
by Nils
06:14
created

QRCodeService::generateForUser()   B

Complexity

Conditions 9
Paths 7

Size

Total Lines 45
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 26
c 1
b 0
f 0
nc 7
nop 1
dl 0
loc 45
rs 8.0555
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Teampass - a collaborative passwords manager.
7
 * ---
8
 * This file is part of the TeamPass project.
9
 * 
10
 * TeamPass is free software: you can redistribute it and/or modify it
11
 * under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, version 3 of the License.
13
 * 
14
 * TeamPass is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 * 
19
 * You should have received a copy of the GNU General Public License
20
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21
 * 
22
 * Certain components of this file may be under different licenses. For
23
 * details, see the `licenses` directory or individual file headers.
24
 * ---
25
 * @file      QRCodeService.php
26
 * @author    Nils Laumaillé ([email protected])
27
 * @copyright 2009-2025 Teampass.net
28
 * @license   GPL-3.0
29
 * @see       https://www.teampass.net
30
 */
31
32
use TeampassClasses\SessionManager\SessionManager;
33
use TeampassClasses\PasswordManager\PasswordManager;
34
use TeampassClasses\Language\Language;
35
use RobThree\Auth\TwoFactorAuth;
36
37
class QRCodeService
38
{
39
    private array $settings;
40
    private Language $lang;
41
    private PasswordManager $passwordManager;
42
43
    public function __construct(array $settings)
44
    {
45
        $this->settings = $settings;
46
        $session = SessionManager::getSession();
47
        $this->lang = new Language($session->get('user-language') ?? 'english');
48
        $this->passwordManager = new PasswordManager();
49
    }
50
51
    public function generateForUser(array $params): string
52
    {
53
        if ($this->isResetBlocked($params['origin'])) {
54
            return $this->respondError(
55
                "113 " . $this->lang->get('error_not_allowed_to') . " - " .
56
                isKeyExistingAndEqual('ga_reset_by_user', 1, $this->settings)
57
            );
58
        }
59
60
        $user = $this->getUser($params['id'], $params['login']);
61
        if (!$user) {
62
            logEvents($this->settings, 'failed_auth', 'user_not_exists', '', stripslashes($params['login']), stripslashes($params['login']));
63
            return $this->respondError($this->lang->get('no_user'), ['tst' => 1]);
64
        }
65
66
        if (
67
            isSetArrayOfValues([$params['password'], $user['pw']]) &&
68
            !$this->passwordManager->verifyPassword($user['pw'], $params['password']) &&
69
            $params['origin'] !== 'users_management_list'
70
        ) {
71
            logEvents($this->settings, 'failed_auth', 'password_is_not_correct', '', stripslashes($params['login']), stripslashes($params['login']));
72
            return $this->respondError($this->lang->get('no_user'), ['tst' => $params['origin']]);
73
        }
74
75
        if (empty($user['email'])) {
76
            return $this->respondError($this->lang->get('no_email_set'));
77
        }
78
79
        $tokenId = $this->handleToken($params['token'], $user['id']);
80
        if ($tokenId === false) {
81
            return $this->respondError('TOKEN already used');
82
        }
83
84
        [$secretKey, $temporaryCode] = $this->generateAndStoreSecret($user['id']);
85
86
        logEvents($this->settings, 'user_connection', 'at_2fa_google_code_send_by_email', (string)$user['id'], stripslashes($params['login']), stripslashes($params['login']));
87
        DB::update(prefixTable('tokens'), ['end_timestamp' => time()], 'id = %i', $tokenId);
88
89
        if ((int)$params['send_mail'] === 1) {
90
            $this->send2FACodeByEmail($user['email'], $temporaryCode);
91
92
            return $this->respondSuccess($user['email'], $params['send_mail']);
93
        }
94
95
        return $this->respondSuccess($user['email']);
96
    }
97
98
    private function isResetBlocked(?string $origin): bool
99
    {
100
        return isKeyExistingAndEqual('ga_reset_by_user', 0, $this->settings)
101
            && ($origin === null || $origin !== 'users_management_list');
102
    }
103
104
    private function getUser($id, string &$login): ?array
105
    {
106
        if (isValueSetNullEmpty($id)) {
107
            $user = DB::queryFirstRow(
108
                'SELECT id, email, pw FROM ' . prefixTable('users') . ' WHERE login = %s',
109
                $login
110
            );
111
        } else {
112
            $user = DB::queryFirstRow(
113
                'SELECT id, login, email, pw FROM ' . prefixTable('users') . ' WHERE id = %i',
114
                $id
115
            );
116
            $login = $user['login'] ?? $login;
117
        }
118
119
        return DB::count() > 0 ? $user : null;
120
    }
121
122
    private function handleToken(string $token, int $userId): int|false
123
    {
124
        $dataToken = DB::queryFirstRow(
125
            'SELECT end_timestamp, reason FROM ' . prefixTable('tokens') . ' WHERE token = %s AND user_id = %i',
126
            $token,
127
            $userId
128
        );
129
130
        if (
131
            DB::count() > 0 &&
132
            !is_null($dataToken['end_timestamp']) &&
133
            $dataToken['reason'] === 'auth_qr_code'
134
        ) {
135
            return false;
136
        }
137
138
        if (DB::count() === 0) {
139
            DB::insert(prefixTable('tokens'), [
140
                'user_id' => $userId,
141
                'token' => $token,
142
                'reason' => 'auth_qr_code',
143
                'creation_timestamp' => time(),
144
            ]);
145
            return DB::insertId();
146
        }
147
148
        return (int) DB::queryFirstField('SELECT id FROM ' . prefixTable('tokens') . ' WHERE token = %s AND user_id = %i', $token, $userId);
149
    }
150
151
    private function generateAndStoreSecret(int $userId): array
152
    {
153
        $tfa = new TwoFactorAuth($this->settings['ga_website_name']);
154
        $secret = $tfa->createSecret();
155
        $passwordManager = new PasswordManager();
156
        $code = $passwordManager->generatePassword(12, false, true, true, false, true);
157
158
        DB::update(prefixTable('users'), [
159
            'ga' => $secret,
160
            'ga_temporary_code' => $code,
161
        ], 'id = %i', $userId);
162
163
        return [$secret, $code];
164
    }
165
166
    private function send2FACodeByEmail(string $email, string $code): void
167
    {
168
        prepareSendingEmail(
169
            $this->lang->get('email_ga_subject'),
170
            str_replace('#2FACode#', $code, $this->lang->get('email_ga_text')),
171
            $email
172
        );
173
    }
174
175
    private function respondError(string $message, array $extra = []): string
176
    {
177
        return prepareExchangedData(array_merge(['error' => true, 'message' => $message], $extra), 'encode');
178
    }
179
180
    private function respondSuccess(string $email, string $message = ''): string
181
    {
182
        return prepareExchangedData([
183
            'error' => false,
184
            'message' => $message,
185
            'email' => $email,
186
            'email_result' => str_replace(
187
                '#email#',
188
                '<b>' . obfuscateEmail($email) . '</b>',
189
                addslashes($this->lang->get('admin_email_result_ok'))
190
            ),
191
        ], 'encode');
192
    }
193
}
194