Add update_com.woltlab.wcf_5.4_migrate_multifactor.php
authorTim Düsterhus <duesterhus@woltlab.com>
Fri, 27 Nov 2020 09:34:10 +0000 (10:34 +0100)
committerTim Düsterhus <duesterhus@woltlab.com>
Mon, 30 Nov 2020 11:11:36 +0000 (12:11 +0100)
com.woltlab.wcf/package.xml
wcfsetup/install/files/acp/update_com.woltlab.wcf_5.4_migrate_multifactor.php [new file with mode: 0644]

index a69365f8257c9fb518538e64b30361f4feaa4a70..a35036aaf53b07bbe5214704815fc22923dc8128 100644 (file)
@@ -98,5 +98,8 @@ tar cvf com.woltlab.wcf/files_pre.tar -C wcfsetup/install/files/ \
                <instruction type="userMenu" />
                <instruction type="page" />
                <instruction type="language" />
+               
+               <!-- Migration of multi-factor authentication. -->
+               <instruction type="script" run="standalone">acp/update_com.woltlab.wcf_5.4_migrate_multifactor.php</instruction>
        </instructions>
 </package>
diff --git a/wcfsetup/install/files/acp/update_com.woltlab.wcf_5.4_migrate_multifactor.php b/wcfsetup/install/files/acp/update_com.woltlab.wcf_5.4_migrate_multifactor.php
new file mode 100644 (file)
index 0000000..dd4a4f9
--- /dev/null
@@ -0,0 +1,151 @@
+<?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 wcf\data\object\type\ObjectTypeCache;
+use wcf\data\package\PackageCache;
+use wcf\data\user\User;
+use wcf\data\user\UserEditor;
+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);
+$statement->execute([
+       'totp',
+       $totpMethod->objectTypeID,
+]);
+$userIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
+
+// 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);
+
+// TODO: Do we need to split this across multiple requests?
+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(),
+                       \bin2hex(\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();
+}