secret_provider = $secret_provider; } /** * Create a signed token for a generic form context. * * @param string $form_id Generic form identifier. * @param int|null $issued_at Issued timestamp. */ public function create_token( string $form_id, ?int $issued_at = null ): string { $issued_at = null === $issued_at ? time() : max( 1, $issued_at ); $context_hash = $this->context_hash( $form_id ); $payload = self::VERSION . '|' . $issued_at . '|' . $context_hash; $signature = $this->sign( $payload ); return $this->base64url_encode( $payload ) . '.' . $signature; } /** * Validate a signed token. * * @param string $token Submitted token. * @param string $form_id Generic form identifier. * @param int $now Current timestamp. * @param int $lifetime_seconds Token lifetime. */ public function validate_token( string $token, string $form_id, int $now, int $lifetime_seconds ): bool { $parts = explode( '.', $token ); if ( 2 !== count( $parts ) ) { return false; } $payload = $this->base64url_decode( $parts[0] ); $signature = $parts[1]; if ( '' === $payload || ! hash_equals( $this->sign( $payload ), $signature ) ) { return false; } $payload_parts = explode( '|', $payload ); if ( 3 !== count( $payload_parts ) || self::VERSION !== $payload_parts[0] ) { return false; } $issued_at = (int) $payload_parts[1]; if ( $payload_parts[2] !== $this->context_hash( $form_id ) ) { return false; } if ( $issued_at <= 0 || $issued_at > ( $now + 60 ) ) { return false; } return ( $now - $issued_at ) <= max( 1, $lifetime_seconds ); } /** * Build a context hash. * * @param string $form_id Generic form identifier. */ private function context_hash( string $form_id ): string { return hash_hmac( 'sha256', strtolower( trim( $form_id ) ), $this->secret_provider->get_secret() ); } /** * Sign a payload. * * @param string $payload Payload. */ private function sign( string $payload ): string { return hash_hmac( 'sha256', $payload, $this->secret_provider->get_secret() ); } /** * Base64url encode a payload. * * @param string $value Raw value. */ private function base64url_encode( string $value ): string { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Token payload is encoded for URL-safe transport, not obfuscation. return rtrim( strtr( base64_encode( $value ), '+/', '-_' ), '=' ); } /** * Base64url decode a payload. * * @param string $value Encoded value. */ private function base64url_decode( string $value ): string { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Token payload is decoded after URL-safe transport, not obfuscated code. $decoded = base64_decode( strtr( $value, '-_', '+/' ), true ); if ( false === $decoded ) { return ''; } return $decoded; } }