From: Vsevolod Stakhov Date: Tue, 23 Sep 2025 09:10:13 +0000 (+0100) Subject: [Project] Add Lua bindings for encrypted zip support X-Git-Tag: 3.13.1~16^2~4 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=35cfe81a586396bc05400ea6f144e2734ab992a4;p=thirdparty%2Frspamd.git [Project] Add Lua bindings for encrypted zip support --- diff --git a/src/libmime/archives.c b/src/libmime/archives.c index 736f981315..f63bde4d7d 100644 --- a/src/libmime/archives.c +++ b/src/libmime/archives.c @@ -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, diff --git a/src/libmime/archives.h b/src/libmime/archives.h index 8bc75f2172..c5c8e29fd9 100644 --- a/src/libmime/archives.h +++ b/src/libmime/archives.h @@ -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 */ diff --git a/src/lua/lua_archive.c b/src/lua/lua_archive.c index 5473bf20b6..9bddb6ed3d 100644 --- a/src/lua/lua_archive.c +++ b/src/lua/lua_archive.c @@ -16,6 +16,7 @@ #include "lua_common.h" #include "unix-std.h" +#include "libmime/archives.h" #include #include @@ -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) {