import { concatBytes } from '@noble/ciphers/utils';
import { AesCtr, AesKw, computeJwkThumbprint, Hkdf, X25519 } from '@enbox/crypto';
import { Encoder } from './encoder.js';
import { KeyDerivationScheme } from './hd-key.js';
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
export var ContentEncryptionAlgorithm;
(function (ContentEncryptionAlgorithm) {
    ContentEncryptionAlgorithm["A256CTR"] = "A256CTR";
})(ContentEncryptionAlgorithm || (ContentEncryptionAlgorithm = {}));
export var KeyAgreementAlgorithm;
(function (KeyAgreementAlgorithm) {
    KeyAgreementAlgorithm["X25519HkdfSha256A256Kw"] = "X25519-HKDF-SHA256+A256KW";
})(KeyAgreementAlgorithm || (KeyAgreementAlgorithm = {}));
export const ROLE_AUDIENCE_DERIVATION_SCHEME = 'roleAudience';
const AES_256_KEY_LENGTH_BYTES = 32;
const AES_CTR_COUNTER_LENGTH_BYTES = 16;
const AES_CTR_COUNTER_LENGTH_BITS = 128;
const KEK_INFO_PREFIX = KeyAgreementAlgorithm.X25519HkdfSha256A256Kw;
export class Encryption {
    static async encrypt(algorithm, keyBytes, initializationVector, plaintext) {
        Encryption.validateContentEncryptionParameters(algorithm, keyBytes, initializationVector);
        return AesCtr.encrypt({
            counter: initializationVector,
            data: plaintext,
            key: Encryption.toA256CtrJwk(keyBytes),
            length: AES_CTR_COUNTER_LENGTH_BITS,
        });
    }
    static async decrypt(algorithm, keyBytes, initializationVector, ciphertext) {
        Encryption.validateContentEncryptionParameters(algorithm, keyBytes, initializationVector);
        return AesCtr.decrypt({
            counter: initializationVector,
            data: ciphertext,
            key: Encryption.toA256CtrJwk(keyBytes),
            length: AES_CTR_COUNTER_LENGTH_BITS,
        });
    }
    static async encryptStream(algorithm, keyBytes, initializationVector, plaintextStream) {
        const plaintext = await Encryption.readStream(plaintextStream);
        const ciphertext = await Encryption.encrypt(algorithm, keyBytes, initializationVector, plaintext);
        return Encryption.toStream(ciphertext);
    }
    static async decryptStream(algorithm, keyBytes, initializationVector, ciphertextStream) {
        const ciphertext = await Encryption.readStream(ciphertextStream);
        const plaintext = await Encryption.decrypt(algorithm, keyBytes, initializationVector, ciphertext);
        return Encryption.toStream(plaintext);
    }
    static async wrapKey(recipientPublicKey, cek, keyInput, ephemeralPrivateKey) {
        return Encryption.wrapX25519Key(recipientPublicKey, cek, keyInput, ephemeralPrivateKey);
    }
    static async wrapX25519Key(recipientPublicKey, cek, keyInput, ephemeralPrivateKey) {
        Encryption.validateContentEncryptionKey(cek);
        const privateKey = ephemeralPrivateKey ?? await X25519.generateKey();
        const publicKey = await X25519.getPublicKey({ key: privateKey });
        const sharedSecret = await X25519.sharedSecret({
            privateKeyA: privateKey,
            publicKeyB: recipientPublicKey,
        });
        const kek = await Encryption.deriveKek(sharedSecret, keyInput);
        const cekJwk = Encryption.toA256CtrJwk(cek);
        const kekJwk = { alg: 'A256KW', k: Encoder.bytesToBase64Url(kek), kty: 'oct' };
        const encryptedKey = await AesKw.wrapKey({ encryptionKey: kekJwk, unwrappedKey: cekJwk });
        return { encryptedKey, ephemeralPublicKey: publicKey };
    }
    static async unwrapKey(recipientPrivateKey, keyEncryption) {
        if (keyEncryption.algorithm === KeyAgreementAlgorithm.X25519HkdfSha256A256Kw) {
            return Encryption.unwrapX25519Key(recipientPrivateKey, keyEncryption);
        }
        throw new Error(`Unsupported key agreement algorithm: ${keyEncryption.algorithm}`);
    }
    static async unwrapX25519Key(recipientPrivateKey, keyEncryption) {
        const sharedSecret = await X25519.sharedSecret({
            privateKeyA: recipientPrivateKey,
            publicKeyB: keyEncryption.ephemeralPublicKey,
        });
        const kek = await Encryption.deriveKek(sharedSecret, keyEncryption);
        const kekJwk = { alg: 'A256KW', k: Encoder.bytesToBase64Url(kek), kty: 'oct' };
        const unwrappedJwk = await AesKw.unwrapKey({
            decryptionKey: kekJwk,
            wrappedKeyAlgorithm: ContentEncryptionAlgorithm.A256CTR,
            wrappedKeyBytes: Encoder.base64UrlToBytes(keyEncryption.encryptedKey),
        });
        return Encoder.base64UrlToBytes(unwrappedJwk.k);
    }
    static async buildEncryptionProperty(encryptionInput) {
        const algorithm = encryptionInput.algorithm ?? ContentEncryptionAlgorithm.A256CTR;
        Encryption.validateContentEncryptionParameters(algorithm, encryptionInput.key, encryptionInput.initializationVector);
        const keyEncryption = [];
        for (const keyInput of encryptionInput.keyEncryptionInputs) {
            keyEncryption.push(await Encryption.buildKeyEncryptionEntry(encryptionInput.key, keyInput));
        }
        return {
            algorithm,
            initializationVector: Encoder.bytesToBase64Url(encryptionInput.initializationVector),
            keyEncryption,
        };
    }
    static async getKeyId(publicKeyJwk) {
        return computeJwkThumbprint({ jwk: publicKeyJwk });
    }
    static validateEncryptionProperty(encryption) {
        const initializationVector = Encoder.base64UrlToBytes(encryption.initializationVector);
        if (initializationVector.byteLength !== AES_CTR_COUNTER_LENGTH_BYTES) {
            throw new DwnError(DwnErrorCode.RecordsWriteValidateIntegrityEncryptionInitializationVectorInvalid, `A256CTR initializationVector must decode to ${AES_CTR_COUNTER_LENGTH_BYTES} bytes.`);
        }
        for (const entry of encryption.keyEncryption) {
            Encryption.validateKeyEncryptionEntry(entry);
        }
    }
    static async buildKeyEncryptionEntry(cek, keyInput) {
        const { encryptedKey, ephemeralPublicKey } = await Encryption.wrapKey(keyInput.publicKey, cek, keyInput);
        const common = {
            algorithm: KeyAgreementAlgorithm.X25519HkdfSha256A256Kw,
            encryptedKey: Encoder.bytesToBase64Url(encryptedKey),
            ephemeralPublicKey,
            keyId: keyInput.keyId,
        };
        if (keyInput.derivationScheme === KeyDerivationScheme.ProtocolPath) {
            return {
                ...common,
                derivationScheme: KeyDerivationScheme.ProtocolPath,
            };
        }
        return {
            ...common,
            derivationScheme: ROLE_AUDIENCE_DERIVATION_SCHEME,
            epoch: keyInput.epoch,
            protocol: keyInput.protocol,
            role: keyInput.role,
        };
    }
    static validateKeyEncryptionEntry(entry) {
        if (entry.algorithm === KeyAgreementAlgorithm.X25519HkdfSha256A256Kw) {
            Encryption.validateX25519KeyEncryptionEntry(entry);
            return;
        }
        throw new DwnError(DwnErrorCode.RecordsWriteValidateIntegrityEncryptionEphemeralPublicKeyInvalid, `Unsupported key agreement algorithm: ${entry.algorithm}`);
    }
    static validateX25519KeyEncryptionEntry(entry) {
        if (entry.ephemeralPublicKey.kty !== 'OKP' || entry.ephemeralPublicKey.crv !== 'X25519') {
            throw new DwnError(DwnErrorCode.RecordsWriteValidateIntegrityEncryptionEphemeralPublicKeyInvalid, 'ephemeralPublicKey must be an OKP X25519 public key.');
        }
    }
    static async deriveKek(sharedSecret, keyEncryption) {
        Encryption.validateSharedSecret(sharedSecret);
        const info = Encryption.getKekInfo(keyEncryption);
        return Hkdf.deriveKeyBytes({
            baseKeyBytes: sharedSecret,
            hash: 'SHA-256',
            info,
            length: 256,
            salt: new Uint8Array(),
        });
    }
    static getKekInfo(keyEncryption) {
        if (keyEncryption.derivationScheme === ROLE_AUDIENCE_DERIVATION_SCHEME) {
            return `${KEK_INFO_PREFIX}|roleAudience|${keyEncryption.protocol}|${keyEncryption.role}|${keyEncryption.epoch}|${keyEncryption.keyId}`;
        }
        return `${KEK_INFO_PREFIX}|protocolPath|${keyEncryption.keyId}`;
    }
    static validateContentEncryptionParameters(algorithm, keyBytes, initializationVector) {
        if (algorithm !== ContentEncryptionAlgorithm.A256CTR) {
            throw new Error(`Unsupported content encryption algorithm: ${algorithm}`);
        }
        Encryption.validateContentEncryptionKey(keyBytes);
        if (initializationVector.byteLength !== AES_CTR_COUNTER_LENGTH_BYTES) {
            throw new Error(`A256CTR initializationVector must be ${AES_CTR_COUNTER_LENGTH_BYTES} bytes.`);
        }
    }
    static validateContentEncryptionKey(keyBytes) {
        if (keyBytes.byteLength !== AES_256_KEY_LENGTH_BYTES) {
            throw new Error(`A256CTR content encryption key must be ${AES_256_KEY_LENGTH_BYTES} bytes.`);
        }
    }
    static validateSharedSecret(sharedSecret) {
        if (sharedSecret.every((byte) => byte === 0)) {
            throw new Error('X25519 shared secret MUST NOT be all zeros.');
        }
    }
    static toA256CtrJwk(keyBytes) {
        return { alg: ContentEncryptionAlgorithm.A256CTR, k: Encoder.bytesToBase64Url(keyBytes), kty: 'oct' };
    }
    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);
    }
    static toStream(data) {
        return new ReadableStream({
            start(controller) {
                controller.enqueue(data);
                controller.close();
            }
        });
    }
}
//# sourceMappingURL=encryption.js.map