src/anonymize.c
Rama master · réplica de lectura del repositorio original.
/**
* ______ ___
* / ____/___ / | _____________ __________
* / / __/ __ \/ /| |/ ___/ ___/ _ \/ ___/ ___/
* / /_/ / /_/ / ___ / /__/ /__/ __(__ |__ )
* \____/\____/_/ |_\___/\___/\___/____/____/
*
* The MIT License (MIT)
* Copyright (c) 2009-2026 Gerardo Orellana <hello @ goaccess.io>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "anonymize.h"
#include "error.h"
#include "gkhash.h"
#include "util.h"
#include "xmalloc.h"
/* Non-IP tokens accepted by --no-ip-validation have no address bits to mask. */
#define ANONYMIZED_UNKNOWN_HOST "-"
/* Keep hash tables at most half full after combining legacy host rows. */
#define COMPACT_TABLE_SLACK 2
/* Use the operating system's cryptographic random source without an SSL dependency. */
#define ANONYMIZE_RANDOM_SOURCE "/dev/urandom"
/* mkstemp creates a private file before its name is linked into the database. */
#define ANONYMIZE_KEY_TEMP_SUFFIX ".XXXXXX"
/* A separate hash domain binds persisted metadata to its original key. */
#define ANONYMIZE_KEY_ID_DOMAIN "goaccess-ip-key"
/* Build the path to the database's private visitor key.
*
* On success, the allocated path is returned; allocation failure is fatal. */
static char *
anonymization_key_path (void) {
const char *dir = conf.db_path ? conf.db_path : DB_PATH;
char *path = xmalloc (strlen (dir) + sizeof (ANONYMIZE_KEY_FILE) + 1);
sprintf (path, "%s/%s", dir, ANONYMIZE_KEY_FILE);
return path;
}
/* Read the database's existing key without accepting truncated or public keys.
*
* On success, 1 is returned. If absent, 0 is returned; other failures are fatal. */
static int
read_anonymization_key (const char *path, uint8_t key[SIPHASH_KEY_SIZE]) {
FILE *fp = fopen (path, "rb");
struct stat st = { 0 };
int valid = 0;
if (!fp) {
if (errno == ENOENT)
return 0;
FATAL ("Unable to read visitor key '%s': %s", path, strerror (errno));
}
if (fstat (fileno (fp), &st) != 0 || !S_ISREG (st.st_mode) || (st.st_mode & (S_IRWXG | S_IRWXO)) != 0)
FATAL ("Visitor key '%s' must be a regular file accessible only to its owner (chmod 600).", path);
valid = fread (key, 1, SIPHASH_KEY_SIZE, fp) == SIPHASH_KEY_SIZE && fgetc (fp) == EOF && !ferror (fp);
fclose (fp);
if (!valid)
FATAL ("Invalid visitor key '%s'; restore the original key with its database.", path);
return 1;
}
/* Initialize a database key from the operating system's random source. */
static void
generate_anonymization_key (uint8_t key[SIPHASH_KEY_SIZE]) {
FILE *fp = fopen (ANONYMIZE_RANDOM_SOURCE, "rb");
if (!fp || fread (key, 1, SIPHASH_KEY_SIZE, fp) != SIPHASH_KEY_SIZE)
FATAL ("Unable to obtain random bytes for the visitor key.");
fclose (fp);
}
/* Identify a key without including the key itself in the public metadata.
*
* On success, the 64-bit identifier is returned; there is no failure case. */
uint64_t
anonymization_key_id (void) {
GKDB *db = get_db_instance (DB_INSTANCE);
return siphash24 (ANONYMIZE_KEY_ID_DOMAIN, sizeof (ANONYMIZE_KEY_ID_DOMAIN) - 1, db->anonymize.key);
}
/* Restore masking settings and initialize the database's single visitor key. */
void
init_anonymization (uint32_t version, uint32_t level, uint64_t key_id) {
GKDB *db = get_db_instance (DB_INSTANCE);
char *path = NULL;
int found = 0;
if (version) {
if (version != ANONYMIZE_VERSION || level < ANONYMIZE_DEFAULT || level > ANONYMIZE_PEDANTIC)
FATAL ("Unsupported persisted IP anonymization settings.");
if (conf.anonymize_level && conf.anonymize_level != (int) level)
FATAL ("This database uses --anonymize-level=%u; restore it with the same level.", level);
conf.anonymize_ip = 1;
conf.anonymize_level = level;
db->anonymize.restored_level = level;
}
if (!conf.anonymize_ip)
return;
if (!conf.anonymize_level)
conf.anonymize_level = ANONYMIZE_DEFAULT;
if (conf.anonymize_level < ANONYMIZE_DEFAULT || conf.anonymize_level > ANONYMIZE_PEDANTIC)
FATAL ("IP anonymization level must be 1, 2 or 3.");
/* Addresses are grouped before reporting, so per-address agents and DNS
* resolution would no longer describe an individual host. */
conf.list_agents = 0;
conf.skip_term_resolver = 1;
conf.enable_html_resolver = 0;
if (conf.restore || conf.persist) {
path = anonymization_key_path ();
found = read_anonymization_key (path, db->anonymize.key);
if (!found && version)
FATAL ("Missing visitor key '%s'; restore the original key with its database.", path);
free (path);
}
if (!found)
generate_anonymization_key (db->anonymize.key);
if (version && anonymization_key_id () != key_id)
FATAL ("Visitor key does not match this database; restore its original IP_PRIVACY.key.");
}
/* Write a key completely, retrying interrupted or short writes.
*
* On success, 1 is returned. On a write failure, 0 is returned. */
static int
write_anonymization_key (int fd, const uint8_t key[SIPHASH_KEY_SIZE]) {
size_t offset = 0;
ssize_t written = 0;
while (offset < SIPHASH_KEY_SIZE) {
written = write (fd, key + offset, SIPHASH_KEY_SIZE - offset);
if (written < 0 && errno == EINTR)
continue;
if (written <= 0)
return 0;
offset += written;
}
return 1;
}
/* Save the key before any dependent data, preserving an existing key. */
void
persist_anonymization_key (void) {
GKDB *db = get_db_instance (DB_INSTANCE);
uint8_t key[SIPHASH_KEY_SIZE];
char *path = NULL, *tmp = NULL;
size_t len = 0;
int fd = -1, ret = 0;
if (!conf.anonymize_ip)
return;
path = anonymization_key_path ();
if (read_anonymization_key (path, key)) {
if (memcmp (key, db->anonymize.key, sizeof (key)) != 0)
FATAL ("Visitor key changed while processing '%s'.", path);
free (path);
return;
}
len = strlen (path);
tmp = xmalloc (len + sizeof (ANONYMIZE_KEY_TEMP_SUFFIX));
memcpy (tmp, path, len);
memcpy (tmp + len, ANONYMIZE_KEY_TEMP_SUFFIX, sizeof (ANONYMIZE_KEY_TEMP_SUFFIX));
if ((fd = mkstemp (tmp)) == -1)
FATAL ("Unable to create visitor key '%s': %s", path, strerror (errno));
ret = write_anonymization_key (fd, db->anonymize.key) && fsync (fd) == 0;
if (close (fd) != 0)
ret = 0;
if (ret)
ret = link (tmp, path) == 0;
unlink (tmp);
if (!ret)
FATAL ("Unable to save visitor key '%s'.", path);
free (tmp);
free (path);
}
/* Key an existing fingerprint so legacy visitor sequences remain usable.
*
* On success, the non-zero fingerprint is returned; there is no failure case. */
uint64_t
anonymize_visitor (uint64_t fingerprint) {
GKDB *db = NULL;
uint8_t bytes[sizeof (fingerprint)];
size_t i;
uint64_t value = 0;
if (!conf.anonymize_ip)
return fingerprint;
db = get_db_instance (DB_INSTANCE);
/* Explicit byte order keeps restored IDs stable across architectures. */
for (i = 0; i < sizeof (bytes); ++i)
bytes[i] = fingerprint >> (i * 8);
value = siphash24 (bytes, sizeof (bytes), db->anonymize.key);
return value ? value : UINT64_MAX;
}
/* Produce a masked host label without retaining unrecognized host tokens. */
void
anonymize_host (const char *host, char masked[INET6_ADDRSTRLEN]) {
if (!mask_ip_address (host, masked, INET6_ADDRSTRLEN, conf.anonymize_level))
strcpy (masked, ANONYMIZED_UNKNOWN_HOST);
}
/* Identify database filenames containing host identities or visitor keys.
*
* On a matching filename, 1 is returned. Otherwise, 0 is returned. */
int
is_anonymized_database (const char *filename) {
static const char *types[] = { "II32", "IS32", "IU64", "SI32", "U648", "IGSL", "SU64" };
const char *part = strchr (filename, '_');
size_t i, len = strlen (filename);
if (strcmp (filename, "U6432_UNIQUE_KEYS.db") == 0 || strcmp (filename, "SI32_UNIQUE_KEYS.db") == 0)
return 1;
if (!part || strncmp (part, "_HOSTS_MTRC_", sizeof ("_HOSTS_MTRC_") - 1) != 0 ||
len < sizeof (".db") || strcmp (filename + len - (sizeof (".db") - 1), ".db") != 0)
return 0;
for (i = 0; i < ARRAY_SIZE (types); ++i) {
if ((size_t) (part - filename) == strlen (types[i]) && strncmp (filename, types[i], part - filename) == 0)
return 1;
}
return 0;
}
/* Combine metrics for host rows that now share the same masked address. */
static void
merge_host_metrics (khash_t (imtv) *metrics, uint32_t from, uint32_t to) {
khint_t source = kh_get (imtv, metrics, from);
GKMetricVals saved = { 0 }, *target = NULL;
if (source == kh_end (metrics))
return;
saved = kh_val (metrics, source);
kh_del (imtv, metrics, source);
if (!(target = ins_imtv (metrics, to)))
FATAL ("Unable to combine anonymized host metrics.");
target->hits += saved.hits;
target->visitors += saved.visitors;
target->bw += saved.bw;
target->cumts += saved.cumts;
target->maxts = MAX (target->maxts, saved.maxts);
target->touched |= saved.touched;
}
/* Replace one day's raw host labels and keys with grouped, masked hosts. */
static void
anonymize_host_date (uint32_t date) {
khash_t (ii32) *keys = get_hash (HOSTS, date, MTRC_KEYMAP);
khash_t (is32) *data = get_hash (HOSTS, date, MTRC_DATAMAP);
khash_t (imtv) *metrics = get_hash (HOSTS, date, MTRC_METRICS);
char masked[INET6_ADDRSTRLEN];
uint32_t hash = 0, survivor = 0, id = 0;
khint_t k;
if (!keys || !data || !metrics)
return;
kh_clear (ii32, keys);
for (k = kh_begin (data); k != kh_end (data); ++k) {
if (!kh_exist (data, k))
continue;
id = kh_key (data, k);
anonymize_host (kh_val (data, k), masked);
hash = djb2 ((const unsigned char *) masked);
survivor = get_ii32 (keys, hash);
free (kh_val (data, k));
if (survivor) {
kh_del (is32, data, k);
merge_host_metrics (metrics, id, survivor);
continue;
}
kh_val (data, k) = xstrdup (masked);
if (ins_ii32 (keys, hash, id) == -1)
FATAL ("Unable to store anonymized host key.");
}
/* HOSTS counts from visitor sequences, so it needs neither the old pair
* set nor the per-IP agent lists after migration. */
del_u648 (get_hash (HOSTS, date, MTRC_UNIQMAP), 0);
del_igsl_free (get_hash (HOSTS, date, MTRC_AGENTS), 1);
if (kh_resize (ii32, keys, kh_size (keys) * COMPACT_TABLE_SLACK) == -1 ||
kh_resize (is32, data, kh_size (data) * COMPACT_TABLE_SLACK) == -1 ||
kh_resize (imtv, metrics, kh_size (metrics) * COMPACT_TABLE_SLACK) == -1)
FATAL ("Unable to compact anonymized host storage.");
}
/* Mask and combine legacy hosts one date at a time, before building caches. */
void
anonymize_restored_hosts (void) {
GKDB *db = get_db_instance (DB_INSTANCE);
khash_t (igkh) *dates = get_hdb (db, MTRC_DATES);
khint_t k;
if (!conf.anonymize_ip || db->anonymize.restored_level)
return;
for (k = kh_begin (dates); k != kh_end (dates); ++k) {
if (kh_exist (dates, k))
anonymize_host_date (kh_key (dates, k));
}
}