"""PhotoSafe crypto core — the SAME scheme the browser implements in static/js/crypto.js.

This module is deliberately small and fully documented so that the format is
reproducible by anyone, with no PhotoSafe software at all.  It is the reference
implementation used by photosafe_cli.py (export / restore) and by the test
harness.  The server NEVER imports the key-bearing functions: it only ever
stores opaque blobs.

FORMAT (v1)
-----------
kek          = PBKDF2-HMAC-SHA256(passphrase, salt || b"photosafe-kek-v1", 600000, 32)
auth_hash    = PBKDF2-HMAC-SHA256(passphrase, salt || b"photosafe-auth-v1", 600000, 32)
master_key   = 32 random bytes, generated once at signup
wrapped_mk   = nonce(12) || AES-256-GCM(kek, master_key)

per object:
  file_key   = 32 random bytes
  ciphertext = nonce(12) || AES-256-GCM(file_key, plaintext)
  wrapped_fk = nonce(12) || AES-256-GCM(master_key, file_key)
  meta_blob  = nonce(12) || AES-256-GCM(master_key, utf8(json(metadata)))

`salt` is 16 random bytes, stored server-side in the clear (it is not secret).
The passphrase, the kek and the master key never leave the client.
"""
import hashlib
import json
import os

from cryptography.hazmat.primitives.ciphers.aead import AESGCM

KDF_ITERS = 600_000
KEK_INFO = b"photosafe-kek-v1"
AUTH_INFO = b"photosafe-auth-v1"


def new_salt() -> bytes:
    return os.urandom(16)


def derive_kek(passphrase: str, salt: bytes) -> bytes:
    return hashlib.pbkdf2_hmac("sha256", passphrase.encode(), salt + KEK_INFO, KDF_ITERS, 32)


def derive_auth_hash(passphrase: str, salt: bytes) -> bytes:
    return hashlib.pbkdf2_hmac("sha256", passphrase.encode(), salt + AUTH_INFO, KDF_ITERS, 32)


def seal(key: bytes, plaintext: bytes, aad: bytes = b"") -> bytes:
    nonce = os.urandom(12)
    return nonce + AESGCM(key).encrypt(nonce, plaintext, aad)


def open_(key: bytes, blob: bytes, aad: bytes = b"") -> bytes:
    return AESGCM(key).decrypt(blob[:12], blob[12:], aad)


def new_master_key() -> bytes:
    return os.urandom(32)


def wrap_master_key(kek: bytes, master_key: bytes) -> bytes:
    return seal(kek, master_key)


def unwrap_master_key(kek: bytes, wrapped: bytes) -> bytes:
    return open_(kek, wrapped)


def encrypt_object(master_key: bytes, plaintext: bytes, metadata: dict):
    """Returns (ciphertext, wrapped_file_key, meta_blob, plain_sha256_hex)."""
    file_key = os.urandom(32)
    ciphertext = seal(file_key, plaintext)
    wrapped_fk = seal(master_key, file_key)
    meta_blob = seal(master_key, json.dumps(metadata, sort_keys=True).encode())
    return ciphertext, wrapped_fk, meta_blob, hashlib.sha256(plaintext).hexdigest()


def decrypt_object(master_key: bytes, ciphertext: bytes, wrapped_fk: bytes):
    file_key = open_(master_key, wrapped_fk)
    return open_(file_key, ciphertext)


def decrypt_meta(master_key: bytes, meta_blob: bytes) -> dict:
    return json.loads(open_(master_key, meta_blob).decode())
