def buildReplyBlock(path, exitType, exitInfo, userKey, expiryTime=None, secretRNG=None): """Construct a 'state-carrying' 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' field of the final block's routing info. (See the spec for more info). path: a list of ServerInfo objects exitType,exitInfo: The address to deliver the final message. userKey: a string used to encrypt the seed. NOTE: We used to allow another kind of 'non-state-carrying' reply block that stored its secrets on disk, and used an arbitrary tag to determine which set of secrets to use. """ if secretRNG is None: secretRNG = Crypto.getCommonPRNG() # We need to pick the seed to generate our keys. To make the decoding # step a little faster, we find a seed such that H(seed|userKey|"Validate") # ends with 0. This way, we can detect whether we really have a reply # message with 99.6% probability. (Otherwise, we'd need to repeatedly # lioness-decrypt the payload in order to see whether the message was # a reply.) while 1: seed = _getRandomTag(secretRNG) if Crypto.sha1(seed + userKey + "Validate")[-1] == "\x00": break prng = Crypto.AESCounterPRNG(Crypto.sha1(seed + userKey + "Generate")[:16]) replyBlock, secrets, tag = _buildReplyBlockImpl(path, exitType, exitInfo, expiryTime, prng, seed) STATUS.log("GENERATED_SURB", formatBase64(tag)) return replyBlock
def _decodeStatelessReplyPayload(payload, tag, userKey): """Decode a (state-carrying) reply payload.""" # Reconstruct the secrets we used to generate the reply block (possibly # too many) seed = Crypto.sha1(tag + userKey + "Generate")[:16] prng = Crypto.AESCounterPRNG(seed) secrets = [prng.getBytes(SECRET_LEN) for _ in xrange(17)] return _decodeReplyPayload(payload, secrets, check=1)
def _decodeStatelessReplyPayload(payload, tag, userKey): """Decode a (state-carrying) reply payload.""" # Reconstruct the secrets we used to generate the reply block (possibly # too many) seed = Crypto.sha1(tag+userKey+"Generate")[:16] prng = Crypto.AESCounterPRNG(seed) secrets = [ prng.getBytes(SECRET_LEN) for _ in xrange(17) ] return _decodeReplyPayload(payload, secrets, check=1)
def buildReplyBlock(path, exitType, exitInfo, userKey, expiryTime=None, secretRNG=None): """Construct a 'state-carrying' 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' field of the final block's routing info. (See the spec for more info). path: a list of ServerInfo objects exitType,exitInfo: The address to deliver the final message. userKey: a string used to encrypt the seed. NOTE: We used to allow another kind of 'non-state-carrying' reply block that stored its secrets on disk, and used an arbitrary tag to determine which set of secrets to use. """ if secretRNG is None: secretRNG = Crypto.getCommonPRNG() # We need to pick the seed to generate our keys. To make the decoding # step a little faster, we find a seed such that H(seed|userKey|"Validate") # ends with 0. This way, we can detect whether we really have a reply # message with 99.6% probability. (Otherwise, we'd need to repeatedly # lioness-decrypt the payload in order to see whether the message was # a reply.) while 1: seed = _getRandomTag(secretRNG) if Crypto.sha1(seed+userKey+"Validate")[-1] == '\x00': break prng = Crypto.AESCounterPRNG(Crypto.sha1(seed+userKey+"Generate")[:16]) replyBlock, secrets, tag = _buildReplyBlockImpl(path, exitType, exitInfo, expiryTime, prng, seed) STATUS.log("GENERATED_SURB", formatBase64(tag)) return replyBlock
def processPacket(self, msg): """Given a 32K mixminion packet, processes it completely. Return one of: None [if the packet should be dropped.] a DeliveryPacket object a RelayedPacket object May raise CryptoError, ParseError, or ContentError if the packet is malformatted, misencrypted, unparseable, repeated, or otherwise unhandleable. WARNING: This implementation does nothing to prevent timing attacks: dropped packets, packets with bad digests, replayed packets, and exit packets are all processed faster than forwarded packets. You must prevent timing attacks elsewhere.""" # Break into headers and payload pkt = Packet.parsePacket(msg) header1 = Packet.parseHeader(pkt.header1) encSubh = header1[:Packet.ENC_SUBHEADER_LEN] header1 = header1[Packet.ENC_SUBHEADER_LEN:] assert len(header1) == Packet.HEADER_LEN - Packet.ENC_SUBHEADER_LEN assert len(header1) == (128*16) - 256 == 1792 # Try to decrypt the first subheader. Try each private key in # order. Only fail if all private keys fail. subh = None e = None self.lock.acquire() try: for pk, hashlog in self.privatekeys: try: subh = Crypto.pk_decrypt(encSubh, pk) break except Crypto.CryptoError, err: e = err finally: self.lock.release() if not subh: # Nobody managed to get us the first subheader. Raise the # most-recently-received error. raise e if len(subh) != Packet.MAX_SUBHEADER_LEN: raise ContentError("Bad length in RSA-encrypted part of subheader") subh = Packet.parseSubheader(subh) #may raise ParseError # Check the version: can we read it? if subh.major != Packet.MAJOR_NO or subh.minor != Packet.MINOR_NO: raise ContentError("Invalid protocol version") # Check the digest of all of header1 but the first subheader. if subh.digest != Crypto.sha1(header1): raise ContentError("Invalid digest") # Get ready to generate packet keys. keys = Crypto.Keyset(subh.secret) # Replay prevention replayhash = keys.get(Crypto.REPLAY_PREVENTION_MODE, Crypto.DIGEST_LEN) if hashlog.seenHash(replayhash): raise ContentError("Duplicate packet detected.") else: hashlog.logHash(replayhash) # If we're meant to drop, drop now. rt = subh.routingtype if rt == Packet.DROP_TYPE: return None # Prepare the key to decrypt the header in counter mode. We'll be # using this more than once. header_sec_key = Crypto.aes_key(keys.get(Crypto.HEADER_SECRET_MODE)) # Prepare key to generate padding junk_key = Crypto.aes_key(keys.get(Crypto.RANDOM_JUNK_MODE)) # Pad the rest of header 1 header1 += Crypto.prng(junk_key, Packet.OAEP_OVERHEAD + Packet.MIN_SUBHEADER_LEN + subh.routinglen) assert len(header1) == (Packet.HEADER_LEN - Packet.ENC_SUBHEADER_LEN + Packet.OAEP_OVERHEAD+Packet.MIN_SUBHEADER_LEN + subh.routinglen) assert len(header1) == 1792 + 42 + 42 + subh.routinglen == \ 1876 + subh.routinglen # Decrypt the rest of header 1, encrypting the padding. header1 = Crypto.ctr_crypt(header1, header_sec_key) # If the subheader says that we have extra routing info that didn't # fit in the RSA-encrypted part, get it now. overflowLength = subh.getOverflowLength() if overflowLength: subh.appendOverflow(header1[:overflowLength]) header1 = header1[overflowLength:] assert len(header1) == ( 1876 + subh.routinglen - max(0,subh.routinglen-Packet.MAX_ROUTING_INFO_LEN)) header1 = subh.underflow + header1 assert len(header1) == Packet.HEADER_LEN # Decrypt the payload. payload = Crypto.lioness_decrypt(pkt.payload, keys.getLionessKeys(Crypto.PAYLOAD_ENCRYPT_MODE)) # If we're an exit node, there's no need to process the headers # further. if rt >= Packet.MIN_EXIT_TYPE: return DeliveryPacket(rt, subh.getExitAddress(0), keys.get(Crypto.APPLICATION_KEY_MODE), payload) # If we're not an exit node, make sure that what we recognize our # routing type. if rt not in (Packet.SWAP_FWD_IPV4_TYPE, Packet.FWD_IPV4_TYPE, Packet.SWAP_FWD_HOST_TYPE, Packet.FWD_HOST_TYPE): raise ContentError("Unrecognized Mixminion routing type") # Decrypt header 2. header2 = Crypto.lioness_decrypt(pkt.header2, keys.getLionessKeys(Crypto.HEADER_ENCRYPT_MODE)) # If we're the swap node, (1) decrypt the payload with a hash of # header2... (2) decrypt header2 with a hash of the payload... # (3) and swap the headers. if Packet.typeIsSwap(rt): hkey = Crypto.lioness_keys_from_header(header2) payload = Crypto.lioness_decrypt(payload, hkey) hkey = Crypto.lioness_keys_from_payload(payload) header2 = Crypto.lioness_decrypt(header2, hkey) header1, header2 = header2, header1 # Build the address object for the next hop address = Packet.parseRelayInfoByType(rt, subh.routinginfo) # Construct the packet for the next hop. pkt = Packet.Packet(header1, header2, payload).pack() return RelayedPacket(address, pkt)
def _checkPayload(payload): "Return true iff the hash on the given payload seems valid" if ord(payload[0]) & 0x80: return payload[3:23] == Crypto.sha1(payload[23:]) else: return payload[2:22] == Crypto.sha1(payload[22:])
def _buildHeader(path, secrets, exitType, exitInfo, paddingPRNG): """Helper method to construct a single header. path: A sequence of serverinfo objects. secrets: A list of 16-byte strings to use as master-secrets for each of the subheaders. exitType: The routing type for the last node in the header exitInfo: The routing info for the last node in the header. (Must include 20-byte decoding tag, if any.) paddingPRNG: A pseudo-random number generator to generate padding """ assert len(path) == len(secrets) for info in path: if not info.supportsPacketVersion(): raise MixError("Server %s does not support any recognized packet format." % info.getNickname()) routing, sizes, totalSize = _getRouting(path, exitType, exitInfo) if totalSize > HEADER_LEN: raise MixError("Path cannot fit in header") # headerKey[i]==the AES key object node i will use to decrypt the header headerKeys = [Crypto.Keyset(secret).get(Crypto.HEADER_SECRET_MODE) for secret in secrets] # Length of padding needed for the header paddingLen = HEADER_LEN - totalSize # Calculate junk. # junkSeen[i]==the junk that node i will see, before it does any # encryption. Note that junkSeen[0]=="", because node 0 # sees no junk. junkSeen = [""] for secret, headerKey, size in zip(secrets, headerKeys, sizes): # Here we're calculating the junk that node i+1 will see. # # Node i+1 sees the junk that node i saw, plus the junk that i appends, # all encrypted by i. prngKey = Crypto.Keyset(secret).get(Crypto.RANDOM_JUNK_MODE) # newJunk is the junk that node i will append. (It's as long as # the data that i removes.) newJunk = Crypto.prng(prngKey, size) lastJunk = junkSeen[-1] nextJunk = lastJunk + newJunk # Before we encrypt the junk, we'll encrypt all the data, and # all the initial padding, but not the RSA-encrypted part. # This is equal to - 256 # + sum(size[current]....size[last]) # + paddingLen # This simplifies to: # startIdx = paddingLen - 256 + totalSize - len(lastJunk) startIdx = HEADER_LEN - ENC_SUBHEADER_LEN - len(lastJunk) nextJunk = Crypto.ctr_crypt(nextJunk, headerKey, startIdx) junkSeen.append(nextJunk) # We start with the padding. header = paddingPRNG.getBytes(paddingLen) # Now, we build the subheaders, iterating through the nodes backwards. for i in range(len(path) - 1, -1, -1): rt, ri = routing[i] # Create a subheader object for this node, but don't fill in the # digest until we've constructed the rest of the header. subhead = Subheader(MAJOR_NO, MINOR_NO, secrets[i], None, rt, ri) # placeholder for as-yet-uncalculated digest # Do we need to include some of the remaining header in the # RSA-encrypted portion? underflowLength = subhead.getUnderflowLength() if underflowLength > 0: underflow = header[:underflowLength] header = header[underflowLength:] else: underflow = "" # Do we need to spill some of the routing info out from the # RSA-encrypted portion? If so, prepend it. header = subhead.getOverflow() + header # Encrypt the symmetrically encrypted part of the header header = Crypto.ctr_crypt(header, headerKeys[i]) # What digest will the next server see? subhead.digest = Crypto.sha1(header + junkSeen[i]) # Encrypt the subheader, plus whatever portion of the previous header # underflows, into 'esh'. pubkey = path[i].getPacketKey() rsaPart = subhead.pack() + underflow esh = Crypto.pk_encrypt(rsaPart, pubkey) # Concatenate the asymmetric and symmetric parts, to get the next # header. header = esh + header return header
def decodePayload(payload, tag, key=None, userKeys=(), retNym=None): """Given a 28K payload and a 20-byte decoding tag, attempt to decode the original message. Returns either a SingletonPayload instance, a FragmentPayload instance, or None. key: an RSA key to decode encrypted forward messages, or None userKeys: a sequence of (name,key) tuples maping identity names to SURB keys. For backward compatibility, 'userKeys' may also be None (no SURBs known), a dict (from name to key), or a single key (implied identity is ""). retNym: If present, and if the payload was a reply, we call retNym.append(pseudonym). (For the default SURB identity, we append the empty string.) 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. """ if userKeys is None: userKeys = [] elif type(userKeys) is types.StringType: userKeys = [("", userKeys)] elif type(userKeys) is types.DictType: userKeys = userKeys.items() if len(payload) != PAYLOAD_LEN: raise MixError("Wrong payload length") if len(tag) not in (0, TAG_LEN): raise MixError("Wrong tag length: %s" % len(tag)) # If the payload already contains a valid checksum, it's a forward # message. if _checkPayload(payload): return parsePayload(payload) if not tag: return None # 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.) for name, userKey in userKeys: 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) if retNym is not None: retNym.append(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 # our RSA key. if key is not None: p = _decodeEncryptedForwardPayload(payload, tag, key) if p is not None: return p return None
def _checkPayload(payload): 'Return true iff the hash on the given payload seems valid' if (ord(payload[0]) & 0x80): return payload[3:23] == Crypto.sha1(payload[23:]) else: return payload[2:22] == Crypto.sha1(payload[22:])
def _buildHeader(path,secrets,exitType,exitInfo,paddingPRNG): """Helper method to construct a single header. path: A sequence of serverinfo objects. secrets: A list of 16-byte strings to use as master-secrets for each of the subheaders. exitType: The routing type for the last node in the header exitInfo: The routing info for the last node in the header. (Must include 20-byte decoding tag, if any.) paddingPRNG: A pseudo-random number generator to generate padding """ assert len(path) == len(secrets) for info in path: if not info.supportsPacketVersion(): raise MixError("Server %s does not support any recognized packet format."%info.getNickname()) routing, sizes, totalSize = _getRouting(path, exitType, exitInfo) if totalSize > HEADER_LEN: raise MixError("Path cannot fit in header") # headerKey[i]==the AES key object node i will use to decrypt the header headerKeys = [ Crypto.Keyset(secret).get(Crypto.HEADER_SECRET_MODE) for secret in secrets ] # Length of padding needed for the header paddingLen = HEADER_LEN - totalSize # Calculate junk. # junkSeen[i]==the junk that node i will see, before it does any # encryption. Note that junkSeen[0]=="", because node 0 # sees no junk. junkSeen = [""] for secret, headerKey, size in zip(secrets, headerKeys, sizes): # Here we're calculating the junk that node i+1 will see. # # Node i+1 sees the junk that node i saw, plus the junk that i appends, # all encrypted by i. prngKey = Crypto.Keyset(secret).get(Crypto.RANDOM_JUNK_MODE) # newJunk is the junk that node i will append. (It's as long as # the data that i removes.) newJunk = Crypto.prng(prngKey,size) lastJunk = junkSeen[-1] nextJunk = lastJunk + newJunk # Before we encrypt the junk, we'll encrypt all the data, and # all the initial padding, but not the RSA-encrypted part. # This is equal to - 256 # + sum(size[current]....size[last]) # + paddingLen # This simplifies to: #startIdx = paddingLen - 256 + totalSize - len(lastJunk) startIdx = HEADER_LEN - ENC_SUBHEADER_LEN - len(lastJunk) nextJunk = Crypto.ctr_crypt(nextJunk, headerKey, startIdx) junkSeen.append(nextJunk) # We start with the padding. header = paddingPRNG.getBytes(paddingLen) # Now, we build the subheaders, iterating through the nodes backwards. for i in range(len(path)-1, -1, -1): rt, ri = routing[i] # Create a subheader object for this node, but don't fill in the # digest until we've constructed the rest of the header. subhead = Subheader(MAJOR_NO, MINOR_NO, secrets[i], None, #placeholder for as-yet-uncalculated digest rt, ri) # Do we need to include some of the remaining header in the # RSA-encrypted portion? underflowLength = subhead.getUnderflowLength() if underflowLength > 0: underflow = header[:underflowLength] header = header[underflowLength:] else: underflow = "" # Do we need to spill some of the routing info out from the # RSA-encrypted portion? If so, prepend it. header = subhead.getOverflow() + header # Encrypt the symmetrically encrypted part of the header header = Crypto.ctr_crypt(header, headerKeys[i]) # What digest will the next server see? subhead.digest = Crypto.sha1(header+junkSeen[i]) # Encrypt the subheader, plus whatever portion of the previous header # underflows, into 'esh'. pubkey = path[i].getPacketKey() rsaPart = subhead.pack() + underflow esh = Crypto.pk_encrypt(rsaPart, pubkey) # Concatenate the asymmetric and symmetric parts, to get the next # header. header = esh + header return header
def decodePayload(payload, tag, key=None, userKeys=(), retNym=None): """Given a 28K payload and a 20-byte decoding tag, attempt to decode the original message. Returns either a SingletonPayload instance, a FragmentPayload instance, or None. key: an RSA key to decode encrypted forward messages, or None userKeys: a sequence of (name,key) tuples maping identity names to SURB keys. For backward compatibility, 'userKeys' may also be None (no SURBs known), a dict (from name to key), or a single key (implied identity is ""). retNym: If present, and if the payload was a reply, we call retNym.append(pseudonym). (For the default SURB identity, we append the empty string.) 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. """ if userKeys is None: userKeys = [] elif type(userKeys) is types.StringType: userKeys = [ ("", userKeys) ] elif type(userKeys) is types.DictType: userKeys = userKeys.items() if len(payload) != PAYLOAD_LEN: raise MixError("Wrong payload length") if len(tag) not in (0, TAG_LEN): raise MixError("Wrong tag length: %s"%len(tag)) # If the payload already contains a valid checksum, it's a forward # message. if _checkPayload(payload): return parsePayload(payload) if not tag: return None # 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.) for name,userKey in userKeys: 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) if retNym is not None: retNym.append(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 # our RSA key. if key is not None: p = _decodeEncryptedForwardPayload(payload, tag, key) if p is not None: return p return None