unl0kr: add a systemd password agent

This commit is contained in:
Vladimir Stoiakin
2024-09-23 16:49:41 +03:00
parent 25aa94b565
commit ee8e31ad4b
8 changed files with 733 additions and 4 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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')

View File

@@ -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

665
unl0kr/unl0kr-agent.c Normal file
View File

@@ -0,0 +1,665 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include <sys/epoll.h>
#include <sys/inotify.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <assert.h>
#include <dirent.h>
#include <errno.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#define INI_STOP_ON_FIRST_ERROR 0 /* Ignore unknown keys */
#include <ini.h>
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;
}

18
unl0kr/unl0kr-agent.path Normal file
View File

@@ -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

View File

@@ -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