import { createView, setBigUint64 } from '../utils.js';
import { polyval } from '../_polyval.js';
import { getWebcryptoSubtle } from './utils.js';
/**
 * AES-GCM-SIV: classic AES-GCM with nonce-misuse resistance.
 * RFC 8452, https://datatracker.ietf.org/doc/html/rfc8452
 */
// AES stuff (same as ff1)
const BLOCK_LEN = 16;
const IV = new Uint8Array(BLOCK_LEN);
async function encryptBlock(msg, key) {
    if (key.length !== 16 && key.length !== 32)
        throw new Error('Invalid key length');
    const mode = { name: `AES-CBC`, length: key.length * 8 };
    const cr = getWebcryptoSubtle();
    const wKey = await cr.importKey('raw', key, mode, true, ['encrypt']);
    const cipher = await cr.encrypt({ name: `aes-cbc`, iv: IV, counter: IV, length: 64 }, wKey, msg);
    return new Uint8Array(cipher).subarray(0, 16);
}
// Kinda constant-time equality
function equalBytes(a, b) {
    // Should not happen
    if (a.length !== b.length)
        throw new Error('equalBytes: Different size of Uint8Arrays');
    let flag = true;
    for (let i = 0; i < a.length; i++)
        if (a[i] !== b[i])
            flag && (flag = false);
    return flag;
}
// Wrap position so it will be in padded to blockSize
const wrapPos = (pos, blockSize) => Math.ceil(pos / blockSize) * blockSize;
const limit = (name, min, max) => (value) => {
    if (!Number.isSafeInteger(value) || min > value || value > max)
        throw new Error(`${name}: invalid value=${value}, must be [${min}..${max}]`);
};
// From RFC 8452: Section 6
const AAD_LIMIT = limit('AAD', 0, 2 ** 36);
const PLAIN_LIMIT = limit('Plaintext', 0, 2 ** 36);
const NONCE_LIMIT = limit('Nonce', 12, 12);
const CIPHER_LIMIT = limit('Ciphertext', 16, 2 ** 36 + 16);
// nodejs api doesn't support 32bit counters, browser does
async function ctr(key, tag, input) {
    // The initial counter block is the tag with the most significant bit of the last byte set to one.
    let block = tag.slice();
    block[15] |= 0x80;
    let view = createView(block);
    let output = new Uint8Array(input.length);
    for (let pos = 0; pos < input.length;) {
        const encryptedBlock = await encryptBlock(block, key);
        view.setUint32(0, view.getUint32(0, true) + 1, true);
        const take = Math.min(input.length, encryptedBlock.length);
        for (let j = 0; j < take; j++, pos++)
            output[pos] = encryptedBlock[j] ^ input[pos];
    }
    return new Uint8Array(output);
}
export async function deriveKeys(key, nonce) {
    NONCE_LIMIT(nonce.length);
    const len = key.length;
    if (len !== 16 && len !== 32)
        throw new Error(`key length must be 16 or 32 bytes, got: ${len} bytes`);
    const encKey = new Uint8Array(len);
    const authKey = new Uint8Array(16);
    let counter = 0;
    const deriveBlock = new Uint8Array(nonce.length + 4);
    deriveBlock.set(nonce, 4);
    const view = createView(deriveBlock);
    for (const derivedKey of [authKey, encKey]) {
        for (let i = 0; i < derivedKey.length; i += 8) {
            view.setUint32(0, counter++, true);
            const block = await encryptBlock(deriveBlock, key);
            derivedKey.set(block.subarray(0, 8), i);
        }
    }
    return { authKey, encKey };
}
export async function aes_256_gcm_siv(key, nonce, AAD) {
    const { encKey, authKey } = await deriveKeys(key, nonce);
    const computeTag = async (data, AAD) => {
        const dataPos = wrapPos(AAD.length, 16);
        const lenPos = wrapPos(dataPos + data.length, 16);
        const block = new Uint8Array(lenPos + 16);
        const view = createView(block);
        block.set(AAD);
        block.set(data, dataPos);
        setBigUint64(view, lenPos, BigInt(AAD.length * 8), true);
        setBigUint64(view, lenPos + 8, BigInt(data.length * 8), true);
        // Compute the expected tag by XORing S_s and the nonce, clearing the
        // most significant bit of the last byte and encrypting with the
        // message-encryption key.
        const tag = polyval(authKey, block);
        for (let i = 0; i < 12; i++)
            tag[i] ^= nonce[i];
        // Clear the highest bit
        tag[15] &= 0x7f;
        return await encryptBlock(tag, encKey);
    };
    return {
        //    computeTag,
        encrypt: async (plaintext) => {
            AAD_LIMIT(AAD.length);
            PLAIN_LIMIT(plaintext.length);
            const tag = await computeTag(plaintext, AAD);
            const out = new Uint8Array(plaintext.length + 16);
            out.set(tag, plaintext.length);
            out.set(await ctr(encKey, tag, plaintext));
            return out;
        },
        decrypt: async (ciphertext) => {
            CIPHER_LIMIT(ciphertext.length);
            AAD_LIMIT(AAD.length);
            const tag = ciphertext.subarray(-16);
            const plaintext = await ctr(encKey, tag, ciphertext.subarray(0, -16));
            const expectedTag = await computeTag(plaintext, AAD);
            if (!equalBytes(tag, expectedTag))
                throw new Error('invalid poly1305 tag');
            return plaintext;
        },
    };
}
//# sourceMappingURL=siv.js.map