[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[minion-cvs]



Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.seul.org:/tmp/cvs-serv9453/lib/mixminion

Modified Files:
	BuildMessage.py ClientMain.py Crypto.py Packet.py test.py 
Log Message:



Index: BuildMessage.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/BuildMessage.py,v
retrieving revision 1.13
retrieving revision 1.14
diff -u -d -r1.13 -r1.14
--- BuildMessage.py	10 Sep 2002 14:45:29 -0000	1.13
+++ BuildMessage.py	13 Oct 2002 01:34:44 -0000	1.14
@@ -11,53 +11,121 @@
 import mixminion.Crypto as Crypto
 import mixminion.Modules as Modules
 
-__all__ = [ 'buildForwardMessage', 'buildReplyBlock', 'buildReplyMessage',
-            'buildStatelessReplyBlock' ]
+__all__ = [ 'Address', 
+           'buildForwardMessage', 'buildEncryptedMessage', 'buildReplyMessage',
+           'buildStatelessReplyBlock', 'buildReplyBlock', 'decodePayload',
+	   'decodeForwardPayload', 'decodeEncryptedPayload', 
+	   'decodeReplyPayload', 'decodeStatelessReplyPayload' ]
 
-def buildForwardMessage(payload, exitType, exitInfo, path1, path2):
+def buildForwardMessage(payload, exitType, exitInfo, path1, path2, 
+			paddingPRNG=None):
     """Construct a forward message.
-            payload: The payload to deliver.
+            payload: The payload to deliver.  Must compress to under 28K-22b.
             exitType: The routing type for the final node
-            exitInfo: The routing info for the final node
+            exitInfo: The routing info for the final node, not including tag.
             path1: Sequence of ServerInfo objects for the first leg of the path
             path2: Sequence of ServerInfo objects for the 2nd leg of the path
+	    paddingPRNG
 
         Note: If either path is empty, the message is vulnerable to tagging 
          attacks! (FFFF we should check this.)
     """
-    return _buildMessage(payload, exitType, exitInfo, path1, path2)
+    if paddingPRNG is None: paddingPRNG = Crypto.AESCounterPRNG()
 
-def buildReplyMessage(payload, path1, replyBlock):
+    payload = _encodePayload(payload, 0, paddingPRNG)
+    tag = _getRandomTag(paddingPRNG)
+    exitInfo = tag + exitInfo 
+    return _buildMessage(payload, exitType, exitInfo, path1, path2,paddingPRNG)
+
+def buildEncryptedForwardMessage(payload, exitType, exitInfo, path1, path2, 
+				 key, paddingPRNG=None, secretRNG=None):
+    """XXXX
+    """
+    if paddingPRNG is None: paddingPRNG = Crypto.AESCounterPRNG()
+    if secretRNG is None: secretRNG = paddingPRNG
+
+    payload = _encodePayload(payload, OAEP_OVERHEAD, paddingPRNG)
+
+    sessionKey = secretRNG.getBytes(SECRET_LEN)
+    payload = sessionKey+payLoad
+    rsaDataLen = key.get_modulus_bytes()-OAEP_OVERHEAD
+    rsaPart = payload[:rsaDataLen]
+    lionessPart = payload[rsaDataLen:]
+    # XXXX DOC
+    while 1:
+	encrypted = mixminion.Crypto.pk_encrypt(rsaPart, key)
+	if not (ord(encrypted[0]) & 0x80):
+	    break
+    #XXXX doc mode 'End-to-end encrypt'
+    k = mixminion.Crypto.Keyset(sessionKey).getLionessKeys("End-to-end encrypt")
+    lionessPart = mixminion.Crypto.lioness_encrypt(lionessPart, k)
+    payload = encrypted + lionessPart
+    tag = payload[:TAG_LEN]
+    payload = payload[TAG_LEN:]
+    exitInfo = tag + exitInfo 
+    assert len(payload) == 28*1024
+    return _buildMessage(payload, exitType, exitInfo, path1, path2,paddingPRNG)
+
+def buildReplyMessage(payload, path1, replyBlock, paddingPRNG=None):
     """Build a message using a reply block.  'path1' is a sequence of
        ServerInfo for the nodes on the first leg of the path.
     """
+    if paddingPRNG is None: paddingPRNG = Crypto.AESCounterPRNG()
+
+    payload = _encodePayload(payload, 0, paddingPRNG)
+   
+    # XXXX Document this mode
+    k = Crypto.Keyset(replyBlock.encryptionKey).getLionessKeys(
+	                 Crypto.PAYLOAD_ENCRYPT_MODE)
+    # XXXX Document why this is decrypt
+    payload = Crypto.lioness_decrypt(payload, k)
+
     return _buildMessage(payload, None, None,
                          path1=path1, path2=replyBlock)
 
-def buildReplyBlock(path, exitType, exitInfo, expiryTime=0, secretPRNG=None):
-    """Return a 2-tuple containing (1) a newly-constructed reply block and (2)
-       a list of secrets used to make it.
+def buildReplyBlock(path, exitType, exitInfo, expiryTime=0, secretPRNG=None, 
+                    tag=None):
+    """Return a 3-tuple containing (1) a newly-constructed reply block, (2)
+       a list of secrets used to make it, (3) a tag.
        
               path: A list of ServerInfo
               exitType: Routing type to use for the final node
-              exitInfo: Routing info for the final node
+              exitInfo: Routing info for the final node, not including tag.
               expiryTime: The time at which this block should expire.
               secretPRNG: A PRNG to use for generating secrets.  If not
                  provided, uses an AES counter-mode stream seeded from our
-                 entropy source.
+                 entropy source.  Note: the secrets are generated so that they
+                 will be used to encrypt the message in reverse order.
+              tag: If provided, a 159-bit tag.  If not provided, a new one 
+                 is generated.
        """
     if secretPRNG is None:
         secretPRNG = Crypto.AESCounterPRNG()
-    secrets = [ secretPRNG.getBytes(SECRET_LEN) for _ in path ]
-    header = _buildHeader(path, secrets, exitType, exitInfo, 
+
+    # The message is encrypted first by the end-to-end key, then by
+    # each of the path keys in order. We need to reverse these steps, so we
+    # generate the path keys back-to-front, followed by the end-to-end key.
+    secrets = [ secretPRNG.getBytes(SECRET_LEN) for _ in range(len(path)+1) ]
+
+    headerSecrets = secrets[:-1]
+    headerSecrets.reverse()
+    sharedKey = secrets[-1]
+
+    if tag is None:
+	tag = _getRandomTag(secretPRNG)
+
+    header = _buildHeader(path, headerSecrets, exitType, tag+exitInfo, 
                           paddingPRNG=Crypto.AESCounterPRNG())
+
     return ReplyBlock(header, expiryTime,
                       Modules.SWAP_FWD_TYPE,
-                      path[0].getRoutingInfo().pack()), secrets
+                      path[0].getRoutingInfo().pack(), sharedKey), secrets, tag
 
 # Maybe we shouldn't even allow this to be called with userKey==None.
-def buildStatelessReplyBlock(path, user, userKey, email=0, expiryTime=0):
-    """Construct a 'stateless' reply block that does not require the
+def buildStatelessReplyBlock(path, exitType, exitInfo, userKey, 
+			     expiryTime=0, secretRNG=None):
+    """XXXX DOC IS NOW WRONG HERE
+       Construct a 'stateless' reply block that does not require the
        reply-message recipient to remember a list of secrets.
        Instead, all secrets are generated from an AES counter-mode
        stream, and the seed for the stream is stored in the 'tag'
@@ -76,24 +144,86 @@
                   email: If true, delivers via SMTP; else delivers via MBOX
        """
     #XXXX Out of sync with the spec.
-    if email and userKey:
-        raise MixError("Requested EMail delivery without password-protection")
+    if secretRNG is None: secretRNG = Crypto.AESCounterPRNG()
+    
+    while 1:
+	seed = _getRandomTag(secretRNG)
+	if Crypto.sha1(seed+userKey+"Validate")[-1] == '\x00':
+	    break
+	
+    prng = Crypto.AESCounterPRNG(Crypto.sha1(seed+userKey+"Generate")[:16])
 
-    seed = Crypto.trng(16)
-    if userKey:
-        tag = Crypto.ctr_crypt(seed,userKey)
-    else:
-        tag = seed
-        
-    if email:
-        exitType = Modules.SMTP_TYPE
-        exitInfo = SMTPInfo(user, "RTRN"+tag).pack()
-    else:
-        exitType = Modules.MBOX_TYPE
-        exitInfo = MBOXInfo(user, "RTRN"+tag).pack()
+    return buildReplyBlock(path, exitType, exitInfo, expiryTime, prng, seed)[0]
+
+#----------------------------------------------------------------------
+# MESSAGE DECODING
+
+def decodePayload(payload, tag, key=None, storedKeys=None, userKey=None):
+    """ DOCDOC XXXX
+        Contract: return payload on success; raise MixError on certain failure,
+          return None if neither.
+    """
+    if _checkPayload(payload):
+	return decodeForwardPayload(payload)
+
+    if storedKeysFn is not None:
+	secrets = storedKeys.get(tag)
+	if secrets is not None:
+	    del storedKeys[tag]
+	    return decodeReplyPayload(payload, secrets)
+
+    if userKey is not None:
+	if Crypto.sha1(tag+userKey+"Validate")[-1] == '\x00': 
+	    try:
+		return decodeStatelessReplyPayload(payload, tag, userKey)
+	    except MixError, _:
+		pass
+
+    if key is not None:
+	p = decodeEncryptedPayload(payload, tag, key)
+	if p is not None:
+	    return p
+	
+    return None
+
+def decodeForwardPayload(payload):
+    "XXXX"
+    return _decodePayload(payload)
 
+def decodeEncryptedPayload(payload, tag, key):
+    "XXXX"
+    msg = tag+payload
+    try:
+	rsaPart = Crypto.pk_decrypt(msg[:key.get_modulus_bytes()], key)
+    except Crypto.CryptoError, _:
+	return None
+    rest = msg[key.get_modulus_bytes():]
+    #XXXX magic string
+    k = Crypto.Keyset(rsaPart[:SECRET_LEN]).getLionessKeys("End-to-end encrypt")
+    rest = rsaPart[SECRET_LEN:] + Crypto.lioness_decrypt(rest, k)
+    return _decodePayload(rest)
+
+def decodeReplyPayload(payload, secrets, check=0):
+    "XXXX"
+    for sec in secrets:
+	k = Crypto.Keyset(sec).getLionessKeys(Crypto.PAYLOAD_ENCRYPT_MODE)
+	# XXXX document why this is encrypt
+	payload = Crypto.lioness_encrypt(payload, k)
+	if check and _checkPayload(payload):
+	    break
+
+    return _decodePayload(payload)
+
+def decodeStatelessReplyPayload(payload, tag, userKey):
+    "XXXX"
+    seed = Crypto.sha1(tag+userKey+"Generate")[:16]
     prng = Crypto.AESCounterPRNG(seed)
-    return buildReplyBlock(path, exitType, exitInfo, expiryTime, prng)[0]
+    secrets = [ prng.getBytes(SECRET_LEN) for _ in xrange(17) ]
+			 
+    return decodeReplyBlock(payload, secrets, check=1)
+	
+	
+    
 
 #----------------------------------------------------------------------
 def _buildMessage(payload, exitType, exitInfo,
@@ -101,7 +231,7 @@
     """Helper method to create a message.
 
     The following fields must be set:
-       payload: the intended exit payload.
+       payload: the intended exit payload.  Must be 28K.
        (exitType, exitInfo): the routing type and info for the final node.
               (Ignored for reply messages)
        path1: a sequence of ServerInfo objects, one for each of the nodes
@@ -115,13 +245,15 @@
              a replyBlock object.
 
     The following fields are optional:
-       paddingPRNG: A pseudo-random number generator used to pad the headers
-         and the payload.  If not provided, we use a counter-mode AES stream
-         seeded from our entropy source.
+       paddingPRNG: A pseudo-random number generator used to pad the headers.
+         If not provided, we use a counter-mode AES stream seeded from our 
+         entropy source. 
+         
        paranoia: If this is false, we use the padding PRNG to generate
          header secrets too.  Otherwise, we read all of our header secrets
          from the true entropy source. 
     """
+    assert len(payload) == PAYLOAD_LEN
     reply = None
     if isinstance(path2, ReplyBlock):
         reply = path2
@@ -147,11 +279,6 @@
         path1exittype = Modules.SWAP_FWD_TYPE
         path1exitinfo = path2[0].getRoutingInfo().pack()
 
-    # Pad the payload, as needed.
-    if len(payload) < PAYLOAD_LEN:
-        # ???? Payload padding/sizing must be handled in spec.
-        payload += paddingPRNG.getBytes(PAYLOAD_LEN-len(payload))
-
     # Generate secrets for path1.
     secrets1 = [ secretRNG.getBytes(SECRET_LEN) for _ in path1 ]
     
@@ -179,7 +306,6 @@
            exitInfo: The routing info for the last node in the header
            paddingPRNG: A pseudo-random number generator to generate padding
     """
-
     assert len(path) == len(secrets)
     if len(path) * ENC_SUBHEADER_LEN > HEADER_LEN:
         raise MixError("Too many nodes in path")
@@ -294,3 +420,40 @@
         payload = Crypto.lioness_encrypt(payload,pkey)
 
     return Message(header1, header2, payload).pack()
+
+def _encodePayload(payload, overhead, paddingPRNG):
+    """XXXX
+    """
+    # FFFF Do fragmentation here.
+    
+    payload = compressData(payload)
+
+    length = len(payload)
+    paddingLen = PAYLOAD_LEN - SINGLETON_PAYLOAD_OVERHEAD - overhead - length
+    if paddingLen < 0:
+	raise MixError("Payload too long for singleton message")
+    
+    payload += paddingPRNG.getBytes(paddingLen)
+
+    return SingletonPayload(length, Crypto.sha1(payload), payload).pack()
+
+def _getRandomTag(rng):
+    "XXXX"
+    b = ord(rng.getBytes(1)) & 0x7f
+    return chr(b) + rng.getBytes(TAG_LEN-1)
+
+def _decodePayload(payload):
+    if not _checkPayload(payload):
+	raise MixError("Hash doesn't match")
+    payload = parsePayload(payload)
+
+    if not payload.isSingleton():
+	raise MixError("Message fragments not yet supported")
+    #if payload.hash != Crypto.sha1(payload.data):
+    #    raise MixError("Hash doesn't match")
+
+    return uncompressData(payload.getContents())
+
+def _checkPayload(payload):
+    return payload[2:22] == Crypto.sha1(payload[22:])
+

Index: ClientMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientMain.py,v
retrieving revision 1.3
retrieving revision 1.4
diff -u -d -r1.3 -r1.4
--- ClientMain.py	10 Sep 2002 20:06:24 -0000	1.3
+++ ClientMain.py	13 Oct 2002 01:34:44 -0000	1.4
@@ -46,6 +46,13 @@
        FFFF want to re-do it entirely once we have directory support, so it
        FFFF doesn't matter so much right now.
        """
+    ## Fields
+    # dirname: the name of the storage directory.  All files in the directory
+    #     should be of the form 'si_XXXX...X', and contain validated server
+    #     descriptors.
+    # servers: a map from nickname to [list of serverinfo objects], or None
+    #     if no servers have been loaded
+    # allServers: a list of serverinfo objects
     def __init__(self, dirname):
 	dirname = os.path.join(dirname, 'servers')
 	createPrivateDir(dirname)
@@ -53,6 +60,8 @@
 	self.servers = None
 
     def load(self, forceReload=0):
+	"""Retrieve a list of servers from disk.  If 'forceReload' is false,
+           only load the servers if we have not already done so."""
 	if not (self.servers is None or forceReload):
 	    return
 	now = time.time()
@@ -80,24 +89,34 @@
 		self.servers[nickname] = info
 
     def getCurrentServer(nickname, when=None, until=None):
-	if type(nickname) == ServerInfo:
-	    return nickname
+        """Return a server descriptor valid during the interval
+           when...until.  If 'nickname' is a string, return only a
+           server with the appropriate nickname.  If 'nickname' is a
+           server descriptor, return that same server descriptor.
+
+           Raise 'MixError' if no appropriate descriptor is found. """
+        self.load()
 	if when is None:
 	    when = time.time()
 	if until is None:
 	    until = when+1
-	try:
-	    serverList = self.servers[nickname]
-	except KeyError, e:
-	    raise MixError("Nothing known about server %s"%nickname)
+	if type(nickname) == ServerInfo:
+	    serverList = [ nickname ]
+	else:
+	    try:
+		serverList = self.servers[nickname]
+	    except KeyError, e:
+		raise MixError("Nothing known about server %s"%nickname)
 	for info in serverList:
 	    #XXXX fail on DNE
 	    server = info['Server']
 	    if server['Valid-After'] <= when <= until <= server['Valid-Until']:
 		return info
-	raise MixError("No current information for server %s"%nickname)
+	raise MixError("No time-valid information for server %s"%nickname)
 
-    def getAllCurrentServers(when=None, until=0):
+    def getAllCurrentServers(when=None, until=None):
+	"""Return all ServerInfo objects valid during a given interval."""
+        self.load()
 	if when is None:
 	    when = time.time()
 	if until is None:
@@ -111,6 +130,8 @@
 	return result
 
     def importServerInfo(self, fname, force=1):
+	"""Import a server descriptor from an external file into the internal
+	   cache.  Return 1 on import; 0 on failure."""
 	self.load()
 	f = open(fname)
 	contents = f.read()
@@ -147,7 +168,10 @@
 	f.write(contents)
 	f.close()
 
+	return 1
+
 def installDefaultConfig(fname):
+    """Create a default, 'fail-safe' configuration in a given file"""
     getLog().warn("No configuration file found. Installing default file in %s",
 		  fname)
     f = open(os.path.expanduser(fname), 'w')

Index: Crypto.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Crypto.py,v
retrieving revision 1.20
retrieving revision 1.21
diff -u -d -r1.20 -r1.21
--- Crypto.py	10 Sep 2002 14:45:30 -0000	1.20
+++ Crypto.py	13 Oct 2002 01:34:44 -0000	1.21
@@ -12,6 +12,7 @@
 import sys
 import stat
 import copy_reg
+import zlib
 from types import StringType
 
 import mixminion._minionlib as _ml
@@ -583,3 +584,4 @@
         return _trng_uncached(n)
 
 _theTrueRNG = _TrueRNG(1024)
+

Index: Packet.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Packet.py,v
retrieving revision 1.10
retrieving revision 1.11
diff -u -d -r1.10 -r1.11
--- Packet.py	10 Sep 2002 20:06:26 -0000	1.10
+++ Packet.py	13 Oct 2002 01:34:44 -0000	1.11
@@ -7,19 +7,22 @@
 
 __all__ = [ 'ParseError', 'Message', 'Header', 'Subheader',
             'parseMessage', 'parseHeader', 'parseSubheader',
-            'getTotalBlocksForRoutingInfoLen', 'ReplyBlock',
+            'getTotalBlocksForRoutingInfoLen', 'parsePayload',
+            'SingletonPayload', 'FragmentPayload', 'ReplyBlock',
             'IPV4Info', 'SMTPInfo', 'MBOXInfo', 'parseIPV4Info',
             'parseSMTPInfo', 'parseMBOXInfo', 'ReplyBlock',
-            'parseReplyBlock', 'ENC_SUBHEADER_LEN',
-            'HEADER_LEN', 'PAYLOAD_LEN', 'MAJOR_NO', 'MINOR_NO',
-            'SECRET_LEN']
+            'parseReplyBlock', 'ENC_SUBHEADER_LEN', 'HEADER_LEN',
+            'PAYLOAD_LEN', 'MAJOR_NO', 'MINOR_NO', 'SECRET_LEN', 'TAG_LEN',
+	    'SINGLETON_PAYLOAD_OVERHEAD', 'OAEP_OVERHEAD',
+	    'FRAGMENT_PAYLOAD_OVERHEAD',
+	    'compressData', 'uncompressData']
 
+import zlib
 import struct
 from socket import inet_ntoa, inet_aton
 from mixminion.Common import MixError, floorDiv
 
 # Major and minor number for the understood packet format.
-# ???? The spec needs to specify this.
 MAJOR_NO, MINOR_NO = 0,1
 
 # Length of a Mixminion message
@@ -42,6 +45,8 @@
 DIGEST_LEN = 20
 # Length of a secret key
 SECRET_LEN = 16
+# Length of end-to-end message tag
+TAG_LEN = 20
 
 # Most info that fits in a single extened subheader
 ROUTING_INFO_PER_EXTENDED_SUBHEADER = ENC_SUBHEADER_LEN
@@ -50,6 +55,9 @@
     """Thrown when a message or portion thereof is incorrectly formatted."""
     pass
 
+#----------------------------------------------------------------------
+# PACKET-LEVEL STRUCTURES
+
 def parseMessage(s):
     """Given a 32K string, returns a Message object that breaks it into
        two headers and a payload."""
@@ -82,7 +90,7 @@
     return Header(s)
 
 class Header:
-    """Represents a 2K Mixminion header"""
+    """Represents a 2K Mixminion header, containing up to 16 subheaders."""
     def __init__(self, contents):
         """Initialize a new header from its contents"""
         self.contents = contents
@@ -141,7 +149,7 @@
         return 2 + floorDiv(extraBytes,ROUTING_INFO_PER_EXTENDED_SUBHEADER)
 
 class Subheader:
-    """Represents a decoded Mixminion header
+    """Represents a decoded Mixminion subheader
 
        Fields: major, minor, secret, digest, routinglen, routinginfo,
                routingtype.
@@ -228,9 +236,120 @@
                 result.append(content)
             return result
 
+#----------------------------------------------------------------------
+# UNENCRYPTED PAYLOADS
 
-RB_UNPACK_PATTERN = "!4sBBL%ssHH" % (HEADER_LEN)
-MIN_RB_LEN = 14+HEADER_LEN
+# XXXX DOCUMENT CONTENTS
+FRAGMENT_MESSAGEID_LEN = 20
+MAX_N_FRAGMENTS = 0x7ffff
+
+SINGLETON_PAYLOAD_OVERHEAD = 2 + DIGEST_LEN
+FRAGMENT_PAYLOAD_OVERHEAD = 2 + DIGEST_LEN + FRAGMENT_MESSAGEID_LEN + 4
+OAEP_OVERHEAD = 42
+
+def parsePayload(payload):
+    "XXXX"
+    if len(payload) not in (PAYLOAD_LEN, PAYLOAD_LEN-OAEP_OVERHEAD):
+	raise ParseError("Payload has bad length")
+    bit0 = ord(payload[0]) & 0x80
+    if bit0:
+	# We have a fragment
+	idx, hash, msgID, msgLen = struct.unpack(FRAGMENT_UNPACK_PATTERN,
+					 payload[:FRAGMENT_PAYLOAD_OVERHEAD])
+	idx &= 0x7f
+	contents = payload[FRAGMENT_PAYLOAD_OVERHEAD:] 
+	if msgLen <= len(contents):
+	    raise ParseError("Payload has an invalid size field")
+	return FragmentPayload(idx,hash,msgID,msgLen,contents)
+    else:
+	# We have a singleton
+	size, hash = struct.unpack(SINGLETON_UNPACK_PATTERN, 
+				   payload[:SINGLETON_PAYLOAD_OVERHEAD])
+	contents = payload[SINGLETON_PAYLOAD_OVERHEAD:]
+	if size > len(contents):
+	    raise ParseError("Payload has invalid size field")
+	return SingletonPayload(size,hash,contents)
+
+# A singleton payload starts with a 0 bit, 15 bits of size, and a 20-byte hash
+SINGLETON_UNPACK_PATTERN = "!H%ds" % (DIGEST_LEN)
+
+# A fragment payload starts with a 1 bit, a 15-bit paket index, a 20-byte hash,
+# a 20-byte message ID, and 4 bytes of message size.
+FRAGMENT_UNPACK_PATTERN = "!H%ds%dsL" % (DIGEST_LEN, FRAGMENT_MESSAGEID_LEN)
+
+class SingletonPayload:
+    "XXXX"
+    def __init__(self, size, hash, data):
+	self.size = size
+	self.hash = hash
+	self.data = data
+
+    def isSingleton(self):
+	"XXXX"
+	return 1
+
+    def getContents(self):
+	"XXXX"
+	return self.data[:self.size]
+
+    def pack(self):
+	"XXXX"
+	assert (0x8000 & self.size) == 0
+	assert 0 <= self.size <= len(self.data)
+	assert len(self.hash) == DIGEST_LEN
+	assert (PAYLOAD_LEN - SINGLETON_PAYLOAD_OVERHEAD - len(self.data)) in \
+	       (0, OAEP_OVERHEAD)
+	header = struct.pack(SINGLETON_UNPACK_PATTERN, self.size, self.hash)
+	return "%s%s" % (header, self.data)
+
+class FragmentPayload:
+    "XXXX"
+    def __init__(self, index, hash, msgID, msgLen, data):
+	self.index = index
+	self.hash = hash
+	self.msgID = msgID
+	self.msgLen = msgLen
+	self.data = data
+
+    def isSingleton(self):
+	"XXXX"
+	return 0
+
+    def pack(self):
+	"XXXX"
+	assert 0 <= self.index <= MAX_N_FRAGMENTS
+	assert len(self.hash) == DIGEST_LEN
+	assert len(self.msgID) == FRAGMENT_MESSAGEID_LEN
+	assert len(self.data) < self.msgLen < 0x100000000L
+	assert (PAYLOAD_LEN - FRAGMENT_PAYLOAD_OVERHEAD - len(self.data)) in \
+	       (0, OAEP_OVERHEAD)
+	idx = self.index | 0x8000
+	header = struct.pack(FRAGMENT_UNPACK_PATTERN, idx, self.hash,
+			     self.msgID, self.msgLen)
+	return "%s%s" % (header, self.data)
+
+#----------------------------------------------------------------------
+# COMPRESSION FOR PAYLOADS
+
+# FFFF Check for zlib acceptability.  Check for correct parameters in zlib
+# FFFF module
+
+def compressData(payload):
+    "XXXX"
+    return zlib.compress(payload, 9)
+
+def uncompressData(payload):
+    "XXXX"
+    try:
+	return zlib.decompress(payload)
+    except zlib.error, _:
+	raise ParseError("Error in compressed data")
+
+#----------------------------------------------------------------------
+# REPLY BLOCKS
+
+RB_UNPACK_PATTERN = "!4sBBL%dsHH%ss" % (HEADER_LEN, SECRET_LEN)
+MIN_RB_LEN = 30+HEADER_LEN
 
 def parseReplyBlock(s):
     """Return a new ReplyBlock object for an encoded reply block"""
@@ -238,7 +357,7 @@
         raise ParseError("Reply block too short")
 
     try:
-        magic, major, minor, timestamp, header, rlen, rt = \
+        magic, major, minor, timestamp, header, rlen, rt, key = \
                struct.unpack(RB_UNPACK_PATTERN, s[:MIN_RB_LEN])
     except struct.error:
         raise ParseError("Misformatted reply block")
@@ -254,26 +373,29 @@
     if len(ri) != rlen:
         raise ParseError("Misformatted reply block")
 
-    return ReplyBlock(header, timestamp, rt, ri)
+    return ReplyBlock(header, timestamp, rt, ri, key)
 
 class ReplyBlock:
     """A mixminion reply block, including the address of the first hop
        on the path, and the RoutingType and RoutingInfo for the server."""
-    # XXXXX Not in sync with spec
-    def __init__(self, header, useBy, rt, ri):
+    def __init__(self, header, useBy, rt, ri, key):
         """Construct a new Reply Block."""
         assert len(header) == HEADER_LEN
         self.header = header
         self.timestamp = useBy
         self.routingType = rt
         self.routingInfo = ri
+	self.encryptionKey = key
 
     def pack(self):
         """Returns the external representation of this reply block"""
         return struct.pack(RB_UNPACK_PATTERN,
-                           "SURB", 0x00, 0x01, self.timestamp,
-                           self.header, len(self.routingInfo),
-                           self.routingType)+self.routingInfo
+                           "SURB", 0x00, 0x01, self.timestamp, self.header, 
+                           len(self.routingInfo), self.routingType, 
+			   self.encryptionKey) + self.routingInfo
+                           
+#----------------------------------------------------------------------
+# Routing info
 
 # An IPV4 address (Used by SWAP_FWD and FWD) is packed as: four bytes
 # of IP address, a short for the portnum, and DIGEST_LEN bytes of keyid.
@@ -292,10 +414,11 @@
     return IPV4Info(ip, port, keyinfo)
 
 class IPV4Info:
-    """An IPV4Info object represents the routinginfo for a FWD or SWAP_FWD hop.
+    """An IPV4Info object represents the routinginfo for a FWD or
+       SWAP_FWD hop.
 
-       Fields: ip (a dotted quad string), port (an int from 0..65535), and keyinfo
-       (a digest)."""
+       Fields: ip (a dotted quad string), port (an int from 0..65535),
+       and keyinfo (a digest)."""
     def __init__(self, ip, port, keyinfo):
         """Construct a new IPV4Info"""
         assert 0 <= port <= 65535
@@ -318,48 +441,40 @@
 
 def parseSMTPInfo(s):
     """Convert the encoding of an SMTP routinginfo into an SMTPInfo object."""
-    lst = s.split("\000",1)
-    if len(lst) == 1:
-        return SMTPInfo(s,None)
-    else:
-        return SMTPInfo(lst[0], lst[1])
+    if len(s) <= TAG_LEN:
+	raise ParseError("SMTP routing info is too short")
+    return SMTPInfo(s[TAG_LEN:], s[:TAG_LEN])
 
 class SMTPInfo:
     """Represents the routinginfo for an SMTP hop.
 
-       Fields: email (an email address), tag (an arbitrary tag, optional)."""
+       Fields: email (an email address), tag (20 bytes)."""
     def __init__(self, email, tag):
+	assert len(tag) == TAG_LEN
         self.email = email
         self.tag = tag
 
     def pack(self):
         """Returns the wire representation of this SMTPInfo"""
-        if self.tag != None:
-            return self.email+"\000"+self.tag
-        else:
-            return self.email
+	return self.tag + self.email
 
 def parseMBOXInfo(s):
     """Convert the encoding of an MBOX routinginfo into an MBOXInfo
        object."""
-    lst = s.split("\000",1)
-    if len(lst) == 1:
-        return MBOXInfo(s,None)
-    else:
-        return MBOXInfo(lst[0], lst[1])
+    if len(s) < TAG_LEN:
+	raise ParseError("MBOX routing info is too short")
+    return MBOXInfo(s[TAG_LEN:], s[:TAG_LEN])
 
 class MBOXInfo:
     """Represents the routinginfo for an MBOX hop.
 
-       Fields: user (a user identifier), tag (an arbitrary tag, optional)."""
+       Fields: user (a user identifier), tag (20 bytes)."""
     def __init__(self, user, tag):
+	assert len(tag) == TAG_LEN
         self.user = user
-        assert user.find('\000') == -1
         self.tag = tag
 
     def pack(self):
         """Return the external representation of this routing info."""
-        if self.tag:
-            return self.user+"\000"+self.tag
-        else:
-            return self.user
+	return self.tag + self.user
+

Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.28
retrieving revision 1.29
diff -u -d -r1.28 -r1.29
--- test.py	10 Sep 2002 20:06:28 -0000	1.28
+++ test.py	13 Oct 2002 01:34:44 -0000	1.29
@@ -615,37 +615,109 @@
     
 
     def test_smtpinfomboxinfo(self):
+	tag = "\x01\x02"+"Z"*18
         for _class, _parse, _key in ((SMTPInfo, parseSMTPInfo, 'email'),
                                      (MBOXInfo, parseMBOXInfo, 'user')):
-            ri = "no-such-user@wangafu.net\x00xyzzy"
+            ri = tag+"no-such-user@wangafu.net"
             inf = _parse(ri)
             self.assertEquals(getattr(inf,_key), "no-such-user@wangafu.net")
-            self.assertEquals(inf.tag, "xyzzy")
-            self.assertEquals(inf.pack(), ri)
-            inf = _class("no-such-user@wangafu.net","xyzzy")
-            self.assertEquals(inf.pack(), ri)
-            # No tag
-            ri = "no-such-user@wangafu.net"
-            inf = _parse(ri)
-            self.assertEquals(inf.tag, None)
-            self.assertEquals(getattr(inf,_key), 'no-such-user@wangafu.net')
+            self.assertEquals(inf.tag, tag)
             self.assertEquals(inf.pack(), ri)
-            # NUL in tag
-            ri = "no-such-user@wangafu.net\x00xyzzy\x00plover"
-            inf = _parse(ri)
-            self.assertEquals(getattr(inf,_key), "no-such-user@wangafu.net")
-            self.assertEquals(inf.tag, "xyzzy\x00plover")
+            inf = _class("no-such-user@wangafu.net",tag)
             self.assertEquals(inf.pack(), ri)
+            # Message too short to have a tag
+            ri = "no-such-user@wangaf"
+	    self.failUnlessRaises(ParseError, _parse, ri)
 
     def test_replyblock(self):
+	key = "\x99"*16
         r = ("SURB\x00\x01"+"\x00\x00\x00\x00"+("Z"*2048)+"\x00\x0A"+"\x00\x01"
-             +("F"*10))
+	     +key+("F"*10))
         rb = parseReplyBlock(r)
         self.assertEquals(rb.timestamp, 0)
         self.assertEquals(rb.header, "Z"*2048)
         self.assertEquals(rb.routingType, 1)
         self.assertEquals(rb.routingInfo, "F"*10)
+        self.assertEquals(rb.encryptionKey, key)
         self.assertEquals(r, rb.pack())
+	rb = ReplyBlock(header="Z"*2048,useBy=0,rt=1,ri="F"*10,key=key)
+	self.assertEquals(r, rb.pack())
+
+    def test_payloads(self):
+	contents = ("payload"*(4*1024))[:28*1024 - 22]
+	hash = "HASH"*5
+	singleton_payload_1 = "\x00\xff"+hash+contents
+	singleton_payload_2 = singleton_payload_1[:-42] #OAEP overhead
+	p1 = parsePayload(singleton_payload_1)
+	p2 = parsePayload(singleton_payload_2)
+	self.failUnless(p1.isSingleton() and p2.isSingleton())
+ 	self.assertEquals(p1.size,255)
+	self.assertEquals(p2.size,255)
+	self.assertEquals(p1.hash,hash)
+	self.assertEquals(p2.hash,hash)
+	self.assertEquals(p1.data,contents)
+	self.assertEquals(p2.data,contents[:-42])
+	self.assertEquals(p1.getContents(), contents[:255])
+	self.assertEquals(p2.getContents(), contents[:255])
+	self.assertEquals(p1.pack(),singleton_payload_1)
+	self.assertEquals(p2.pack(),singleton_payload_2)
+
+	self.assertEquals(singleton_payload_1,
+			  SingletonPayload(255, hash, contents).pack())
+	self.assertEquals(singleton_payload_2,
+			  SingletonPayload(255, hash, contents[:-42]).pack())
+
+	# Impossible payload lengths
+	self.failUnlessRaises(ParseError,parsePayload,singleton_payload_1+"a")
+	self.failUnlessRaises(ParseError,parsePayload,singleton_payload_2+"a")
+	self.failUnlessRaises(ParseError,parsePayload,singleton_payload_2[:-1])
+	# Impossible value for size field
+	bad = "\x7fff" + singleton_payload_1[2:]
+	self.failUnlessRaises(ParseError,parsePayload,bad)
+
+	## Now, for the fragment payloads.
+	msgID = "This is a message123"
+	assert len(msgID) == 20
+	contents = contents[:28*1024 - 46]
+	frag_payload_1 = "\x80\x02"+hash+msgID+"\x00\x01\x00\x00"+contents
+	frag_payload_2 = frag_payload_1[:-42] # oaep overhead
+	p1 = parsePayload(frag_payload_1)
+	p2 = parsePayload(frag_payload_2)
+	self.failUnless(not p1.isSingleton() and not p2.isSingleton())
+ 	self.assertEquals(p1.index,2)
+	self.assertEquals(p2.index,2)
+	self.assertEquals(p1.hash,hash)
+	self.assertEquals(p2.hash,hash)
+	self.assertEquals(p1.msgID,msgID)
+	self.assertEquals(p2.msgID,msgID)
+	self.assertEquals(p1.msgLen,64*1024)
+	self.assertEquals(p2.msgLen,64*1024)
+	self.assertEquals(p1.data,contents)
+	self.assertEquals(p2.data,contents[:-42])
+	self.assertEquals(p1.pack(),frag_payload_1)
+	self.assertEquals(p2.pack(),frag_payload_2)
+
+	self.assertEquals(frag_payload_1,
+		  FragmentPayload(2,hash,msgID,64*1024,contents).pack())
+	self.assertEquals(frag_payload_2,
+		  FragmentPayload(2,hash,msgID,64*1024,contents[:-42]).pack())
+
+	# Impossible payload lengths
+	self.failUnlessRaises(ParseError,parsePayload,frag_payload_1+"a")
+	self.failUnlessRaises(ParseError,parsePayload,frag_payload_2+"a")
+	self.failUnlessRaises(ParseError,parsePayload,frag_payload_2[:-1])
+	
+	# Impossible message sizes
+	min_payload_1 = "\x80\x02"+hash+msgID+"\x00\x00\x6F\xD3"+contents
+	bad_payload_1 = "\x80\x02"+hash+msgID+"\x00\x00\x6F\xD2"+contents
+	min_payload_2 = "\x80\x02"+hash+msgID+"\x00\x00\x6F\xA9"+contents[:-42]
+	bad_payload_2 = "\x80\x02"+hash+msgID+"\x00\x00\x6F\xA8"+contents[:-42]
+	min_payload_3 = "\x80\x02"+hash+msgID+"\x00\x00\x6F\xD2"+contents[:-42]
+	parsePayload(min_payload_1)
+	parsePayload(min_payload_2)
+	parsePayload(min_payload_3)
+	self.failUnlessRaises(ParseError,parsePayload,bad_payload_1)
+	self.failUnlessRaises(ParseError,parsePayload,bad_payload_2)
 
 #----------------------------------------------------------------------
 from mixminion.HashLog import HashLog
@@ -788,7 +860,7 @@
                  "Hi mom")
         self.do_header_test(head, pks, secrets, rtypes, rinfo)
 
-    def do_header_test(self, head, pks, secrets, rtypes, rinfo):
+    def do_header_test(self, head, pks, secrets, rtypes, rinfo, withTag=0):
         """Unwraps and checks the layers of a single header.
                     head: the header to check
                     pks: sequence of public keys for hops in the path
@@ -802,6 +874,7 @@
               Returns a list of the secrets encountered.
               If rinfo is None, also returns a list of the routinginfo objects.
             """
+        tag = None
         retsecrets = []
         retinfo = []
         if secrets is None:
@@ -822,32 +895,45 @@
             ks = Keyset(secret)
             key = ks.get(HEADER_SECRET_MODE)
             prngkey = ks.get(RANDOM_JUNK_MODE)
+
+	    if rt < 0x100: # extra bytes for tag
+		ext = 0
+	    else:
+		ext = 20
+		if ri:
+		    tag = ri[:20]
             if not subh.isExtended():
                 if ri:
-                    self.assertEquals(subh.routinginfo, ri)
-                    self.assertEquals(subh.routinglen, len(ri))
+		    self.assertEquals(subh.routinginfo[ext:], ri)
+                    self.assertEquals(subh.routinglen, len(ri)+ext)
                 else:
                     retinfo.append(subh.routinginfo)
                 size = 128
                 n = 0
             else:
-                self.assert_(len(ri) > mixminion.Packet.MAX_ROUTING_INFO_LEN)
+                self.assert_(len(ri)+ext>mixminion.Packet.MAX_ROUTING_INFO_LEN)
                 n = subh.getNExtraBlocks()
                 size = (1+n)*128
                 more = ctr_crypt(head[128:128+128*n], key)
                 subh.appendExtraBlocks(more)
                 if ri:
-                    self.assertEquals(subh.routinginfo, ri)
-                    self.assertEquals(subh.routinglen, len(ri))
+		    self.assertEquals(subh.routinginfo[ext:], ri)
+                    self.assertEquals(subh.routinglen, len(ri)+ext)
                 else:
                     retinfo.append(subh.routinginfo)
 
             head = ctr_crypt(head[size:]+prng(prngkey,size), key, 128*n)
 
         if retinfo:
-            return retsecrets, retinfo
+	    if withTag:
+		return retsecrets, retinfo, tag
+	    else:
+		return retsecrets, retinfo
         else:
-            return retsecrets
+	    if withTag:
+		return retsecrets, tag
+	    else:
+		return retsecrets
 
     def test_extended_routinginfo(self):
         bhead = BuildMessage._buildHeader
@@ -863,10 +949,11 @@
 
         # Now try a header with extended **intermediate** routing info.
         # Since this never happens in the wild, we fake it.
+	tag = "dref"*5
         longStr2 = longStr * 2
 
         def getLongRoutingInfo(longStr2=longStr2):
-            return MBOXInfo("fred",longStr2)
+            return MBOXInfo(longStr2,tag)
 
         server4 = FakeServerInfo("127.0.0.1", 1, self.pk1, "X"*20)
         server4.getRoutingInfo = getLongRoutingInfo
@@ -876,7 +963,7 @@
                      AESCounterPRNG())
         pks = (self.pk2,self.pk1)
         rtypes = (FWD_TYPE,99)
-        rinfo = ("fred\000"+longStr2,longStr)
+        rinfo = (tag+longStr2,longStr)
         self.do_header_test(head, pks, secrets, rtypes, rinfo)
 
         # Now we make sure that overlong routing info fails.
@@ -955,9 +1042,10 @@
     def do_message_test(self, msg,
                         header_info_1,
                         header_info_2,
-                        payload):
+                        payload, decoder=None):
         """Decrypts the layers of a message one by one, checking them for
            correctness.
+	   XXXX Document decoder
                       msg: the message to examine
                       header_info_1: a tuple of (pks,secrets,rtypes,rinfo)
                             as used by do_header_test for the first header.
@@ -977,20 +1065,23 @@
 	p = lioness_decrypt(p,mixminion.Crypto.lioness_keys_from_header(h2))
         h2 = lioness_decrypt(h2,mixminion.Crypto.lioness_keys_from_payload(p))
 
-        sec = self.do_header_test(h2, *header_info_2)
+        sec, tag = self.do_header_test(h2, withTag=1, *header_info_2)
         for s in sec:
             ks = Keyset(s)
             p = lioness_decrypt(p,ks.getLionessKeys(PAYLOAD_ENCRYPT_MODE))
 
-        # FFFF Need to do something about size encoding.
-        self.assertEquals(payload, p[:len(payload)])
+	if decoder is None:
+	    p = BuildMessage.decodeForwardPayload(p)
+	else:
+	    p = decoder(p, tag)
 
+        self.assertEquals(payload, p[:len(payload)])
 
     def test_build_fwd_message(self):
         bfm = BuildMessage.buildForwardMessage
         payload = "Hello"
 
-        m = bfm(payload, 99, "Goodbye",
+        m = bfm(payload, 500, "Goodbye",
                 [self.server1, self.server2],
                 [self.server3, self.server2])
 
@@ -1000,12 +1091,12 @@
                                (self.server2.getRoutingInfo().pack(),
                                 self.server3.getRoutingInfo().pack()) ),
                              ( (self.pk3, self.pk2), None,
-                               (FWD_TYPE, 99),
+                               (FWD_TYPE, 500),
                                (self.server2.getRoutingInfo().pack(),
                                 "Goodbye") ),
                              "Hello")
 
-        m = bfm(payload, 99, "Goodbye",
+        m = bfm(payload, 500, "Goodbye",
                 [self.server1,],
                 [self.server3,])
 
@@ -1014,7 +1105,7 @@
                                (SWAP_FWD_TYPE,),
                                (self.server3.getRoutingInfo().pack(),) ),
                              ( (self.pk3,), None,
-                               (99,),
+                               (500,),
                                ("Goodbye",) ),
                              "Hello")
 
@@ -1024,11 +1115,14 @@
         brm = BuildMessage.buildReplyMessage
 
         ## Stateful reply blocks.
-        reply, secrets = \
+        reply, secrets, tag = \
              brb([self.server3, self.server1, self.server2,
                   self.server1, self.server3],
                  SMTP_TYPE,
-                 SMTPInfo("no-such-user@invalid", None).pack())
+		 "no-such-user@invalid", tag=("+"*20))
+	hsecrets = secrets[:-1]
+	hsecrets.reverse()
+
         pks_1 = (self.pk3, self.pk1, self.pk2, self.pk1, self.pk3)
         infos = (self.server1.getRoutingInfo().pack(),
                  self.server2.getRoutingInfo().pack(),
@@ -1041,43 +1135,35 @@
                 [self.server3, self.server1],
                 reply)
 
+	#XXXX Explain this thing
+	def decoder(p,t,secrets=secrets):
+	    return BuildMessage.decodeReplyPayload(p,secrets[-1:])
+
         self.do_message_test(m,
                              ((self.pk3, self.pk1), None,
                               (FWD_TYPE,SWAP_FWD_TYPE),
                               (self.server1.getRoutingInfo().pack(),
                                self.server3.getRoutingInfo().pack())),
-                             (pks_1, secrets,
+                             (pks_1, hsecrets, # stop after first pk???????XXXX
                               (FWD_TYPE,FWD_TYPE,FWD_TYPE,FWD_TYPE,SMTP_TYPE),
-                              infos+(
-                               SMTPInfo("no-such-user@invalid",None).pack(),
-                               )),
-                             "Information?")
+                              infos+("no-such-user@invalid",)),
+                             "Information?",
+			     decoder=decoder)
 
         ## Stateless replies
         reply = bsrb([self.server3, self.server1, self.server2,
-                      self.server1, self.server3],
+                      self.server1, self.server3], MBOX_TYPE,
                      "fred", "Galaxy Far Away.", 0)
 
         sec,(loc,) = self.do_header_test(reply.header, pks_1, None,
                             (FWD_TYPE,FWD_TYPE,FWD_TYPE,FWD_TYPE,MBOX_TYPE),
                             infos+(None,))
-        s = "fred\x00RTRN"
-        self.assert_(loc.startswith(s))
-        seed = ctr_crypt(loc[len(s):], "Galaxy Far Away.")
-        prng = AESCounterPRNG(seed)
-        self.assert_(sec == [ prng.getBytes(16) for _ in range(5) ])
-
-        ## Stateless reply, no user key (trusted server)
-        reply = bsrb([self.server3, self.server1, self.server2,
-                      self.server1, self.server3],
-                     "fred", None)
-        sec,(loc,) = self.do_header_test(reply.header, pks_1, None,
-                            (FWD_TYPE,FWD_TYPE,FWD_TYPE,FWD_TYPE,MBOX_TYPE),
-                                         infos+(None,))
-        self.assert_(loc.startswith(s))
-        seed = loc[len(s):]
-        prng = AESCounterPRNG(seed)
-        self.assert_(sec == [ prng.getBytes(16) for _ in range(5) ])
+	if 0: #XXXXX
+	    s = "fred\x00RTRN"
+	    self.assert_(loc.startswith(s)) 
+	    seed = ctr_crypt(loc[len(s):], "Galaxy Far Away.")
+	    prng = AESCounterPRNG(seed)
+	    self.assert_(sec == [ prng.getBytes(16) for _ in range(5) ])
 
 #----------------------------------------------------------------------
 # Having tested BuildMessage without using PacketHandler, we can now use
@@ -1106,14 +1192,15 @@
         self.hlog.close()
 
     def do_test_chain(self, m, sps, routingtypes, routinginfo, payload,
-                      appkey=None):
+                      appkey=None, decoder=None):
         """Routes a message through a series of servers, making sure that
            each one decrypts it properly and routes it correctly to the
            next.
+	   XXXX DOC DECODER
                     m: the message to test
                     sps: sequence of PacketHandler objects for m's path
                     routingtypes: sequence of expected routingtype
-                    routinginfo: sequence of expected routinginfo
+                    routinginfo: sequence of expected routinginfo, excl tags
                     payload: beginning of expected final payload."""
         for sp, rt, ri in zip(sps,routingtypes,routinginfo):
             res = sp.processMessage(m)
@@ -1126,10 +1213,16 @@
             else:
                 self.assertEquals(res[0], "EXIT")
                 self.assertEquals(res[1][0], rt)
-                self.assertEquals(res[1][1], ri)
+		self.assertEquals(res[1][1][20:], ri)
                 if appkey:
                     self.assertEquals(appkey, res[1][2])
-                self.assert_(res[1][3].startswith(payload))
+		
+		p = res[1][3]
+		if decoder is None:
+		    p = BuildMessage.decodeForwardPayload(p)
+		else:
+		    p = decoder(p, res[1][1])
+                self.assert_(p.startswith(payload))
                 break
 
     def test_successful(self):
@@ -1207,7 +1300,7 @@
         self.failUnlessRaises(ContentError, self.sp1.processMessage, m)
 
         # Duplicate reply blocks need to fail
-        reply,s = brb([self.server3], SMTP_TYPE, "fred@invalid")
+        reply,s,tag = brb([self.server3], SMTP_TYPE, "fred@invalid")
         m = brm("Y", [self.server2], reply)
         m2 = brm("Y", [self.server1], reply)
         q, (a,m) = self.sp2.processMessage(m)
@@ -1217,9 +1310,9 @@
 
         # Even duplicate secrets need to go.
         prng = AESCounterPRNG(" "*16)
-        reply1,s = brb([self.server1], SMTP_TYPE, "fred@invalid",0,prng)
+        reply1,s,t = brb([self.server1], SMTP_TYPE, "fred@invalid",0,prng)
         prng = AESCounterPRNG(" "*16)
-        reply2,s = brb([self.server2], MBOX_TYPE, "foo",0,prng)
+        reply2,s,t = brb([self.server2], MBOX_TYPE, "foo",0,prng)
         m = brm("Y", [self.server3], reply1)
         m2 = brm("Y", [self.server3], reply2)
         q, (a,m) = self.sp3.processMessage(m)
@@ -2523,7 +2616,11 @@
 #----------------------------------------------------------------------
 class ClientMainTests(unittest.TestCase):
     def testClientKeystore(self):
-	pass
+	import mixminion.ClientMain
+	dirname = mix_mktemp()
+	dc = mixminion.ClientMain.DirectoryCache(dirname)
+	dc.load()
+	
 
 #----------------------------------------------------------------------
 def testSuite():
@@ -2533,9 +2630,6 @@
 
     suite.addTest(tc(ClientMainTests))
     suite.addTest(tc(ServerMainTests))
-
-    if 0: return suite
-
     suite.addTest(tc(MiscTests))
     suite.addTest(tc(MinionlibCryptoTests))
     suite.addTest(tc(CryptoTests))