[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))