Display-o-Tron: display IP while waiting for passphrase

Hi!

I have a server on a Raspberry Pi 2 that uses an encrypted root filesystem. It’s pretty easy to do with Debian. Although it requires an initramfs which is not how most installations are done. It’s not really hard to set it up for anyone experienced with Debian though.

But this box is meant to be a small server. So how do you enter the encryption passphrase without the need to plug a display and keyboard? The dropbear-initramfs package is the answer. It will start an SSH server to make it possible to remotely enter the passphrase.

Having to nmap a network to find which IP address the system feels cumbersome. So why not use the shiny Display-o-Tron to display the IP address when it’s ready?

The trick is that embedding an entire Python interpreter and several libraries in the initramfs is hard. But we can use some bits of C and a couple of shell scripts!

First, we will need a small C program to do I2C requests to control the backlight. Here’s backlight.c:

/* Copyright © 2016 Lunar
 *
 * This work is free. You can redistribute it and/or modify it under the
 * terms of the Do What The Fuck You Want To Public License, Version 2,
 * as published by Sam Hocevar. See http://www.wtfpl.net/ for more details.
 */

#include <stdio.h>
#include <stdlib.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <math.h>

#define I2C_BUS_ID 1
#define I2C_ADDRESS 0x54
#define CMD_ENABLE_OUTPUT 0x00
#define CMD_SET_PWM_VALUES 0x01
#define CMD_ENABLE_LEDS 0x13
#define CMD_UPDATE 0x16
#define CMD_RESET 0x17
#define RGB_LED_COUNT 6

static void
panic(const char * error)
{
    fprintf(stderr, error);
    fprintf(stderr, "\n");
    exit(1);
}

static void
enable(int fd)
{
    const unsigned char data[] = { 0x01 };

    if (i2c_smbus_write_i2c_block_data(fd, CMD_ENABLE_OUTPUT, 1, data) < 0) {
        panic("enable() failed");
    }
}

static void
disable(int fd)
{
    const unsigned char data[] = { 0x00 };

    if (i2c_smbus_write_i2c_block_data(fd, CMD_ENABLE_OUTPUT, 1, data) < 0) {
        panic("disable() failed");
    }
}

static void
reset(int fd)
{
    const unsigned char data[] = { 0xFF };

    if (i2c_smbus_write_i2c_block_data(fd, CMD_RESET, 1, data) < 0) {
        panic("reset() failed");
    }
}

static void
enable_leds(int fd, unsigned long mask)
{
    unsigned char led_data[3];
    const unsigned char update_data[] = { 0xFF };

    led_data[0] = mask & 0x3F;
    led_data[1] = (mask >> 6) & 0x3F;
    led_data[2] = (mask >> 12) & 0x3F;

    if (i2c_smbus_write_i2c_block_data(fd, CMD_ENABLE_LEDS, 3, led_data) < 0) {
        panic("enable_leds() failed");
    }
    if (i2c_smbus_write_i2c_block_data(fd, CMD_UPDATE, 1, update_data) < 0) {
        panic("enable_leds() failed");
    }
}

static void
output(int fd, const unsigned long * values)
{
    unsigned char red;
    unsigned char green;
    unsigned char blue;
    int rgb_led_index;
    unsigned char pwm_data[3 * RGB_LED_COUNT];
    const unsigned char update_data[] = { 0xFF };

    for (rgb_led_index = 0; rgb_led_index < RGB_LED_COUNT; rgb_led_index++) {
        red = (values[rgb_led_index] & 0xFF000) >> 16;
        green = (values[rgb_led_index] & 0x00FF00) >> 8;
        blue = values[rgb_led_index] & 0x0000FF;
        pwm_data[rgb_led_index * 3] = (unsigned char) pow(255, blue - 1);
        pwm_data[rgb_led_index * 3 + 1] = (unsigned char) pow(255, green - 1) / 1.6;
        pwm_data[rgb_led_index * 3 + 2] = (unsigned char) pow(255, red - 1) / 1.4;
    }
    if (i2c_smbus_write_i2c_block_data(fd, CMD_SET_PWM_VALUES, 3 * RGB_LED_COUNT, pwm_data) < 0) {
        panic("output() failed");
    }
    if (i2c_smbus_write_i2c_block_data(fd, CMD_UPDATE, 1, update_data) < 0) {
        panic("output() failed");
    }
}

static int
open_i2c(void)
{
    char filename[256];
    int fd;

    snprintf(filename, 256, "/dev/i2c-%d", I2C_BUS_ID);
    if (-1 == (fd = open(filename, O_RDWR))) {
        panic("Unable to open i2c device");
    }
    if (ioctl(fd, I2C_SLAVE, I2C_ADDRESS) < 0) {
        panic("Unable to acquire bus access or talk to slave");
    }
    return fd;
}

static unsigned long
parse_arg(char const * arg)
{
    char * endptr;
    unsigned long ret;

    ret = strtoul(arg, &endptr, 16);
    if (arg == endptr || *endptr != '\0') {
        panic("Unable to parse argument");
    }
    return ret;
}

static void
parse_args(const char ** args, unsigned long * values)
{
    int i;

    for (i = 0; i < RGB_LED_COUNT; i++) {
        values[i] = parse_arg(args[i]);
    }
}

int
main(int argc, const char ** argv)
{
    unsigned long values[RGB_LED_COUNT];
    int fd;

    fd = open_i2c();
    if (argc == 1) {
        disable(fd);
        reset(fd);
        return 0;
    }

    if (argc != RGB_LED_COUNT + 1) {
        panic("Usage: backlight 0xFFFFFF 0xFFFFFF 0xFFFFFF 0xFFFFFF 0xFFFFFF 0xFFFFFF");
    }
    parse_args(&argv[1], values);
    enable(fd);
    enable_leds(fd, (unsigned long) pow(2, 3 * RGB_LED_COUNT) - 1);
    output(fd, values);

    return 0;
}

Build it with:

make CFLAGS="-Wall -Werror -std=c99" LDFLAGS="-lm" backlight

Next, another small C program to do SPI transfers from the shell. Imaginatively named spixfer.c:

/* Copyright © 2016 Lunar
 *
 * This work is free. You can redistribute it and/or modify it under the
 * terms of the Do What The Fuck You Want To Public License, Version 2,
 * as published by Sam Hocevar. See http://www.wtfpl.net/ for more details.
 */

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/types.h>
#include <linux/spi/spidev.h>

#define SPI_DEVICE 0
#define SPI_CHIP_SELECT 0
#define SPEED_HZ 1000000

static void
panic(const char * error)
{
    fprintf(stderr, error);
    fprintf(stderr, "\n");
    exit(1);
}

static int
open_spi(void)
{
    char filename[256];
    int fd;

    snprintf(filename, 256, "/dev/spidev%d.%d", SPI_DEVICE, SPI_CHIP_SELECT);
    if (-1 == (fd = open(filename, O_RDWR))) {
        panic("Unable to open SPI device");
    }
    return fd;
}

static int
xfer(int fd, const unsigned char * tx_buf, unsigned char * rx_buf, unsigned int len)
{
    struct spi_ioc_transfer xfer[1];
    int status;

    xfer[0].tx_buf = (unsigned long) tx_buf;
    xfer[0].rx_buf = (unsigned long) rx_buf;
    xfer[0].len = len;
    xfer[0].speed_hz = SPEED_HZ;

    status = ioctl(fd, SPI_IOC_MESSAGE(1), &xfer);
    if (status < 0) {
        panic("SPI_IOC_MESSAGE(1) failed");
    }
    return status;
}

static unsigned char
parse_arg(char const * arg)
{
    char * endptr;
    unsigned long ret;

    ret = strtoul(arg, &endptr, 16);
    if (arg == endptr || *endptr != '\0') {
        panic("Unable to parse argument");
    }
    if (ret > 255) {
        panic("Maximum 0xFF for a byte");
    }
    return (unsigned char) ret;
}

static void
parse_args(int argc, const char ** argv, unsigned char * tx_buf)
{
    int i;

    for (i = 0; i < argc; i++) {
        tx_buf[i] = parse_arg(argv[i]);
    }
}

static void
print_response(int status, const unsigned char * rx_buf, unsigned int len)
{
    unsigned int i;

    printf("response(%d): ", status);
    for (i = 0; i < len; i++) {
        printf("%02x ", rx_buf[i]);
    }
    printf("\n");
}

int
main(int argc, const char ** argv)
{
    int fd;
    unsigned int len;
    unsigned char * tx_buf;
    unsigned char * rx_buf;
    int status;

    fd = open_spi();

    len = argc - 1;
    tx_buf = malloc(len);
    rx_buf = malloc(len);
    parse_args(len, &argv[1], tx_buf);

    status = xfer(fd, tx_buf, rx_buf, len);

    print_response(status, rx_buf, len);

    return 0;
}

Build it with:

make CFLAGS="-Wall -Werror -std=c99" spixfer

The C code is crude. It might be reusable for other projects but has only been very lightly tested. If it breaks you keep all pieces. ;)

Copy both binaries in a newly created directory /etc/initramfs-tools/lcd. Then let’s add in the same place our little shell library dothat_functions.sh:

# Copyright © 2016 Lunar
#
# This work is free. You can redistribute it and/or modify it under the
# terms of the Do What The Fuck You Want To Public License, Version 2,
# as published by Sam Hocevar. See http://www.wtfpl.net/ for more details.

REGISTER_SELECT_PIN=25
RESET_PIN=12

spixfer() {
	/sbin/spixfer "$@"
}

backlight() {
	/sbin/backlight "$@"
}

set_backlight_color() {
	local color="$1"

	backlight "$1"  "$1"  "$1"  "$1"  "$1"  "$1"
}

export_gpio() {
	local pin="$1"

	if ! [ -e /sys/class/gpio/gpio$pin ]; then
		echo "$pin" > /sys/class/gpio/export
	fi
}

unexport_gpio() {
	local pin="$1"

	if [ -e /sys/class/gpio/gpio$pin ]; then
		echo "$pin" > /sys/class/gpio/unexport
	fi
}

setup_gpio() {
	local pin="$1"
	local direction="$2"

	echo "$direction" > /sys/class/gpio/gpio$pin/direction
}

output_gpio() {
	local pin="$1"
	local value="$2"

	echo "$value" > /sys/class/gpio/gpio$pin/value
}

reset() {
	export_gpio "$RESET_PIN"
	setup_gpio "$RESET_PIN" out
	output_gpio "$RESET_PIN" 0
	sleep 0.001
	output_gpio "$RESET_PIN" 1
	sleep 0.001
	unexport_gpio "$RESET_PIN"
}

initialize() {
	export_gpio "$REGISTER_SELECT_PIN"
	setup_gpio "$REGISTER_SELECT_PIN" out

        # set entry mode (no shift, cursor direction)
	write_command 0 0x06
	# set default display mode (enabled, no cursor, no blink)
	write_command 0 0x0c
	set_default_contrast
	set_cursor_position 0
}

cleanup() {
	unexport_gpio "$REGISTER_SELECT_PIN"
}

spi_xfer() {
	local read_bytes="$1"

	shift
	spixfer "$@" > /dev/null
}

write_instruction_set() {
	local instruction_set="$1"

	output_gpio "$REGISTER_SELECT_PIN" 0
	# 0b00111000 (template) | set | 0 (double height)
	spi_xfer 0 "$(printf "0x%02x" $((56 + $instruction_set)))"
        sleep 0.00006
}

write_command() {
	local set="$1"
	local command="$2"

	output_gpio "$REGISTER_SELECT_PIN" 0
        write_instruction_set "$set"
	spi_xfer 0 "$command"
        sleep 0.00006
}

set_default_contrast() {
	# set contrast to 42
	write_command 1 0x56
	write_command 1 0x6b
	write_command 1 0x7a
}

write_string() {
	local string="$1"

	output_gpio "$REGISTER_SELECT_PIN" 1

	echo -n "$1" | sed -e 's/./\0\n/g' | while read char; do
		if [ -z "$char" ]; then
			char=' '
		fi
		spi_xfer 0 $(printf "0x%02x" "'$char")
		sleep 0.00005
	done
}

set_cursor_position() {
	local position="$1"

	write_command 0 $(printf "0x%02x" $((128 + $position)))
}

clear_display() {
	write_command 0 0x01
}

scroll_left() {
	write_command 0 0x18
}

scroll_right() {
	write_command 0 0x1C
}

disable_display() {
	write_command 0 0x08
}

Cool. Now let’s add a hook to install all this in /etc/initramfs-tools/hooks/lcd:

#!/bin/sh
#
# Copyright © 2016 Lunar
#
# This work is free. You can redistribute it and/or modify it under the
# terms of the Do What The Fuck You Want To Public License, Version 2,
# as published by Sam Hocevar. See http://www.wtfpl.net/ for more details.

PREREQ=""

prereqs() {
	echo "$PREREQ"
}

case "$1" in
	prereqs)
		prereqs
		exit 0
	;;
esac

. "${CONFDIR}/initramfs.conf"
. /usr/share/initramfs-tools/hook-functions

copy_exec /etc/initramfs-tools/lcd/backlight /sbin
copy_exec /etc/initramfs-tools/lcd/spixfer /sbin
cp /etc/initramfs-tools/lcd/dothat_functions.sh $DESTDIR/lib
manual_add_modules spi_bcm2708 i2c_bcm2708 i2c_dev

And we are now ready to add two small scripts to be run at boot. One at the same time dropbear is started, /etc/initramfs-tools/scripts/init-premount/lcd:

#!/bin/sh
#
# Copyright © 2016 Lunar
#
# This work is free. You can redistribute it and/or modify it under the
# terms of the Do What The Fuck You Want To Public License, Version 2,
# as published by Sam Hocevar. See http://www.wtfpl.net/ for more details.

PREREQ="udev devpts"

prereqs() {
	echo "$PREREQ"
}

case "$1" in
	prereqs)
		prereqs
		exit 0
	;;
esac

. /scripts/functions

[ -e /lib/dothat_functions.sh ] || exit 0

. /lib/dothat_functions.sh

modprobe spi_bcm2708
modprobe i2c_bcm2708
modprobe i2c_dev

reset
initialize
set_backlight_color 0xFFFF00
clear_display
set_cursor_position 0
write_string "Enter passphrase"
set_cursor_position 16
write_string "----------------"
set_cursor_position 32
write_string "Getting IP addr."

(
IP_ADDRESS=
while [ -z "$IP_ADDRESS" ]; do
	IP_ADDRESS=$(ip addr show dev eth0 | sed -n -e 's/.*inet \([0-9.]\+\).*/\1/p')
done
set_cursor_position 32
write_string "                "
set_cursor_position 32
write_string "$IP_ADDRESS"

cleanup
) &

And another that will be run once the root filesystem has been unlocked, in /etc/initramfs-tools/script/init-bottom/lcd:

#!/bin/sh
#
# Copyright © 2016 Lunar
#
# This work is free. You can redistribute it and/or modify it under the
# terms of the Do What The Fuck You Want To Public License, Version 2,
# as published by Sam Hocevar. See http://www.wtfpl.net/ for more details.

PREREQ=""

prereqs() {
	echo "$PREREQ"
}

case "$1" in
	prereqs)
		prereqs
		exit 0
	;;
esac

. /scripts/functions

[ -e /lib/dothat_functions.sh ] || exit 0

. /lib/dothat_functions.sh

initialize
clear_display
set_backlight_color 0x007FFF
set_cursor_position 16
write_string "....booting....."
(
sleep 1
set_backlight_color 0x000000
disable_display
cleanup
) &

Make sure to have them all executables:

chmod +x /etc/initramfs/{hooks,scripts/init-premount,scripts/init-bottom}/lcd

Regenerate an initramfs:

update-initramfs -u

Hopefully, on the next reboot the display will show a nice message and the IP address!

To unlock the device, the correct SSH public key needs to be put first in /etc/initramfs-tools/root/.ssh/authorized_keys (before update-initarmfs -u). Then, once we have the IP, we can do:

laptop$ ssh root@192.168.XXX.XXX
~ # /lib/cryptsetup/askpass 'passphrase: ' > /lib/cryptsetp/passfifo
passphrase:

And here we go. :)

1 Like

Guess realPy’s kernel module would allow us to get rid of a good amount of the code. That’ll be for a next time.

hi lunar!
Welcome hère!
Have make same thing with my server except my uncrypt partition is LVM for rolling back and boot on another systèmes quickly.
Or course you can use my module to avoid your exécutable :).
I have begin a work to make a menu to choice the lvm partition to load just after the uncrypt. We can also make this menu with the dothat!
PM me de can talk together about things like.
I must to see my work but i remember that the debian setupcrypt doesn’t work n’y default but must be correct…

I had no problems with a standard Debian Jessie on a Raspberry Pi 2 using U-Boot. I might have done a couple of tricks to get the kernel package as integrated as possible but sadly I have written anything done. I am really hoping that—with the work done on upstreaming kernel code— we will have support for the Raspberry Pi 2 fully integrated in time for Debian Stretch. There should not be too many changes required to have the debian-installer working… and then installing an encrypted system should be a piece of cake.