-
Notifications
You must be signed in to change notification settings - Fork 0
feat(packages): add GNOME desktop apps for isDesktop #1164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4b4f70c
25c4441
5d170aa
66d07b0
8c5618c
c514f3b
7a7875c
263190b
c046327
9c3274c
69fd621
b28f48b
581b089
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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" ]; | ||||||||||||||||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||||||||||||||||
| 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)) | ||||||||||||||||
|
Comment on lines
+172
to
+173
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Infinite loop if the daemon closes the connection before sending a full 8-byte response. Prompt for AI agents
Suggested change
|
||||||||||||||||
| _, 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) | ||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Handle Prompt for AI agents
Suggested change
|
||||||||||||||||
| 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; | ||||||||||||||||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||||||||||||||||
| }; | ||||||||||||||||
| 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) | ||||||||||||||||
|
Comment on lines
+531
to
+532
Comment on lines
+530
to
+532
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: For desktop usage, the official GnuPG documentation recommends the default values for gpg-agent: default-cache-ttl of 600 seconds (10 minutes) and max-cache-ttl of 7200 seconds (2 hours). These are explicitly stated as the defaults in the gpg-agent man page and agent options documentation across multiple GnuPG versions, including the current stable series (2.5.x as of 2026). No specific "recommended" values different from defaults are given for desktop usage in official docs; the defaults are considered suitable for general interactive use, balancing convenience and security. Setting default-cache-ttl and max-cache-ttl to 2147483647 (2^31-1 seconds, approximately 68 years) effectively disables passphrase expiration. Security implications include: - Passphrases remain in memory indefinitely (until agent restart or manual eviction), increasing risk if the machine is compromised, stolen, or accessed by an unauthorized local user. - Heightens exposure to memory-dumping attacks (e.g., via tools like gpg_reaper demonstrated in a GnuPG bug report). - Conflicts with security best practices for desktop environments, where session-based caching (e.g., until logout) is preferable. - Even with long TTLs, newer GnuPG versions use timers for eviction (not just on access), but extremely long values still pose risks. To apply: Edit ~/.gnupg/gpg-agent.conf with the options (without leading --), then gpgconf --kill gpg-agent; gpgconf --launch gpg-agent. Citations:
Avoid effectively-forever GPG cache TTLs. Lines 473–474 set passphrase cache retention to ~68 years (2147483647 seconds). Official GnuPG documentation recommends default-cache-ttl of 600 seconds (10 minutes) and max-cache-ttl of 7200 seconds (2 hours). Extended TTLs significantly weaken workstation security if the user session is compromised, as passphrases remain in memory indefinitely and increase exposure to memory-dumping attacks. Keep the GNOME unlock flow but bound gpg-agent TTLs to practical limits. Suggested change services.gpg-agent = {
enable = true;
enableSshSupport = false;
pinentry.package = pkgs.pinentry-gnome3;
- defaultCacheTtl = 2147483647; # max (effectively forever)
- maxCacheTtl = 2147483647; # max (effectively forever)
+ defaultCacheTtl = 28800; # 8h
+ maxCacheTtl = 86400; # 24h cap
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| }; | ||||||||||||||||
|
|
||||||||||||||||
| # GPG_TTY is set in fish shell init instead of sessionVariables | ||||||||||||||||
|
|
||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change introduces
loupe, and another part of the pull request addseog(on line 137). Both are image viewers, withloupebeing the modern successor toeogin GNOME. To avoid redundancy in installed applications, you might want to consider including only one of them. For a more modern setup, keepingloupeand removingeogwould be a reasonable choice.