diff --git a/config.example.json b/config.example.json index 228cb47..e2dbf13 100644 --- a/config.example.json +++ b/config.example.json @@ -1,3 +1,3 @@ { - "secret": "YOUR_BASE32_SECRET_HERE" + "launcher_cmd": "flatpak run dev.goats.xivlauncher" } \ No newline at end of file diff --git a/installer.sh b/installer.sh index f3bef78..398a9b8 100755 --- a/installer.sh +++ b/installer.sh @@ -142,25 +142,10 @@ if [ -f "$CONFIG_FILE" ]; then fi if [ "$SETUP_CONFIG" = true ]; then - echo "We will now set up the configuration file." - echo "You will need your generic TOTP secret (base32 format) OR a full otpauth:// URL." - read -p "Enter your TOTP Secret or URL: " SECRET - - if [ -z "$SECRET" ]; then - echo "Error: Secret cannot be empty." - # We can continue to install files even if config fails? - # Or should we exit? Probably exit or just warn. - # Let's exit for safety if they intended to set it up but failed. - exit 1 - fi - - cat > "$CONFIG_FILE" < "$CONFIG_FILE" + echo "Configuration created at: $CONFIG_FILE" + echo "You will be prompted for your OTP secret when you run the wrapper for the first time." fi echo "" diff --git a/wrapper.py b/wrapper.py index 0389023..3cb71c6 100755 --- a/wrapper.py +++ b/wrapper.py @@ -13,6 +13,8 @@ import hashlib import os import shutil import configparser +import keyring +import getpass import urllib.parse @@ -165,6 +167,76 @@ def detect_launch_command(): return commands + return commands + +def get_secret(config, config_path): + """ + Retrieves the OTP secret in the following priority: + 1. System Keyring (SERVICE_NAME, USERNAME) + 2. config.json (Legacy) - if found, migrates to Keyring and removes from config. + 3. User Input - prompts user and saves to Keyring. + """ + SERVICE_NAME = "XIVLauncherWrapper" + USERNAME = "OTP_SECRET" + + # 1. Try Keyring + try: + secret = keyring.get_password(SERVICE_NAME, USERNAME) + if secret: + return secret + except Exception as e: + print(f"Warning: Failed to access keyring: {e}") + + # 2. Try Config (Legacy/Migration) + if config and "secret" in config: + secret = config["secret"] + if secret and secret != "YOUR_BASE32_SECRET_HERE": + print("Migrating secret from config.json to system keyring...") + try: + keyring.set_password(SERVICE_NAME, USERNAME, secret) + + # Remove from config + del config["secret"] + with open(config_path, 'w') as f: + json.dump(config, f, indent=4) + print("Secret migrated successfully and removed from config.json.") + return secret + except Exception as e: + print(f"Error migrating to keyring: {e}") + print("Continuing with secret from config...") + return secret + + # 3. Prompt User + print("\nOTP Secret not found in keyring.") + print("Please enter your TOTP Secret (base32) or otpauth:// URL.") + while True: + try: + secret_input = getpass.getpass("Secret: ").strip() + if not secret_input: + print("Secret cannot be empty.") + continue + + # Basic validation/parsing check + parsed = parse_secret(secret_input) + if not parsed: + print("Invalid secret format. Please try again.") + continue + + try: + keyring.set_password(SERVICE_NAME, USERNAME, parsed) + print("Secret saved to system keyring.") + return parsed + except Exception as e: + print(f"Error saving to keyring: {e}") + # If we can't save to keyring, we might just return it in memory for this session + # But typically we want persistence. + print("Proceeding with in-memory secret for this session.") + return parsed + + except KeyboardInterrupt: + print("\nOperation cancelled.") + sys.exit(1) + def main(): # Determine config path (XDG standard) xdg_config_home = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config')) @@ -194,7 +266,6 @@ def main(): # 1. Check and Update launcher.ini check_and_update_launcher_ini() - secret = config.get("secret") cmd = config.get("launcher_cmd") config_dirty = False @@ -233,8 +304,8 @@ def main(): except ValueError: pass print("Invalid selection. Please enter a number from the list.") - - # Save config if updated + + # Save config if updated (e.g. launcher_cmd detected) if config_dirty: try: with open(config_path, 'w') as f: @@ -243,8 +314,11 @@ def main(): except OSError as e: print(f"Warning: Could not save updated config: {e}") - if not secret or secret == "YOUR_BASE32_SECRET_HERE": - print("Error: Invalid or missing 'secret' in config.json.") + # Retrieve Secret (Keyring refactor) + secret = get_secret(config, config_path) + + if not secret: + print("Error: No secret available.") sys.exit(1) print(f"Launch Command: {cmd}") @@ -260,7 +334,11 @@ def main(): import shlex args = shlex.split(cmd) - proc = subprocess.Popen(args) + # Inject XL_SECRET_PROVIDER=file environment variable + env = os.environ.copy() + env['XL_SECRET_PROVIDER'] = 'file' + + proc = subprocess.Popen(args, env=env) except FileNotFoundError: print(f"Error: Command executable not found: {cmd}") sys.exit(1)