From: Vsevolod Stakhov Date: Tue, 23 Sep 2025 19:34:38 +0000 (+0100) Subject: [Project] Switch to libarchive for encrypted zip archives X-Git-Tag: 3.13.1~16^2 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=1e4089b1f1894db62e96462f4ddee54fa4897238;p=thirdparty%2Frspamd.git [Project] Switch to libarchive for encrypted zip archives --- diff --git a/src/libmime/archives.c b/src/libmime/archives.c index a1cf7b3699..983857b503 100644 --- a/src/libmime/archives.c +++ b/src/libmime/archives.c @@ -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, <); - - 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)); -} - -/* --- 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, diff --git a/src/libmime/archives.h b/src/libmime/archives.h index c5c8e29fd9..6e05cd53fa 100644 --- a/src/libmime/archives.h +++ b/src/libmime/archives.h @@ -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 diff --git a/src/lua/lua_archive.c b/src/lua/lua_archive.c index 8f76588e1f..43699b153b 100644 --- a/src/lua/lua_archive.c +++ b/src/lua/lua_archive.c @@ -16,7 +16,6 @@ #include "lua_common.h" #include "unix-std.h" -#include "libmime/archives.h" #include #include @@ -29,15 +28,41 @@ /*** * @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 diff --git a/test/lua/unit/archive.lua b/test/lua/unit/archive.lua index f12ef91b10..99a73e832b 100644 --- a/test/lua/unit/archive.lua +++ b/test/lua/unit/archive.lua @@ -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" },