From ee8e31ad4b78db717de3b9dab0a0983ce24de798 Mon Sep 17 00:00:00 2001 From: Vladimir Stoiakin Date: Mon, 23 Sep 2024 16:49:41 +0300 Subject: [PATCH] unl0kr: add a systemd password agent --- CHANGELOG.md | 1 + README.md | 9 +- meson.build | 3 +- meson_options.txt | 1 + unl0kr/meson.build | 24 +- unl0kr/unl0kr-agent.c | 665 +++++++++++++++++++++++++++++++++ unl0kr/unl0kr-agent.path | 18 + unl0kr/unl0kr-agent.service.in | 16 + 8 files changed, 733 insertions(+), 4 deletions(-) create mode 100644 unl0kr/unl0kr-agent.c create mode 100644 unl0kr/unl0kr-agent.path create mode 100644 unl0kr/unl0kr-agent.service.in diff --git a/CHANGELOG.md b/CHANGELOG.md index 0606389..d3936c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ If a change only affects particular applications, they are listed in parentheses ## Unreleased +- feat: Add a systemd password agent (!33, thanks @vstoiakin) - feat: Load config from /usr/share aswell (!26 & !28, thanks @fossdd & @pabloyoyoista) - feat(buffyboard): Add man pages (!23, thanks @Jarrah) - misc: Unify build system (!23 & !29, thanks @Jarrah & @vladimir.stoyakin) diff --git a/README.md b/README.md index 8958818..8928e9e 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,14 @@ $ sudo ./_build/unl0kr/unl0kr # For Unl0kr $ sudo ./_build/buffyboard/buffyboard # For Buffyboard ``` -With meson <0\.55 use `ninja` instead of `meson compile`\. +On distributions based on systemd, `unl0kr` can be used as a [password agent](https://systemd.io/PASSWORD_AGENTS/), to give systemd the ability to ask passwords on touchscreen-only devices: + +``` +# systemctl stop systemd-ask-password-console.path +# systemctl stop systemd-ask-password-wall.path +# systemctl start unl0kr-agent.path +# systemd-ask-password --no-tty # Unl0kr is started +``` ## Making a release diff --git a/meson.build b/meson.build index df0162f..042ab15 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project('buffybox', 'c', version: '3.2.0', - default_options: 'warning_level=3', + default_options: ['warning_level=3', 'b_ndebug=if-release'], meson_version: '>= 0.59.0' ) @@ -9,6 +9,7 @@ add_project_arguments('-DPROJECT_VERSION="@0@"'.format(meson.project_version()), depinih = dependency('inih') deplibinput = dependency('libinput') deplibudev = dependency('libudev') +depxkbcommon = dependency('xkbcommon') # For unl0kr only if get_option('man') depscdoc = dependency('scdoc', native: true) diff --git a/meson_options.txt b/meson_options.txt index 5a1bc13..4fc1e4b 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,3 +1,4 @@ option('with-drm', type: 'feature', value: 'auto', description: 'Enable DRM backend') option('man', type: 'boolean', value: true, description: 'Install manual pages') option('systemd-buffyboard-service', type: 'feature', value: 'auto', description: 'Install systemd service file for buffyboard') +option('systemd-password-agent', type: 'feature', value: 'auto', description: 'Build a systemd password agent for touchscreens') diff --git a/unl0kr/meson.build b/unl0kr/meson.build index 076ce0b..04bdc8e 100644 --- a/unl0kr/meson.build +++ b/unl0kr/meson.build @@ -1,8 +1,6 @@ # Copyright 2021 Clayton Craft # SPDX-License-Identifier: GPL-3.0-or-later -depxkbcommon = dependency('xkbcommon') - unl0kr_sources = files( 'backends.c', 'command_line.c', @@ -32,3 +30,25 @@ executable('unl0kr', install_data('unl0kr.conf', install_dir: get_option('sysconfdir')) +depsystemd = dependency('systemd', required: get_option('systemd-password-agent')) +if depsystemd.found() + executable('unl0kr-agent', + sources: files('unl0kr-agent.c'), + dependencies: depinih, + c_args: '-DUNL0KR_BINARY="@0@"'.format(get_option('prefix') / get_option('bindir') / 'unl0kr'), + install: true, + install_dir: get_option('libexecdir') + ) + + system_unit_dir = depsystemd.get_variable(pkgconfig: 'systemd_system_unit_dir') + + install_data('unl0kr-agent.path', install_dir: system_unit_dir) + + configure_file( + configuration: {'LIBEXECDIR': get_option('prefix') / get_option('libexecdir')}, + input: 'unl0kr-agent.service.in', + output: 'unl0kr-agent.service', + install: true, + install_dir: system_unit_dir + ) +endif diff --git a/unl0kr/unl0kr-agent.c b/unl0kr/unl0kr-agent.c new file mode 100644 index 0000000..64c759b --- /dev/null +++ b/unl0kr/unl0kr-agent.c @@ -0,0 +1,665 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define INI_STOP_ON_FIRST_ERROR 0 /* Ignore unknown keys */ + +#include + +struct Request +{ + uint64_t not_after; + char* file; + char* socket; + char* message; + char* icon; + pid_t pid; + bool accept_cached; + bool echo; + bool silent; +}; + +void Request_init(struct Request* req) +{ + req->not_after = 0; + req->file = NULL; + req->socket = NULL; + req->message = NULL; + req->icon = NULL; + req->pid = 0; + req->accept_cached = false; + req->echo = false; + req->silent = false; +} + +void Request_free(struct Request* req) +{ + if (req->file) + free(req->file); + if (req->socket) + free(req->socket); + if (req->message) + free(req->message); + if (req->icon) + free(req->icon); +} + +void Request_reset(struct Request* req) +{ + Request_free(req); + Request_init(req); +} + +struct Request request; +int fd_epoll, fd_inotify; +pid_t pid_unl0kr; +bool unl0kr_exited; + +timer_t id_timer; +enum { + NO_ACTION, + TERMINATE_UNL0KR, + KILL_UNL0KR +} timer_action; +bool timer_expired; + +void erase_and_free(char* p) +{ + const size_t length = strlen(p); + for (size_t i = 0; i < length; i++) + p[i] = 0; + + free(p); +} + +int send_password(const char *password) +{ + int fd_socket = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0); + if (fd_socket < 0) { + int ret = errno; + perror("socket() is failed"); + return ret; + } + + struct sockaddr_un address; + address.sun_family = AF_UNIX; + strncpy(address.sun_path, request.socket, sizeof(address.sun_path) - 1); + address.sun_path[sizeof(address.sun_path) - 1] = 0; + + ssize_t n = sendto(fd_socket, password, strlen(password), MSG_NOSIGNAL, (const struct sockaddr*) &address, sizeof(address)); + if (n < 0) { + int ret = errno; + perror("sendto() is failed"); + close(fd_socket); + return ret; + } + + close(fd_socket); + return 0; +} + +bool to_bool(const char* value) +{ + if (strcmp(value, "true") == 0) { + return true; + } else if (strcmp(value, "1") == 0) { + return true; + } else if (strcmp(value, "yes") == 0) { + return true; + } else if (strcmp(value, "false") == 0) { + return false; + } else if (strcmp(value, "0") == 0) { + return false; + } else if (strcmp(value, "no") == 0) { + return false; + } else if (strcmp(value, "") == 0) { + return false; + } else { + fprintf(stderr, "The value '%s' is not a boolean\n", value); + return false; + } +} + +int ini_parser(void* user, const char* section, const char* name, const char* value) +{ + struct Request* d = (struct Request*) user; + + if (strcmp(section, "Ask") != 0) { + fprintf(stderr, "The ini file contains unknown section: %s\n", section); + return 0; + } + + if (strcmp(name, "NotAfter") == 0) { + d->not_after = atol(value); + } else if (strcmp(name, "Socket") == 0) { + d->socket = strdup(value); + } else if (strcmp(name, "Message") == 0) { + d->message = strdup(value); + } else if (strcmp(name, "Icon") == 0) { + d->icon = strdup(value); + } else if (strcmp(name, "PID") == 0) { + d->pid = atoi(value); + } else if (strcmp(name, "AcceptCached") == 0) { + d->accept_cached = to_bool(value); + } else if (strcmp(name, "Echo") == 0) { + d->echo = to_bool(value); + } else if (strcmp(name, "Silent") == 0) { + d->silent = to_bool(value); + } else { + fprintf(stderr, "The ini file contains unknown key: %s\n", name); + return 0; + } + + return 1; +} + +int find_request(char** ret_file) +{ + const char* ask_folder = "/run/systemd/ask-password"; + const size_t ask_folder_length = strlen(ask_folder); + + DIR* dir = opendir(ask_folder); + if (!dir) { + int ret = errno; + if (errno != ENOENT) { + fprintf(stderr, "Can't open '%s': %s\n", ask_folder, strerror(errno)); + } + return ret; + } + + struct dirent* entry; + while ((entry = readdir(dir))) { + if (entry->d_type != DT_REG && entry->d_type != DT_LNK) + continue; + + if (strncmp(entry->d_name, "ask.", 4) != 0) + continue; + + break; + } + if (!entry) { + closedir(dir); + return ENOENT; + } + + char* file = malloc(ask_folder_length + 1 + strlen(entry->d_name) + 1); + if (!file) { + closedir(dir); + fprintf(stderr, "Out of memory\n"); + return ENOMEM; + } + + strcpy(file, ask_folder); + strcpy(file + ask_folder_length, "/"); + strcpy(file + ask_folder_length + 1, entry->d_name); + + closedir(dir); + + *ret_file = file; + return 0; +} + +int process_inotify_events() +{ + /* We expect only IN_DELETE_SELF and IN_IGNORED */ + size_t buffer_size = sizeof(struct inotify_event) * 2; + uint8_t buffer[buffer_size]; + + ssize_t block_size = read(fd_inotify, buffer, buffer_size); + if (block_size < 0) { + int ret = errno; + perror("read() is failed"); + return ret; + } + + assert((size_t) block_size == buffer_size); + + struct inotify_event* ievent1 = (struct inotify_event*) buffer; + struct inotify_event* ievent2 = ievent1 + 1; + + assert(ievent1->mask & IN_DELETE_SELF); + assert(ievent2->mask & IN_IGNORED); + + assert(read(fd_inotify, buffer, buffer_size) == -1 && errno == EAGAIN); // no more events + return 0; +} + +void sigalarm(int signo, siginfo_t *info, void *context) +{ + assert(signo == SIGALRM); + (void)(context); + + if (info->si_code == SI_TIMER) { + assert(info->si_value.sival_ptr == &id_timer); + timer_expired = true; + } + + switch (timer_action) { + case TERMINATE_UNL0KR: + if (kill(pid_unl0kr, SIGTERM) == 0) { + struct itimerspec spec; + spec.it_interval.tv_sec = 0; + spec.it_interval.tv_nsec = 0; + spec.it_value.tv_sec = 5; + spec.it_value.tv_nsec = 0; + timer_settime(id_timer, 0, &spec, NULL); + timer_action = KILL_UNL0KR; + } + break; + case KILL_UNL0KR: + kill(pid_unl0kr, SIGKILL); + break; + default: + break; + } +} + +void sigchild(int signo, siginfo_t *info, void *context) +{ + assert(signo == SIGCHLD); + (void)(context); + + if (pid_unl0kr == 0) + return; + + assert(info->si_pid == pid_unl0kr); + + if (info->si_code == CLD_EXITED || + info->si_code == CLD_KILLED || + info->si_code == CLD_DUMPED ) { + unl0kr_exited = true; + } +} + +int event_loop(pid_t pid) +{ + int ret = 0; + int r; + + if (request.not_after != 0) { + struct itimerspec spec; + spec.it_interval.tv_sec = 0; + spec.it_interval.tv_nsec = 0; + spec.it_value.tv_sec = request.not_after / 1000000; + spec.it_value.tv_nsec = (request.not_after % 1000000) * 1000; + r = timer_settime(id_timer, TIMER_ABSTIME, &spec, NULL); + if (r == -1) + perror("timer_settime() is failed"); + } + + struct epoll_event event; + pid_unl0kr = pid; + timer_action = TERMINATE_UNL0KR; + timer_expired = false; + unl0kr_exited = false; + + sigset_t sigmask; + sigemptyset(&sigmask); + + for (;;) { + r = epoll_pwait(fd_epoll, &event, 1, -1, &sigmask); + if (r == -1) { + if (errno != EINTR) { + ret = errno; + perror("epoll_pwait() is failed"); + break; + } + + if (unl0kr_exited) + break; + + if (timer_expired && ret != ECANCELED) + ret = ETIME; + + continue; + } + + r = process_inotify_events(); + if (r != 0) { + ret = errno; + break; + } + + ret = ECANCELED; + + if (timer_expired) + continue; + + if (request.not_after != 0) { + struct itimerspec spec; + spec.it_interval.tv_sec = 0; + spec.it_interval.tv_nsec = 0; + spec.it_value.tv_sec = 0; + spec.it_value.tv_nsec = 0; + r = timer_settime(id_timer, 0, &spec, NULL); + if (r == -1) + perror("Disarming the timer is failed"); + } + + r = raise(SIGALRM); + if (r != 0) { + ret = errno; + perror("raise() is failed"); + break; + } + } + + timer_action = NO_ACTION; + pid_unl0kr = 0; + + /* Stop the timer unconditionally because it can be armed by sigalarm() */ + struct itimerspec spec; + spec.it_interval.tv_sec = 0; + spec.it_interval.tv_nsec = 0; + spec.it_value.tv_sec = 0; + spec.it_value.tv_nsec = 0; + r = timer_settime(id_timer, 0, &spec, NULL); + if (r == -1) + perror("Disarming the timer is failed"); + + return ret; +} + +int exec_unl0kr(char** ret_password) +{ + int ret = 0; + int r; + + int fd_pipe[2]; + if (pipe(fd_pipe) != 0) { + ret = errno; + perror("Can't create a pipe"); + return ret; + } + + sigset_t used_signals; + sigemptyset(&used_signals); + sigaddset(&used_signals, SIGCHLD); + sigaddset(&used_signals, SIGALRM); + + r = sigprocmask(SIG_BLOCK, &used_signals, NULL); + if (r == -1) { + ret = errno; + perror("sigprocmask(SIG_BLOCK) is failed"); + goto exit1; + } + + pid_t pid = fork(); + if (pid == -1) { + ret = errno; + perror("fork() is failed"); + goto exit2; + } + if (pid == 0) { + /* Child */ + close(fd_pipe[0]); + + if (dup2(fd_pipe[1], STDOUT_FILENO) == -1) { + perror("dup2() is failed"); + exit(EXIT_FAILURE); + } + + execl(UNL0KR_BINARY, "unl0kr", (char*) 0); + + perror("exec() is failed"); + exit(EXIT_FAILURE); + } + + /* Parent */ + r = event_loop(pid); + + int status; + if (waitpid(pid, &status, 0) == -1) { + ret = errno; + perror("waitpid() is failed"); + goto exit2; + } + + if (r != 0) { + ret = r; + goto exit2; + } + + if (!WIFEXITED(status)) { + ret = ECHILD; + fprintf(stderr, "unl0kr terminated abnormally\n"); + goto exit2; + } + + int password_size; + if (ioctl(fd_pipe[0], FIONREAD, &password_size) == -1) { + ret = errno; + perror("ioctl() is failed"); + goto exit2; + } + + char* password = malloc(1 + password_size + 1); + if (!password) { + ret = ENOMEM; + fprintf(stderr, "Out of memory\n"); + goto exit2; + } + + password[0] = '+'; + if (password_size != 0) { + password_size = read(fd_pipe[0], password + 1, password_size); + if (password_size == -1) { + ret = errno; + perror("read() is failed"); + free(password); + goto exit2; + } + } + password[1 + password_size] = 0; + + *ret_password = password; +exit2: + r = sigprocmask(SIG_UNBLOCK, &used_signals, NULL); + if (r == -1) + perror("sigprocmask(SIG_UNBLOCK) is failed"); +exit1: + close(fd_pipe[0]); + close(fd_pipe[1]); + return ret; +} + +int wait_for_file_removed() +{ + struct epoll_event event; + + int r = epoll_wait(fd_epoll, &event, 1, 20000); + if (r == -1) { + int ret = errno; + perror("epoll_wait() is failed"); + return ret; + } else if (r == 0) { + fprintf(stderr, "The file '%s' was not removed as expected, exiting.\n", request.file); + return ETIME; + } + + return process_inotify_events(); +} + +int main() +{ + int exit_code = EXIT_SUCCESS; + int r; + + Request_init(&request); + + fd_epoll = epoll_create1(EPOLL_CLOEXEC); + if (fd_epoll == -1) { + perror("epoll_create1() is failed"); + exit_code = EXIT_FAILURE; + goto exit1; + } + + fd_inotify = inotify_init1(IN_NONBLOCK|IN_CLOEXEC); + if (fd_inotify == -1) { + perror("inotify_init1() is failed"); + exit_code = EXIT_FAILURE; + goto exit2; + } + + struct epoll_event epevent_inotify; + epevent_inotify.events = EPOLLIN|EPOLLET; + epevent_inotify.data.fd = fd_inotify; + + r = epoll_ctl(fd_epoll, EPOLL_CTL_ADD, fd_inotify, &epevent_inotify); + if (r == -1) { + perror("epoll_ctl() is failed"); + exit_code = EXIT_FAILURE; + goto exit3; + } + + struct sigevent sigevent_timer; + sigevent_timer.sigev_notify = SIGEV_SIGNAL; + sigevent_timer.sigev_signo = SIGALRM; + sigevent_timer.sigev_value.sival_ptr = &id_timer; + + r = timer_create(CLOCK_MONOTONIC, &sigevent_timer, &id_timer); + if (r == -1) { + perror("timer_create() is failed"); + exit_code = EXIT_FAILURE; + goto exit3; + } + + struct sigaction sigaction_alarm; + sigaction_alarm.sa_sigaction = sigalarm; + sigaction_alarm.sa_flags = SA_SIGINFO; + sigemptyset(&sigaction_alarm.sa_mask); + sigaddset(&sigaction_alarm.sa_mask, SIGCHLD); + + r = sigaction(SIGALRM, &sigaction_alarm, NULL); + if (r == -1) { + perror("sigaction() for SIGALRM is failed"); + exit_code = EXIT_FAILURE; + goto exit4; + } + + struct sigaction sigaction_child; + sigaction_child.sa_sigaction = sigchild; + sigaction_child.sa_flags = SA_SIGINFO | SA_NOCLDSTOP; + sigemptyset(&sigaction_child.sa_mask); + sigaddset(&sigaction_child.sa_mask, SIGALRM); + + r = sigaction(SIGCHLD, &sigaction_child, NULL); + if (r == -1) { + perror("sigaction() for SIGCHLD is failed"); + exit_code = EXIT_FAILURE; + goto exit4; + } + + for (;;) { + char* file; + r = find_request(&file); + if (r != 0) { + if (r != ENOENT) + exit_code = EXIT_FAILURE; + break; + } + + int wd_inotify = inotify_add_watch(fd_inotify, file, IN_DELETE_SELF | IN_DONT_FOLLOW); + if (wd_inotify == -1) { + fprintf(stderr, "inotify_add_watch() is failed for '%s': %s\n", file, strerror(errno)); + free(file); + exit_code = EXIT_FAILURE; + break; + } + + Request_reset(&request); + request.file = file; + + r = ini_parse(file, ini_parser, &request); + if (r < 0) { + fprintf(stderr, "The file '%s' can't be parsed: %d\n", request.file, r); + exit_code = EXIT_FAILURE; + break; + } + + if (request.pid != 0) { + r = kill(request.pid, 0); + if (r == -1 && errno == ESRCH) { + fprintf(stderr, "The file '%s' contains invalid PID, removing.\n", request.file); + remove(request.file); + goto loop1; + } + } + + if (!request.socket) { + fprintf(stderr, "The file '%s' doesn't contain a socket, waiting for removal.\n", request.file); + goto loop1; + } + + if (request.not_after != 0) { + struct timespec ts; + r = clock_gettime(CLOCK_MONOTONIC, &ts); + if (r == -1) { + perror("clock_gettime() is failed"); + exit_code = EXIT_FAILURE; + break; + } + uint64_t now = ts.tv_sec * 1000000 + ts.tv_nsec / 1000; + if (request.not_after <= now) { + fprintf(stderr, "The request '%s' expired, waiting for removal.\n", request.file); + goto loop1; + } + } + + char* password; + r = exec_unl0kr(&password); + if (r != 0) { + if (r == ECANCELED) + continue; + else if (r == ETIME) { + send_password("-"); + goto loop1; + } else { + exit_code = EXIT_FAILURE; + break; + } + } + + r = send_password(password); + erase_and_free(password); + if (r != 0) { + exit_code = EXIT_FAILURE; + break; + } + +loop1: + r = wait_for_file_removed(); + if (r != 0) { + exit_code = EXIT_FAILURE; + break; + } + } + +exit4: + timer_delete(id_timer); +exit3: + close(fd_inotify); +exit2: + close(fd_epoll); +exit1: + Request_free(&request); + return exit_code; +} diff --git a/unl0kr/unl0kr-agent.path b/unl0kr/unl0kr-agent.path new file mode 100644 index 0000000..d63497d --- /dev/null +++ b/unl0kr/unl0kr-agent.path @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +[Unit] +Description=Dispatch Password Requests to unl0kr Directory Watch + +ConditionPathExists=!/run/plymouth/pid + +DefaultDependencies=no +After=plymouth-start.service +Before=paths.target cryptsetup.target +Conflicts=emergency.service +Before=emergency.service +Conflicts=shutdown.target +Before=shutdown.target + +[Path] +DirectoryNotEmpty=/run/systemd/ask-password +MakeDirectory=yes diff --git a/unl0kr/unl0kr-agent.service.in b/unl0kr/unl0kr-agent.service.in new file mode 100644 index 0000000..d42b002 --- /dev/null +++ b/unl0kr/unl0kr-agent.service.in @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +[Unit] +Description=Dispatch Password Requests to unl0kr + +ConditionPathExists=!/run/plymouth/pid + +DefaultDependencies=no +After=plymouth-start.service +Conflicts=emergency.service +Before=emergency.service +Conflicts=shutdown.target initrd-switch-root.target +Before=shutdown.target initrd-switch-root.target + +[Service] +ExecStart=@LIBEXECDIR@/unl0kr-agent