import { concatBytes } from '@noble/ciphers/utils';
import { Encoder } from './encoder.js';
import { KeyDerivationScheme } from './hd-key.js';
import { AesGcm, AesKw, ConcatKdf, X25519, XChaCha20Poly1305 } from '@enbox/crypto';
/**
 * Content encryption algorithms supported by the DWN.
 * Both are AEAD (Authenticated Encryption with Associated Data) ciphers.
 */
export var ContentEncryptionAlgorithm;
(function (ContentEncryptionAlgorithm) {
    /** AES-256 in Galois/Counter Mode. NIST-approved, hardware-accelerated. 96-bit nonce. */
    ContentEncryptionAlgorithm["A256GCM"] = "A256GCM";
    /** XChaCha20-Poly1305. 192-bit nonce (safe to randomize). Constant-time. */
    ContentEncryptionAlgorithm["XC20P"] = "XC20P";
})(ContentEncryptionAlgorithm || (ContentEncryptionAlgorithm = {}));
/**
 * Key agreement algorithm used by the DWN.
 * ECDH-ES with X25519 key agreement and AES-256 Key Wrap.
 */
export var KeyAgreementAlgorithm;
(function (KeyAgreementAlgorithm) {
    KeyAgreementAlgorithm["EcdhEsA256kw"] = "ECDH-ES+A256KW";
})(KeyAgreementAlgorithm || (KeyAgreementAlgorithm = {}));
/** Size of the AES-GCM authentication tag in bytes. */
const AES_GCM_TAG_LENGTH_BYTES = 16;
/** Size of the Poly1305 authentication tag in bytes. */
const POLY1305_TAG_LENGTH_BYTES = 16;
/**
 * Utility class for DWN encryption operations using JWE (RFC 7516).
 * Uses ECDH-ES+A256KW key agreement with X25519 and either AES-256-GCM or XChaCha20-Poly1305
 * for authenticated content encryption.
 */
export class Encryption {
    /**
     * Encrypts data using an AEAD cipher (A256GCM or XC20P).
     * Returns ciphertext with the authentication tag appended.
     */
    static async aeadEncrypt(algorithm, keyBytes, iv, plaintext) {
        if (algorithm === ContentEncryptionAlgorithm.A256GCM) {
            const keyJwk = { kty: 'oct', k: Encoder.bytesToBase64Url(keyBytes), alg: 'A256GCM' };
            // Web Crypto AES-GCM returns ciphertext || tag
            const combined = await AesGcm.encrypt({ data: plaintext, iv, key: keyJwk });
            const ciphertext = combined.slice(0, combined.length - AES_GCM_TAG_LENGTH_BYTES);
            const tag = combined.slice(combined.length - AES_GCM_TAG_LENGTH_BYTES);
            return { ciphertext, tag };
        }
        else if (algorithm === ContentEncryptionAlgorithm.XC20P) {
            // @noble/ciphers XChaCha20-Poly1305 returns ciphertext || tag
            const combined = await XChaCha20Poly1305.encryptRaw({ data: plaintext, keyBytes, nonce: iv });
            const ciphertext = combined.slice(0, combined.length - POLY1305_TAG_LENGTH_BYTES);
            const tag = combined.slice(combined.length - POLY1305_TAG_LENGTH_BYTES);
            return { ciphertext, tag };
        }
        else {
            throw new Error(`Unsupported content encryption algorithm: ${algorithm}`);
        }
    }
    /**
     * Decrypts data using an AEAD cipher (A256GCM or XC20P).
     * Expects ciphertext and tag as separate inputs.
     */
    static async aeadDecrypt(algorithm, keyBytes, iv, ciphertext, tag) {
        // Both Web Crypto (AES-GCM) and @noble/ciphers (XChaCha20-Poly1305) expect ciphertext || tag
        const combined = concatBytes(ciphertext, tag);
        if (algorithm === ContentEncryptionAlgorithm.A256GCM) {
            const keyJwk = { kty: 'oct', k: Encoder.bytesToBase64Url(keyBytes), alg: 'A256GCM' };
            return AesGcm.decrypt({ data: combined, iv, key: keyJwk });
        }
        else if (algorithm === ContentEncryptionAlgorithm.XC20P) {
            return XChaCha20Poly1305.decryptRaw({ data: combined, keyBytes, nonce: iv });
        }
        else {
            throw new Error(`Unsupported content encryption algorithm: ${algorithm}`);
        }
    }
    /**
     * Encrypts data as a ReadableStream using an AEAD cipher.
     * Collects all chunks, encrypts, and returns a new stream of ciphertext || tag.
     * The iv and tag are NOT embedded in the stream — they are stored in the JWE structure.
     */
    static async aeadEncryptStream(algorithm, keyBytes, iv, plaintextStream) {
        const plaintext = await Encryption.readStream(plaintextStream);
        const { ciphertext, tag } = await Encryption.aeadEncrypt(algorithm, keyBytes, iv, plaintext);
        const ciphertextStream = new ReadableStream({
            start(controller) {
                controller.enqueue(ciphertext);
                controller.close();
            }
        });
        return { ciphertextStream, tag };
    }
    /**
     * Decrypts a ciphertext stream using an AEAD cipher.
     * Returns a ReadableStream of plaintext.
     */
    static async aeadDecryptStream(algorithm, keyBytes, iv, ciphertextStream, tag) {
        const ciphertext = await Encryption.readStream(ciphertextStream);
        const plaintext = await Encryption.aeadDecrypt(algorithm, keyBytes, iv, ciphertext, tag);
        return new ReadableStream({
            start(controller) {
                controller.enqueue(plaintext);
                controller.close();
            }
        });
    }
    /**
     * Performs ECDH-ES key agreement with X25519 and wraps the CEK using AES-256 Key Wrap.
     *
     * @param ephemeralPrivateKey - Ephemeral X25519 private key (JWK).
     * @param recipientPublicKey - Recipient's X25519 public key (JWK).
     * @param cek - The Content Encryption Key to wrap.
     * @returns The wrapped CEK bytes.
     */
    static async ecdhEsWrapKey(ephemeralPrivateKey, recipientPublicKey, cek) {
        // 1. ECDH shared secret
        const sharedSecret = await X25519.sharedSecret({
            privateKeyA: ephemeralPrivateKey,
            publicKeyB: recipientPublicKey,
        });
        // 2. Derive KEK via Concat KDF (RFC 7518 Section 4.6.2)
        const kek = await ConcatKdf.deriveKey({
            sharedSecret,
            keyDataLen: 256,
            fixedInfo: {
                algorithmId: 'A256KW',
                partyUInfo: '',
                partyVInfo: '',
                suppPubInfo: 256,
            },
        });
        // 3. AES-256 Key Wrap
        const cekJwk = { kty: 'oct', k: Encoder.bytesToBase64Url(cek), alg: 'A256GCM' };
        const kekJwk = { kty: 'oct', k: Encoder.bytesToBase64Url(kek), alg: 'A256KW' };
        const wrappedKey = await AesKw.wrapKey({ unwrappedKey: cekJwk, encryptionKey: kekJwk });
        return wrappedKey;
    }
    /**
     * Performs ECDH-ES key agreement with X25519 and unwraps the CEK using AES-256 Key Unwrap.
     *
     * @param recipientPrivateKey - Recipient's X25519 private key (JWK).
     * @param ephemeralPublicKey - Ephemeral X25519 public key from the JWE recipient header (JWK).
     * @param wrappedKey - The wrapped CEK bytes.
     * @returns The unwrapped CEK bytes.
     */
    static async ecdhEsUnwrapKey(recipientPrivateKey, ephemeralPublicKey, wrappedKey) {
        // 1. ECDH shared secret
        const sharedSecret = await X25519.sharedSecret({
            privateKeyA: recipientPrivateKey,
            publicKeyB: ephemeralPublicKey,
        });
        // 2. Derive KEK via Concat KDF
        const kek = await ConcatKdf.deriveKey({
            sharedSecret,
            keyDataLen: 256,
            fixedInfo: {
                algorithmId: 'A256KW',
                partyUInfo: '',
                partyVInfo: '',
                suppPubInfo: 256,
            },
        });
        // 3. AES-256 Key Unwrap
        const kekJwk = { kty: 'oct', k: Encoder.bytesToBase64Url(kek), alg: 'A256KW' };
        const unwrappedJwk = await AesKw.unwrapKey({
            wrappedKeyBytes: wrappedKey,
            wrappedKeyAlgorithm: 'A256GCM',
            decryptionKey: kekJwk,
        });
        return Encoder.base64UrlToBytes(unwrappedJwk.k);
    }
    /**
     * Builds a JWE encryption property structure from encryption input.
     * The ciphertext (encrypted record data) is stored separately in the DataStore,
     * so only the key wrapping metadata, IV, and authentication tag are included here.
     *
     * @param encryptionInput - Describes the CEK, IV, and recipient key encryption inputs.
     * @param tag - The authentication tag produced by the AEAD cipher during data encryption.
     */
    static async buildJwe(encryptionInput, tag) {
        const enc = encryptionInput.algorithm ?? ContentEncryptionAlgorithm.A256GCM;
        const protectedHeader = {
            alg: KeyAgreementAlgorithm.EcdhEsA256kw,
            enc,
        };
        const protectedHeaderBase64url = Encoder.stringToBase64Url(JSON.stringify(protectedHeader));
        const recipients = [];
        for (const keyInput of encryptionInput.keyEncryptionInputs) {
            // Generate ephemeral X25519 key pair for each recipient
            const ephemeralPrivateKey = await X25519.generateKey();
            const ephemeralPublicKey = await X25519.getPublicKey({ key: ephemeralPrivateKey });
            // Wrap the CEK
            const wrappedKey = await Encryption.ecdhEsWrapKey(ephemeralPrivateKey, keyInput.publicKey, encryptionInput.key);
            const recipientHeader = {
                kid: keyInput.publicKeyId,
                epk: ephemeralPublicKey,
                derivationScheme: keyInput.derivationScheme,
            };
            // Attach derived public key for protocolContext scheme
            if (keyInput.derivationScheme === KeyDerivationScheme.ProtocolContext) {
                recipientHeader.derivedPublicKey = keyInput.publicKey;
            }
            recipients.push({
                header: recipientHeader,
                encrypted_key: Encoder.bytesToBase64Url(wrappedKey),
            });
        }
        return {
            protected: protectedHeaderBase64url,
            iv: Encoder.bytesToBase64Url(encryptionInput.initializationVector),
            tag: Encoder.bytesToBase64Url(tag),
            recipients,
        };
    }
    /**
     * Parses the JWE protected header from its base64url encoding.
     */
    static parseProtectedHeader(protectedBase64url) {
        return Encoder.base64UrlToObject(protectedBase64url);
    }
    /**
     * Reads a ReadableStream to completion and returns all bytes concatenated.
     */
    static async readStream(stream) {
        const reader = stream.getReader();
        const chunks = [];
        for (;;) {
            const { done, value } = await reader.read();
            if (done) {
                break;
            }
            chunks.push(value);
        }
        return concatBytes(...chunks);
    }
}
//# sourceMappingURL=encryption.js.map