From fbae3d9bf6b64870a91ddffeceaf5d6ce492c863 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 26 Aug 2026 11:09:34 -0400 Subject: [PATCH 1/8] fix(auth): size token keys only for stored passwords Validate the encrypted password length before generating a key, use the RSA-OAEP plaintext limit when selecting the key size, and avoid generating 4096-bit keys when encrypted password storage is disabled. Signed-off-by: Josh --- .../Token/PublicKeyTokenProvider.php | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php index 597bae331126d..7fbbcc60bb228 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenProvider.php +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -30,9 +30,16 @@ class PublicKeyTokenProvider implements IProvider { public const TOKEN_MIN_LENGTH = 22; + /** Token cache TTL in seconds */ private const int TOKEN_CACHE_TTL = 10; + /** + * Maximum plaintext size for a 2048-bit RSA key using OAEP with SHA-1: + * 256 - (2 * 20) - 2 = 214 bytes. + */ + private const int RSA_2048_OAEP_MAX_PLAINTEXT_LENGTH = 214; + use TTransactional; /** @var ICache */ @@ -439,12 +446,13 @@ private function hashTokenWithEmptySecret(string $token): string { } /** - * @throws \RuntimeException when OpenSSL reports a problem + * @throws \RuntimeException when the password cannot be stored or OpenSSL reports a problem */ - private function newToken(string $token, + private function newToken( + string $token, string $uid, string $loginName, - $password, + ?string $password, string $name, int $type, int $remember, @@ -454,9 +462,25 @@ private function newToken(string $token, $dbToken->setUid($uid); $dbToken->setLoginName($loginName); + $storeCryptedPassword = $password !== null + && $this->config->getSystemValueBool('auth.storeCryptedPassword', true); + $passwordLength = $storeCryptedPassword ? strlen($password) : 0; + + if ($storeCryptedPassword && $passwordLength > IUserManager::MAX_PASSWORD_LENGTH) { + throw new \RuntimeException(sprintf( + 'Storing an encrypted password longer than %d bytes in an authentication token is not supported.', + IUserManager::MAX_PASSWORD_LENGTH, + )); + } + + $requiredKeySize = $storeCryptedPassword + && $passwordLength > self::RSA_2048_OAEP_MAX_PLAINTEXT_LENGTH + ? 4096 + : 2048; + $config = array_merge([ 'digest_alg' => 'sha512', - 'private_key_bits' => $password !== null && strlen($password) > 250 ? 4096 : 2048, + 'private_key_bits' => $requiredKeySize, ], $this->config->getSystemValue('openssl', [])); // Generate new key @@ -478,10 +502,7 @@ private function newToken(string $token, $dbToken->setPublicKey($publicKey); $dbToken->setPrivateKey($this->encrypt($privateKey, $token)); - if (!is_null($password) && $this->config->getSystemValueBool('auth.storeCryptedPassword', true)) { - if (strlen($password) > IUserManager::MAX_PASSWORD_LENGTH) { - throw new \RuntimeException('Trying to save a password with more than 469 characters is not supported. If you want to use big passwords, disable the auth.storeCryptedPassword option in config.php'); - } + if ($storeCryptedPassword) { $dbToken->setPassword($this->encryptPassword($password, $publicKey)); $dbToken->setPasswordHash($this->hashPassword($password)); } From 322a5d4630ab97d6b3d1bbcce7fdc2f726c0a48f Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 26 Aug 2026 11:29:32 -0400 Subject: [PATCH 2/8] fix(auth): handle token password encryption failures Check the result of OpenSSL public-key encryption and fail explicitly instead of encoding and storing missing or invalid ciphertext. This also provides a controlled failure when a token created with a 2048-bit key cannot accommodate a subsequently changed, longer password. Signed-off-by: Josh --- .../Authentication/Token/PublicKeyTokenProvider.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php index 7fbbcc60bb228..1b9e66a773cd1 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenProvider.php +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -420,10 +420,13 @@ private function decrypt(string $cipherText, string $token): string { } private function encryptPassword(string $password, string $publicKey): string { - openssl_public_encrypt($password, $encryptedPassword, $publicKey, OPENSSL_PKCS1_OAEP_PADDING); - $encryptedPassword = base64_encode($encryptedPassword); + if (!openssl_public_encrypt($password, $encryptedPassword, $publicKey, OPENSSL_PKCS1_OAEP_PADDING)) { + // Never store missing or invalid ciphertext when password encryption fails. + $this->logOpensslError(); + throw new \RuntimeException('OpenSSL reported a problem'); + } - return $encryptedPassword; + return base64_encode($encryptedPassword); } private function decryptPassword(string $encryptedPassword, string $privateKey): string { From ea327045331c010c18930ac96d52fc79e891b8ed Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 26 Aug 2026 11:33:34 -0400 Subject: [PATCH 3/8] chore(auth): fixup spacing in newToken Signed-off-by: Josh --- lib/private/Authentication/Token/PublicKeyTokenProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php index 1b9e66a773cd1..15088619409c2 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenProvider.php +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -471,7 +471,7 @@ private function newToken( if ($storeCryptedPassword && $passwordLength > IUserManager::MAX_PASSWORD_LENGTH) { throw new \RuntimeException(sprintf( - 'Storing an encrypted password longer than %d bytes in an authentication token is not supported.', + 'Storing an encrypted password longer than %d bytes in an authentication token is not supported.', IUserManager::MAX_PASSWORD_LENGTH, )); } From 5110d02d23e4d046835fa61b743ae86944b6c72d Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 26 Aug 2026 11:44:25 -0400 Subject: [PATCH 4/8] test(auth): update the oversized-password test Replace and strengthen testGenerateTokenLongPassword(): - validate the new message - confirm that no token is inserted - rename test for clarity - use constant rather than hard-coded arbitrary 500 Signed-off-by: Josh --- .../Token/PublicKeyTokenProviderTest.php | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php b/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php index 1f6eaf8db599d..aabc491e4f188 100644 --- a/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php +++ b/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php @@ -22,6 +22,7 @@ use OCP\ICacheFactory; use OCP\IConfig; use OCP\IDBConnection; +use OCP\IUserManager; use OCP\Security\ICrypto; use OCP\Security\IHasher; use OCP\Server; @@ -128,23 +129,33 @@ public function testGenerateTokenNoPassword(): void { $this->tokenProvider->getPassword($actual, $token); } - public function testGenerateTokenLongPassword(): void { - $token = 'tokentokentokentokentoken'; - $uid = 'user'; - $user = 'User'; - $password = ''; - for ($i = 0; $i < 500; $i++) { - $password .= 'e'; - } + public function testGenerateTokenRejectsPasswordAboveStorageLimit(): void { + $password = str_repeat('e', IUserManager::MAX_PASSWORD_LENGTH + 1); $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; - $type = IToken::PERMANENT_TOKEN; + $this->config->method('getSystemValueBool') ->willReturnMap([ ['auth.storeCryptedPassword', true, true], ]); - $this->expectException(\RuntimeException::class); - $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + $this->mapper->expects($this->never()) + ->method('insert'); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage(sprintf( + 'Storing an encrypted password longer than %d bytes in an authentication token is not supported.', + IUserManager::MAX_PASSWORD_LENGTH, + )); + + $this->tokenProvider->generateToken( + 'tokentokentokentokentoken', + 'user', + 'User', + $password, + $name, + IToken::PERMANENT_TOKEN, + IToken::DO_NOT_REMEMBER, + ); } public function testGenerateTokenInvalidName(): void { From 192cf6c337d406052a2860b8d9f6ae1c2d51c10e Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 26 Aug 2026 11:49:51 -0400 Subject: [PATCH 5/8] test(auth): add key-size boundary tests Cover both sides of 214-byte password boundary. These tests specifically catch the old incorrect 250 threshold: the 215-byte case would fail under the old implementation. Signed-off-by: Josh --- .../Token/PublicKeyTokenProviderTest.php | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php b/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php index aabc491e4f188..af7e54f20d699 100644 --- a/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php +++ b/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php @@ -158,6 +158,68 @@ public function testGenerateTokenRejectsPasswordAboveStorageLimit(): void { ); } + private function getPublicKeyBits(PublicKeyToken $token): int { + $publicKey = openssl_pkey_get_public($token->getPublicKey()); + $this->assertNotFalse($publicKey); + + $details = openssl_pkey_get_details($publicKey); + $this->assertIsArray($details); + + return $details['bits']; + } + + public function testGenerateTokenUses2048BitKeyAtOaepLimit(): void { + // 214 = PublicKeyTokenProvider::RSA_2048_OAEP_MAX_PLAINTEXT_LENGTH + $password = str_repeat('a', 214); + + $this->config->method('getSystemValueBool') + ->willReturnMap([ + ['auth.storeCryptedPassword', true, true], + ]); + + $actual = $this->tokenProvider->generateToken( + 'tokentokentokentokentoken', + 'user', + 'User', + $password, + 'Test token', + IToken::PERMANENT_TOKEN, + IToken::DO_NOT_REMEMBER, + ); + + $this->assertSame(2048, $this->getPublicKeyBits($actual)); + $this->assertSame( + $password, + $this->tokenProvider->getPassword($actual, 'tokentokentokentokentoken'), + ); + } + + public function testGenerateTokenUses4096BitKeyAboveOaepLimit(): void { + // 215 = PublicKeyTokenProvider::RSA_2048_OAEP_MAX_PLAINTEXT_LENGTH + 1 + $password = str_repeat('a', 215); + + $this->config->method('getSystemValueBool') + ->willReturnMap([ + ['auth.storeCryptedPassword', true, true], + ]); + + $actual = $this->tokenProvider->generateToken( + 'tokentokentokentokentoken', + 'user', + 'User', + $password, + 'Test token', + IToken::PERMANENT_TOKEN, + IToken::DO_NOT_REMEMBER, + ); + + $this->assertSame(4096, $this->getPublicKeyBits($actual)); + $this->assertSame( + $password, + $this->tokenProvider->getPassword($actual, 'tokentokentokentokentoken'), + ); + } + public function testGenerateTokenInvalidName(): void { $token = 'tokentokentokentokentoken'; $uid = 'user'; From 643db3e58810f9f6a8418cf072a1802b6abf982e Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 26 Aug 2026 11:53:29 -0400 Subject: [PATCH 6/8] test(auth): long-passwords when storage is disabled This verifies both that the maximum is not applied and that a 4096-bit key is not generated unnecessarily. Signed-off-by: Josh --- .../Token/PublicKeyTokenProviderTest.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php b/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php index af7e54f20d699..30d46aa23cc9b 100644 --- a/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php +++ b/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php @@ -158,6 +158,29 @@ public function testGenerateTokenRejectsPasswordAboveStorageLimit(): void { ); } + public function testGenerateTokenDoesNotStoreLongPasswordWhenStorageIsDisabled(): void { + $password = str_repeat('a', IUserManager::MAX_PASSWORD_LENGTH + 1); + + $this->config->method('getSystemValueBool') + ->willReturnMap([ + ['auth.storeCryptedPassword', true, false], + ]); + + $actual = $this->tokenProvider->generateToken( + 'tokentokentokentokentoken', + 'user', + 'User', + $password, + 'Test token', + IToken::PERMANENT_TOKEN, + IToken::DO_NOT_REMEMBER, + ); + + $this->assertSame(2048, $this->getPublicKeyBits($actual)); + $this->assertNull($actual->getPassword()); + $this->assertNull($actual->getPasswordHash()); + } + private function getPublicKeyBits(PublicKeyToken $token): int { $publicKey = openssl_pkey_get_public($token->getPublicKey()); $this->assertNotFalse($publicKey); From dd42470af2fa797d147ff9853df693344edc4f68 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 26 Aug 2026 11:59:58 -0400 Subject: [PATCH 7/8] test(auth): short-to-long pw regression coverage Adds a short-to-long transition regression test for encryptPassword(). Verifies an existing 2048-bit existing token fails explicitly when password is storage is enabled and the password is changed to a >214-byte password. Assisted-by: Copilot:gpt-5.6-sol Signed-off-by: Josh --- .../Token/PublicKeyTokenProviderTest.php | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php b/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php index 30d46aa23cc9b..7ca90415957fa 100644 --- a/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php +++ b/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php @@ -385,6 +385,48 @@ public function testSetPassword(): void { $this->assertSame($newpass, $this->tokenProvider->getPassword($actual, 'tokentokentokentokentoken')); } + public function testSetPasswordFailsWhenExistingKeyIsTooSmall(): void { + $tokenId = 'tokentokentokentokentoken'; + + $this->config->method('getSystemValueBool') + ->willReturnMap([ + ['auth.storeCryptedPassword', true, true], + ]); + + $token = $this->tokenProvider->generateToken( + $tokenId, + 'user', + 'User', + 'short-password', + 'Test token', + IToken::PERMANENT_TOKEN, + IToken::DO_NOT_REMEMBER, + ); + + $this->assertSame(2048, $this->getPublicKeyBits($token)); + + $this->mapper->method('getTokenByUser') + ->with('user') + ->willReturn([$token]); + + $this->logger->expects($this->once()) + ->method('critical') + ->with($this->stringStartsWith('Something is wrong with your openssl setup:')); + + $this->mapper->expects($this->never()) + ->method('update'); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('OpenSSL reported a problem'); + + $this->tokenProvider->setPassword( + $token, + $tokenId, + // 215 = PublicKeyTokenProvider::RSA_2048_OAEP_MAX_PLAINTEXT_LENGTH + 1 + str_repeat('a', 215), + ); + } + public function testSetPasswordInvalidToken(): void { $this->expectException(InvalidTokenException::class); From 9003ba1e5cee0ae108abf1a9c47f9ff2b96d4950 Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 30 Aug 2026 08:28:14 -0400 Subject: [PATCH 8/8] chore: fixup spacing in PublicKeyTokenProviderTest Signed-off-by: Josh --- tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php b/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php index 7ca90415957fa..167c42521cbe8 100644 --- a/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php +++ b/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php @@ -242,7 +242,7 @@ public function testGenerateTokenUses4096BitKeyAboveOaepLimit(): void { $this->tokenProvider->getPassword($actual, 'tokentokentokentokentoken'), ); } - + public function testGenerateTokenInvalidName(): void { $token = 'tokentokentokentokentoken'; $uid = 'user';