]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Project] Switch to libarchive for encrypted zip archives 5628/head
authorVsevolod Stakhov <vsevolod@rspamd.com>
Tue, 23 Sep 2025 19:34:38 +0000 (20:34 +0100)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Tue, 23 Sep 2025 19:34:38 +0000 (20:34 +0100)
src/libmime/archives.c
src/libmime/archives.h
src/lua/lua_archive.c
test/lua/unit/archive.lua

index a1cf7b36999f471bfdf0d7e5e1ca96d6e671ae19..983857b5034822aa7f474b628bfa64a8b3356f84 100644 (file)
@@ -71,585 +71,6 @@ 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, &lt);
-
-       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, &lt);
-
-       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));
-}
-
-/* --- 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 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])
-{
-       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 *
-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_zipcrypto = (password != NULL && *password != '\0');
-
-               /* actual method will be decided after deflate; default is deflate */
-
-               guint16 extra_len = 0;
-               guint32 csize_for_header = 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,
-                                                                         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);
-                       }
-                       else {
-                               /* high 2 bytes of CRC32 of plaintext */
-                               hdr[10] = (guint8) ((crc >> 16) & 0xff);
-                               hdr[11] = (guint8) ((crc >> 24) & 0xff);
-                       }
-                       /* 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 */
-
-                       /* 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) {
-                               uLong bound = deflateBound(&zst, (uLong) f->len);
-                               deflateEnd(&zst);
-
-                               /* 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;
-                                               }
-                                       }
-                               }
-                       }
-                       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;
-                               /* 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 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);
-                       }
-                       /* 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 */
-                       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);
-                               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);
-                       }
-                       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);
-               }
-
-               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);
-               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_str);
-       }
-
-       /* 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, 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;
-}
-
-/* removed obsolete whole-archive AES-256-CBC function */
 
 static bool
 rspamd_archive_file_try_utf(struct rspamd_task *task,
index c5c8e29fd98c215e2e5a4a6b8272ad028102b355..6e05cd53fae7e7b421c551bb297dae728f51a242 100644 (file)
@@ -55,29 +55,7 @@ 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 an in-memory ZIP archive from provided files.
- * - Uses DEFLATE (method 8) or STORE (method 0) per entry, depending on gain.
- * - If 'password' is non-NULL, each entry is encrypted using WinZip AES (AE-2):
- *   method 99 + 0x9901 extra, PBKDF2-HMAC-SHA1(1000), AES-CTR, 10-byte HMAC-SHA1 tag.
- *   Interoperable with 7-Zip/WinZip/libarchive.
- * - UTF-8 filenames (GPBF bit 11) are used.
- * 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);
+/* Writer API removed: use libarchive for writing ZIP archives */
 
 /**
  * Process archives from a worker task
index 8f76588e1f794030cb7d41812d69bd3a01166165..43699b153be8d6be6abc3f6208ff619366eed44c 100644 (file)
@@ -16,7 +16,6 @@
 
 #include "lua_common.h"
 #include "unix-std.h"
-#include "libmime/archives.h"
 
 #include <archive.h>
 #include <archive_entry.h>
 /***
  * @function archive.pack(format, files[, options])
  * Packs a list of files into an in-memory archive using libarchive.
- * @param {string} format archive format name (e.g. "zip", "tar", "7zip", ...)
+ *
+ * @param {string} format archive format name (typical: "zip", "tar", "7zip")
  * @param {table} files array of tables: { name = string, content = string|rspamd_text, [mode|perms] = int, [mtime] = int }
- * @param {table} options optional table; `filters` can be string or array of strings (e.g. "gzip", "xz", "zstd")
+ * @param {table} options optional table configuring filters and format behavior
+ *  - filters: string or array of strings; compression filters to apply (e.g. "gzip", "xz", "zstd", "bzip2")
+ *  - password: string passphrase for encrypted formats (ZIP only here)
+ *  - format_options: table of format-specific options (alternatively, a nested table named after the format, e.g. options.zip = {...})
+ *
+ * ZIP-specific options (via options.format_options or options.zip):
+ *  - encryption: "traditional" (aka "zipcrypt"), "aes128", or "aes256"
+ *  - compression: "store" | "deflate"
+ *  - compression-level: integer 0..9 (0 implies "store")
+ *  - zip64: boolean (true to force Zip64; use with care)
+ *  - hdrcharset: character set name for filenames
+ *  - experimental, fakecrc32: booleans (testing only; not recommended for production)
+ *
+ * Notes:
+ *  - If options.password is set and encryption is omitted, you can specify it explicitly as shown below.
+ *  - For a complete list of libarchive ZIP options, consult libarchive documentation.
+ *
  * @return {text} archive bytes
- * @example
+ *
+ * @example -- Plain ZIP
  * local blob = archive.pack("zip", {
- *   { name = "test.txt", content = "Hello" },
- *   { name = "dir/readme.md", content = "# Readme" },
- * }, { filters = "zstd" })
+ *   { name = "a.txt", content = "Hello" },
+ * })
+ *
+ * @example -- ZIP with ZipCrypto (traditional)
+ * local blob = archive.pack("zip", files, { password = "secret", zip = { encryption = "traditional" } })
+ *
+ * @example -- ZIP with AES-128
+ * local blob = archive.pack("zip", files, { password = "secret", format_options = { encryption = "aes128" } })
+ *
+ * @example -- TAR.GZ
+ * local blob = archive.pack("tar", files, { filters = "gzip" })
  */
 
 LUA_FUNCTION_DEF(archive, pack);
@@ -189,6 +214,96 @@ lua_archive_set_format(lua_State *L, struct archive *a, const char *fmt)
        lua_pushfstring(L, "unsupported format: %s", fmt ? fmt : "(nil)");
        return FALSE;
 }
+
+static gboolean
+lua_archive_set_format_options_table(lua_State *L, struct archive *a, const char *fmt, int idx)
+{
+       gboolean ok = TRUE;
+
+       if (!lua_istable(L, idx)) {
+               return ok;
+       }
+
+       lua_pushnil(L);
+
+       while (lua_next(L, idx)) {
+               const char *key = lua_tostring(L, -2);
+               const char *valstr = NULL;
+               char nb[64];
+
+               if (key) {
+                       int t = lua_type(L, -1);
+                       if (t == LUA_TSTRING) {
+                               valstr = lua_tostring(L, -1);
+                       }
+                       else if (t == LUA_TNUMBER) {
+                               rspamd_snprintf(nb, sizeof(nb), "%l", (long) lua_tointeger(L, -1));
+                               valstr = nb;
+                       }
+                       else if (t == LUA_TBOOLEAN) {
+                               valstr = lua_toboolean(L, -1) ? "1" : "0";
+                       }
+
+                       if (valstr) {
+                               int r = archive_write_set_format_option(a, fmt, key, valstr);
+                               if (r != ARCHIVE_OK && r != ARCHIVE_WARN) {
+                                       ok = FALSE;
+                                       lua_pop(L, 1); /* value */
+                                       break;
+                               }
+                       }
+               }
+
+               lua_pop(L, 1); /* value */
+       }
+
+       return ok;
+}
+
+static gboolean
+lua_archive_add_format_options(lua_State *L, struct archive *a, const char *fmt, int opts_idx)
+{
+       gboolean ok = TRUE;
+
+       if (opts_idx <= 0 || !lua_istable(L, opts_idx)) {
+               return ok;
+       }
+
+       /* Optional password */
+       lua_getfield(L, opts_idx, "password");
+       if (lua_isstring(L, -1)) {
+               const char *pw = lua_tostring(L, -1);
+               if (pw && *pw) {
+                       int r = archive_write_set_passphrase(a, pw);
+                       if (r != ARCHIVE_OK && r != ARCHIVE_WARN) {
+                               ok = FALSE;
+                       }
+               }
+       }
+       lua_pop(L, 1);
+
+       /* Generic format_options table */
+       lua_getfield(L, opts_idx, "format_options");
+       if (!lua_isnil(L, -1)) {
+               if (!lua_archive_set_format_options_table(L, a, fmt, lua_gettop(L))) {
+                       ok = FALSE;
+               }
+       }
+       lua_pop(L, 1);
+
+       /* Also support nested table named after format (e.g. options.zip) */
+       if (fmt) {
+               lua_getfield(L, opts_idx, fmt);
+               if (!lua_isnil(L, -1)) {
+                       if (!lua_archive_set_format_options_table(L, a, fmt, lua_gettop(L))) {
+                               ok = FALSE;
+                       }
+               }
+               lua_pop(L, 1);
+       }
+
+       return ok;
+}
 static int
 lua_archive_zip(lua_State *L)
 {
@@ -204,9 +319,10 @@ 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 with traditional ZipCrypto (PKWARE).
- * - Widely compatible (Info-ZIP/unzip/7-Zip/WinZip), but not cryptographically strong
+ * Convenience helper for creating ZIP archives.
+ * - If password is provided and non-empty, uses libarchive with ZIP traditional encryption (ZipCrypto).
+ * - If password is nil/empty, produces a plain (unencrypted) ZIP.
+ * - For AES encryption, prefer archive.pack("zip", files, { password = "...", zip = { encryption = "aes128"|"aes256" } }).
  * @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
@@ -215,7 +331,8 @@ static int
 lua_archive_zip_encrypt(lua_State *L)
 {
        LUA_TRACE_POINT;
-       luaL_checktype(L, 1, LUA_TTABLE);
+       /* Re-route to libarchive packer with traditional encryption */
+       luaL_checktype(L, 1, LUA_TTABLE); /* files */
        const char *password = NULL;
        if (lua_gettop(L) >= 2 && !lua_isnil(L, 2)) {
                if (lua_type(L, 2) == LUA_TSTRING) {
@@ -225,108 +342,30 @@ lua_archive_zip_encrypt(lua_State *L)
                        return luaL_error(L, "invalid password (string expected)");
                }
        }
-       GArray *specs = g_array_sized_new(FALSE, FALSE, sizeof(struct rspamd_zip_file_spec), 8);
-       GError *err = NULL;
 
-       /* Iterate files array */
-       lua_pushnil(L);
-
-       while (lua_next(L, 1)) {
-               if (!lua_istable(L, -1)) {
-                       g_array_free(specs, TRUE);
-                       return luaL_error(L, "invalid file entry (expected table)");
-               }
-
-               int item_idx = lua_gettop(L);
-               const char *name = NULL;
-               const char *sdata = NULL;
-               size_t slen = 0;
-               time_t mtime = (time_t) 0;
-               guint32 mode = 0644;
-
-               lua_getfield(L, item_idx, "name");
-               name = lua_tostring(L, -1);
-               if (name == NULL || *name == '\0') {
-                       lua_pop(L, 2);
-                       g_array_free(specs, TRUE);
-                       return luaL_error(L, "invalid file entry (missing name)");
-               }
-               char *dupname = g_strdup(name);
-               lua_pop(L, 1);
-
-               lua_getfield(L, item_idx, "content");
-               struct rspamd_lua_text *t = NULL;
-               if ((t = lua_check_text_or_string(L, -1)) != NULL) {
-                       sdata = (const char *) t->start;
-                       slen = t->len;
-               }
-               else if (lua_isstring(L, -1)) {
-                       sdata = lua_tolstring(L, -1, &slen);
-               }
-               else {
-                       lua_pop(L, 2);
-                       g_free(dupname);
-                       g_array_free(specs, TRUE);
-                       return luaL_error(L, "invalid file entry (missing content)");
-               }
-               unsigned char *dupdata = NULL;
-               if (slen > 0) {
-                       dupdata = g_malloc(slen);
-                       memcpy(dupdata, sdata, slen);
-               }
-               lua_pop(L, 1);
-
-               lua_getfield(L, item_idx, "mode");
-               if (lua_isnumber(L, -1)) {
-                       mode = (guint32) lua_tointeger(L, -1);
-               }
-               lua_pop(L, 1);
-               lua_getfield(L, item_idx, "perms");
-               if (lua_isnumber(L, -1)) {
-                       mode = (guint32) lua_tointeger(L, -1);
-               }
-               lua_pop(L, 1);
-
-               lua_getfield(L, item_idx, "mtime");
-               if (lua_isnumber(L, -1)) {
-                       mtime = (time_t) lua_tointeger(L, -1);
-               }
-               lua_pop(L, 1);
-
-               struct rspamd_zip_file_spec s;
-               s.name = dupname;
-               s.data = dupdata;
-               s.len = (gsize) slen;
-               s.mtime = mtime;
-               s.mode = mode;
-               g_array_append_val(specs, s);
-
-               lua_pop(L, 1);
+       /* Build args: ["zip", files, options] */
+       lua_settop(L, 1); /* keep only files */
+       if (password && *password) {
+               lua_newtable(L); /* options */
+               lua_pushstring(L, "password");
+               lua_pushstring(L, password);
+               lua_settable(L, -3);
+               /* options.zip = { encryption = "traditional" } */
+               lua_pushstring(L, "zip");
+               lua_newtable(L);
+               lua_pushstring(L, "encryption");
+               lua_pushstring(L, "traditional");
+               lua_settable(L, -3);
+               lua_settable(L, -3); /* options.zip = {...} */
        }
-
-       GByteArray *ba = rspamd_archives_zip_write((const struct rspamd_zip_file_spec *) specs->data,
-                                                                                          specs->len,
-                                                                                          password,
-                                                                                          &err);
-
-       for (guint i = 0; i < specs->len; i++) {
-               struct rspamd_zip_file_spec *s = &g_array_index(specs, struct rspamd_zip_file_spec, i);
-               if (s->name) g_free((gpointer) s->name);
-               if (s->data) g_free((gpointer) s->data);
+       else {
+               lua_pushnil(L); /* no options => plain ZIP */
        }
-       g_array_free(specs, TRUE);
 
-       if (ba == NULL) {
-               const char *emsg = (err && err->message) ? err->message : "zip encryption failed";
-               if (err) g_error_free(err);
-               return luaL_error(L, "%s", emsg);
-       }
+       lua_pushstring(L, "zip");
+       lua_insert(L, 1); /* fmt at 1, files at 2, options/nil at 3 */
 
-       size_t outlen = ba->len;
-       guint8 *outdata = g_byte_array_free(ba, FALSE);
-       struct rspamd_lua_text *txt = lua_new_text(L, (const char *) outdata, outlen, FALSE);
-       txt->flags |= RSPAMD_TEXT_FLAG_OWN;
-       return 1;
+       return lua_archive_pack(L);
 }
 
 static int
@@ -534,10 +573,18 @@ lua_archive_pack(lua_State *L)
                return luaL_error(L, "%s", lua_tostring(L, -1));
        }
 
-       /* Options (filters, etc.) at index 3 */
+       /* Options (filters, format options, password) at index 3 */
        if (!lua_archive_add_filters(L, a, 3)) {
+               lua_pushstring(L, "cannot set compression filter(s)");
                archive_write_free(a);
-               return luaL_error(L, "cannot set compression filter(s)");
+               return lua_error(L);
+       }
+
+       if (!lua_archive_add_format_options(L, a, fmt, 3)) {
+               const char *aerr = archive_error_string(a);
+               lua_pushfstring(L, "cannot set format options: %s", aerr ? aerr : "unknown error");
+               archive_write_free(a);
+               return lua_error(L);
        }
 
        wctx.buf = g_byte_array_new();
@@ -613,7 +660,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 ZipCrypto)
+ * @param {string} password optional passphrase for encrypted archives (ZIP: ZipCrypto/AES)
  * @return {table} array of files: { name = string, content = text } (non-regular entries are skipped)
  */
 static int
index f12ef91b10ff90dd8640c17e68d14d916be16ae3..99a73e832b5ce26aac373238b6333e578d56c399 100644 (file)
@@ -74,6 +74,44 @@ context("Lua archive bindings", function()
     assert_equal(ok, false)
   end)
 
+  test("pack zip with AES-128 via libarchive roundtrip", function()
+    local files = {
+      { name = "dir/x.txt", content = "secret" },
+      { name = "y.bin",     content = rspamd_text.fromstring("\001\002\003") },
+    }
+    local opts = { password = "testpass123", format_options = { encryption = "aes128" } }
+    local ok_pack, blob_or_err = pcall(function()
+      return archive.pack("zip", files, opts)
+    end)
+    -- If libarchive lacks AES write support, skip quietly
+    if not ok_pack then return end
+    local blob = blob_or_err
+    assert_equal(type(blob), "userdata")
+    local out = archive.unpack(blob, "zip", opts.password)
+    assert_equal(#out, 2)
+    local names = {}
+    for _, f in ipairs(out) do names[f.name] = f.content end
+    assert_rspamd_eq({ actual = names["dir/x.txt"], expect = rspamd_text.fromstring("secret") })
+    assert_rspamd_eq({ actual = names["y.bin"], expect = rspamd_text.fromstring("\001\002\003") })
+  end)
+
+  test("pack zip with AES-256 via libarchive wrong password fails", function()
+    local files = {
+      { name = "a.txt", content = "Hello" },
+    }
+    local opts = { password = "goodpass", zip = { encryption = "aes256" } }
+    local ok_pack, blob_or_err = pcall(function()
+      return archive.pack("zip", files, opts)
+    end)
+    if not ok_pack then return end
+    local blob = blob_or_err
+    assert_equal(type(blob), "userdata")
+    local ok, err = pcall(function()
+      archive.unpack(blob, "zip", "badpass")
+    end)
+    assert_equal(ok, false)
+  end)
+
   test("tar/untar helpers roundtrip (no compression)", function()
     local files = {
       { name = "x.txt", content = "X" },