helium: unlock the keyring through secretstorage; anchor the running-browser check
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run

`secret-tool lookup` is non-interactive — on a locked keyring it returns
nothing and shows no dialog — and a busctl Unlock+Prompt pair cannot work
either, because Secret Service prompt objects die with the D-Bus
connection that created them. secretstorage keeps one connection:
get_default_collection().unlock() pops the gcr dialog and blocks until it
is answered. Verified on the real profile: 5 v11 rows -> v10, then with
the keyring re-locked Helium (through the wrapper, so the flags file and
LeechBlock apply) lists all 5 logins and keeps every row.

`pgrep -f /opt/helium-browser-bin` also matched the invoking shell when
its argv mentioned the path, so the guard refused on an idle machine;
now anchored to `^/opt/helium-browser-bin/`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8YGXVZdNUG79q5DGsdMm3
This commit is contained in:
Krzysztof Rudnicki 2026-09-20 18:29:49 +02:00
parent b627140cb2
commit ec2da0b575
7 changed files with 69 additions and 48 deletions

View File

@ -279,6 +279,7 @@ repos:
# Chromium os_crypt blobs (AES-CBC).
- websockets
- cryptography
- secretstorage
# Test suites and conftest fixtures are linted separately; they use
# patterns (protected-access, missing docstrings, fixture shadowing)
# that don't belong in the source-code 10/10 gate.

View File

@ -1,36 +0,0 @@
{
"intent": "Give the artifact cap a one-command fix, so the gate can never block a commit with no way out except reconstructing today's selection logic by hand.",
"scope": [
"meta/scripts/check_repo_size.sh - adds --prune",
"Non-goal: pruning anything now. The tree is at 21/21, well under the 60 cap."
],
"changes": [
"Added --prune: stages the removal of the oldest artifacts, keeping the newest KEEP_ARTIFACTS=20 per directory plus template.json, and leaves the commit to the operator.",
"Fixed a same-day ordering bug found by running it: the first version sorted on %cs (date only), so every artifact committed on the same day tied, sort fell back to comparing the path, and the prune deleted alphabetically. It removed cap-artifact-growth-2026-08-23.json - one of the newest files - instead of the oldest. Now sorts on %ct (unix seconds) with sort -rn."
],
"verification": [
{
"command": "./meta/scripts/check_repo_size.sh --prune (in a detached worktree)",
"result": "pass",
"evidence": "First run staged 2 deletions but they were WRONG: cap-artifact-growth-2026-08-23.json and flatten-linux-configuration-2026-08-23.json, both same-day files chosen alphabetically. This is what exposed the %cs tie."
},
{
"command": "./meta/scripts/check_repo_size.sh --prune after the %ct fix (fresh worktree)",
"result": "pass",
"evidence": "Now stages quarantine-arch-sysctl-cases-2026-08-22.json and thorium-repairs-tests-2026-08-23.json - the genuinely oldest. Templates survive; contracts correctly reports 'nothing to prune' at 20 + template."
},
{
"command": "./meta/scripts/check_repo_size.sh; pre-commit run --files meta/scripts/check_repo_size.sh",
"result": "pass",
"evidence": "'1050 source files (ceiling 1200)' EXIT=0; shfmt and shellcheck clean. Tested in a throwaway worktree so the real tree was never modified."
}
],
"risks": [
"--prune stages deletions rather than committing them, so an operator who runs it and forgets to review still has git diff --cached as the checkpoint.",
"KEEP_ARTIFACTS=20 is below MAX_ARTIFACTS_PER_DIR=60 on purpose; a prune therefore buys headroom instead of landing back on the limit."
],
"rollback": [
"git revert the commit. --prune is additive and off by default; the enforcing path is unchanged.",
"After rollback, confirm ./meta/scripts/check_repo_size.sh still exits 0 and prints the count."
]
}

View File

@ -0,0 +1,38 @@
{
"intent": "Make migrate_helium_passwords_to_basic.py actually work on the user's box: the first version handed back twice never showed the keyring dialog, and the running-Helium guard false-positived on an idle machine.",
"scope": [
"linux_configuration/installers/migrate_helium_passwords_to_basic.py (unlock via secretstorage; anchored process check)",
"linux_configuration/installers/verify_helium.py (anchored process check)",
"linux_configuration/installers/install_helium.sh (+python-secretstorage dep)",
".pre-commit-config.yaml pylint deps, meta/requirements.txt (+secretstorage 3.5.0)"
],
"changes": [
"`secret-tool lookup` is non-interactive: on a locked keyring it returns nothing and shows no dialog. A busctl Unlock+Prompt sequence also cannot work because Secret Service prompt objects die with the connection that created them. secretstorage keeps one D-Bus connection: get_default_collection().unlock() pops the gcr dialog and blocks until answered.",
"`pgrep -f /opt/helium-browser-bin` matched the invoking shell whose argv mentioned the path, so the guard reported Helium running on an idle machine; the pattern is now anchored to `^/opt/helium-browser-bin/`."
],
"verification": [
{
"command": "linux_configuration/installers/migrate_helium_passwords_to_basic.py (run by Claude, user answered the gcr dialog)",
"result": "pass",
"evidence": "keyring is locked — answer the unlock dialog on screen; backup: Default/Login Data.pre-basic-20260920-182556; converted 5 v11 -> v10 rows; 0 already v10; added --password-store=basic to ~/.config/helium-browser-flags.conf"
},
{
"command": "keyring re-locked via Secret.Service.Lock; /usr/local/bin/helium-browser (wrapper) on the real profile under Xvfb; chrome://password-manager/passwords deep-text; chrome://version command line; /json/list",
"result": "pass",
"evidence": "all 5 sites listed (kuhy-cloud.duckdns.org, google.com, typeracer.com, leetcode.com, infakt.pl); command line has --password-store=basic and the LeechBlock --load-extension; uBO background_page present; keyring still locked afterwards; Login Data still 5 rows, all v10 (nothing deleted on read)"
},
{
"command": "pre-commit run --files <changed files> (in the commit worktree)",
"result": "pass",
"evidence": "ruff, ruff-format, pylint, shellcheck, codespell, file-length, dependency-freshness green"
}
],
"risks": [
"secretstorage's unlock() blocks with no timeout until the dialog is answered or dismissed; the script says so on stdout before it blocks.",
"The backup file Login Data.pre-basic-<ts> still holds the v11 (keyring-encrypted) blobs; it is useless without the keyring key and can be deleted once the user is happy."
],
"rollback": [
"Restore Default/'Login Data.pre-basic-20260920-182556' over 'Login Data' and remove --password-store=basic from ~/.config/helium-browser-flags.conf.",
"Validate: Helium prompts for the keyring again and the 5 logins decrypt after unlock."
]
}

View File

@ -53,7 +53,7 @@ usage() {
# Everything the clone + verify steps fork: rsync, Xvfb, the CDP websocket
# client; base-devel + git only matter on a box without yay (makepkg path).
readonly -a PACMAN_DEPS=(rsync xorg-server-xvfb python-websockets python-cryptography base-devel git)
readonly -a PACMAN_DEPS=(rsync xorg-server-xvfb python-websockets python-cryptography python-secretstorage base-devel git)
validate_requirements() {
command -v pacman >/dev/null 2>&1 || {

View File

@ -11,8 +11,9 @@ Linux os_crypt scheme (unchanged since Chromium 4x): key = PBKDF2-HMAC-SHA1
(secret, "saltysalt", 1 iter, 16 B); AES-128-CBC, IV = 16 spaces, PKCS#7;
``v11`` uses the keyring secret, ``v10`` uses the fixed string "peanuts".
Needs the keyring unlocked ONCE — secret-tool pops the unlock dialog, so run
this yourself, with the browser closed. Never prints a secret.
Needs the keyring unlocked ONCE: it asks the Secret Service to unlock the
default collection, which pops the gcr dialog. Browser must be closed.
Never prints a secret.
"""
from __future__ import annotations
@ -28,6 +29,7 @@ import time
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import secretstorage
DEFAULT_PROFILE = Path.home() / ".config" / "net.imput.helium" / "Default"
KEYRING_SCHEMA = "chrome_libsecret_os_crypt_password_v2"
@ -37,7 +39,8 @@ KEY_LEN = 16
BASIC_SECRET = b"peanuts"
V10 = b"v10"
V11 = b"v11"
HELIUM_PROCESS_MARK = "/opt/helium-browser-bin"
# Anchored: a shell whose argv merely mentions the path must not match.
HELIUM_PROCESS_MARK = "^/opt/helium-browser-bin/"
FLAGS_FILE = Path.home() / ".config" / "helium-browser-flags.conf"
BASIC_FLAG = "--password-store=basic"
@ -69,13 +72,27 @@ def encrypt(key: bytes, plain: bytes) -> bytes:
def keyring_secret(application: str) -> bytes:
"""Fetch the browser's Safe Storage secret (this pops the unlock dialog)."""
for attrs in (["application", application], []):
cmd = ["/usr/bin/secret-tool", "lookup", "xdg:schema", KEYRING_SCHEMA, *attrs]
out = subprocess.run(cmd, capture_output=True, check=False).stdout
if out:
return out.rstrip(b"\n")
msg = f"no {KEYRING_SCHEMA} item in the keyring (locked? dialog cancelled?)"
"""Unlock the default keyring (this pops the gcr dialog) and read the key.
One D-Bus connection for the whole exchange: Secret Service prompt objects
live only as long as the connection that created them, which is why a
`secret-tool`/`busctl` sequence of separate processes can never work.
"""
conn = secretstorage.dbus_init()
collection = secretstorage.get_default_collection(conn)
if collection.is_locked():
say("keyring is locked — answer the unlock dialog on screen")
if collection.unlock(): # True means the dialog was dismissed
msg = "unlock dialog dismissed; keyring still locked"
raise SystemExit(msg)
for attrs in (
{"xdg:schema": KEYRING_SCHEMA, "application": application},
{"xdg:schema": KEYRING_SCHEMA},
):
items = list(collection.search_items(attrs))
if items:
return items[0].get_secret()
msg = f"no {KEYRING_SCHEMA} item in the default keyring"
raise SystemExit(msg)

View File

@ -25,7 +25,7 @@ from typing import Self
from websockets.sync.client import connect
HELIUM_BIN = "/usr/bin/helium-browser"
HELIUM_BIN_DIR = "/opt/helium-browser-bin"
HELIUM_BIN_DIR = "^/opt/helium-browser-bin/" # anchored pgrep -f pattern
DEFAULT_PROFILE = str(Path.home() / ".config" / "net.imput.helium")
UBO_ID = "blockjmkbacgjkknlgpkjjiijinjdanf"
POLICY_DIR = Path("/etc/chromium/policies/managed")

View File

@ -81,6 +81,7 @@ ruff==0.16.8
# safety is deliberately not pinned: its only reference in this repo is a
# commented-out pre-commit hook, and safety 3.8.1 declares `typer<0.26`,
# which would hold the typer this repo actually imports below latest.
secretstorage==3.5.0
selenium==4.49.0
typer==0.27.2
types-python-dateutil==2.9.0.20260807