[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[minion-cvs] Refactor keyring encryption and passwords into a separa...
Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.mit.edu:/tmp/cvs-serv8130/lib/mixminion
Modified Files:
ClientMain.py test.py
Added Files:
ClientUtils.py
Log Message:
Refactor keyring encryption and passwords into a separate class.
--- NEW FILE: ClientUtils.py ---
# Copyright 2002-2003 Nick Mathewson. See LICENSE for licensing information.
# Id: ClientMain.py,v 1.89 2003/06/05 18:41:40 nickm Exp $
"""mixminion.ClientUtils
This module holds helper code not included in the Mixminion Client
API, but useful for more than one user interface.
"""
__all__ = [ 'NoPassword', 'PasswordManager', 'getPassword_term',
'getNewPassword_term', ]
import cPickle
import getpass
import os
import sys
from mixminion.Common import readFile, writeFile, MixError
import mixminion.Crypto
#----------------------------------------------------------------------
class BadPassword(MixError):
pass
class PasswordManager:
# passwords: name -> string
def __init__(self):
self.passwords = {}
def _getPassword(self, name, prompt):
raise NotImplemented()
def _getNewPassword(self, name, prompt):
raise NotImplemented()
def setPassword(self, name, password):
self.passwords[name] = password
def getPassword(self, name, prompt, confirmFn, maxTries=-1):
if self.passwords.has_key(name):
return self.passwords[name]
for othername, pwd in self.passwords.items():
if self._confirm(name, pwd):
self.passwords[name] = pwd
return pwd
pmt = prompt
while maxTries:
pwd = self._getPassword(name, pmt)
if confirmFn(pwd):
self.passwords[name] = pwd
return pwd
maxTries -= 1
pmt = "Incorrect password. "+prompt
raise BadPassword()
def getNewPassword(self, name, prompt):
self.passwords[name] = self._getNewPassword(name, prompt)
class CLIPasswordManager(PasswordManager):
def __init__(self):
PasswordManager.__init__(self)
def _getPassword(self, name, prompt):
return getPassword_term(prompt)
def getPassword_term(prompt):
"""Read a password from the console, then return it. Use the string
'message' as a prompt."""
# getpass.getpass uses stdout by default .... but stdout may have
# been redirected. If stdout is not a terminal, write the message
# to stderr instead.
if os.isatty(sys.stdout.fileno()):
f = sys.stdout
nl = 0
else:
f = sys.stderr
nl = 1
f.write(prompt)
f.flush()
try:
p = getpass.getpass("")
except KeyboardInterrupt:
if nl: print >>f
raise UIError("Interrupted")
if nl: print >>f
return p
def getNewPassword_term(prompt):
"""Read a new password from the console, then return it."""
s1 = "Enter new password for %s:"%which
s2 = "Verify password:".rjust(len(s1))
if os.isatty(sys.stdout.fileno()):
f = sys.stdout
else:
f = sys.stderr
while 1:
p1 = self.getPassword_term(s1)
p2 = self.getPassword_term(s2)
if p1 == p2:
return p1
f.write("Passwords do not match.\n")
f.flush()
#----------------------------------------------------------------------
def readEncryptedFile(fname, password, magic):
"""DOCDOC
return None on failure; raise MixError on corrupt file.
"""
# variable [File specific magic] "KEYRING1"
# 8 [8 bytes of salt]
# variable ENCRYPTED DATA:KEY=sha1(salt+password+salt)
# DATA=data+
# sha1(data+salt+magic)
s = readFile(fname, 1)
if not s.startswith(magic):
raise ValueError("Invalid versioning on %s"%fname)
s = s[len(magic):]
if len(s) < 28:
raise MixError("File %s too short."%fname)
salt = s[:8]
s = s[8:]
key = mixminion.Crypto.sha1(salt+password+salt)[:16]
s = mixminion.Crypto.ctr_crypt(s, key)
data = s[:-20]
hash = s[-20:]
if hash != mixminion.Crypto.sha1(data+salt+magic):
raise BadPassword()
return data
def writeEncryptedFile(fname, password, magic, data):
salt = mixminion.Crypto.getCommonPRNG().getBytes(8)
key = mixminion.Crypto.sha1(salt+password+salt)[:16]
hash = mixminion.Crypto.sha1("".join([data+salt+magic]))
encrypted = mixminion.Crypto.ctr_crypt(data+hash, key)
writeFile(fname, "".join([magic,salt,encrypted]), binary=1)
def readEncryptedPickled(fname, password, magic):
return cPickle.loads(readEncryptedFile(fname, password, magic))
def writeEncryptedPickled(fname, password, magic, obj):
data = cPickle.dumps(obj, 1)
writeEncryptedFile(fname, password, magic, data)
class LazyEncryptedPickled:
def __init__(self, fname, pwdManager, pwdName, queryPrompt, newPrompt,
magic, initFn):
self.fname = fname
self.pwdManager = pwdManager
self.pwdName = pwdName
self.queryPrompt = queryPrompt
self.newPrompt = newPrompt
self.magic = magic
self.object = None
self.loaded = 0
self.password = None
self.initFn = initFn
def load(self, create=0,password=None):
if self.loaded:
return
elif os.path.exists(self.fname):
if not readFile(self.fname).startswith(self.magic):
raise MixError("Unrecognized versioning on file %s"%self.fname)
# ... see if we can load it with no password ...
if self._loadWithPassword(""):
return
if password is not None:
self._loadWithPassword(password)
if not self.loaded:
raise BadPassword()
else:
# sets self.password on successs
self.pwdManager.getPassword(self.pwdName, self.queryPrompt,
self._loadWithPassword)
elif create:
if password is not None:
self.password = password
else:
self.password = self.pwdManager.getNewPassword(
self.pwdName, self.newPrompt)
self.object = self.initFn()
self.loaded = 1
self.save()
else:
return
def _loadWithPassword(self, password):
try:
self.object = readEncryptedPickled(self.fname,password,self.magic)
self.password = password
self.loaded = 1
return 1
except MixError:
return 0
def isLoaded(self):
return self.loaded
def get(self):
assert self.loaded
return self.object
def set(self, val):
self.object = val
self.loaded = 1
def setPassword(self, pwd):
self.password = pwd
def save(self):
assert self.loaded and self.password
writeEncryptedPickled(self.fname, self.password, self.magic,
self.object)
Index: ClientMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientMain.py,v
retrieving revision 1.112
retrieving revision 1.113
diff -u -d -r1.112 -r1.113
--- ClientMain.py 31 Aug 2003 19:29:29 -0000 1.112
+++ ClientMain.py 6 Sep 2003 21:49:48 -0000 1.113
@@ -45,6 +45,7 @@
from mixminion.Packet import encodeMailHeaders, ParseError, parseMBOXInfo, \
parseReplyBlocks, parseSMTPInfo, parseTextEncodedMessages, \
parseTextReplyBlocks, ReplyBlock, MBOX_TYPE, SMTP_TYPE, DROP_TYPE
+import mixminion.ClientUtils
# FFFF This should be made configurable and adjustable.
MIXMINION_DIRECTORY_URL = "http://mixminion.net/directory/Directory.gz"
@@ -101,7 +102,7 @@
## Layout:
# DIR/cache: A cPickled tuple of ("ClientKeystore-0.2",
# lastModified, lastDownload, clientVersions, serverlist,
- # fullServerList, digestMap)
+ # fullServerList, digestMap) DOCDOC is this correct?
# DIR/dir.gz *or* DIR/dir: A (possibly gzipped) directory file.
# DIR/imported/: A directory of server descriptors.
MAGIC = "ClientKeystore-0.3"
@@ -949,30 +950,21 @@
is limited to a single SURB decryption key. In the future, we may
include more SURB keys, as well as end-to-end encryption keys.
"""
- ## Fields:
- # keyDir: The directory where we store our keys.
- # keyring: Dict to map from strings of the form "SURB-keyname" to SURB
- # secrets.
- # keyringPassword: The password for our encrypted keyfile
- ## Format:
- # We store keys in a file holding:
- # variable [File specific magic] "KEYRING1"
- # 8 [8 bytes of salt]
- # variable ENCRYPTED DATA:KEY=sha1(salt+password+salt)
- # DATA=encrypted_pickled_data+
- # sha1(data+salt+magic)
-
- # XXXX There needs to be some way to rotate and expire SURB secrets.
- # XXXX Otherwise, we're very vulnerable to compromise.
-
- def __init__(self, keyDir):
- """Create a new ClientKeyring to store data in keyDir"""
- self.keyDir = keyDir
- createPrivateDir(self.keyDir)
- self.keyring = None
- self.keyringPassword = None
+ # XXXX006 Are the error messages here still reasonable?
+ def __init__(self, keyDir, passwordManager=None):
+ """DOCDOC"""
+ if passwordManager is None:
+ passwordManager = mixminion.ClientUtils.CLIPasswordManager()
+ createPrivateDir(keyDir)
+ fn = os.path.join(keyDir, "keyring")
+ self.keyring = mixminion.ClientUtils.LazyEncryptedPickled(
+ fn, passwordManager, pwdName="ClientKeyring",
+ queryPrompt="Enter password for keyring:",
+ newPrompt="keyring",
+ magic="KEYRING1",
+ initFn=lambda:{})
- def getKey(self, keyid, create=0, createFn=None, password=None):
+ def _getKey(self, keyid, create=0, createFn=None, password=None):
"""Helper function. Return a key for a given keyid.
keyid -- the name of the key.
@@ -980,176 +972,50 @@
createFn -- a callback to return a new key.
password -- Optionally, a password for the keyring.
"""
- if self.keyring is None:
- self.getKeyring(create=create,password=password)
- if self.keyring is None:
+ if not self.keyring.isLoaded():
+ try:
+ self.keyring.load(create=create,password=password)
+ except mixminion.ClientUtils.BadPassword:
+ LOG.error("Incorrect password")
+ return None
+ if not self.keyring.isLoaded():
return None
try:
- return self.keyring[keyid]
+ return self.keyring.get()[keyid]
except KeyError:
if not create:
return None
else:
LOG.info("Creating new key for identity %r", keyid)
key = createFn()
- self.keyring[keyid] = key
- self._saveKeyring()
+ self.keyring.get()[keyid] = key
+ self.keyring.save()
return key
- def getKeyring(self, create=0, password=None):
- """Return a the current keyring, loading it if necessary.
-
- create -- if true, create a new keyring if none is found.
- password -- optionally, a password for the keyring.
- """
- if self.keyring is not None:
- return self.keyring
- fn = os.path.join(self.keyDir, "keyring")
- magic = "KEYRING1"
- if os.path.exists(fn):
- # If the keyring exists, make sure the magic is correct.
- self._checkMagic(fn, magic)
- # ...then see if we can load it without a password...
- try:
- data = self._load(fn, magic, "")
- self.keyring = cPickle.loads(data)
- self.keyringPassword = ""
- return self.keyring
- except MixError, e:
- pass
- # ...then ask the user for a password 'till it loads.
- while 1:
- if password is not None:
- p = password
- else:
- p = self._getPassword("Enter password for keyring:")
- try:
- data = self._load(fn, magic, p)
- self.keyring = cPickle.loads(data)
- self.keyringPassword = p
- return self.keyring
- except (MixError, cPickle.UnpicklingError), e:
- LOG.error("Cannot load keyring: %s", e)
- if password is not None: return None
- elif create:
- # If the key file doesn't exist, and 'create' is set, create it.
- LOG.warn("No keyring found; generating.")
- if password is not None:
- self.keyringPassword = password
- else:
- self.keyringPassword = self._getNewPassword("keyring")
- self.keyring = {}
- self._saveKeyring()
- return self.keyring
- else:
- return {}
-
- def _saveKeyring(self):
- """Save the current keyring to disk."""
- assert self.keyringPassword is not None
- fn = os.path.join(self.keyDir, "keyring")
- LOG.trace("Saving keyring to %s", fn)
- self._save(fn+"_tmp",
- cPickle.dumps(self.keyring,1),
- "KEYRING1", self.keyringPassword)
- replaceFile(fn+"_tmp", fn)
-
def getSURBKey(self, name="", create=0, password=None):
"""Return the key for a given SURB identity."""
- k = self.getKey("SURB-"+name,
+ k = self._getKey("SURB-"+name,
create=create, createFn=lambda: trng(20),
password=password)
if k is not None and len(k) != 20:
raise MixError("Bad length on SURB key")
return k
- def getSURBKeys(self,password=None):
- """Return the keys for _all_ SURB identities as a map from name
- to key."""
- self.getKeyring(create=0,password=password)
- if not self.keyring: return {}
+ def getSURBKeys(self, name="", password=None):
+ """Return the keys for _all_ SURB identities as a map from
+ name to key."""
+ try:
+ self.keyring.load(create=0,password=password)
+ except mixminion.ClientUtils.BadPassword:
+ LOG.error("Incorrect password")
+ if not self.keyring.isLoaded(): return {}
r = {}
- for k in self.keyring.keys():
+ d = self.keyring.get()
+ for k,v in d.items():
if k.startswith("SURB-"):
- r[k[5:]] = self.keyring[k]
+ r[k[5:]] = v
return r
- def _checkMagic(self, fn, magic):
- """Make sure that the magic string on a given key file %s starts with
- is equal to 'magic'. Raise MixError if it isn't."""
- if not readFile(fn, 1).startswith(magic):
- raise MixError("Invalid versioning on key file")
-
- def _save(self, fn, data, magic, password):
- """Save the key data 'data' into the file 'fn' using the magic string
- 'magic' and the password 'password'."""
- salt = mixminion.Crypto.getCommonPRNG().getBytes(8)
- key = sha1(salt+password+salt)[:16]
- f = open(fn, 'wb')
- f.write(magic)
- f.write(salt)
- f.write(ctr_crypt(data+sha1(data+salt+magic), key))
- f.close()
-
- def _load(self, fn, magic, password):
- """Load and return the key stored in 'fn' using the magic string
- 'magic' and the password 'password'. Raise MixError on failure."""
- s = readFile(fn, 1)
- if not s.startswith(magic):
- raise MixError("Invalid versioning on key file")
-
- s = s[len(magic):]
- if len(s) < 8:
- raise MixError("Key file too short")
- salt = s[:8]
- s = s[8:]
- if len(s) < 20:
- raise MixError("Key file too short")
- key = sha1(salt+password+salt)[:16]
- s = ctr_crypt(s, key)
- data, hash = s[:-20], s[-20:]
- if hash != sha1(data+salt+magic):
- raise MixError("Incorrect password")
- return data
-
- def _getPassword(self, message):
- """Read a password from the console, then return it. Use the string
- 'message' as a prompt."""
- # getpass.getpass uses stdout by default .... but stdout may have
- # been redirected. If stdout is not a terminal, write the message
- # to stderr instead.
- if os.isatty(sys.stdout.fileno()):
- f = sys.stdout
- nl = 0
- else:
- f = sys.stderr
- nl = 1
- f.write(message)
- f.flush()
- try:
- p = getpass.getpass("")
- except KeyboardInterrupt:
- if nl: print >>f
- raise UIError("Interrupted")
- if nl: print >>f
- return p
-
- def _getNewPassword(self, which):
- """Read a new password from the console, then return it."""
- s1 = "Enter new password for %s:"%which
- s2 = "Verify password:".rjust(len(s1))
- if os.isatty(sys.stdout.fileno()):
- f = sys.stdout
- else:
- f = sys.stderr
- while 1:
- p1 = self._getPassword(s1)
- p2 = self._getPassword(s2)
- if p1 == p2:
- return p1
- f.write("Passwords do not match.\n")
- f.flush()
-
def installDefaultConfig(fname):
"""Create a default, 'fail-safe' configuration in a given file"""
LOG.warn("No configuration file found. Installing default file in %s",
@@ -1424,7 +1290,8 @@
userdir = self.config['User']['UserDir']
createPrivateDir(userdir)
keyDir = os.path.join(userdir, "keys")
- self.keys = ClientKeyring(keyDir)
+ self.pwdManager = mixminion.ClientUtils.CLIPasswordManager()
+ self.keys = ClientKeyring(keyDir, self.pwdManager)
self.surbLogFilename = os.path.join(userdir, "surbs", "log")
# Initialize PRNG
Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.152
retrieving revision 1.153
diff -u -d -r1.152 -r1.153
--- test.py 5 Sep 2003 00:46:24 -0000 1.152
+++ test.py 6 Sep 2003 21:49:48 -0000 1.153
@@ -40,6 +40,7 @@
import mixminion.BuildMessage as BuildMessage
import mixminion.ClientMain
+import mixminion.ClientUtils
import mixminion.Config
import mixminion.Crypto as Crypto
import mixminion.Filestore
@@ -5872,6 +5873,27 @@
# variable to hold the latest instance of FakeBCC.
BCC_INSTANCE = None
+
+class ClientUtilTests(TestCase):
+ def testEncryptedFiles(self):
+ CU = mixminion.ClientUtils
+ d = mix_mktemp()
+ createPrivateDir(d)
+ f1 = os.path.join(d, "foo")
+ CU.writeEncryptedFile(f1, password="x", magic="ABC", data="xyzzyxyzzy")
+ contents = readFile(f1)
+ self.assertEquals(contents[:3], "ABC")
+ salt = contents[3:11]
+ key = mixminion.Crypto.sha1(salt+"x"+salt)[:16]
+ decrypted = mixminion.Crypto.ctr_crypt(contents[11:], key)
+ self.assertEquals(decrypted, "xyzzyxyzzy"+mixminion.Crypto.sha1(
+ "xyzzyxyzzy"+salt+"ABC"))
+
+ self.assertEquals("xyzzyxyzzy",
+ CU.readEncryptedFile(f1, "x", "ABC"))
+
+ #XXXX006 finish testing corner cases and pickles.
+
class ClientMainTests(TestCase):
def testClientDirectory(self):
"""Check out ClientMain's directory implementation"""
@@ -6406,7 +6428,6 @@
keyring = mixminion.ClientMain.ClientKeyring(keydir)
# Check for some nonexistent keys.
self.assertEquals({}, keyring.getSURBKeys(password="pwd"))
-
self.assertEquals(None, keyring.getSURBKey(create=0))
# Reload, try again:
kfirst = None
@@ -6414,17 +6435,10 @@
keyring = mixminion.ClientMain.ClientKeyring(keydir)
self.assertEquals(kfirst, keyring.getSURBKey(
create=0,password="pwd"))
- try:
- suspendLog()
- k1 = keyring.getSURBKey(create=1,password="pwd")
- finally:
- s = resumeLog()
+ k1 = keyring.getSURBKey(create=1,password="pwd")
if kfirst:
- self.assertEquals(s, "")
self.assertEquals(k1, kfirst)
- else:
- self.assert_(stringContains(s, "No keyring found"))
- kfirst = k1
+ kfirst = k1
self.assertEquals(20, len(k1))
k2 = keyring.getSURBKey(name="Bob",create=1)
self.assertEquals(20, len(k2))
@@ -6764,7 +6778,7 @@
tc = loader.loadTestsFromTestCase
if 0:
- suite.addTest(tc(ServerInfoTests))
+ suite.addTest(tc(ClientMainTests))
return suite
testClasses = [MiscTests,
MinionlibCryptoTests,