Created
February 6, 2026 05:28
-
-
Save zbowling/586626607b744c7f61291bea1b420625 to your computer and use it in GitHub Desktop.
Import KWallet Chrome keys to GNOME Keyring after switching from KDE
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| kwallet-to-libsecret.py | |
| Import Chrome/Chromium Safe Storage keys from a KWallet XML export into | |
| GNOME Keyring (libsecret). This allows Chrome to decrypt its saved passwords | |
| after switching from KDE Plasma to GNOME or another desktop environment. | |
| Usage: | |
| 1. Export your KWallet: Open KWalletManager → File → Export as XML | |
| 2. Run: python3 kwallet-to-libsecret.py /path/to/wallet-export.xml | |
| 3. Restart Chrome | |
| Requirements: | |
| - secret-tool (usually part of libsecret-tools package) | |
| - GNOME Keyring or another Secret Service provider running | |
| Author: Zac Bowling | |
| License: MIT | |
| """ | |
| import argparse | |
| import subprocess | |
| import sys | |
| import xml.etree.ElementTree as ET | |
| from dataclasses import dataclass | |
| @dataclass | |
| class ImportResult: | |
| chrome_key: bool = False | |
| chromium_key: bool = False | |
| wifi_passwords: list = None | |
| def __post_init__(self): | |
| if self.wifi_passwords is None: | |
| self.wifi_passwords = [] | |
| def secret_tool_store(label: str, schema: str, application: str, secret: str) -> bool: | |
| """Store a secret in libsecret using secret-tool.""" | |
| cmd = [ | |
| "secret-tool", "store", | |
| "--label", label, | |
| "xdg:schema", schema, | |
| "application", application, | |
| ] | |
| result = subprocess.run(cmd, input=secret, capture_output=True, text=True) | |
| return result.returncode == 0 | |
| def parse_kwallet_xml(xml_path: str) -> dict: | |
| """Parse KWallet XML export and extract relevant secrets.""" | |
| tree = ET.parse(xml_path) | |
| root = tree.getroot() | |
| secrets = { | |
| "chrome_key": None, | |
| "chromium_key": None, | |
| "wifi_passwords": [], | |
| } | |
| seen_folders = set() | |
| for folder in root.findall("folder"): | |
| folder_name = folder.get("name") | |
| # KWallet exports can have duplicate folders - skip them | |
| folder_content_hash = ET.tostring(folder, encoding="unicode") | |
| if folder_content_hash in seen_folders: | |
| continue | |
| seen_folders.add(folder_content_hash) | |
| if folder_name == "Chrome Keys": | |
| for pw in folder.findall("password"): | |
| if pw.get("name") == "Chrome Safe Storage" and pw.text: | |
| secrets["chrome_key"] = pw.text.strip() | |
| elif folder_name == "Chromium Keys": | |
| for pw in folder.findall("password"): | |
| if pw.get("name") == "Chromium Safe Storage" and pw.text: | |
| secrets["chromium_key"] = pw.text.strip() | |
| elif folder_name == "Network Management": | |
| for map_entry in folder.findall("map"): | |
| map_name = map_entry.get("name", "") | |
| for entry in map_entry.findall("mapentry"): | |
| if entry.get("name") == "psk" and entry.text: | |
| secrets["wifi_passwords"].append({ | |
| "network": map_name, | |
| "psk": entry.text, | |
| }) | |
| return secrets | |
| def import_to_libsecret(secrets: dict, dry_run: bool = False) -> ImportResult: | |
| """Import parsed secrets into libsecret.""" | |
| result = ImportResult() | |
| schema = "chrome_libsecret_os_crypt_password_v2" | |
| if secrets["chrome_key"]: | |
| if dry_run: | |
| print(f"[DRY RUN] Would import Chrome Safe Storage key") | |
| result.chrome_key = True | |
| else: | |
| success = secret_tool_store( | |
| label="Chrome Safe Storage", | |
| schema=schema, | |
| application="chrome", | |
| secret=secrets["chrome_key"], | |
| ) | |
| result.chrome_key = success | |
| status = "✓" if success else "✗" | |
| print(f"{status} Chrome Safe Storage key") | |
| if secrets["chromium_key"]: | |
| if dry_run: | |
| print(f"[DRY RUN] Would import Chromium Safe Storage key") | |
| result.chromium_key = True | |
| else: | |
| success = secret_tool_store( | |
| label="Chromium Safe Storage", | |
| schema=schema, | |
| application="chromium", | |
| secret=secrets["chromium_key"], | |
| ) | |
| result.chromium_key = success | |
| status = "✓" if success else "✗" | |
| print(f"{status} Chromium Safe Storage key") | |
| # Deduplicate WiFi passwords | |
| seen_psk = set() | |
| for wifi in secrets["wifi_passwords"]: | |
| if wifi["psk"] not in seen_psk: | |
| result.wifi_passwords.append(wifi["psk"]) | |
| seen_psk.add(wifi["psk"]) | |
| return result | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Import KWallet Chrome/Chromium keys into GNOME Keyring (libsecret)", | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| epilog=""" | |
| Examples: | |
| %(prog)s ~/Documents/kwallet-export.xml | |
| %(prog)s --dry-run ~/Documents/kwallet-export.xml | |
| %(prog)s --show-wifi ~/Documents/kwallet-export.xml | |
| After importing, restart Chrome to access your saved passwords. | |
| """, | |
| ) | |
| parser.add_argument("xml_file", help="Path to KWallet XML export file") | |
| parser.add_argument( | |
| "--dry-run", "-n", | |
| action="store_true", | |
| help="Show what would be imported without actually importing", | |
| ) | |
| parser.add_argument( | |
| "--show-wifi", | |
| action="store_true", | |
| help="Display WiFi PSKs found in the export (not imported, just shown)", | |
| ) | |
| args = parser.parse_args() | |
| # Check for secret-tool | |
| if subprocess.run(["which", "secret-tool"], capture_output=True).returncode != 0: | |
| print("Error: secret-tool not found.", file=sys.stderr) | |
| print("Install it with: sudo apt install libsecret-tools", file=sys.stderr) | |
| print(" or: sudo dnf install libsecret", file=sys.stderr) | |
| sys.exit(1) | |
| try: | |
| secrets = parse_kwallet_xml(args.xml_file) | |
| except FileNotFoundError: | |
| print(f"Error: File not found: {args.xml_file}", file=sys.stderr) | |
| sys.exit(1) | |
| except ET.ParseError as e: | |
| print(f"Error: Invalid XML: {e}", file=sys.stderr) | |
| sys.exit(1) | |
| print("Found in KWallet export:") | |
| print(f" Chrome Safe Storage key: {'Yes' if secrets['chrome_key'] else 'No'}") | |
| print(f" Chromium Safe Storage key: {'Yes' if secrets['chromium_key'] else 'No'}") | |
| print(f" WiFi passwords: {len(secrets['wifi_passwords'])}") | |
| print() | |
| if not secrets["chrome_key"] and not secrets["chromium_key"]: | |
| print("No Chrome/Chromium keys found to import.") | |
| sys.exit(0) | |
| print("Importing to libsecret..." if not args.dry_run else "Dry run:") | |
| result = import_to_libsecret(secrets, dry_run=args.dry_run) | |
| if args.show_wifi and result.wifi_passwords: | |
| print() | |
| print("WiFi PSKs (for manual entry in NetworkManager):") | |
| for psk in result.wifi_passwords: | |
| print(f" • {psk}") | |
| print() | |
| if not args.dry_run: | |
| print("Done! Restart Chrome to access your saved passwords.") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment