[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[minion-cvs] Many hacks, checkpointing.
Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.mit.edu:/tmp/cvs-serv24513/lib/mixminion
Modified Files:
BuildMessage.py ClientMain.py Main.py test.py
Log Message:
Many hacks, checkpointing.
README, Main, ServerMain:
- Rename "mixminion server" to "mixminion server-start" for consistency.
The old command is there, but deprecated.
BuildMessage, ClientMain:
- Die with a useful error message if the path is too long to fit the
address in the last hop. (This can happen even if path_len < 32.)
- Remove dead code for "stateful" reply blocks.
- Use multiple SURB keys to detect which identity was used; resists
George's SURB-swapping attack.
ClientMain:
- s/www.mixminion.net/mixminion.net/
- Beautify list-servers output a little
- Change key storage format to allow multiple keys to be stored with same
password
- Avoid dumping binary messages to ttys unless --force is given.
Main:
- Don't print a stack trace when the user hits ctrl-c.
HashLog:
- Correct comment
ServerMain, EventStats, Modules, ServerConfig, Main, mixminiond.conf:
- Beginnings of code to track server statistics.
Index: BuildMessage.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/BuildMessage.py,v
retrieving revision 1.40
retrieving revision 1.41
diff -u -d -r1.40 -r1.41
--- BuildMessage.py 5 Mar 2003 21:21:20 -0000 1.40
+++ BuildMessage.py 26 Mar 2003 16:36:46 -0000 1.41
@@ -10,13 +10,13 @@
import operator
import mixminion.Crypto as Crypto
from mixminion.Packet import *
-from mixminion.Common import MixError, MixFatalError, LOG
+from mixminion.Common import MixError, MixFatalError, LOG, UIError
if sys.version_info[:3] < (2,2,0):
import mixminion._zlibutil as zlibutil
__all__ = ['buildForwardMessage', 'buildEncryptedMessage', 'buildReplyMessage',
- 'buildReplyBlock', 'decodePayload' ]
+ 'buildReplyBlock', 'checkPathLength', 'decodePayload' ]
def buildForwardMessage(payload, exitType, exitInfo, path1, path2,
paddingPRNG=None):
@@ -241,12 +241,35 @@
return _buildReplyBlockImpl(path, exitType, exitInfo, expiryTime, prng,
seed)[0]
+def checkPathLength(path1, path2, exitType, exitInfo, explicitSwap=0):
+ "XXXX DOCDOC"
+ err = 0
+ if path1 is not None:
+ try:
+ _getRouting(path1, SWAP_FWD_TYPE, path2[0].getRoutingInfo().pack())
+ except MixError:
+ err = 1
+ if exitType != DROP_TYPE and exitInfo is not None:
+ exitInfo += "X"*20
+ else:
+ exitInfo = ""
+ if err == 0:
+ try:
+ _getRouting(path2, exitType, exitInfo)
+ except MixError:
+ err = 2
+ if err and not explicitSwap:
+ raise UIError("Address and path will not fit in one header")
+ elif err:
+ raise UIError("Address and %s leg of path will not fit in one header",
+ ["first", "second"][err-1])
+
#----------------------------------------------------------------------
# MESSAGE DECODING
def decodePayload(payload, tag, key=None,
- #storedKeys=None, # 'Stateful' reply blocks are disabled.
- userKey=None):
+ userKey=None,
+ userKeys={}):
"""Given a 28K payload and a 20-byte decoding tag, attempt to decode and
decompress the original message.
@@ -256,8 +279,12 @@
If we can successfully decrypt the payload, we return it. If we
might be able to decrypt the payload given more/different keys,
we return None. If the payload is corrupt, we raise MixError.
+
+ DOCDOC userKeys
"""
# FFFF Take a list of keys?
+ if userKey and not userKeys:
+ userKeys = { "" : userKey }
if len(payload) != PAYLOAD_LEN or len(tag) != TAG_LEN:
raise MixError("Wrong payload or tag length")
@@ -267,26 +294,19 @@
if _checkPayload(payload):
return _decodeForwardPayload(payload)
- # ('Stateful' reply blocks are disabled.)
-
-## # If we have a list of keys associated with the tag, it's a reply message
-## # using those keys.
-
-## if storedKeys is not None:
-## secrets = storedKeys.get(tag)
-## if secrets is not None:
-## del storedKeys[tag]
-## return _decodeReplyPayload(payload, secrets)
-
# If H(tag|userKey|"Validate") ends with 0, then the message _might_
# be a reply message using H(tag|userKey|"Generate") as the seed for
# its master secrets. (There's a 1-in-256 chance that it isn't.)
- if userKey is not None:
- if Crypto.sha1(tag+userKey+"Validate")[-1] == '\x00':
- try:
- return _decodeStatelessReplyPayload(payload, tag, userKey)
- except MixError:
- pass
+ if userKeys:
+ for name,userKey in userKeys.items():
+ if Crypto.sha1(tag+userKey+"Validate")[-1] == '\x00':
+ try:
+ p = _decodeStatelessReplyPayload(payload, tag, userKey)
+ if name:
+ LOG.info("Decoded reply message to identity: %r", name)
+ return p
+ except MixError:
+ pass
# If we have an RSA key, and none of the above steps get us a good
# payload, then we may as well try to decrypt the start of tag+key with
@@ -447,18 +467,7 @@
if len(path) * ENC_SUBHEADER_LEN > HEADER_LEN:
raise MixError("Too many nodes in path")
- # Construct a list 'routing' of exitType, exitInfo.
- routing = [ (FWD_TYPE, node.getRoutingInfo().pack()) for
- node in path[1:] ]
- routing.append((exitType, exitInfo))
-
- # sizes[i] is size, in blocks, of subheaders for i.
- sizes =[ getTotalBlocksForRoutingInfoLen(len(ri)) for _, ri in routing]
-
- # totalSize is the total number of blocks.
- totalSize = reduce(operator.add, sizes)
- if totalSize * ENC_SUBHEADER_LEN > HEADER_LEN:
- raise MixError("Routing info won't fit in header")
+ routing, sizes, totalSize = _getRouting(path, exitType, exitInfo)
# headerKey[i]==the AES key object node i will use to decrypt the header
headerKeys = [ Crypto.Keyset(secret).get(Crypto.HEADER_SECRET_MODE)
@@ -620,3 +629,19 @@
'Return true iff the hash on the given payload seems valid'
return payload[2:22] == Crypto.sha1(payload[22:])
+def _getRouting(path, exitType, exitInfo):
+ "XXXX DOCDOC"
+ # Construct a list 'routing' of exitType, exitInfo.
+ routing = [ (FWD_TYPE, node.getRoutingInfo().pack()) for
+ node in path[1:] ]
+ routing.append((exitType, exitInfo))
+
+ # sizes[i] is size, in blocks, of subheaders for i.
+ sizes =[ getTotalBlocksForRoutingInfoLen(len(ri)) for _, ri in routing]
+
+ # totalSize is the total number of blocks.
+ totalSize = reduce(operator.add, sizes)
+ if totalSize * ENC_SUBHEADER_LEN > HEADER_LEN:
+ raise MixError("Routing info won't fit in header")
+
+ return routing, sizes, totalSize
Index: ClientMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientMain.py,v
retrieving revision 1.66
retrieving revision 1.67
diff -u -d -r1.66 -r1.67
--- ClientMain.py 20 Feb 2003 16:57:38 -0000 1.66
+++ ClientMain.py 26 Mar 2003 16:36:46 -0000 1.67
@@ -26,7 +26,8 @@
import mixminion.MMTPClient
from mixminion.Common import IntervalSet, LOG, floorDiv, MixError, \
MixFatalError, MixProtocolError, UIError, UsageError, ceilDiv, \
- createPrivateDir, isSMTPMailbox, formatDate, formatFnameTime, formatTime,\
+ createPrivateDir, isPrintingAscii, \
+ isSMTPMailbox, formatDate, formatFnameTime, formatTime,\
Lockfile, openUnique, previousMidnight, readPossiblyGzippedFile, \
secureDelete, stringContains, succeedingMidnight
from mixminion.Crypto import sha1, ctr_crypt, trng
@@ -37,7 +38,7 @@
MBOX_TYPE, SMTP_TYPE, DROP_TYPE
# FFFF This should be made configurable and adjustable.
-MIXMINION_DIRECTORY_URL = "http://www.mixminion.net/directory/directory.gz"
+MIXMINION_DIRECTORY_URL = "http://mixminion.net/directory/directory.gz"
MIXMINION_DIRECTORY_FINGERPRINT = "CD80DD1B8BE7CA2E13C928D57499992D56579CCD"
#----------------------------------------------------------------------
@@ -393,15 +394,15 @@
return [ "No servers known" ]
longestnamelen = max(map(len, nicknames))
fmtlen = min(longestnamelen, 20)
- format = "%"+str(fmtlen)+"s:"
+ nnFormat = "%"+str(fmtlen)+"s:"
for n in nicknames:
nnreal = self.byNickname[n][0][0].getNickname()
- lines.append(format%nnreal)
+ lines.append(nnFormat%nnreal)
for info, where in self.byNickname[n]:
caps = info.getCaps()
va = formatDate(info['Server']['Valid-After'])
vu = formatDate(info['Server']['Valid-Until'])
- line = " %15s (valid %s to %s)"%(" ".join(caps),va,vu)
+ line = " [%s to %s] %s"%(va,vu," ".join(caps))
lines.append(line)
return lines
@@ -704,12 +705,14 @@
the path except that it support relay.
"""
assert (not halfPath) or (nSwap==-1)
+ explicitSwap = nSwap is not None
# First, find out what the exit node needs to be (or support).
if address is None:
routingType = None
+ routingInfo = None
exitNode = None
else:
- routingType, _, exitNode = address.getRouting()
+ routingType, routingInfo, exitNode = address.getRouting()
if exitNode:
exitNode = directory.getServerInfo(exitNode, startAt, endAt)
@@ -750,8 +753,15 @@
nSwap = ceilDiv(len(path),2)-1
path1, path2 = path[:nSwap+1], path[nSwap+1:]
+ if not halfPath and len(path1)+len(path2) < 2:
+ raise UIError("Path is too short")
if not halfPath and (not path1 or not path2):
raise UIError("Each leg of the path must have at least 1 hop")
+
+ mixminion.BuildMessage.checkPathLength(path1, path2,
+ routingType, routingInfo,
+ explicitSwap)
+
return path1, path2
def parsePath(directory, config, path, address, nHops=None,
@@ -899,49 +909,54 @@
return path2
class ClientKeyring:
+ # XXXX004 testme
"""Class to manage storing encrypted keys for a client. Right now, this
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.
- # surbKey: a 20-byte key for SURBs.
+ # keyring: DICT XXXX
+ # keyringPassword: The password for our encrypted keyfile
## Format:
# We store keys in a file holding:
- # variable [Key specific magic] "SURBKEY0"
- # 8 [8 bytes of salt]
- # keylen+20 bytes ENCRYPTED DATA:KEY=sha1(salt+password+salt)[:16]
- # DATA=encrypted_key+sha1(data+salt+magic)
+ # 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)
+
def __init__(self, keyDir):
"""Create a new ClientKeyring to store data in keyDir"""
self.keyDir = keyDir
createPrivateDir(self.keyDir)
- self.surbKey = None
+ self.keyring = None
+ self.keyringPassword = None
- # XXXX support multiple pseudoidentities.
- def getSURBKey(self, create=0):
- """Return the 20-byte SURB key. If it has not already been loaded,
- load it, asking the user for a password if needed. If 'create' is
- true and the key doesn't exist, ask the user for a new password
- and create a new SURB key. If 'create' is false and the key
- doesn't exist, return None."""
- if self.surbKey is not None:
- return self.surbKey
- fn = os.path.join(self.keyDir, "SURBKey")
- self.surbKey = self._getKey(fn, magic="SURBKEY0", which="reply block",
- create=create)
- return self.surbKey
+ def getKey(self, keyid, create=0, createFn=None):
+ if self.keyring is None:
+ self.getKeyring(create=create)
+ if self.keyring is None:
+ return None
+ try:
+ return self.keyring[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()
+ return key
- def _getKey(self, fn, magic, which, bytes=20, create=0):
- """Helper: Load an arbitrary key from the keystore, from file 'fn'.
- We expect the magic to be 'magic'; Error messages will describe the
- key as the "'which' key" . If create is true, and the key doesn't
- exist, generate a new 'bytes'-byte key. Else if the key doesn't
- exist, return None.
- """
-
+ def getKeyring(self, create=0):
+ 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 key exists, make sure the magic is correct.
+ # 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:
@@ -950,20 +965,47 @@
pass
# ...then ask the user for a password 'till it loads.
while 1:
- p = self._getPassword("Enter password for %s key:"%which)
+ p = self._getPassword("Enter password for keyring:")
try:
- return self._load(fn, magic, p)
- except MixError, e:
- LOG.error("Cannot load %s key: %s", which, e)
+ 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)
elif create:
# If the key file doesn't exist, and 'create' is set, create it.
- LOG.warn("No %s key found; generating.", which)
- key = trng(bytes)
- p = self._getNewPassword(which)
- self._save(fn, key, magic, p)
- return key
+ LOG.warn("No keyring found; generating.")
+ self.keyringPassword = self._getNewPassword("keyring")
+ self.keyring = {}
+ self._saveKeyring()
+ return self.keyring
else:
- return None
+ return {}
+
+ def _saveKeyring(self):
+ assert self.keyringPassword
+ fn = os.path.join(self.keyDir, "keyring")
+ LOG.trace("Saving keyring to %s", fn)
+ self._save(fn,
+ cPickle.dumps(self.keyring,1),
+ "KEYRING1", self.keyringPassword)
+
+ def getSURBKey(self, name="", create=0):
+ k = self.getKey("SURB-"+name,
+ create=create, createFn=lambda: trng(20))
+ if len(k) != 20:
+ raise MixError("Bad length on SURB key")
+ return k
+
+ def getSURBKeys(self):
+ self.getKeyring(create=0)
+ if not self.keyring: return {}
+ r = {}
+ for k in self.keyring.keys():
+ if k.startswith("SURB-"):
+ r[k[5:]] = self.keyring[k]
+ return r
def _checkMagic(self, fn, magic):
"""Make sure that the magic string on a given key file %s starts with
@@ -1021,10 +1063,12 @@
nl = 1
f.write(message)
f.flush()
- p = getpass.getpass("")
- if nl:
- f.write("\n")
- 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):
@@ -1353,7 +1397,7 @@
self.sendMessages([message], routing, noPool=forceNoPool)
- def generateReplyBlock(self, address, servers, expiryTime=0):
+ def generateReplyBlock(self, address, servers, name="", expiryTime=0):
"""Generate an return a new ReplyBlock object.
address -- the results of a parseAddress call
servers -- lists of ServerInfos for the reply leg of the path.
@@ -1361,7 +1405,7 @@
still be valid, and after which it should not be used.
"""
#XXXX004 write unit tests
- key = self.keys.getSURBKey(create=1)
+ key = self.keys.getSURBKey(name=name, create=1)
exitType, exitInfo, _ = address.getRouting()
block = mixminion.BuildMessage.buildReplyBlock(
@@ -1538,7 +1582,7 @@
LOG.info("Message pooled")
return handles
- def decodeMessage(self, s, force=0):
+ def decodeMessage(self, s, force=0, isatty=0):
"""Given a string 's' containing one or more text-encoed messages,
return a list containing the decoded messages.
@@ -1557,14 +1601,18 @@
if not msg.isEncrypted():
results.append(msg.getContents())
else:
- surbKey = self.keys.getSURBKey(create=0)
+ surbKeys = self.keys.getSURBKeys()
p = mixminion.BuildMessage.decodePayload(msg.getContents(),
tag=msg.getTag(),
- userKey=surbKey)
+ userKeys=surbKeys)
if p:
results.append(p)
else:
raise UIError("Unable to decode message")
+ if isatty and not force:
+ for p in results:
+ if not isPrintingAscii(p,allowISO=1):
+ raise UIError("Not writing binary message to terminal: Use -F to do it anyway.")
return results
def parseAddress(s):
@@ -2298,6 +2346,8 @@
# ???? Should we sometimes open this in text mode?
out = open(outputFile, 'wb')
+ tty = os.isatty(out.fileno())
+
if inputFile == '-':
s = sys.stdin.read()
else:
@@ -2308,7 +2358,7 @@
except OSError, e:
LOG.error("Could not read file %s: %s", inputFile, e)
try:
- res = client.decodeMessage(s, force=force)
+ res = client.decodeMessage(s, force=force, isatty=tty)
except ParseError, e:
raise UIError("Couldn't parse message: %s"%e)
@@ -2364,11 +2414,12 @@
options, args = getopt.getopt(args, "hvf:D:t:H:P:o:bn:",
['help', 'verbose', 'config=', 'download-directory=',
'to=', 'hops=', 'path=', 'lifetime=',
- 'output=', 'binary', 'count='])
+ 'output=', 'binary', 'count=', 'identity='])
outputFile = '-'
binary = 0
count = 1
+ identity = ""
for o,v in options:
if o in ('-o', '--output'):
outputFile = v
@@ -2380,7 +2431,8 @@
except ValueError:
print "ERROR: %s expects an integer" % o
sys.exit(1)
-
+ elif o in ('--identity',):
+ identity = v
try:
parser = CLIArgumentParser(options, wantConfig=1, wantClient=1,
wantLog=1, wantClientDirectory=1,
@@ -2412,7 +2464,8 @@
out = open(outputFile, 'w')
for i in xrange(count):
- surb = client.generateReplyBlock(address, path1, parser.endTime)
+ surb = client.generateReplyBlock(address, path1, name=identity,
+ expiryTime=parser.endTime)
if binary:
out.write(surb.pack())
else:
Index: Main.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Main.py,v
retrieving revision 1.36
retrieving revision 1.37
diff -u -d -r1.36 -r1.37
--- Main.py 20 Feb 2003 16:57:39 -0000 1.36
+++ Main.py 26 Mar 2003 16:36:46 -0000 1.37
@@ -128,16 +128,14 @@
"inspect-surbs" : ( 'mixminion.ClientMain', 'inspectSURBs' ),
"flush" : ( 'mixminion.ClientMain', 'flushPool' ),
"inspect-pool" : ( 'mixminion.ClientMain', 'listPool' ),
+ # XXXX Obsolete; use "server-start"; remove in 0.0.5
"server" : ( 'mixminion.server.ServerMain', 'runServer' ),
- "start-server" : ( 'mixminion.server.ServerMain', 'runServer' ),
- # obsolete; use server-stop #XXXX004 remove.
- "stop-server" : ( 'mixminion.server.ServerMain', 'signalServer' ),
- # obsolete; use server-reload #XXXX004 remove.
- "reload-server" : ( 'mixminion.server.ServerMain', 'signalServer' ),
+ "server-start" : ( 'mixminion.server.ServerMain', 'runServer' ),
"server-stop" : ( 'mixminion.server.ServerMain', 'signalServer' ),
"server-reload" : ( 'mixminion.server.ServerMain', 'signalServer' ),
"server-keygen" : ( 'mixminion.server.ServerMain', 'runKeygen'),
"server-DELKEYS" : ( 'mixminion.server.ServerMain', 'removeKeys'),
+ "server-stats" : ( 'mixminion.server.ServerMain', 'printServerStats' ),
"dir": ( 'mixminion.directory.DirMain', 'main'),
}
@@ -157,12 +155,13 @@
" generate-surb [Generate a single-use reply block]\n"+
" inspect-surbs [Describe a single-use reply block]\n"+
" (For Servers)\n"+
- " server [Begin running a Mixminion server]\n"+
+ " server-start [Begin running a Mixminion server]\n"+
" server-stop [Halt a running Mixminion server]\n"+
" server-reload [Make running Mixminion server reload its config\n"+
" (Not implemented yet; only restarts logging.)]\n"+
" server-keygen [Generate keys for a Mixminion server]\n"+
" server-DELKEYS [Remove generated keys for a Mixminion server]\n"+
+ " server-stats [XXXX]\n"+
" (For Developers)\n"+
" dir [Administration for server directories]\n"+
" unittests [Run the mixminion unit tests]\n"+
@@ -220,6 +219,8 @@
func(commandStr, ["--help"])
except uiErrorClass, e:
e.dumpAndExit()
+ except KeyboardInterrupt:
+ print "Interrupted."
if __name__ == '__main__':
main(sys.argv)
Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.94
retrieving revision 1.95
diff -u -d -r1.94 -r1.95
--- test.py 20 Feb 2003 16:57:40 -0000 1.94
+++ test.py 26 Mar 2003 16:36:46 -0000 1.95
@@ -5261,6 +5261,10 @@
parseFails("0x9999") # No data
parseFails("0xFEEEF:zymurgy") # Hex literal out of range
+ def testClientKeyring(self):
+ keydir = mix_mktemp()
+ keyring = mixminion.ClientMain.ClientKeyring(keyring)
+
def testMixminionClient(self):
# Create and configure a MixminionClient object...
parseAddress = mixminion.ClientMain.parseAddress