From c6b22f5e9484537d76c25b78f5881377aefd5b9b Mon Sep 17 00:00:00 2001 From: Justus Piater Date: Mon, 3 Jun 2019 21:10:43 +0200 Subject: [PATCH] first public version --- .gitignore | 3 + Makefile | 15 +++++ NOTES | 42 +++++++++++++ README.md | 37 +++++++++++- invkeymap.h | 25 ++++++++ keymap.c | 162 +++++++++++++++++++++++++++++++++++++++++++++++++++ keymap.h | 21 +++++++ main.c | 120 ++++++++++++++++++++++++++++++++++++++ tuinputchars | 9 +++ uinput.c | 97 ++++++++++++++++++++++++++++++ uinput.h | 25 ++++++++ 11 files changed, 555 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 NOTES create mode 100644 invkeymap.h create mode 100644 keymap.c create mode 100644 keymap.h create mode 100644 main.c create mode 100755 tuinputchars create mode 100644 uinput.c create mode 100644 uinput.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..13ff817 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.d +*.o +uinputchars diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a8f4ffa --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +CC = gcc +CFLAGS = -MMD -MP -Wall + +SRC=$(wildcard *.c) +OBJ=$(SRC:.c=.o) +DEP=$(SRC:.c=.d) + +uinputchars: $(OBJ) + $(CC) $(LDFLAGS) -o $@ $^ + +-include $(DEP) + +.PHONY: clean +clean: + rm -f $(OBJ) $(DEP) uinputchars diff --git a/NOTES b/NOTES new file mode 100644 index 0000000..7ec00f5 --- /dev/null +++ b/NOTES @@ -0,0 +1,42 @@ +This program reads a character string and translates it into virtual +keystrokes that are entered into the system via /dev/uinput. + +The key difficulty (pun intended) is to map characters to keystrokes. +These are the principal bits and pieces involved, along with +highly-simplified descriptions: + +- Character Encoding: This defines the representation of a character + by a numeric value (or a sequence of numeric values) in the input + character string. On modern, western systems, a typical character + encoding is UTF-8. + +- Kernel Keymap: Each key on the keyboard has an associated Keycode + (or Event Code, defined as KEY_... macros in + linux/input-event-codes.h). The Kernel Keymap maps a Keycode (or a + combination of Keycodes, if modifier or compose keys are pressed) to + an Action Code representing a character (or some other action, + defined as K_... macros in linux/keyboard.h). This mapping is + determined by the active keymap (as loaded by the loadkeys utility). + +- The mapping from Action Code to characters is quite complicated and + can be traced in dumpkeys utility. It appears that for ISO-8859-1 + characters (0..255), the lower byte of the Action Code equals the + ISO-8859-1 (= UTF-8) character code. + + Thus, for ISO-8859-1 characters, this mapping is essentially the + identity mapping. This is all this program implements so far; + therefore, it is currently limited to ISO-8859-1 characters. + + Generally, the Action Code of a character appear to be closely + related to their Unicode code point. + +To enter a character via /dev/uinput, a corresponding sequence of +Keycodes is written to this device. This can be done via the +following steps: + +1. Map the character to its corresponding Action Code. + +2. Map the Action Code to a corresponding (sequence of) Keycode(s) via + an inverse keyboard mapping. + +3. Write the Keycode(s) to /dev/uinput. diff --git a/README.md b/README.md index 305f85b..a4e996d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,37 @@ # uinputchars -A utility to type character strings into /dev/uinput +A Linux utility to type character strings into `/dev/uinput` + + +## Usage + +A typical use case is [entering +passwords](https://github.com/clawoflight/puma). In contrast to +classical password managers, uinputchars can type character strings +into anything - Web forms, e-mail clients, Emacs - without any client +support. In contrast to xdotool, it does not require X; it also works +under Wayland or even on the console. + +Generally, uinputchars is run without arguments. + +Character strings are read from standard input. There is currently no +option to pass them on the command line because this would make them +appear in the process table. + +The current implementation only accepts ISO-8859-1 characters (encoded +as specified by the locale). Depending on the kernel keymap in use, +only a subset of them may be accessible. + +The uinputchars utility needs to retrieve the kernel keymap and to +write into `/dev/uinput`; both will generally require root rights. It +is recommended to configure this in `/etc/sudoers`. + +Applications receiving events from `/dev/uinput` must be given time to +process them. This is achieved by brief sleeps that can be adjusted +via command-line options. + + +## Installation + +Under Linux, just run (GNU) make, and move or link the +resulting executable to wherever you want. If this does not work, I +welcome your patches. diff --git a/invkeymap.h b/invkeymap.h new file mode 100644 index 0000000..4579719 --- /dev/null +++ b/invkeymap.h @@ -0,0 +1,25 @@ +/* Copyright 2019 Justus Piater */ +/* This file is part of UINPUTCHARS. + + UINPUTCHARS is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + UINPUTCHARS is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with UINPUTCHARS. If not, see . */ + +#ifndef INVKEYMAP_H +#define INVKEYMAP_H + +typedef struct { + unsigned char modifiers; + unsigned char principal; +} keyvent; + +#endif diff --git a/keymap.c b/keymap.c new file mode 100644 index 0000000..7eda0cb --- /dev/null +++ b/keymap.c @@ -0,0 +1,162 @@ +/* Copyright 2019 Justus Piater, with one exception documented below */ +/* This file is part of UINPUTCHARS. + + UINPUTCHARS is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + UINPUTCHARS is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with UINPUTCHARS. If not, see . */ + +/* Some documentation relevant to this code: + man 4 console_ioctl + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "keymap.h" + +#define MAX_KEYVENTS 0x100 +#define LINEFEED 0x0a + + +/* The two functions is_a_console() and getfd() below evolved from + code copied from kbd/src/getfd.c (GPL; Copyright (C) 1994-1999 + Andries E. Brouwer) from http://kbd-project.org/. + + This reused GPL (v2 or later) code is one reason why all parts of + UINPUTCHARS are released under the GPL. */ + +static int is_a_console(int fd) { + char arg = 0; + return (isatty(fd) && + ioctl(fd, KDGKBTYPE, &arg) == 0 && + ((arg == KB_101) || (arg == KB_84)) ); +} + + +static int getfd() { + static char *conspath[] = + { "/proc/self/fd/0", + "/dev/tty", + "/dev/tty0", + "/dev/vc/0", + "/dev/systty", + "/dev/console", + NULL }; + + for (int i = 0; conspath[i]; i++) { + /* Permissions don't matter: */ + int fd = open(conspath[i], O_RDWR); + if (fd < 0) fd = open(conspath[i], O_WRONLY); + if (fd < 0) fd = open(conspath[i], O_RDONLY); + if (fd >= 0) { + if (is_a_console(fd)) + return fd; + close(fd); + } + } + + for (int fd = 0; fd < 3; fd++) + if (is_a_console(fd)) + return fd; + + fprintf(stderr, + "Couldn't get a file descriptor referring to the console\n"); + return -1; +} + + +static void print_keyventry(unsigned int c, unsigned char modifiers, + unsigned char principal) { + printf("[%02x=%c %02x %3u]\n", c, c, modifiers, principal); +} + +static void print_keyvent(const keyvent* invkeymap, unsigned char c) { + print_keyventry(c, invkeymap[c].modifiers, invkeymap[c].principal); +} + + +void print_invkeymap(const keyvent* invkeymap) { + for (int c = 0; c < MAX_KEYVENTS; c++) { + if (invkeymap[c].principal) { + printf("%02x ", c); + print_keyvent(invkeymap, c); + } + } +} + + +static void get_keys(int fd, keyvent* invkeymap) { + /* This loop was inspired by lk_kernel_keys() + in kbd/src/libkeymap/kernel.c (GPL) from http://kbd-project.org/. */ + for (int table = 0; table < MAX_NR_KEYMAPS; table++) { + for (int index = 0; index < NR_KEYS; index++) { + struct kbentry kbe; + kbe.kb_table = table; + kbe.kb_index = index; + kbe.kb_value = 0; + + if (ioctl(fd, KDGKBENT, (unsigned long)&kbe)) + fprintf(stderr, "KDGKBENT: %s: error at index %d in table %d\n", + strerror(errno), index, table); + + if (!index && kbe.kb_value == K_NOSUCHMAP) + break; + + if (KTYP(kbe.kb_value) == KT_LATIN || + KTYP(kbe.kb_value) == KT_LETTER ) { + int charvalue = KVAL(kbe.kb_value); + if (!invkeymap[charvalue].principal) { + /* Prefer lower-valued tables */ + invkeymap[charvalue].modifiers = table; + invkeymap[charvalue].principal = index; + } + } + else if (kbe.kb_value == K_ENTER && + !invkeymap[LINEFEED].principal) { + invkeymap[LINEFEED].modifiers = table; + invkeymap[LINEFEED].principal = index; + } + } + } +} + + +keyvent* get_invkeymap() { + int fd = getfd(); + if (fd < 0) + return NULL; + + keyvent* invkeymap = calloc(MAX_KEYVENTS, sizeof(*invkeymap)); + if (!invkeymap) { + perror("invkeymap"); + return NULL; + } + + get_keys(fd, invkeymap); + close(fd); + + return invkeymap; +} + + +void release_invkeymap(keyvent* invkeymap) { + free(invkeymap); +} diff --git a/keymap.h b/keymap.h new file mode 100644 index 0000000..09b1632 --- /dev/null +++ b/keymap.h @@ -0,0 +1,21 @@ +/* Copyright 2019 Justus Piater */ +/* This file is part of UINPUTCHARS. + + UINPUTCHARS is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + UINPUTCHARS is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with UINPUTCHARS. If not, see . */ + +#include "invkeymap.h" + +keyvent* get_invkeymap(void); +void release_invkeymap(keyvent* invkeymap); +void print_invkeymap(const keyvent* invkeymap); diff --git a/main.c b/main.c new file mode 100644 index 0000000..8cc4aa7 --- /dev/null +++ b/main.c @@ -0,0 +1,120 @@ +/* Copyright 2019 Justus Piater */ +/* This file is part of UINPUTCHARS. + + UINPUTCHARS is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + UINPUTCHARS is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with UINPUTCHARS. If not, see . */ + +#include +#include +#include +#include +#include +#include +#include "keymap.h" +#include "uinput.h" + + +static void print_help(const char* argv0, struct uinput_opts_t* uinput_opts) { + printf("Usage: %s [options]\n" + "Reads characters from stdin and types them into /dev/uinput.\n" + "Currently limited to ISO-8859-1 characters. Depending on the " + "currently-active\nkernel keymap, only a subset of these may be " + "available.\nMust typically be run as root.\n" + "Options:\n" + "-h --help display this help and exit\n" + "-p --pre-pause milliseconds to wait after /dev/uinput setup " + "(%lu ms)\n" + "-i --inter-pause milliseconds to wait between characters typed " + "(%lu ms)\n" + "-d --dump display inverse keymap and exit\n", + argv0, + uinput_opts->pre_pause / 1000, uinput_opts->inter_pause / 1000); +} + + +int main(int argc, char* argv[]) { + struct uinput_opts_t uinput_opts = { 200000, 10000 }; + int dump = 0; + + static struct option options[] = + { { "help", no_argument, NULL, 'h' }, + { "pre-pause", required_argument, NULL, 'p' }, + { "inter-pause", required_argument, NULL, 'i' }, + { "dump", no_argument, NULL, 'd' }, + { NULL, 0, NULL, 0 } }; + while (1) { + int c = getopt_long(argc, argv, "hp:i:d", options, NULL); + if (c == -1) + break; + switch (c) { + case 'h': + print_help(argv[0], &uinput_opts); + exit(0); + case 'p': + uinput_opts.pre_pause = strtoul(optarg, NULL, 10) * 1000; + break; + case 'i': + uinput_opts.inter_pause = strtoul(optarg, NULL, 10) * 1000; + break; + case 'd': + dump = 1; + break; + case '?': + exit(1); + default: + fprintf(stderr, "unrecognized option: %c\n", c); + exit(1); + } + } + + keyvent* invkeymap = get_invkeymap(); + if (!invkeymap) + exit(2); + + if (dump) { + print_invkeymap(invkeymap); + release_invkeymap(invkeymap); + exit(0); + } + + int result = 0; + + setlocale(LC_CTYPE, ""); + iconv_t cd = iconv_open("ISO-8859-1", ""); + + const int bufsize = 1024; + char localebuf[bufsize]; + while (fgets(localebuf, bufsize, stdin)) { + char latin1buf[1024]; + size_t inbytesleft = strlen(localebuf); + size_t outbytesleft = bufsize; + char* inbuf = localebuf; + char* outbuf = latin1buf; + size_t converted = iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft); + *outbuf = 0; + if (converted == (size_t)-1) { + perror("Error: iconv"); + result = 3; + break; + } + + if (emit_string(latin1buf, invkeymap, &uinput_opts)) { + result = 4; + break; + } + } + + iconv_close(cd); + release_invkeymap(invkeymap); + exit(result); +} diff --git a/tuinputchars b/tuinputchars new file mode 100755 index 0000000..a9cef44 --- /dev/null +++ b/tuinputchars @@ -0,0 +1,9 @@ +#!/bin/sh + +for out in "$@"; do + echo $out | sudo ./uinputchars + read in + if [ $in != $out ]; then + echo "$out != $in" + fi +done diff --git a/uinput.c b/uinput.c new file mode 100644 index 0000000..d5fa20e --- /dev/null +++ b/uinput.c @@ -0,0 +1,97 @@ +/* Copyright 2019 Justus Piater */ +/* This file is part of UINPUTCHARS. + + UINPUTCHARS is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + UINPUTCHARS is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with UINPUTCHARS. If not, see . */ + +/* Some documentation relevant to this code: + https://www.kernel.org/doc/html/v5.1/input/uinput.html */ + +#include +#include +#include +#include +#include +#include + +#include "uinput.h" + + +static void emit(int fd, int type, int code, int val) +{ + struct input_event ie = {}; + ie.type = type; + ie.code = code; + ie.value = val; + + if (write(fd, &ie, sizeof(ie)) < sizeof(ie)) + perror("emit"); +} + + +int emit_string(const char* string, const keyvent* invkeymap, + const struct uinput_opts_t* uinput_opts) { + int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK); + if (fd < 0) { + perror("open"); + return -1; + } + + ioctl(fd, UI_SET_EVBIT, EV_KEY); + int nchars = strlen(string); + for (int i = 0; i < nchars; i++) { + unsigned char principal = + invkeymap[(int)(unsigned char)string[i]].principal; + if (principal) + ioctl(fd, UI_SET_KEYBIT, principal); + else { + fprintf(stderr, "Error: no key for character 0x%02x=%c\n", + (unsigned char)string[i], string[i]); + close(fd); + return -1; + } + } + ioctl(fd, UI_SET_KEYBIT, KEY_LEFTSHIFT); + ioctl(fd, UI_SET_KEYBIT, KEY_RIGHTALT); + + struct uinput_setup usetup = {}; + strcpy(usetup.name, "Synthesized Keyboard Input"); + ioctl(fd, UI_DEV_SETUP, &usetup); + ioctl(fd, UI_DEV_CREATE); + + usleep(uinput_opts->pre_pause); + + for (int i = 0; i < nchars; i++) { + const keyvent* kev = invkeymap + (unsigned char)string[i]; + + if (kev->modifiers & K_SHIFTTAB) + emit(fd, EV_KEY, KEY_LEFTSHIFT, 1); + if (kev->modifiers & K_ALTTAB) + emit(fd, EV_KEY, KEY_RIGHTALT, 1); + + emit(fd, EV_KEY, kev->principal, 1); + emit(fd, EV_KEY, kev->principal, 0); + + if (kev->modifiers & K_ALTTAB) + emit(fd, EV_KEY, KEY_RIGHTALT, 0); + if (kev->modifiers & K_SHIFTTAB) + emit(fd, EV_KEY, KEY_LEFTSHIFT, 0); + + emit(fd, EV_SYN, SYN_REPORT, 0); + usleep(uinput_opts->inter_pause); + } + + ioctl(fd, UI_DEV_DESTROY); + close(fd); + return 0; +} diff --git a/uinput.h b/uinput.h new file mode 100644 index 0000000..a760271 --- /dev/null +++ b/uinput.h @@ -0,0 +1,25 @@ +/* Copyright 2019 Justus Piater */ +/* This file is part of UINPUTCHARS. + + UINPUTCHARS is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + UINPUTCHARS is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with UINPUTCHARS. If not, see . */ + +#include "invkeymap.h" + +struct uinput_opts_t { + unsigned long pre_pause; /* microseconds */ + unsigned long inter_pause; /* microseconds */ +}; + +int emit_string(const char* string, const keyvent* invkeymap, + const struct uinput_opts_t* uinput_opts);