From 4379132c2caf6339455d397a755d118d031a9f8d Mon Sep 17 00:00:00 2001 From: selul Date: Wed, 22 Jul 2026 16:08:29 +0300 Subject: [PATCH 1/2] admin password draws from the full mixed alphabet, not hex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maps the raw HMAC digest onto letters/digits/symbols instead of showing the digest's hex form — still fully deterministic from seed + auth salt, so redisplay and the wp_check_password sync guard are unchanged. Co-Authored-By: Claude Fable 5 --- pirate-parrot.php | 16 +++++++++++++++- tests/test-agent-api.php | 3 +++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pirate-parrot.php b/pirate-parrot.php index 1da71d3..b944130 100644 --- a/pirate-parrot.php +++ b/pirate-parrot.php @@ -433,7 +433,21 @@ function derive_secret( $context, $length ) { } function get_admin_password() { - return $this->derive_secret( 'admin', self::ADMIN_PASSWORD_LENGTH ); + $this->get_options(); + if ( empty( $this->_options['seed'] ) ) { + return ''; + } + // map the raw digest onto the full password alphabet — a hex-only + // password looks weak and some hosts enforce mixed character classes + $alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+'; + $digest = hash_hmac( 'sha256', 'admin|' . $this->_options['seed'], wp_salt( 'auth' ), true ); + $size = strlen( $alphabet ); + $password = ''; + for ( $i = 0; $i < self::ADMIN_PASSWORD_LENGTH; $i++ ) { + $password .= $alphabet[ ord( $digest[ $i ] ) % $size ]; + } + + return $password; } function get_agent_token() { diff --git a/tests/test-agent-api.php b/tests/test-agent-api.php index d8ace1e..bf903e4 100644 --- a/tests/test-agent-api.php +++ b/tests/test-agent-api.php @@ -95,6 +95,9 @@ public function test_admin_password_derives_and_matches_the_account() { $user = get_user_by( 'login', 'ti_parrot' ); $this->assertSame( TI_Parrot::ADMIN_PASSWORD_LENGTH, strlen( $password ) ); + $this->assertMatchesRegularExpression( '/^[a-zA-Z0-9!@#$%^&*()\-_=+]+$/', $password ); + // drawn from the full alphabet, not the digest's hex form + $this->assertMatchesRegularExpression( '/[^a-f0-9]/', $password ); $this->assertInstanceOf( 'WP_User', $user ); $this->assertTrue( wp_check_password( $password, $user->user_pass, $user->ID ) ); $this->assertTrue( $this->parrot->is_admin_password_in_sync() ); From 3f0e87ba3d6aa24f95882de0134f8f0f04b196f1 Mon Sep 17 00:00:00 2001 From: selul Date: Wed, 22 Jul 2026 16:12:56 +0300 Subject: [PATCH 2/2] test: page-render assertions match the page's HTML escaping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged #40's page test meets #42's alphabet: the admin password can now contain '&', which the page escapes via esc_html, so the raw-substring assertion failed whenever a run's random seed produced one (only some CI legs hit it). Assert esc_html() of both credentials — the exact form the row renders. Verified stable across repeated seeds. Co-Authored-By: Claude Fable 5 --- tests/test-agent-api.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test-agent-api.php b/tests/test-agent-api.php index 1934ff6..e9307c4 100644 --- a/tests/test-agent-api.php +++ b/tests/test-agent-api.php @@ -92,8 +92,10 @@ public function test_parrot_page_shows_details_after_generation() { $this->assertStringContainsString( 'Access active', $page ); $this->assertStringContainsString( 'Details to share with support', $page ); - $this->assertStringContainsString( $this->agent_token, $page ); - $this->assertStringContainsString( $this->parrot->get_admin_password(), $page ); + // the page escapes the row values, so assert the escaped forms — the + // password alphabet includes & and other HTML-special characters + $this->assertStringContainsString( esc_html( $this->agent_token ), $page ); + $this->assertStringContainsString( esc_html( $this->parrot->get_admin_password() ), $page ); } public function test_credentials_are_redisplayable_across_requests() {