Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
UpdateSecretsToEncryptedFormat
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 3
132
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 doDBUpdates
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
90
 getUpdateKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 *
5 * @file
6 * @ingroup Maintenance
7 */
8
9declare( strict_types = 1 );
10
11namespace MediaWiki\Extension\OATHAuth\Maintenance;
12
13use MediaWiki\Extension\OATHAuth\Key\RecoveryCodeKeys;
14use MediaWiki\Extension\OATHAuth\Key\TOTPKey;
15use MediaWiki\Extension\OATHAuth\Module\RecoveryCodes;
16use MediaWiki\Extension\OATHAuth\Module\TOTP;
17use MediaWiki\Extension\OATHAuth\OATHAuthServices;
18use MediaWiki\Json\FormatJson;
19use MediaWiki\Maintenance\LoggedUpdateMaintenance;
20
21// @codeCoverageIgnoreStart
22$IP = getenv( 'MW_INSTALL_PATH' );
23if ( $IP === false ) {
24    $IP = __DIR__ . '/../../..';
25}
26
27require_once "$IP/maintenance/Maintenance.php";
28// @codeCoverageIgnoreEnd
29
30/**
31 * Updates TOTP secret to an encrypted format in the database
32 *
33 * Usage: php UpdateSecretsToEncryptedFormat.php
34 */
35class UpdateSecretsToEncryptedFormat extends LoggedUpdateMaintenance {
36
37    public function __construct() {
38        parent::__construct();
39        $this->requireExtension( 'OATHAuth' );
40        $this->addDescription( 'Update TOTP secrets and recovery codes to use encypted format within database' );
41    }
42
43    /** @inheritDoc */
44    protected function doDBUpdates() {
45        if ( !extension_loaded( 'sodium' ) ) {
46            $this->fatalError( "libsodium is not installed with php in this environment!" );
47        }
48
49        $encryptionHelper = OATHAuthServices::getInstance( $this->getServiceContainer() )
50            ->getEncryptionHelper();
51
52        if ( !$encryptionHelper->isEnabled() ) {
53            // phpcs:disable Generic.Files.LineLength.TooLong
54            $this->fatalError( "\$wgOATHSecretKey is not set correctly! It should be set to an immutable, 64-character hexadecimal value!" );
55        }
56
57        $startTime = time();
58        $updatedCount = 0;
59        $alreadyEncrypted = 0;
60        $totalRows = 0;
61
62        $services = $this->getServiceContainer();
63
64        $moduleRegistry = OATHAuthServices::getInstance()->getModuleRegistry();
65        $totpModuleId = $moduleRegistry->getModuleId( TOTP::MODULE_NAME );
66        $recoveryModuleId = $moduleRegistry->getModuleId( RecoveryCodes::MODULE_NAME );
67
68        $dbw = $services
69            ->getDBLoadBalancerFactory()
70            ->getPrimaryDatabase( 'virtual-oathauth' );
71        $res = $dbw->newSelectQueryBuilder()
72            ->select( [ 'oad_id', 'oad_data', 'oad_type' ] )
73            ->from( 'oathauth_devices' )
74            ->where( [ 'oad_type' => [ $totpModuleId, $recoveryModuleId ] ] )
75            ->caller( __METHOD__ )
76            ->fetchResultSet();
77
78        foreach ( $res as $row ) {
79            $totalRows++;
80            $data = FormatJson::decode( $row->oad_data, true );
81
82            if ( array_key_exists( 'nonce', $data ) ) {
83                // Already encrypted
84                $alreadyEncrypted++;
85                continue;
86            }
87
88            $key = null;
89            if ( (int)$row->oad_type === $totpModuleId ) {
90                $key = TOTPKey::newFromArray( $data );
91            } elseif ( (int)$row->oad_type === $recoveryModuleId ) {
92                $key = RecoveryCodeKeys::newFromArray( $data );
93            } else {
94                // Impossible
95                $this->output( "Unable to update row with oad_id {$row->oad_id} and oad_type {$row->oad_type}.\n" );
96                continue;
97            }
98
99            $dbw->newUpdateQueryBuilder()
100                ->update( 'oathauth_devices' )
101                ->set( [ 'oad_data' => FormatJson::encode( $key->jsonSerialize() ) ] )
102                ->where( [ 'oad_id' => $row->oad_id ] )
103                ->caller( __METHOD__ )
104                ->execute();
105
106            $updatedCount++;
107            if ( $updatedCount % 50 === 0 ) {
108                $this->output( "{$updatedCount}\n" );
109            }
110        }
111
112        $totalTimeInSeconds = time() - $startTime;
113        $this->output( "Done. Updated {$updatedCount} of {$totalRows} rows in {$totalTimeInSeconds} seconds.\n" );
114        if ( $alreadyEncrypted > 0 ) {
115            $this->output( "{$alreadyEncrypted} rows were already encrypted.\n" );
116        }
117        return true;
118    }
119
120    /**
121     * @return string
122     */
123    protected function getUpdateKey() {
124        return __CLASS__;
125    }
126}
127
128// @codeCoverageIgnoreStart
129$maintClass = UpdateSecretsToEncryptedFormat::class;
130require_once RUN_MAINTENANCE_IF_MAIN;
131// @codeCoverageIgnoreEnd