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