Manual

snenslock is a simple lockscreen written in Rust. It switches to an unused TTY and prohibits switching back to another TTY until an authentication challenge was passed, e.g. typing in a password.

This approach is strongly inspired by physlock.

Below is a screenshot of snenslock in action using the tui-greeter:

Screenshot of `tui`-greeter

The screenshots are generated during the integration tests running on Hydra CI.

License

snenslock is released under the terms of GNU GPLv2.0.

This was also done to be compatible with its primary inspiration physlock(1).

How it works

The way how snenslock locks your screen can be best explained with a flowchart:

Flowchart w/ explanation

Additional notes

  • The currently active user session is determined via logind's DBus API, namely org.freedesktop.login1(5).
  • The following safe-guards are implemented:
    • The lockscreen is rendered to an unused TTY. To prohibit switching back to another TTY, the ioctl(2) called VT_LOCKSWITCH is used.
    • The log-level for the console is set to 1 to make sure that no kernel messages are written to the TTY.
    • The sysrq shortcuts to kill a process and to alter the log-level are disabled.
  • By default, the machine asks for the root password if the password of the locking user was wrongly entered five times. This can be changed using the CLI flag -t (or programs.snenslock.rootAtAuthTimes).
  • Optionally it's possible to initiate a shutdown if the authentication failed N times. This can be configured using the CLI flag -p N (or programs.snenslock.poweroffAfter)
  • The purple box is considered a "safe state": as soon as that is reached, the machine can be considered to be locked. If the greeter crashes, it will be restarted. However, please make sure that this state is actually reached, before that your machine is not locked!
  • There are two greeters: a tui greeter (see screenshot above) and a minimal greeter. Since greeters can have arbitrarily complex GUI code (e.g. future greeters being fully graphic), a fallback is implemented to use a very minimal greeter. If the selected greeter crashes >3 times, this one is activated rather than restarting the previously used greeter.
  • The code of the main process (i.e. the code that does locking, PAM check etc) is designed to not crash. If the greeter (i.e. the process interacting with the user to get a password) crashes, it will be restarted. However, software does crash. That means if snenslock itself crashes, the machine remains locked, but cannot be unlocked with snenslock. In that case there are two ways to unlock:
    • Force a reboot with sysrq.
    • Login via SSH and unlock the TTYs manually.

Important Assumptions

There are a few assumptions that must be met to make sure this software functions properly:

  • The software is installed via the NixOS module in the repository's flake.
  • As soon as the lock is active, the computer can be considered secured. It must be ensured that the lock can be created.
  • There must be a free, unused TTY. Otherwise creating the lock fails.
  • The software is used to either lock TTYs or Wayland sessions (only tested with sway currently). E.g. X11 support is explicitly out of scope. It may work, but it was never tested. You'll probably want to use XScreenSaver then.

Installation

With flakes

{
  inputs.snenslock.url = "git+https://git.mbosch.me/ma27/snenslock?shallow=true";
  inputs.snenslock.inputs.nixpkgs.follows = "nixpkgs";
  outputs = { self, nixpkgs, snenslock, ... }: {
    nixosConfigurations.yourmachine = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        snenslock.nixosModules.snenslock
        { security.polkit.enable = true;
          programs.snenslock = {
            enable = true;
            greeter = "tui";
          };
        }
      ];
    };
  };
}

Additional config options can be found in flake.nix or in the options' reference

Without flakes

Note: it's highly recommended to specify a revision here and not to always fetch latest master.

{
  imports = [
    (import (builtins.fetchGit {
      url = "https://git.mbosch.me/ma27/snenslock";
      shallow = true;
      ref = "master";
    })).nixosModules.snenslock
  ];
  programs.snenslock.enable = true;
  security.polkit.enable = true;
}

Binary cache

The flake is regularly built on my personal Hydra, the binary cache can be used to substitute prebuilt artifacts of snenslock. All output paths are signed with the following key:

hydra.ist.nicht-so.sexy-1:E+AwZnzYPdycs1IkHrlG0eJeBleAW/ukX10lcjTc2RQ=

Usage

You can lock your screen by running systemctl start snenslock. If you'd like to do that as unprivileged user without a permission check, you can add your user to the group screenlock. Please note that this bypass is implemented as polkit(8) script in the NixOS module!

Work around sway problems

Display settings

It is a known problem that sway(1) doesn't like TTY switches which may result in the following behavior when using external (HiDPI) screens:

  • The lockscreen doesn't disappear on an external monitor.
  • The scaling is broken when using larger fonts on the TTY.

It can be worked around by declaring programs.snenslock.fixExternalMonitorsInSway = true;. This script basically sets the resolution of external screens to a low resolution (1280x720) and then resets it to the previous resolution after snenslock has unlocked the screen. See also flake.nix for further details.

swayidle

When using swayidle to lock with snenslock, swayidle doesn't get back to "active state" on unlock until some activity such as hitting a random key is made by the user. This means that swayidle assumes it's still locked after unlock and thus doesn't lock again after the timeout is reached the next time.

The only workaround known so far is to restart swayidle.service after unlock in via programs.snenslock.hooks.afterLockCommands.

Hacking

  • Install pam(8) service for snenslock (already done if you're on a machine with programs.snenslock.enable = true;):

    {
      security.pam.services.snenslock = {};
    }
    
  • Then, snenslock can be executed locally with e.g. the following command:

    sudo -E nix run .# -- --greeter tui --greeter-cfg snenslock.example.toml
    

Alternatively, the VM tests in flake.nix may be used.

Options' reference

programs.snenslock.enable

Whether to enable snenslock.

Type: boolean

Default: false

Example: true

programs.snenslock.disableFallbackToRoot

Whether to disable authenticating as root after each N failed attempts.

Type: boolean

Default: false

programs.snenslock.fixExternalMonitorsInSway

Whether to enable fixing mode of external monitors in sway.

Type: boolean

Default: false

Example: true

programs.snenslock.greeter

Which greeter to use to unlock the machine.

Type: one of “minimal”, “tui”

Default: "minimal"

programs.snenslock.greeterCfg.tui.asterisk_char

Character to use as asterisk.

Type: string matching the pattern ^.{1}$

Default: "*"

programs.snenslock.greeterCfg.tui.clock_format

Format of the clock in the lockscreen.

Type: string

Default: "%a %d %b %H:%M"

programs.snenslock.greeterCfg.tui.form_width

Width of the TUI login form.

Type: positive integer, meaning >0

Default: 50

programs.snenslock.greeterCfg.tui.no_asterisks

Whether to disable asterisks for passwords.

Type: boolean

Default: false

programs.snenslock.greeterCfg.tui.refresh_seconds

Seconds after which the clock gets refreshed.

Type: positive integer, meaning >0

Default: 1

programs.snenslock.greeterCfg.tui.relative_battery_refresh

Update times of the battery indicator relative to the refresh time of the UI itself (currently configured via ).

By default this is 10 whereas the UI gets refreshed every second (implying that the tui renderer loop gets executed 10 times). Every tenth iteration (=every ~10 seconds) the battery status also gets updated.

Type: unsigned integer, meaning >=0

Default: 10

programs.snenslock.greeterCfg.tui.show_battery_status

Whether to enable displaying battery capacity & charging status at the bottom right.

Type: boolean

Default: false

Example: true

programs.snenslock.hooks.afterLockCommands

Commands to execute after the lock has finished. Useful to perform commands such as resuming running music, clearing cached gpg passphrases etc.

Type: strings concatenated with “\n”

Default: ""

programs.snenslock.hooks.beforeLockCommands

Commands to execute before starting the lcokscreen. Useful for e.g. pausing music etc.

Type: strings concatenated with “\n”

Default: ""

programs.snenslock.poweroffAfter

After how many failed attempts the machine should be powered off.

Type: null or (positive integer, meaning >0)

Default: null

programs.snenslock.rootAtAuthTimes

After each N attempts to authenticate as the locking user, an unlock as root is attempted.

Type: positive integer, meaning >0

Default: 5

programs.snenslock.screenlockGroup

Name of the group that’s allowed to invoke the screen-lock. If the group is the default group (snenslock), it is created by the module. If you choose another group, you need to create it on your own (since it indicates that you want to use a custom group that you manage on yoru own for locking).

Type: string

Default: "screenlock"