1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-06-04 17:41:18 +03:00

Use keyring for password storage

Fixes #89
This commit is contained in:
Jonathan Harris 2017-01-27 13:38:40 +00:00
parent ca7d0f9c4c
commit a3949c17c8
6 changed files with 52 additions and 21 deletions

View File

@ -83,7 +83,8 @@ try:
else: else:
session = companion.Session() session = companion.Session()
if config.get('cmdrs'): if config.get('cmdrs'):
session.login(config.get('fdev_usernames')[0], config.get('fdev_passwords')[0]) username = config.get('fdev_usernames')[0]
session.login(username, config.get_password(username))
else: # <= 2.25 not yet migrated else: # <= 2.25 not yet migrated
session.login(config.get('username'), config.get('password')) session.login(config.get('username'), config.get('password'))
querytime = int(time()) querytime = int(time())

View File

@ -6,6 +6,7 @@ from sys import platform
from collections import OrderedDict from collections import OrderedDict
from functools import partial from functools import partial
import json import json
import keyring
from os import chdir, mkdir from os import chdir, mkdir
from os.path import dirname, expanduser, isdir, join from os.path import dirname, expanduser, isdir, join
import re import re
@ -276,6 +277,11 @@ class AppWindow:
if not getattr(sys, 'frozen', False): if not getattr(sys, 'frozen', False):
self.updater.checkForUpdates() # Sparkle / WinSparkle does this automatically for packaged apps self.updater.checkForUpdates() # Sparkle / WinSparkle does this automatically for packaged apps
try:
config.get_password('') # Prod SecureStorage on Linux to initialise
except RuntimeError:
pass
# Migration from <= 2.25 # Migration from <= 2.25
if not config.get('cmdrs') and config.get('username') and config.get('password'): if not config.get('cmdrs') and config.get('username') and config.get('password'):
try: try:
@ -287,6 +293,9 @@ class AppWindow:
self.postprefs() # Companion login happens in callback from monitor self.postprefs() # Companion login happens in callback from monitor
if keyring.get_keyring().priority < 1:
self.status['text'] = 'Warning: Storing passwords as text' # Shouldn't happen unless no secure storage on Linux
# Try to obtain exclusive lock on journal cache, even if we don't need it yet # Try to obtain exclusive lock on journal cache, even if we don't need it yet
if not eddn.load(): if not eddn.load():
self.status['text'] = 'Error: Is another copy of this app already running?' # Shouldn't happen - don't bother localizing self.status['text'] = 'Error: Is another copy of this app already running?' # Shouldn't happen - don't bother localizing
@ -345,7 +354,8 @@ class AppWindow:
if not monitor.cmdr or not config.get('cmdrs') or monitor.cmdr not in config.get('cmdrs'): if not monitor.cmdr or not config.get('cmdrs') or monitor.cmdr not in config.get('cmdrs'):
raise companion.CredentialsError() raise companion.CredentialsError()
idx = config.get('cmdrs').index(monitor.cmdr) idx = config.get('cmdrs').index(monitor.cmdr)
self.session.login(config.get('fdev_usernames')[idx], config.get('fdev_passwords')[idx]) username = config.get('fdev_usernames')[idx]
self.session.login(username, config.get_password(username))
self.status['text'] = '' self.status['text'] = ''
except companion.VerificationRequired: except companion.VerificationRequired:
return prefs.AuthenticationDialog(self.w, partial(self.verify, self.login)) return prefs.AuthenticationDialog(self.w, partial(self.verify, self.login))

View File

@ -165,17 +165,17 @@ Download and extract the source code of the [latest release](https://github.com/
Mac: Mac:
* Requires the Python “requests” and “watchdog” modules, plus an up-to-date “py2app” module if you also want to package the app - install these with `easy_install -U requests watchdog py2app` . * Requires the Python “keyring”, “requests” and “watchdog” modules, plus an up-to-date “py2app” module if you also want to package the app - install these with `easy_install -U keyring requests watchdog py2app` .
* Run with `./EDMarketConnector.py` . * Run with `./EDMarketConnector.py` .
Windows: Windows:
* Requires Python2.7 and the Python “requests” and “watchdog” modules. * Requires Python2.7 and the Python “keyring”, “requests” and “watchdog” modules, plus “py2exe” 0.6 if you also want to package the app.
* Run with `EDMarketConnector.py` . * Run with `EDMarketConnector.py` .
Linux: Linux:
* Requires the Python “imaging-tk”, “iniparse” and “requests” modules. On Debian-based systems install these with `sudo apt-get install python-imaging-tk python-iniparse python-requests` . * Requires the Python “imaging-tk”, “iniparse”, “keyring” and “requests” modules. On Debian-based systems install these with `sudo apt-get install python-imaging-tk python-iniparse python-keyring python-requests` .
* Run with `./EDMarketConnector.py` . * Run with `./EDMarketConnector.py` .
Command-line Command-line

View File

@ -1,3 +1,4 @@
import keyring
import numbers import numbers
import sys import sys
from os import getenv, makedirs, mkdir, pardir from os import getenv, makedirs, mkdir, pardir
@ -130,12 +131,12 @@ class Config:
if not getattr(sys, 'frozen', False): if not getattr(sys, 'frozen', False):
# Don't use Python's settings if interactive # Don't use Python's settings if interactive
self.bundle = 'uk.org.marginal.%s' % appname.lower() self.identifier = 'uk.org.marginal.%s' % appname.lower()
NSBundle.mainBundle().infoDictionary()['CFBundleIdentifier'] = self.bundle NSBundle.mainBundle().infoDictionary()['CFBundleIdentifier'] = self.identifier
else: else:
self.bundle = NSBundle.mainBundle().bundleIdentifier() self.identifier = NSBundle.mainBundle().bundleIdentifier()
self.defaults = NSUserDefaults.standardUserDefaults() self.defaults = NSUserDefaults.standardUserDefaults()
self.settings = dict(self.defaults.persistentDomainForName_(self.bundle) or {}) # make writeable self.settings = dict(self.defaults.persistentDomainForName_(self.identifier) or {}) # make writeable
# Check out_dir exists # Check out_dir exists
if not self.get('outdir') or not isdir(self.get('outdir')): if not self.get('outdir') or not isdir(self.get('outdir')):
@ -161,7 +162,7 @@ class Config:
self.settings.pop(key, None) self.settings.pop(key, None)
def save(self): def save(self):
self.defaults.setPersistentDomain_forName_(self.settings, self.bundle) self.defaults.setPersistentDomain_forName_(self.settings, self.identifier)
self.defaults.synchronize() self.defaults.synchronize()
def close(self): def close(self):
@ -188,6 +189,8 @@ class Config:
self.respath = dirname(getattr(sys, 'frozen', False) and sys.executable or __file__) self.respath = dirname(getattr(sys, 'frozen', False) and sys.executable or __file__)
self.identifier = applongname
self.hkey = HKEY() self.hkey = HKEY()
disposition = DWORD() disposition = DWORD()
if RegCreateKeyEx(HKEY_CURRENT_USER, r'Software\Marginal\EDMarketConnector', 0, None, 0, KEY_ALL_ACCESS, None, ctypes.byref(self.hkey), ctypes.byref(disposition)): if RegCreateKeyEx(HKEY_CURRENT_USER, r'Software\Marginal\EDMarketConnector', 0, None, 0, KEY_ALL_ACCESS, None, ctypes.byref(self.hkey), ctypes.byref(disposition)):
@ -281,6 +284,8 @@ class Config:
self.respath = dirname(__file__) self.respath = dirname(__file__)
self.identifier = 'uk.org.marginal.%s' % appname.lower()
self.filename = join(getenv('XDG_CONFIG_HOME', expanduser('~/.config')), appname, '%s.ini' % appname) self.filename = join(getenv('XDG_CONFIG_HOME', expanduser('~/.config')), appname, '%s.ini' % appname)
if not isdir(dirname(self.filename)): if not isdir(dirname(self.filename)):
makedirs(dirname(self.filename)) makedirs(dirname(self.filename))
@ -334,5 +339,19 @@ class Config:
def __init__(self): def __init__(self):
raise NotImplementedError('Implement me') raise NotImplementedError('Implement me')
# Common
def get_password(self, account):
return keyring.get_password(self.identifier, account)
def set_password(self, account, password):
keyring.set_password(self.identifier, account, password)
def delete_password(self, account):
try:
keyring.delete_password(self.identifier, account)
except:
pass # don't care - silently fail
# singleton # singleton
config = Config() config = Config()

View File

@ -352,15 +352,15 @@ class PreferencesDialog(tk.Toplevel):
if monitor.cmdr and config.get('cmdrs') and monitor.cmdr in config.get('cmdrs'): if monitor.cmdr and config.get('cmdrs') and monitor.cmdr in config.get('cmdrs'):
config_idx = config.get('cmdrs').index(monitor.cmdr) config_idx = config.get('cmdrs').index(monitor.cmdr)
self.username.insert(0, config.get('fdev_usernames')[config_idx] or '') self.username.insert(0, config.get('fdev_usernames')[config_idx] or '')
self.password.insert(0, config.get('fdev_passwords')[config_idx] or '') self.password.insert(0, config.get_password(config.get('fdev_usernames')[config_idx]) or '')
self.edsm_user.insert(0, config.get('edsm_usernames')[config_idx] or '') self.edsm_user.insert(0, config.get('edsm_usernames')[config_idx] or '')
self.edsm_apikey.insert(0, config.get('edsm_apikeys')[config_idx] or '') self.edsm_apikey.insert(0, config.get('edsm_apikeys')[config_idx] or '')
elif monitor.cmdr and not config.get('cmdrs') and config.get('username') and config.get('password'): elif monitor.cmdr and not config.get('cmdrs') and config.get('username') and config.get('password'):
# migration from <= 2.25 # migration from <= 2.25
self.username.insert(0, config.get('username')[config_idx] or '') self.username.insert(0, config.get('username') or '')
self.password.insert(0, config.get('password')[config_idx] or '') self.password.insert(0, config.get('password') or '')
self.edsm_user.insert(0,config.get('edsm_cmdrname')[config_idx] or '') self.edsm_user.insert(0,config.get('edsm_cmdrname') or '')
self.edsm_apikey.insert(0, config.get('edsm_apikey')[config_idx] or '') self.edsm_apikey.insert(0, config.get('edsm_apikey') or '')
self.cmdr = monitor.cmdr self.cmdr = monitor.cmdr
cmdr_state = not monitor.is_beta and monitor.cmdr and tk.NORMAL or tk.DISABLED cmdr_state = not monitor.is_beta and monitor.cmdr and tk.NORMAL or tk.DISABLED
@ -519,17 +519,19 @@ class PreferencesDialog(tk.Toplevel):
def apply(self): def apply(self):
if self.cmdr and not monitor.is_beta: if self.cmdr and not monitor.is_beta:
if self.password.get().strip():
config.set_password(self.username.get().strip(), self.password.get().strip()) # Can fail if keyring not unlocked
else:
config.delete_password(self.username.get().strip()) # user may have cleared the password field
if not config.get('cmdrs'): if not config.get('cmdrs'):
config.set('cmdrs', [self.cmdr]) config.set('cmdrs', [self.cmdr])
config.set('fdev_usernames', [self.username.get().strip()]) config.set('fdev_usernames', [self.username.get().strip()])
config.set('fdev_passwords', [self.password.get().strip()])
config.set('edsm_usernames', [self.edsm_user.get().strip()]) config.set('edsm_usernames', [self.edsm_user.get().strip()])
config.set('edsm_apikeys', [self.edsm_apikey.get().strip()]) config.set('edsm_apikeys', [self.edsm_apikey.get().strip()])
else: else:
idx = config.get('cmdrs').index(self.cmdr) if self.cmdr in config.get('cmdrs') else -1 idx = config.get('cmdrs').index(self.cmdr) if self.cmdr in config.get('cmdrs') else -1
_putfirst('cmdrs', idx, self.cmdr) _putfirst('cmdrs', idx, self.cmdr)
_putfirst('fdev_usernames', idx, self.username.get().strip()) _putfirst('fdev_usernames', idx, self.username.get().strip())
_putfirst('fdev_passwords', idx, self.password.get().strip())
_putfirst('edsm_usernames', idx, self.edsm_user.get().strip()) _putfirst('edsm_usernames', idx, self.edsm_user.get().strip())
_putfirst('edsm_apikeys', idx, self.edsm_apikey.get().strip()) _putfirst('edsm_apikeys', idx, self.edsm_apikey.get().strip())
@ -677,9 +679,9 @@ class AuthenticationDialog(tk.Toplevel):
# migration from <= 2.25. Assumes current Cmdr corresponds to the saved credentials # migration from <= 2.25. Assumes current Cmdr corresponds to the saved credentials
def migrate(current_cmdr): def migrate(current_cmdr):
if current_cmdr and not config.get('cmdrs') and config.get('username') and config.get('password'): if current_cmdr and not config.get('cmdrs') and config.get('username') and config.get('password'):
config.set_password(config.get('username'), config.get('password')) # Can fail on Linux
config.set('cmdrs', [current_cmdr]) config.set('cmdrs', [current_cmdr])
config.set('fdev_usernames', [config.get('username')]) config.set('fdev_usernames', [config.get('username')])
config.set('fdev_passwords', [config.get('password')])
config.set('edsm_usernames', [config.get('edsm_cmdrname') or '']) config.set('edsm_usernames', [config.get('edsm_cmdrname') or ''])
config.set('edsm_apikeys', [config.get('edsm_apikey') or '']) config.set('edsm_apikeys', [config.get('edsm_apikey') or ''])
# XXX to be done for release # XXX to be done for release
@ -694,7 +696,6 @@ def make_current(current_cmdr):
idx = config.get('cmdrs').index(current_cmdr) idx = config.get('cmdrs').index(current_cmdr)
_putfirst('cmdrs', idx) _putfirst('cmdrs', idx)
_putfirst('fdev_usernames', idx) _putfirst('fdev_usernames', idx)
_putfirst('fdev_passwords', idx)
_putfirst('edsm_usernames', idx) _putfirst('edsm_usernames', idx)
_putfirst('edsm_apikeys', idx) _putfirst('edsm_apikeys', idx)

View File

@ -70,7 +70,7 @@ if sys.platform=='darwin':
OPTIONS = { 'py2app': OPTIONS = { 'py2app':
{'dist_dir': dist_dir, {'dist_dir': dist_dir,
'optimize': 2, 'optimize': 2,
'packages': [ 'requests' ], 'packages': [ 'requests', 'keyring.backends' ],
'frameworks': [ 'Sparkle.framework' ], 'frameworks': [ 'Sparkle.framework' ],
'excludes': [ 'PIL', 'simplejson' ], 'excludes': [ 'PIL', 'simplejson' ],
'iconfile': '%s.icns' % APPNAME, 'iconfile': '%s.icns' % APPNAME,
@ -100,7 +100,7 @@ elif sys.platform=='win32':
OPTIONS = { 'py2exe': OPTIONS = { 'py2exe':
{'dist_dir': dist_dir, {'dist_dir': dist_dir,
'optimize': 2, 'optimize': 2,
'packages': [ 'requests' ], 'packages': [ 'requests', 'keyring.backends' ],
'excludes': [ 'PIL', 'simplejson' ], 'excludes': [ 'PIL', 'simplejson' ],
} }
} }