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. :)