def _decodeEncryptedForwardPayload(payload, tag, key): """Helper function: decode an encrypted forward payload. Return values are the same as decodePayload. payload: the payload to decode tag: the decoding tag key: the RSA key of the payload's recipient.""" assert len(tag) == TAG_LEN assert len(payload) == PAYLOAD_LEN # Given an N-byte RSA key, the first N bytes of tag+payload will be # encrypted with RSA, and the rest with a lioness key given in the # first N. Try decrypting... 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():] k = Crypto.Keyset(rsaPart[:SECRET_LEN]).getLionessKeys( Crypto.END_TO_END_ENCRYPT_MODE) rest = rsaPart[SECRET_LEN:] + Crypto.lioness_decrypt(rest, k) # ... and then, check the checksum and continue. if not _checkPayload(rest): raise MixError("Invalid checksum on encrypted forward payload") return parsePayload(rest)
def buildEncryptedForwardPacket(payload, exitType, exitInfo, path1, path2, key, paddingPRNG=None, secretRNG=None): """Construct a forward message encrypted with the public key of a given user. payload: The payload to deliver. Must be 28K-42b long. exitType: The routing type for the final node. (2 bytes, >=0x100) 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 key: Public key of this message's recipient. paddingPRNG: random number generator used to generate padding. If None, a new PRNG is initialized. """ if paddingPRNG is None: paddingPRNG = Crypto.getCommonPRNG() if secretRNG is None: secretRNG = paddingPRNG LOG.debug("Encoding encrypted forward message for %s-byte payload", len(payload)) LOG.debug(" Using path %s/%s", [s.getNickname() for s in path1], [s.getNickname() for s in path2]) LOG.debug(" Delivering to %04x:%r", exitType, exitInfo) # (For encrypted-forward messages, we have overhead for OAEP padding # and the session key, but we save 20 bytes by spilling into the tag.) assert len(payload) == PAYLOAD_LEN - ENC_FWD_OVERHEAD # Generate the session key, and prepend it to the payload. sessionKey = secretRNG.getBytes(SECRET_LEN) payload = sessionKey+payload # We'll encrypt the first part of the new payload with RSA, and the # second half with Lioness, based on the session key. rsaDataLen = key.get_modulus_bytes()-OAEP_OVERHEAD rsaPart = payload[:rsaDataLen] lionessPart = payload[rsaDataLen:] # RSA encryption: To avoid leaking information about our RSA modulus, # we keep trying to encrypt until the MSBit of our encrypted value is # zero. while 1: encrypted = Crypto.pk_encrypt(rsaPart, key) if not (ord(encrypted[0]) & 0x80): break # Lioness encryption. k= Crypto.Keyset(sessionKey).getLionessKeys(Crypto.END_TO_END_ENCRYPT_MODE) lionessPart = Crypto.lioness_encrypt(lionessPart, k) # Now we re-divide the payload into the part that goes into the tag, and # the 28K of the payload proper... payload = encrypted + lionessPart tag = payload[:TAG_LEN] payload = payload[TAG_LEN:] exitInfo = tag + exitInfo assert len(payload) == 28*1024 # And now, we can finally build the message. return _buildPacket(payload, exitType, exitInfo, path1, path2,paddingPRNG)
def _constructMessage(secrets1, secrets2, header1, header2, payload): """Helper method: Builds a message, given both headers, all known secrets, and the padded payload. If using a reply block for header2, secrets2 should be null. """ assert len(payload) == PAYLOAD_LEN assert len(header1) == len(header2) == HEADER_LEN if secrets2: # (Copy secrets2 so we don't reverse the original) secrets2 = secrets2[:] # If we're not using a reply block, encrypt the payload for # each key in the second path, in reverse order. secrets2.reverse() for secret in secrets2: ks = Crypto.Keyset(secret) key = ks.getLionessKeys(Crypto.PAYLOAD_ENCRYPT_MODE) payload = Crypto.lioness_encrypt(payload, key) # Encrypt header2 with a hash of the payload. key = Crypto.lioness_keys_from_payload(payload) header2 = Crypto.lioness_encrypt(header2, key) # Encrypt payload with a hash of header2. Now tagging either will make # both unrecoverable. key = Crypto.lioness_keys_from_header(header2) payload = Crypto.lioness_encrypt(payload, key) # Copy secrets1 so we don't reverse the original. secrets1 = secrets1[:] # Now, encrypt header2 and the payload for each node in path1, reversed. secrets1.reverse() for secret in secrets1: ks = Crypto.Keyset(secret) hkey = ks.getLionessKeys(Crypto.HEADER_ENCRYPT_MODE) pkey = ks.getLionessKeys(Crypto.PAYLOAD_ENCRYPT_MODE) header2 = Crypto.lioness_encrypt(header2,hkey) payload = Crypto.lioness_encrypt(payload,pkey) return Packet(header1, header2, payload).pack()
def _decodeReplyPayload(payload, secrets, check=0): """Helper function: decode a reply payload, given a known list of packet master secrets. If 'check' is true, then 'secrets' may be overlong. Return values are the same as decodePayload. [secrets must be in _reverse_ order] """ # Reverse the 'decrypt' operations of the reply mixes, and the initial # 'decrypt' of the originating user... for sec in secrets: k = Crypto.Keyset(sec).getLionessKeys(Crypto.PAYLOAD_ENCRYPT_MODE) payload = Crypto.lioness_encrypt(payload, k) if check and _checkPayload(payload): return parsePayload(payload) # If 'check' is false, then we might still have a good payload. If # 'check' is true, we don't. if check or not _checkPayload(payload): raise MixError("Invalid checksum on reply payload") return parsePayload(payload)
def buildReplyPacket(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. 'payload' must be exactly 28K long. """ if paddingPRNG is None: paddingPRNG = Crypto.getCommonPRNG() LOG.debug("Encoding reply message for %s-byte payload", len(payload)) LOG.debug(" Using path %s/??",[s.getNickname() for s in path1]) assert len(payload) == PAYLOAD_LEN # Encrypt the payload so that it won't appear as plaintext to the # crossover note. (We use 'decrypt' so that the message recipient can # simply use 'encrypt' to reverse _all_ the steps of the reply path.) k = Crypto.Keyset(replyBlock.encryptionKey).getLionessKeys( Crypto.PAYLOAD_ENCRYPT_MODE) payload = Crypto.lioness_decrypt(payload, k) return _buildPacket(payload, None, None, path1=path1, path2=replyBlock)
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 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)