unl0kr: port to an s6 service

this has some drawbacks in its current form and will be tidied

it writes the password also to the consold. it requires 'sudo'.
This commit is contained in:
2024-07-25 18:45:01 +00:00
parent b554d32133
commit 8ef5920d84
5 changed files with 22 additions and 126 deletions

View File

@@ -16,7 +16,9 @@
'';
});
persist.byStore.private = [
# N.B.: we can't persist anything to `private` storage at this point,
# because mounting the private storage generally relies on having a service manager running.
persist.byStore.ephemeral = [
".local/share/s6/logs"
];

View File

@@ -3,44 +3,6 @@ let
cfg = config.sane.programs.unl0kr;
tty = "tty${builtins.toString cfg.config.vt}";
redirect-tty = pkgs.static-nix-shell.mkPython3 {
pname = "redirect-tty";
srcRoot = ./.;
};
launcher = pkgs.writeShellApplication {
name = "unl0kr-login";
text = ''
extraPath=/run/current-system/sw/bin:/bin:${lib.makeBinPath [ cfg.package config.sane.programs.shadow.package redirect-tty ]}
locate() {
PATH=$PATH:$extraPath command -v "$1"
}
# give some time for the framebuffer device to appear;
# unl0kr depends on it but doesn't know to wait for it.
for _ in $(seq 25); do
if [ -e /dev/fb0 ]; then
break
fi
sleep 0.2
done
# TODO: make this more robust to failure.
# - if `unl0kr` fails, then the second `redirect-tty` sends a newline to `login`, causing it to exit and the service fails.
# - if `redirect-tty` fails, then... the service is left hanging.
# possible alternatives:
# - wait on `unl0kr` to complete _before_ starting `login`, and re-introduce a timeout to `login`
# i.e. `pw=$(unl0kr); (sleep 1 && echo "$pw" | redirect-tty "/dev/(tty)") &; login -p <user>`
# but modified to not leak pword to CLI
# - implement some sort of watchdog (e.g. detect spawned children?)
# - set a timeout at the outer scope (which gets canceled upon successful login)
PATH=$PATH:$extraPath sh -c 'redirect-tty "/dev/${tty}" unl0kr ; sleep 2 ; redirect-tty "/dev/${tty}" echo ""' &
# N.B.: invoke `login` by full path instead of modifying `PATH`,
# because we don't want the user session to inherit the PATH of this script!
_login="$(locate login)"
"$_login" -p ${cfg.config.user}
'';
};
in
{
sane.programs.unl0kr = {
@@ -60,14 +22,6 @@ in
type = types.int;
default = 1;
};
options.user = mkOption {
type = types.str;
description = ''
which user to login by default.
unl0kr is just a virtual keyboard for entering a password: one has to choose the user to login before launching it.
on a typical single-user install, leave this unset and the user will be chosen based on who this package is installed for.
'';
};
options.delay = mkOption {
type = types.int;
default = 3;
@@ -76,28 +30,10 @@ in
this is a safety mechanism, to allow users an exit in case DE is broken.
'';
};
options.launcher = mkOption {
type = types.package;
default = launcher;
description = ''
script to tie `unl0kr` and `login` together.
exposed for debugging.
'';
};
config = lib.mkMerge (lib.mapAttrsToList
(userName: en: lib.optionalAttrs en {
user = lib.mkDefault userName;
})
cfg.enableFor.user
);
};
};
suggestedPrograms = [
"shadow" #< for login
];
# TODO: lift this into toplevel s6 stuff
fs.".profile".symlink.text = ''
unl0krCheck() {
# if already running a desktop environment, or if running from ssh, then `tty` will show /dev/pts/NN.
@@ -125,38 +61,23 @@ in
# it still *works*, but wow, kinda weird and concerning
"/dev/tty0"
];
};
# unl0kr is run as root, and especially with sandboxing, needs to be installed for root if expected to work.
sane.programs.unl0kr.enableFor.system = lib.mkIf (builtins.any (en: en) (builtins.attrValues cfg.enableFor.user)) true;
systemd = lib.mkIf cfg.enabled {
# prevent nixos-rebuild from killing us after a redeploy
services."autovt@${tty}".enable = false;
services.unl0kr = {
serviceConfig.ExecStart = "${cfg.config.launcher}/bin/unl0kr-login";
serviceConfig.Type = "simple";
serviceConfig.Restart = "always";
serviceConfig.RestartSec = "5";
# XXX: unsure which of these are necessary nor what they do.
# copied from greetd and agetty services.
# serviceConfig.{TTYPath,StandardInput,StandardOutput} connects the corresponding tty to the service's stdin/stdout.
serviceConfig.TTYPath = "/dev/${tty}";
serviceConfig.TTYReset = "yes";
serviceConfig.StandardInput = "tty";
serviceConfig.StandardOutput = "tty";
# copied from greetd; not sure how `after` and `conflict` being identical makes any sense.
after = [ "getty@${tty}.service" ];
conflicts = [ "getty@${tty}.service" ];
wantedBy = [ "graphical.target" ];
# don't kill session on nixos re-deploy
restartIfChanged = false;
description = "unl0kr framebuffer password entry/filesystem unlocker";
partOf = [ "private-storage" ];
# TODO: lift the gocryptfs stuff into a separate service
command = pkgs.writeShellScript "unl0kr-start" ''
# if ! test -d $XDG_RUNTIME_DIR/unl0kr; then
# mkdir $XDG_RUNTIME_DIR/unl0kr
# fi
# unl0kr > $XDG_RUNTIME_DIR/unl0kr/passwd
if ! test -f /mnt/persist/private/init; then
unl0kr | sudo gocryptfs -fg /nix/persist/private /mnt/persist/private -o allow_other
fi
#^ otherwise, if already unlocked, exit true
'';
readiness.waitExists = [ "/mnt/persist/private/init" ];
};
defaultUnit = "graphical.target";
};
security.loginDefs.settings = lib.mkIf cfg.enabled {

View File

@@ -1,30 +0,0 @@
#!/usr/bin/env nix-shell
#!nix-shell -i python3 -p python3
# vim: set filetype=python :
"""
launch some program, and redirect its output to appear as if it
were *input* by the user, on some TTY.
this allows piping into programs which expect to be running directly on a TTY (like `login`, or `sway`).
based on: <https://unix.stackexchange.com/a/345572>
"""
import fcntl
import subprocess
import sys
import termios
def write_to(text: bytes, dest_path: str) -> None:
with open(dest_path, "w") as f:
for byte in text:
fcntl.ioctl(f.fileno(), termios.TIOCSTI, bytes([byte]))
def main(argv: list[str]):
dest_path = argv[1]
cmd = argv[2:]
cmd_output = subprocess.check_output(cmd)
write_to(cmd_output, dest_path)
if __name__ == "__main__":
main(sys.argv)

View File

@@ -117,4 +117,5 @@
# '';
sane.users.colin.default = true;
services.getty.autologinUser = "colin";
}

View File

@@ -240,7 +240,9 @@ let
# only do this for the services which are *defined* by this program though (i.e. `scvCfg ? description`)
# so as to avoid idioms like when sway adds `graphical-session.partOf = default`
depends = svcCfg.depends
++ lib.optionals (svcName != "dbus" && builtins.elem "user" config.sandbox.whitelistDbus && cfg.dbus.enabled) [
++ lib.optionals (((config.persist.byStore or {}).private or []) != []) [
"private-storage"
] ++ lib.optionals (svcName != "dbus" && builtins.elem "user" config.sandbox.whitelistDbus && cfg.dbus.enabled) [
"dbus"
] ++ lib.optionals ((!builtins.elem "wayland" svcCfg.partOf) && config.sandbox.whitelistWayland) [
"wayland"