feat: Implement keyring-based OTP secret storage with migration from config.json and allow launcher command configuration.
This commit is contained in:
parent
1ec9339b26
commit
d52743d69c
3 changed files with 89 additions and 26 deletions
90
wrapper.py
90
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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue