359 lines
12 KiB
C
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;
|
|
}
|