<?php
namespace EasyChat\Library\Security;

 
final class AeadEnvelopeCipher {
    private const VERSION = 1;
    private $key_id;
    private $key;

    public function __construct(string $key_id, string $key) {
        if (preg_match('/\A[A-Za-z0-9][A-Za-z0-9._-]{0,63}\z/D', $key_id) !== 1 || strlen($key) !== 32) {
            throw new \InvalidArgumentException('storage_key_invalid');
        }
        if (!function_exists('openssl_encrypt') || !in_array('aes-256-gcm', openssl_get_cipher_methods(), true)) {
            throw new \RuntimeException('storage_aead_unavailable');
        }
        $this->key_id = $key_id;
        $this->key = $key;
    }

    public function encrypt(string $context, string $plaintext): string {
        $this->assertContext($context);
        $nonce = random_bytes(12);
        $tag = '';
        $key = $this->derive($context);
        $aad = $this->aad($context);
        $ciphertext = openssl_encrypt($plaintext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $nonce, $tag, $aad, 16);
        if (!is_string($ciphertext) || strlen($tag) !== 16) throw new \RuntimeException('storage_encrypt_failed');
        $json = json_encode([
            'v' => self::VERSION,
            'kid' => $this->key_id,
            'nonce' => base64_encode($nonce),
            'tag' => base64_encode($tag),
            'ciphertext' => base64_encode($ciphertext),
        ], JSON_UNESCAPED_SLASHES);
        if (!is_string($json)) throw new \RuntimeException('storage_envelope_encode_failed');
        return $json;
    }

    public function decrypt(string $context, string $envelope): string {
        $this->assertContext($context);
        $data = json_decode($envelope, true);
        if (!is_array($data) || array_keys($data) !== ['v', 'kid', 'nonce', 'tag', 'ciphertext']
            || (int)$data['v'] !== self::VERSION || !is_string($data['kid']) || !hash_equals($this->key_id, $data['kid'])) {
            throw new \RuntimeException('storage_envelope_invalid');
        }
        $nonce = $this->decode($data['nonce'], 12, 12);
        $tag = $this->decode($data['tag'], 16, 16);
        $ciphertext = $this->decode($data['ciphertext'], 0, 16777216);
        $plaintext = openssl_decrypt($ciphertext, 'aes-256-gcm', $this->derive($context), OPENSSL_RAW_DATA, $nonce, $tag, $this->aad($context));
        if (!is_string($plaintext)) throw new \RuntimeException('storage_authentication_failed');
        return $plaintext;
    }

    private function derive(string $context): string {
        return hash_hkdf('sha256', $this->key, 32, 'EasyChat private envelope v1', hash('sha256', $context, true));
    }
    private function aad(string $context): string { return "EasyChat\0private-envelope\0v1\0" . $this->key_id . "\0" . $context; }
    private function assertContext(string $context): void {
        if ($context === '' || strlen($context) > 2048) throw new \InvalidArgumentException('storage_context_invalid');
    }
    private function decode($value, int $min, int $max): string {
        $decoded = is_string($value) ? base64_decode($value, true) : false;
        if (!is_string($decoded) || strlen($decoded) < $min || strlen($decoded) > $max || base64_encode($decoded) !== $value) {
            throw new \RuntimeException('storage_envelope_base64_invalid');
        }
        return $decoded;
    }
}
