]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Project] Switch to ZipCrypto for encrypted zip archives (AES is not supported by...
authorVsevolod Stakhov <vsevolod@rspamd.com>
Tue, 23 Sep 2025 17:18:03 +0000 (18:18 +0100)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Tue, 23 Sep 2025 17:18:03 +0000 (18:18 +0100)
src/libmime/archives.c
src/lua/lua_archive.c
test/lua/unit/archive.lua

index f63bde4d7dd43487090be96ed3ce2760300fbad6..a1cf7b36999f471bfdf0d7e5e1ca96d6e671ae19 100644 (file)
 #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
+#include "ottery.h"
 
 #define msg_debug_archive(...) rspamd_conditional_debug_fast(NULL, NULL,                                                 \
                                                                                                                         rspamd_archive_log_id, "archive", task->task_pool->tag.uid, \
@@ -289,24 +284,81 @@ rspamd_zip_write_central_header(GByteArray *cd,
        g_byte_array_append(cd, (const guint8 *) name, strlen(name));
 }
 
-#define ZIP_AES_EXTRA_ID 0x9901
+/* --- ZipCrypto (PKWARE traditional) helpers --- */
+static const guint32 rspamd_zip_crc32_tab[256] = {
+       0x00000000U, 0x77073096U, 0xEE0E612CU, 0x990951BAU, 0x076DC419U, 0x706AF48FU, 0xE963A535U, 0x9E6495A3U,
+       0x0EDB8832U, 0x79DCB8A4U, 0xE0D5E91EU, 0x97D2D988U, 0x09B64C2BU, 0x7EB17CBDU, 0xE7B82D07U, 0x90BF1D91U,
+       0x1DB71064U, 0x6AB020F2U, 0xF3B97148U, 0x84BE41DEU, 0x1ADAD47DU, 0x6DDDE4EBU, 0xF4D4B551U, 0x83D385C7U,
+       0x136C9856U, 0x646BA8C0U, 0xFD62F97AU, 0x8A65C9ECU, 0x14015C4FU, 0x63066CD9U, 0xFA0F3D63U, 0x8D080DF5U,
+       0x3B6E20C8U, 0x4C69105EU, 0xD56041E4U, 0xA2677172U, 0x3C03E4D1U, 0x4B04D447U, 0xD20D85FDU, 0xA50AB56BU,
+       0x35B5A8FAU, 0x42B2986CU, 0xDBBBC9D6U, 0xACBCF940U, 0x32D86CE3U, 0x45DF5C75U, 0xDCD60DCFU, 0xABD13D59U,
+       0x26D930ACU, 0x51DE003AU, 0xC8D75180U, 0xBFD06116U, 0x21B4F4B5U, 0x56B3C423U, 0xCFBA9599U, 0xB8BDA50FU,
+       0x2802B89EU, 0x5F058808U, 0xC60CD9B2U, 0xB10BE924U, 0x2F6F7C87U, 0x58684C11U, 0xC1611DABU, 0xB6662D3DU,
+       0x76DC4190U, 0x01DB7106U, 0x98D220BCU, 0xEFD5102AU, 0x71B18589U, 0x06B6B51FU, 0x9FBFE4A5U, 0xE8B8D433U,
+       0x7807C9A2U, 0x0F00F934U, 0x9609A88EU, 0xE10E9818U, 0x7F6A0DBBU, 0x086D3D2DU, 0x91646C97U, 0xE6635C01U,
+       0x6B6B51F4U, 0x1C6C6162U, 0x856530D8U, 0xF262004EU, 0x6C0695EDU, 0x1B01A57BU, 0x8208F4C1U, 0xF50FC457U,
+       0x65B0D9C6U, 0x12B7E950U, 0x8BBEB8EAU, 0xFCB9887CU, 0x62DD1DDFU, 0x15DA2D49U, 0x8CD37CF3U, 0xFBD44C65U,
+       0x4DB26158U, 0x3AB551CEU, 0xA3BC0074U, 0xD4BB30E2U, 0x4ADFA541U, 0x3DD895D7U, 0xA4D1C46DU, 0xD3D6F4FBU,
+       0x4369E96AU, 0x346ED9FCU, 0xAD678846U, 0xDA60B8D0U, 0x44042D73U, 0x33031DE5U, 0xAA0A4C5FU, 0xDD0D7CC9U,
+       0x5005713CU, 0x270241AAU, 0xBE0B1010U, 0xC90C2086U, 0x5768B525U, 0x206F85B3U, 0xB966D409U, 0xCE61E49FU,
+       0x5EDEF90EU, 0x29D9C998U, 0xB0D09822U, 0xC7D7A8B4U, 0x59B33D17U, 0x2EB40D81U, 0xB7BD5C3BU, 0xC0BA6CADU,
+       0xEDB88320U, 0x9ABFB3B6U, 0x03B6E20CU, 0x74B1D29AU, 0xEAD54739U, 0x9DD277AFU, 0x04DB2615U, 0x73DC1683U,
+       0xE3630B12U, 0x94643B84U, 0x0D6D6A3EU, 0x7A6A5AA8U, 0xE40ECF0BU, 0x9309FF9DU, 0x0A00AE27U, 0x7D079EB1U,
+       0xF00F9344U, 0x8708A3D2U, 0x1E01F268U, 0x6906C2FEU, 0xF762575DU, 0x806567CBU, 0x196C3671U, 0x6E6B06E7U,
+       0xFED41B76U, 0x89D32BE0U, 0x10DA7A5AU, 0x67DD4ACCU, 0xF9B9DF6FU, 0x8EBEEFF9U, 0x17B7BE43U, 0x60B08ED5U,
+       0xD6D6A3E8U, 0xA1D1937EU, 0x38D8C2C4U, 0x4FDFF252U, 0xD1BB67F1U, 0xA6BC5767U, 0x3FB506DDU, 0x48B2364BU,
+       0xD80D2BDAU, 0xAF0A1B4CU, 0x36034AF6U, 0x41047A60U, 0xDF60EFC3U, 0xA867DF55U, 0x316E8EEFU, 0x4669BE79U,
+       0xCB61B38CU, 0xBC66831AU, 0x256FD2A0U, 0x5268E236U, 0xCC0C7795U, 0xBB0B4703U, 0x220216B9U, 0x5505262FU,
+       0xC5BA3BBEU, 0xB2BD0B28U, 0x2BB45A92U, 0x5CB36A04U, 0xC2D7FFA7U, 0xB5D0CF31U, 0x2CD99E8BU, 0x5BDEAE1DU,
+       0x9B64C2B0U, 0xEC63F226U, 0x756AA39CU, 0x026D930AU, 0x9C0906A9U, 0xEB0E363FU, 0x72076785U, 0x05005713U,
+       0x95BF4A82U, 0xE2B87A14U, 0x7BB12BAEU, 0x0CB61B38U, 0x92D28E9BU, 0xE5D5BE0DU, 0x7CDCEFB7U, 0x0BDBDF21U,
+       0x86D3D2D4U, 0xF1D4E242U, 0x68DDB3F8U, 0x1FDA836EU, 0x81BE16CDU, 0xF6B9265BU, 0x6FB077E1U, 0x18B74777U,
+       0x88085AE6U, 0xFF0F6A70U, 0x66063BCAU, 0x11010B5CU, 0x8F659EFFU, 0xF862AE69U, 0x616BFFD3U, 0x166CCF45U,
+       0xA00AE278U, 0xD70DD2EEU, 0x4E048354U, 0x3903B3C2U, 0xA7672661U, 0xD06016F7U, 0x4969474DU, 0x3E6E77DBU,
+       0xAED16A4AU, 0xD9D65ADCU, 0x40DF0B66U, 0x37D83BF0U, 0xA9BCAE53U, 0xDEBB9EC5U, 0x47B2CF7FU, 0x30B5FFE9U,
+       0xBDBDF21CU, 0xCABAC28AU, 0x53B39330U, 0x24B4A3A6U, 0xBAD03605U, 0xCDD70693U, 0x54DE5729U, 0x23D967BFU,
+       0xB3667A2EU, 0xC4614AB8U, 0x5D681B02U, 0x2A6F2B94U, 0xB40BBE37U, 0xC30C8EA1U, 0x5A05DF1BU, 0x2D02EF8DU};
+
+static inline guint32
+rspamd_zip_crc32_update(guint32 crc, guint8 c)
+{
+       return rspamd_zip_crc32_tab[(crc ^ c) & 0xff] ^ (crc >> 8);
+}
+static inline void
+rspamd_zipcrypto_init_keys(guint32 keys[3])
+{
+       keys[0] = 0x12345678UL;
+       keys[1] = 0x23456789UL;
+       keys[2] = 0x34567890UL;
+}
 
-static void
-rspamd_zip_write_extra_aes(GByteArray *ba, guint16 vendor_version, guint8 strength, guint16 actual_method)
+static inline void
+rspamd_zipcrypto_update_keys(guint32 keys[3], guint8 c)
+{
+       keys[0] = rspamd_zip_crc32_update(keys[0], c);
+       keys[1] = (keys[1] + (keys[0] & 0xff));
+       keys[1] = keys[1] * 134775813UL + 1;
+       guint8 t = (keys[1] >> 24) & 0xff;
+       keys[2] = rspamd_zip_crc32_update(keys[2], t);
+}
+
+static inline guint8
+rspamd_zipcrypto_crypt_byte(const guint32 keys[3])
 {
-       /* 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);
+       guint16 t = (guint16) ((keys[2] & 0xffff) | 2);
+       return (guint8) (((t * (t ^ 1)) >> 8) & 0xff);
+}
+
+static inline void
+rspamd_zipcrypto_init_with_password(guint32 keys[3], const char *password)
+{
+       rspamd_zipcrypto_init_keys(keys);
+       if (password != NULL) {
+               const unsigned char *p = (const unsigned char *) password;
+               while (*p) {
+                       rspamd_zipcrypto_update_keys(keys, *p++);
+               }
+       }
 }
 
 GByteArray *
@@ -340,178 +392,147 @@ rspamd_archives_zip_write(const struct rspamd_zip_file_spec *files,
                guint16 method = 8;            /* deflate */
                guint16 gp_flags = (1u << 11); /* UTF-8 */
                guint16 ver_needed = 20;       /* default */
-               const gboolean use_aes = (password != NULL && *password != '\0');
+               const gboolean use_zipcrypto = (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;
+               gboolean use_descriptor = FALSE;
+               if (use_zipcrypto) {
+                       /* Traditional PKWARE ZipCrypto */
+                       gp_flags |= 1u;        /* encrypted */
+                       gp_flags |= (1u << 3); /* data descriptor present */
+                       use_descriptor = TRUE;
+                       /* method remains 8 or 0 depending on compression effectiveness */
+                       /* no extra field */
                }
 
                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;
+               rspamd_zip_write_local_header(zip, f->name, ver_needed, gp_flags, method, f->mtime,
+                                                                         use_descriptor ? 0 : crc,
+                                                                         use_descriptor ? 0 : csize_for_header,
+                                                                         use_descriptor ? 0 : (guint32) f->len,
+                                                                         extra_len);
+               msg_debug_archive_taskless("lfh: off=%d ver_needed=%d gp_flags=%d method=%d name_len=%d extra_len=%d",
+                                                                  (int) lfh_off, (int) ver_needed, (int) gp_flags, (int) method,
+                                                                  (int) strlen(f->name), (int) extra_len);
+               if (use_zipcrypto) {
+                       /* Prepare ZipCrypto keys */
+                       guint32 keys[3];
+                       rspamd_zipcrypto_init_with_password(keys, password);
+
+                       /* Build 12-byte encryption header */
+                       guint8 hdr[12];
+                       ottery_rand_bytes(hdr, sizeof(hdr));
+                       /* set verification bytes */
+                       if (use_descriptor) {
+                               /* when bit 3 is set, use MS-DOS time field */
+                               guint16 dos_t = rspamd_zip_time_dos(f->mtime);
+                               hdr[10] = (guint8) (dos_t & 0xff);
+                               hdr[11] = (guint8) ((dos_t >> 8) & 0xff);
                        }
-
-                       /* 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;
+                       else {
+                               /* high 2 bytes of CRC32 of plaintext */
+                               hdr[10] = (guint8) ((crc >> 16) & 0xff);
+                               hdr[11] = (guint8) ((crc >> 24) & 0xff);
                        }
-                       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;
+                       /* Encrypt header in place */
+                       for (guint i = 0; i < sizeof(hdr); i++) {
+                               guint8 k = rspamd_zipcrypto_crypt_byte(keys);
+                               guint8 c = hdr[i] ^ k;
+                               hdr[i] = c;
+                               /* update keys with header plaintext byte */
+                               rspamd_zipcrypto_update_keys(keys, (guint8) (c ^ k));
                        }
+                       g_byte_array_append(zip, hdr, sizeof(hdr));
+
+                       /* Now compress directly into zip buffer (in place) or store */
+                       gsize produced = 0;
+                       gboolean used_deflate = TRUE;
+                       guint32 data_off = zip->len; /* start of (plaintext) data before encryption */
 
-                       /* Deflate directly into zip and encrypt in-place */
+                       /* Try to reserve space by deflateBound and compress into zip */
                        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) {
+                       if (deflateInit2(&zst, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, MAX_MEM_LEVEL - 1, Z_DEFAULT_STRATEGY) == Z_OK) {
+                               uLong bound = deflateBound(&zst, (uLong) f->len);
                                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;
+
+                               /* Reserve space */
+                               g_byte_array_set_size(zip, data_off + bound);
+
+                               memset(&zst, 0, sizeof(zst));
+                               if (deflateInit2(&zst, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, MAX_MEM_LEVEL - 1, Z_DEFAULT_STRATEGY) != Z_OK) {
+                                       /* fallback to store */
+                                       used_deflate = FALSE;
+                               }
+                               else {
+                                       zst.next_in = (unsigned char *) f->data;
+                                       zst.avail_in = f->len;
+                                       zst.next_out = zip->data + data_off;
+                                       zst.avail_out = bound;
+                                       int rc = deflate(&zst, Z_FINISH);
+                                       if (rc != Z_STREAM_END && rc != Z_OK && rc != Z_BUF_ERROR) {
+                                               used_deflate = FALSE;
+                                               deflateEnd(&zst);
+                                       }
+                                       else {
+                                               produced = bound - zst.avail_out;
+                                               deflateEnd(&zst);
+                                               if (produced >= f->len) {
+                                                       used_deflate = FALSE;
+                                               }
+                                       }
+                               }
                        }
-                       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);
+                       else {
+                               used_deflate = FALSE;
+                       }
+
+                       if (!used_deflate) {
+                               /* Store: reset to data_off and copy original data */
+                               g_byte_array_set_size(zip, data_off);
+                               g_byte_array_set_size(zip, data_off + f->len);
+                               memcpy(zip->data + data_off, f->data, f->len);
                                produced = f->len;
-                               actual_method = 0;
+                               /* patch method in local header (offset +8) */
+                               guint16 *pm = (guint16 *) (zip->data + lfh_off + 8);
+                               method = 0;
+                               *pm = GUINT16_TO_LE(method);
                        }
 
-                       /* 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;
+                       /* Encrypt in place over zip->data[data_off .. data_off+produced) */
+                       for (gsize i = 0; i < produced; i++) {
+                               guint8 k = rspamd_zipcrypto_crypt_byte(keys);
+                               guint8 pt = zip->data[data_off + i];
+                               zip->data[data_off + i] = pt ^ k;
+                               rspamd_zipcrypto_update_keys(keys, pt);
                        }
-                       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(cd, TRUE);
-                       g_byte_array_free(zip, TRUE);
-                       g_set_error(err, q, ENOTSUP, "AES-CTR encryption requires OpenSSL");
-                       return NULL;
-#endif
+                       /* Shrink to actual size (if deflated) */
+                       g_byte_array_set_size(zip, data_off + produced);
+
+                       /* compressed size includes 12-byte header + encrypted data */
+                       csize_for_header = (guint32) (12 + produced);
+                       if (!use_descriptor) {
+                               /* 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);
+                               /* uncompressed size already set in LFH */
+                       }
+                       else {
+                               /* append data descriptor with signature */
+                               rspamd_ba_append_u32le(zip, 0x08074b50);
+                               rspamd_ba_append_u32le(zip, crc);
+                               rspamd_ba_append_u32le(zip, csize_for_header);
+                               rspamd_ba_append_u32le(zip, (guint32) f->len);
+                       }
+
+                       msg_debug_archive_taskless("zip-zipcrypto: added entry '%s' (usize=%L, csize=%L, method=%s)",
+                                                                          f->name, (int64_t) f->len, (int64_t) csize_for_header,
+                                                                          used_deflate ? "deflate+zipcrypto" : "store+zipcrypto");
                }
                else {
                        /* Not encrypted: deflate directly into zip, fallback to store */
@@ -553,6 +574,8 @@ rspamd_archives_zip_write(const struct rspamd_zip_file_spec *files,
                                /* patch method in local header (offset +8) */
                                guint16 *pm = (guint16 *) (zip->data + lfh_off + 8);
                                *pm = GUINT16_TO_LE(method);
+                               msg_debug_archive_taskless("zip: fallback to store (no encryption) - deflated=%L, original=%L",
+                                                                                  (int64_t) (bound - zst.avail_out), (int64_t) f->len);
                        }
                        else {
                                g_byte_array_set_size(zip, off + produced);
@@ -565,18 +588,24 @@ rspamd_archives_zip_write(const struct rspamd_zip_file_spec *files,
                        *p32 = GUINT32_TO_LE(csize_for_header);
                }
 
+               guint32 cd_off = cd->len;
                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);
-               }
+               msg_debug_archive_taskless("cd_entry: off=%d lfh_off=%d name=%s csize=%d usize=%d",
+                                                                  (int) cd_off, (int) lfh_off, f->name, (int) csize_for_header, (int) f->len);
+               msg_debug_archive_taskless("cd: ver_needed=%d gp_flags=%d method=%d csize=%L usize=%L",
+                                                                  (int) ver_needed, (int) gp_flags, (int) method,
+                                                                  (int64_t) csize_for_header, (int64_t) f->len);
 
                guint64 logged_csize = (guint64) csize_for_header;
+               const char *method_str;
+               method_str = (use_zipcrypto ? (method == 0 ? "store+zipcrypto" : "deflate+zipcrypto")
+                                                                       : (method == 0 ? "store" : "deflate"));
                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");
+                                                                  method_str);
        }
 
        /* Central directory start */
@@ -600,7 +629,23 @@ rspamd_archives_zip_write(const struct rspamd_zip_file_spec *files,
        /* zip comment length */
        rspamd_ba_append_u16le(zip, 0);
 
-       msg_debug_archive_taskless("zip: created archive (%L bytes)", (int64_t) zip->len);
+       msg_debug_archive_taskless("zip: created archive (%L bytes, cd_start=%d, cd_size=%d)",
+                                                          (int64_t) zip->len, (int) cd_start, (int) cd_size);
+
+       /* Debug: check archive structure */
+       if (zip->len >= 4) {
+               guint32 sig = GUINT32_FROM_LE(*(guint32 *) zip->data);
+               msg_debug_archive_taskless("zip: first 4 bytes = %xd (should be 4034b50 for PK\\003\\004)", sig);
+       }
+
+       /* Additional validation */
+       if (cd_start + cd_size + 22 != zip->len) {
+               msg_debug_archive_taskless("zip: WARNING - archive size mismatch: cd_start(%d) + cd_size(%d) + eocd(22) = %d, but zip->len = %d",
+                                                                  (int) cd_start, (int) cd_size, (int) (cd_start + cd_size + 22), (int) zip->len);
+       }
+
+       /* no debug dump */
+
        return zip;
 }
 
@@ -1611,7 +1656,7 @@ rspamd_7zip_read_pack_info(struct rspamd_task *task,
        while (p != NULL && p < end) {
                t = *p;
                SZ_SKIP_BYTES(1);
-               msg_debug_archive("7zip: read pack info %xc", t);
+               msg_debug_archive("7zip: read pack info %xd", t);
 
                switch (t) {
                case kSize:
@@ -1630,7 +1675,7 @@ rspamd_7zip_read_pack_info(struct rspamd_task *task,
                        break;
                default:
                        p = NULL;
-                       msg_debug_archive("bad 7zip type: %xc; %s", t, G_STRLOC);
+                       msg_debug_archive("bad 7zip type: %xd; %s", t, G_STRLOC);
                        goto end;
                        break;
                }
@@ -1776,7 +1821,7 @@ rspamd_7zip_read_coders_info(struct rspamd_task *task,
 
                t = *p;
                SZ_SKIP_BYTES(1);
-               msg_debug_archive("7zip: read coders info %xc", t);
+               msg_debug_archive("7zip: read coders info %xd", t);
 
                switch (t) {
                case kFolder:
@@ -1845,7 +1890,7 @@ rspamd_7zip_read_coders_info(struct rspamd_task *task,
                        break;
                default:
                        p = NULL;
-                       msg_debug_archive("bad 7zip type: %xc; %s", t, G_STRLOC);
+                       msg_debug_archive("bad 7zip type: %xd; %s", t, G_STRLOC);
                        goto end;
                        break;
                }
@@ -1907,7 +1952,7 @@ rspamd_7zip_read_substreams_info(struct rspamd_task *task,
                t = *p;
                SZ_SKIP_BYTES(1);
 
-               msg_debug_archive("7zip: read substream info %xc", t);
+               msg_debug_archive("7zip: read substream info %xd", t);
 
                switch (t) {
                case kNumUnPackStream:
@@ -1944,7 +1989,7 @@ rspamd_7zip_read_substreams_info(struct rspamd_task *task,
                        break;
                default:
                        p = NULL;
-                       msg_debug_archive("bad 7zip type: %xc; %s", t, G_STRLOC);
+                       msg_debug_archive("bad 7zip type: %xd; %s", t, G_STRLOC);
                        goto end;
                        break;
                }
@@ -1965,7 +2010,7 @@ rspamd_7zip_read_main_streams_info(struct rspamd_task *task,
        while (p != NULL && p < end) {
                t = *p;
                SZ_SKIP_BYTES(1);
-               msg_debug_archive("7zip: read main streams info %xc", t);
+               msg_debug_archive("7zip: read main streams info %xd", t);
 
                /*
                 *
@@ -2001,7 +2046,7 @@ rspamd_7zip_read_main_streams_info(struct rspamd_task *task,
                        break;
                default:
                        p = NULL;
-                       msg_debug_archive("bad 7zip type: %xc; %s", t, G_STRLOC);
+                       msg_debug_archive("bad 7zip type: %xd; %s", t, G_STRLOC);
                        goto end;
                        break;
                }
@@ -2105,7 +2150,7 @@ rspamd_7zip_read_files_info(struct rspamd_task *task,
                t = *p;
                SZ_SKIP_BYTES(1);
 
-               msg_debug_archive("7zip: read file data type %xc", t);
+               msg_debug_archive("7zip: read file data type %xd", t);
 
                if (t == kEnd) {
                        goto end;
@@ -2186,7 +2231,7 @@ rspamd_7zip_read_files_info(struct rspamd_task *task,
                        break;
                default:
                        p = NULL;
-                       msg_debug_archive("bad 7zip type: %xc; %s", t, G_STRLOC);
+                       msg_debug_archive("bad 7zip type: %xd; %s", t, G_STRLOC);
                        goto end;
                        break;
                }
@@ -2206,7 +2251,7 @@ rspamd_7zip_read_next_section(struct rspamd_task *task,
 
        SZ_SKIP_BYTES(1);
 
-       msg_debug_archive("7zip: read section %xc", t);
+       msg_debug_archive("7zip: read section %xd", t);
 
        switch (t) {
        case kHeader:
@@ -2272,7 +2317,7 @@ rspamd_7zip_read_next_section(struct rspamd_task *task,
                break;
        default:
                p = NULL;
-               msg_debug_archive("bad 7zip type: %xc; %s", t, G_STRLOC);
+               msg_debug_archive("bad 7zip type: %xd; %s", t, G_STRLOC);
                break;
        }
 
index 7752b549ea63a6f886c9db10ebaf1c194236b5f8..8f76588e1f794030cb7d41812d69bd3a01166165 100644 (file)
@@ -205,8 +205,8 @@ lua_archive_zip(lua_State *L)
 /***
  * @function archive.zip_encrypt(files[, password])
  * Create a ZIP archive in-memory using Rspamd ZIP writer.
- * If password is provided and non-empty, entries are encrypted via WinZip AES (AE-2).
- * - AES-256-CTR with HMAC-SHA1 (10-byte tag), interoperable with 7-Zip/WinZip/libarchive
+ * If password is provided and non-empty, entries are encrypted with traditional ZipCrypto (PKWARE).
+ * - Widely compatible (Info-ZIP/unzip/7-Zip/WinZip), but not cryptographically strong
  * @param {table} files array: { name = string, content = string|rspamd_text, [mode|perms] = int, [mtime] = int }
  * @param {string} password optional password string
  * @return {text} archive bytes
@@ -613,7 +613,7 @@ lua_archive_pack(lua_State *L)
  * Unpacks an archive from a Lua string (or rspamd_text) using libarchive.
  * @param {string|text} data archive contents
  * @param {string} format optional format name to restrict autodetection (e.g. "zip")
- * @param {string} password optional password for encrypted archives (e.g. ZIP AES)
+ * @param {string} password optional password for encrypted archives (e.g. ZIP ZipCrypto)
  * @return {table} array of files: { name = string, content = text } (non-regular entries are skipped)
  */
 static int
@@ -671,6 +671,11 @@ lua_archive_unpack(lua_State *L)
                return lua_error(L);
        }
 
+       /* Debug: check if archive has encrypted entries */
+       if (archive_read_has_encrypted_entries(a) > 0) {
+               /* encrypted entries detected */
+       }
+
        lua_newtable(L);
 
        struct archive_entry *ae;
index d52d14e577a92a8b00b3f2e935ee30e72403a4e0..f12ef91b10ff90dd8640c17e68d14d916be16ae3 100644 (file)
@@ -44,7 +44,7 @@ context("Lua archive bindings", function()
     assert_rspamd_eq({ actual = out[1].content, expect = rspamd_text.fromstring("Hello") })
   end)
 
-  test("zip_encrypt with password (AE-2) roundtrip via libarchive", function()
+  test("zip_encrypt with password (ZipCrypto) roundtrip via libarchive", function()
     local files = {
       { name = "dir/x.txt", content = "secret" },
       { name = "y.bin",     content = rspamd_text.fromstring("\001\002\003") },
@@ -52,7 +52,7 @@ context("Lua archive bindings", function()
     local pwd = "testpass123"
     local blob = archive.zip_encrypt(files, pwd)
     assert_equal(type(blob), "userdata")
-    -- libarchive can read AE-2, so unpack should succeed and yield the same files
+    -- libarchive can read ZipCrypto, so unpack should succeed and yield the same files
     local out = archive.unpack(blob, "zip", pwd)
     assert_equal(#out, 2)
     local names = {}