annotate easyhg2.py @ 439:51c5332aa957

Salt passwords
author Chris Cannam
date Tue, 28 Jun 2011 13:58:07 +0100
parents a5696a1f2dc5
children 0d779f3cb4bc
rev   line source
Chris@427 1 # -*- coding: utf-8 -*-
Chris@427 2 #
Chris@427 3 # EasyMercurial
Chris@427 4 #
Chris@427 5 # Based on hgExplorer by Jari Korhonen
Chris@427 6 # Copyright (c) 2010 Jari Korhonen
Chris@427 7 # Copyright (c) 2010 Chris Cannam
Chris@427 8 # Copyright (c) 2010 Queen Mary, University of London
Chris@427 9 #
Chris@427 10 # This program is free software; you can redistribute it and/or
Chris@427 11 # modify it under the terms of the GNU General Public License as
Chris@427 12 # published by the Free Software Foundation; either version 2 of the
Chris@427 13 # License, or (at your option) any later version. See the file
Chris@427 14 # COPYING included with this distribution for more information.
Chris@427 15
Chris@438 16 import sys, os, stat, urllib, urllib2, urlparse
Chris@427 17
Chris@438 18 from mercurial.i18n import _
Chris@433 19 from mercurial import ui, util, error
Chris@427 20 try:
Chris@427 21 from mercurial.url import passwordmgr
Chris@427 22 except:
Chris@427 23 from mercurial.httprepo import passwordmgr
Chris@427 24
Chris@427 25 # The value assigned here may be modified during installation, by
Chris@427 26 # replacing its default value with another one. We can't compare
Chris@427 27 # against its default value, because then the comparison text would
Chris@427 28 # get modified as well. So, compare using prefix only.
Chris@427 29 #
Chris@427 30 easyhg_import_path = 'NO_EASYHG_IMPORT_PATH'
Chris@427 31 if not easyhg_import_path.startswith('NO_'):
Chris@427 32 # We have an installation path: append it twice, once with
Chris@427 33 # the Python version suffixed
Chris@427 34 version_suffix = "Py" + str(sys.version_info[0]) + "." + str(sys.version_info[1]);
Chris@427 35 sys.path.append(easyhg_import_path + "/" + version_suffix)
Chris@427 36 sys.path.append(easyhg_import_path)
Chris@427 37
Chris@427 38 # Try to load the PyQt4 module that we need. If this fails, we should
Chris@427 39 # bail out later (in uisetup), because if we bail out now, Mercurial
Chris@427 40 # will just continue without us and report success. The invoking
Chris@427 41 # application needs to be able to discover whether the module load
Chris@427 42 # succeeded or not, so we need to ensure that Mercurial itself returns
Chris@427 43 # failure if it didn't.
Chris@427 44 #
Chris@427 45 easyhg_pyqt_ok = True
Chris@427 46 try:
Chris@427 47 from PyQt4 import Qt, QtGui
Chris@427 48 except ImportError:
Chris@427 49 easyhg_pyqt_ok = False
Chris@427 50 easyhg_qtapp = None
Chris@427 51
Chris@438 52 # These imports are optional, we just can't use the authfile (=
Chris@438 53 # "remember this password") feature without them
Chris@438 54 #
Chris@437 55 easyhg_authfile_imports_ok = True
Chris@437 56 try:
Chris@437 57 from Crypto.Cipher import AES
Chris@437 58 import ConfigParser # Mercurial version won't write files
Chris@437 59 import base64
Chris@437 60 except ImportError:
Chris@437 61 easyhg_authfile_imports_ok = False
Chris@433 62
Chris@431 63 #!!! should be in a class here
Chris@431 64
Chris@439 65 def encrypt_salted(text, key):
Chris@439 66 salt = os.urandom(8)
Chris@439 67 text = '%d.%s.%s' % (len(text), base64.b64encode(salt), text)
Chris@431 68 text += (16 - len(text) % 16) * ' '
Chris@431 69 cipher = AES.new(key)
Chris@431 70 return base64.b64encode(cipher.encrypt(text))
Chris@431 71
Chris@439 72 def decrypt_salted(ctext, key):
Chris@431 73 cipher = AES.new(key)
Chris@431 74 text = cipher.decrypt(base64.b64decode(ctext))
Chris@431 75 (tlen, d, text) = text.partition('.')
Chris@439 76 (salt, d, text) = text.partition('.')
Chris@431 77 return text[0:int(tlen)]
Chris@431 78
Chris@427 79 def monkeypatch_method(cls):
Chris@427 80 def decorator(func):
Chris@427 81 setattr(cls, func.__name__, func)
Chris@427 82 return func
Chris@427 83 return decorator
Chris@427 84
Chris@427 85 def uisetup(ui):
Chris@427 86 if not easyhg_pyqt_ok:
Chris@427 87 raise util.Abort(_('Failed to load PyQt4 module required by easyhg.py'))
Chris@427 88 global easyhg_qtapp
Chris@427 89 easyhg_qtapp = QtGui.QApplication([])
Chris@427 90
Chris@427 91 orig_find = passwordmgr.find_user_password
Chris@427 92
Chris@427 93 # from mercurial_keyring by Marcin Kasperski
Chris@427 94 def canonical_url(authuri):
Chris@427 95 """
Chris@427 96 Strips query params from url. Used to convert urls like
Chris@427 97 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
Chris@427 98 to
Chris@427 99 https://repo.machine.com/repos/apps/module
Chris@427 100 """
Chris@427 101 parsed_url = urlparse.urlparse(authuri)
Chris@427 102 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
Chris@427 103 parsed_url.path)
Chris@427 104
Chris@436 105 def load_config(pcfg, pfile):
Chris@434 106 fp = None
Chris@434 107 try:
Chris@434 108 fp = open(pfile)
Chris@434 109 except:
Chris@436 110 pass
Chris@434 111 if fp:
Chris@434 112 pcfg.readfp(fp)
Chris@436 113 fp.close()
Chris@434 114
Chris@434 115 def save_config(pcfg, pfile):
Chris@434 116 ofp = None
Chris@434 117 try:
Chris@434 118 ofp = open(pfile, 'w')
Chris@434 119 except:
Chris@434 120 self.ui.write("failed to open authfile %s for writing\n" % pfile)
Chris@434 121 raise
Chris@434 122 try:
Chris@438 123 #!!! Windows equivalent?
Chris@438 124 os.fchmod(ofp.fileno(), stat.S_IRUSR | stat.S_IWUSR)
Chris@434 125 except:
Chris@434 126 ofp.close()
Chris@434 127 ofp = None
Chris@438 128 self.ui.write("failed to set permissions on authfile %s\n" % pfile)
Chris@434 129 raise
Chris@434 130 pcfg.write(ofp)
Chris@434 131 ofp.close()
Chris@434 132
Chris@436 133 def get_from_config(pcfg, sect, key):
Chris@436 134 data = None
Chris@436 135 try:
Chris@436 136 data = pcfg.get(sect, key)
Chris@436 137 except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
Chris@436 138 pass
Chris@436 139 return data
Chris@436 140
Chris@436 141 def get_boolean_from_config(pcfg, sect, key, deflt):
Chris@436 142 data = deflt
Chris@436 143 try:
Chris@436 144 data = pcfg.getboolean(sect, key)
Chris@436 145 except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
Chris@436 146 pass
Chris@436 147 return data
Chris@436 148
Chris@436 149 def set_to_config(pcfg, sect, key, data):
Chris@436 150 if not pcfg.has_section(sect):
Chris@436 151 pcfg.add_section(sect)
Chris@436 152 pcfg.set(sect, key, data)
Chris@436 153
Chris@438 154 def remote_key(uri, user):
Chris@438 155 return base64.b64encode('%s@@%s' % (uri, user)).replace('=', '_')
Chris@438 156
Chris@427 157 @monkeypatch_method(passwordmgr)
Chris@427 158 def find_user_password(self, realm, authuri):
Chris@427 159
Chris@427 160 if not self.ui.interactive():
Chris@427 161 return orig_find(self, realm, authuri)
Chris@427 162 if not easyhg_pyqt_ok:
Chris@427 163 return orig_find(self, realm, authuri)
Chris@427 164
Chris@427 165 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
Chris@427 166 self, realm, authuri)
Chris@427 167 user, passwd = authinfo
Chris@427 168
Chris@427 169 if user and passwd:
Chris@427 170 return orig_find(self, realm, authuri)
Chris@427 171
Chris@427 172 self.ui.write("want username and/or password for %s\n" % authuri)
Chris@427 173
Chris@427 174 uri = canonical_url(authuri)
Chris@427 175
Chris@431 176 pekey = self.ui.config('easyhg', 'authkey')
Chris@434 177 pfile = self.ui.config('easyhg', 'authfile')
Chris@437 178 use_authfile = (easyhg_authfile_imports_ok and pekey and pfile)
Chris@437 179 if pfile:
Chris@437 180 pfile = os.path.expanduser(pfile)
Chris@431 181 pdata = None
Chris@431 182
Chris@431 183 self.ui.write("pekey is %s\n" % pekey)
Chris@431 184 self.ui.write("pfile is %s\n" % pfile)
Chris@427 185
Chris@427 186 dialog = QtGui.QDialog()
Chris@427 187 layout = QtGui.QGridLayout()
Chris@427 188 dialog.setLayout(layout)
Chris@427 189
Chris@432 190 layout.addWidget(QtGui.QLabel(_('<h3>Login required</h3><p>Please provide your login details for the repository at<br><code>%s</code>:') % uri), 0, 0, 1, 2)
Chris@427 191
Chris@427 192 userfield = QtGui.QLineEdit()
Chris@427 193 if user:
Chris@427 194 userfield.setText(user)
Chris@427 195 layout.addWidget(QtGui.QLabel(_('User:')), 1, 0)
Chris@427 196 layout.addWidget(userfield, 1, 1)
Chris@427 197
Chris@427 198 passfield = QtGui.QLineEdit()
Chris@427 199 passfield.setEchoMode(QtGui.QLineEdit.Password)
Chris@427 200 if passwd:
Chris@428 201 passfield.setText(passwd)
Chris@427 202 layout.addWidget(QtGui.QLabel(_('Password:')), 2, 0)
Chris@427 203 layout.addWidget(passfield, 2, 1)
Chris@427 204
Chris@435 205 userfield.connect(userfield, Qt.SIGNAL("textChanged(QString)"), passfield, Qt.SLOT("clear()"))
Chris@435 206
Chris@431 207 remember = None
Chris@433 208 pcfg = None
Chris@433 209
Chris@436 210 if use_authfile:
Chris@431 211 # load pwd from our cache file, decrypt with given key
Chris@433 212 pcfg = ConfigParser.RawConfigParser()
Chris@434 213 load_config(pcfg, pfile)
Chris@436 214 remember_default = get_boolean_from_config(pcfg, 'preferences', 'remember', False)
Chris@438 215 pdata = get_from_config(pcfg, 'auth', remote_key(uri, user))
Chris@434 216 if pdata:
Chris@439 217 cachedpwd = decrypt_salted(pdata, pekey)
Chris@434 218 passfield.setText(cachedpwd)
Chris@431 219 remember = QtGui.QCheckBox()
Chris@433 220 remember.setChecked(remember_default)
Chris@431 221 remember.setText(_('Remember this password until EasyMercurial exits'))
Chris@431 222 layout.addWidget(remember, 3, 1)
Chris@431 223
Chris@427 224 bb = QtGui.QDialogButtonBox()
Chris@427 225 ok = bb.addButton(bb.Ok)
Chris@427 226 cancel = bb.addButton(bb.Cancel)
Chris@427 227 cancel.setDefault(False)
Chris@427 228 cancel.setAutoDefault(False)
Chris@427 229 ok.setDefault(True)
Chris@427 230 bb.connect(ok, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("accept()"))
Chris@427 231 bb.connect(cancel, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("reject()"))
Chris@431 232 layout.addWidget(bb, 4, 0, 1, 2)
Chris@427 233
Chris@428 234 dialog.setWindowTitle(_('EasyMercurial: Login'))
Chris@427 235 dialog.show()
Chris@428 236
Chris@428 237 if not user:
Chris@428 238 userfield.setFocus(True)
Chris@428 239 elif not passwd:
Chris@428 240 passfield.setFocus(True)
Chris@428 241
Chris@427 242 dialog.raise_()
Chris@427 243 ok = dialog.exec_()
Chris@436 244 if not ok:
Chris@436 245 raise util.Abort(_('password entry cancelled'))
Chris@431 246
Chris@436 247 self.ui.write('Dialog accepted\n')
Chris@436 248 user = userfield.text()
Chris@436 249 passwd = passfield.text()
Chris@432 250
Chris@436 251 if use_authfile:
Chris@436 252 set_to_config(pcfg, 'preferences', 'remember', remember.isChecked())
Chris@438 253 if user:
Chris@438 254 if passwd and remember.isChecked():
Chris@439 255 pdata = encrypt_salted(passwd, pekey)
Chris@438 256 set_to_config(pcfg, 'auth', remote_key(uri, user), pdata)
Chris@438 257 else:
Chris@438 258 set_to_config(pcfg, 'auth', remote_key(uri, user), '')
Chris@436 259 save_config(pcfg, pfile)
Chris@433 260
Chris@436 261 self.add_password(realm, authuri, user, passwd)
Chris@427 262 return (user, passwd)
Chris@427 263
Chris@427 264