From: Vsevolod Stakhov Date: Mon, 22 Sep 2025 20:31:46 +0000 (+0100) Subject: [Project] Add encrypted zip support to archives module X-Git-Tag: 3.13.1~16^2~5 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8d23011c9f8a92cfc7f3ab66b0d5890bb09d5daf;p=thirdparty%2Frspamd.git [Project] Add encrypted zip support to archives module This commit adds encrypted zip support to the archives module. It adds a new function to the archives module that encrypts a zip file using AES-256-CBC. It also adds a new function to the archives module that decrypts a zip file using AES-256-CBC. --- diff --git a/src/libmime/archives.c b/src/libmime/archives.c index b02a659e2e..736f981315 100644 --- a/src/libmime/archives.c +++ b/src/libmime/archives.c @@ -26,14 +26,36 @@ #include #include +#include +#ifdef HAVE_OPENSSL +#include +#include +#include +#include +#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) { @@ -54,6 +76,627 @@ 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, diff --git a/src/libmime/archives.h b/src/libmime/archives.h index cafa060477..8bc75f2172 100644 --- a/src/libmime/archives.h +++ b/src/libmime/archives.h @@ -55,6 +55,37 @@ struct rspamd_archive { GPtrArray *files; /* Array of struct rspamd_archive_file */ }; +/* Writer API */ +struct rspamd_zip_file_spec { + const char *name; /* UTF-8 relative path */ + const unsigned char *data; /* file content */ + gsize len; /* content length */ + /* Optional attrs */ + time_t mtime; /* 0 means now */ + guint32 mode; /* UNIX perm bits; 0 means 0644 */ +}; + +/** + * Create a ZIP archive in-memory from provided files (DEFLATE compression) + * If password is non-NULL, the ZIP is created normally and then encrypted as a whole + * using AES-256-CBC with PBKDF2-HMAC-SHA256 and a random salt/IV. The result format is: + * [ 'RZAE0001' (8 bytes) | salt (16 bytes) | iv (16 bytes) | ciphertext ] + * Returns newly allocated GByteArray on success, NULL on error and sets err + */ +GByteArray *rspamd_archives_zip_write(const struct rspamd_zip_file_spec *files, + gsize nfiles, + const char *password, + GError **err); + +/** + * AES-256-CBC encrypts arbitrary data buffer using PBKDF2-HMAC-SHA256 derived key. + * Output format: [ 'RZAE0001' | salt(16) | iv(16) | ciphertext ] + */ +GByteArray *rspamd_archives_encrypt_aes256_cbc(const unsigned char *in, + gsize inlen, + const char *password, + GError **err); + /** * Process archives from a worker task */