Building a Better Screen Locker for GNU/Linux

by idk

As vendors begin to realize that shipping proprietary firmware only makes devices less competitive and less secure, and heroic reverse-engineering efforts make progress in Freedreno, etnaviV, OpenFWWF, and Lima, software freedom is finally closer than ever on mobile devices.

This makes 2017 and beyond a much more exciting time, with the ability to run a few devices in full freedom, if you are willing to make a few sacrifices in terms of hardware.  Unfortunately, this fifth(ish) userspace/middleware in the mobile space means that even more basic components will have to be re-produced in the new environment.  One of the most illustrative examples is the screen locker.

Deficiencies of Modern Screen Lockers on Desktop GNU/Linux

What do you want from a screen locker for a mobile device in 2017 and beyond?

I, for one, want something out of a screen locker that no GNU/Linux screen locker I am aware of can give.

That is a transparent, reasonably secure ability to encrypt files on a running device to mitigate the effect of exfiltration by physical means, i.e., someone grabbing my device and walking away with it.

On iOS, the device manages multiple keys, many of which are managed by the screen lock.  When the screen lock engages, the user's personal folders are encrypted until the passphrase is re-entered to the lock screen.  Actually, that's something I'd like on the desktop, too.

So what do we need to build a sufficient screen locker?

Goals

1.)  Delay access by a physical attacker with easy-to-obtain resources, such as malicious HID emulators, physical keyloggers, and attackers who compromise a device by obvious theft.

2.)  Hamper the installation of malware by a physical attacker which may be used to log and exfiltrate the screen locker passphrase by disabling channels that may be used to install it.

3.)  Give the user of the device a datastore which can be transparently and unobtrusively encrypted and decrypted when the user locks and unlocks the screen, which, if exfiltrated, will be untfeasibly difficult to decrypt.

4.)  Have different disk encryption, user login/$HOME decryption, screen lock, and Encrypted Data Store (EDS) passphrases.  Never physically enter EDS passphrase.  Instead, entering the screen lock passphrase causes it to be unlocked and the EDS locks itself automatically after timing out, or with the screen lock.

Materials

Slock: github.com/fyrix/slock

We use Slock for this project because Slock doesn't do things that suck, like create unnecessarily confusing code.  This makes it very easy to modify for our purposes, and there is example code available that can assist us to this effect.  You could, in theory, do this with any screensaver, but I did it with Slock.  I encourage you to do it with your screensaver of choice.

(Christopher Jeffrey) chjj's Slock: github.com/chjj/slock

Original Slock: git.suckless.org/slock

Xssstate: git.suckless.org/xssstate

We use Xssstate to monitor the X screensaver state.  This is because it also sucks a lot less than other ways to do it, and works nicely with Slock.

GPG: www.gnupg.org

For reasons that are perfectly obvious, we use GPG for encrypting the password to the encrypted data store.  It's pretty much the only reasonable tool for this.  We will be writing a wrapper to help us make sure things are done in a consistent way.

EncFS: vgough.github.io/encfs

Finally, we'll be using EncFS as the way we guard the encrypted data store.  Make sure you get the latest version!  EncFS is undergoing significant improvements.

GRsecurity: grsecurity.org

We use GRsecurity in a slightly custom configuration in order to make it possible to prevent USB attacks that attempt to brute-force our screensaver by emulating a Human Interface Device (HID).  The configuration available to Debian Sid/Jessie-Backports works well with one config-only modification.

Other Things to Note

Creating Our Password and Data Store

To create and manage our data store as best we reasonably can, we should make some preparations.  First, we're going to need a passphrase-protected keypair to use with the data in the classic, asymmetric fashion.  Just do this with the regular: gpg --gen-key

But generate a random, long passphrase for it and write it down before you finish.  Mine is 128 random characters long.

Then, we're going to need a (short) passphrase-protected, symmetrically-encrypted file that contains nothing but the generated data store passphrase.  For example:

$ LONG_RANDOM_PASSWORD=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 128 | head -n 1)
$ echo $LONG_RANDOM_PASSWORD
vSn7WFrYDA96Cvbd9A4YIb73vVVM3trwI8lZmZ3oSanaUpHmtNfIRmXKw66iEwQQlTwuDLBY8AexJw7YyCSOhIfuuIGkWRZaD65SnFd0tU8nIZS4LoCzvPgYeq7y0Mtz

Lastly, create an "encrypted" folder in your $HOME directory using EncFS.

$ mkdir -p ~/.crypt ~/crypt
$ echo $LONG_RANDOM_PASSWORD | encfs --stdinpass ~/.crypt ~/crypt

Example Wrappers for GPG

Now we need to create some wrappers for GPG which will help us when we call out to it from the screensaver to check the password.

This is just a shell script, and on my system it's at /usr/bin/masterscreen:

#!/bin/sh
basic_gpg_decrypt() {
  [ ! -z "$1" ] && VAL=$(gpg --passphrase "$1" -d $HOME/.masterscreen.gpg)
  echo "$VAL"
}

generate_gpg_pwfile() {
  PASS=$(whiptail --passwordbox "please enter your secret password" 8 78 --title "password dialog" 3>&1 1>&2 2>&3)
  PASSC=$(whiptail --passwordbox "please confirm your secret password" 8 78 --title "password dialog" 3>&1 1>&2 2>&3)
  LONG_RANDOM_PASSWORD=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 128 | head -n 1)
  [ "$PASS" = "$PASSC" ] && echo "$LONG_RANDOM_PASSWORD" | gpg --cipher-algo AES256 --passphrase "$PASS" --output "$HOME/.masterscreen.gpg" --symmetric
  unset PASS; unset PASSC;
  echo "%echo Generating a basic OpenPGP key
  Key-Type: RSA
  Key-Length: 4096
  Name-Real: masterscreen
  Name-Email: masterscreen@localhost
  Expire-Date: 1y
  Passphrase: $PASSS
  %commit
  %echo done" | gpg --gen-key --batch -
  mkdir -p $HOME/crypt $HOME/.crypt
  echo $PASSS | encfs --stdinpass ~/.crypt ~/crypt
  unset PASSS
}

unload_gpg_datask() {
  fusermount -u ~/crypt
  gpg-connect-agent reloadagent /bye
}

load_gpg_datask() {
  VAL=basic_gpg_decrypt "$1"
  gpg-agent --add "$2" --passphrase "$VAL" || echo "failure" && unload_gpg_datask
  echo $VAL | encfs $HOME/.crypt $HOME/crypt --stdinpass || echo "failure" && unload_gpg_datask
}

if [ -f "$HOME/.masterscreen.gpg" ]; then
  [ -z "$2" ] && [ -z "$1" ] && load_gpg_datask "$2" "$1"
  [ ! -z "$1" ] && unload_gpg_datask
else
  generate_gpg_pwfile
fi

Init Scripts

If you're going to want to start Slock under its own username, you will probably want to use your init system to start it.

If you use System V init, or if your systemd supports /etc/rc.local, then you can simply start it there and get pretty decent results.

Create this simple wrapper script (called xsidle.sh and is from the Xssstate examples) for Slock, place it into /bin, and run it from your init system.

#!/bin/sh
#
# Use xset s $time to control the timeout when this will run.
#
if [ $# -lt 1 ];
  then
    printf "usage: %s cmd\n" "$(basename $0)" 2>&1
    exit 1
fi
cmd="$@"
while true
  do
    if [ $(xssstate -s) != "disabled" ];
      then
        tosleep=$(($(xssstate -t) / 1000))
        if [ $tosleep -le 0 ];
          then
            $cmd
        else
          sleep $tosleep
        fi
    else
      sleep 10
   fi
done

If you can use /etc/rc.local, it's as simple as:

$ su $USER /bin/xsidle.sh /usr/bin/slock &

YMMV, though.  Or, you can run it as your own user by starting it with your .bashrc.

Modifying Slock

First, add the following pre-processing options:

#define USBOFF 1
#define GPGOFF 1
#define STRICT_USBOFF 0

Next, create the USB lock functions:

// Turn off USB if we're in danger.
static void usboff(void)
{
#if USBOFF
// Needs sudo privileges - alter your /etc/sudoers file:
// sysctl: [username] [hostname] =NOPASSWD: /sbin/sysctl kernel.grsecurity.deny_new_usb=0
  char *args[] = { "sudo", "sysctl", "kernel.grsecurity.deny_new_usb=1", NULL };
#if STRICT_USBOFF
  char *argst[] = { "sudo", "sysctl", "kernel.grsecurity.grsec_lock=1", NULL };
  execvp(argst[0], argst);
#endif
  execvp(args[0], args);
#else
  return;
#endif
}

// Turn on USB when the correct password is entered.
static void usbon(void)
{
#if USBOFF
// Needs sudo privileges - alter your /etc/sudoers file:
// sysctl: [username] [hostname] =NOPASSWD: /sbin/sysctl kernel.grsecurity.deny_new_usb=0
  char *args[] = { "sudo", "sysctl", "kernel.grsecurity.deny_new_usb=0", NULL };
  execvp(args[0], args);
#else
  return;
#endif
}

Now, add the GPG/EncFS lock functions:

// Release the gpg keys and unmount the encfs data store
static void gpgon(void)
{
#if GPGOFF
// This resets the GPG agent when the screen is locked.
  char *args[] = { "masterscreen", NULL };
  execvp(args[0], args);
#else
  return;
#endif
}

// Re-add the GPG keys and re-mount the encfs encrypted store.
static int gpgoff(const char *password)
{
#if GPGOFF
// This function checks the password from the Screen Locker against the symmetric file.
// If it succeeds, then the screen will be unlocked and the key will be added to the gpg-agent.
  char buf[128];
  int i = 0;
  char *args[] = { "masterscreen", "masterscreen@localhost", &password, NULL };
  FILE *p = popen(&args, "r");
  if (p != NULL) {
    while (!feof(p) && (i < 128)) {
      fread(&buf[i++], 1, 1, p);
    }
    buf[i] = 0;
    if (strstr(buf, "failure")) {
      return -1;
    }
    pclose(p);
    return 0;
  } else {
    return -1;
  }
#else
  return;
#endif
}

Finally, add the appropriate triggers in the main screenlocker loop, at the bottom of lockscreen (around line 600):

usboff();
gpgon();
return lock;
}

at the top of unlockscreen (around line 480):

static void unlockscreen(Display * dpy, Lock * lock)
{
  usbon();

and in the middle of readpw() (around line 350):

#if GPGOFF
if (gpgoff(passwd) == 0) {
  running = 0;
#else

Configure Settings

Some of our commands require root access for the Slock user.  Since Slock is runnable by the logged-in user without root, you need to make some exceptions to your sudoers policy to make it do what it needs to.  I prefer to make the commands totally explicit.

For automatic shutdown after five password attempts (part of the pre-existing mods by chjj):

Modify /etc/sudoers:

systemd: $USER $HOST =NOPASSWD: /usr/bin/systemctl poweroff
sysvinit: $USER $HOST =NOPASSWD: /usr/bin/shutdown -h now

For USB enabling/disabling:

$USER $HOST =NOPASSWD: /sbin/sysctl kernel.grsecurity.deny_new_usb=0
$USER $HOST =NOPASSWD: /sbin/sysctl kernel.grsecurity.deny_new_usb=1

In order to change the USB connectivity on-the-fly, we'll have to leave GRsecurity sysctl available to the root user by disabling grsecurity.grsec_lock.  You'll need to modify /etc/sysctl.d/grsec.conf.

Simply change: kernel.grsecurity.grsec_lock = 1 to kernel.grsecurity.grsec_lock = 0

In Conclusion

It's possible to have a screen locker for GNU/Linux which is reasonably resilient to local attackers who attempt to brute force the password or install malware while the lock is engaged to log and exfiltrate the password.  While a clever attacker will just find another way to install keylogging malware, shoulder surfers, physical keyloggers, or even TEMPEST-style EM keylogging will fail to exfiltrate the real password to the encrypted data store.

ACK

The Suckless Community, chjj on GitHub (whose fork of Slock I in turn forked), Brad Spengler of GRsecurity, Luc Verhaegen for kickstarting ARM GPU freedom, GPG, and all the cypherpunks who came before.  And in general, to RMS and the Free Software movement.

Code: masterscreen.sh

Code: xsidle.sh

Code: modify-slock1

Code: modify-slock2

Code: modify-slock3

Code: modify-slock4

Code: modify-slock5

Code: modify-slock6

Return to $2600 Index