From 1510ac9e765dec0b575b04fab1fd0ba9955e5d46 Mon Sep 17 00:00:00 2001 From: Aaron Rainbolt Date: Tue, 1 Oct 2024 00:03:53 -0500 Subject: [PATCH] Add X event buffering for cloaking user input patterns The user may wish to prevent biometric information about their mouse and keyboard patterns from leaking into certain Qubes. To make this possible, this feature inserts random noise into the delivery timing of certain X events, making it more difficult to distinguish the user from other X event buffering users. The maximum delay in event delivery is user-configurable through the "xbuf_max_delay" configuration option. --- gui-common/txrx-vchan.c | 13 +--- gui-daemon/guid.conf | 6 ++ gui-daemon/xside.c | 128 ++++++++++++++++++++++++++++++++++++++-- gui-daemon/xside.h | 12 ++++ include/txrx.h | 2 +- 5 files changed, 144 insertions(+), 17 deletions(-) diff --git a/gui-common/txrx-vchan.c b/gui-common/txrx-vchan.c index 669d48f6..1d22b2da 100644 --- a/gui-common/txrx-vchan.c +++ b/gui-common/txrx-vchan.c @@ -38,8 +38,6 @@ int vchan_is_closed = 0; */ int double_buffered = 1; -static int wait_for_vchan_or_argfd_once(libvchan_t *vchan, int fd); - void vchan_register_at_eof(void (*new_vchan_at_eof)(void)) { vchan_at_eof = new_vchan_at_eof; } @@ -109,7 +107,7 @@ int read_data(libvchan_t *vchan, char *buf, int size) return size; } -static int wait_for_vchan_or_argfd_once(libvchan_t *vchan, int fd) +int wait_for_vchan_or_argfd_once(libvchan_t *vchan, int fd) { int ret; write_data(vchan, NULL, 0); // trigger write of queued data, if any present @@ -117,7 +115,7 @@ static int wait_for_vchan_or_argfd_once(libvchan_t *vchan, int fd) { .fd = libvchan_fd_for_select(vchan), .events = POLLIN, .revents = 0 }, { .fd = fd, .events = POLLIN, .revents = 0 }, }; - ret = poll(fds, fd == -1 ? 1 : 2, 1000); + ret = poll(fds, fd == -1 ? 1 : 2, 1); // short timeout necessary for xbuf if (ret < 0) { if (errno == EINTR) return -1; @@ -140,10 +138,3 @@ static int wait_for_vchan_or_argfd_once(libvchan_t *vchan, int fd) } return ret; } - -int wait_for_vchan_or_argfd(libvchan_t *vchan, int fd) -{ - int ret; - while ((ret=wait_for_vchan_or_argfd_once(vchan, fd)) == 0); - return ret; -} diff --git a/gui-daemon/guid.conf b/gui-daemon/guid.conf index 3f1530a9..58ae107b 100644 --- a/gui-daemon/guid.conf +++ b/gui-daemon/guid.conf @@ -80,4 +80,10 @@ global: { # Timeout when waiting for qubes-gui-agent # # startup_timeout = 45; + + # Maximum delay in milliseconds for X input buffering (used for obfuscating + # biometric data that could be obtained by observing keyboard and mosue + # patterns). Set to 0 to disable buffering entirely. + # + # xbuf_max_delay = 0; } diff --git a/gui-daemon/xside.c b/gui-daemon/xside.c index 947596c8..4c4e95af 100644 --- a/gui-daemon/xside.c +++ b/gui-daemon/xside.c @@ -37,6 +37,8 @@ #include #include #include +#include +#include #include #include #include @@ -112,6 +114,13 @@ static Ghandles ghandles; #define ignore_result(x) do { __typeof__(x) __attribute__((unused)) _ignore=(x); } while (0) +/* xbuf state and definitions */ +TAILQ_HEAD(tailhead, xbuf_entry) xbuf_head; +union xbuf_rand xbuf_rand_data; +long xbuf_lower_bound = 0; +long xbuf_prev_release_time = 0; +long xbuf_max_delay = 0; + static int (*default_x11_io_error_handler)(Display *dpy); static void inter_appviewer_lock(Ghandles *g, int mode); static void release_mapped_mfns(Ghandles * g, struct windowdata *vm_window); @@ -2386,11 +2395,61 @@ static void process_xevent_xembed(Ghandles * g, const XClientMessageEvent * ev) } +/* get current time */ +static long xbuf_current_time_ms() +{ + struct timespec spec; + clock_gettime(CLOCK_MONOTONIC, &spec); + return (spec.tv_sec) * 1000 + (spec.tv_nsec) / 1000000; +} + +/* get double-bounded random number */ +static long xbuf_random_between(long lower, long upper) +{ + long maxval; + long randval; + size_t randsize; + if (lower >= upper) + return upper; + + maxval = upper - lower + 1; + if (maxval > UINT32_MAX) + return UINT32_MAX; + + do { + randsize = getrandom(xbuf_rand_data.raw, sizeof(uint64_t), 0); + if (randsize != sizeof(uint64_t)) + continue; + } while (xbuf_rand_data.val >= UINT64_MAX - (UINT64_MAX % maxval)); + + randval = (long)(xbuf_rand_data.val % maxval); + return lower + randval; +} + +/* queue input event */ +static void xbuf_queue_xevent(XEvent xev) +{ + long current_time; + long random_delay; + struct xbuf_entry *new_xbuf_entry; + + current_time = xbuf_current_time_ms(); + xbuf_lower_bound = min(max(xbuf_prev_release_time - current_time, 0), xbuf_max_delay); + random_delay = xbuf_random_between(xbuf_lower_bound, xbuf_max_delay); + new_xbuf_entry = malloc(sizeof(struct xbuf_entry)); + if (new_xbuf_entry == NULL) { + perror("Could not allocate xbuf_entry:"); + exit(1); + } + new_xbuf_entry->time = current_time + random_delay; + new_xbuf_entry->xev = xev; + TAILQ_INSERT_TAIL(&xbuf_head, new_xbuf_entry, entries); + xbuf_prev_release_time = new_xbuf_entry->time; +} + /* dispatch local Xserver event */ -static void process_xevent(Ghandles * g) +static void process_xevent_core(Ghandles * g, XEvent event_buffer) { - XEvent event_buffer; - XNextEvent(g->display, &event_buffer); switch (event_buffer.type) { case KeyPress: case KeyRelease: @@ -2449,6 +2508,55 @@ static void process_xevent(Ghandles * g) } } +/* dispatch queued events */ +static void xbuf_release_xevents(Ghandles * g) +{ + long current_time; + struct xbuf_entry *current_xbuf_entry; + + current_time = xbuf_current_time_ms(); + while ((current_xbuf_entry = TAILQ_FIRST(&xbuf_head)) + && (current_time >= current_xbuf_entry->time)) { + XEvent event_buffer = current_xbuf_entry->xev; + process_xevent_core(g, event_buffer); + TAILQ_REMOVE(&xbuf_head, current_xbuf_entry, entries); + free(current_xbuf_entry); + } +} + +/* handle or queue local Xserver event */ +static void process_xevent(Ghandles * g) +{ + XEvent event_buffer; + XNextEvent(g->display, &event_buffer); + if (xbuf_max_delay > 0) { + switch (event_buffer.type) { + case ReparentNotify: + case ConfigureNotify: + case EnterNotify: + case LeaveNotify: + case Expose: + case MapNotify: + case PropertyNotify: + case ClientMessage: + process_xevent_core(g, event_buffer); + break; + default: + // Buffered: + // KeyPress + // KeyRelease + // ButtonPress + // ButtonRelease + // MotionNotify + // FocusIn + // FocusOut + xbuf_queue_xevent(event_buffer); + } + } else { + process_xevent_core(g, event_buffer); + } +} + /* handle VM message: MSG_SHMIMAGE * pass message data to do_shm_update - there input validation will be done */ @@ -4242,6 +4350,11 @@ static void parse_vm_config(Ghandles * g, config_setting_t * group) exit(1); } } + + if ((setting = + config_setting_get_member(group, "xbuf_max_delay"))) { + xbuf_max_delay = config_setting_get_int(setting); + } } static void parse_config(Ghandles * g) @@ -4364,11 +4477,13 @@ int main(int argc, char **argv) /* parse cmdline, possibly overriding values from config */ parse_cmdline(&ghandles, argc, argv); get_boot_lock(ghandles.domid); + /* init event queue */ + TAILQ_INIT(&xbuf_head); if (!ghandles.nofork) { // daemonize... if (pipe(pipe_notify) < 0) { - perror("canot create pipe:"); + perror("cannot create pipe:"); exit(1); } @@ -4559,8 +4674,11 @@ int main(int argc, char **argv) handle_message(&ghandles); busy = 1; } + if (xbuf_max_delay > 0) { + xbuf_release_xevents(&ghandles); + } } while (busy); - wait_for_vchan_or_argfd(ghandles.vchan, xfd); + wait_for_vchan_or_argfd_once(ghandles.vchan, xfd); } return 0; } diff --git a/gui-daemon/xside.h b/gui-daemon/xside.h index f799c86e..d2611eca 100644 --- a/gui-daemon/xside.h +++ b/gui-daemon/xside.h @@ -82,6 +82,7 @@ #include #include #include +#include #include #include #include @@ -243,6 +244,17 @@ struct _global_handles { typedef struct _global_handles Ghandles; +struct xbuf_entry { + XEvent xev; + long time; + TAILQ_ENTRY(xbuf_entry) entries; +}; + +union xbuf_rand { + uint64_t val; + char raw[sizeof(uint64_t)]; +}; + #define ASSERT_HEIGHT_UNSIGNED(h) \ do { \ __typeof__(h) _h = (h); \ diff --git a/include/txrx.h b/include/txrx.h index 876da78b..ca410bc3 100644 --- a/include/txrx.h +++ b/include/txrx.h @@ -33,7 +33,7 @@ int read_data(libvchan_t *vchan, char *buf, int size); x.untrusted_len = sizeof(y); \ real_write_message(vchan, (char*)&x, sizeof(x), (char*)&y, sizeof(y)); \ } while(0) -int wait_for_vchan_or_argfd(libvchan_t *vchan, int fd); +int wait_for_vchan_or_argfd_once(libvchan_t *vchan, int fd); void vchan_register_at_eof(void (*new_vchan_at_eof)(void)); #endif /* _QUBES_TXRX_H */