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
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"secret": "YOUR_BASE32_SECRET_HERE"
|
||||
"launcher_cmd": "flatpak run dev.goats.xivlauncher"
|
||||
}
|
||||
23
installer.sh
23
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" <<EOF
|
||||
{
|
||||
"secret": "$SECRET"
|
||||
}
|
||||
EOF
|
||||
echo "Configuration created successfully at: $CONFIG_FILE"
|
||||
|
||||
echo "Creating empty configuration file..."
|
||||
echo "{}" > "$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 ""
|
||||
|
|
|
|||
88
wrapper.py
88
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
|
||||
|
|
@ -234,7 +305,7 @@ def main():
|
|||
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