diff --git a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step1.php b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step1.php index 09e41d6dcb0..bddd2c5e6f7 100644 --- a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step1.php +++ b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step1.php @@ -8,6 +8,7 @@ * @license GNU Lesser General Public License */ +use wcf\system\database\table\column\BinaryDatabaseTableColumn; use wcf\system\database\table\column\CharDatabaseTableColumn; use wcf\system\database\table\column\DefaultFalseBooleanDatabaseTableColumn; use wcf\system\database\table\column\IntDatabaseTableColumn; @@ -75,8 +76,23 @@ ]), PartialDatabaseTable::create('wcf1_acp_session_log') ->columns([ + BinaryDatabaseTableColumn::create('sessionID') + ->notNull() + ->length(40), NotNullVarchar255DatabaseTableColumn::create('hostname') ->defaultValue('') ->drop(), ]), + PartialDatabaseTable::create('wcf1_session') + ->columns([ + BinaryDatabaseTableColumn::create('sessionID') + ->notNull() + ->length(40), + ]), + PartialDatabaseTable::create('wcf1_user_session') + ->columns([ + BinaryDatabaseTableColumn::create('sessionID') + ->notNull() + ->length(40), + ]), ]; diff --git a/wcfsetup/install/files/lib/system/package/PackageInstallationDispatcher.class.php b/wcfsetup/install/files/lib/system/package/PackageInstallationDispatcher.class.php index 57807398a03..1781485b8b9 100644 --- a/wcfsetup/install/files/lib/system/package/PackageInstallationDispatcher.class.php +++ b/wcfsetup/install/files/lib/system/package/PackageInstallationDispatcher.class.php @@ -2,7 +2,6 @@ namespace wcf\system\package; -use ParagonIE\ConstantTime\Hex; use wcf\data\application\Application; use wcf\data\application\ApplicationBuilder; use wcf\data\devtools\project\DevtoolsProjectAction; @@ -257,9 +256,9 @@ protected function finalizeWcfSetup(): void 'user_session', CryptoUtil::createSignedString( \pack( - 'CA20C', - 1, - Hex::decode(WCF::getSession()->sessionID), + 'Ca40C', + 2, + WCF::getSession()->sessionID, 0 ) ) diff --git a/wcfsetup/install/files/lib/system/session/SessionHandler.class.php b/wcfsetup/install/files/lib/system/session/SessionHandler.class.php index a8165e6b540..0ac288a471c 100644 --- a/wcfsetup/install/files/lib/system/session/SessionHandler.class.php +++ b/wcfsetup/install/files/lib/system/session/SessionHandler.class.php @@ -2,6 +2,7 @@ namespace wcf\system\session; +use ParagonIE\ConstantTime\Base32; use ParagonIE\ConstantTime\Hex; use wcf\data\session\Session as LegacySession; use wcf\data\session\SessionEditor; @@ -194,6 +195,19 @@ private function parseCookie(string $value): array return $data; + case 2: + if ($length !== 42) { + throw new \InvalidArgumentException(\sprintf( + 'Expected exactly 42 Bytes, %d given.', + $length + )); + } + $data = \unpack('Cversion/a40sessionId/Ctimestep', $value); + \assert($data['version'] === 2); + \assert(\strlen($data['sessionId']) === 40); + + return $data; + default: throw new \InvalidArgumentException(\sprintf( 'Unknown version %d', @@ -275,9 +289,9 @@ private function getCookieValue(): string } return CryptoUtil::createSignedString(\pack( - 'CA20C', - 1, - Hex::decode($this->sessionID), + 'Ca40C', + 2, + $this->sessionID, $this->getCookieTimestep() )); } @@ -637,7 +651,7 @@ private function getExistingSession(string $sessionID): bool */ private function create(): void { - $this->sessionID = Hex::encode(\random_bytes(20)); + $this->sessionID = $this->generateSessionID(); $variables = [ 'frontend' => [], @@ -692,6 +706,25 @@ private function create(): void ); } + /** + * Generates a session ID that starts with its creation time, keeping inserts into + * the primary key of `wcf1_user_session` close to sequential. + * + * Format: 4 bytes of seconds and 1 byte of 1/256 second steps as Base16, followed by + * 17 random bytes (136 bits) as Base32 and the format version `V2`. + */ + private function generateSessionID(): string + { + // Both parts must come from the same clock reading, `TIME_NOW` is fixed at + // the start of the request and would break the ordering within a second. + $now = \microtime(true); + $seconds = (int)$now; + + return Hex::encode(\pack('NC', $seconds, (int)(($now - $seconds) * 256))) + . \substr(Base32::encode(\random_bytes(17)), 0, 28) + . 'V2'; + } + /** * Returns the existing legacy session that is shared by all guest sessions of the given spider. */ diff --git a/wcfsetup/setup/db/install_com.woltlab.wcf.php b/wcfsetup/setup/db/install_com.woltlab.wcf.php index 6ed39a027a8..e286eda0908 100644 --- a/wcfsetup/setup/db/install_com.woltlab.wcf.php +++ b/wcfsetup/setup/db/install_com.woltlab.wcf.php @@ -307,10 +307,9 @@ DatabaseTable::create('wcf1_acp_session_log') ->columns([ ObjectIdDatabaseTableColumn::create('sessionLogID'), - CharDatabaseTableColumn::create('sessionID') + BinaryDatabaseTableColumn::create('sessionID') ->notNull() - ->length(40) - ->defaultValue(''), + ->length(40), IntDatabaseTableColumn::create('userID'), VarcharDatabaseTableColumn::create('ipAddress') ->notNull() @@ -3216,7 +3215,7 @@ ]), DatabaseTable::create('wcf1_session') ->columns([ - CharDatabaseTableColumn::create('sessionID') + BinaryDatabaseTableColumn::create('sessionID') ->notNull() ->length(40), IntDatabaseTableColumn::create('userID'), @@ -4777,7 +4776,7 @@ ]), DatabaseTable::create('wcf1_user_session') ->columns([ - CharDatabaseTableColumn::create('sessionID') + BinaryDatabaseTableColumn::create('sessionID') ->notNull() ->length(40), IntDatabaseTableColumn::create('userID'),