storeId = $storeId; $this->secret = $secret; $this->boundUrl = $boundUrl; } /** * The stored credentials, or null when the site is not connected or the key * cannot be decrypted (the salts changed). */ public static function load(): ?self { $stored = get_option( self::OPTION ); if ( ! is_array( $stored ) || ! is_string( $stored['store_id'] ?? null ) || ! is_string( $stored['secret'] ?? null ) ) { return null; } $secret = self::decrypt( $stored['secret'] ); if ( null === $secret ) { return null; } return new self( $stored['store_id'], $secret, (string) ( $stored['bound_url'] ?? '' ) ); } /** * Persist new credentials, bound to the current site address. * * @param string $storeId Store id. * @param string $secret Signing key. */ public static function save( string $storeId, string $secret ): self { $credentials = new self( $storeId, $secret, home_url() ); update_option( self::OPTION, [ 'store_id' => $storeId, 'secret' => self::encrypt( $secret ), 'bound_url' => $credentials->boundUrl, 'connected_at' => time(), ], false ); return $credentials; } /** * Forget the credentials. */ public static function forget(): void { delete_option( self::OPTION ); } /** * Is this still the site that paired? */ public function matchesSite(): bool { return self::host( $this->boundUrl ) === self::host( home_url() ); } /** * Host without `www.`, lower case. * * @param string $url URL. */ private static function host( string $url ): string { $host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) ); return str_starts_with( $host, 'www.' ) ? substr( $host, 4 ) : $host; } /** * Encrypt with libsodium's secretbox. * * @param string $plain Plain text. */ private static function encrypt( string $plain ): string { $nonce = random_bytes( SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ); return base64_encode( $nonce . sodium_crypto_secretbox( $plain, $nonce, self::key() ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Binary ciphertext stored in an option, not obfuscation. } /** * Decrypt, or null if the value was not produced with this site's key. * * @param string $stored Stored value. */ private static function decrypt( string $stored ): ?string { $raw = base64_decode( $stored, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- See encrypt(). if ( false === $raw || strlen( $raw ) <= SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ) { return null; } $plain = sodium_crypto_secretbox_open( substr( $raw, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ), substr( $raw, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ), self::key() ); return false === $plain ? null : $plain; } /** * 32-byte key derived from the site's auth salt. */ private static function key(): string { return hash( 'sha256', wp_salt( 'auth' ) . '|rahkar-hub', true ); } }