Passed
Push — master ( df7a06...000e40 )
by Tim
03:26
created

Database::getUsernameByHashedId()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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