diff --git a/home-manager/packages/default.nix b/home-manager/packages/default.nix index 21326a611..5a74686e5 100644 --- a/home-manager/packages/default.nix +++ b/home-manager/packages/default.nix @@ -125,19 +125,26 @@ with pkgs; ] ++ lib.optionals (stdenv.isLinux && isDesktop) [ _1password-gui + baobab brightnessctl + celluloid + cheese chromium clickup cliphist code-cursor discord + eog evince ffmpeg + file-roller + gedit ghostty github-desktop gnome-disk-utility google-chrome grim + gthumb hypridle hyprlock hyprpicker @@ -146,11 +153,14 @@ with pkgs; hyprsunset libnotify linux-wallpaperengine + loupe nautilus pavucontrol playerctl rofi rofimoji + seahorse + shotwell signal-desktop slack slurp diff --git a/named-hosts/matic/README.md b/named-hosts/matic/README.md new file mode 100644 index 000000000..9b4723f1e --- /dev/null +++ b/named-hosts/matic/README.md @@ -0,0 +1,41 @@ +# matic + +Setup and operational notes for the `matic` NixOS host. + +--- + +## GNOME Keyring Auto-Unlock via TPM2 + +The system unlocks the GNOME Keyring automatically at login — including when using fingerprint auth — by storing the keyring password as a `systemd-creds` credential encrypted with the machine's TPM2 + host key. The credential can only be decrypted on this machine. + +### One-time setup + +After first running `make switch`, create the credential (requires sudo for TPM access): + +```bash +sudo bash -c 'mkdir -p /etc/credstore.encrypted && \ + systemd-ask-password "Keyring password:" | \ + systemd-creds encrypt --name=gnome-keyring --with-key=tpm2+host \ + - /etc/credstore.encrypted/gnome-keyring.cred' +``` + +Then restart the service: + +```bash +sudo systemctl restart gnome-keyring-unlock.service +``` + +### How it works + +1. `services.gnome.gnome-keyring.enable` starts the keyring daemon at login via PAM. +2. `security.pam.services.greetd.enableGnomeKeyring` auto-unlocks for password logins. +3. `systemd.services.gnome-keyring-unlock` runs as a **system service** with `User=skakinoki` so the system manager handles TPM decryption. It then speaks the gnome-keyring **control socket protocol** directly to unlock the running daemon — covering fingerprint logins where PAM has no password to forward. +4. The service skips silently if the credential file does not exist yet. + +> **Note:** `gnome-keyring-daemon --unlock` (v48+) ignores `GNOME_KEYRING_CONTROL` and always starts a fresh instance. The service works around this by writing directly to `$XDG_RUNTIME_DIR/keyring/control` using the binary protocol: credentials byte + big-endian `[oplen][op=1][pwlen][password]`, reads `[8][result]`. +> +> **Note:** The credential must be at `/etc/credstore.encrypted/gnome-keyring.cred` (not `~/.config`). User-level systemd services cannot access TPM/host keys — only the system manager can. + +### Re-encrypting after keyring password change + +If you change your keyring password (via seahorse), re-run the setup command above to update the credential. diff --git a/named-hosts/matic/default.nix b/named-hosts/matic/default.nix index d6198060a..c326ef810 100644 --- a/named-hosts/matic/default.nix +++ b/named-hosts/matic/default.nix @@ -118,10 +118,99 @@ inputs.nixpkgs.lib.nixosSystem { # Thunderbolt support services.hardware.bolt.enable = true; + # GNOME Keyring - auto-unlocks GPG key on login via PAM + services.gnome.gnome-keyring.enable = true; + + # Unlock GNOME Keyring via TPM2 credential at login. + # System service so the system manager (not user manager) handles TPM decryption. + # Credential stored at /etc/credstore.encrypted/gnome-keyring.cred — create once with: + # sudo bash -c 'mkdir -p /etc/credstore.encrypted && \ + # systemd-ask-password "Keyring password:" | \ + # systemd-creds encrypt --name=gnome-keyring --with-key=tpm2+host \ + # - /etc/credstore.encrypted/gnome-keyring.cred' + systemd.services.gnome-keyring-unlock = { + description = "Unlock GNOME Keyring via TPM2 credential"; + after = [ "user@${toString 1000}.service" ]; + wantedBy = [ "user@${toString 1000}.service" ]; + unitConfig.ConditionPathExists = "/etc/credstore.encrypted/gnome-keyring.cred"; + serviceConfig = { + Type = "oneshot"; + User = username; + TimeoutStartSec = 60; + LoadCredentialEncrypted = "gnome-keyring:/etc/credstore.encrypted/gnome-keyring.cred"; + ExecStart = + let + # Speaks the gnome-keyring control socket protocol directly. + # gnome-keyring-daemon --unlock (v48) ignores GNOME_KEYRING_CONTROL + # and always starts a new instance, so we bypass it entirely. + # + # Protocol (all big-endian): + # 1. connect to $XDG_RUNTIME_DIR/keyring/control (UNIX stream) + # 2. send \x00 — daemon reads our UID via SO_PEERCRED + # 3. send [oplen:4][op=1:4][pwlen:4][password bytes] + # where oplen = 8 + 4 + len(password) + # 4. read [8:4][result:4] — result 0 = OK + unlockPy = pkgs.writeScript "unlock-gnome-keyring.py" '' + #!${pkgs.python3}/bin/python3 + import os, socket, struct, stat, sys + + def unlock(password): + uid = os.getuid() + xdg = os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{uid}") + sock_path = os.path.join(xdg, "keyring", "control") + st = os.lstat(sock_path) + if not stat.S_ISSOCK(st.st_mode) or st.st_uid != uid: + raise RuntimeError(f"bad socket: {sock_path}") + pw = password.encode() + oplen = 8 + 4 + len(pw) + pkt = struct.pack(">II", oplen, 1) + struct.pack(">I", len(pw)) + pw + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s: + s.connect(sock_path) + s.sendall(b"\x00") + s.sendall(pkt) + resp = b"" + while len(resp) < 8: + resp += s.recv(8 - len(resp)) + _, result = struct.unpack(">II", resp) + return result + + import time + + pw = sys.stdin.read().rstrip("\n") + codes = {0: "OK", 1: "DENIED", 2: "FAILED", 3: "NO_DAEMON"} + uid = os.getuid() + sock_path = f"/run/user/{uid}/keyring/control" + + for attempt in range(10): + # Wait for the control socket to appear (keyring daemon to start) + if not os.path.exists(sock_path): + print(f"attempt {attempt+1}: waiting for control socket...", flush=True) + time.sleep(3) + continue + result = unlock(pw) + print(f"attempt {attempt+1}: gnome-keyring unlock: {codes.get(result, result)}", flush=True) + if result == 0: + sys.exit(0) + # DENIED might mean daemon not fully ready yet, retry + time.sleep(3) + + print("gnome-keyring unlock: gave up after 10 attempts", flush=True) + sys.exit(1) + ''; + in + pkgs.writeShellScript "unlock-keyring" '' + export XDG_RUNTIME_DIR="/run/user/$(id -u)" + cat "$CREDENTIALS_DIRECTORY/gnome-keyring" | ${unlockPy} + ''; + RemainAfterExit = "yes"; + }; + }; + # Fingerprint authentication services.fprintd.enable = true; security.pam.services.greetd = { fprintAuth = true; + enableGnomeKeyring = true; }; security.pam.services.hyprlock = { fprintAuth = true; @@ -438,9 +527,9 @@ inputs.nixpkgs.lib.nixosSystem { services.gpg-agent = { enable = true; enableSshSupport = false; - pinentry.package = pkgs.pinentry-tty; - defaultCacheTtl = 94608000; # 3 years - maxCacheTtl = 94608000; # 3 years + pinentry.package = pkgs.pinentry-gnome3; + defaultCacheTtl = 2147483647; # max (effectively forever) + maxCacheTtl = 2147483647; # max (effectively forever) }; # GPG_TTY is set in fish shell init instead of sessionVariables