Skip to content

Instantly share code, notes, and snippets.

@zbowling
Created February 6, 2026 05:28
Show Gist options
  • Select an option

  • Save zbowling/586626607b744c7f61291bea1b420625 to your computer and use it in GitHub Desktop.

Select an option

Save zbowling/586626607b744c7f61291bea1b420625 to your computer and use it in GitHub Desktop.
Import KWallet Chrome keys to GNOME Keyring after switching from KDE
#!/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