Passed
Push — master ( 8995ac...1b609d )
by Nils
06:33
created

get_user_keys()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 56
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 7
eloc 28
c 2
b 1
f 0
nc 6
nop 3
dl 0
loc 56
rs 8.5386

How to fix   Long Method   

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 library is distributed in the hope that it will be useful,
6
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
7
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8
 * ---
9
 *
10
 * @project   Teampass
11
 * @version    API
12
 *
13
 * @file      jwt_utils.php
14
 * ---
15
 *
16
 * @author    Nils Laumaillé ([email protected])
17
 *
18
 * @copyright 2009-2025 Teampass.net
19
 *
20
 * @license   https://spdx.org/licenses/GPL-3.0-only.html#licenseText GPL-3.0
21
 * ---
22
 *
23
 * @see       https://www.teampass.net
24
 */
25
26
use Symfony\Component\HttpFoundation\Request AS symfonyRequest;
27
use Firebase\JWT\JWT;
28
use Firebase\JWT\Key;
29
use Firebase\JWT\SignatureInvalidException;
30
use Firebase\JWT\BeforeValidException;
31
use Firebase\JWT\ExpiredException;
32
33
/**
34
 * Is the JWT valid
35
 *
36
 * @param string $jwt
37
 * @return boolean
38
 */
39
function is_jwt_valid($jwt) {
40
	try {
41
		$decoded = (array) JWT::decode($jwt, new Key(DB_PASSWD, 'HS256'));
42
43
		// Check if expiration is reached
44
		if ($decoded['exp'] - time() < 0) {
45
			return false;
46
		}
47
/*
48
		$decoded1 = JWT::decode($jwt, new Key(DB_PASSWD, 'HS256'), $headers = new stdClass());
49
		print_r($headers);
50
*/
51
52
		return true;
53
	} catch (InvalidArgumentException $e) {
54
		// provided key/key-array is empty or malformed.
55
		return false;
56
	} catch (DomainException $e) {
57
		// provided algorithm is unsupported OR
58
		// provided key is invalid OR
59
		// unknown error thrown in openSSL or libsodium OR
60
		// libsodium is required but not available.
61
		return false;
62
	} catch (SignatureInvalidException $e) {
63
		// provided JWT signature verification failed.
64
		return false;
65
	} catch (BeforeValidException $e) {
66
		// provided JWT is trying to be used before "nbf" claim OR
67
		// provided JWT is trying to be used before "iat" claim.
68
		return false;
69
	} catch (ExpiredException $e) {
70
		// provided JWT is trying to be used after "exp" claim.
71
		return false;
72
	} catch (UnexpectedValueException $e) {
73
		// provided JWT is malformed OR
74
		// provided JWT is missing an algorithm / using an unsupported algorithm OR
75
		// provided JWT algorithm does not match provided key OR
76
		// provided key ID in key/key-array is empty or invalid.
77
		return false;
78
	}
79
}
80
81
function base64url_encode($data) {
82
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
83
}
84
85
function get_authorization_header()
86
{
87
	$request = symfonyRequest::createFromGlobals();
88
	$authorizationHeader = $request->headers->get('Authorization');
89
	$headers = null;
90
	
91
	// Check if the authorization header is not empty
92
	if (!empty($authorizationHeader)) {
93
		$headers = trim($authorizationHeader);
94
	} else if (function_exists('apache_request_headers') === true) {
95
		$requestHeaders = (array) apache_request_headers();
96
		// Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization)
97
		$requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders));
98
		//print_r($requestHeaders);
99
		if (isset($requestHeaders['Authorization']) === true) {
100
			$headers = trim($requestHeaders['Authorization']);
101
		}
102
	}
103
	
104
	return $headers;
105
}
106
107
function get_bearer_token() {
108
    $headers = get_authorization_header();
109
	
110
    // HEADER: Get the access token from the header
111
    if (empty($headers) === false) {
112
        if (preg_match('/Bearer\s(\S+)/', $headers, $matches)) {
113
            return $matches[1];
114
        }
115
    }
116
    return null;
117
}
118
119
function get_bearer_data($jwt) {
120
    // split the jwt
121
	$tokenParts = explode('.', $jwt);
122
	$payload = base64_decode($tokenParts[1]);
123
124
    // HEADER: Get the access token from the header
125
    if (empty($payload) === false) {
126
        return json_decode($payload, true);
127
    }
128
    return null;
129
}
130
131
/**
132
 * Get user encryption keys from database
133
 *
134
 * This function retrieves the user's public and private keys from the database.
135
 * The private key is stored ENCRYPTED in the database and is decrypted using
136
 * the session key from the JWT token.
137
 *
138
 * Security architecture (defense in depth):
139
 * - The encrypted private key in DB is useless without the session_key
140
 * - The session_key in JWT is useless without the encrypted key from DB
141
 * - Both are required together to get the decrypted private key
142
 * - key_tempo is validated to ensure session is still valid
143
 *
144
 * @param int $userId User ID from JWT token
145
 * @param string $keyTempo Session token from JWT (for validation)
146
 * @param string $sessionKey Session key from JWT (base64 encoded) for decryption
147
 * @return array|null Array containing public_key and private_key (decrypted), or null if validation fails
148
 */
149
function get_user_keys(int $userId, string $keyTempo, string $sessionKey): ?array
150
{
151
    require_once API_ROOT_PATH . '/inc/encryption_utils.php';
152
153
    // Retrieve user's public key and encrypted private key from database
154
    $userInfo = DB::queryfirstrow(
155
        "SELECT u.public_key, u.key_tempo, a.encrypted_private_key
156
        FROM " . prefixTable('users') . " AS u
157
        INNER JOIN " . prefixTable('api') . " AS a ON (a.user_id = u.id)
158
        WHERE u.id = %i",
159
        $userId
160
    );
161
162
    if (DB::count() === 0) {
163
        // User not found or no API configuration
164
        error_log('[API] get_user_keys: User not found or no API config for user ID ' . $userId);
165
        return null;
166
    }
167
168
    // Validate key_tempo matches (security check - ensures session is still valid)
169
    if ($userInfo['key_tempo'] !== $keyTempo) {
170
        // Session invalid or expired
171
        error_log('[API] get_user_keys: Invalid key_tempo for user ID ' . $userId);
172
        return null;
173
    }
174
175
    // Check if encrypted private key exists
176
    if (empty($userInfo['encrypted_private_key'])) {
177
        // No encrypted key found - user needs to re-authenticate
178
        error_log('[API] get_user_keys: No encrypted private key found for user ID ' . $userId);
179
        return null;
180
    }
181
182
    // Decode the session key from base64
183
    $sessionKeyDecoded = base64_decode($sessionKey, true);
184
185
    if ($sessionKeyDecoded === false || strlen($sessionKeyDecoded) !== 32) {
186
        error_log('[API] get_user_keys: Invalid session key format');
187
        return null;
188
    }
189
190
    // Decrypt the private key using the session key
191
    $privateKeyDecrypted = decrypt_with_session_key(
192
        $userInfo['encrypted_private_key'],
193
        $sessionKeyDecoded
194
    );
195
196
    if ($privateKeyDecrypted === false) {
197
        // Decryption failed - wrong key or tampered data
198
        error_log('[API] get_user_keys: Failed to decrypt private key for user ID ' . $userId);
199
        return null;
200
    }
201
202
    return [
203
        'public_key' => $userInfo['public_key'],
204
        'private_key' => $privateKeyDecrypted, // Returns the DECRYPTED private key
205
    ];
206
}