From: Tim Düsterhus Date: Fri, 27 Nov 2020 09:34:10 +0000 (+0100) Subject: Add update_com.woltlab.wcf_5.4_migrate_multifactor.php X-Git-Tag: 5.4.0_Alpha_1~555^2~11^2 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=ca2fdcd202147471f0f6e0ecea1aa54edfd498ff;p=GitHub%2FWoltLab%2FWCF.git Add update_com.woltlab.wcf_5.4_migrate_multifactor.php --- diff --git a/com.woltlab.wcf/package.xml b/com.woltlab.wcf/package.xml index a69365f825..a35036aaf5 100644 --- a/com.woltlab.wcf/package.xml +++ b/com.woltlab.wcf/package.xml @@ -98,5 +98,8 @@ tar cvf com.woltlab.wcf/files_pre.tar -C wcfsetup/install/files/ \ + + + acp/update_com.woltlab.wcf_5.4_migrate_multifactor.php 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 index 0000000000..dd4a4f988f --- /dev/null +++ b/wcfsetup/install/files/acp/update_com.woltlab.wcf_5.4_migrate_multifactor.php @@ -0,0 +1,151 @@ + + * @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(); +}