mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-14 17:30:47 +01:00
Chromium 151 removed the last Manifest V2 code paths and the Web Store purged uBlock Origin on 2026-08-31, so ungoogled-chromium 153 cannot load the full blocker any more. Helium (GPL-3, ungoogled-chromium base) bundles uBO 1.75.0 as a component extension and keeps MV2 alive. - installers/install_helium.sh: pacman deps, AUR via yay or makepkg (+validpgpkeys import), /usr/local/bin/helium-browser -> browser-preexec-wrapper so LeechBlock NG and the hosts refresh still apply, --clone-profile with a downgrade check and running-browser refusal, then the verifier. Passed in vmbox from scratch, then host. - installers/verify_helium.py: Xvfb + DevTools exit-code gate — uBO background_page + manifest_version 2, every /etc/chromium/policies key on chrome://policy (Helium reads that dir natively), adblock-tester >= 95. - installers/migrate_helium_passwords_to_basic.py: lightdm autologin leaves gnome-keyring locked, so the 5 v11 logins make Helium prompt on every launch; this re-encrypts them as v10 and only then enables --password-store=basic. - register helium-browser in aur_packages.txt and the periodic browser wrapper/service lists; pylint hook gets websockets + cryptography; codespell learns the vendor name; anthropic 1.6.0 -> 1.7.0 (gate red); oldest evidence artifact pruned to stay under the 60-file cap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8YGXVZdNUG79q5DGsdMm3
184 lines
6.5 KiB
Python
Executable File
184 lines
6.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Re-encrypt keyring-bound (v11) Chromium passwords as basic-store (v10).
|
|
|
|
Why: lightdm autologin leaves gnome-keyring locked, so a Chromium-family
|
|
browser that finds its "Safe Storage" key in the keyring prompts to unlock it
|
|
on every launch. Running Helium with ``--password-store=basic`` stops that,
|
|
but ``v11`` rows (encrypted with the keyring key) become unreadable — and
|
|
Chromium deletes undecryptable logins on read. This converts them first.
|
|
|
|
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.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
from pathlib import Path
|
|
import shutil
|
|
import sqlite3
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
from cryptography.hazmat.primitives import padding
|
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
|
|
DEFAULT_PROFILE = Path.home() / ".config" / "net.imput.helium" / "Default"
|
|
KEYRING_SCHEMA = "chrome_libsecret_os_crypt_password_v2"
|
|
SALT = b"saltysalt"
|
|
IV = b" " * 16
|
|
KEY_LEN = 16
|
|
BASIC_SECRET = b"peanuts"
|
|
V10 = b"v10"
|
|
V11 = b"v11"
|
|
HELIUM_PROCESS_MARK = "/opt/helium-browser-bin"
|
|
FLAGS_FILE = Path.home() / ".config" / "helium-browser-flags.conf"
|
|
BASIC_FLAG = "--password-store=basic"
|
|
|
|
|
|
def say(msg: str, *, err: bool = False) -> None:
|
|
"""Write one line to stdout (or stderr); explicit so lint autofix cannot drop it."""
|
|
(sys.stderr if err else sys.stdout).write(msg + "\n")
|
|
|
|
|
|
def derive_key(secret: bytes) -> bytes:
|
|
"""Chromium's os_crypt Linux KDF."""
|
|
return hashlib.pbkdf2_hmac("sha1", secret, SALT, 1, KEY_LEN)
|
|
|
|
|
|
def decrypt(key: bytes, blob: bytes) -> bytes:
|
|
"""AES-128-CBC + PKCS#7 strip; ``blob`` is the payload after the 3-byte tag."""
|
|
dec = Cipher(algorithms.AES(key), modes.CBC(IV)).decryptor()
|
|
padded = dec.update(blob) + dec.finalize()
|
|
unpad = padding.PKCS7(algorithms.AES.block_size).unpadder()
|
|
return unpad.update(padded) + unpad.finalize()
|
|
|
|
|
|
def encrypt(key: bytes, plain: bytes) -> bytes:
|
|
"""Inverse of :func:`decrypt`, without the tag."""
|
|
pad = padding.PKCS7(algorithms.AES.block_size).padder()
|
|
padded = pad.update(plain) + pad.finalize()
|
|
enc = Cipher(algorithms.AES(key), modes.CBC(IV)).encryptor()
|
|
return enc.update(padded) + enc.finalize()
|
|
|
|
|
|
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?)"
|
|
raise SystemExit(msg)
|
|
|
|
|
|
def browser_running() -> bool:
|
|
"""SQLite writes under a live browser are lost or corrupt the DB."""
|
|
res = subprocess.run(
|
|
["/usr/bin/pgrep", "-f", HELIUM_PROCESS_MARK], capture_output=True, check=False
|
|
)
|
|
return res.returncode == 0
|
|
|
|
|
|
def migrate(
|
|
db: Path, ring_key: bytes, basic_key: bytes, *, dry_run: bool
|
|
) -> tuple[int, int]:
|
|
"""Rewrite every v11 row in ``logins`` as v10; return (converted, already_v10)."""
|
|
conn = sqlite3.connect(db)
|
|
try:
|
|
rows = conn.execute("SELECT id, password_value FROM logins").fetchall()
|
|
converted = already = 0
|
|
for row_id, blob in rows:
|
|
if blob[:3] == V10:
|
|
already += 1
|
|
continue
|
|
if blob[:3] != V11:
|
|
say(f" row {row_id}: unknown tag {blob[:3]!r}, left alone", err=True)
|
|
continue
|
|
plain = decrypt(ring_key, blob[3:])
|
|
new_blob = V10 + encrypt(basic_key, plain)
|
|
if decrypt(basic_key, new_blob[3:]) != plain:
|
|
msg = f"row {row_id}: round-trip mismatch, aborting before any write"
|
|
raise SystemExit(msg)
|
|
if not dry_run:
|
|
conn.execute(
|
|
"UPDATE logins SET password_value = ? WHERE id = ?",
|
|
(new_blob, row_id),
|
|
)
|
|
converted += 1
|
|
if not dry_run:
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
return converted, already
|
|
|
|
|
|
def enable_basic_store() -> None:
|
|
"""Append the flag Helium's wrapper reads — only after the rows are converted.
|
|
|
|
Written here rather than by the installer so a restart can never happen
|
|
between "flag on" and "rows converted": with the flag on and v11 rows
|
|
still present, Chromium would treat them as undecryptable and delete them.
|
|
"""
|
|
existing = FLAGS_FILE.read_text() if FLAGS_FILE.exists() else ""
|
|
if BASIC_FLAG in existing.split():
|
|
return
|
|
with FLAGS_FILE.open("a") as fh:
|
|
fh.write(f"{BASIC_FLAG}\n")
|
|
say(f"added {BASIC_FLAG} to {FLAGS_FILE}")
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
"""CLI flags."""
|
|
parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
|
|
parser.add_argument(
|
|
"--profile",
|
|
type=Path,
|
|
default=DEFAULT_PROFILE,
|
|
help="profile dir holding 'Login Data'",
|
|
)
|
|
parser.add_argument(
|
|
"--application",
|
|
default="chromium",
|
|
help="keyring item's 'application' attribute",
|
|
)
|
|
parser.add_argument(
|
|
"--dry-run", action="store_true", help="decrypt + verify only, write nothing"
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> int:
|
|
"""Back up, convert, report counts."""
|
|
args = parse_args()
|
|
db = args.profile / "Login Data"
|
|
if not db.is_file():
|
|
say(f"no such file: {db}", err=True)
|
|
return 2
|
|
if browser_running():
|
|
say("Helium is running — close it first, then rerun.", err=True)
|
|
return 3
|
|
ring_key = derive_key(keyring_secret(args.application))
|
|
basic_key = derive_key(BASIC_SECRET)
|
|
if not args.dry_run:
|
|
backup = db.with_name(f"Login Data.pre-basic-{time.strftime('%Y%m%d-%H%M%S')}")
|
|
shutil.copy2(db, backup)
|
|
say(f"backup: {backup}")
|
|
converted, already = migrate(db, ring_key, basic_key, dry_run=args.dry_run)
|
|
verb = "would convert" if args.dry_run else "converted"
|
|
say(f"{verb} {converted} v11 -> v10 rows; {already} already v10")
|
|
if not args.dry_run:
|
|
enable_basic_store()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|