diff --git a/app/Http/Controllers/API/WpOrg/SecretKey/SecretKeyController.php b/app/Http/Controllers/API/WpOrg/SecretKey/SecretKeyController.php index 780d056..53c5ee5 100644 --- a/app/Http/Controllers/API/WpOrg/SecretKey/SecretKeyController.php +++ b/app/Http/Controllers/API/WpOrg/SecretKey/SecretKeyController.php @@ -3,9 +3,8 @@ namespace App\Http\Controllers\API\WpOrg\SecretKey; use App\Http\Controllers\Controller; -use Exception; -use Illuminate\Http\Request; use Illuminate\Http\Response; +use Random\RandomException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class SecretKeyController extends Controller @@ -13,11 +12,14 @@ class SecretKeyController extends Controller // From https://github.com/wp-cli/config-command/blob/main/src/Config_Command.php public const VALID_KEY_CHARACTERS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|'; - public function index(string $version, Request $request): Response + /** + * @throws RandomException + */ + public function index(string $version): Response { $response = match ($version) { - '1.0' => $this->generate_1_0(), - '1.1' => $this->generate_1_1(), + '1.0' => $this->generateKey_1_0(), + '1.1' => $this->generateKey_1_1(), default => throw new NotFoundHttpException('unsupported version'), }; @@ -26,31 +28,30 @@ public function index(string $version, Request $request): Response public function salt(): Response { - return response($this->generate_salt_1_1(), 200, ['Content-Type' => 'text/plain']); + return response($this->generateSalt_1_1(), 200, ['Content-Type' => 'text/plain']); } - private function generate_1_0(): string + /** + * @throws RandomException + */ + private function generateKey_1_0(): string { - $key = self::unique_key(); + $key = self::uniqueKey(); + return "define('SECRET_KEY', '$key');\n"; - // define('SECRET_KEY', '/zG$R}A5yD(&R2ob_,{g#N\\%d&MoV=NpFNFl%,e*0Zx~\"CMim^6hgQm=e20%n@er'); } - private function generate_1_1(): string + private function generateKey_1_1(): string { $out = ''; foreach (['AUTH_KEY', 'SECURE_AUTH_KEY', 'LOGGED_IN_KEY', 'NONCE_KEY'] as $name) { $param = "'$name',"; - $out .= sprintf("define(%-18s '%s');\n", $param, self::unique_key()); + $out .= sprintf("define(%-18s '%s');\n", $param, self::uniqueKey()); } return $out; - // define('AUTH_KEY', 'y*k]cp10`=Ut@0G9GwLD]nYX-#L_dkFyQ=7ts{,=R+@Z!Up|5PQIp9'); - // define('SECURE_AUTH_KEY', '{4dzc%{cAx{4L%V-w_eLBo,S- Y !LU!QpUZ=xvM?q-@H_.dSjEyK=}_O+ u[@I2S!lA?8v}7HrL/I.{p07U3<0Dzb|p`'); - // define('SECURE_AUTH_SALT', '{sAB~6ta%o8o3|`$!8I5$`fS7M3X9VTX1*ZoX&/9_b^QID+pbds^.HYEewz^xCwB'); - // define('LOGGED_IN_SALT', 'kj,?{%-qhT}#P Q?+oSjLN;^cZu=,V2ZjSHI;XgU-h@`H+?1?::muJ*4--&~!$.+'); - // define('NONCE_SALT', ':nUJz*%EU1R0 3x<8`=:>PC+^Vhk9DjyjI@tjuMVOj@dk(N_jn-(+AC/I7wOs`yT'); } - private static function unique_key(int $length = 64): string + /** + * @throws RandomException + */ + private static function uniqueKey(int $length = 64): string { - $chars = self::VALID_KEY_CHARACTERS; - $key = ''; - $len = strlen($chars) - 1; - - for ($i = 0; $i < $length; $i++) { - $key .= $chars[random_int(0, $len)]; - } - - return $key; + return implode(array_map( + static fn() => self::VALID_KEY_CHARACTERS[random_int(0, $length)], + array_fill(0, $length, null) + )); } - } diff --git a/tests/Feature/API/WpOrg/SecretKeyControllerTest.php b/tests/Feature/API/WpOrg/SecretKeyControllerTest.php new file mode 100644 index 0000000..c6faa5e --- /dev/null +++ b/tests/Feature/API/WpOrg/SecretKeyControllerTest.php @@ -0,0 +1,95 @@ +toHaveCount(2); + + // Extract the key value from the matches + $keyValue = $matches[1]; + + // Validate that the key contains only valid characters + expect(preg_match('/^[' . preg_quote($validKeys, '/') . ']{64}$/', $keyValue)) + ->toBe(1) + ->and(preg_match( + '/^define\(\'' . preg_quote( + $keyName, + '/' + ) . '\',\s+\'[^\']+\'\);$/', + $matches[0] + ))->toBe(1); + } +} + +it( + 'can generate a secret keys for version 1.0 and 1.1', + function (string $version, int $expectedKeys) use ($validKeys) { + $response = $this->getJson("/secret-key/$version"); + + expect($response->getStatusCode()) + ->toBe(200) + ->and($response->headers->get('Content-Type'))->toContain('text/plain'); + + $expectedKeyNames = match ($version) { + '1.0' => [ 'SECRET_KEY' ], + '1.1' => [ 'AUTH_KEY', 'SECURE_AUTH_KEY', 'LOGGED_IN_KEY', 'NONCE_KEY' ], + }; + + $content = $response->getContent(); + + // Validate the number of keys, the +1 is for the last line break + expect(explode("\n", $content))->toHaveCount($expectedKeys + 1); + + validateKeys($content, $expectedKeyNames, $validKeys); + } +)->with([ + '1.0 version' => [ '1.0', 1 ], + '1.1 version' => [ '1.1', 4 ], +]); + +it('can generate a secret keys with salt for version 1.1', function () use ($validKeys) { + $response = $this->getJson('/secret-key/1.1/salt'); + + expect($response->getStatusCode()) + ->toBe(200) + ->and($response->headers->get('Content-Type'))->toContain('text/plain'); + + $content = $response->getContent(); + + // Validate the number of keys, the +1 is for the last line break + expect(explode("\n", $content))->toHaveCount(8 + 1); + + $expectedKeyNames = [ + 'AUTH_KEY', + 'SECURE_AUTH_KEY', + 'LOGGED_IN_KEY', + 'NONCE_KEY', + 'AUTH_SALT', + 'SECURE_AUTH_SALT', + 'LOGGED_IN_SALT', + 'NONCE_SALT', + ]; + + validateKeys($content, $expectedKeyNames, $validKeys); +}); + +it('returns 404 for unsupported salt versions', function () { + $response = $this->getJson('/secret-key/1.0/salt'); + expect($response->getStatusCode())->toBe(404); +}); + +it('returns 404 for unsupported secret key versions', function () { + $response = $this->getJson('/secret-key/2.0'); + expect($response->getStatusCode())->toBe(404); +});