<?php
/**
 * Encrypt secrets and hash one-time codes for 2FA storage.
 *
 * @package TwoFactorAuthentication
 * @since 1.7.0
 */

namespace TwoFactorAuthentication;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

final class Crypto {

	public const ENC_PREFIX = 'v1:';

	/**
	 * Encrypt a TOTP secret for user-meta storage (legacy plaintext values pass through on read).
	 */
	public static function encryptSecret( string $plaintext ): string {
		if ( $plaintext === '' ) {
			return '';
		}
		if ( str_starts_with( $plaintext, self::ENC_PREFIX ) ) {
			return $plaintext;
		}

		$key = self::secretKey();
		if ( function_exists( 'sodium_crypto_secretbox' ) ) {
			$nonce  = random_bytes( SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
			$cipher = sodium_crypto_secretbox( $plaintext, $nonce, $key );
			return self::ENC_PREFIX . base64_encode( $nonce . $cipher ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
		}

		$iv        = random_bytes( 16 );
		$encrypted = openssl_encrypt( $plaintext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv );
		if ( $encrypted === false ) {
			return $plaintext;
		}

		return self::ENC_PREFIX . base64_encode( $iv . $encrypted ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
	}

	/**
	 * Decrypt a stored TOTP secret; returns legacy plaintext unchanged.
	 */
	public static function decryptSecret( string $stored ): string {
		if ( $stored === '' || ! str_starts_with( $stored, self::ENC_PREFIX ) ) {
			return $stored;
		}

		$raw = base64_decode( substr( $stored, strlen( self::ENC_PREFIX ) ), true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
		if ( $raw === false || $raw === '' ) {
			return '';
		}

		$key = self::secretKey();
		if ( function_exists( 'sodium_crypto_secretbox_open' ) ) {
			$nonce_len = SODIUM_CRYPTO_SECRETBOX_NONCEBYTES;
			if ( strlen( $raw ) <= $nonce_len ) {
				return '';
			}
			$nonce  = substr( $raw, 0, $nonce_len );
			$cipher = substr( $raw, $nonce_len );
			$plain  = sodium_crypto_secretbox_open( $cipher, $nonce, $key );
			return $plain === false ? '' : (string) $plain;
		}

		if ( strlen( $raw ) <= 16 ) {
			return '';
		}
		$iv        = substr( $raw, 0, 16 );
		$encrypted = substr( $raw, 16 );
		$plain     = openssl_decrypt( $encrypted, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv );

		return $plain === false ? '' : (string) $plain;
	}

	/**
	 * Hash an email OTP before storing in a transient.
	 */
	public static function hashEmailCode( string $code ): string {
		return hash_hmac( 'sha256', $code, wp_salt( 'wpps_2fa_otp' ) );
	}

	/**
	 * Compare an email OTP to its stored hash (supports legacy plaintext in transients).
	 *
	 * @param array<string, mixed> $session_data Pending session payload.
	 */
	public static function verifyEmailCode( string $code, array $session_data ): bool {
		if ( strlen( $code ) !== 8 || ! ctype_digit( $code ) ) {
			return false;
		}
		if ( ! empty( $session_data['code_hash'] ) && is_string( $session_data['code_hash'] ) ) {
			return hash_equals( (string) $session_data['code_hash'], self::hashEmailCode( $code ) );
		}
		if ( isset( $session_data['code'] ) ) {
			return hash_equals( (string) $session_data['code'], $code );
		}

		return false;
	}

	/**
	 * @return string 32-byte key
	 */
	private static function secretKey(): string {
		return hash( 'sha256', wp_salt( 'wpps_2fa_store' ), true );
	}
}
