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