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

[minion-cvs] Many hacks, checkpointing.



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

Modified Files:
	BuildMessage.py ClientMain.py Main.py test.py 
Log Message:
Many hacks, checkpointing.

README, Main, ServerMain:
- Rename "mixminion server" to "mixminion server-start" for consistency.
  The old command is there, but deprecated.

BuildMessage, ClientMain:
- Die with a useful error message if the path is too long to fit the 
  address in the last hop.  (This can happen even if path_len < 32.)
- Remove dead code for "stateful" reply blocks.
- Use multiple SURB keys to detect which identity was used; resists
  George's SURB-swapping attack.

ClientMain:
- s/www.mixminion.net/mixminion.net/
- Beautify list-servers output a little
- Change key storage format to allow multiple keys to be stored with same
  password
- Avoid dumping binary messages to ttys unless --force is given.

Main:
- Don't print a stack trace when the user hits ctrl-c.

HashLog:
- Correct comment

ServerMain, EventStats, Modules, ServerConfig, Main, mixminiond.conf:
- Beginnings of code to track server statistics.



Index: BuildMessage.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/BuildMessage.py,v
retrieving revision 1.40
retrieving revision 1.41
diff -u -d -r1.40 -r1.41
--- BuildMessage.py	5 Mar 2003 21:21:20 -0000	1.40
+++ BuildMessage.py	26 Mar 2003 16:36:46 -0000	1.41
@@ -10,13 +10,13 @@
 import operator
 import mixminion.Crypto as Crypto
 from mixminion.Packet import *
-from mixminion.Common import MixError, MixFatalError, LOG
+from mixminion.Common import MixError, MixFatalError, LOG, UIError
 
 if sys.version_info[:3] < (2,2,0):
     import mixminion._zlibutil as zlibutil
 
 __all__ = ['buildForwardMessage', 'buildEncryptedMessage', 'buildReplyMessage',
-           'buildReplyBlock', 'decodePayload' ]
+           'buildReplyBlock', 'checkPathLength', 'decodePayload' ]
 
 def buildForwardMessage(payload, exitType, exitInfo, path1, path2,
                         paddingPRNG=None):
@@ -241,12 +241,35 @@
     return _buildReplyBlockImpl(path, exitType, exitInfo, expiryTime, prng,
                                 seed)[0]
 
+def checkPathLength(path1, path2, exitType, exitInfo, explicitSwap=0):
+    "XXXX DOCDOC"
+    err = 0
+    if path1 is not None:
+        try:
+            _getRouting(path1, SWAP_FWD_TYPE, path2[0].getRoutingInfo().pack())
+        except MixError:
+            err = 1
+    if exitType != DROP_TYPE and exitInfo is not None:
+        exitInfo += "X"*20
+    else:
+        exitInfo = ""
+    if err == 0:
+        try:
+            _getRouting(path2, exitType, exitInfo)
+        except MixError:
+            err = 2
+    if err and not explicitSwap:
+        raise UIError("Address and path will not fit in one header")
+    elif err:
+        raise UIError("Address and %s leg of path will not fit in one header",
+                      ["first", "second"][err-1])
+    
 #----------------------------------------------------------------------
 # MESSAGE DECODING
 
 def decodePayload(payload, tag, key=None,
-                  #storedKeys=None, # 'Stateful' reply blocks are disabled.
-                  userKey=None):
+                  userKey=None,
+                  userKeys={}):
     """Given a 28K payload and a 20-byte decoding tag, attempt to decode and
        decompress the original message.
 
@@ -256,8 +279,12 @@
        If we can successfully decrypt the payload, we return it.  If we
        might be able to decrypt the payload given more/different keys,
        we return None.  If the payload is corrupt, we raise MixError.
+
+       DOCDOC userKeys
     """
     # FFFF Take a list of keys?
+    if userKey and not userKeys:
+        userKeys = { "" : userKey }
 
     if len(payload) != PAYLOAD_LEN or len(tag) != TAG_LEN:
         raise MixError("Wrong payload or tag length")
@@ -267,26 +294,19 @@
     if _checkPayload(payload):
         return _decodeForwardPayload(payload)
 
-    # ('Stateful' reply blocks are disabled.)
-
-##    # If we have a list of keys associated with the tag, it's a reply message
-##    # using those keys.
-
-##     if storedKeys is not None:
-##      secrets = storedKeys.get(tag)
-##      if secrets is not None:
-##          del storedKeys[tag]
-##          return _decodeReplyPayload(payload, secrets)
-
     # If H(tag|userKey|"Validate") ends with 0, then the message _might_
     # be a reply message using H(tag|userKey|"Generate") as the seed for
     # its master secrets.  (There's a 1-in-256 chance that it isn't.)
-    if userKey is not None:
-        if Crypto.sha1(tag+userKey+"Validate")[-1] == '\x00':
-            try:
-                return _decodeStatelessReplyPayload(payload, tag, userKey)
-            except MixError:
-                pass
+    if userKeys:
+        for name,userKey in userKeys.items():
+            if Crypto.sha1(tag+userKey+"Validate")[-1] == '\x00':
+                try:
+                    p = _decodeStatelessReplyPayload(payload, tag, userKey)
+                    if name:
+                        LOG.info("Decoded reply message to identity: %r", name)
+                    return p
+                except MixError:
+                    pass
 
     # If we have an RSA key, and none of the above steps get us a good
     # payload, then we may as well try to decrypt the start of tag+key with
@@ -447,18 +467,7 @@
     if len(path) * ENC_SUBHEADER_LEN > HEADER_LEN:
         raise MixError("Too many nodes in path")
 
-    # Construct a list 'routing' of exitType, exitInfo.
-    routing = [ (FWD_TYPE, node.getRoutingInfo().pack()) for
-                node in path[1:] ]
-    routing.append((exitType, exitInfo))
-
-    # sizes[i] is size, in blocks, of subheaders for i.
-    sizes =[ getTotalBlocksForRoutingInfoLen(len(ri)) for _, ri in routing]
-
-    # totalSize is the total number of blocks.
-    totalSize = reduce(operator.add, sizes)
-    if totalSize * ENC_SUBHEADER_LEN > HEADER_LEN:
-        raise MixError("Routing info won't fit in header")
+    routing, sizes, totalSize = _getRouting(path, exitType, exitInfo)
 
     # headerKey[i]==the AES key object node i will use to decrypt the header
     headerKeys = [ Crypto.Keyset(secret).get(Crypto.HEADER_SECRET_MODE)
@@ -620,3 +629,19 @@
     'Return true iff the hash on the given payload seems valid'
     return payload[2:22] == Crypto.sha1(payload[22:])
 
+def _getRouting(path, exitType, exitInfo):
+    "XXXX DOCDOC"
+    # Construct a list 'routing' of exitType, exitInfo.
+    routing = [ (FWD_TYPE, node.getRoutingInfo().pack()) for
+                node in path[1:] ]
+    routing.append((exitType, exitInfo))
+
+    # sizes[i] is size, in blocks, of subheaders for i.
+    sizes =[ getTotalBlocksForRoutingInfoLen(len(ri)) for _, ri in routing]
+
+    # totalSize is the total number of blocks.
+    totalSize = reduce(operator.add, sizes)
+    if totalSize * ENC_SUBHEADER_LEN > HEADER_LEN:
+        raise MixError("Routing info won't fit in header")
+
+    return routing, sizes, totalSize

Index: ClientMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientMain.py,v
retrieving revision 1.66
retrieving revision 1.67
diff -u -d -r1.66 -r1.67
--- ClientMain.py	20 Feb 2003 16:57:38 -0000	1.66
+++ ClientMain.py	26 Mar 2003 16:36:46 -0000	1.67
@@ -26,7 +26,8 @@
 import mixminion.MMTPClient
 from mixminion.Common import IntervalSet, LOG, floorDiv, MixError, \
      MixFatalError, MixProtocolError, UIError, UsageError, ceilDiv, \
-     createPrivateDir, isSMTPMailbox, formatDate, formatFnameTime, formatTime,\
+     createPrivateDir, isPrintingAscii, \
+     isSMTPMailbox, formatDate, formatFnameTime, formatTime,\
      Lockfile, openUnique, previousMidnight, readPossiblyGzippedFile, \
      secureDelete, stringContains, succeedingMidnight
 from mixminion.Crypto import sha1, ctr_crypt, trng
@@ -37,7 +38,7 @@
      MBOX_TYPE, SMTP_TYPE, DROP_TYPE
 
 # FFFF This should be made configurable and adjustable.
-MIXMINION_DIRECTORY_URL = "http://www.mixminion.net/directory/directory.gz";
+MIXMINION_DIRECTORY_URL = "http://mixminion.net/directory/directory.gz";
 MIXMINION_DIRECTORY_FINGERPRINT = "CD80DD1B8BE7CA2E13C928D57499992D56579CCD"
 
 #----------------------------------------------------------------------
@@ -393,15 +394,15 @@
             return [ "No servers known" ]
         longestnamelen = max(map(len, nicknames))
         fmtlen = min(longestnamelen, 20)
-        format = "%"+str(fmtlen)+"s:"
+        nnFormat = "%"+str(fmtlen)+"s:"
         for n in nicknames:
             nnreal = self.byNickname[n][0][0].getNickname()
-            lines.append(format%nnreal)
+            lines.append(nnFormat%nnreal)
             for info, where in self.byNickname[n]:
                 caps = info.getCaps()
                 va = formatDate(info['Server']['Valid-After'])
                 vu = formatDate(info['Server']['Valid-Until'])
-                line = "   %15s (valid %s to %s)"%(" ".join(caps),va,vu)
+                line = "      [%s to %s] %s"%(va,vu," ".join(caps))
                 lines.append(line)
         return lines
 
@@ -704,12 +705,14 @@
        the path except that it support relay.
        """
     assert (not halfPath) or (nSwap==-1)
+    explicitSwap = nSwap is not None
     # First, find out what the exit node needs to be (or support).
     if address is None:
         routingType = None
+        routingInfo = None
         exitNode = None
     else:
-        routingType, _, exitNode = address.getRouting()
+        routingType, routingInfo, exitNode = address.getRouting()
         
     if exitNode:
         exitNode = directory.getServerInfo(exitNode, startAt, endAt)
@@ -750,8 +753,15 @@
         nSwap = ceilDiv(len(path),2)-1
 
     path1, path2 = path[:nSwap+1], path[nSwap+1:]
+    if not halfPath and len(path1)+len(path2) < 2:
+        raise UIError("Path is too short")
     if not halfPath and (not path1 or not path2):
         raise UIError("Each leg of the path must have at least 1 hop")
+
+    mixminion.BuildMessage.checkPathLength(path1, path2,
+                                           routingType, routingInfo,
+                                           explicitSwap)
+
     return path1, path2
 
 def parsePath(directory, config, path, address, nHops=None,
@@ -899,49 +909,54 @@
     return path2
     
 class ClientKeyring:
+    # XXXX004 testme
     """Class to manage storing encrypted keys for a client.  Right now, this
        is limited to a single SURB decryption key.  In the future, we may
        include more SURB keys, as well as end-to-end encryption keys.
     """
     ## Fields:
     # keyDir: The directory where we store our keys.
-    # surbKey: a 20-byte key for SURBs.
+    # keyring: DICT XXXX
+    # keyringPassword: The password for our encrypted keyfile
     ## Format:
     # We store keys in a file holding:
-    #  variable         [Key specific magic]        "SURBKEY0"
-    #   8               [8 bytes of salt]
-    #  keylen+20 bytes  ENCRYPTED DATA:KEY=sha1(salt+password+salt)[:16]
-    #                                  DATA=encrypted_key+sha1(data+salt+magic)
+    #  variable         [File specific magic]       "KEYRING1"
+    #  8                [8 bytes of salt]
+    #  variable         ENCRYPTED DATA:KEY=sha1(salt+password+salt)
+    #                                  DATA=encrypted_pickled_data+
+    #                                                   sha1(data+salt+magic)
+
     def __init__(self, keyDir):
         """Create a new ClientKeyring to store data in keyDir"""
         self.keyDir = keyDir
         createPrivateDir(self.keyDir)
-        self.surbKey = None
+        self.keyring = None
+        self.keyringPassword = None
 
-    # XXXX support multiple pseudoidentities.
-    def getSURBKey(self, create=0):
-        """Return the 20-byte SURB key.  If it has not already been loaded,
-           load it, asking the user for a password if needed.  If 'create' is
-           true and the key doesn't exist, ask the user for a new password
-           and create a new SURB key.  If 'create' is false and the key
-           doesn't exist, return None."""
-        if self.surbKey is not None:
-            return self.surbKey
-        fn = os.path.join(self.keyDir, "SURBKey")
-        self.surbKey = self._getKey(fn, magic="SURBKEY0", which="reply block",
-                                    create=create)
-        return self.surbKey
+    def getKey(self, keyid, create=0, createFn=None):
+        if self.keyring is None:
+            self.getKeyring(create=create)
+            if self.keyring is None:
+                return None
+        try:
+            return self.keyring[keyid]
+        except KeyError:
+            if not create:
+                return None
+            else:
+                LOG.info("Creating new key for identity %r", keyid)
+                key = createFn()
+                self.keyring[keyid] = key
+                self._saveKeyring()
+                return key
 
-    def _getKey(self, fn, magic, which, bytes=20, create=0):
-        """Helper: Load an arbitrary key from the keystore, from file 'fn'.
-           We expect the magic to be 'magic'; Error messages will describe the
-           key as the "'which' key" .  If create is true, and the key doesn't
-           exist, generate a new 'bytes'-byte key.  Else if the key doesn't
-           exist, return None.
-        """
-        
+    def getKeyring(self, create=0):
+        if self.keyring is not None:
+            return self.keyring
+        fn = os.path.join(self.keyDir, "keyring")
+        magic = "KEYRING1"
         if os.path.exists(fn):
-            # If the key exists, make sure the magic is correct.
+            # If the keyring exists, make sure the magic is correct.
             self._checkMagic(fn, magic)
             # ...then see if we can load it without a password...
             try:
@@ -950,20 +965,47 @@
                 pass
             # ...then ask the user for a password 'till it loads.
             while 1:
-                p = self._getPassword("Enter password for %s key:"%which)
+                p = self._getPassword("Enter password for keyring:")
                 try:
-                    return self._load(fn, magic, p)
-                except MixError, e:
-                    LOG.error("Cannot load %s key: %s", which, e)
+                    data = self._load(fn, magic, p)
+                    self.keyring = cPickle.loads(data)
+                    self.keyringPassword = p
+                    return self.keyring
+                except (MixError, cPickle.UnpicklingError), e:
+                    LOG.error("Cannot load keyring: %s", e)
         elif create:
             # If the key file doesn't exist, and 'create' is set, create it.
-            LOG.warn("No %s key found; generating.", which)
-            key = trng(bytes)
-            p = self._getNewPassword(which)
-            self._save(fn, key, magic, p)
-            return key
+            LOG.warn("No keyring found; generating.")
+            self.keyringPassword = self._getNewPassword("keyring")
+            self.keyring = {}
+            self._saveKeyring()
+            return self.keyring
         else:
-            return None
+            return {}
+
+    def _saveKeyring(self):
+        assert self.keyringPassword
+        fn = os.path.join(self.keyDir, "keyring")
+        LOG.trace("Saving keyring to %s", fn)
+        self._save(fn,
+                   cPickle.dumps(self.keyring,1),
+                   "KEYRING1", self.keyringPassword)
+
+    def getSURBKey(self, name="", create=0):
+        k = self.getKey("SURB-"+name,
+                        create=create, createFn=lambda: trng(20))
+        if len(k) != 20:
+            raise MixError("Bad length on SURB key")
+        return k
+
+    def getSURBKeys(self):
+        self.getKeyring(create=0)
+        if not self.keyring: return {}
+        r = {}
+        for k in self.keyring.keys():
+            if k.startswith("SURB-"):
+                r[k[5:]] = self.keyring[k]
+        return r
 
     def _checkMagic(self, fn, magic):
         """Make sure that the magic string on a given key file %s starts with
@@ -1021,10 +1063,12 @@
             nl = 1
         f.write(message)
         f.flush()
-        p = getpass.getpass("")
-        if nl:
-            f.write("\n")
-            f.flush()
+        try:
+            p = getpass.getpass("")
+        except KeyboardInterrupt:
+            if nl: print >>f
+            raise UIError("Interrupted")
+        if nl: print >>f
         return p
 
     def _getNewPassword(self, which):
@@ -1353,7 +1397,7 @@
             self.sendMessages([message], routing, noPool=forceNoPool)
 
 
-    def generateReplyBlock(self, address, servers, expiryTime=0):
+    def generateReplyBlock(self, address, servers, name="", expiryTime=0):
         """Generate an return a new ReplyBlock object.
             address -- the results of a parseAddress call
             servers -- lists of ServerInfos for the reply leg of the path.
@@ -1361,7 +1405,7 @@
                still be valid, and after which it should not be used.
         """
         #XXXX004 write unit tests
-        key = self.keys.getSURBKey(create=1)
+        key = self.keys.getSURBKey(name=name, create=1)
         exitType, exitInfo, _ = address.getRouting()
 
         block = mixminion.BuildMessage.buildReplyBlock(
@@ -1538,7 +1582,7 @@
             LOG.info("Message pooled")
         return handles
 
-    def decodeMessage(self, s, force=0):
+    def decodeMessage(self, s, force=0, isatty=0):
         """Given a string 's' containing one or more text-encoed messages,
            return a list containing the decoded messages.
            
@@ -1557,14 +1601,18 @@
             if not msg.isEncrypted():
                 results.append(msg.getContents())
             else:
-                surbKey = self.keys.getSURBKey(create=0)
+                surbKeys = self.keys.getSURBKeys()
                 p = mixminion.BuildMessage.decodePayload(msg.getContents(),
                                                          tag=msg.getTag(),
-                                                         userKey=surbKey)
+                                                         userKeys=surbKeys)
                 if p:
                     results.append(p)
                 else:
                     raise UIError("Unable to decode message")
+        if isatty and not force:
+            for p in results:
+                if not isPrintingAscii(p,allowISO=1):
+                    raise UIError("Not writing binary message to terminal: Use -F to do it anyway.")
         return results
 
 def parseAddress(s):
@@ -2298,6 +2346,8 @@
         # ???? Should we sometimes open this in text mode?
         out = open(outputFile, 'wb')
 
+    tty = os.isatty(out.fileno())
+
     if inputFile == '-':
         s = sys.stdin.read()
     else:
@@ -2308,7 +2358,7 @@
         except OSError, e:
             LOG.error("Could not read file %s: %s", inputFile, e)
     try:
-        res = client.decodeMessage(s, force=force)
+        res = client.decodeMessage(s, force=force, isatty=tty)
     except ParseError, e:
         raise UIError("Couldn't parse message: %s"%e)
         
@@ -2364,11 +2414,12 @@
     options, args = getopt.getopt(args, "hvf:D:t:H:P:o:bn:",
           ['help', 'verbose', 'config=', 'download-directory=',
            'to=', 'hops=', 'path=', 'lifetime=',
-           'output=', 'binary', 'count='])
+           'output=', 'binary', 'count=', 'identity='])
            
     outputFile = '-'
     binary = 0
     count = 1
+    identity = ""
     for o,v in options:
         if o in ('-o', '--output'):
             outputFile = v
@@ -2380,7 +2431,8 @@
             except ValueError:
                 print "ERROR: %s expects an integer" % o
                 sys.exit(1)
-            
+        elif o in ('--identity',):
+            identity = v
     try:
         parser = CLIArgumentParser(options, wantConfig=1, wantClient=1,
                                    wantLog=1, wantClientDirectory=1,
@@ -2412,7 +2464,8 @@
         out = open(outputFile, 'w')
 
     for i in xrange(count):
-        surb = client.generateReplyBlock(address, path1, parser.endTime)
+        surb = client.generateReplyBlock(address, path1, name=identity,
+                                         expiryTime=parser.endTime)
         if binary:
             out.write(surb.pack())
         else:

Index: Main.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Main.py,v
retrieving revision 1.36
retrieving revision 1.37
diff -u -d -r1.36 -r1.37
--- Main.py	20 Feb 2003 16:57:39 -0000	1.36
+++ Main.py	26 Mar 2003 16:36:46 -0000	1.37
@@ -128,16 +128,14 @@
     "inspect-surbs" :  ( 'mixminion.ClientMain', 'inspectSURBs' ),
     "flush" :          ( 'mixminion.ClientMain', 'flushPool' ),
     "inspect-pool" :   ( 'mixminion.ClientMain', 'listPool' ),
+    # XXXX Obsolete; use "server-start"; remove in 0.0.5
     "server" :         ( 'mixminion.server.ServerMain', 'runServer' ),
-    "start-server" :   ( 'mixminion.server.ServerMain', 'runServer' ),
-    # obsolete; use server-stop #XXXX004 remove.
-    "stop-server" :    ( 'mixminion.server.ServerMain', 'signalServer' ),
-    # obsolete; use server-reload #XXXX004 remove.
-    "reload-server" :  ( 'mixminion.server.ServerMain', 'signalServer' ),
+    "server-start" :   ( 'mixminion.server.ServerMain', 'runServer' ),
     "server-stop" :    ( 'mixminion.server.ServerMain', 'signalServer' ),
     "server-reload" :  ( 'mixminion.server.ServerMain', 'signalServer' ),
     "server-keygen" :  ( 'mixminion.server.ServerMain', 'runKeygen'),
     "server-DELKEYS" : ( 'mixminion.server.ServerMain', 'removeKeys'),
+    "server-stats" :   ( 'mixminion.server.ServerMain', 'printServerStats' ),
     "dir":             ( 'mixminion.directory.DirMain', 'main'),
 }
 
@@ -157,12 +155,13 @@
   "       generate-surb  [Generate a single-use reply block]\n"+
   "       inspect-surbs  [Describe a single-use reply block]\n"+
   "                               (For Servers)\n"+
-  "       server         [Begin running a Mixminion server]\n"+
+  "       server-start   [Begin running a Mixminion server]\n"+
   "       server-stop    [Halt a running Mixminion server]\n"+
   "       server-reload  [Make running Mixminion server reload its config\n"+
   "                        (Not implemented yet; only restarts logging.)]\n"+
   "       server-keygen  [Generate keys for a Mixminion server]\n"+
   "       server-DELKEYS [Remove generated keys for a Mixminion server]\n"+
+  "       server-stats   [XXXX]\n"+
   "                             (For Developers)\n"+
   "       dir            [Administration for server directories]\n"+
   "       unittests      [Run the mixminion unit tests]\n"+
@@ -220,6 +219,8 @@
         func(commandStr, ["--help"])
     except uiErrorClass, e:
         e.dumpAndExit()
+    except KeyboardInterrupt:
+        print "Interrupted."
 
 if __name__ == '__main__':
     main(sys.argv)

Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.94
retrieving revision 1.95
diff -u -d -r1.94 -r1.95
--- test.py	20 Feb 2003 16:57:40 -0000	1.94
+++ test.py	26 Mar 2003 16:36:46 -0000	1.95
@@ -5261,6 +5261,10 @@
         parseFails("0x9999") # No data
         parseFails("0xFEEEF:zymurgy") # Hex literal out of range
 
+    def testClientKeyring(self):
+        keydir = mix_mktemp()
+        keyring = mixminion.ClientMain.ClientKeyring(keyring)
+
     def testMixminionClient(self):
         # Create and configure a MixminionClient object...
         parseAddress = mixminion.ClientMain.parseAddress