pragma circom 2.1.6;

include "circomlib/circuits/bitify.circom";
include "circomlib/circuits/poseidon.circom";
include "@zk-email/zk-regex-circom/circuits/common/body_hash_regex.circom";
include "./lib/base64.circom";
include "./lib/rsa.circom";
include "./lib/sha.circom";
include "./utils/array.circom";
include "./utils/regex.circom";
include "./utils/hash.circom";
include "./utils/bytes.circom";
include "./helpers/remove-soft-line-breaks.circom";


/// @title EmailVerifier
/// @notice Circuit to verify email signature as per DKIM standard.
/// @notice Verifies the signature is valid for the given header and pubkey, and the hash of the body matches the hash in the header.
/// @notice This cicuit only verifies signature as per `rsa-sha256` algorithm.
/// @param maxHeadersLength Maximum length for the email header.
/// @param maxBodyLength Maximum length for the email body.
/// @param n Number of bits per chunk the RSA key is split into. Recommended to be 121.
/// @param k Number of chunks the RSA key is split into. Recommended to be 17.
/// @param ignoreBodyHashCheck Set 1 to skip body hash check in case data to prove/extract is only in the headers.
/// @param enableHeaderMasking Set 1 to turn on header masking.
/// @param enableBodyMasking Set 1 to turn on body masking.
/// @param removeSoftLineBreaks Set 1 to remove soft line breaks from the email body.
/// @input emailHeader[maxHeadersLength] Email headers that are signed (ones in `DKIM-Signature` header) as ASCII int[], padded as per SHA-256 block size.
/// @input emailHeaderLength Length of the email header including the SHA-256 padding.
/// @input pubkey[k] RSA public key split into k chunks of n bits each.
/// @input signature[k] RSA signature split into k chunks of n bits each.
/// @input emailBody[maxBodyLength] Email body after the precomputed SHA as ASCII int[], padded as per SHA-256 block size.
/// @input emailBodyLength Length of the email body including the SHA-256 padding.
/// @input bodyHashIndex Index of the body hash `bh` in the emailHeader.
/// @input precomputedSHA[32] Precomputed SHA-256 hash of the email body till the bodyHashIndex.
/// @input decodedEmailBodyIn[maxBodyLength] Decoded email body without soft line breaks.
/// @input mask[maxBodyLength] Mask for the email body.
/// @output pubkeyHash Poseidon hash of the pubkey - Poseidon(n/2)(n/2 chunks of pubkey with k*2 bits per chunk).
/// @output decodedEmailBodyOut[maxBodyLength] Decoded email body with soft line breaks removed.
/// @output maskedHeader[maxHeadersLength] Masked email header.
/// @output maskedBody[maxBodyLength] Masked email body.
template EmailVerifier(maxHeadersLength, maxBodyLength, n, k, ignoreBodyHashCheck, enableHeaderMasking, enableBodyMasking, removeSoftLineBreaks) {
    assert(maxHeadersLength % 64 == 0);
    assert(maxBodyLength % 64 == 0);
    assert(n * k > 2048); // to support 2048 bit RSA
    assert(n < (255 \ 2)); // for multiplication to fit in the field (255 bits)


    signal input emailHeader[maxHeadersLength];
    signal input emailHeaderLength;
    signal input pubkey[k];
    signal input signature[k];

    signal output pubkeyHash;


    // Assert `emailHeaderLength` fits in `ceil(log2(maxHeadersLength))`
    component n2bHeaderLength = Num2Bits(log2Ceil(maxHeadersLength));
    n2bHeaderLength.in <== emailHeaderLength;


    // Assert `emailHeader` data after `emailHeaderLength` are zeros
    AssertZeroPadding(maxHeadersLength)(emailHeader, emailHeaderLength);
    

    // Calculate SHA256 hash of the `emailHeader` - 506,670 constraints
    signal sha[256] <== Sha256Bytes(maxHeadersLength)(emailHeader, emailHeaderLength);
    component bitPacker = PackBits(256, 128);
    bitPacker.in <== sha;
    signal output shaHi <== bitPacker.out[0];
    signal output shaLo <== bitPacker.out[1];

    // Pack SHA output bytes to int[] for RSA input message
    var rsaMessageSize = (256 + n) \ n;
    component rsaMessage[rsaMessageSize];
    for (var i = 0; i < rsaMessageSize; i++) {
        rsaMessage[i] = Bits2Num(n);
    }
    for (var i = 0; i < 256; i++) {
        rsaMessage[i \ n].in[i % n] <== sha[255 - i];
    }
    for (var i = 256; i < n * rsaMessageSize; i++) {
        rsaMessage[i \ n].in[i % n] <== 0;
    }

    // Verify RSA signature - 149,251 constraints
    component rsaVerifier = RSAVerifier65537(n, k);
    for (var i = 0; i < rsaMessageSize; i++) {
        rsaVerifier.message[i] <== rsaMessage[i].out;
    }
    for (var i = rsaMessageSize; i < k; i++) {
        rsaVerifier.message[i] <== 0;
    }
    rsaVerifier.modulus <== pubkey;
    rsaVerifier.signature <== signature;

    if (enableHeaderMasking == 1) {
        signal input headerMask[maxHeadersLength];
        signal output maskedHeader[maxHeadersLength];
        component byteMask = ByteMask(maxHeadersLength);
        
        byteMask.in <== emailHeader;
        byteMask.mask <== headerMask;
        maskedHeader <== byteMask.out;
    }

    // Calculate the SHA256 hash of the body and verify it matches the hash in the header
    if (ignoreBodyHashCheck != 1) {
        signal input bodyHashIndex;
        signal input precomputedSHA[32];
        signal input emailBody[maxBodyLength];
        signal input emailBodyLength;


        // Assert `emailBodyLength` fits in `ceil(log2(maxBodyLength))`
        component n2bBodyLength = Num2Bits(log2Ceil(maxBodyLength));
        n2bBodyLength.in <== emailBodyLength;


        // Assert data after the body (`maxBodyLength - emailBody.length`) is all zeroes
        AssertZeroPadding(maxBodyLength)(emailBody, emailBodyLength);


        // Body hash regex - 617,597 constraints
        // Extract the body hash from the header (i.e. the part after bh= within the DKIM-signature section)
        signal (bhRegexMatch, bhReveal[maxHeadersLength]) <== BodyHashRegex(maxHeadersLength)(emailHeader);
        bhRegexMatch === 1;

        var shaB64Length = 44; // Length of SHA-256 hash when base64 encoded - ceil(32 / 3) * 4
        signal bhBase64[shaB64Length] <== SelectRegexReveal(maxHeadersLength, shaB64Length)(bhReveal, bodyHashIndex);
        signal headerBodyHash[32] <== Base64Decode(32)(bhBase64);

        // Compute SHA256 of email body : 760,142 constraints (for maxBodyLength = 1536)
        // We are using a technique to save constraints by precomputing the SHA hash of the body till the area we want to extract
        // It doesn't have an impact on security since a user must have known the pre-image of a signed message to be able to fake it
        signal computedBodyHash[256] <== Sha256BytesPartial(maxBodyLength)(emailBody, emailBodyLength, precomputedSHA);

        // Ensure the bodyHash from the header matches the calculated body hash
        component computedBodyHashInts[32];
        for (var i = 0; i < 32; i++) {
            computedBodyHashInts[i] = Bits2Num(8);
            for (var j = 0; j < 8; j++) {
                computedBodyHashInts[i].in[7 - j] <== computedBodyHash[i * 8 + j];
            }
            computedBodyHashInts[i].out === headerBodyHash[i];
        }

        if (removeSoftLineBreaks == 1) {
            signal input decodedEmailBodyIn[maxBodyLength];
            component qpEncodingChecker = RemoveSoftLineBreaks(maxBodyLength);

            qpEncodingChecker.encoded <== emailBody;
            qpEncodingChecker.decoded <== decodedEmailBodyIn;

            qpEncodingChecker.isValid === 1;
        }

        if (enableBodyMasking == 1) {
            signal input bodyMask[maxBodyLength];
            signal output maskedBody[maxBodyLength];
            component byteMask = ByteMask(maxBodyLength);
            
            byteMask.in <== emailBody;
            byteMask.mask <== bodyMask;
            maskedBody <== byteMask.out;
        }
    }

    // Calculate the Poseidon hash of DKIM public key as output
    // This can be used to verify (by verifier/contract) the pubkey used in the proof without needing the full key
    // Since PoseidonLarge concatenates nearby values its important to use same n/k (recommended 121*17) to produce uniform hashes
    // https://zkrepl.dev/?gist=43ce7dce2466c63812f6efec5b13aa73 - This can be used to calculate the pubkey hash separately
    pubkeyHash <== PoseidonLarge(n, k)(pubkey);
}
