Files
2026-03-10 19:14:51 -07:00

359 lines
12 KiB
C

/*
* age-verification-daemon.c
* Implements org.freedesktop.AgeVerification1 on the system D-Bus.
* Runs as root. Stores age data in /var/lib/age-verification/ (root-owned, 0600).
*
* Dependencies: libdbus-1, libsystemd (optional, for sd_notify)
* Build: gcc -o age-verification-daemon age-verification-daemon.c \
* $(pkg-config --cflags --libs dbus-1) -lsystemd
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pwd.h>
#include <limits.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <dbus/dbus.h>
/* Optional systemd notify support */
#ifdef HAVE_SYSTEMD
#include <systemd/sd-daemon.h>
#define NOTIFY_READY() sd_notify(0, "READY=1")
#else
#define NOTIFY_READY() do {} while(0)
#endif
#define STORAGE_DIR "/var/lib/age-verification"
#define STORAGE_FILE STORAGE_DIR "/ages.conf"
#define DBUS_SERVICE "org.freedesktop.AgeVerification1"
#define DBUS_IFACE "org.freedesktop.AgeVerification1"
#define DBUS_PATH "/org/freedesktop/AgeVerification1"
#define ERR_NO_USER "org.freedesktop.AgeVerification1.Error.NoSuchUser"
#define ERR_PERM_DENIED "org.freedesktop.AgeVerification1.Error.PermissionDenied"
#define ERR_INVALID_DATE "org.freedesktop.AgeVerification1.Error.InvalidDate"
#define ERR_AGE_UNDEF "org.freedesktop.AgeVerification1.Error.AgeUndefined"
#define ERR_INTERNAL "org.freedesktop.AgeVerification1.Error.Internal"
/* Age brackets as per California law */
typedef enum {
BRACKET_UNDER_13 = 1,
BRACKET_13_TO_15 = 2,
BRACKET_16_TO_17 = 3,
BRACKET_ADULT = 4
} AgeBracket;
/* ---------------------------------------------------------------------------
* Utility: get UID_MIN / UID_MAX from /etc/login.defs
* --------------------------------------------------------------------------- */
static void get_uid_range(uid_t *uid_min, uid_t *uid_max) {
*uid_min = 1000;
*uid_max = 60000;
FILE *f = fopen("/etc/login.defs", "r");
if (!f) return;
char line[256];
while (fgets(line, sizeof(line), f)) {
unsigned long val;
if (sscanf(line, "UID_MIN %lu", &val) == 1) *uid_min = (uid_t)val;
if (sscanf(line, "UID_MAX %lu", &val) == 1) *uid_max = (uid_t)val;
}
fclose(f);
}
/* ---------------------------------------------------------------------------
* Utility: validate that 'username' is a real, non-system UNIX user
* --------------------------------------------------------------------------- */
static int validate_user(const char *username, uid_t *out_uid) {
uid_t uid_min, uid_max;
get_uid_range(&uid_min, &uid_max);
errno = 0;
struct passwd *pw = getpwnam(username);
if (!pw) return 0;
if (pw->pw_uid < uid_min || pw->pw_uid > uid_max) return 0;
if (out_uid) *out_uid = pw->pw_uid;
return 1;
}
/* ---------------------------------------------------------------------------
* Utility: get the UID of the D-Bus caller
* --------------------------------------------------------------------------- */
static uid_t get_caller_uid(DBusConnection *conn, DBusMessage *msg) {
const char *sender = dbus_message_get_sender(msg);
if (!sender) return (uid_t)-1;
DBusError err;
dbus_error_init(&err);
unsigned long uid = dbus_bus_get_unix_user(conn, sender, &err);
if (dbus_error_is_set(&err)) {
dbus_error_free(&err);
return (uid_t)-1;
}
return (uid_t)uid;
}
/* ---------------------------------------------------------------------------
* Storage: read bracket for a username. Returns 0 if not found.
* File format: one "username=bracket" per line.
* --------------------------------------------------------------------------- */
static int read_bracket(const char *username) {
FILE *f = fopen(STORAGE_FILE, "r");
if (!f) return 0;
char line[512];
int found = 0;
while (fgets(line, sizeof(line), f)) {
/* strip newline */
line[strcspn(line, "\n")] = 0;
char *eq = strchr(line, '=');
if (!eq) continue;
*eq = 0;
if (strcmp(line, username) == 0) {
found = atoi(eq + 1);
break;
}
}
fclose(f);
return found;
}
/* ---------------------------------------------------------------------------
* Storage: write or update bracket for a username
* --------------------------------------------------------------------------- */
static int write_bracket(const char *username, int bracket) {
/* Ensure storage dir exists with correct perms */
struct stat st;
if (stat(STORAGE_DIR, &st) != 0) {
if (mkdir(STORAGE_DIR, 0700) != 0) return 0;
}
/* Read existing entries, skip the one we're updating */
char tmpfile[] = STORAGE_FILE ".tmp.XXXXXX";
int tmpfd = mkstemp(tmpfile);
if (tmpfd < 0) return 0;
fchmod(tmpfd, 0600);
FILE *out = fdopen(tmpfd, "w");
if (!out) { close(tmpfd); return 0; }
FILE *in = fopen(STORAGE_FILE, "r");
if (in) {
char line[512];
while (fgets(line, sizeof(line), in)) {
char copy[512];
strncpy(copy, line, sizeof(copy));
copy[strcspn(copy, "\n")] = 0;
char *eq = strchr(copy, '=');
if (eq) { *eq = 0; }
if (!eq || strcmp(copy, username) != 0) {
fputs(line, out);
}
}
fclose(in);
}
fprintf(out, "%s=%d\n", username, bracket);
fclose(out);
rename(tmpfile, STORAGE_FILE);
chmod(STORAGE_FILE, 0600);
return 1;
}
/* ---------------------------------------------------------------------------
* Age bracket calculation
* --------------------------------------------------------------------------- */
static AgeBracket bracket_from_years(unsigned int years) {
if (years < 13) return BRACKET_UNDER_13;
if (years < 16) return BRACKET_13_TO_15;
if (years < 18) return BRACKET_16_TO_17;
return BRACKET_ADULT;
}
static AgeBracket bracket_from_dob(const char *iso_date) {
/* Parse YYYY-MM-DD */
int y, m, d;
if (sscanf(iso_date, "%d-%d-%d", &y, &m, &d) != 3) return 0;
if (m < 1 || m > 12 || d < 1 || d > 31) return 0;
time_t now = time(NULL);
struct tm *t = gmtime(&now);
int today_y = t->tm_year + 1900;
int today_m = t->tm_mon + 1;
int today_d = t->tm_mday;
int age = today_y - y;
if (today_m < m || (today_m == m && today_d < d)) age--;
if (age < 0) return 0;
return bracket_from_years((unsigned int)age);
}
/* ---------------------------------------------------------------------------
* D-Bus method: SetAge
* --------------------------------------------------------------------------- */
static DBusMessage *handle_set_age(DBusConnection *conn, DBusMessage *msg) {
const char *username;
dbus_uint32_t years;
DBusError err;
dbus_error_init(&err);
if (!dbus_message_get_args(msg, &err,
DBUS_TYPE_STRING, &username,
DBUS_TYPE_UINT32, &years,
DBUS_TYPE_INVALID)) {
DBusMessage *r = dbus_message_new_error(msg, DBUS_ERROR_INVALID_ARGS, err.message);
dbus_error_free(&err);
return r;
}
uid_t target_uid;
if (!validate_user(username, &target_uid))
return dbus_message_new_error(msg, ERR_NO_USER, "No such non-system user");
uid_t caller_uid = get_caller_uid(conn, msg);
if (caller_uid != 0 && caller_uid != target_uid)
return dbus_message_new_error(msg, ERR_PERM_DENIED, "Permission denied");
AgeBracket bracket = bracket_from_years(years);
if (!write_bracket(username, (int)bracket))
return dbus_message_new_error(msg, ERR_INTERNAL, "Failed to write storage");
return dbus_message_new_method_return(msg);
}
/* ---------------------------------------------------------------------------
* D-Bus method: SetDateOfBirth
* --------------------------------------------------------------------------- */
static DBusMessage *handle_set_dob(DBusConnection *conn, DBusMessage *msg) {
const char *username, *date;
DBusError err;
dbus_error_init(&err);
if (!dbus_message_get_args(msg, &err,
DBUS_TYPE_STRING, &username,
DBUS_TYPE_STRING, &date,
DBUS_TYPE_INVALID)) {
DBusMessage *r = dbus_message_new_error(msg, DBUS_ERROR_INVALID_ARGS, err.message);
dbus_error_free(&err);
return r;
}
uid_t target_uid;
if (!validate_user(username, &target_uid))
return dbus_message_new_error(msg, ERR_NO_USER, "No such non-system user");
uid_t caller_uid = get_caller_uid(conn, msg);
if (caller_uid != 0 && caller_uid != target_uid)
return dbus_message_new_error(msg, ERR_PERM_DENIED, "Permission denied");
AgeBracket bracket = bracket_from_dob(date);
if (bracket == 0)
return dbus_message_new_error(msg, ERR_INVALID_DATE, "Invalid ISO8601 date");
if (!write_bracket(username, (int)bracket))
return dbus_message_new_error(msg, ERR_INTERNAL, "Failed to write storage");
return dbus_message_new_method_return(msg);
}
/* ---------------------------------------------------------------------------
* D-Bus method: GetAgeBracket
* --------------------------------------------------------------------------- */
static DBusMessage *handle_get_bracket(DBusConnection *conn, DBusMessage *msg) {
const char *username;
DBusError err;
dbus_error_init(&err);
if (!dbus_message_get_args(msg, &err,
DBUS_TYPE_STRING, &username,
DBUS_TYPE_INVALID)) {
DBusMessage *r = dbus_message_new_error(msg, DBUS_ERROR_INVALID_ARGS, err.message);
dbus_error_free(&err);
return r;
}
uid_t target_uid;
if (!validate_user(username, &target_uid))
return dbus_message_new_error(msg, ERR_NO_USER, "No such non-system user");
uid_t caller_uid = get_caller_uid(conn, msg);
if (caller_uid != 0 && caller_uid != target_uid)
return dbus_message_new_error(msg, ERR_PERM_DENIED, "Permission denied");
int bracket = read_bracket(username);
if (bracket == 0)
return dbus_message_new_error(msg, ERR_AGE_UNDEF, "No age configured for user");
DBusMessage *reply = dbus_message_new_method_return(msg);
dbus_uint32_t val = (dbus_uint32_t)bracket;
dbus_message_append_args(reply, DBUS_TYPE_UINT32, &val, DBUS_TYPE_INVALID);
return reply;
}
/* ---------------------------------------------------------------------------
* D-Bus message dispatch
* --------------------------------------------------------------------------- */
static DBusHandlerResult message_handler(DBusConnection *conn,
DBusMessage *msg, void *data) {
(void)data;
if (dbus_message_get_type(msg) != DBUS_MESSAGE_TYPE_METHOD_CALL)
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
if (!dbus_message_has_interface(msg, DBUS_IFACE))
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
DBusMessage *reply = NULL;
const char *member = dbus_message_get_member(msg);
if (strcmp(member, "SetAge") == 0) reply = handle_set_age(conn, msg);
else if (strcmp(member, "SetDateOfBirth") == 0) reply = handle_set_dob(conn, msg);
else if (strcmp(member, "GetAgeBracket") == 0) reply = handle_get_bracket(conn, msg);
else return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
if (reply) {
dbus_connection_send(conn, reply, NULL);
dbus_message_unref(reply);
}
return DBUS_HANDLER_RESULT_HANDLED;
}
/* ---------------------------------------------------------------------------
* Main
* --------------------------------------------------------------------------- */
int main(void) {
DBusError err;
dbus_error_init(&err);
DBusConnection *conn = dbus_bus_get(DBUS_BUS_SYSTEM, &err);
if (dbus_error_is_set(&err)) {
fprintf(stderr, "D-Bus connection error: %s\n", err.message);
dbus_error_free(&err);
return 1;
}
int ret = dbus_bus_request_name(conn, DBUS_SERVICE,
DBUS_NAME_FLAG_REPLACE_EXISTING, &err);
if (dbus_error_is_set(&err)) {
fprintf(stderr, "Name request error: %s\n", err.message);
dbus_error_free(&err);
return 1;
}
if (ret != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
fprintf(stderr, "Not primary owner of %s\n", DBUS_SERVICE);
return 1;
}
static const DBusObjectPathVTable vtable = {
.message_function = message_handler
};
dbus_connection_register_object_path(conn, DBUS_PATH, &vtable, NULL);
NOTIFY_READY();
fprintf(stdout, "age-verification-daemon running on system bus\n");
while (dbus_connection_read_write_dispatch(conn, -1));
return 0;
}