nilsteampassnet /
TeamPass
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
| 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-2025 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 | error_log(print_r($inputData, true)); |
||
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
| 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 = bin2hex(random_bytes(16)); |
||
| 102 | /*DB::update( |
||
| 103 | prefixTable('users'), |
||
| 104 | [ |
||
| 105 | 'key_tempo' => $keyTempo, |
||
| 106 | ], |
||
| 107 | 'id = %i', |
||
| 108 | $userInfo['id'] |
||
| 109 | );*/ |
||
| 110 | |||
| 111 | // Generate a unique session key for this API session (256 bits / 32 bytes) |
||
| 112 | // This key will be stored in the JWT and used to decrypt the private key |
||
| 113 | $sessionKey = random_bytes(32); |
||
| 114 | $sessionKeySalt = bin2hex(random_bytes(16)); |
||
| 115 | |||
| 116 | // Encrypt the decrypted private key with the session key |
||
| 117 | // This allows us to store it securely in the database without exposing it |
||
| 118 | require_once API_ROOT_PATH . '/inc/encryption_utils.php'; |
||
| 119 | $encryptedPrivateKey = encrypt_with_session_key($privateKeyClear, $sessionKey); |
||
| 120 | |||
| 121 | if ($encryptedPrivateKey === false) { |
||
| 122 | return ["error" => "Login failed.", "info" => "Failed to encrypt private key"]; |
||
| 123 | } |
||
| 124 | |||
| 125 | // Store the ENCRYPTED private key in the API table |
||
| 126 | // Even if the database is compromised, the key cannot be used without the session_key from the JWT |
||
| 127 | DB::update( |
||
| 128 | prefixTable('api'), |
||
| 129 | [ |
||
| 130 | 'encrypted_private_key' => $encryptedPrivateKey, |
||
| 131 | 'session_key_salt' => $sessionKeySalt, |
||
| 132 | 'session_key' => $keyTempo, |
||
| 133 | 'timestamp' => time(), |
||
| 134 | ], |
||
| 135 | 'user_id = %i', |
||
| 136 | $userInfo['id'] |
||
| 137 | ); |
||
| 138 | |||
| 139 | // get user folders list |
||
| 140 | $ret = $this->buildUserFoldersList($userInfo); |
||
| 141 | |||
| 142 | // Load config |
||
| 143 | $configManager = new ConfigManager(); |
||
| 144 | $SETTINGS = $configManager->getAllSettings(); |
||
| 145 | |||
| 146 | // Log user |
||
| 147 | logEvents($SETTINGS, 'api', 'user_connection', (string) $userInfo['id'], stripslashes($userInfo['login'])); |
||
| 148 | |||
| 149 | // create JWT with session key |
||
| 150 | return $this->createUserJWT( |
||
| 151 | (int) $userInfo['id'], |
||
| 152 | (string) $inputData['login'], |
||
| 153 | (int) $userInfo['personal_folder'], |
||
| 154 | (string) implode(",", $ret['folders']), |
||
| 155 | (string) implode(",", $ret['items']), |
||
| 156 | (string) $keyTempo, |
||
| 157 | (string) base64_encode($sessionKey), // Session key for decrypting private key |
||
| 158 | (int) $userInfo['admin'], |
||
| 159 | (int) $userInfo['gestionnaire'], |
||
| 160 | (int) $userInfo['can_create_root_folder'], |
||
| 161 | (int) $userInfo['can_manage_all_users'], |
||
| 162 | (string) $userInfo['fonction_id'], |
||
| 163 | (string) $userInfo['api_allowed_folders'], |
||
| 164 | (int) $userInfo['api_allowed_to_create'], |
||
| 165 | (int) $userInfo['api_allowed_to_read'], |
||
| 166 | (int) $userInfo['api_allowed_to_update'], |
||
| 167 | (int) $userInfo['api_allowed_to_delete'], |
||
| 168 | ); |
||
| 169 | } else { |
||
| 170 | return ["error" => "Login failed.", "info" => "Credentials not valid"]; |
||
| 171 | } |
||
| 172 | } |
||
| 173 | } |
||
| 174 | //end getUserAuth |
||
| 175 | |||
| 176 | /** |
||
| 177 | * Create a JWT |
||
| 178 | * |
||
| 179 | * Note: User encryption keys (public_key, private_key) are no longer stored in the JWT |
||
| 180 | * to reduce token size. Instead, the private key is encrypted with a session key and |
||
| 181 | * stored in the database. The session key is stored in the JWT (~44 bytes) which is |
||
| 182 | * used to decrypt the private key when needed. |
||
| 183 | * |
||
| 184 | * Security approach (defense in depth): |
||
| 185 | * - Encrypted private key in DB is useless without session_key |
||
| 186 | * - session_key in JWT is useless without encrypted private key from DB |
||
| 187 | * - Both are required together to access the decrypted private key |
||
| 188 | * |
||
| 189 | * @param integer $id |
||
| 190 | * @param string $login |
||
| 191 | * @param integer $pf_enabled |
||
| 192 | * @param string $folders |
||
| 193 | * @param string $items |
||
| 194 | * @param string $keyTempo |
||
| 195 | * @param string $sessionKey Session key (base64) for decrypting private key from DB |
||
| 196 | * @param integer $admin |
||
| 197 | * @param integer $manager |
||
| 198 | * @param integer $can_create_root_folder |
||
| 199 | * @param integer $can_manage_all_users |
||
| 200 | * @param string $roles |
||
| 201 | * @param string $allowed_folders |
||
| 202 | * @param integer $allowed_to_create |
||
| 203 | * @param integer $allowed_to_read |
||
| 204 | * @param integer $allowed_to_update |
||
| 205 | * @param integer $allowed_to_delete |
||
| 206 | * @return array |
||
| 207 | */ |
||
| 208 | private function createUserJWT( |
||
| 209 | int $id, |
||
| 210 | string $login, |
||
| 211 | int $pf_enabled, |
||
| 212 | string $folders, |
||
| 213 | string $items, |
||
| 214 | string $keyTempo, |
||
| 215 | string $sessionKey, |
||
| 216 | int $admin, |
||
| 217 | int $manager, |
||
| 218 | int $can_create_root_folder, |
||
| 219 | int $can_manage_all_users, |
||
| 220 | string $roles, |
||
| 221 | string $allowed_folders, |
||
| 222 | int $allowed_to_create, |
||
| 223 | int $allowed_to_read, |
||
| 224 | int $allowed_to_update, |
||
| 225 | int $allowed_to_delete, |
||
| 226 | ): array |
||
| 227 | { |
||
| 228 | // Load config |
||
| 229 | $configManager = new ConfigManager(); |
||
| 230 | $SETTINGS = $configManager->getAllSettings(); |
||
| 231 | |||
| 232 | $payload = [ |
||
| 233 | 'username' => $login, |
||
| 234 | 'id' => $id, |
||
| 235 | 'exp' => (time() + $SETTINGS['api_token_duration'] + 600), |
||
| 236 | 'pf_enabled' => $pf_enabled, |
||
| 237 | 'folders_list' => $folders, |
||
| 238 | 'restricted_items_list' => $items, |
||
| 239 | 'key_tempo' => $keyTempo, |
||
| 240 | 'session_key' => $sessionKey, // Session key for decrypting private key from database |
||
| 241 | 'is_admin' => $admin, |
||
| 242 | 'is_manager' => $manager, |
||
| 243 | 'user_can_create_root_folder' => $can_create_root_folder, |
||
| 244 | 'user_can_manage_all_users' => $can_manage_all_users, |
||
| 245 | 'roles' => $roles, |
||
| 246 | 'allowed_folders' => $allowed_folders, |
||
| 247 | 'allowed_to_create' => $allowed_to_create, |
||
| 248 | 'allowed_to_read' => $allowed_to_read, |
||
| 249 | 'allowed_to_update' => $allowed_to_update, |
||
| 250 | 'allowed_to_delete' => $allowed_to_delete, |
||
| 251 | ]; |
||
| 252 | |||
| 253 | return ['token' => JWT::encode($payload, DB_PASSWD, 'HS256')]; |
||
| 254 | } |
||
| 255 | |||
| 256 | //end createUserJWT |
||
| 257 | |||
| 258 | |||
| 259 | /** |
||
| 260 | * Permit to build the list of folders the user can access |
||
| 261 | * |
||
| 262 | * @param array $userInfo |
||
| 263 | * @return array |
||
| 264 | */ |
||
| 265 | private function buildUserFoldersList(array $userInfo): array |
||
| 266 | { |
||
| 267 | //Build tree |
||
| 268 | $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title'); |
||
| 269 | |||
| 270 | // Start by adding the manually added folders |
||
| 271 | $allowedFolders = array_map('intval', explode(";", $userInfo['groupes_visibles'])); |
||
| 272 | $readOnlyFolders = []; |
||
| 273 | $allowedFoldersByRoles = []; |
||
| 274 | $restrictedFoldersForItems = []; |
||
| 275 | $foldersLimited = []; |
||
| 276 | $foldersLimitedFull = []; |
||
| 277 | $restrictedItems = []; |
||
| 278 | $personalFolders = []; |
||
| 279 | |||
| 280 | $userFunctionId = explode(";", $userInfo['fonction_id']); |
||
| 281 | |||
| 282 | // Get folders from the roles |
||
| 283 | if (count($userFunctionId) > 0) { |
||
| 284 | $rows = DB::query( |
||
| 285 | 'SELECT * |
||
| 286 | FROM ' . prefixTable('roles_values') . ' |
||
| 287 | WHERE role_id IN %li AND type IN ("W", "ND", "NE", "NDNE", "R")', |
||
| 288 | $userFunctionId |
||
| 289 | ); |
||
| 290 | foreach ($rows as $record) { |
||
| 291 | if ($record['type'] === 'R') { |
||
| 292 | array_push($readOnlyFolders, $record['folder_id']); |
||
| 293 | } elseif (in_array($record['folder_id'], $allowedFolders) === false) { |
||
| 294 | array_push($allowedFoldersByRoles, $record['folder_id']); |
||
| 295 | } |
||
| 296 | } |
||
| 297 | $allowedFoldersByRoles = array_unique($allowedFoldersByRoles); |
||
| 298 | $readOnlyFolders = array_unique($readOnlyFolders); |
||
| 299 | // Clean arrays |
||
| 300 | foreach ($allowedFoldersByRoles as $value) { |
||
| 301 | $key = array_search($value, $readOnlyFolders); |
||
| 302 | if ($key !== false) { |
||
| 303 | unset($readOnlyFolders[$key]); |
||
| 304 | } |
||
| 305 | } |
||
| 306 | } |
||
| 307 | |||
| 308 | // Does this user is allowed to see other items |
||
| 309 | $inc = 0; |
||
| 310 | $rows = DB::query( |
||
| 311 | 'SELECT id, id_tree |
||
| 312 | FROM ' . prefixTable('items') . ' |
||
| 313 | WHERE restricted_to LIKE %s'. |
||
| 314 | (count($userFunctionId) > 0 ? ' AND id_tree NOT IN %li' : ''), |
||
| 315 | $userInfo['id'], |
||
| 316 | count($userFunctionId) > 0 ? $userFunctionId : DB::sqleval('0') |
||
| 317 | ); |
||
| 318 | foreach ($rows as $record) { |
||
| 319 | // Exclude restriction on item if folder is fully accessible |
||
| 320 | $restrictedFoldersForItems[$inc] = $record['id_tree']; |
||
| 321 | ++$inc; |
||
| 322 | } |
||
| 323 | |||
| 324 | // Check for the users roles if some specific rights exist on items |
||
| 325 | $rows = DB::query( |
||
| 326 | 'SELECT i.id_tree, r.item_id |
||
| 327 | FROM ' . prefixTable('items') . ' AS i |
||
| 328 | INNER JOIN ' . prefixTable('restriction_to_roles') . ' AS r ON (r.item_id=i.id) |
||
| 329 | WHERE '.(count($userFunctionId) > 0 ? ' id_tree NOT IN %li AND ' : '').' i.id_tree != "" |
||
| 330 | ORDER BY i.id_tree ASC', |
||
| 331 | count($userFunctionId) > 0 ? $userFunctionId : DB::sqleval('0') |
||
| 332 | ); |
||
| 333 | foreach ($rows as $record) { |
||
| 334 | $foldersLimited[$record['id_tree']][$inc] = $record['item_id']; |
||
| 335 | //array_push($foldersLimitedFull, $record['item_id']); |
||
| 336 | array_push($restrictedItems, $record['item_id']); |
||
| 337 | array_push($foldersLimitedFull, $record['id_tree']); |
||
| 338 | ++$inc; |
||
| 339 | } |
||
| 340 | |||
| 341 | // Add all personal folders |
||
| 342 | $rows = DB::queryFirstRow( |
||
| 343 | 'SELECT id |
||
| 344 | FROM ' . prefixTable('nested_tree') . ' |
||
| 345 | WHERE title = %i AND personal_folder = 1'. |
||
| 346 | (count($userFunctionId) > 0 ? ' AND id NOT IN %li' : ''), |
||
| 347 | $userInfo['id'], |
||
| 348 | count($userFunctionId) > 0 ? $userFunctionId : DB::sqleval('0') |
||
| 349 | ); |
||
| 350 | if (empty($rows['id']) === false) { |
||
| 351 | array_push($personalFolders, $rows['id']); |
||
| 352 | // get all descendants |
||
| 353 | $ids = $tree->getDescendants($rows['id'], false, false, true); |
||
| 354 | foreach ($ids as $id) { |
||
| 355 | array_push($personalFolders, $id); |
||
| 356 | } |
||
| 357 | } |
||
| 358 | |||
| 359 | // All folders visibles |
||
| 360 | return [ |
||
| 361 | 'folders' => array_unique( |
||
| 362 | array_filter( |
||
| 363 | array_merge( |
||
| 364 | $allowedFolders, |
||
| 365 | $foldersLimitedFull, |
||
| 366 | $allowedFoldersByRoles, |
||
| 367 | $restrictedFoldersForItems, |
||
| 368 | $readOnlyFolders, |
||
| 369 | $personalFolders |
||
| 370 | ) |
||
| 371 | ) |
||
| 372 | ), |
||
| 373 | 'items' => array_unique($restrictedItems), |
||
| 374 | ]; |
||
| 375 | } |
||
| 376 | //end buildUserFoldersList |
||
| 377 | } |