From 0e24de1cad2e044ba9ab1be1f9f6186cd80e9cfe Mon Sep 17 00:00:00 2001 From: Vsevolod Stakhov Date: Mon, 29 Sep 2025 13:59:50 +0100 Subject: [PATCH] [Feature] Add support for encrypted maps --- src/libserver/maps/map.c | 619 ++++++++++++++---- src/libserver/maps/map_private.h | 4 + .../maps/advance_fee_rules.map.zst.enc | Bin 0 -> 1465 bytes test/functional/configs/regexp_maps.conf | 10 +- 4 files changed, 516 insertions(+), 117 deletions(-) create mode 100644 test/functional/configs/maps/advance_fee_rules.map.zst.enc diff --git a/src/libserver/maps/map.c b/src/libserver/maps/map.c index 1910bd6140..d413870669 100644 --- a/src/libserver/maps/map.c +++ b/src/libserver/maps/map.c @@ -25,6 +25,10 @@ #include "rspamd.h" #include "contrib/libev/ev.h" #include "contrib/uthash/utlist.h" +#include "libutil/str_util.h" +#include "libcryptobox/cryptobox.h" + +#include #include @@ -394,6 +398,163 @@ rspamd_map_get_respectful_interval(time_t map_check_interval) return map_check_interval; } +static gboolean +rspamd_map_secretbox_decrypt_buf(struct rspamd_map_backend *bk, + const unsigned char *in, + gsize inlen, + unsigned char **out, + gsize *outlen) +{ + /* Logging-aware helper */ + struct rspamd_map *map = bk ? bk->map : NULL; + + if (!bk->has_secretbox_key) { + msg_err_map("%s: secretbox key is not configured", bk->uri); + return FALSE; + } + + if (inlen < crypto_secretbox_NONCEBYTES + crypto_secretbox_MACBYTES) { + msg_err_map("%s: too short buffer for secretbox: %z bytes (need >= %d)", + bk->uri, inlen, (int) (crypto_secretbox_NONCEBYTES + crypto_secretbox_MACBYTES)); + return FALSE; + } + + const unsigned char *nonce = in; + const unsigned char *ct = in + crypto_secretbox_NONCEBYTES; + gsize clen = inlen - crypto_secretbox_NONCEBYTES; + + if (clen < crypto_secretbox_MACBYTES) { + msg_err_map("%s: invalid ciphertext length for secretbox: %z (< MAC %d)", + bk->uri, clen, (int) crypto_secretbox_MACBYTES); + return FALSE; + } + + gsize ptlen = clen - crypto_secretbox_MACBYTES; + unsigned char *pt = g_malloc(ptlen); + + msg_debug_map("%s: attempting secretbox decrypt; nonce_len=%d ct_len=%z pt_len=%z", + bk->uri, (int) crypto_secretbox_NONCEBYTES, clen, ptlen); + + if (crypto_secretbox_open_easy(pt, ct, clen, nonce, bk->secretbox_key) != 0) { + msg_err_map("%s: secretbox authentication failed (nonce_len=%d, ct_len=%z)", + bk->uri, (int) crypto_secretbox_NONCEBYTES, clen); + g_free(pt); + return FALSE; + } + + *out = pt; + *outlen = ptlen; + return TRUE; +} + +static gboolean +rspamd_map_decode_secretbox_key(const char *in, gsize inlen, unsigned char out[32]) +{ + /* Be compatible with lua_secretbox: derive a 32-byte key via crypto_generichash + * from the provided secret (which can be hex/base64/raw). */ + if (in == NULL || inlen == 0) { + return FALSE; + } + + unsigned char *raw = NULL; + gsize rawlen = 0; + bool allocated = false; + + /* Detect hex (only hex chars, even length) */ + bool maybe_hex = (inlen % 2 == 0); + for (gsize i = 0; i < inlen && maybe_hex; i++) { + char c = in[i]; + if (!g_ascii_isxdigit((int) c)) { + maybe_hex = false; + } + } + + if (maybe_hex) { + gsize tmp_len = inlen / 2 + 1; + raw = g_malloc(tmp_len); + gssize dec = rspamd_decode_hex_buf(in, inlen, raw, tmp_len); + if (dec > 0) { + rawlen = (gsize) dec; + allocated = true; + } + else { + g_free(raw); + raw = NULL; + } + } + + if (raw == NULL && rspamd_cryptobox_base64_is_valid(in, inlen)) { + /* Try base64 */ + /* Worst-case allocate */ + gsize tmp_len = (inlen / 4 + 1) * 3 + 8; + raw = g_malloc(tmp_len); + if (rspamd_cryptobox_base64_decode(in, inlen, raw, &rawlen)) { + allocated = true; + } + else { + g_free(raw); + raw = NULL; + rawlen = 0; + } + } + + if (raw == NULL) { + /* Treat as raw string */ + raw = (unsigned char *) in; + rawlen = inlen; + } + + /* Derive 32-byte key */ + crypto_generichash(out, crypto_secretbox_KEYBYTES, raw, rawlen, NULL, 0); + + if (allocated) { + rspamd_explicit_memzero(raw, rawlen); + g_free(raw); + } + + return TRUE; +} + +static inline gboolean +rspamd_map_payload_is_zstd(const unsigned char *p, gsize len) +{ + /* Zstandard frame magic number: 0x28B52FFD at start of frame */ + if (len >= 4) { + const unsigned char zstd_magic[4] = {0x28u, 0xB5u, 0x2Fu, 0xFDu}; + if (memcmp(p, zstd_magic, 4) == 0) { + return TRUE; + } + } + + return FALSE; +} + +static void +rspamd_map_try_load_secretbox_key(struct rspamd_config *cfg, + struct rspamd_map_backend *bk) +{ + const ucl_object_t *maps_obj = ucl_object_lookup(cfg->cfg_ucl_obj, "maps"); + if (maps_obj == NULL || ucl_object_type(maps_obj) != UCL_OBJECT) { + const ucl_object_t *opts_obj = ucl_object_lookup(cfg->cfg_ucl_obj, "options"); + if (opts_obj && ucl_object_type(opts_obj) == UCL_OBJECT) { + maps_obj = ucl_object_lookup(opts_obj, "maps"); + } + } + + if (maps_obj && ucl_object_type(maps_obj) == UCL_OBJECT) { + const ucl_object_t *src = ucl_object_lookup(maps_obj, bk->uri); + if (!src) src = maps_obj; + const ucl_object_t *kobj = ucl_object_lookup_any(src, "secretbox_key", "secretbox-key", "enc_key", NULL); + if (kobj && ucl_object_type(kobj) == UCL_STRING) { + gsize klen = 0; + const char *k = ucl_object_tolstring(kobj, &klen); + if (rspamd_map_decode_secretbox_key(k, klen, bk->secretbox_key)) { + bk->has_secretbox_key = TRUE; + msg_info_config("loaded secretbox key late for %s", bk->uri); + } + } + } +} static int http_map_finish(struct rspamd_http_connection *conn, struct rspamd_http_message *msg) @@ -517,7 +678,7 @@ http_map_finish(struct rspamd_http_connection *conn, MAP_RETAIN(cbd->shmem_data, "shmem_data"); cbd->data->gen++; - /* Store cached data */ + /* Store cached data: raw HTTP body in SHM */ rspamd_strlcpy(data->cache->shmem_name, cbd->shmem_data->shm_name, sizeof(data->cache->shmem_name)); data->cache->len = cbd->data_len; @@ -549,27 +710,46 @@ http_map_finish(struct rspamd_http_connection *conn, } - if (cbd->bk->is_compressed) { + /* Prepare payload: decrypt (if needed) then optionally decompress */ + unsigned char *payload = NULL; + gsize payload_len = 0; + unsigned char *final_out = NULL; + + if (cbd->bk->is_encrypted) { + if (!rspamd_map_secretbox_decrypt_buf(cbd->bk, in, dlen, &payload, &payload_len)) { + msg_err_map("%s(%s): cannot decrypt data", cbd->bk->uri, + rspamd_inet_address_to_string_pretty(cbd->addr)); + MAP_RELEASE(cbd->shmem_data, "shmem_data"); + goto err; + } + } + else { + /* Use mapped buffer directly */ + payload = (unsigned char *) in; + payload_len = dlen; + } + + /* If compressed flag is set OR payload looks like zstd, decompress */ + if (cbd->bk->is_compressed || rspamd_map_payload_is_zstd(payload, payload_len)) { ZSTD_DStream *zstream; ZSTD_inBuffer zin; ZSTD_outBuffer zout; - unsigned char *out; gsize outlen, r; zstream = ZSTD_createDStream(); ZSTD_initDStream(zstream); zin.pos = 0; - zin.src = in; - zin.size = dlen; + zin.src = payload; + zin.size = payload_len; if ((outlen = ZSTD_getDecompressedSize(zin.src, zin.size)) == 0) { outlen = ZSTD_DStreamOutSize(); } - out = g_malloc(outlen); + final_out = g_malloc(outlen); - zout.dst = out; + zout.dst = final_out; zout.pos = 0; zout.size = outlen; @@ -582,7 +762,11 @@ http_map_finish(struct rspamd_http_connection *conn, rspamd_inet_address_to_string_pretty(cbd->addr), ZSTD_getErrorName(r)); ZSTD_freeDStream(zstream); - g_free(out); + g_free(final_out); + if (cbd->bk->is_encrypted && payload && payload != (unsigned char *) in) { + rspamd_explicit_memzero(payload, payload_len); + g_free(payload); + } MAP_RELEASE(cbd->shmem_data, "shmem_data"); goto err; } @@ -590,8 +774,8 @@ http_map_finish(struct rspamd_http_connection *conn, if (zout.pos == zout.size) { /* We need to extend output buffer */ zout.size = zout.size * 2 + 1.0; - out = g_realloc(zout.dst, zout.size); - zout.dst = out; + final_out = g_realloc(zout.dst, zout.size); + zout.dst = final_out; } } @@ -600,23 +784,27 @@ http_map_finish(struct rspamd_http_connection *conn, "%z uncompressed, next check at %s", cbd->bk->uri, rspamd_inet_address_to_string_pretty(cbd->addr), - dlen, zout.pos, next_check_date); - map->read_callback(out, zout.pos, &cbd->periodic->cbdata, TRUE); - rspamd_map_save_http_cached_file(map, bk, cbd->data, out, zout.pos); - g_free(out); + payload_len, zout.pos, next_check_date); + map->read_callback(final_out, zout.pos, &cbd->periodic->cbdata, TRUE); + rspamd_map_save_http_cached_file(map, bk, cbd->data, final_out, zout.pos); + g_free(final_out); } else { msg_info_map("%s(%s): read map data %z bytes, next check at %s", cbd->bk->uri, rspamd_inet_address_to_string_pretty(cbd->addr), - dlen, next_check_date); - rspamd_map_save_http_cached_file(map, bk, cbd->data, in, cbd->data_len); - map->read_callback(in, cbd->data_len, &cbd->periodic->cbdata, TRUE); + payload_len, next_check_date); + rspamd_map_save_http_cached_file(map, bk, cbd->data, payload, payload_len); + map->read_callback(payload, payload_len, &cbd->periodic->cbdata, TRUE); } MAP_RELEASE(cbd->shmem_data, "shmem_data"); cbd->periodic->cur_backend++; + if (cbd->bk->is_encrypted && payload && payload != (unsigned char *) in) { + rspamd_explicit_memzero(payload, payload_len); + g_free(payload); + } munmap(in, dlen); /* Announce for other processes */ @@ -961,7 +1149,7 @@ read_map_file(struct rspamd_map *map, struct file_map_data *data, &periodic->cbdata, TRUE); } else { - if (bk->is_compressed) { + if (bk->is_compressed || bk->is_encrypted) { bytes = rspamd_file_xmap(data->filename, PROT_READ, &len, TRUE); if (bytes == NULL) { @@ -969,59 +1157,104 @@ read_map_file(struct rspamd_map *map, struct file_map_data *data, return FALSE; } - ZSTD_DStream *zstream; - ZSTD_inBuffer zin; - ZSTD_outBuffer zout; - unsigned char *out; - gsize outlen, r; + unsigned char *payload = (unsigned char *) bytes; + gsize payload_len = len; - zstream = ZSTD_createDStream(); - ZSTD_initDStream(zstream); + unsigned char *dec = NULL; + gsize declen = 0; - zin.pos = 0; - zin.src = bytes; - zin.size = len; - - if ((outlen = ZSTD_getDecompressedSize(zin.src, zin.size)) == 0) { - outlen = ZSTD_DStreamOutSize(); + if (bk->is_encrypted) { + if (!rspamd_map_secretbox_decrypt_buf(bk, payload, payload_len, &dec, &declen)) { + msg_err_map("%s: cannot decrypt data: secretbox auth failed", + data->filename); + munmap(bytes, len); + return FALSE; + } + payload = dec; + payload_len = declen; } - out = g_malloc(outlen); + /* If compressed flag is set OR payload looks like zstd, decompress */ + if (bk->is_compressed || rspamd_map_payload_is_zstd((const unsigned char *) payload, payload_len)) { + ZSTD_DStream *zstream; + ZSTD_inBuffer zin; + ZSTD_outBuffer zout; + unsigned char *out; + gsize outlen, r; - zout.dst = out; - zout.pos = 0; - zout.size = outlen; + zstream = ZSTD_createDStream(); + ZSTD_initDStream(zstream); - while (zin.pos < zin.size) { - r = ZSTD_decompressStream(zstream, &zout, &zin); + zin.pos = 0; + zin.src = payload; + zin.size = payload_len; - if (ZSTD_isError(r)) { - msg_err_map("%s: cannot decompress data: %s", - data->filename, - ZSTD_getErrorName(r)); - ZSTD_freeDStream(zstream); - g_free(out); - munmap(bytes, len); - return FALSE; + if ((outlen = ZSTD_getDecompressedSize(zin.src, zin.size)) == 0) { + outlen = ZSTD_DStreamOutSize(); } - if (zout.pos == zout.size) { - /* We need to extend output buffer */ - zout.size = zout.size * 2 + 1; - out = g_realloc(zout.dst, zout.size); - zout.dst = out; + out = g_malloc(outlen); + + zout.dst = out; + zout.pos = 0; + zout.size = outlen; + + while (zin.pos < zin.size) { + r = ZSTD_decompressStream(zstream, &zout, &zin); + + if (ZSTD_isError(r)) { + msg_err_map("%s: cannot decompress data: %s", + data->filename, + ZSTD_getErrorName(r)); + ZSTD_freeDStream(zstream); + g_free(out); + if (dec) { + rspamd_explicit_memzero(dec, declen); + g_free(dec); + } + munmap(bytes, len); + return FALSE; + } + + if (zout.pos == zout.size) { + /* We need to extend output buffer */ + zout.size = zout.size * 2 + 1; + out = g_realloc(zout.dst, zout.size); + zout.dst = out; + } } - } - ZSTD_freeDStream(zstream); - msg_info_map("%s: read map data, %z bytes compressed, " - "%z uncompressed)", - data->filename, - len, zout.pos); - map->read_callback(out, zout.pos, &periodic->cbdata, TRUE); - g_free(out); + ZSTD_freeDStream(zstream); + msg_info_map("%s: read map data, %z bytes compressed, " + "%z uncompressed)", + data->filename, + payload_len, zout.pos); + map->read_callback(out, zout.pos, &periodic->cbdata, TRUE); + g_free(out); - munmap(bytes, len); + if (dec) { + rspamd_explicit_memzero(dec, declen); + g_free(dec); + } + munmap(bytes, len); + } + else if (bk->is_encrypted) { + /* Already decrypted, pass through */ + msg_info_map("%s: read map data, %z bytes (decrypted)", + data->filename, payload_len); + map->read_callback(payload, payload_len, &periodic->cbdata, TRUE); + rspamd_explicit_memzero(payload, payload_len); + g_free(payload); + munmap(bytes, len); + } + else { + /* Should not happen here as we only mmap for compressed or encrypted */ + munmap(bytes, len); + if (!read_map_file_chunks(map, &periodic->cbdata, data->filename, + len, 0)) { + return FALSE; + } + } } else { /* Perform buffered read: fail-safe */ @@ -1509,67 +1742,104 @@ rspamd_map_read_cached(struct rspamd_map *map, struct rspamd_map_backend *bk, */ len = data->cache->len; - if (bk->is_compressed) { - ZSTD_DStream *zstream; - ZSTD_inBuffer zin; - ZSTD_outBuffer zout; - unsigned char *out; - gsize outlen, r; + if (bk->is_encrypted || bk->is_compressed) { + unsigned char *payload = (unsigned char *) in; + gsize payload_len = len; + unsigned char *dec = NULL; + gsize declen = 0; - zstream = ZSTD_createDStream(); - ZSTD_initDStream(zstream); + if (bk->is_encrypted) { + if (!rspamd_map_secretbox_decrypt_buf(bk, payload, payload_len, &dec, &declen)) { + munmap(in, mmap_len); + return FALSE; + } + payload = dec; + payload_len = declen; + } - zin.pos = 0; - zin.src = in; - zin.size = len; + /* If compressed flag is set OR payload looks like zstd, decompress */ + if (bk->is_compressed || rspamd_map_payload_is_zstd(payload, payload_len)) { + ZSTD_DStream *zstream; + ZSTD_inBuffer zin; + ZSTD_outBuffer zout; + unsigned char *out; + gsize outlen, r; - if ((outlen = ZSTD_getDecompressedSize(zin.src, zin.size)) == 0) { - outlen = ZSTD_DStreamOutSize(); - } + zstream = ZSTD_createDStream(); + ZSTD_initDStream(zstream); - out = g_malloc(outlen); + zin.pos = 0; + zin.src = payload; + zin.size = payload_len; - zout.dst = out; - zout.pos = 0; - zout.size = outlen; + if ((outlen = ZSTD_getDecompressedSize(zin.src, zin.size)) == 0) { + outlen = ZSTD_DStreamOutSize(); + } - while (zin.pos < zin.size) { - r = ZSTD_decompressStream(zstream, &zout, &zin); + out = g_malloc(outlen); - if (ZSTD_isError(r)) { - msg_err_map("%s: cannot decompress data: %s", - bk->uri, - ZSTD_getErrorName(r)); - ZSTD_freeDStream(zstream); - g_free(out); - munmap(in, mmap_len); - return FALSE; + zout.dst = out; + zout.pos = 0; + zout.size = outlen; + + while (zin.pos < zin.size) { + r = ZSTD_decompressStream(zstream, &zout, &zin); + + if (ZSTD_isError(r)) { + msg_err_map("%s: cannot decompress data: %s", + bk->uri, + ZSTD_getErrorName(r)); + ZSTD_freeDStream(zstream); + g_free(out); + if (dec) { + rspamd_explicit_memzero(dec, declen); + g_free(dec); + } + munmap(in, mmap_len); + return FALSE; + } + + if (zout.pos == zout.size) { + /* We need to extend output buffer */ + zout.size = zout.size * 2 + 1; + out = g_realloc(zout.dst, zout.size); + zout.dst = out; + } } - if (zout.pos == zout.size) { - /* We need to extend output buffer */ - zout.size = zout.size * 2 + 1; - out = g_realloc(zout.dst, zout.size); - zout.dst = out; + ZSTD_freeDStream(zstream); + msg_info_map("%s: read map data cached %z bytes compressed, " + "%z uncompressed", + bk->uri, + payload_len, zout.pos); + map->read_callback(out, zout.pos, &periodic->cbdata, TRUE); + g_free(out); + if (dec) { + rspamd_explicit_memzero(dec, declen); + g_free(dec); + } + } + else { + msg_info_map("%s: read map data cached %z bytes%s", bk->uri, payload_len, + bk->is_encrypted ? " (decrypted)" : ""); + map->read_callback(payload, payload_len, &periodic->cbdata, TRUE); + if (dec) { + rspamd_explicit_memzero(dec, declen); + g_free(dec); } } - ZSTD_freeDStream(zstream); - msg_info_map("%s: read map data cached %z bytes compressed, " - "%z uncompressed", - bk->uri, - len, zout.pos); - map->read_callback(out, zout.pos, &periodic->cbdata, TRUE); - g_free(out); + munmap(in, mmap_len); + + return TRUE; } else { + /* Neither encrypted nor compressed: pass cached bytes as-is */ msg_info_map("%s: read map data cached %z bytes", bk->uri, len); map->read_callback(in, len, &periodic->cbdata, TRUE); + munmap(in, mmap_len); + return TRUE; } - - munmap(in, mmap_len); - - return TRUE; } static gboolean @@ -1864,10 +2134,83 @@ rspamd_map_read_http_cached_file(struct rspamd_map *map, close(fd); /* Now read file data */ - /* Perform buffered read: fail-safe */ - if (!read_map_file_chunks(map, cbdata, path, - st.st_size - header.data_off, header.data_off)) { - return FALSE; + /* Perform buffered read: fail-safe, but for encrypted files, we must read whole buffer */ + if (bk->is_encrypted) { + gsize flen; + unsigned char *fbytes = rspamd_file_xmap(path, PROT_READ, &flen, TRUE); + if (!fbytes) { + return FALSE; + } + const unsigned char *enc = fbytes + header.data_off; + gsize enclen = flen - header.data_off; + unsigned char *dec = NULL; + gsize declen = 0; + + if (!rspamd_map_secretbox_decrypt_buf(bk, enc, enclen, &dec, &declen)) { + munmap(fbytes, flen); + return FALSE; + } + /* If compressed, decompress after decryption */ + if (bk->is_compressed) { + ZSTD_DStream *zstream; + ZSTD_inBuffer zin; + ZSTD_outBuffer zout; + unsigned char *out; + gsize outlen, r; + + zstream = ZSTD_createDStream(); + ZSTD_initDStream(zstream); + + zin.pos = 0; + zin.src = dec; + zin.size = declen; + + if ((outlen = ZSTD_getDecompressedSize(zin.src, zin.size)) == 0) { + outlen = ZSTD_DStreamOutSize(); + } + + out = g_malloc(outlen); + + zout.dst = out; + zout.pos = 0; + zout.size = outlen; + + while (zin.pos < zin.size) { + r = ZSTD_decompressStream(zstream, &zout, &zin); + + if (ZSTD_isError(r)) { + ZSTD_freeDStream(zstream); + g_free(out); + rspamd_explicit_memzero(dec, declen); + g_free(dec); + munmap(fbytes, flen); + return FALSE; + } + + if (zout.pos == zout.size) { + /* We need to extend output buffer */ + zout.size = zout.size * 2 + 1; + out = g_realloc(zout.dst, zout.size); + zout.dst = out; + } + } + + ZSTD_freeDStream(zstream); + map->read_callback(out, zout.pos, cbdata, TRUE); + g_free(out); + } + else { + map->read_callback(dec, declen, cbdata, TRUE); + } + rspamd_explicit_memzero(dec, declen); + g_free(dec); + munmap(fbytes, flen); + } + else { + if (!read_map_file_chunks(map, cbdata, path, + st.st_size - header.data_off, header.data_off)) { + return FALSE; + } } struct tm tm; @@ -2751,7 +3094,7 @@ rspamd_map_parse_backend(struct rspamd_config *cfg, const char *map_line) struct http_map_data *hdata = NULL; struct static_map_data *sdata = NULL; struct http_parser_url up; - const char *end, *p; + const char *end; rspamd_ftok_t tok; bk = g_malloc0(sizeof(*bk)); @@ -2768,13 +3111,21 @@ rspamd_map_parse_backend(struct rspamd_config *cfg, const char *map_line) } end = map_line + strlen(map_line); - if (end - map_line > 5) { - p = end - 5; - if (g_ascii_strcasecmp(p, ".zstd") == 0) { + if (end - map_line > 4) { + /* Support combinations: .enc, .zst, .zstd, .zst.enc, .zstd.enc */ + const char *fname = map_line; + gsize flen = end - map_line; + + /* Check .enc suffix */ + if (flen >= 4 && g_ascii_strcasecmp(fname + flen - 4, ".enc") == 0) { + bk->is_encrypted = TRUE; + flen -= 4; + } + + if (flen >= 5 && g_ascii_strcasecmp(fname + flen - 5, ".zstd") == 0) { bk->is_compressed = TRUE; } - p = end - 4; - if (g_ascii_strcasecmp(p, ".zst") == 0) { + else if (flen >= 4 && g_ascii_strcasecmp(fname + flen - 4, ".zst") == 0) { bk->is_compressed = TRUE; } } @@ -2953,6 +3304,48 @@ rspamd_map_parse_backend(struct rspamd_config *cfg, const char *map_line) bk->data.sd = sdata; } + /* Load optional secretbox key from maps { uri> { secretbox_key = "..." } } + * or maps { secretbox_key = ... } either at the root or under options { maps { ... } } + */ + if (bk->is_encrypted) { + const ucl_object_t *maps_obj = ucl_object_lookup(cfg->cfg_ucl_obj, "maps"); + if (maps_obj == NULL || ucl_object_type(maps_obj) != UCL_OBJECT) { + const ucl_object_t *opts_obj = ucl_object_lookup(cfg->cfg_ucl_obj, "options"); + if (opts_obj && ucl_object_type(opts_obj) == UCL_OBJECT) { + maps_obj = ucl_object_lookup(opts_obj, "maps"); + } + } + const ucl_object_t *src = NULL; + if (maps_obj && ucl_object_type(maps_obj) == UCL_OBJECT) { + /* Per-backend */ + src = ucl_object_lookup(maps_obj, bk->uri); + if (!src) src = maps_obj; + const ucl_object_t *kobj = ucl_object_lookup_any(src, "secretbox_key", "secretbox-key", "enc_key", NULL); + if (kobj && ucl_object_type(kobj) == UCL_STRING) { + gsize klen = 0; + const char *k = ucl_object_tolstring(kobj, &klen); + if (rspamd_map_decode_secretbox_key(k, klen, bk->secretbox_key)) { + bk->has_secretbox_key = TRUE; + msg_info_config("loaded secretbox key for %s", bk->uri); + } + else { + msg_info_config("cannot decode secretbox key for %s", bk->uri); + } + } + else { + msg_debug_config("no secretbox_key found in maps block for %s", bk->uri); + } + } + else { + msg_debug_config("maps block not found at root nor under options; no secretbox_key (uri=%s)", bk->uri); + } + } + + /* Fallback: if encrypted but key not loaded (e.g. maps block parsed later), try again */ + if (bk->is_encrypted && !bk->has_secretbox_key) { + rspamd_map_try_load_secretbox_key(cfg, bk); + } + return bk; err: diff --git a/src/libserver/maps/map_private.h b/src/libserver/maps/map_private.h index fba8821601..ec5c809392 100644 --- a/src/libserver/maps/map_private.h +++ b/src/libserver/maps/map_private.h @@ -148,6 +148,10 @@ struct rspamd_map_backend { enum fetch_proto protocol; gboolean is_signed; gboolean is_compressed; + /* Symmetric encryption (Secretbox) */ + gboolean is_encrypted; + gboolean has_secretbox_key; + unsigned char secretbox_key[32]; /* crypto_secretbox_KEYBYTES */ gboolean is_fallback; struct rspamd_map *map; struct ev_loop *event_loop; diff --git a/test/functional/configs/maps/advance_fee_rules.map.zst.enc b/test/functional/configs/maps/advance_fee_rules.map.zst.enc new file mode 100644 index 0000000000000000000000000000000000000000..35ea95ad328ad0998a8708f6eb90e2541fecb518 GIT binary patch literal 1465 zc-jHE1xES^P7Cb4GFuRNNnpu-@vprRT|1d~9)Cv>ycCytu=lNd2PM&_UJ6kU!=as+ z3XQH1;VMXz_>4k|{#(4JDJr!fhn-WU4sOhF%nTCy3xF>)#$ck`+43{3hIYdu3WS)= zYE)k_A-+=N#9u3Yql#uWHR@ISM^(hSWo}@~v`Xg?KOW@=>7e+hv;U@BzkEoJSJaYB zUwLcFcVD)}>eBJM@V9~_IaPsS=!)50V0B>mk&=($a`vndehMCq+(}782IA}zLlNe_ z2<37qYFR1R%-p=124a!ChS`HK5yl!1letED!fo?W!M>$o9xTwG>WRGfm2kuiOUbg} zJ(Qd8Zo1BjJ`eKAVWkfBuj(EReRGdQi^~UC2R0rze)IogP~o%OSl;Ky;~(BTq(OQV zw2?R$76kf-hZ-A?2Ta-~DKSe7UHKjY!HRvU5#i9LgdZHt`OjL?jcc>p62klNKM>^* zp(XIisv_L<8gE0Rb^*Bp54NkGtZggFGG8wq4)-FBHAva#HnIIurS))tdE#LFnQzP~ ze8!rpHJQHGw{a7|xZX1kyx0R*n4-4B-c`*6P2sWYLBr74VNbK5`oA8SLw5SM1?JUq zzb06?V~8QZfKtm!z?cb0w#Oi%6V^7xdwjloCyT=gLc+z@s6sro(KQpWH^J>sQOBq+ zKIKodO{#jC&0D8buBz%S%gnWP!?L@0FJ~Ayck3ypjkf|uta*L);V);Q&Tyfc-IdLz z&>eIrtPW3--O}#pB?{0?^ZmB+p{Wy7$l<IqK&s}rc7_L)AvVcYTz{OW zG9J|JgqyST5EsS!qaEZ0zJxZN^Pv1GSXX@iZT{e&d{TU1z8rlRgSp?8%Oeo3aPG(M z3iiEQpS36Z`0OpNSme#sGv3;-i{ryj>y;zA&z(O-S-zMWVxi$q?k+PS?^KDn|AZr` zF(}e5~6Y1AR3xvh^#}d8*?sd4z0F}GRS8| zcdsyghtfpQ*9lXOjU0;Uxy&=uW?fz9zphvju(m|gz~)lYSF*2_hpf7KZh+6ff^rHb znA+HK{vYrS2OJJ_A<;-i=$4piO`3jGR!~qbXG7vtS-s7A!?YmAvgqqU5%Wv4x^nPK z{=y+|$Q}OL9o)fEnh(<}XG|C+wkLP%27)jw>vN0uQcd)V&xe%FUy9+or%H-;Zgwe! z4K5@4>ABxd#QNcwc652IDoF$_lYN%l%dbxCTabZO7>D@_j-{WoUQ*laP_rPaNdygXYlOp5~!q0GESRQz3jZ+-pKQs=^@_5Od|0qF(`&s&7>&+y${A9(C=~{5?)d zGmEA!CbN5O%W<3=nsAkA84I=W`F{m%^+Lrq1Qdz1+=JlS@qjkKI@H_fm?!d8k3!>% z*pfto)g@(z0)r~I7F0d(;gkv@;TVM4JpPtm6~k@|>dvNM&NH50!1e6;ate3tXQO0d zGx$IO?wb3KAnK|$xBNVC6-_5RGk$HRr%KF!AjBdXAHLbm$y&dWE($F%9XjIYZzjd5 zxrObZVyV2hm73u>3ScW8F(afXq>J&pQ?vIq!}Rf}w3H58fZ#$2547zU zKqdd|Xgx}s=- z7I90a+h1A;>gk*C$+7#Siz!jEcf_)rzYw;k2|Mr>4*>7u94CpOo>jSoDY=pNr>p+n zu=VUbtjy6SEjH_YoPDdu|MxyeCcxIrjL>jC