import * as KeetaNetClient from '@keetanetwork/keetanet-client';
import * as oids from '../services/kyc/oids.generated.js';
import * as ASN1 from './utils/asn1.js';
import { arrayBufferLikeToBuffer, arrayBufferToBuffer, Buffer, bufferToArrayBuffer } from './utils/buffer.js';
import crypto from './utils/crypto.js';
import { CertificateAttributeSchema } from '../services/kyc/iso20022.generated.js';
import { getOID, lookupByOID } from './utils/oid.js';
/**
 * Short alias for printing a debug representation of an object
 */
const DPO = KeetaNetClient.lib.Utils.Helper.debugPrintableObject.bind(KeetaNetClient.lib.Utils.Helper);
/**
 * Sensitive Attribute Schema
 *
 * ASN.1 Schema:
 * SensitiveAttributes DEFINITIONS ::= BEGIN
 *         SensitiveAttribute ::= SEQUENCE {
 *                 version        INTEGER { v1(0) },
 *                 cipher         SEQUENCE {
 *                         algorithm    OBJECT IDENTIFIER,
 *                         ivOrNonce    OCTET STRING,
 *                         key          OCTET STRING
 *                 },
 *                 hashedValue    SEQUENCE {
 *                         encryptedSalt  OCTET STRING,
 *                         algorithm      OBJECT IDENTIFIER,
 *                         value          OCTET STRING
 *                 },
 *                 encryptedValue OCTET STRING
 *         }
 * END
 *
 * https://keeta.notion.site/Keeta-KYC-Certificate-Extensions-13e5da848e588042bdcef81fc40458b7
 *
 * @internal
 */
const SensitiveAttributeSchemaInternal = [
    0n,
    [
        ASN1.ValidateASN1.IsOID,
        ASN1.ValidateASN1.IsOctetString,
        ASN1.ValidateASN1.IsOctetString
    ],
    [
        ASN1.ValidateASN1.IsOctetString,
        ASN1.ValidateASN1.IsOID,
        ASN1.ValidateASN1.IsOctetString
    ],
    ASN1.ValidateASN1.IsOctetString
];
/*
 * Database of permitted algorithms and their OIDs
 */
const sensitiveAttributeOIDDB = {
    'aes-256-gcm': oids.AES_256_GCM,
    'aes-256-cbc': oids.AES_256_CBC,
    'sha2-256': oids.SHA2_256,
    'sha3-256': oids.SHA3_256,
    'sha256': oids.SHA2_256,
    'aes256-gcm': oids.AES_256_GCM,
    'aes256-cbc': oids.AES_256_CBC
};
/**
 * Encode an attribute value using its ASN.1 schema
 */
export function encodeAttribute(name, value) {
    const schema = CertificateAttributeSchema[name];
    let encodedJS;
    try {
        encodedJS = new ASN1.ValidateASN1(schema).fromJavaScriptObject(value);
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        throw (new Error(`Attribute ${name}: ${message} (value: ${JSON.stringify(DPO(value))})`));
    }
    if (encodedJS === undefined) {
        throw (new Error(`Unsupported attribute value for encoding: ${JSON.stringify(DPO(value))}`));
    }
    const asn1Object = ASN1.JStoASN1(encodedJS);
    if (!asn1Object) {
        throw (new Error(`Failed to encode value for attribute ${name}`));
    }
    return (asn1Object.toBER(false));
}
/**
 * Prepare a value for inclusion in a SensitiveAttribute: pre-encode complex and date types
 */
export function encodeForSensitive(name, value) {
    if (Buffer.isBuffer(value)) {
        return (value);
    }
    if (value instanceof ArrayBuffer) {
        return (arrayBufferToBuffer(value));
    }
    if (typeof value === 'string') {
        const asn1 = ASN1.JStoASN1({ type: 'string', kind: 'utf8', value });
        return (arrayBufferToBuffer(asn1.toBER(false)));
    }
    if (value instanceof Date) {
        const asn1 = ASN1.JStoASN1(value);
        return (arrayBufferToBuffer(asn1.toBER(false)));
    }
    if (typeof value === 'object' && value !== null) {
        if (!name) {
            throw (new Error('attributeName required for complex types'));
        }
        const encoded = encodeAttribute(name, value);
        return (arrayBufferToBuffer(encoded));
    }
    return (Buffer.from(String(value), 'utf-8'));
}
/**
 * Check if value is an ASN.1-like wrapper object
 */
function isASN1Wrapper(obj) {
    return ('type' in obj && 'value' in obj && typeof obj.type === 'string');
}
/**
 * Normalize the result (unwrap ASN.1-like objects)
 */
function normalizeDecodedValue(input) {
    if (input === undefined || input === null || typeof input !== 'object') {
        return (input);
    }
    if (input instanceof Date || Buffer.isBuffer(input) || input instanceof ArrayBuffer) {
        return (input);
    }
    if (Array.isArray(input)) {
        return (input.map(item => normalizeDecodedValue(item)));
    }
    // Unwrap ASN.1-like objects
    if (isASN1Wrapper(input)) {
        if (input.type === 'string' && typeof input.value === 'string') {
            return (input.value);
        }
        if (input.type === 'date' && input.value instanceof Date) {
            return (input.value);
        }
    }
    // Recursively normalize object properties
    const result = {};
    for (const [key, value] of Object.entries(input)) {
        result[key] = normalizeDecodedValue(value);
    }
    return (result);
}
/**
 * Decode ASN.1 data using a schema
 *
 * Note: The ASN1 library uses dynamic typing internally, so we consolidate
 * the unsafe operations here rather than scattering them throughout the code.
 */
function decodeWithSchema(buffer, schema) {
    /* eslint-disable
        @typescript-eslint/no-explicit-any,
        @typescript-eslint/consistent-type-assertions,
        @typescript-eslint/no-unsafe-member-access,
        @typescript-eslint/no-unsafe-call,
        @typescript-eslint/no-unsafe-assignment
    */
    let decodedASN1;
    try {
        decodedASN1 = new ASN1.BufferStorageASN1(buffer, schema).getASN1();
    }
    catch {
        decodedASN1 = ASN1.ASN1toJS(buffer);
    }
    if (decodedASN1 === undefined) {
        throw (new Error('Failed to decode ASN1 data'));
    }
    const validator = new ASN1.ValidateASN1(schema);
    return (validator.toJavaScriptObject(decodedASN1));
    /* eslint-enable */
}
/**
 * GCM cipher helpers - Node's crypto types don't properly type getAuthTag/setAuthTag
 * when using createCipheriv/createDecipheriv, so we use typed wrappers.
 */
/* eslint-disable @typescript-eslint/consistent-type-assertions -- Reflect.get returns unknown */
function getGCMAuthTag(cipher) {
    const getAuthTag = Reflect.get(cipher, 'getAuthTag');
    if (typeof getAuthTag !== 'function') {
        throw (new TypeError('getAuthTag is not available on cipher'));
    }
    return (getAuthTag.call(cipher));
}
function setGCMAuthTag(decipher, tag) {
    const setAuthTag = Reflect.get(decipher, 'setAuthTag');
    if (typeof setAuthTag !== 'function') {
        throw (new TypeError('setAuthTag is not available on decipher'));
    }
    setAuthTag.call(decipher, tag);
}
/* eslint-enable @typescript-eslint/consistent-type-assertions */
/**
 * Decode a value from its ASN.1 representation back to the original type
 *
 * @internal
 */
function decodeForSensitive(name, data) {
    const buffer = Buffer.isBuffer(data) ? bufferToArrayBuffer(data) : data;
    const schema = CertificateAttributeSchema[name];
    const plainObject = decodeWithSchema(buffer, schema);
    return (normalizeDecodedValue(plainObject));
}
export class SensitiveAttribute {
    static SensitiveAttributeObjectTypeID = 'c0cc9591-cebb-4441-babe-23739279e3f2';
    SensitiveAttributeObjectTypeID;
    #account;
    #encryptedDER;
    #info;
    #decoder;
    constructor(account, data, decoder) {
        Object.defineProperty(this, 'SensitiveAttributeObjectTypeID', {
            value: SensitiveAttribute.SensitiveAttributeObjectTypeID,
            enumerable: false
        });
        this.#account = account;
        this.#encryptedDER = Buffer.isBuffer(data) ? bufferToArrayBuffer(data) : data;
        this.#info = this.decode(data);
        if (decoder) {
            this.#decoder = decoder;
        }
    }
    /**
     * Check if a value is a SensitiveAttribute instance
     */
    static isInstance(input) {
        if (typeof input !== 'object' || input === null) {
            return (false);
        }
        return (Reflect.get(input, 'SensitiveAttributeObjectTypeID') === SensitiveAttribute.SensitiveAttributeObjectTypeID);
    }
    /**
     * Get the public key this attribute was encrypted for
     */
    get publicKey() {
        return (this.#account.publicKeyString.get());
    }
    /**
     * Get the raw encrypted DER for certificate embedding
     */
    toDER() {
        return (this.#encryptedDER);
    }
    decode(data) {
        if (Buffer.isBuffer(data)) {
            data = bufferToArrayBuffer(data);
        }
        let decodedAttribute;
        try {
            const dataObject = new ASN1.BufferStorageASN1(data, SensitiveAttributeSchemaInternal);
            decodedAttribute = dataObject.getASN1();
        }
        catch {
            const js = ASN1.ASN1toJS(data);
            throw (new Error(`SensitiveAttribute.decode: unexpected DER shape ${JSON.stringify(DPO(js))}`));
        }
        const decodedVersion = decodedAttribute[0] + 1n;
        if (decodedVersion !== 1n) {
            throw (new Error(`Unsupported Sensitive Attribute version (${decodedVersion})`));
        }
        return ({
            version: decodedVersion,
            publicKey: this.#account.publicKeyString.get(),
            cipher: {
                algorithm: lookupByOID(decodedAttribute[1][0].oid, sensitiveAttributeOIDDB),
                iv: decodedAttribute[1][1],
                key: decodedAttribute[1][2]
            },
            hashedValue: {
                encryptedSalt: decodedAttribute[2][0],
                algorithm: lookupByOID(decodedAttribute[2][1].oid, sensitiveAttributeOIDDB),
                value: decodedAttribute[2][2]
            },
            encryptedValue: decodedAttribute[3]
        });
    }
    async #decryptValue(value) {
        const decryptedKey = await this.#account.decrypt(bufferToArrayBuffer(this.#info.cipher.key));
        const algorithm = this.#info.cipher.algorithm;
        const iv = this.#info.cipher.iv;
        const decipher = crypto.createDecipheriv(algorithm, Buffer.from(decryptedKey), iv);
        // For AES-GCM, extract and set the 16-byte authentication tag
        if (algorithm === 'aes-256-gcm') {
            const authTag = value.subarray(-16);
            const ciphertext = value.subarray(0, -16);
            setGCMAuthTag(decipher, authTag);
            const decrypted = decipher.update(ciphertext);
            decipher.final(); // Verify auth tag
            return (decrypted);
        }
        // For other algorithms (like CBC), just decrypt normally
        const decrypted = decipher.update(value);
        decipher.final();
        return (decrypted);
    }
    /**
     * Get the value of the sensitive attribute
     *
     * This will decrypt the value using the account's private key
     * and return the value as an ArrayBuffer
     *
     * Since sensitive attributes are binary blobs, this returns an
     * ArrayBuffer
     */
    async get() {
        const decryptedValue = await this.#decryptValue(arrayBufferLikeToBuffer(this.#info.encryptedValue));
        return (bufferToArrayBuffer(decryptedValue));
    }
    async getValue() {
        const value = await this.get();
        if (!this.#decoder) {
            /**
             * TypeScript complains that T may not be the correct
             * type here, but gives us no tools to enforce that it
             * is -- it should always be ArrayBuffer if no decoder
             * is provided, but someone could always specify a
             * type parameter in that case and we cannot check
             * that at runtime since T is only a compile-time type.
             */
            // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
            return value;
        }
        return (await this.#decoder(value));
    }
    /**
     * Generate a proof that a sensitive attribute is a given value,
     * which can be validated by a third party using the certificate
     * and the `validateProof` method
     */
    async getProof() {
        const value = await this.get();
        const salt = await this.#decryptValue(arrayBufferLikeToBuffer(this.#info.hashedValue.encryptedSalt));
        return ({
            value: Buffer.from(value).toString('base64'),
            hash: {
                salt: salt.toString('base64')
            }
        });
    }
    /**
     * Validate the proof that a sensitive attribute is a given value
     */
    async validateProof(proof) {
        const plaintextValue = Buffer.from(proof.value, 'base64');
        const proofSaltBuffer = Buffer.from(proof.hash.salt, 'base64');
        const publicKeyBuffer = Buffer.from(this.#account.publicKey.get());
        const encryptedValue = this.#info.encryptedValue;
        const hashInput = Buffer.concat([proofSaltBuffer, publicKeyBuffer, encryptedValue, plaintextValue]);
        const hashedAndSaltedValue = KeetaNetClient.lib.Utils.Hash.Hash(hashInput);
        const hashedAndSaltedValueBuffer = Buffer.from(hashedAndSaltedValue);
        return (this.#info.hashedValue.value.equals(hashedAndSaltedValueBuffer));
    }
    toJSON() {
        const info = KeetaNetClient.lib.Utils.Conversion.toJSONSerializable(this.#info);
        return (info);
    }
    /**
     * Encrypt a value and create a new SensitiveAttribute
     */
    static async create(account, name, value) {
        // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
        const encodedValue = encodeForSensitive(name, value);
        const salt = crypto.randomBytes(32);
        const hashingAlgorithm = KeetaNetClient.lib.Utils.Hash.HashFunctionName;
        const publicKey = Buffer.from(account.publicKey.get());
        const cipher = 'aes-256-gcm';
        const key = crypto.randomBytes(32);
        const nonce = crypto.randomBytes(12);
        const encryptedKey = await account.encrypt(bufferToArrayBuffer(key));
        function encrypt(input) {
            const cipherObject = crypto.createCipheriv(cipher, key, nonce);
            let retval = Buffer.concat([cipherObject.update(input), cipherObject.final()]);
            // For AES-GCM, append the 16-byte authentication tag
            if (cipher === 'aes-256-gcm') {
                retval = Buffer.concat([retval, getGCMAuthTag(cipherObject)]);
            }
            return (retval);
        }
        const encryptedValue = encrypt(encodedValue);
        const encryptedSalt = encrypt(arrayBufferLikeToBuffer(salt));
        const saltedValue = Buffer.concat([salt, publicKey, encryptedValue, encodedValue]);
        const hashedAndSaltedValue = KeetaNetClient.lib.Utils.Hash.Hash(saltedValue);
        const attributeStructure = [
            /* Version */
            0n,
            /* Cipher Details */
            [
                /* Algorithm */
                { type: 'oid', oid: getOID(cipher, sensitiveAttributeOIDDB) },
                /* IV or Nonce */
                nonce,
                /* Symmetric key, encrypted with the public key of the account */
                Buffer.from(encryptedKey)
            ],
            /* Hashed Value */
            [
                /* Encrypted Salt */
                Buffer.from(encryptedSalt),
                /* Hashing Algorithm */
                { type: 'oid', oid: getOID(hashingAlgorithm, sensitiveAttributeOIDDB) },
                /* Hash of <Encrypted Salt> || <Public Key> || <Value> */
                Buffer.from(hashedAndSaltedValue)
            ],
            /* Encrypted Value, encrypted with the Cipher above */
            encryptedValue
        ];
        const encodedAttributeObject = ASN1.JStoASN1(attributeStructure);
        const encryptedDER = encodedAttributeObject.toBER(false);
        const decoder = function (data) {
            // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
            return decodeForSensitive(name, data);
        };
        const result = new SensitiveAttribute(account, encryptedDER, decoder);
        return (result);
    }
}
//# sourceMappingURL=sensitive-attribute.js.map