/
update_com.woltlab.wcf_5.4_migrate_multifactor.php
165 lines (141 loc) · 5.48 KB
/
update_com.woltlab.wcf_5.4_migrate_multifactor.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
<?php
/**
* Migrates multifactor authentication data from the Two Step Verification plugin developed
* by Hanashi Development.
*
* @author Tim Duesterhus
* @copyright 2001-2020 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core
*/
use ParagonIE\ConstantTime\Base32;
use ParagonIE\ConstantTime\Hex;
use wcf\data\object\type\ObjectTypeCache;
use wcf\data\package\PackageCache;
use wcf\data\user\User;
use wcf\data\user\UserEditor;
use wcf\system\package\SplitNodeException;
use wcf\system\user\authentication\password\algorithm\Wcf1;
use wcf\system\user\authentication\password\PasswordAlgorithmManager;
use wcf\system\user\multifactor\Setup;
use wcf\system\WCF;
$hanashiTwoStep = PackageCache::getInstance()->getPackageByIdentifier('eu.hanashi.wsc.two-step-verification');
if (!$hanashiTwoStep) {
return;
}
// Fetch the object types for the relevant MFA methods.
$totpMethod = ObjectTypeCache::getInstance()
->getObjectTypeByName('com.woltlab.wcf.multifactor', 'com.woltlab.wcf.multifactor.totp');
$backupMethod = ObjectTypeCache::getInstance()
->getObjectTypeByName('com.woltlab.wcf.multifactor', 'com.woltlab.wcf.multifactor.backup');
// Fetch the backup code hashing algorithm.
// We use the Wcf1 algorithm as it's super cheap compared to BCrypt and the previous
// backup codes were stored in plaintext, leading to a net improvement.
$hashAlgorithm = new Wcf1();
$hashAlgorithmName = PasswordAlgorithmManager::getInstance()->getNameFromAlgorithm($hashAlgorithm);
// Fetch the affected user IDs.
$sql = "SELECT DISTINCT userID
FROM wcf" . WCF_N . "_user_authenticator
WHERE type = ?
AND userID NOT IN (
SELECT userID
FROM wcf" . WCF_N . "_user_multifactor
WHERE objectTypeID = ?
)";
$statement = WCF::getDB()->prepareStatement($sql, 30);
$statement->execute([
'totp',
$totpMethod->objectTypeID,
]);
$userIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
// Check whether we processed all users.
if (empty($userIDs)) {
return;
}
// Prepare the statements for use in user processing.
$sql = "SELECT name, secret, time
FROM wcf" . WCF_N . "_user_authenticator
WHERE type = ?
AND userID = ?
FOR UPDATE";
$existingTotpAuthenticatorStatement = WCF::getDB()->prepareStatement($sql);
$sql = "SELECT backupCode
FROM wcf" . WCF_N . "_user_backup_code
WHERE userID = ?
FOR UPDATE";
$existingBackupStatement = WCF::getDB()->prepareStatement($sql);
$sql = "INSERT INTO wcf" . WCF_N . "_user_multifactor_totp
(setupID, deviceID, deviceName, secret, minCounter, createTime)
VALUES (?, ?, ?, ?, ?, ?)";
$createTotpStatement = WCF::getDB()->prepareStatement($sql);
$sql = "INSERT INTO wcf" . WCF_N . "_user_multifactor_backup
(setupID, identifier, code, createTime)
VALUES (?, ?, ?, ?)";
$createBackupStatement = WCF::getDB()->prepareStatement($sql);
foreach ($userIDs as $userID) {
WCF::getDB()->beginTransaction();
// Do not use UserRuntimeCache due to possible memory constraints.
$user = new User($userID);
$userEditor = new UserEditor($user);
if (Setup::find($totpMethod, $user) !== null) {
// Skip this user, because they have an enabled TOTP method.
// This should never happen, because these users are filtered out
// when selecting, but we are going to play safe.
continue;
}
$totpSetup = Setup::allocateSetUpId($totpMethod, $user);
$existingTotpAuthenticatorStatement->execute([
'totp',
$user->userID,
]);
$earliestTotp = null;
while ($row = $existingTotpAuthenticatorStatement->fetchArray()) {
$createTotpStatement->execute([
$totpSetup->getId(),
Hex::encode(\random_bytes(16)),
$row['name'],
Base32::decodeUpper($row['secret']),
($row['time'] / 30),
$row['time'],
]);
if ($earliestTotp === null || $earliestTotp > $row['time']) {
$earliestTotp = $row['time'];
}
}
$backupSetup = Setup::allocateSetUpId($backupMethod, $user);
$existingBackupStatement->execute([
$user->userID,
]);
$usedIdentifiers = [];
while ($row = $existingBackupStatement->fetchArray()) {
// We intentionally do not validate the signature for resiliency and because
// we trust the database to not contain bogus information.
$parts = \explode('-', $row['backupCode'], 2);
if (\count($parts) < 2) {
continue;
}
$code = @\base64_decode($parts[1]);
if (!$code) {
continue;
}
$identifier = \mb_substr($code, 0, 5, '8bit');
if (isset($usedIdentifiers[$identifier])) {
continue;
}
$usedIdentifiers[$identifier] = $identifier;
$createBackupStatement->execute([
$backupSetup->getId(),
$identifier,
$hashAlgorithmName . ':' . $hashAlgorithm->hash($code),
$earliestTotp,
]);
}
$userEditor->update([
'multifactorActive' => 1,
]);
WCF::getDB()->commitTransaction();
}
// If we reached this location we processed at least one user.
// If this was the final user the next iteration will abort this
// script early, thus not splitting the node.
throw new SplitNodeException();