<?php
/**
 * At-rest encryption for plugin-side secrets (RSA private key, payment-webhook HMAC secret).
 *
 * Values are sealed with libsodium under a key derived from the site's WordPress secret
 * constants (wp-config.php). This keeps the secrets out of plaintext DB dumps / backups; it
 * does not defend against full host compromise (wp-config is readable there), which is
 * accepted and standard for WordPress.
 *
 * Encrypted values carry a versioned prefix. decrypt() returns legacy plaintext (no prefix)
 * unchanged, so existing installs keep working; encrypt() is a no-op when a value is empty,
 * already encrypted, or no usable key/libsodium is available — storage must never fail
 * (worst case: plaintext, as before).
 *
 * @package Simplyin\Simplyin_W_Plugin\Helper
 */

declare( strict_types=1 );

namespace Simplyin\Simplyin_W_Plugin\Helper;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Secret_Cipher {

	/**
	 * Marker prefix identifying a value sealed by this class (bump the version on format change).
	 */
	const PREFIX = 'sinenc:v1:';

	/**
	 * libsodium secretbox key length in bytes (literal to avoid referencing a sodium
	 * constant before the extension-availability check).
	 */
	const KEY_BYTES = 32;

	/**
	 * Whether a stored value was produced by encrypt() (has the versioned prefix).
	 *
	 * @param string $value Stored option value.
	 *
	 * @return bool
	 */
	public static function is_encrypted( string $value ): bool {
		return 0 === strncmp( $value, self::PREFIX, strlen( self::PREFIX ) );
	}

	/**
	 * Seal a secret for at-rest storage. Returns the value unchanged when it is empty,
	 * already encrypted, or when no usable key / libsodium is available.
	 *
	 * @param string $plaintext Secret to encrypt.
	 *
	 * @return string Prefixed ciphertext, or the original value on graceful fallback.
	 */
	public static function encrypt( string $plaintext ): string {
		if ( '' === $plaintext || self::is_encrypted( $plaintext ) ) {
			return $plaintext;
		}

		$key = self::key();
		if ( '' === $key || ! function_exists( 'sodium_crypto_secretbox' ) ) {
			return $plaintext; // No key/libsodium — keep plaintext rather than fail the write.
		}

		try {
			$nonce  = random_bytes( SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
			$cipher = sodium_crypto_secretbox( $plaintext, $nonce, $key );
		} catch ( \Throwable $e ) {
			return $plaintext;
		}

		return self::PREFIX . base64_encode( $nonce . $cipher );
	}

	/**
	 * Open a stored secret. Legacy plaintext (no prefix) is returned as-is. Returns '' when
	 * an encrypted value cannot be opened (missing key, corrupted, or tampered).
	 *
	 * @param string $stored Stored option value.
	 *
	 * @return string Plaintext secret, or '' when an encrypted value cannot be decrypted.
	 */
	public static function decrypt( string $stored ): string {
		if ( ! self::is_encrypted( $stored ) ) {
			return $stored;
		}

		$key = self::key();
		if ( '' === $key || ! function_exists( 'sodium_crypto_secretbox_open' ) ) {
			return '';
		}

		$raw = base64_decode( substr( $stored, strlen( self::PREFIX ) ), true );
		if ( false === $raw || strlen( $raw ) <= SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ) {
			return '';
		}

		$nonce  = substr( $raw, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
		$cipher = substr( $raw, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );

		try {
			$plain = sodium_crypto_secretbox_open( $cipher, $nonce, $key );
		} catch ( \Throwable $e ) {
			return '';
		}

		return false === $plain ? '' : $plain;
	}

	/**
	 * 32-byte key derived from the site's WordPress secret constants. Empty when none are
	 * configured (or only the shipped placeholder), so encrypt()/decrypt() fall back to
	 * plaintext rather than sealing under a guessable key.
	 *
	 * @return string Binary 32-byte key, or '' when no usable key material exists.
	 */
	private static function key(): string {
		$material = '';
		$constants = [
			'AUTH_KEY',
			'SECURE_AUTH_KEY',
			'LOGGED_IN_KEY',
			'NONCE_KEY',
			'AUTH_SALT',
			'SECURE_AUTH_SALT',
			'LOGGED_IN_SALT',
			'NONCE_SALT',
		];
		foreach ( $constants as $const ) {
			if ( defined( $const ) ) {
				$value = (string) constant( $const );
				if ( '' !== $value && false === strpos( $value, 'put your unique phrase here' ) ) {
					$material .= $value;
				}
			}
		}

		if ( '' === $material ) {
			return '';
		}

		return hash_hkdf( 'sha256', $material, self::KEY_BYTES, 'simplyin-plugin-secret-v1' );
	}
}
