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
|
fi
|
||||||
|
|
||||||
if [ "$SETUP_CONFIG" = true ]; then
|
if [ "$SETUP_CONFIG" = true ]; then
|
||||||
echo "We will now set up the configuration file."
|
echo "Creating empty configuration file..."
|
||||||
echo "You will need your generic TOTP secret (base32 format) OR a full otpauth:// URL."
|
echo "{}" > "$CONFIG_FILE"
|
||||||
read -p "Enter your TOTP Secret or URL: " SECRET
|
echo "Configuration created at: $CONFIG_FILE"
|
||||||
|
echo "You will be prompted for your OTP secret when you run the wrapper for the first time."
|
||||||
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"
|
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
|
||||||
88
wrapper.py
88
wrapper.py
|
|
@ -13,6 +13,8 @@ import hashlib
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import configparser
|
import configparser
|
||||||
|
import keyring
|
||||||
|
import getpass
|
||||||
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
|
|
@ -165,6 +167,76 @@ def detect_launch_command():
|
||||||
|
|
||||||
return commands
|
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():
|
def main():
|
||||||
# Determine config path (XDG standard)
|
# Determine config path (XDG standard)
|
||||||
xdg_config_home = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config'))
|
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
|
# 1. Check and Update launcher.ini
|
||||||
check_and_update_launcher_ini()
|
check_and_update_launcher_ini()
|
||||||
|
|
||||||
secret = config.get("secret")
|
|
||||||
cmd = config.get("launcher_cmd")
|
cmd = config.get("launcher_cmd")
|
||||||
|
|
||||||
config_dirty = False
|
config_dirty = False
|
||||||
|
|
@ -234,7 +305,7 @@ def main():
|
||||||
pass
|
pass
|
||||||
print("Invalid selection. Please enter a number from the list.")
|
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:
|
if config_dirty:
|
||||||
try:
|
try:
|
||||||
with open(config_path, 'w') as f:
|
with open(config_path, 'w') as f:
|
||||||
|
|
@ -243,8 +314,11 @@ def main():
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
print(f"Warning: Could not save updated config: {e}")
|
print(f"Warning: Could not save updated config: {e}")
|
||||||
|
|
||||||
if not secret or secret == "YOUR_BASE32_SECRET_HERE":
|
# Retrieve Secret (Keyring refactor)
|
||||||
print("Error: Invalid or missing 'secret' in config.json.")
|
secret = get_secret(config, config_path)
|
||||||
|
|
||||||
|
if not secret:
|
||||||
|
print("Error: No secret available.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print(f"Launch Command: {cmd}")
|
print(f"Launch Command: {cmd}")
|
||||||
|
|
@ -260,7 +334,11 @@ def main():
|
||||||
import shlex
|
import shlex
|
||||||
args = shlex.split(cmd)
|
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:
|
except FileNotFoundError:
|
||||||
print(f"Error: Command executable not found: {cmd}")
|
print(f"Error: Command executable not found: {cmd}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue