/* * 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 #include #include #include #include #include #include #include #include #include #include /* Optional systemd notify support */ #ifdef HAVE_SYSTEMD #include #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; }