#include <archive.h>
#include <archive_entry.h>
+#include <zlib.h>
+#ifdef HAVE_OPENSSL
+#include <openssl/evp.h>
+#include <openssl/rand.h>
+#include <openssl/err.h>
+#include <openssl/hmac.h>
+#endif
#define msg_debug_archive(...) rspamd_conditional_debug_fast(NULL, NULL, \
rspamd_archive_log_id, "archive", task->task_pool->tag.uid, \
G_STRFUNC, \
__VA_ARGS__)
+#define msg_debug_archive_taskless(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ rspamd_archive_log_id, "archive", NULL, \
+ G_STRFUNC, \
+ __VA_ARGS__)
INIT_LOG_MODULE(archive)
+static GQuark
+rspamd_archives_err_quark(void)
+{
+ static GQuark q = 0;
+ if (G_UNLIKELY(q == 0)) {
+ q = g_quark_from_static_string("archives");
+ }
+
+ return q;
+}
+
static void
rspamd_archive_dtor(gpointer p)
{
g_ptr_array_free(arch->files, TRUE);
}
+static inline guint16
+rspamd_zip_time_dos(time_t t)
+{
+ struct tm lt;
+
+ if (t == 0) {
+ t = time(NULL);
+ }
+
+ (void) localtime_r(&t, <);
+
+ guint16 dos_time = ((guint16) (lt.tm_hour & 0x1f) << 11) |
+ ((guint16) (lt.tm_min & 0x3f) << 5) |
+ ((guint16) ((lt.tm_sec / 2) & 0x1f));
+
+ return dos_time;
+}
+
+static inline guint16
+rspamd_zip_date_dos(time_t t)
+{
+ struct tm lt;
+
+ if (t == 0) {
+ t = time(NULL);
+ }
+
+ (void) localtime_r(&t, <);
+
+ int year = lt.tm_year + 1900;
+ if (year < 1980) {
+ year = 1980; /* DOS date epoch */
+ }
+
+ guint16 dos_date = ((guint16) ((year - 1980) & 0x7f) << 9) |
+ ((guint16) ((lt.tm_mon + 1) & 0x0f) << 5) |
+ ((guint16) (lt.tm_mday & 0x1f));
+
+ return dos_date;
+}
+
+static inline void
+rspamd_ba_append_u16le(GByteArray *ba, guint16 v)
+{
+ union {
+ guint16 u16;
+ unsigned char b[2];
+ } u;
+
+ u.u16 = GUINT16_TO_LE(v);
+ g_byte_array_append(ba, u.b, sizeof(u.b));
+}
+
+static inline void
+rspamd_ba_append_u32le(GByteArray *ba, guint32 v)
+{
+ union {
+ guint32 u32;
+ unsigned char b[4];
+ } u;
+
+ u.u32 = GUINT32_TO_LE(v);
+ g_byte_array_append(ba, u.b, sizeof(u.b));
+}
+
+static gboolean
+rspamd_zip_deflate_alloc(const unsigned char *in,
+ gsize inlen,
+ unsigned char **outbuf,
+ gsize *outlen)
+{
+ int rc;
+ z_stream strm;
+
+ memset(&strm, 0, sizeof(strm));
+ /* raw DEFLATE stream for ZIP */
+ rc = deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED,
+ -MAX_WBITS, MAX_MEM_LEVEL - 1, Z_DEFAULT_STRATEGY);
+
+ if (rc != Z_OK) {
+ return FALSE;
+ }
+
+ /* Compute upper bound and allocate */
+ uLong bound = deflateBound(&strm, (uLong) inlen);
+ unsigned char *obuf = g_malloc(bound);
+
+ strm.next_in = (unsigned char *) in;
+ strm.avail_in = inlen;
+ strm.next_out = obuf;
+ strm.avail_out = bound;
+
+ rc = deflate(&strm, Z_FINISH);
+
+ if (rc != Z_STREAM_END && rc != Z_OK && rc != Z_BUF_ERROR) {
+ deflateEnd(&strm);
+ g_free(obuf);
+ return FALSE;
+ }
+
+ *outlen = bound - strm.avail_out;
+ *outbuf = obuf;
+ deflateEnd(&strm);
+
+ return TRUE;
+}
+
+static gboolean
+rspamd_zip_validate_name(const char *name)
+{
+ if (name == NULL || *name == '\0') {
+ return FALSE;
+ }
+ /* Disallow absolute paths and parent traversals */
+ if (name[0] == '/' || name[0] == '\\') {
+ return FALSE;
+ }
+ if (strstr(name, "..") != NULL) {
+ return FALSE;
+ }
+ if (strchr(name, ':') != NULL) {
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+rspamd_zip_write_local_header(GByteArray *zip,
+ const char *name,
+ guint16 ver_needed,
+ guint16 gp_flags,
+ guint16 method,
+ time_t mtime,
+ guint32 crc,
+ guint32 csize,
+ guint32 usize,
+ guint16 extra_len)
+{
+ /* Local file header */
+ /* signature */
+ rspamd_ba_append_u32le(zip, 0x04034b50);
+ /* version needed to extract */
+ rspamd_ba_append_u16le(zip, ver_needed);
+ /* general purpose bit flag */
+ rspamd_ba_append_u16le(zip, gp_flags);
+ /* compression method */
+ rspamd_ba_append_u16le(zip, method);
+ /* last mod file time/date */
+ rspamd_ba_append_u16le(zip, rspamd_zip_time_dos(mtime));
+ rspamd_ba_append_u16le(zip, rspamd_zip_date_dos(mtime));
+ /* CRC-32 */
+ rspamd_ba_append_u32le(zip, crc);
+ /* compressed size */
+ rspamd_ba_append_u32le(zip, csize);
+ /* uncompressed size */
+ rspamd_ba_append_u32le(zip, usize);
+ /* file name length */
+ rspamd_ba_append_u16le(zip, (guint16) strlen(name));
+ /* extra field length */
+ rspamd_ba_append_u16le(zip, extra_len);
+ /* file name */
+ g_byte_array_append(zip, (const guint8 *) name, strlen(name));
+}
+
+static void
+rspamd_zip_write_central_header(GByteArray *cd,
+ const char *name,
+ guint16 ver_needed,
+ guint16 gp_flags,
+ guint16 method,
+ time_t mtime,
+ guint32 crc,
+ guint32 csize,
+ guint32 usize,
+ guint32 lfh_offset,
+ guint32 mode,
+ guint16 extra_len)
+{
+ /* Central directory file header */
+ rspamd_ba_append_u32le(cd, 0x02014b50);
+ /* version made by: 3 (UNIX) << 8 | 20 */
+ rspamd_ba_append_u16le(cd, (guint16) ((3 << 8) | 20));
+ /* version needed to extract */
+ rspamd_ba_append_u16le(cd, ver_needed);
+ /* general purpose bit flag */
+ rspamd_ba_append_u16le(cd, gp_flags);
+ /* compression method */
+ rspamd_ba_append_u16le(cd, method);
+ /* time/date */
+ rspamd_ba_append_u16le(cd, rspamd_zip_time_dos(mtime));
+ rspamd_ba_append_u16le(cd, rspamd_zip_date_dos(mtime));
+ /* CRC and sizes */
+ rspamd_ba_append_u32le(cd, crc);
+ rspamd_ba_append_u32le(cd, csize);
+ rspamd_ba_append_u32le(cd, usize);
+ /* name len, extra len, comment len */
+ rspamd_ba_append_u16le(cd, (guint16) strlen(name));
+ rspamd_ba_append_u16le(cd, extra_len);
+ rspamd_ba_append_u16le(cd, 0);
+ /* disk number start, internal attrs */
+ rspamd_ba_append_u16le(cd, 0);
+ rspamd_ba_append_u16le(cd, 0);
+ /* external attrs: UNIX perms in upper 16 bits */
+ guint32 xattr = (mode ? mode : 0644);
+ xattr = (xattr & 0xFFFF) << 16;
+ rspamd_ba_append_u32le(cd, xattr);
+ /* relative offset of local header */
+ rspamd_ba_append_u32le(cd, lfh_offset);
+ /* file name */
+ g_byte_array_append(cd, (const guint8 *) name, strlen(name));
+}
+
+#define ZIP_AES_EXTRA_ID 0x9901
+
+static void
+rspamd_zip_write_extra_aes(GByteArray *ba, guint16 vendor_version, guint8 strength, guint16 actual_method)
+{
+ /* Extra field header id and size */
+ rspamd_ba_append_u16le(ba, ZIP_AES_EXTRA_ID);
+ /* data size = 7 */
+ rspamd_ba_append_u16le(ba, 7);
+ /* Vendor version */
+ rspamd_ba_append_u16le(ba, vendor_version);
+ /* Vendor ID 'AE' */
+ const guint8 vid[2] = {'A', 'E'};
+ g_byte_array_append(ba, vid, sizeof(vid));
+ /* Strength */
+ g_byte_array_append(ba, &strength, 1);
+ /* Actual compression method */
+ rspamd_ba_append_u16le(ba, actual_method);
+}
+
+GByteArray *
+rspamd_archives_zip_write(const struct rspamd_zip_file_spec *files,
+ gsize nfiles,
+ const char *password,
+ GError **err)
+{
+ GByteArray *zip = NULL, *cd = NULL;
+ GQuark q = rspamd_archives_err_quark();
+
+ if (files == NULL || nfiles == 0) {
+ g_set_error(err, q, EINVAL, "no files to archive");
+ return NULL;
+ }
+
+ zip = g_byte_array_new();
+ cd = g_byte_array_new();
+
+ for (gsize i = 0; i < nfiles; i++) {
+ const struct rspamd_zip_file_spec *f = &files[i];
+ if (!rspamd_zip_validate_name(f->name)) {
+ g_set_error(err, q, EINVAL, "invalid zip entry name: %s", f->name ? f->name : "(null)");
+ g_byte_array_free(cd, TRUE);
+ g_byte_array_free(zip, TRUE);
+ return NULL;
+ }
+
+ guint32 crc = crc32(0L, Z_NULL, 0);
+ crc = crc32(crc, f->data, f->len);
+ guint16 method = 8; /* deflate */
+ guint16 gp_flags = (1u << 11); /* UTF-8 */
+ guint16 ver_needed = 20; /* default */
+ const gboolean use_aes = (password != NULL && *password != '\0');
+
+ /* actual method will be decided after deflate; default is deflate */
+
+ guint16 extra_len = 0;
+ guint16 actual_method = method;
+ guint32 csize_for_header = 0;
+ const guint8 aes_strength = 0x03; /* AES-256 */
+ const guint16 aes_vendor_ver = 0x0002; /* AE-2 */
+ guint8 salt_len = 0;
+ if (use_aes) {
+ /* Per APPNOTE: method=99 (0x63), AES extra 0x9901 in both headers */
+ ver_needed = MAX(ver_needed, (guint16) 51);
+ gp_flags |= 1u; /* encrypted */
+ method = 99;
+ extra_len = 2 /*id*/ + 2 /*size*/ + 7 /*payload*/;
+ /* salt length by strength */
+ salt_len = (aes_strength == 0x01 ? 8 : (aes_strength == 0x02 ? 12 : 16));
+ /* compressed size will be computed after deflate/encrypt */
+ /* CRC-32 not used with AES: set to 0 */
+ crc = 0;
+ }
+
+ guint32 lfh_off = zip->len;
+ rspamd_zip_write_local_header(zip, f->name, ver_needed, gp_flags, method, f->mtime, crc,
+ csize_for_header,
+ (guint32) f->len, extra_len);
+ if (use_aes) {
+ /* Write AES extra for local header */
+ rspamd_zip_write_extra_aes(zip, aes_vendor_ver, aes_strength, actual_method);
+
+#ifdef HAVE_OPENSSL
+ /* Derive keys and encrypt: PBKDF2-HMAC-SHA1 per AE-2 */
+ unsigned char salt[16];
+ if (RAND_bytes(salt, salt_len) != 1) {
+ g_byte_array_free(cd, TRUE);
+ g_byte_array_free(zip, TRUE);
+ g_set_error(err, q, EIO, "cannot generate AES salt");
+ return NULL;
+ }
+
+ /* key sizes by strength */
+ int klen = (aes_strength == 0x01 ? 16 : (aes_strength == 0x02 ? 24 : 32));
+ int dklen = klen * 2 + 2;
+ unsigned char *dk = g_malloc(dklen);
+ if (PKCS5_PBKDF2_HMAC(password, (int) strlen(password), salt, salt_len, 1000, EVP_sha1(), dklen, dk) != 1) {
+ g_free(dk);
+ g_byte_array_free(cd, TRUE);
+ g_byte_array_free(zip, TRUE);
+ g_set_error(err, q, EIO, "PBKDF2(HMAC-SHA1) failed");
+ return NULL;
+ }
+ unsigned char *ekey = dk; /* klen */
+ unsigned char *akey = dk + klen; /* klen */
+ unsigned char *pv = dk + klen * 2; /* 2 bytes */
+
+ /* Append salt and password verification value */
+ g_byte_array_append(zip, salt, salt_len);
+ g_byte_array_append(zip, pv, 2);
+
+ /* AES-CTR encrypt */
+ EVP_CIPHER_CTX *cctx = EVP_CIPHER_CTX_new();
+ const EVP_CIPHER *cipher = (klen == 16 ? EVP_aes_128_ctr() : (klen == 24 ? EVP_aes_192_ctr() : EVP_aes_256_ctr()));
+ unsigned char iv[16];
+ memset(iv, 0, sizeof(iv)); /* WinZip AES uses counter mode starting at 0 */
+ if (EVP_EncryptInit_ex(cctx, cipher, NULL, ekey, iv) != 1) {
+ EVP_CIPHER_CTX_free(cctx);
+ rspamd_explicit_memzero(dk, dklen);
+ g_free(dk);
+ g_byte_array_free(cd, TRUE);
+ g_byte_array_free(zip, TRUE);
+ g_set_error(err, q, EIO, "AES-CTR init failed");
+ return NULL;
+ }
+
+ /* Deflate directly into zip and encrypt in-place */
+ z_stream zst;
+ memset(&zst, 0, sizeof(zst));
+ if (deflateInit2(&zst, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, MAX_MEM_LEVEL - 1, Z_DEFAULT_STRATEGY) != Z_OK) {
+ EVP_CIPHER_CTX_free(cctx);
+ rspamd_explicit_memzero(dk, dklen);
+ g_free(dk);
+ g_byte_array_free(cd, TRUE);
+ g_byte_array_free(zip, TRUE);
+ g_set_error(err, q, EIO, "deflateInit2 failed");
+ return NULL;
+ }
+ uLong bound = deflateBound(&zst, (uLong) f->len);
+ deflateEnd(&zst);
+ /* Append salt+pv already written; now reserve deflate bound */
+ gsize plain_off = zip->len;
+ g_byte_array_set_size(zip, zip->len + bound);
+ unsigned char *plain_ptr = zip->data + plain_off;
+ memset(&zst, 0, sizeof(zst));
+ if (deflateInit2(&zst, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, MAX_MEM_LEVEL - 1, Z_DEFAULT_STRATEGY) != Z_OK) {
+ EVP_CIPHER_CTX_free(cctx);
+ rspamd_explicit_memzero(dk, dklen);
+ g_free(dk);
+ g_byte_array_free(cd, TRUE);
+ g_byte_array_free(zip, TRUE);
+ g_set_error(err, q, EIO, "deflateInit2 failed");
+ return NULL;
+ }
+ zst.next_in = (unsigned char *) f->data;
+ zst.avail_in = f->len;
+ zst.next_out = plain_ptr;
+ zst.avail_out = bound;
+ int rc = deflate(&zst, Z_FINISH);
+ if (rc != Z_STREAM_END && rc != Z_OK && rc != Z_BUF_ERROR) {
+ deflateEnd(&zst);
+ EVP_CIPHER_CTX_free(cctx);
+ rspamd_explicit_memzero(dk, dklen);
+ g_free(dk);
+ g_byte_array_free(cd, TRUE);
+ g_byte_array_free(zip, TRUE);
+ g_set_error(err, q, EIO, "deflate failed");
+ return NULL;
+ }
+ gsize produced = bound - zst.avail_out;
+ deflateEnd(&zst);
+ if (produced >= f->len) {
+ /* fallback to store */
+ g_byte_array_set_size(zip, plain_off);
+ g_byte_array_set_size(zip, zip->len + f->len);
+ memcpy(zip->data + plain_off, f->data, f->len);
+ produced = f->len;
+ actual_method = 0;
+ }
+
+ /* Encrypt in place */
+ gsize enc_before = plain_off;
+ unsigned char *enc_ptr = zip->data + enc_before;
+ int outl = 0, finl = 0;
+ if (EVP_EncryptUpdate(cctx, enc_ptr, &outl, enc_ptr, (int) produced) != 1 ||
+ EVP_EncryptFinal_ex(cctx, enc_ptr + outl, &finl) != 1) {
+ EVP_CIPHER_CTX_free(cctx);
+ rspamd_explicit_memzero(dk, dklen);
+ g_free(dk);
+ g_byte_array_free(cd, TRUE);
+ g_byte_array_free(zip, TRUE);
+ g_set_error(err, q, EIO, "AES-CTR encrypt failed");
+ return NULL;
+ }
+ EVP_CIPHER_CTX_free(cctx);
+ /* shrink to ciphertext size */
+ g_byte_array_set_size(zip, enc_before + outl + finl);
+
+ /* HMAC-SHA1 over ciphertext */
+ unsigned char mac[EVP_MAX_MD_SIZE];
+ unsigned int maclen = 0;
+ HMAC(EVP_sha1(), akey, klen, zip->data + enc_before, (int) (zip->len - enc_before), mac, &maclen);
+ /* append first 10 bytes */
+ g_byte_array_append(zip, mac, 10);
+
+ /* cleanup */
+ rspamd_explicit_memzero(dk, dklen);
+ g_free(dk);
+
+ /* Patch local header: compressed size and actual method in AES extra */
+ csize_for_header = (guint32) (salt_len + 2 + (outl + finl) + 10);
+ /* compressed size at offset lfh_off + 18 */
+ guint32 *p32 = (guint32 *) (zip->data + lfh_off + 18);
+ *p32 = GUINT32_TO_LE(csize_for_header);
+ /* patch actual method in AES extra (last 2 bytes of AES extra payload) */
+ guint16 *p16 = (guint16 *) (zip->data + lfh_off + 30 + (guint32) strlen(f->name) + 9);
+ *p16 = GUINT16_TO_LE(actual_method);
+#else
+ g_byte_array_free(cdata, TRUE);
+ g_byte_array_free(cd, TRUE);
+ g_byte_array_free(zip, TRUE);
+ g_set_error(err, q, ENOTSUP, "AES-CTR encryption requires OpenSSL");
+ return NULL;
+#endif
+ }
+ else {
+ /* Not encrypted: deflate directly into zip, fallback to store */
+ z_stream zst;
+ memset(&zst, 0, sizeof(zst));
+ if (deflateInit2(&zst, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, MAX_MEM_LEVEL - 1, Z_DEFAULT_STRATEGY) != Z_OK) {
+ g_set_error(err, q, EIO, "deflateInit2 failed");
+ return NULL;
+ }
+ uLong bound = deflateBound(&zst, (uLong) f->len);
+ deflateEnd(&zst);
+ gsize off = zip->len;
+ g_byte_array_set_size(zip, zip->len + bound);
+ unsigned char *outp = zip->data + off;
+ memset(&zst, 0, sizeof(zst));
+ if (deflateInit2(&zst, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, MAX_MEM_LEVEL - 1, Z_DEFAULT_STRATEGY) != Z_OK) {
+ g_set_error(err, q, EIO, "deflateInit2 failed");
+ return NULL;
+ }
+ zst.next_in = (unsigned char *) f->data;
+ zst.avail_in = f->len;
+ zst.next_out = outp;
+ zst.avail_out = bound;
+ int rc = deflate(&zst, Z_FINISH);
+ if (rc != Z_STREAM_END && rc != Z_OK && rc != Z_BUF_ERROR) {
+ deflateEnd(&zst);
+ g_set_error(err, q, EIO, "deflate failed");
+ return NULL;
+ }
+ gsize produced = bound - zst.avail_out;
+ deflateEnd(&zst);
+ if (produced >= f->len) {
+ /* store */
+ g_byte_array_set_size(zip, off);
+ g_byte_array_set_size(zip, zip->len + f->len);
+ memcpy(zip->data + off, f->data, f->len);
+ produced = f->len;
+ method = 0;
+ /* patch method in local header (offset +8) */
+ guint16 *pm = (guint16 *) (zip->data + lfh_off + 8);
+ *pm = GUINT16_TO_LE(method);
+ }
+ else {
+ g_byte_array_set_size(zip, off + produced);
+ }
+ csize_for_header = (guint32) produced;
+ /* patch CRC (offset +14) and compressed size (offset +18) */
+ guint32 *p32 = (guint32 *) (zip->data + lfh_off + 14);
+ *p32 = GUINT32_TO_LE(crc);
+ p32 = (guint32 *) (zip->data + lfh_off + 18);
+ *p32 = GUINT32_TO_LE(csize_for_header);
+ }
+
+ rspamd_zip_write_central_header(cd, f->name, ver_needed, gp_flags, method, f->mtime, crc,
+ csize_for_header,
+ (guint32) f->len,
+ lfh_off, f->mode, extra_len);
+ if (use_aes) {
+ rspamd_zip_write_extra_aes(cd, aes_vendor_ver, aes_strength, actual_method);
+ }
+
+ guint64 logged_csize = (guint64) csize_for_header;
+ msg_debug_archive_taskless("zip: added entry '%s' (usize=%L, csize=%L, method=%s)",
+ f->name, (int64_t) f->len, (int64_t) logged_csize,
+ method == 0 ? "store" : "deflate");
+ }
+
+ /* Central directory start */
+ guint32 cd_start = zip->len;
+ g_byte_array_append(zip, cd->data, cd->len);
+ guint32 cd_size = cd->len;
+ g_byte_array_free(cd, TRUE);
+
+ /* EOCD */
+ rspamd_ba_append_u32le(zip, 0x06054b50);
+ /* disk numbers */
+ rspamd_ba_append_u16le(zip, 0);
+ rspamd_ba_append_u16le(zip, 0);
+ /* total entries on this disk / total entries */
+ rspamd_ba_append_u16le(zip, (guint16) nfiles);
+ rspamd_ba_append_u16le(zip, (guint16) nfiles);
+ /* size of central directory */
+ rspamd_ba_append_u32le(zip, cd_size);
+ /* offset of central directory */
+ rspamd_ba_append_u32le(zip, cd_start);
+ /* zip comment length */
+ rspamd_ba_append_u16le(zip, 0);
+
+ msg_debug_archive_taskless("zip: created archive (%L bytes)", (int64_t) zip->len);
+ return zip;
+}
+
+GByteArray *
+rspamd_archives_encrypt_aes256_cbc(const unsigned char *in,
+ gsize inlen,
+ const char *password,
+ GError **err)
+{
+#ifndef HAVE_OPENSSL
+ (void) in;
+ (void) inlen;
+ (void) password;
+ GQuark q = rspamd_archives_err_quark();
+ g_set_error(err, q, ENOTSUP, "OpenSSL is not available");
+ return NULL;
+#else
+ GQuark q = rspamd_archives_err_quark();
+ unsigned char salt[16];
+ unsigned char iv[16];
+ unsigned char key[32];
+ const int kdf_iters = 100000;
+ GByteArray *out = NULL;
+ EVP_CIPHER_CTX *ctx = NULL;
+
+ if (password == NULL || *password == '\0') {
+ g_set_error(err, q, EINVAL, "empty password");
+ return NULL;
+ }
+
+ if (RAND_bytes(salt, sizeof(salt)) != 1 || RAND_bytes(iv, sizeof(iv)) != 1) {
+ g_set_error(err, q, EIO, "cannot generate random salt/iv: %s", ERR_error_string(ERR_get_error(), NULL));
+ return NULL;
+ }
+
+ if (PKCS5_PBKDF2_HMAC(password, (int) strlen(password), salt, (int) sizeof(salt),
+ kdf_iters, EVP_sha256(), (int) sizeof(key), key) != 1) {
+ g_set_error(err, q, EIO, "PBKDF2 failed: %s", ERR_error_string(ERR_get_error(), NULL));
+ return NULL;
+ }
+
+ ctx = EVP_CIPHER_CTX_new();
+ if (ctx == NULL) {
+ g_set_error(err, q, ENOMEM, "cannot alloc cipher ctx");
+ rspamd_explicit_memzero(key, sizeof(key));
+ return NULL;
+ }
+
+ if (EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv) != 1) {
+ g_set_error(err, q, EIO, "cipher init failed: %s", ERR_error_string(ERR_get_error(), NULL));
+ EVP_CIPHER_CTX_free(ctx);
+ rspamd_explicit_memzero(key, sizeof(key));
+ return NULL;
+ }
+
+ /* Prepare output: magic + salt + iv + ciphertext; write directly into GByteArray */
+ const char magic[8] = {'R', 'Z', 'A', 'E', '0', '0', '0', '1'};
+ out = g_byte_array_sized_new(8 + sizeof(salt) + sizeof(iv) + inlen + 32);
+ g_byte_array_append(out, (const guint8 *) magic, sizeof(magic));
+ g_byte_array_append(out, salt, sizeof(salt));
+ g_byte_array_append(out, iv, sizeof(iv));
+
+ gsize before = out->len;
+ g_byte_array_set_size(out, out->len + inlen + EVP_CIPHER_block_size(EVP_aes_256_cbc()));
+ unsigned char *cptr = out->data + before;
+ int outlen = 0;
+
+ if (EVP_EncryptUpdate(ctx, cptr, &outlen, in, (int) inlen) != 1) {
+ g_set_error(err, q, EIO, "encrypt update failed: %s", ERR_error_string(ERR_get_error(), NULL));
+ EVP_CIPHER_CTX_free(ctx);
+ rspamd_explicit_memzero(key, sizeof(key));
+ g_byte_array_set_size(out, before);
+ g_byte_array_free(out, TRUE);
+ return NULL;
+ }
+
+ int fin = 0;
+ if (EVP_EncryptFinal_ex(ctx, cptr + outlen, &fin) != 1) {
+ g_set_error(err, q, EIO, "encrypt final failed: %s", ERR_error_string(ERR_get_error(), NULL));
+ EVP_CIPHER_CTX_free(ctx);
+ rspamd_explicit_memzero(key, sizeof(key));
+ g_byte_array_set_size(out, before);
+ g_byte_array_free(out, TRUE);
+ return NULL;
+ }
+
+ g_byte_array_set_size(out, before + outlen + fin);
+ EVP_CIPHER_CTX_free(ctx);
+ rspamd_explicit_memzero(key, sizeof(key));
+
+ msg_info("zip: AES-256-CBC envelope created (PBKDF2-SHA256 iters=%d)", kdf_iters);
+ return out;
+#endif
+}
+
static bool
rspamd_archive_file_try_utf(struct rspamd_task *task,
struct rspamd_archive *arch,