feat: implement graceful fallback to config.json for OTP secret storage when the keyring module is unavailable.

This commit is contained in:
CPTN Cosmo 2026-02-02 02:27:56 +01:00
parent d52743d69c
commit 252a7ed239
No known key found for this signature in database

View file

@ -13,7 +13,11 @@ import hashlib
import os import os
import shutil import shutil
import configparser import configparser
import keyring try:
import keyring
HAS_KEYRING = True
except ImportError:
HAS_KEYRING = False
import getpass import getpass
import urllib.parse import urllib.parse
@ -172,42 +176,51 @@ def detect_launch_command():
def get_secret(config, config_path): def get_secret(config, config_path):
""" """
Retrieves the OTP secret in the following priority: Retrieves the OTP secret in the following priority:
1. System Keyring (SERVICE_NAME, USERNAME) 1. System Keyring (SERVICE_NAME, USERNAME) - if available
2. config.json (Legacy) - if found, migrates to Keyring and removes from config. 2. config.json - if Keyring unavailable or secret found there (and Keyring unavailable for migration)
3. User Input - prompts user and saves to Keyring. 3. User Input - prompts user and saves to Keyring (if available) or config.json.
""" """
SERVICE_NAME = "XIVLauncherWrapper" SERVICE_NAME = "XIVLauncherWrapper"
USERNAME = "OTP_SECRET" USERNAME = "OTP_SECRET"
# 1. Try Keyring # 1. Try Keyring
try: if HAS_KEYRING:
secret = keyring.get_password(SERVICE_NAME, USERNAME) try:
if secret: secret = keyring.get_password(SERVICE_NAME, USERNAME)
return secret if secret:
except Exception as e: return secret
print(f"Warning: Failed to access keyring: {e}") except Exception as e:
print(f"Warning: Failed to access keyring: {e}")
# 2. Try Config (Legacy/Migration) # 2. Try Config
if config and "secret" in config: if config and "secret" in config:
secret = config["secret"] secret = config["secret"]
if secret and secret != "YOUR_BASE32_SECRET_HERE": if secret and secret != "YOUR_BASE32_SECRET_HERE":
print("Migrating secret from config.json to system keyring...") if HAS_KEYRING:
try: print("Migrating secret from config.json to system keyring...")
keyring.set_password(SERVICE_NAME, USERNAME, secret) try:
keyring.set_password(SERVICE_NAME, USERNAME, secret)
# Remove from config # Remove from config
del config["secret"] del config["secret"]
with open(config_path, 'w') as f: with open(config_path, 'w') as f:
json.dump(config, f, indent=4) json.dump(config, f, indent=4)
print("Secret migrated successfully and removed from config.json.") print("Secret migrated successfully and removed from config.json.")
return secret return secret
except Exception as e: except Exception as e:
print(f"Error migrating to keyring: {e}") print(f"Error migrating to keyring: {e}")
print("Continuing with secret from config...") print("Continuing with secret from config...")
return secret return secret
else:
# No keyring, just use config
return secret
# 3. Prompt User # 3. Prompt User
print("\nOTP Secret not found in keyring.") if HAS_KEYRING:
print("\nOTP Secret not found in keyring.")
else:
print("\nOTP Secret not found in config.json and keyring module is missing.")
print("Please enter your TOTP Secret (base32) or otpauth:// URL.") print("Please enter your TOTP Secret (base32) or otpauth:// URL.")
while True: while True:
try: try:
@ -222,16 +235,26 @@ def get_secret(config, config_path):
print("Invalid secret format. Please try again.") print("Invalid secret format. Please try again.")
continue continue
try: if HAS_KEYRING:
keyring.set_password(SERVICE_NAME, USERNAME, parsed) try:
print("Secret saved to system keyring.") keyring.set_password(SERVICE_NAME, USERNAME, parsed)
return parsed print("Secret saved to system keyring.")
except Exception as e: return parsed
print(f"Error saving to keyring: {e}") except Exception as e:
# If we can't save to keyring, we might just return it in memory for this session print(f"Error saving to keyring: {e}")
# But typically we want persistence. print("Proceeding with in-memory secret for this session.")
print("Proceeding with in-memory secret for this session.") return parsed
return parsed else:
# Save to config.json
try:
config['secret'] = parsed
with open(config_path, 'w') as f:
json.dump(config, f, indent=4)
print(f"Secret saved to {config_path}")
return parsed
except Exception as e:
print(f"Error saving to config.json: {e}")
return parsed
except KeyboardInterrupt: except KeyboardInterrupt:
print("\nOperation cancelled.") print("\nOperation cancelled.")