Database   A
last analyzed

Complexity

Total Complexity 25

Size/Duplication

Total Lines 313
Duplicated Lines 0 %

Importance

Changes 9
Bugs 4 Features 0
Metric Value
eloc 105
c 9
b 4
f 0
dl 0
loc 313
rs 10
wmc 25

10 Methods

Rating   Name   Duplication   Size   Complexity  
A is2FAEnabled() 0 31 6
A __sleep() 0 4 1
A getUsernameByHashedId() 0 13 2
B __construct() 0 42 7
A doesCredentialExist() 0 14 2
A getTokenData() 0 18 3
A storeTokenData() 0 35 1
A __wakeup() 0 3 1
A deleteTokenData() 0 10 1
A updateSignCount() 0 10 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\webauthn\WebAuthn\Store;
6
7
use PDO;
8
use SimpleSAML\Configuration;
9
use SimpleSAML\Database as SSP_Database;
10
use SimpleSAML\Logger;
11
use SimpleSAML\Module\webauthn\Store;
12
13
/**
14
 * Store FIDO2 information in database.
15
 *
16
 * This class implements a store which stores the FIDO2 information in a
17
 * database. It is tested with MySQL, others might work, too.
18
 *
19
 * It has the following options:
20
 * - dsn: The DSN which should be used to connect to the database server. See
21
 *   the PHP Manual for supported drivers and DSN formats.
22
 * - username: The username used for database connection.
23
 * - password: The password used for database connection.
24
 *
25
 * @author Stefan Winter <[email protected]>
26
 * @package SimpleSAMLphp
27
 */
28
29
class Database extends Store
30
{
31
    /**
32
     * Database handle.
33
     *
34
     * This variable can't be serialized.
35
     */
36
    private SSP_Database $db;
37
38
39
    /**
40
     * The configuration for our database store.
41
     *
42
     * @var array
43
     */
44
    private array $config;
45
46
47
    /**
48
     * Parse configuration.
49
     *
50
     * This constructor parses the configuration.
51
     *
52
     * @param array $config Configuration for database consent store.
53
     *
54
     * @throws \Exception in case of a configuration error.
55
     */
56
    public function __construct(array $config)
57
    {
58
        parent::__construct($config);
59
        $this->config = $config;
60
        $this->db = SSP_Database::getInstance(Configuration::loadFromArray($config));
61
        $driver = $this->db->getDriver();
62
        // phpcs:disable Generic.Files.LineLength.TooLong
63
        try {
64
            $this->db->read("SELECT COUNT(*) FROM credentials");
65
        } catch (\Exception $e) {
66
            $this->db->write("
67
                CREATE TABLE IF NOT EXISTS credentials (
68
                    creation_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
69
                    user_id VARCHAR(80) NOT NULL,
70
                    credentialId VARCHAR(1024) " . ($driver === 'mysql' ? "CHARACTER SET 'binary'" : '') . " NOT NULL,
71
                    credential " . ($driver === 'pgsql' ? 'BYTEA' : 'MEDIUMBLOB') . " NOT NULL,
72
                    algo INT DEFAULT NULL,
73
                    presenceLevel INT DEFAULT NULL,
74
                    isResidentKey BOOLEAN DEFAULT NULL,
75
                    signCounter INT NOT NULL,
76
                    friendlyName VARCHAR(100) DEFAULT 'Unnamed Token',
77
                    hashedId VARCHAR(128) DEFAULT '---',
78
                    aaguid VARCHAR(64) DEFAULT NULL,
79
                    " . ($driver === 'pgsql'
80
                        ? "attLevel VARCHAR(6) NOT NULL DEFAULT 'None' CHECK (attLevel IN ('None','Basic','Self','AttCA')),"
81
                        : "attLevel ENUM('None','Basic','Self','AttCA') NOT NULL DEFAULT 'None',")
82
                    . "
83
                    lastUsedTime TIMESTAMP DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(),
84
                    lastUsedIp VARCHAR(64) DEFAULT NULL,
85
                    CONSTRAINT credentials_user_id_credentialId_key UNIQUE (user_id, credentialId)
86
                )
87
            ");
88
        }
89
        try {
90
            $this->db->read("SELECT COUNT(*) FROM userstatus");
91
        } catch (\Exception $e) {
92
            $this->db->write("CREATE TABLE IF NOT EXISTS userstatus (
93
            user_id VARCHAR(80) NOT NULL,
94
            " . ($driver === 'pgsql'
95
                ? "fido2Status VARCHAR(14) NOT NULL DEFAULT 'FIDO2Disabled' CHECK (fido2Status IN ('FIDO2Disabled', 'FIDO2Enabled')),"
96
                : "fido2Status ENUM('FIDO2Disabled','FIDO2Enabled') NOT NULL DEFAULT 'FIDO2Disabled',")
97
            . "
98
            CONSTRAINT userstatus_user_id_key UNIQUE (user_id)
99
            )");
100
        }
101
        // phpcs:enable Generic.Files.LineLength.TooLong
102
    }
103
104
    /**
105
     * Called before serialization.
106
     *
107
     * @return array The variables which should be serialized.
108
     */
109
    public function __sleep(): array
110
    {
111
        return [
112
            'config',
113
        ];
114
    }
115
116
117
    /**
118
     * Called after unserialization.
119
     * @return void
120
     */
121
    public function __wakeup(): void
122
    {
123
        $this->db = SSP_Database::getInstance(Configuration::loadFromArray($this->config));
124
    }
125
126
127
    /**
128
     * is the user subject to 2nd factor at all?
129
     *
130
     * This function checks whether a given user has been enabled for WebAuthn.
131
     *
132
     * @param string $userId The hash identifying the user at an IdP.
133
     * @param bool $defaultIfNx if not found in the DB, should the user be considered enabled (true)
134
     *                              or disabled(false)
135
     * @param bool $useDatabase a bool that determines whether to use local database or not
136
     * @param bool $toggle variable which is associated with $force because it determines its meaning, it either
137
     *                     simply means whether to trigger webauthn authentication or switch the default settings,
138
     * @param bool $force switch that determines how $toggle will be used, if true then value of $toggle
139
     *                    will mean whether to trigger (true) or not (false) the webauthn authentication,
140
     *                    if false then $toggle means whether to switch the value of $defaultEnabled and then use that
141
     *
142
     * @return bool True if the user is enabled for 2FA, false if not
143
     */
144
    public function is2FAEnabled(
145
        string $userId,
146
        bool $defaultIfNx,
147
        bool $useDatabase = true,
148
        bool $toggle = false,
149
        bool $force = true,
150
    ): bool {
151
        if (!$useDatabase) {
152
            if ($force) {
153
                return $toggle;
154
            } else {
155
                return $toggle ? !$defaultIfNx : $defaultIfNx;
156
            }
157
        }
158
        $st = $this->db->read('SELECT COUNT(*) FROM userstatus WHERE user_id = :userId', ['userId' => $userId]);
159
160
        $c = $st->fetchColumn();
161
        if ($c == 0) {
162
            Logger::debug('User does not exist in DB, returning desired default.');
163
            return $defaultIfNx;
164
        } else {
165
            $st2 = $this->db->read(
166
                "SELECT COUNT(*) FROM userstatus WHERE user_id = :userId AND fido2Status = 'FIDO2Disabled'",
167
                ['userId' => $userId],
168
            );
169
            $rowCount2 = $st2->fetchColumn();
170
            if ($rowCount2 === 1 /* explicitly disabled user in DB */) {
171
                return false;
172
            }
173
            Logger::debug('User exists and is not disabled -> enabled.');
174
            return true;
175
        }
176
    }
177
178
179
    /**
180
     * does a given credentialID already exist?
181
     *
182
     * This function checks whether a given credential ID already exists in the database
183
     *
184
     * @param string $credIdHex The hex representation of the credentialID to look for.
185
     *
186
     * @return bool True if the credential exists, false if not
187
     */
188
    public function doesCredentialExist(string $credIdHex): bool
189
    {
190
        $st = $this->db->read(
191
            'SELECT COUNT(*) FROM credentials WHERE credentialId = :credentialId',
192
            ['credentialId' => $credIdHex],
193
        );
194
195
        $rowCount = $st->fetchColumn();
196
        if (!$rowCount) {
197
            Logger::debug('Credential does not exist yet.');
198
            return false;
199
        } else {
200
            Logger::debug('Credential exists.');
201
            return true;
202
        }
203
    }
204
205
206
    /**
207
     * store newly enrolled token data
208
     *
209
     * @param string $userId        The user.
210
     * @param string $credentialId  The id identifying the credential.
211
     * @param string $credential    The credential.
212
     * @param int    $algo          The algorithm used.
213
     * @param int    $presenceLevel UV or UP?
214
     * @param int    $signCounter   The signature counter for this credential.
215
     * @param string $friendlyName  A user-supplied name for this token.
216
     * @param string $hashedId      hashed user ID
217
     *
218
     * @return true
219
     */
220
    public function storeTokenData(
221
        string $userId,
222
        string $credentialId,
223
        string $credential,
224
        int $algo,
225
        int $presenceLevel,
226
        int $isResidentKey,
227
        int $signCounter,
228
        string $friendlyName,
229
        string $hashedId,
230
        string $aaguid,
231
        string $attLevel,
232
    ): bool {
233
        // phpcs:disable Generic.Files.LineLength.TooLong
234
        $this->db->write(
235
            'INSERT INTO credentials ' .
236
            '(user_id, credentialId, credential, algo, presenceLevel, isResidentKey, signCounter, friendlyName, hashedId, aaguid, attLevel) VALUES ' .
237
            '(:userId,:credentialId,:credential,:algo,:presenceLevel,:isResidentKey,:signCounter,:friendlyName,:hashedId,:aaguid,:attLevel)',
238
            [
239
                'userId' => $userId,
240
                'credentialId' => $credentialId,
241
                'credential' => $credential,
242
                'algo' => $algo,
243
                'presenceLevel' => $presenceLevel,
244
                'isResidentKey' => $isResidentKey,
245
                'signCounter' => $signCounter,
246
                'friendlyName' => $friendlyName,
247
                'hashedId' => $hashedId,
248
                'aaguid' => $aaguid,
249
                'attLevel' => $attLevel,
250
            ],
251
        );
252
        // phpcs:enable Generic.Files.LineLength.TooLong
253
254
        return true;
255
    }
256
257
258
    /**
259
     * remove an existing credential from the database
260
     *
261
     * @param string $credentialId the credential
262
     * @return true
263
     */
264
    public function deleteTokenData(string $credentialId): bool
265
    {
266
        $this->db->write(
267
            'DELETE FROM credentials WHERE credentialId = :credentialId',
268
            ['credentialId' => $credentialId],
269
        );
270
271
        Logger::debug('webauthn:Database - DELETED credential.');
272
273
        return true;
274
    }
275
276
277
    /**
278
     * increment the signature counter after a successful authentication
279
     *
280
     * @param string $credentialId the credential
281
     * @param int    $signCounter  the new counter value
282
     * @return true
283
     */
284
    public function updateSignCount(string $credentialId, int $signCounter): bool
285
    {
286
        $this->db->write(
287
            'UPDATE credentials SET signCounter = :signCounter WHERE credentialId = :credentialId',
288
            ['signCounter' => $signCounter, 'credentialId' => $credentialId],
289
        );
290
291
        Logger::debug('webauthn:Database - UPDATED signature counter.');
292
293
        return true;
294
    }
295
296
297
    /**
298
     * Retrieve existing token data
299
     *
300
     * @param string $userId the username
301
     * @return array Array of all crypto data we have on file.
302
     */
303
    public function getTokenData(string $userId): array
304
    {
305
        $ret = [];
306
307
        $st = $this->db->read(
308
        // phpcs:ignore Generic.Files.LineLength.TooLong
309
            'SELECT credentialId, credential, signCounter, friendlyName, algo, presenceLevel, isResidentKey FROM credentials WHERE user_id = :userId',
310
            ['userId' => $userId],
311
        );
312
313
        while ($row = $st->fetch(PDO::FETCH_NUM)) {
314
            if (is_resource($row[1])) {
315
                $row[1] = stream_get_contents($row[1]);
316
            }
317
            $ret[] = $row;
318
        }
319
320
        return $ret;
321
    }
322
323
    /**
324
     * Retrieve username, given a credential ID
325
     *
326
     * @param string $hashedId the credential ID
327
     * @return string the username, if found (otherwise, empty string)
328
     */
329
    public function getUsernameByHashedId(string $hashedId): string
330
    {
331
        $st = $this->db->read(
332
            'SELECT user_id FROM credentials WHERE hashedId = :hashId',
333
            ['hashId' => $hashedId],
334
        );
335
336
        // return on first match, credential IDs are unique
337
        while ($row = $st->fetch(PDO::FETCH_NUM)) {
338
            return $row[0];
339
        }
340
341
        return "";
342
    }
343
}
344