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

[minion-cvs] Start work on client reply block support



Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.mit.edu:/tmp/cvs-serv21913/lib/mixminion

Modified Files:
	ClientMain.py Packet.py test.py 
Log Message:
Start work on client reply block support

Index: ClientMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientMain.py,v
retrieving revision 1.44
retrieving revision 1.45
diff -u -d -r1.44 -r1.45
--- ClientMain.py	17 Jan 2003 06:18:06 -0000	1.44
+++ ClientMain.py	4 Feb 2003 02:38:23 -0000	1.45
@@ -21,6 +21,7 @@
 
 import cPickle
 import getopt
+import getpass
 import os
 import socket
 import stat
@@ -35,6 +36,7 @@
      MixFatalError, ceilDiv, createPrivateDir, isSMTPMailbox, formatDate, \
      formatFnameTime, formatTime, openUnique, previousMidnight, \
      readPossiblyGzippedFile
+from mixminion.Crypto import sha1, ctr_crypt, trng
 
 from mixminion.Config import ClientConfig, ConfigError
 from mixminion.ServerInfo import ServerInfo, ServerDirectory
@@ -766,6 +768,79 @@
     return resolvePath(keystore, address, enterPath, exitPath,
                        myNHops, myNSwap, startAt, endAt)
 
+class ClientKeyring:
+    "DOCDOC"
+    def __init__(self, keyDir):
+        self.keyDir = keyDir
+        createPrivateDir(self.keyDir)
+        self.surbKey = None
+
+    def getSURBKey(self):
+        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")
+        return self.surbKey
+
+    def _getKey(self, fn, magic, which, bytes=20):
+        if os.path.exists(fn):
+            self._checkMagic(fn, magic)
+            while 1:
+                p = self._getPassword(which)
+                try:
+                    return self._load(fn, magic, p)
+                except MixError, e:
+                    LOG.error("Cannot load key", e)
+        else:
+            LOG.warn("No %s key found; generating.", which)
+            key = trng(bytes)
+            p = self._getNewPassword(which)
+            self._save(fn, key, magic, p)
+            return key
+
+    def _checkMagic(fn, magic):
+        f = open(rn, 'rb')
+        s = f.read()
+        f.close()
+        if not s.startswith(magic):
+            raise MixError("Invalid magic on key file")
+
+    def _save(self, fn, data, magic, password):
+        # File holds magic, enc(sha1(password)[:16],data+sha1(data+magic))
+        # XXXX Gosh, that's a lousy key scheme.
+        f = open(fn, 'wb')
+        f.write(magic)
+        f.write(ctr_crypt(data+sha1(data+magic), sha1(password)[:16]))
+        f.close()
+    
+    def _load(self, fn, magic, password):
+        f = open(fn, 'rb')
+        s = f.read()
+        f.close()
+        if not s.startswith(magic):
+            raise MixError("Invalid key file")
+        s = s[len(magic):]
+        s = ctr_crypt(s, sha1(password)[:16])
+        data, hash = s[:-20], s[-20:]
+        if hash != sha1(data+magic):
+            raise MixError("Incorrect password")
+        return data
+        
+    def _getPassword(self, which):
+        s = "Enter password for %s:"%which
+        p = getpass.getpass(s)
+        return p
+
+    def _getNewPassword(self, which):
+        s1 = "Enter new password for %s:"%which
+        s2 = "Verify password:".rjust(len(s1))
+        while 1:
+            p1 = getpass.getpass(s1)
+            p2 = getpass.getpass(s2)
+            if p1 == p2:
+                return p1
+            print "Passwords do not match."
+
 def installDefaultConfig(fname):
     """Create a default, 'fail-safe' configuration in a given file"""
     LOG.warn("No configuration file found. Installing default file in %s",
@@ -802,6 +877,8 @@
     ## Fields:
     # config: The ClientConfig object with the current configuration
     # prng: A pseudo-random number generator for padding and path selection
+    # keyDir: DOCDOC
+    # surbKey: DOCDOC
     def __init__(self, conf):
         """Create a new MixminionClient with a given configuration"""
         self.config = conf
@@ -809,6 +886,8 @@
         # Make directories
         userdir = os.path.expanduser(self.config['User']['UserDir'])
         createPrivateDir(userdir)
+        keyDir = os.path.join(userdir, "keys")
+        self.keys = ClientKeyring(keyDir)
 
         # Initialize PRNG
         self.prng = mixminion.Crypto.getCommonPRNG()
@@ -826,15 +905,24 @@
 
         self.sendMessages([message], firstHop)
 
+    def generateReplyBlock(self, address, servers, expiryTime=0):
+        #DOCDOC
+        key = self.keys.getSURBKey()
+        exitType, exitInfo, _ = address.getRouting()
+        
+        block = mixminion.BuildMessage.buildReplyBlock(
+            servers, exitType, exitInfo, key, expiryTime)
+
+        return block
+
     def generateForwardMessage(self, address, payload, servers1, servers2):
         """Generate a forward message, but do not send it.  Returns
            a tuple of (the message body, a ServerInfo for the first hop.)
 
             address -- the results of a parseAddress call
-            payload -- the contents of the message to send
+            payload -- the contents of the message to send  (None for DROP
+              messages)
             path1,path2 -- lists of servers.
-
-            DOCDOC payload == None.
             """
 
         routingType, routingInfo, _ = address.getRouting()

Index: Packet.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Packet.py,v
retrieving revision 1.23
retrieving revision 1.24
diff -u -d -r1.23 -r1.24
--- Packet.py	17 Jan 2003 06:18:06 -0000	1.23
+++ Packet.py	4 Feb 2003 02:38:23 -0000	1.24
@@ -9,23 +9,27 @@
    packets, see BuildMessage.py.  For functions that handle
    server-side processing of packets, see PacketHandler.py."""
 
-__all__ = [ 'ParseError', 'Message', 'Header', 'Subheader',
-            'parseMessage', 'parseHeader', 'parseSubheader',
+__all__ = [ 'ParseError', 'Message', 'Header', 'Subheader', 'parseMessage',
+            'parseHeader', 'parseSubheader',
             'getTotalBlocksForRoutingInfoLen', 'parsePayload',
-            'SingletonPayload', 'FragmentPayload', 'ReplyBlock',
-            'IPV4Info', 'SMTPInfo', 'MBOXInfo', 'parseIPV4Info',
-            'parseSMTPInfo', 'parseMBOXInfo', 'ReplyBlock',
-            'parseReplyBlock', 'ENC_SUBHEADER_LEN', 'HEADER_LEN',
+            'SingletonPayload', 'FragmentPayload', 'ReplyBlock', 'IPV4Info',
+            'SMTPInfo', 'MBOXInfo', 'parseIPV4Info', 'parseSMTPInfo',
+            'parseMBOXInfo', 'ReplyBlock', 'parseReplyBlock',
+            'parseTextReplyBlocks', 'ENC_SUBHEADER_LEN', 'HEADER_LEN',
             'PAYLOAD_LEN', 'MAJOR_NO', 'MINOR_NO', 'SECRET_LEN', 'TAG_LEN',
             'SINGLETON_PAYLOAD_OVERHEAD', 'OAEP_OVERHEAD',
-            'FRAGMENT_PAYLOAD_OVERHEAD', 'ENC_FWD_OVERHEAD',
-            'DROP_TYPE', 'FWD_TYPE', 'SWAP_FWD_TYPE',
-            'SMTP_TYPE', 'MBOX_TYPE', 'MIN_EXIT_TYPE'
-]
+            'FRAGMENT_PAYLOAD_OVERHEAD', 'ENC_FWD_OVERHEAD', 'DROP_TYPE',
+            'FWD_TYPE', 'SWAP_FWD_TYPE', 'SMTP_TYPE', 'MBOX_TYPE',
+            'MIN_EXIT_TYPE'
+          ]
 
+import base64
+import binascii
+import re
 import struct
 from socket import inet_ntoa, inet_aton
-from mixminion.Common import MixError, floorDiv, isSMTPMailbox
+import mixminion.BuildMessage
+from mixminion.Common import MixError, floorDiv, isSMTPMailbox, LOG
 
 # Major and minor number for the understood packet format.
 MAJOR_NO, MINOR_NO = 0,1  #XXXX003 Bump minor_no for 0.0.3
@@ -392,9 +396,33 @@
 #   routingInfo for the last server.
 RB_UNPACK_PATTERN = "!4sBBL%dsHH%ss" % (HEADER_LEN, SECRET_LEN)
 MIN_RB_LEN = 30+HEADER_LEN
+RB_TEXT_START = "======= BEGIN TYPE III REPLY BLOCK ========"
+RB_TEXT_END   = "======== END TYPE III REPLY BLOCK ========="
+RB_TEXT_RE = re.compile(RB_TEXT_START+
+                        r'[\r\n]+Version: (\d+.\d+)\s*[\r\n]+(.*)[\r\n]+'+
+                        RB_TEXT_END, re.M) 
+
+def parseTextReplyBlocks(s):
+    """DOCDOC"""
+    idx = 0
+    blocks = []
+    while 1:
+        idx = s.find(RB_TEXT_START, idx)
+        if idx == -1:
+            break
+        m = RB_TEXT_RE.match(s, idx)
+        if not m:
+            raise ParseError("Misformatted reply block")
+        version, text = m.group(1), m.group(2)
+        if version != '0.1':
+            LOG.warn("Unrecognized reply block version: %s", version)
+        val = binascii.a2b_base64(text)
+        blocks.append(parseReplyBlock(val))
+        idx = m.end()
+    return blocks
 
 def parseReplyBlock(s):
-    """Return a new ReplyBlock object for an encoded reply block"""
+    """Return a new ReplyBlock object for an encoded reply block."""        
     if len(s) < MIN_RB_LEN:
         raise ParseError("Reply block too short")
     try:
@@ -435,6 +463,12 @@
                            len(self.routingInfo), self.routingType,
                            self.encryptionKey) + self.routingInfo
 
+    def packAsText(self):
+        text = binascii.b2a_base64(self.pack())
+        if not text.endswith("\n"):
+            text += "\n"
+        return "%s\nVersion: 0.1\n%s%s\n"%(RB_TEXT_START,text,RB_TEXT_END)
+    
 #----------------------------------------------------------------------
 # Routing info
 
@@ -513,3 +547,113 @@
     def pack(self):
         """Return the external representation of this routing info."""
         return self.user
+
+#----------------------------------------------------------------------
+# Ascii-encoded packets
+
+MESSAGE_START_LINE = "======= TYPE III ANONYMOUS MESSAGE BEGINS ========"
+MESSAGE_END_LINE   = "======== TYPE III ANONYMOUS MESSAGE ENDS ========="
+_FIRST_LINE_RE = re.compile(r'''^Decoding-handle:\s(.*)\r*\n|
+                                 Message-type:\s(.*)\r*\n''', re.X+re.S)
+_LINE_RE = re.compile(r'[^\r\n]+\r*\n', re.S)
+
+def _nextLine(s, idx):
+    m = _LINE_RE.match(s)
+    if m is None:
+        return len(s)
+    else:
+        return m.end()
+
+def getMessageContents(msg,force=0,idx=0):
+    """ Returns
+            ( 'TXT'|'ENC'|'LONG'|'BIN', tag|None, message, end-idx )
+    """
+    idx = msg.find(MESSAGE_START_LINE)
+    if idx < 0:
+        raise ParseError("No begin line found")
+    endIdx = msg.find(MESSAGE_END_LINE, idx)
+    if endIdx < 0:
+        raise ParseError("No end line found")
+    idx = _nextLine(msg, idx)
+    firstLine = msg[idx:_nextLine(msg, idx)]
+    m = _FIRST_LINE_RE.match(firstLine)
+    if m is None:
+        msgType = 'TXT'
+    elif m.group(1):
+        ascTag = m.group(1)
+        msgType = "ENC" #XXXX003 refactor
+        idx = firstLine
+    elif m.group(2):
+        if m.group(2) == 'overcompressed':
+            msgType = 'LONG' #XXXX003 refactor
+        elif m.group(2) == 'binary':
+            msgType = 'BIN' #XXXX003 refactor
+        else:
+            raise ParseError("Unknown message type: %r"%m.group(2))
+        idx = firstLine
+
+    msg = msg[idx:endIdx]
+    endIdx = _nextLine(endIdx)
+
+    if msgType == 'TXT':
+        return 'TXT', None, msg, endIdx
+
+    msg = binascii.a2b_base64(msg) #XXXX May raise
+    if msgType == 'BIN':
+        return 'BIN', None, msg, endIdx
+    elif msgType == 'LONG':
+        if force:
+            msg = mixminion.BuildMessage.uncompressData(msg) #XXXX may raise
+        return 'LONG', None, msg, endIdx
+    elif msgType == 'ENC':
+        tag = binascii.a2b_base64(ascTag)
+        return 'ENC', tag, msg, endIdx
+    else:
+        raise MixFatalError("unreached")
+
+class AsciiEncodedMessage:
+    def __init__(self, contents, messageType, tag=None):
+        assert messageType in ('TXT', 'ENC', 'LONG', 'BIN')
+        assert tag is None or (messageType == 'ENC' and len(tag) == 20)
+        self.contents = contents
+        self.messageType = messageType
+        self.tag = tag
+    def isBinary(self):
+        return self.messageType == 'BIN'
+    def isText(self):
+        return self.messageType == 'TXT'
+    def isEncrypted(self):
+        return self.messageType == 'ENC'
+    def isOvercompressed(self):
+        return self.messageType == 'LONG'
+    def getContents(self):
+        return self.contents
+    def getTag(self):
+        return self.tag
+    def pack(self):
+        c = self.contents
+        preNL = ""
+
+        if self.messageType != 'TXT':
+            c = base64.encodestring(c)
+        else:
+            if (c.startswith("Decoding-handle:") or
+                c.startswith("Message-type:")):
+                preNL = "\n"
+                
+        preNL = postNL = ""
+        if self.messageType == 'TXT':
+            tagLine = ""
+        elif self.messageType == 'ENC':
+            ascTag = binascii.b2a_base64(self.tag).strip()
+            tagLine = "Decoding-handle: %s\n" % ascTag
+        elif self.messageType == 'LONG':
+            tagLine = "Message-type: overcompressed\n"
+        elif self.messageType == 'BIN':
+            tagLine = "Message-type: binary\n"
+
+        if c[-1] != '\n':
+            postNL = "\n"
+
+        return "%s\n%s%s%s%s%s\n" % (
+            MESSAGE_START_LINE, tagLine, preNL, c, postNL, MESSAGE_END_LINE)

Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.75
retrieving revision 1.76
diff -u -d -r1.75 -r1.76
--- test.py	17 Jan 2003 06:18:06 -0000	1.75
+++ test.py	4 Feb 2003 02:38:23 -0000	1.76
@@ -18,6 +18,7 @@
 import gzip
 import os
 import re
+import socket
 import stat
 import sys
 import threading
@@ -1688,7 +1689,7 @@
         ## Stateless replies
         reply = brb([self.server3, self.server1, self.server2,
                       self.server1, self.server3], MBOX_TYPE,
-                     "fred", "Tyrone Slothrop", 0)
+                     "fred", "Tyrone Slothrop", 3)
 
         sec,(loc,), _ = self.do_header_test(reply.header, pks_1, None,
                             (FWD_TYPE,FWD_TYPE,FWD_TYPE,FWD_TYPE,MBOX_TYPE),
@@ -1696,6 +1697,32 @@
 
         self.assertEquals(loc[20:], "fred")
 
+        # (Test reply block formats)
+        self.assertEquals(reply.timestamp, 3)
+        self.assertEquals(reply.routingType, SWAP_FWD_TYPE)
+        self.assertEquals(reply.routingInfo,
+                          self.server3.getRoutingInfo().pack())
+        self.assertEquals(reply.pack(),
+                          "SURB\x00\x01\x00\x00\x00\x03"+reply.header+
+                         "\x00"+chr(len(self.server3.getRoutingInfo().pack()))+
+                          "\x00\x02"+reply.encryptionKey+
+                          self.server3.getRoutingInfo().pack())
+        self.assertEquals(reply.pack(), parseReplyBlock(reply.pack()).pack())
+        txt = reply.packAsText()
+        self.assert_(txt.startswith(
+            "======= BEGIN TYPE III REPLY BLOCK ========\nVersion: 0.1\n"))
+        self.assert_(txt.endswith(
+            "\n======== END TYPE III REPLY BLOCK =========\n"))
+        parsed = parseTextReplyBlocks(txt)
+        self.assertEquals(1, len(parsed))
+        self.assertEquals(reply.pack(), parsed[0].pack())
+        parsed2 = parseTextReplyBlocks((txt+"   9999 \n")*2)
+        self.assertEquals(2, len(parsed2))
+        self.assertEquals(reply.pack(), parsed2[1].pack())
+
+        #XXXX003 test failing cases for parseTextReplyBlocks
+        
+        # Test decoding
         seed = loc[:20]
         prng = AESCounterPRNG(sha1(seed+"Tyrone SlothropGenerate")[:16])
         sec.reverse()
@@ -2784,9 +2811,24 @@
         t.join()
 
     def testStallingTransmission(self):
+        def threadfn(pausing):
+            # helper fn to run in a different thread: bind a socket,
+            # but don't listen.
+            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+            sock.bind(("127.0.0.1", TEST_PORT))
+            while pausing[0] > 0:
+                time.sleep(.1)
+                pausing[0] -= .1
+            time.sleep(2)
+            sock.close()
+        pausing = [3]
+        t = threading.Thread(None, threadfn, args=(pausing,))
+        t.start()
+        
         now = time.time()
         try:
-            mixminion.MMTPClient.sendMessages("0.0.0.1",
+            mixminion.MMTPClient.sendMessages("127.0.0.1",
                                               #Is there a better IP????
                                               TEST_PORT, "Z"*20, ["JUNK"],
                                               connectTimeout=1)
@@ -2795,6 +2837,8 @@
             pass
         passed = time.time() - now
         self.assert_(passed < 2)
+        pausing[0] = 0
+        t.join()
 
     def _testNonblockingTransmission(self):
         server, listener, messagesIn, keyid = _getMMTPServer()
@@ -3842,10 +3886,10 @@
         ####
         # Tests escapeMessageForEmail
         self.assert_(stringContains(eme(FDPFast('plain',message)), message))
-        expect = "BEGINS ========\n"+\
+        expect = "BEGINS ========\nMessage-type: binary\n"+\
                  base64.encodestring(binmessage)+"====="
         self.assert_(stringContains(eme(FDPFast('plain',binmessage)), expect))
-        expect = "BEGINS ========\nDecoding handle: "+\
+        expect = "BEGINS ========\nDecoding-handle: "+\
                  base64.encodestring(tag)+\
                  base64.encodestring(binmessage)+"====="
         self.assert_(stringContains(eme(FDPFast('enc',binmessage,tag)),
@@ -3875,7 +3919,7 @@
 message encrypted to you; or 3) junk.
 
 ======= TYPE III ANONYMOUS MESSAGE BEGINS ========
-Decoding handle: eHh4eHh4eHh4eHh4eHh4eHh4eHg=
+Decoding-handle: eHh4eHh4eHh4eHh4eHh4eHh4eHg=
 7/rOqx76yt7v+s6rHvrK3u/6zqse+sre7/rOqx76yt7v+s6rHvrK3u/6zqse+sre7/rOqx76yt7v
 +s6rHvrK3u/6zqse+sre7/rOqx76yt7v+s6rHvrK3u/6zqse+sre7/rOqx76yt7v+s6rHvrK3u/6
 zqse+sre7/rOqx76yt7v+s6rHvrK3u/6zqse+sre7/rOqx76yt7v+s6rHvrK3g==
@@ -5163,7 +5207,7 @@
     tc = loader.loadTestsFromTestCase
 
     if 0:
-        suite.addTest(tc(MMTPTests))
+        suite.addTest(tc(BuildMessageTests))
         return suite
 
     suite.addTest(tc(MiscTests))