]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Project] Add Lua bindings for encrypted zip support
authorVsevolod Stakhov <vsevolod@rspamd.com>
Tue, 23 Sep 2025 09:10:13 +0000 (10:10 +0100)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Tue, 23 Sep 2025 09:10:13 +0000 (10:10 +0100)
src/libmime/archives.c
src/libmime/archives.h
src/lua/lua_archive.c

index 736f9813158df27ebd2060ca657d1c7ad788ef95..f63bde4d7dd43487090be96ed3ce2760300fbad6 100644 (file)
@@ -507,7 +507,6 @@ rspamd_archives_zip_write(const struct rspamd_zip_file_spec *files,
                        guint16 *p16 = (guint16 *) (zip->data + lfh_off + 30 + (guint32) strlen(f->name) + 9);
                        *p16 = GUINT16_TO_LE(actual_method);
 #else
-                       g_byte_array_free(cdata, TRUE);
                        g_byte_array_free(cd, TRUE);
                        g_byte_array_free(zip, TRUE);
                        g_set_error(err, q, ENOTSUP, "AES-CTR encryption requires OpenSSL");
@@ -605,97 +604,7 @@ rspamd_archives_zip_write(const struct rspamd_zip_file_spec *files,
        return zip;
 }
 
-GByteArray *
-rspamd_archives_encrypt_aes256_cbc(const unsigned char *in,
-                                                                  gsize inlen,
-                                                                  const char *password,
-                                                                  GError **err)
-{
-#ifndef HAVE_OPENSSL
-       (void) in;
-       (void) inlen;
-       (void) password;
-       GQuark q = rspamd_archives_err_quark();
-       g_set_error(err, q, ENOTSUP, "OpenSSL is not available");
-       return NULL;
-#else
-       GQuark q = rspamd_archives_err_quark();
-       unsigned char salt[16];
-       unsigned char iv[16];
-       unsigned char key[32];
-       const int kdf_iters = 100000;
-       GByteArray *out = NULL;
-       EVP_CIPHER_CTX *ctx = NULL;
-
-       if (password == NULL || *password == '\0') {
-               g_set_error(err, q, EINVAL, "empty password");
-               return NULL;
-       }
-
-       if (RAND_bytes(salt, sizeof(salt)) != 1 || RAND_bytes(iv, sizeof(iv)) != 1) {
-               g_set_error(err, q, EIO, "cannot generate random salt/iv: %s", ERR_error_string(ERR_get_error(), NULL));
-               return NULL;
-       }
-
-       if (PKCS5_PBKDF2_HMAC(password, (int) strlen(password), salt, (int) sizeof(salt),
-                                                 kdf_iters, EVP_sha256(), (int) sizeof(key), key) != 1) {
-               g_set_error(err, q, EIO, "PBKDF2 failed: %s", ERR_error_string(ERR_get_error(), NULL));
-               return NULL;
-       }
-
-       ctx = EVP_CIPHER_CTX_new();
-       if (ctx == NULL) {
-               g_set_error(err, q, ENOMEM, "cannot alloc cipher ctx");
-               rspamd_explicit_memzero(key, sizeof(key));
-               return NULL;
-       }
-
-       if (EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv) != 1) {
-               g_set_error(err, q, EIO, "cipher init failed: %s", ERR_error_string(ERR_get_error(), NULL));
-               EVP_CIPHER_CTX_free(ctx);
-               rspamd_explicit_memzero(key, sizeof(key));
-               return NULL;
-       }
-
-       /* Prepare output: magic + salt + iv + ciphertext; write directly into GByteArray */
-       const char magic[8] = {'R', 'Z', 'A', 'E', '0', '0', '0', '1'};
-       out = g_byte_array_sized_new(8 + sizeof(salt) + sizeof(iv) + inlen + 32);
-       g_byte_array_append(out, (const guint8 *) magic, sizeof(magic));
-       g_byte_array_append(out, salt, sizeof(salt));
-       g_byte_array_append(out, iv, sizeof(iv));
-
-       gsize before = out->len;
-       g_byte_array_set_size(out, out->len + inlen + EVP_CIPHER_block_size(EVP_aes_256_cbc()));
-       unsigned char *cptr = out->data + before;
-       int outlen = 0;
-
-       if (EVP_EncryptUpdate(ctx, cptr, &outlen, in, (int) inlen) != 1) {
-               g_set_error(err, q, EIO, "encrypt update failed: %s", ERR_error_string(ERR_get_error(), NULL));
-               EVP_CIPHER_CTX_free(ctx);
-               rspamd_explicit_memzero(key, sizeof(key));
-               g_byte_array_set_size(out, before);
-               g_byte_array_free(out, TRUE);
-               return NULL;
-       }
-
-       int fin = 0;
-       if (EVP_EncryptFinal_ex(ctx, cptr + outlen, &fin) != 1) {
-               g_set_error(err, q, EIO, "encrypt final failed: %s", ERR_error_string(ERR_get_error(), NULL));
-               EVP_CIPHER_CTX_free(ctx);
-               rspamd_explicit_memzero(key, sizeof(key));
-               g_byte_array_set_size(out, before);
-               g_byte_array_free(out, TRUE);
-               return NULL;
-       }
-
-       g_byte_array_set_size(out, before + outlen + fin);
-       EVP_CIPHER_CTX_free(ctx);
-       rspamd_explicit_memzero(key, sizeof(key));
-
-       msg_info("zip: AES-256-CBC envelope created (PBKDF2-SHA256 iters=%d)", kdf_iters);
-       return out;
-#endif
-}
+/* removed obsolete whole-archive AES-256-CBC function */
 
 static bool
 rspamd_archive_file_try_utf(struct rspamd_task *task,
index 8bc75f2172b11026dbc9a8c61f9328fba9678946..c5c8e29fd98c215e2e5a4a6b8272ad028102b355 100644 (file)
@@ -66,26 +66,19 @@ struct rspamd_zip_file_spec {
 };
 
 /**
- * Create a ZIP archive in-memory from provided files (DEFLATE compression)
- * If password is non-NULL, the ZIP is created normally and then encrypted as a whole
- * using AES-256-CBC with PBKDF2-HMAC-SHA256 and a random salt/IV. The result format is:
- *  [ 'RZAE0001' (8 bytes) | salt (16 bytes) | iv (16 bytes) | ciphertext ]
- * Returns newly allocated GByteArray on success, NULL on error and sets err
+ * 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);
 
-/**
- * AES-256-CBC encrypts arbitrary data buffer using PBKDF2-HMAC-SHA256 derived key.
- * Output format: [ 'RZAE0001' | salt(16) | iv(16) | ciphertext ]
- */
-GByteArray *rspamd_archives_encrypt_aes256_cbc(const unsigned char *in,
-                                                                                          gsize inlen,
-                                                                                          const char *password,
-                                                                                          GError **err);
-
 /**
  * Process archives from a worker task
  */
index 5473bf20b61c33cded7081edb353c597cb0f4bee..9bddb6ed3d96701cc2aa85ab012cd8de9b8a57ec 100644 (file)
@@ -16,6 +16,7 @@
 
 #include "lua_common.h"
 #include "unix-std.h"
+#include "libmime/archives.h"
 
 #include <archive.h>
 #include <archive_entry.h>
@@ -61,6 +62,7 @@ LUA_FUNCTION_DEF(archive, supported_formats);
  * @return {text} archive bytes
  */
 LUA_FUNCTION_DEF(archive, zip);
+LUA_FUNCTION_DEF(archive, zip_encrypt);
 /***
  * @function archive.unzip(data)
  * Extract files from a ZIP archive.
@@ -94,6 +96,7 @@ static const struct luaL_reg arch_mod_f[] = {
        LUA_INTERFACE_DEF(archive, unpack),
        LUA_INTERFACE_DEF(archive, supported_formats),
        LUA_INTERFACE_DEF(archive, zip),
+       LUA_INTERFACE_DEF(archive, zip_encrypt),
        LUA_INTERFACE_DEF(archive, unzip),
        LUA_INTERFACE_DEF(archive, tar),
        LUA_INTERFACE_DEF(archive, untar),
@@ -199,6 +202,119 @@ lua_archive_zip(lua_State *L)
        return lua_archive_pack(L);
 }
 
+/**
+ * zip_encrypt(files, password) -> text
+ */
+static int
+lua_archive_zip_encrypt(lua_State *L)
+{
+       LUA_TRACE_POINT;
+       luaL_checktype(L, 1, LUA_TTABLE);
+       const char *password = luaL_checkstring(L, 2);
+       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);
+       }
+
+       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);
+       }
+       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);
+       }
+
+       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;
+}
+
 static int
 lua_archive_enable_read_format_by_name(struct archive *a, const char *fmt)
 {