AuthModel::buildUserFoldersList()   C
last analyzed

Complexity

Conditions 17
Paths 16

Size

Total Lines 109
Code Lines 69

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 17
eloc 69
c 0
b 0
f 0
nc 16
nop 1
dl 0
loc 109
rs 5.2166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Teampass - a collaborative passwords manager.
4
 * ---
5
 * This file is part of the TeamPass project.
6
 * 
7
 * TeamPass is free software: you can redistribute it and/or modify it
8
 * under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation, version 3 of the License.
10
 * 
11
 * TeamPass is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 * 
16
 * You should have received a copy of the GNU General Public License
17
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18
 * 
19
 * Certain components of this file may be under different licenses. For
20
 * details, see the `licenses` directory or individual file headers.
21
 * ---
22
 * @version    API
23
 *
24
 * @file      AuthModel.php
25
 * @author    Nils Laumaillé ([email protected])
26
 * @copyright 2009-2026 Teampass.net
27
 * @license   GPL-3.0
28
 * @see       https://www.teampass.net
29
 */
30
31
use TeampassClasses\PasswordManager\PasswordManager;
32
use TeampassClasses\NestedTree\NestedTree;
33
use TeampassClasses\ConfigManager\ConfigManager;
34
use Firebase\JWT\JWT;
35
use Firebase\JWT\Key;
36
37
class AuthModel
38
{
39
40
41
    /**
42
     * Is the user allowed
43
     *
44
     * @param string $login
45
     * @param string $password
46
     * @param string $apikey
47
     * @return array
48
     */
49
    public function getUserAuth(string $login, string $password, string $apikey): array
50
    {
51
        // Sanitize
52
        // IMPORTANT: Password should NOT be escaped/sanitized - treat as opaque binary data
53
        // Only trim whitespace which is safe and expected (fix 3.1.5.10)
54
        include_once API_ROOT_PATH . '/../sources/main.functions.php';
55
        $inputData = dataSanitizer(
56
            [
57
                'login' => isset($login) === true ? $login : '',
58
                'password' => isset($password) === true ? $password : '',
59
                'apikey' => isset($apikey) === true ? $apikey : '',
60
            ],
61
            [
62
                'login' => 'trim|escape|strip_tags',
63
                'password' => 'trim', // Only trim, NO escape/sanitization
64
                'apikey' => 'trim|escape|strip_tags',
65
            ]
66
        );
67
        
68
        // Check apikey and credentials
69
        if (empty($inputData['login']) === true || empty($inputData['apikey']) === true || empty($inputData['password']) === true) {
70
            // case where it is a generic key
71
            // Not allowed to use this API
72
73
            return ["error" => "Login failed.", "info" => "User password is requested"];
74
        } else {
75
            // case where it is a user api key
76
            // Check if user exists
77
            $userInfo = getUserCompleteData($inputData['login']);
78
79
            if ($userInfo === null) {
80
                return ["error" => "Login failed.", "info" => "Credentials not valid"];
81
            }
82
83
            // Check if user is enabled
84
            if ((int) $userInfo['api_enabled'] === 0) {
85
                return ["error" => "Login failed.", "info" => "User not allowed to use API"];
86
            }
87
            
88
            // Check password
89
            $passwordManager = new PasswordManager();
90
            if ($passwordManager->verifyPassword($userInfo['pw'], $inputData['password']) === true) {
91
                // Correct credentials
92
                // get user keys
93
                $privateKeyClear = decryptPrivateKey($inputData['password'], (string) $userInfo['private_key']);
94
95
                // check API key
96
                if ($inputData['apikey'] !== base64_decode(decryptUserObjectKey($userInfo['api_key'], $privateKeyClear))) {
97
                    return ["error" => "Login failed.", "info" => "API Key not valid"];
98
                }
99
100
                // Update user's key_tempo
101
                $keyTempo = getOrRotateKeyTempo($userInfo['id'], 3600);
102
103
                // Generate a unique session key for this API session (256 bits / 32 bytes)
104
                // This key will be stored in the JWT and used to decrypt the private key
105
                $sessionKey = random_bytes(32);
106
                $sessionKeySalt = bin2hex(random_bytes(16));
107
108
                // Encrypt the decrypted private key with the session key
109
                // This allows us to store it securely in the database without exposing it
110
                require_once API_ROOT_PATH . '/inc/encryption_utils.php';
111
                $encryptedPrivateKey = encrypt_with_session_key($privateKeyClear, $sessionKey);
112
113
                if ($encryptedPrivateKey === false) {
114
                    return ["error" => "Login failed.", "info" => "Failed to encrypt private key"];
115
                }
116
117
                // Store the ENCRYPTED private key in the API table
118
                // Even if the database is compromised, the key cannot be used without the session_key from the JWT
119
                DB::update(
120
                    prefixTable('api'),
121
                    [
122
                        'encrypted_private_key' => $encryptedPrivateKey,
123
                        'session_key_salt' => $sessionKeySalt,
124
                        'session_key' => $keyTempo,
125
                        'timestamp' => time(),
126
                    ],
127
                    'user_id = %i',
128
                    $userInfo['id']
129
                );
130
131
                // get user folders list
132
                $ret = $this->buildUserFoldersList($userInfo);
133
134
                // Load config
135
                $configManager = new ConfigManager();
136
                $SETTINGS = $configManager->getAllSettings();
137
138
                // Log user
139
                logEvents($SETTINGS, 'api', 'user_connection', (string) $userInfo['id'], stripslashes($userInfo['login']));
140
141
                // create JWT with session key
142
                return $this->createUserJWT(
143
                    (int) $userInfo['id'],
144
                    (string) $inputData['login'],
145
                    (string) $userInfo['email'],
146
                    (int) $userInfo['personal_folder'],
147
                    (string) implode(",", $ret['folders']),
148
                    (string) implode(",", $ret['items']),
149
                    (string) $keyTempo,
150
                    (string) base64_encode($sessionKey), // Session key for decrypting private key
151
                    (int) $userInfo['admin'],
152
                    (int) $userInfo['gestionnaire'],
153
                    (int) $userInfo['can_create_root_folder'],
154
                    (int) $userInfo['can_manage_all_users'],
155
                    (string) $userInfo['fonction_id'],
156
                    (string) $userInfo['api_allowed_folders'],
157
                    (int) $userInfo['api_allowed_to_create'],
158
                    (int) $userInfo['api_allowed_to_read'],
159
                    (int) $userInfo['api_allowed_to_update'],
160
                    (int) $userInfo['api_allowed_to_delete'],
161
                    (int) $SETTINGS['api_token_duration'] ?? 60,
162
                    (int) $SETTINGS['pwd_maximum_length'] ?? 60,
163
                    (int) $SETTINGS['maintenance_mode'] ?? 0,
164
                );
165
            } else {
166
                return ["error" => "Login failed.", "info" => "Credentials not valid"];
167
            }
168
        }
169
    }
170
    //end getUserAuth
171
172
    /**
173
     * Create a JWT
174
     *
175
     * Note: User encryption keys (public_key, private_key) are no longer stored in the JWT
176
     * to reduce token size. Instead, the private key is encrypted with a session key and
177
     * stored in the database. The session key is stored in the JWT (~44 bytes) which is
178
     * used to decrypt the private key when needed.
179
     *
180
     * Security approach (defense in depth):
181
     * - Encrypted private key in DB is useless without session_key
182
     * - session_key in JWT is useless without encrypted private key from DB
183
     * - Both are required together to access the decrypted private key
184
     *
185
     * @param integer $id
186
     * @param string $login
187
     * @param string $email
188
     * @param integer $pf_enabled
189
     * @param string $folders
190
     * @param string $items
191
     * @param string $keyTempo
192
     * @param string $sessionKey Session key (base64) for decrypting private key from DB
193
     * @param integer $admin
194
     * @param integer $manager
195
     * @param integer $can_create_root_folder
196
     * @param integer $can_manage_all_users
197
     * @param string $roles
198
     * @param string $allowed_folders
199
     * @param integer $allowed_to_create
200
     * @param integer $allowed_to_read
201
     * @param integer $allowed_to_update
202
     * @param integer $allowed_to_delete
203
     * @param integer $api_token_duration
204
     * @param integer $pwd_maximum_length
205
     * @param integer $maintenance_mode
206
     * @return array
207
     */
208
    private function createUserJWT(
209
        int $id,
210
        string $login,
211
        string $email,
212
        int $pf_enabled,
213
        string $folders,
214
        string $items,
215
        string $keyTempo,
216
        string $sessionKey,
217
        int $admin,
218
        int $manager,
219
        int $can_create_root_folder,
220
        int $can_manage_all_users,
221
        string $roles,
222
        string $allowed_folders,
223
        int $allowed_to_create,
224
        int $allowed_to_read,
225
        int $allowed_to_update,
226
        int $allowed_to_delete,
227
        int $api_token_duration,
228
        int $pwd_maximum_length,
229
        int $maintenance_mode
230
    ): array
231
    {
232
        // Load config
233
        $configManager = new ConfigManager();
234
        $SETTINGS = $configManager->getAllSettings();
235
236
		$payload = [
237
            'username' => $login,
238
            'id' => $id,
239
            'exp' => (time() + $SETTINGS['api_token_duration'] + 600),
240
            'pf_enabled' => $pf_enabled,
241
            'folders_list' => $folders,
242
            'restricted_items_list' => $items,
243
            'key_tempo' => $keyTempo,
244
            'session_key' => $sessionKey, // Session key for decrypting private key from database
245
            'is_admin' => $admin,
246
            'is_manager' => $manager,
247
            'user_can_create_root_folder' => $can_create_root_folder,
248
            'user_can_manage_all_users' => $can_manage_all_users,
249
            'roles' => $roles,
250
            'allowed_folders' => $allowed_folders,
251
            'allowed_to_create' => $allowed_to_create,
252
            'allowed_to_read' => $allowed_to_read,
253
            'allowed_to_update' => $allowed_to_update,
254
            'allowed_to_delete' => $allowed_to_delete,
255
            'email' => $email,
256
            'api_token_duration' => $api_token_duration,
257
            'pwd_maximum_length' => $pwd_maximum_length,
258
            'maintenance_mode' => $maintenance_mode,
259
        ];
260
261
        return ['token' => JWT::encode($payload, DB_PASSWD, 'HS256')];
262
    }
263
264
    //end createUserJWT
265
266
267
    /**
268
     * Permit to build the list of folders the user can access
269
     *
270
     * @param array $userInfo
271
     * @return array
272
     */
273
    private function buildUserFoldersList(array $userInfo): array
274
    {
275
        //Build tree
276
        $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
277
        
278
        // Start by adding the manually added folders
279
        $allowedFolders = array_map('intval', explode(";", $userInfo['groupes_visibles']));
280
        $readOnlyFolders = [];
281
        $allowedFoldersByRoles = [];
282
        $restrictedFoldersForItems = [];
283
        $foldersLimited = [];
284
        $foldersLimitedFull = [];
285
        $restrictedItems = [];
286
        $personalFolders = [];
287
288
        $userFunctionId = explode(";", $userInfo['fonction_id']);
289
290
        // Get folders from the roles
291
        if (count($userFunctionId) > 0) {
292
            $rows = DB::query(
293
                'SELECT * 
294
                FROM ' . prefixTable('roles_values') . '
295
                WHERE role_id IN %li  AND type IN ("W", "ND", "NE", "NDNE", "R")',
296
                $userFunctionId
297
            );
298
            foreach ($rows as $record) {
299
                if ($record['type'] === 'R') {
300
                    array_push($readOnlyFolders, $record['folder_id']);
301
                } elseif (in_array($record['folder_id'], $allowedFolders) === false) {
302
                    array_push($allowedFoldersByRoles, $record['folder_id']);
303
                }
304
            }
305
            $allowedFoldersByRoles = array_unique($allowedFoldersByRoles);
306
            $readOnlyFolders = array_unique($readOnlyFolders);
307
            // Clean arrays
308
            foreach ($allowedFoldersByRoles as $value) {
309
                $key = array_search($value, $readOnlyFolders);
310
                if ($key !== false) {
311
                    unset($readOnlyFolders[$key]);
312
                }
313
            }
314
        }
315
        
316
        // Does this user is allowed to see other items
317
        $inc = 0;
318
        $rows = DB::query(
319
            'SELECT id, id_tree 
320
            FROM ' . prefixTable('items') . '
321
            WHERE restricted_to LIKE %s'.
322
            (count($userFunctionId) > 0 ? ' AND id_tree NOT IN %li' : ''),
323
            $userInfo['id'],
324
            count($userFunctionId) > 0 ? $userFunctionId : DB::sqleval('0')
325
        );
326
        foreach ($rows as $record) {
327
            // Exclude restriction on item if folder is fully accessible
328
            $restrictedFoldersForItems[$inc] = $record['id_tree'];
329
            ++$inc;
330
        }
331
332
        // Check for the users roles if some specific rights exist on items
333
        $rows = DB::query(
334
            'SELECT i.id_tree, r.item_id
335
            FROM ' . prefixTable('items') . ' AS i
336
            INNER JOIN ' . prefixTable('restriction_to_roles') . ' AS r ON (r.item_id=i.id)
337
            WHERE '.(count($userFunctionId) > 0 ? ' id_tree NOT IN %li AND ' : '').' i.id_tree != ""
338
            ORDER BY i.id_tree ASC',
339
            count($userFunctionId) > 0 ? $userFunctionId : DB::sqleval('0')
340
        );
341
        foreach ($rows as $record) {
342
            $foldersLimited[$record['id_tree']][$inc] = $record['item_id'];
343
            //array_push($foldersLimitedFull, $record['item_id']);
344
            array_push($restrictedItems, $record['item_id']);
345
            array_push($foldersLimitedFull, $record['id_tree']);
346
            ++$inc;
347
        }
348
349
        // Add all personal folders
350
        $rows = DB::queryFirstRow(
351
            'SELECT id 
352
            FROM ' . prefixTable('nested_tree') . '
353
            WHERE title = %i AND personal_folder = 1'.
354
            (count($userFunctionId) > 0 ? ' AND id NOT IN %li' : ''),
355
            $userInfo['id'],
356
            count($userFunctionId) > 0 ? $userFunctionId : DB::sqleval('0')
357
        );
358
        if (empty($rows['id']) === false) {
359
            array_push($personalFolders, $rows['id']);
360
            // get all descendants
361
            $ids = $tree->getDescendants($rows['id'], false, false, true);
362
            foreach ($ids as $id) {
363
                array_push($personalFolders, $id);
364
            }
365
        }
366
367
        // All folders visibles
368
        return [
369
            'folders' => array_unique(
370
            array_filter(
371
                array_merge(
372
                    $allowedFolders,
373
                    $foldersLimitedFull,
374
                    $allowedFoldersByRoles,
375
                    $restrictedFoldersForItems,
376
                    $readOnlyFolders,
377
                    $personalFolders
378
                )
379
                )
380
            ),
381
            'items' => array_unique($restrictedItems),
382
        ];
383
    }
384
    //end buildUserFoldersList
385
}