diff --git a/pupy/network/lib/__init__.py b/pupy/network/lib/__init__.py index aad25618..a150c325 100644 --- a/pupy/network/lib/__init__.py +++ b/pupy/network/lib/__init__.py @@ -93,9 +93,3 @@ except Exception as e: ECMTransportServer = None ECMTransportClient = None -try: - from .transports.scramblesuit.scramblesuit import ScrambleSuitClient, ScrambleSuitServer -except Exception as e: - logger.exception('Transport scramblesuit disabled: %s', e) - ScrambleSuitClient = None - ScrambleSuitServer = None diff --git a/pupy/network/lib/transports/cryptoutils/aes.py b/pupy/network/lib/transports/cryptoutils/aes.py index 8b8b4d69..b385a9ee 100644 --- a/pupy/network/lib/transports/cryptoutils/aes.py +++ b/pupy/network/lib/transports/cryptoutils/aes.py @@ -18,6 +18,7 @@ import sys if sys.version_info.major > 2: xrange = range + long = int def to_byte(x): return bytes((x,)) diff --git a/pupy/network/lib/transports/scramblesuit/__init__.py b/pupy/network/lib/transports/scramblesuit/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pupy/network/lib/transports/scramblesuit/const.py b/pupy/network/lib/transports/scramblesuit/const.py deleted file mode 100644 index 88f0588e..00000000 --- a/pupy/network/lib/transports/scramblesuit/const.py +++ /dev/null @@ -1,118 +0,0 @@ -""" -This module defines constant values for the ScrambleSuit protocol. - -While some values can be changed, in general they should not. If you do not -obey, be at least careful because the protocol could easily break. -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -# Length of the key of the HMAC which used to authenticate tickets in bytes. -TICKET_HMAC_KEY_LENGTH = 32 - -# Length of the AES key used to encrypt tickets in bytes. -TICKET_AES_KEY_LENGTH = 16 - -# Length of the IV for AES-CBC which is used to encrypt tickets in bytes. -TICKET_AES_CBC_IV_LENGTH = 16 - -# Directory where long-lived information is stored. It defaults to the current -# directory but is later set by `setStateLocation()' in util.py. -STATE_LOCATION = "" - -# Contains a ready-to-use bridge descriptor (in managed mode) or simply the -# server's bind address together with the password (in external mode). -PASSWORD_FILE = "server_password" - -# Divisor (in seconds) for the Unix epoch used to defend against replay -# attacks. -EPOCH_GRANULARITY = 3600 - -# Flags which can be set in a ScrambleSuit protocol message. -FLAG_PAYLOAD = (1 << 0) -FLAG_NEW_TICKET = (1 << 1) -FLAG_PRNG_SEED = (1 << 2) - -# Length of ScrambleSuit's header in bytes. -HDR_LENGTH = 16 + 2 + 2 + 1 - -# Length of the HMAC-SHA256-128 digest in bytes. -HMAC_SHA256_128_LENGTH = 16 - -# Whether or not to use inter-arrival time obfuscation. Disabling this option -# makes the transported protocol more identifiable but increases throughput a -# lot. -USE_IAT_OBFUSCATION = False - -# Key rotation time for session ticket keys in seconds. -KEY_ROTATION_TIME = 60 * 60 * 24 * 7 - -# Mark used to easily locate the HMAC authenticating handshake messages in -# bytes. -MARK_LENGTH = 16 - -# The master key's length in bytes. -MASTER_KEY_LENGTH = 32 - -# Maximum amount of seconds, a packet is delayed due to inter arrival time -# obfuscation. -MAX_PACKET_DELAY = 0.01 - -# The maximum amount of padding to be appended to handshake data. -MAX_PADDING_LENGTH = 1500 - -# The maximum length of a handshake in bytes (UniformDH as well as session -# tickets). -MAX_HANDSHAKE_LENGTH = MAX_PADDING_LENGTH + \ - MARK_LENGTH + \ - HMAC_SHA256_128_LENGTH - -# Length of ScrambleSuit's MTU in bytes. Note that this is *not* the link MTU -# which is probably 1500. -MTU = 1448 - -# Maximum payload unit of a ScrambleSuit message in bytes. -MPU = MTU - HDR_LENGTH - -# The minimum amount of distinct bins for probability distributions. -MIN_BINS = 1 - -# The maximum amount of distinct bins for probability distributions. -MAX_BINS = 100 - -# Length of a UniformDH public key in bytes. -PUBLIC_KEY_LENGTH = 192 - -# Length of the PRNG seed used to generate probability distributions in bytes. -PRNG_SEED_LENGTH = 32 - -# File which holds the server's state information. -SERVER_STATE_FILE = "server_state.cpickle" - -# Life time of session tickets in seconds. -SESSION_TICKET_LIFETIME = KEY_ROTATION_TIME - -# SHA256's digest length in bytes. -SHA256_LENGTH = 32 - -# The length of the UniformDH shared secret in bytes. It should be a multiple -# of 5 bytes since outside ScrambleSuit it is encoded in Base32. That way, we -# can avoid padding which might confuse users. -SHARED_SECRET_LENGTH = 20 - -# States which are used for the protocol state machine. -ST_WAIT_FOR_AUTH = 0 -ST_AUTH_FAILED = 1 -ST_CONNECTED = 2 - -# Static validation string embedded in all tickets. Must be a multiple of 16 -# bytes due to AES' block size. -TICKET_IDENTIFIER = b"ScrambleSuitTicket" - -# Length of a session ticket in bytes. -TICKET_LENGTH = 112 - -# The protocol name which is used in log messages. -TRANSPORT_NAME = "ScrambleSuit" diff --git a/pupy/network/lib/transports/scramblesuit/message.py b/pupy/network/lib/transports/scramblesuit/message.py deleted file mode 100644 index e9a70e14..00000000 --- a/pupy/network/lib/transports/scramblesuit/message.py +++ /dev/null @@ -1,232 +0,0 @@ -""" -This module provides code to handle ScrambleSuit protocol messages. - -The exported classes and functions provide interfaces to handle protocol -messages, check message headers for validity and create protocol messages out -of application data. -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -from ..obfscommon import serialize as pack -from ... import base -from . import mycrypto -from . import const -import logging - -log = logging - - -def createProtocolMessages(data, flags=const.FLAG_PAYLOAD): - """ - Create protocol messages out of the given payload. - - The given `data' is turned into a list of protocol messages with the given - `flags' set. The list is then returned. If possible, all messages fill - the MTU. - """ - - messages = [] - - while len(data) > const.MPU: - messages.append(ProtocolMessage(data[:const.MPU], flags=flags)) - data = data[const.MPU:] - - messages.append(ProtocolMessage(data, flags=flags)) - - #log.debug("Created %d protocol messages." % len(messages)) - - return messages - - -def getFlagNames(flags): - """ - Return the flag name encoded in the integer `flags' as string. - - This function is only useful for printing easy-to-read flag names in debug - log messages. - """ - - if flags == 1: - return "PAYLOAD" - - elif flags == 2: - return "NEW_TICKET" - - elif flags == 4: - return "PRNG_SEED" - - else: - return "Undefined" - - -def isSane(totalLen, payloadLen, flags): - """ - Verifies whether the given header fields are sane. - - The values of the fields `totalLen', `payloadLen' and `flags' are checked - for their sanity. If they are in the expected range, `True' is returned. - If any of these fields has an invalid value, `False' is returned. - """ - - def isFine(length): - """ - Check if the given length is fine. - """ - - return True if (0 <= length <= const.MPU) else False - - #log.debug("Message header: totalLen=%d, payloadLen=%d, flags" - # "=%s" % (totalLen, payloadLen, getFlagNames(flags))) - - validFlags = [ - const.FLAG_PAYLOAD, - const.FLAG_NEW_TICKET, - const.FLAG_PRNG_SEED, - ] - - return isFine(totalLen) and \ - isFine(payloadLen) and \ - totalLen >= payloadLen and \ - (flags in validFlags) - - -class ProtocolMessage(object): - - """ - Represents a ScrambleSuit protocol message. - - This class provides methods to deal with protocol messages. The methods - make it possible to add padding as well as to encrypt and authenticate - protocol messages. - """ - - def __init__(self, payload=b'', paddingLen=0, flags=const.FLAG_PAYLOAD): - """ - Initialises a ProtocolMessage object. - """ - - payloadLen = len(payload) - if (payloadLen + paddingLen) > const.MPU: - raise base.PluggableTransportError("No overly long messages.") - - self.totalLen = payloadLen + paddingLen - self.payloadLen = payloadLen - self.payload = payload - self.flags = flags - - def encryptAndHMAC(self, crypter, hmacKey): - """ - Encrypt and authenticate this protocol message. - - This protocol message is encrypted using `crypter' and authenticated - using `hmacKey'. Finally, the encrypted message prepended by a - HMAC-SHA256-128 is returned and ready to be sent over the wire. - """ - - encrypted = crypter.encrypt( - pack.htons(self.totalLen) + \ - pack.htons(self.payloadLen) + \ - pack.asbyte(self.flags) + self.payload + \ - (self.totalLen - self.payloadLen) * b'\0') - - hmac = mycrypto.HMAC_SHA256_128(hmacKey, encrypted) - - return hmac + encrypted - - def addPadding(self, paddingLen): - """ - Add padding to this protocol message. - - Padding is added to this protocol message. The exact amount is - specified by `paddingLen'. - """ - - # The padding must not exceed the message size. - if (self.totalLen + paddingLen) > const.MPU: - raise base.PluggableTransportError("Can't pad more than the MTU.") - - if paddingLen == 0: - return - - #log.debug("Adding %d bytes of padding to %d-byte message." % - # (paddingLen, const.HDR_LENGTH + self.totalLen)) - self.totalLen += paddingLen - - def __len__(self): - """ - Return the length of this protocol message. - """ - - return const.HDR_LENGTH + self.totalLen - -# Alias class name in order to provide a more intuitive API. -new = ProtocolMessage - -class MessageExtractor(object): - - """ - Extracts ScrambleSuit protocol messages out of an encrypted stream. - """ - - def __init__(self): - """ - Initialise a new MessageExtractor object. - """ - - self.recvBuf = b'' - self.totalLen = None - self.payloadLen = None - self.flags = None - - def extract(self, data, aes, hmacKey): - """ - Extracts (i.e., decrypts and authenticates) protocol messages. - - The raw `data' coming directly from the wire is decrypted using `aes' - and authenticated using `hmacKey'. The payload is then returned as - unencrypted protocol messages. In case of invalid headers or HMACs, an - exception is raised. - """ - - self.recvBuf += data - msgs = [] - - # Keep trying to unpack as long as there is at least a header. - while len(self.recvBuf) >= const.HDR_LENGTH: - - # If necessary, extract the header fields. - if self.totalLen is None and self.payloadLen is None and self.flags is None: - self.totalLen = pack.ntohs(aes.decrypt(self.recvBuf[16:18])) - self.payloadLen = pack.ntohs(aes.decrypt(self.recvBuf[18:20])) - self.flags = pack.frombyte(aes.decrypt(self.recvBuf[20])) - - if not isSane(self.totalLen, self.payloadLen, self.flags): - raise base.PluggableTransportError( - "Invalid header. (totalLen={} payloadLen={} flags={})".format( - self.totalLen, self.payloadLen, self.flags)) - - # Parts of the message are still on the wire; waiting. - if (len(self.recvBuf) - const.HDR_LENGTH) < self.totalLen: - break - - rcvdHMAC = self.recvBuf[0:const.HMAC_SHA256_128_LENGTH] - vrfyHMAC = mycrypto.HMAC_SHA256_128(hmacKey, - self.recvBuf[const.HMAC_SHA256_128_LENGTH: - (self.totalLen + const.HDR_LENGTH)]) - - if rcvdHMAC != vrfyHMAC: - raise base.PluggableTransportError("Invalid message HMAC.") - - # Decrypt the message and remove it from the input buffer. - extracted = aes.decrypt(self.recvBuf[const.HDR_LENGTH: - (self.totalLen + const.HDR_LENGTH)])[:self.payloadLen] - msgs.append(ProtocolMessage(payload=extracted, flags=self.flags)) - self.recvBuf = self.recvBuf[const.HDR_LENGTH + self.totalLen:] - - # Protocol message processed; now reset length fields. - self.totalLen = self.payloadLen = self.flags = None - - return msgs diff --git a/pupy/network/lib/transports/scramblesuit/mycrypto.py b/pupy/network/lib/transports/scramblesuit/mycrypto.py deleted file mode 100644 index 1aa2e2f0..00000000 --- a/pupy/network/lib/transports/scramblesuit/mycrypto.py +++ /dev/null @@ -1,158 +0,0 @@ -""" -This module provides cryptographic functions not implemented in PyCrypto. - -The implemented algorithms include HKDF-SHA256, HMAC-SHA256-128, (CS)PRNGs and -an interface for encryption and decryption using AES in counter mode. -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -from ... import base -from ..cryptoutils import ( - hmac_sha256_digest, AES_MODE_CTR, NewAESCipher -) - -from struct import pack, unpack - -from . import const -import logging - -from math import ceil - -log = logging - - -class HKDF_SHA256(object): - - """ - Implements HKDF using SHA256: https://tools.ietf.org/html/rfc5869 - - This class only implements the `expand' but not the `extract' stage since - the provided PRK already exhibits strong entropy. - """ - - __slots__ = ( - 'hashLen', 'N', 'prk', 'info', - 'length', 'ctr', 'T' - ) - - def __init__(self, prk, info=b'', length=32): - """ - Initialise a HKDF_SHA256 object. - """ - - self.hashLen = const.SHA256_LENGTH - - if length > (self.hashLen * 255): - raise ValueError("The OKM's length cannot be larger than %d." % - (self.hashLen * 255)) - - if len(prk) < self.hashLen: - raise ValueError("The PRK must be at least %d bytes in length " - "(%d given)." % (self.hashLen, len(prk))) - - self.N = ceil(length / self.hashLen) - self.prk = prk - self.info = info - self.length = length - self.ctr = 1 - self.T = b'' - - def expand(self): - """ - Return the expanded output key material. - - The output key material is calculated based on the given PRK, info and - L. - """ - - # Prevent the accidental re-use of output keying material. - if len(self.T) > 0: - raise base.PluggableTransportError("HKDF-SHA256 OKM must not " - "be re-used by application.") - - tmp_data = [] - tmp_len = 0 - tmp = b'' - - while self.length > tmp_len: - tmp = hmac_sha256_digest( - self.prk, b''.join((tmp, self.info, pack('B', self.ctr))) - ) - - tmp_len += len(tmp) - tmp_data.append(tmp) - - self.ctr += 1 - - self.T = b''.join(tmp_data) - return self.T[:self.length] - - -def HMAC_SHA256_128(key, msg): - """ - Return the HMAC-SHA256-128 of the given `msg' authenticated by `key'. - """ - - assert(len(key) >= const.SHARED_SECRET_LENGTH) - - h = hmac_sha256_digest(key, msg) - - # Return HMAC truncated to 128 out of 256 bits. - return h[:16] - - -class PayloadCrypter(object): - - """ - Provides methods to encrypt data using AES in counter mode. - - This class provides methods to set a session key as well as an - initialisation vector and to encrypt and decrypt data. - """ - - __slots__ = ('sessionKey', 'crypter') - - def __init__(self): - """ - Initialise a PayloadCrypter object. - """ - - log.debug("Initialising AES-CTR instance.") - - self.sessionKey = None - self.crypter = None - - def setSessionKey(self, key, iv): - """ - Set AES' session key and the initialisation vector for counter mode. - - The given `key' and `iv' are used as 256-bit AES key and as 128-bit - initialisation vector for counter mode. Both, the key as well as the - IV must come from a CSPRNG. - """ - - self.sessionKey = key - - # Our 128-bit counter has the following format: - # [ 64-bit static and random IV ] [ 64-bit incrementing counter ] - # Counter wrapping is not allowed which makes it possible to transfer - # 2^64 * 16 bytes of data while avoiding counter reuse. That amount is - # effectively out of reach given today's networking performance. - log.debug("Setting IV for AES-CTR.") - - iv = (unpack('>Q', iv)[0] << 64) + 1 - - self.crypter = NewAESCipher(key, iv, AES_MODE_CTR) - - def encrypt(self, data): - """ - Encrypts the given `data' using AES in counter mode. - """ - - return self.crypter.encrypt(data) - - # Encryption equals decryption in AES-CTR. - decrypt = encrypt diff --git a/pupy/network/lib/transports/scramblesuit/packetmorpher.py b/pupy/network/lib/transports/scramblesuit/packetmorpher.py deleted file mode 100644 index 37e19fba..00000000 --- a/pupy/network/lib/transports/scramblesuit/packetmorpher.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -Provides code to morph a chunk of data to a given probability distribution. - -The class provides an interface to morph a network packet's length to a -previously generated probability distribution. The packet lengths of the -morphed network data should then match the probability distribution. -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -import random - -from . import message -from . import probdist -from . import const - -import logging - -log = logging - -class PacketMorpher(object): - - """ - Implements methods to morph data to a target probability distribution. - - This class is used to modify ScrambleSuit's packet length distribution on - the wire. The class provides a method to determine the padding for packets - smaller than the MTU. - """ - - def __init__(self, dist=None): - """ - Initialise the packet morpher with the given distribution `dist'. - - If `dist' is `None', a new discrete probability distribution is - generated randomly. - """ - - if dist: - self.dist = dist - else: - self.dist = probdist.new(lambda: random.randint(const.HDR_LENGTH, - const.MTU)) - - def getPadding(self, sendCrypter, sendHMAC, dataLen): - """ - Based on the burst's size, return a ready-to-send padding blurb. - """ - - padLen = self.calcPadding(dataLen) - - assert const.HDR_LENGTH <= padLen < (const.MTU + const.HDR_LENGTH), \ - "Invalid padding length %d." % padLen - - # We have to use two padding messages if the padding is > MTU. - if padLen > const.MTU: - padMsgs = [message.new(b"", paddingLen=700 - const.HDR_LENGTH), - message.new(b"", paddingLen=padLen - 700 - \ - const.HDR_LENGTH)] - else: - padMsgs = [message.new(b"", paddingLen=padLen - const.HDR_LENGTH)] - - blurbs = [msg.encryptAndHMAC(sendCrypter, sendHMAC) for msg in padMsgs] - - return b"".join(blurbs) - - def calcPadding(self, dataLen): - """ - Based on `dataLen', determine and return a burst's padding. - - ScrambleSuit morphs the last packet in a burst, i.e., packets which - don't fill the link's MTU. This is done by drawing a random sample - from our probability distribution which is used to determine and return - the padding for such packets. This effectively gets rid of Tor's - 586-byte signature. - """ - - # The `is' and `should-be' length of the burst's last packet. - dataLen = dataLen % const.MTU - sampleLen = self.dist.randomSample() - - # Now determine the padding length which is in {0..MTU-1}. - if sampleLen >= dataLen: - padLen = sampleLen - dataLen - else: - padLen = (const.MTU - dataLen) + sampleLen - - if padLen < const.HDR_LENGTH: - padLen += const.MTU - - #log.debug("Morphing the last %d-byte packet to %d bytes by adding %d " - # "bytes of padding." % - # (dataLen % const.MTU, sampleLen, padLen)) - - return padLen - -# Alias class name in order to provide a more intuitive API. -new = PacketMorpher diff --git a/pupy/network/lib/transports/scramblesuit/probdist.py b/pupy/network/lib/transports/scramblesuit/probdist.py deleted file mode 100644 index 8ed116cf..00000000 --- a/pupy/network/lib/transports/scramblesuit/probdist.py +++ /dev/null @@ -1,106 +0,0 @@ -""" -This module provides code to generate and sample probability distributions. - -The class RandProbDist provides an interface to randomly generate probability -distributions. Random samples can then be drawn from these distributions. -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -import random - -from . import const - -import sys -import logging - -log = logging - -if sys.version_info.major > 2: - xrange = range - - -class RandProbDist: - - """ - Provides code to generate, sample and dump probability distributions. - """ - - def __init__(self, genSingleton, seed=None): - """ - Initialise a discrete probability distribution. - - The parameter `genSingleton' is expected to be a function which yields - singletons for the probability distribution. The optional `seed' can - be used to seed the PRNG so that the probability distribution is - generated deterministically. - """ - - self.prng = random if (seed is None) else random.Random(seed) - - self.sampleList = [] - self.dist = self.genDistribution(genSingleton) - self.dumpDistribution() - - def genDistribution(self, genSingleton): - """ - Generate a discrete probability distribution. - - The parameter `genSingleton' is a function which is used to generate - singletons for the probability distribution. - """ - - dist = {} - - # Amount of distinct bins, i.e., packet lengths or inter arrival times. - bins = self.prng.randint(const.MIN_BINS, const.MAX_BINS) - - # Cumulative probability of all bins. - cumulProb = 0 - - for _ in xrange(bins): - prob = self.prng.uniform(0, (1 - cumulProb)) - cumulProb += prob - - singleton = genSingleton() - dist[singleton] = prob - self.sampleList.append((cumulProb, singleton,)) - - dist[genSingleton()] = (1 - cumulProb) - - return dist - - def dumpDistribution(self): - """ - Dump the probability distribution using the logging object. - - Only probabilities > 0.01 are dumped. - """ - - log.debug("Dumping probability distribution.") - - for singleton in self.dist: - # We are not interested in tiny probabilities. - if self.dist[singleton] > 0.01: - log.debug("P(%s) = %.3f" % - (str(singleton), self.dist[singleton])) - - def randomSample(self): - """ - Draw and return a random sample from the probability distribution. - """ - - assert len(self.sampleList) > 0 - - rand = random.random() - - for cumulProb, singleton in self.sampleList: - if rand <= cumulProb: - return singleton - - return self.sampleList[-1][1] - -# Alias class name in order to provide a more intuitive API. -new = RandProbDist diff --git a/pupy/network/lib/transports/scramblesuit/replay.py b/pupy/network/lib/transports/scramblesuit/replay.py deleted file mode 100644 index f118b159..00000000 --- a/pupy/network/lib/transports/scramblesuit/replay.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -This module implements a mechanism to protect against replay attacks. - -The replay protection mechanism is based on a dictionary which caches -previously observed keys. New keys can be added to the dictionary and existing -ones can be queried. A pruning mechanism deletes expired keys from the -dictionary. -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -import time - -from . import const - -import logging - -log = logging - - -class Tracker(object): - - """ - Implement methods to keep track of replayed keys. - - This class provides methods to add new keys (elements), check whether keys - are already present in the dictionary and to prune the lookup table. - """ - - def __init__(self): - """ - Initialise a `Tracker' object. - """ - - self.table = dict() - - def addElement(self, element): - """ - Add the given `element' to the lookup table. - """ - - if self.isPresent(element): - raise LookupError("Element already present in table.") - - # The key is a HMAC and the value is the current Unix timestamp. - self.table[element] = int(time.time()) - - def isPresent(self, element): - """ - Check if the given `element' is already present in the lookup table. - - Return `True' if `element' is already in the lookup table and `False' - otherwise. - """ - - log.debug("Looking for existing element in size-%d lookup table." % - len(self.table)) - - # Prune the replay table before looking up the given `element'. This - # could be done more efficiently, e.g. by pruning every n minutes and - # only checking the timestamp of this particular element. - self.prune() - - return (element in self.table) - - def prune(self): - """ - Delete expired elements from the lookup table. - - Keys whose Unix timestamps are older than `const.EPOCH_GRANULARITY' are - being removed from the lookup table. - """ - - log.debug("Pruning the replay table.") - - deleteList = [] - now = int(time.time()) - - for element in self.table: - if (now - self.table[element]) > const.EPOCH_GRANULARITY: - deleteList.append(element) - - # We can't delete from a dictionary while iterating over it; therefore - # this construct. - for elem in deleteList: - log.debug("Deleting expired element.") - del self.table[elem] diff --git a/pupy/network/lib/transports/scramblesuit/scramblesuit.py b/pupy/network/lib/transports/scramblesuit/scramblesuit.py deleted file mode 100644 index 73534847..00000000 --- a/pupy/network/lib/transports/scramblesuit/scramblesuit.py +++ /dev/null @@ -1,605 +0,0 @@ -""" -The scramblesuit module implements the ScrambleSuit obfuscation protocol. - -The paper discussing the design and evaluation of the ScrambleSuit pluggable -transport protocol is available here: -http://www.cs.kau.se/philwint/scramblesuit/ -""" - -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -import logging - -import random -import base64 -import argparse -import time - -from io import open - -from ... import base - -from . import probdist -from . import mycrypto -from . import message -from . import const -from . import util -from . import packetmorpher -from . import uniformdh -from . import ticket -from . import state - -from pupy.network.lib.buffer import Buffer - -log = logging - - -class ReadPassFile(argparse.Action): - def __call__(self, parser, namespace, values, option_string=None): - with open(values) as f: - setattr(namespace, self.dest, f.readline().strip()) - - -class ScrambleSuitTransport(base.BaseTransport): - - """ - Implement the ScrambleSuit protocol. - - The class implements methods which implement the ScrambleSuit protocol. A - large part of the protocol's functionality is outsources to different - modules. - """ - - def __init__(self, *args, **kwargs): - """ - Initialise a ScrambleSuitTransport object. - """ - - #log.debug("Initialising %s." % const.TRANSPORT_NAME) - - super(ScrambleSuitTransport, self).__init__(*args, **kwargs) - - self.drainedHandshake = 0 - - # Load the server's persistent state from file. - if self.weAreServer: - self.srvState = state.load() - - # Initialise the protocol's state machine. - #log.debug("Switching to state ST_WAIT_FOR_AUTH.") - self.protoState = const.ST_WAIT_FOR_AUTH - - # Buffer for outgoing data. - self.sendBuf = b'' - - # Buffer for inter-arrival time obfuscation. - self.choppingBuf = Buffer() - - # AES instances to decrypt incoming and encrypt outgoing data. - self.sendCrypter = mycrypto.PayloadCrypter() - self.recvCrypter = mycrypto.PayloadCrypter() - - # Packet morpher to modify the protocol's packet length distribution. - self.pktMorpher = packetmorpher.new(self.srvState.pktDist - if self.weAreServer else None) - - # Inter-arrival time morpher to obfuscate inter arrival times. - self.iatMorpher = self.srvState.iatDist if self.weAreServer else \ - probdist.new(lambda: random.random() % - const.MAX_PACKET_DELAY) - - # Used to extract protocol messages from encrypted data. - self.protoMsg = message.MessageExtractor() - - # Used by the server-side: `True' if the ticket is already - # decrypted but not yet authenticated. - self.decryptedTicket = False - - # If we are in external mode we should already have a shared - # secret set up because of validate_external_mode_cli(). - if self.weAreExternal: - assert(self.uniformDHSecret) - - if self.weAreClient and not self.weAreExternal: - # As a client in managed mode, we get the shared secret - # from callback `handle_socks_args()' per-connection. Set - # the shared secret to None for now. - self.uniformDHSecret = None - - self.uniformdh = uniformdh.new(self.uniformDHSecret, self.weAreServer) - - @classmethod - def setup(cls, transportConfig): - """ - Called once when obfsproxy starts. - """ - - #log.error("\n\n################################################\n" - # "Do NOT rely on ScrambleSuit for strong security!\n" - # "################################################\n") - - util.setStateLocation(transportConfig.getStateLocation()) - - cls.weAreClient = transportConfig.weAreClient - cls.weAreServer = not cls.weAreClient - cls.weAreExternal = transportConfig.weAreExternal - - # If we are server and in managed mode, we should get the - # shared secret from the server transport options. - if cls.weAreServer and not cls.weAreExternal: - cfg = transportConfig.getServerTransportOptions() - if cfg and "password" in cfg: - try: - cls.uniformDHSecret = base64.b32decode(util.sanitiseBase32( - cfg["password"])) - except (TypeError, AttributeError) as error: - raise base.TransportSetupFailed( - "Password could not be base32 decoded (%s)" % error) - - cls.uniformDHSecret = cls.uniformDHSecret.strip() - - if cls.weAreServer: - if not hasattr(cls, "uniformDHSecret"): - #log.debug("Using fallback password for descriptor file.") - srv = state.load() - cls.uniformDHSecret = srv.fallbackPassword - - if len(cls.uniformDHSecret) != const.SHARED_SECRET_LENGTH: - raise base.TransportSetupFailed( - "Wrong password length (%d instead of %d)" - % len(cls.uniformDHSecret), const.SHARED_SECRET_LENGTH) - - if not const.STATE_LOCATION: - raise base.TransportSetupFailed( - "No state location set. If you are using external mode, " \ - "please set it using the --data-dir switch.") - - state.writeServerPassword(cls.uniformDHSecret) - - @classmethod - def get_public_server_options(cls, transportOptions): - """ - Return ScrambleSuit's BridgeDB parameters, i.e., the shared secret. - - As a fallback mechanism, we return an automatically generated password - if the bridge operator did not use `ServerTransportOptions'. - """ - - #log.debug("Tor's transport options: %s" % str(transportOptions)) - - if "password" not in transportOptions: - #log.warning("No password found in transport options (use Tor's " \ - # "`ServerTransportOptions' to set your own password)." \ - # " Using automatically generated password instead.") - srv = state.load() - transportOptions = {"password": - base64.b32encode(srv.fallbackPassword)} - cls.uniformDHSecret = srv.fallbackPassword - - return transportOptions - - def deriveSecrets(self, masterKey): - """ - Derive various session keys from the given `masterKey'. - - The argument `masterKey' is used to derive two session keys and nonces - for AES-CTR and two HMAC keys. The derivation is done using - HKDF-SHA256. - """ - - assert len(masterKey) == const.MASTER_KEY_LENGTH - - #log.debug("Deriving session keys from %d-byte master key." % - # len(masterKey)) - - # We need key material for two symmetric AES-CTR keys, nonces and - # HMACs. In total, this equals 144 bytes of key material. - hkdf = mycrypto.HKDF_SHA256(masterKey, b'', (32 * 4) + (8 * 2)) - okm = hkdf.expand() - assert len(okm) >= ((32 * 4) + (8 * 2)) - - # Set AES-CTR keys and nonces for our two AES instances. - self.sendCrypter.setSessionKey(okm[0:32], okm[32:40]) - self.recvCrypter.setSessionKey(okm[40:72], okm[72:80]) - - # Set the keys for the two HMACs protecting our data integrity. - self.sendHMAC = okm[80:112] - self.recvHMAC = okm[112:144] - - if self.weAreServer: - self.sendHMAC, self.recvHMAC = self.recvHMAC, self.sendHMAC - self.sendCrypter, self.recvCrypter = self.recvCrypter, \ - self.sendCrypter - - def circuitConnected(self): - """ - Initiate a ScrambleSuit handshake. - - This method is only relevant for clients since servers never initiate - handshakes. If a session ticket is available, it is redeemed. - Otherwise, a UniformDH handshake is conducted. - """ - - # The server handles the handshake passively. - if self.weAreServer: - return - - # The preferred authentication mechanism is a session ticket. - if self.uniformDHSecret is None: - # log.warning("A UniformDH password is not set, most likely " \ - # "a missing 'password' argument.") - - raise EOFError('A UniformDH password is not set') - #log.debug("No session ticket to redeem. Running UniformDH.") - - self.downstream.write(self.uniformdh.createHandshake()) - - def sendRemote(self, data, flags=const.FLAG_PAYLOAD): - """ - Send data to the remote end after a connection was established. - - The given `data' is first encapsulated in protocol messages. Then, the - protocol message(s) are sent over the wire. The argument `flags' - specifies the protocol message flags with the default flags signalling - payload. - """ - - #log.debug("Processing %d bytes of outgoing data." % len(data)) - - # Wrap the application's data in ScrambleSuit protocol messages. - messages = message.createProtocolMessages(data, flags=flags) - blurb = b''.join((msg.encryptAndHMAC(self.sendCrypter, - self.sendHMAC) for msg in messages)) - - # Flush data chunk for chunk to obfuscate inter-arrival times. - if const.USE_IAT_OBFUSCATION: - - if len(self.choppingBuf) == 0: - self.choppingBuf.write(blurb) - time.sleep(self.iatMorpher.randomSample()) - self.flushPieces() - else: - # flushPieces() is still busy processing the chopping buffer. - self.choppingBuf.write(blurb) - - else: - padBlurb = self.pktMorpher.getPadding(self.sendCrypter, - self.sendHMAC, - len(blurb)) - self.downstream.write(blurb + padBlurb) - - def flushPieces(self): - """ - Write the application data in chunks to the wire. - - The cached data is sent over the wire in chunks. After every write - call, control is given back to the Twisted reactor so it has a chance - to flush the data. Shortly thereafter, this function is called again - to write the next chunk of data. The delays in between subsequent - write calls are controlled by the inter-arrival time obfuscator. - """ - - # Drain and send an MTU-sized chunk from the chopping buffer. - if len(self.choppingBuf) > const.MTU: - - self.downstream.write(self.choppingBuf.read(const.MTU)) - - # Drain and send whatever is left in the output buffer. - else: - blurb = self.choppingBuf.read() - padBlurb = self.pktMorpher.getPadding(self.sendCrypter, - self.sendHMAC, - len(blurb)) - self.downstream.write(blurb + padBlurb) - return - - time.sleep(self.iatMorpher.randomSample()) - self.flushPieces() - - def processMessages(self, data): - """ - Acts on extracted protocol messages based on header flags. - - After the incoming `data' is decrypted and authenticated, this method - processes the received data based on the header flags. Payload is - written to the local application, new tickets are stored, or keys are - added to the replay table. - """ - - if (data is None) or (len(data) == 0): - return - - # Try to extract protocol messages from the encrypted blurb. - msgs = self.protoMsg.extract(data, self.recvCrypter, self.recvHMAC) - if (msgs is None) or (len(msgs) == 0): - return - - for msg in msgs: - # Forward data to the application. - if msg.flags == const.FLAG_PAYLOAD: - self.upstream.write(msg.payload) - - # Store newly received ticket. - elif self.weAreClient and (msg.flags == const.FLAG_NEW_TICKET): - assert len(msg.payload) == ( - const.TICKET_LENGTH + const.MASTER_KEY_LENGTH) - - # Use the PRNG seed to generate the same probability distributions - # as the server. That's where the polymorphism comes from. - elif self.weAreClient and (msg.flags == const.FLAG_PRNG_SEED): - assert len(msg.payload) == const.PRNG_SEED_LENGTH - #log.debug("Obtained PRNG seed.") - prng = random.Random(msg.payload) - pktDist = probdist.new(lambda: prng.randint(const.HDR_LENGTH, - const.MTU), - seed=msg.payload) - self.pktMorpher = packetmorpher.new(pktDist) - self.iatMorpher = probdist.new(lambda: prng.random() % - const.MAX_PACKET_DELAY, - seed=msg.payload) - - else: - #log.warning("Invalid message flags: %d." % msg.flags) - pass - - def flushSendBuffer(self): - """ - Flush the application's queued data. - - The application could have sent data while we were busy authenticating - the remote machine. This method flushes the data which could have been - queued in the meanwhile in `self.sendBuf'. - """ - - if len(self.sendBuf) == 0: - #log.debug("Send buffer is empty; nothing to flush.") - return - - # Flush the buffered data, the application is so eager to send. - #log.debug("Flushing %d bytes of buffered application data." % - # len(self.sendBuf)) - - self.sendRemote(self.sendBuf) - self.sendBuf = b'' - - def receiveTicket(self, data): - """ - Extract and verify a potential session ticket. - - The given `data' is treated as a session ticket. The ticket is being - decrypted and authenticated (yes, in that order). If all these steps - succeed, `True' is returned. Otherwise, `False' is returned. - """ - - if len(data) < (const.TICKET_LENGTH + const.MARK_LENGTH + \ - const.HMAC_SHA256_128_LENGTH): - return False - - potentialTicket = data.peek() - - # Now try to decrypt and parse the ticket. We need the master key - # inside to verify the HMAC in the next step. - if not self.decryptedTicket: - newTicket = ticket.decrypt(potentialTicket[:const.TICKET_LENGTH], - self.srvState) - if newTicket is not None and newTicket.isValid(): - self.deriveSecrets(newTicket.masterKey) - self.decryptedTicket = True - else: - return False - - # First, find the mark to efficiently locate the HMAC. - mark = mycrypto.HMAC_SHA256_128(self.recvHMAC, - potentialTicket[:const.TICKET_LENGTH]) - - index = util.locateMark(mark, potentialTicket) - if not index: - return False - - # Now, verify if the HMAC is valid. - existingHMAC = potentialTicket[ - index + const.MARK_LENGTH:index + const.MARK_LENGTH + \ - const.HMAC_SHA256_128_LENGTH] - - authenticated = False - for epoch in util.expandedEpoch(): - myHMAC = mycrypto.HMAC_SHA256_128(self.recvHMAC, - potentialTicket[0:index + \ - const.MARK_LENGTH] + epoch) - - if util.isValidHMAC(myHMAC, existingHMAC, self.recvHMAC): - authenticated = True - break - - #log.debug("HMAC invalid. Trying next epoch value.") - - if not authenticated: - #log.warning("Could not verify the authentication message's HMAC.") - return False - - # Do nothing if the ticket is replayed. Immediately closing the - # connection would be suspicious. - if self.srvState.isReplayed(existingHMAC): - #log.warning("The HMAC was already present in the replay table.") - return False - - data.drain(index + const.MARK_LENGTH + const.HMAC_SHA256_128_LENGTH) - - #log.debug("Adding the HMAC authenticating the ticket message to the " \ - # "replay table: %s." % existingHMAC.encode('hex')) - self.srvState.registerKey(existingHMAC) - - #log.debug("Switching to state ST_CONNECTED.") - self.protoState = const.ST_CONNECTED - - return True - - def receivedUpstream(self, data): - """ - Sends data to the remote machine or queues it to be sent later. - - Depending on the current protocol state, the given `data' is either - directly sent to the remote machine or queued. The buffer is then - flushed once, a connection is established. - """ - - if self.protoState == const.ST_CONNECTED: - self.sendRemote(data.read()) - - # Buffer data we are not ready to transmit yet. - else: - self.sendBuf += data.read() - #log.debug("Buffered %d bytes of outgoing data." % - # len(self.sendBuf)) - - def sendTicketAndSeed(self): - """ - Send a session ticket and the PRNG seed to the client. - - This method is only called by the server after successful - authentication. Finally, the server's send buffer is flushed. - """ - - #log.debug("Sending a new session ticket and the PRNG seed to the " \ - # "client.") - - self.sendRemote(ticket.issueTicketAndKey(self.srvState), - flags=const.FLAG_NEW_TICKET) - self.sendRemote(self.srvState.prngSeed, - flags=const.FLAG_PRNG_SEED) - self.flushSendBuffer() - - def receivedDownstream(self, data): - """ - Receives and processes data coming from the remote machine. - - The incoming `data' is dispatched depending on the current protocol - state and whether we are the client or the server. The data is either - payload or authentication data. - """ - - if self.weAreServer and (self.protoState == const.ST_AUTH_FAILED): - - self.drainedHandshake += len(data) - data.drain(len(data)) - - if self.drainedHandshake > self.srvState.closingThreshold: - #log.info("Terminating connection after having received >= %d" - # " bytes because client could not " - # "authenticate." % self.srvState.closingThreshold) - - raise EOFError('Authentication still was not completed') - - elif self.weAreServer and (self.protoState == const.ST_WAIT_FOR_AUTH): - - # First, try to interpret the incoming data as session ticket. - if self.receiveTicket(data): - #log.debug("Ticket authentication succeeded.") - - self.sendTicketAndSeed() - - # Second, interpret the data as a UniformDH handshake. - elif self.uniformdh.receivePublicKey(data, self.deriveSecrets, - self.srvState): - # Now send the server's UniformDH public key to the client. - handshakeMsg = self.uniformdh.createHandshake(srvState= - self.srvState) - - #log.debug("Sending %d bytes of UniformDH handshake and " - # "session ticket." % len(handshakeMsg)) - - self.downstream.write(handshakeMsg) - #log.debug("UniformDH authentication succeeded.") - - #log.debug("Switching to state ST_CONNECTED.") - self.protoState = const.ST_CONNECTED - - self.sendTicketAndSeed() - - elif len(data) > const.MAX_HANDSHAKE_LENGTH: - self.protoState = const.ST_AUTH_FAILED - self.drainedHandshake = len(data) - data.drain(self.drainedHandshake) - #log.info("No successful authentication after having " \ - # "received >= %d bytes. Now ignoring client." % \ - # const.MAX_HANDSHAKE_LENGTH) - return - - else: - #log.debug("Authentication unsuccessful so far. " - # "Waiting for more data.") - return - - elif self.weAreClient and (self.protoState == const.ST_WAIT_FOR_AUTH): - - if not self.uniformdh.receivePublicKey(data, self.deriveSecrets): - #log.debug("Unable to finish UniformDH handshake just yet.") - return - - #log.debug("UniformDH authentication succeeded.") - - #log.debug("Switching to state ST_CONNECTED.") - self.protoState = const.ST_CONNECTED - self.flushSendBuffer() - - if self.protoState == const.ST_CONNECTED: - self.processMessages(data.read()) - -class ScrambleSuitClient(ScrambleSuitTransport): - - """ - Extend the ScrambleSuit class. - """ - - password=None - - def __init__(self, *args, **kwargs): - """ - Initialise a ScrambleSuitClient object. - """ - - self.weAreServer=False - self.weAreClient=True - self.weAreExternal=True - if 'password' in kwargs: - self.password=kwargs['password'] - uniformDHSecret = self.password - rawLength = len(uniformDHSecret) - if rawLength != const.SHARED_SECRET_LENGTH: - raise base.PluggableTransportError( - "The UniformDH password must be %d bytes in length, but %d bytes are given." - % (const.SHARED_SECRET_LENGTH, rawLength)) - else: - self.uniformDHSecret = uniformDHSecret - ScrambleSuitTransport.__init__(self, *args, **kwargs) - - -class ScrambleSuitServer(ScrambleSuitTransport): - - """ - Extend the ScrambleSuit class. - """ - password=None - - def __init__(self, *args, **kwargs): - """ - Initialise a ScrambleSuitServer object. - """ - - self.weAreServer=True - self.weAreClient=False - self.weAreExternal=True - if 'password' in kwargs: - self.password=kwargs['password'] - uniformDHSecret = self.password - rawLength = len(uniformDHSecret) - if rawLength != const.SHARED_SECRET_LENGTH: - raise base.PluggableTransportError( - "The UniformDH password must be %d bytes in length, but %d bytes are given." - % (const.SHARED_SECRET_LENGTH, rawLength)) - else: - self.uniformDHSecret = uniformDHSecret - ScrambleSuitTransport.__init__(self, *args, **kwargs) diff --git a/pupy/network/lib/transports/scramblesuit/state.py b/pupy/network/lib/transports/scramblesuit/state.py deleted file mode 100644 index d4c99675..00000000 --- a/pupy/network/lib/transports/scramblesuit/state.py +++ /dev/null @@ -1,183 +0,0 @@ -# Original file edited by contact@n1nj4.eu to avoid writing state to disk contact - -""" -Provide a way to store the server's state information on disk. - -The server possesses state information which should persist across runs. This -includes key material to encrypt and authenticate session tickets, replay -tables and PRNG seeds. This module provides methods to load, store and -generate such state information. - -""" - -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -import os -import sys -import time - -import random -import base64 - -import logging - -from io import open - -from . import const -from . import replay -from . import probdist - -from ..cryptoutils import get_random - -log = logging - -memoryState = None - - -def load(): - global memoryState - - """ - Load the server's state object from file. - - The server's state file is loaded and the state object returned. If no - state file is found, a new one is created and returned. - """ - - if memoryState: - return memoryState - - log.info("The server's state file does not exist (yet).") - memoryState = State() - memoryState.genState() - return memoryState - - -def writeServerPassword(password): - """ - Dump our ScrambleSuit server descriptor to file. - - The file should make it easy for bridge operators to obtain copy & - pasteable server descriptors. - """ - - assert len(password) == const.SHARED_SECRET_LENGTH - assert const.STATE_LOCATION != "" - - passwordFile = os.path.join(const.STATE_LOCATION, const.PASSWORD_FILE) - log.info("Writing server password to file `%s'." % passwordFile) - - password_str = "# You are supposed to give this password to your clients to append it to their Bridge line" - password_str = "# For example: Bridge scramblesuit 192.0.2.1:5555 EXAMPLEFINGERPRINTNOTREAL password=EXAMPLEPASSWORDNOTREAL" - password_str = "# Here is your password:" - password_str = "password=%s\n" % base64.b32encode(password) - try: - with open(passwordFile, 'w') as fd: - fd.write(password_str) - except IOError as err: - log.error("Error writing password file to `%s': %s" % - (passwordFile, err)) - - -class State(object): - - """ - Implement a state class which stores the server's state. - - This class makes it possible to store state information on disk. It - provides methods to generate and write state information. - """ - - def __init__(self): - """ - Initialise a `State' object. - """ - - self.prngSeed = None - self.keyCreation = None - self.hmacKey = None - self.aesKey = None - self.oldHmacKey = None - self.oldAesKey = None - self.ticketReplay = None - self.uniformDhReplay = None - self.pktDist = None - self.iatDist = None - self.fallbackPassword = None - self.closingThreshold = None - - def genState(self): - """ - Populate all the local variables with values. - """ - - log.info("Generating parameters for the server's state file.") - - # PRNG seed for the client to reproduce the packet and IAT morpher. - self.prngSeed = get_random(const.PRNG_SEED_LENGTH) - - # HMAC and AES key used to encrypt and authenticate tickets. - self.hmacKey = get_random(const.TICKET_HMAC_KEY_LENGTH) - self.aesKey = get_random(const.TICKET_AES_KEY_LENGTH) - self.keyCreation = int(time.time()) - - # The previous HMAC and AES keys. - self.oldHmacKey = None - self.oldAesKey = None - - # Replay dictionary for both authentication mechanisms. - self.replayTracker = replay.Tracker() - - # Distributions for packet lengths and inter arrival times. - prng = random.Random(self.prngSeed) - self.pktDist = probdist.new(lambda: prng.randint(const.HDR_LENGTH, - const.MTU), - seed=self.prngSeed) - self.iatDist = probdist.new(lambda: prng.random() % - const.MAX_PACKET_DELAY, - seed=self.prngSeed) - - # Fallback UniformDH shared secret. Only used if the bridge operator - # did not set `ServerTransportOptions'. - self.fallbackPassword = os.urandom(const.SHARED_SECRET_LENGTH) - - # Unauthenticated connections are closed after having received the - # following amount of bytes. - self.closingThreshold = prng.randint(const.MAX_HANDSHAKE_LENGTH, - const.MAX_HANDSHAKE_LENGTH * 5) - - self.writeState() - - def isReplayed(self, hmac): - """ - Check if `hmac' is present in the replay table. - - Return `True' if the given `hmac' is present in the replay table and - `False' otherwise. - """ - - assert self.replayTracker is not None - - log.debug("Querying if HMAC is present in the replay table.") - - return self.replayTracker.isPresent(hmac) - - def registerKey(self, hmac): - """ - Add the given `hmac' to the replay table. - """ - - assert self.replayTracker is not None - - log.debug("Adding a new HMAC to the replay table.") - self.replayTracker.addElement(hmac) - - # We must write the data to disk immediately so that other ScrambleSuit - # connections can share the same state. - self.writeState() - - def writeState(self): - pass diff --git a/pupy/network/lib/transports/scramblesuit/ticket.py b/pupy/network/lib/transports/scramblesuit/ticket.py deleted file mode 100644 index 2a16a211..00000000 --- a/pupy/network/lib/transports/scramblesuit/ticket.py +++ /dev/null @@ -1,285 +0,0 @@ -#!/usr/bin/env python - -""" -This module provides a session ticket mechanism. - -The implemented mechanism is a subset of session tickets as proposed for -TLS in RFC 5077. - -The format of a 112-byte ticket is: - +------------+------------------+--------------+ - | 16-byte IV | 64-byte E(state) | 32-byte HMAC | - +------------+------------------+--------------+ - -The 64-byte encrypted state contains: - +-------------------+--------------------+--------------------+-------------+ - | 4-byte issue date | 18-byte identifier | 32-byte master key | 10-byte pad | - +-------------------+--------------------+--------------------+-------------+ -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -import time -from . import const -import struct -import random -import datetime - -from ..cryptoutils import ( - NewAESCipher, hmac_sha256_digest, AES_BLOCK_SIZE, - get_random -) - -from .mycrypto import HMAC_SHA256_128 - -import logging - -from . import util - - -def createTicketMessage(rawTicket, HMACKey): - """ - Create and return a ready-to-be-sent ticket authentication message. - - Pseudo-random padding and a mark are added to `rawTicket' and the result is - then authenticated using `HMACKey' as key for a HMAC. The resulting - authentication message is then returned. - """ - - assert len(rawTicket) == const.TICKET_LENGTH - assert len(HMACKey) == const.TICKET_HMAC_KEY_LENGTH - - # Subtract the length of the ticket to make the handshake on - # average as long as a UniformDH handshake message. - padding = get_random( - random.randint( - 0, const.MAX_PADDING_LENGTH - const.TICKET_LENGTH)) - - mark = HMAC_SHA256_128(HMACKey, rawTicket) - hmac = HMAC_SHA256_128( - HMACKey, rawTicket + padding + mark + util.getEpoch()) - - return rawTicket + padding + mark + hmac - - -def issueTicketAndKey(srvState): - """ - Issue a new session ticket and append it to the according master key. - - The parameter `srvState' contains the key material and is passed on to - `SessionTicket'. The returned ticket and key are ready to be wrapped into - a protocol message with the flag FLAG_NEW_TICKET set. - """ - - logging.info("Issuing new session ticket and master key.") - masterKey = get_random(const.MASTER_KEY_LENGTH) - newTicket = (SessionTicket(masterKey, srvState)).issue() - - return masterKey + newTicket - - -def checkKeys(srvState): - """ - Check whether the key material for session tickets must be rotated. - - The key material (i.e., AES and HMAC keys for session tickets) contained in - `srvState' is checked if it needs to be rotated. If so, the old keys are - stored and new ones are created. - """ - - assert (srvState.hmacKey is not None) and \ - (srvState.aesKey is not None) and \ - (srvState.keyCreation is not None) - - if (int(time.time()) - srvState.keyCreation) > const.KEY_ROTATION_TIME: - logging.info("Rotating server key material for session tickets.") - - # Save expired keys to be able to validate old tickets. - srvState.oldAesKey = srvState.aesKey - srvState.oldHmacKey = srvState.hmacKey - - # Create new key material... - srvState.aesKey = get_random(const.TICKET_AES_KEY_LENGTH) - srvState.hmacKey = get_random(const.TICKET_HMAC_KEY_LENGTH) - srvState.keyCreation = int(time.time()) - - # ...and save it to disk. - srvState.writeState() - - -def decrypt(ticket, srvState): - """ - Decrypts, verifies and returns the given `ticket'. - - The key material used to verify the ticket is contained in `srvState'. - First, the HMAC over the ticket is verified. If it is valid, the ticket is - decrypted. Finally, a `ProtocolState()' object containing the master key - and the ticket's issue date is returned. If any of these steps fail, - `None' is returned. - """ - - assert (ticket is not None) and (len(ticket) == const.TICKET_LENGTH) - assert (srvState.hmacKey is not None) and (srvState.aesKey is not None) - - logging.debug("Attempting to decrypt and verify ticket.") - - checkKeys(srvState) - - # Verify the ticket's authenticity before decrypting. - hmac = hmac_sha256_digest(srvState.hmacKey, ticket[0:80]) - if util.isValidHMAC(hmac, ticket[80:const.TICKET_LENGTH], - srvState.hmacKey): - aesKey = srvState.aesKey - else: - if srvState.oldHmacKey is None: - return None - - # Was the HMAC created using the rotated key material? - oldHmac = hmac_sha256_digest(srvState.oldHmacKey, ticket[0:80]) - if util.isValidHMAC(oldHmac, ticket[80:const.TICKET_LENGTH], - srvState.oldHmacKey): - aesKey = srvState.oldAesKey - else: - return None - - # Decrypt the ticket to extract the state information. - aes = NewAESCipher( - aesKey, ticket[0:const.TICKET_AES_CBC_IV_LENGTH] - ) - plainTicket = aes.decrypt(ticket[const.TICKET_AES_CBC_IV_LENGTH:80]) - - issueDate = struct.unpack('I', plainTicket[0:4])[0] - identifier = plainTicket[4:22] - masterKey = plainTicket[22:54] - - if not (identifier == const.TICKET_IDENTIFIER): - logging.error("The ticket's HMAC is valid but the identifier is invalid. " - "The ticket could be corrupt.") - return None - - return ProtocolState(masterKey, issueDate=issueDate) - - -class ProtocolState(object): - - """ - Defines a ScrambleSuit protocol state contained in a session ticket. - - A protocol state is essentially a master key which can then be used by the - server to derive session keys. Besides, a state object contains an issue - date which specifies the expiry date of a ticket. This class contains - methods to check the expiry status of a ticket and to dump it in its raw - form. - """ - - def __init__(self, masterKey, issueDate=int(time.time())): - """ - The constructor of the `ProtocolState' class. - - The four class variables are initialised. - """ - - self.identifier = const.TICKET_IDENTIFIER - self.masterKey = masterKey - self.issueDate = issueDate - # Pad to multiple of 16 bytes to match AES' block size. - self.pad = b'\0\0\0\0\0\0\0\0\0\0' - - def isValid(self): - """ - Verifies the expiry date of the object's issue date. - - If the expiry date is not yet reached and the protocol state is still - valid, `True' is returned. If the protocol state has expired, `False' - is returned. - """ - - assert self.issueDate - - lifetime = int(time.time()) - self.issueDate - if lifetime > const.SESSION_TICKET_LIFETIME: - logging.debug("The ticket is invalid and expired %s ago." % - str(datetime.timedelta(seconds= - (lifetime - const.SESSION_TICKET_LIFETIME)))) - return False - - logging.debug("The ticket is still valid for %s." % - str(datetime.timedelta(seconds= - (const.SESSION_TICKET_LIFETIME - lifetime)))) - return True - - def __repr__(self): - """ - Return a raw string representation of the object's protocol state. - - The length of the returned representation is exactly 64 bytes; a - multiple of AES' 16-byte block size. That makes it suitable to be - encrypted using AES-CBC. - """ - - return struct.pack('I', self.issueDate) + self.identifier + \ - self.masterKey + self.pad - - -class SessionTicket(object): - - """ - Encrypts and authenticates an encapsulated `ProtocolState()' object. - - This class implements a session ticket which can be redeemed by clients. - The class contains methods to initialise and issue session tickets. - """ - - def __init__(self, masterKey, srvState): - """ - The constructor of the `SessionTicket()' class. - - The class variables are initialised and the validity of the symmetric - keys for the session tickets is checked. - """ - - assert (masterKey is not None) and \ - len(masterKey) == const.MASTER_KEY_LENGTH - - checkKeys(srvState) - - # Initialisation vector for AES-CBC. - self.IV = get_random(const.TICKET_AES_CBC_IV_LENGTH) - - # The server's (encrypted) protocol state. - self.state = ProtocolState(masterKey) - - # AES and HMAC keys to encrypt and authenticate the ticket. - self.symmTicketKey = srvState.aesKey - self.hmacTicketKey = srvState.hmacKey - - def issue(self): - """ - Returns a ready-to-use session ticket after prior initialisation. - - After the `SessionTicket()' class was initialised with a master key, - this method encrypts and authenticates the protocol state and returns - the final result which is ready to be sent over the wire. - """ - - self.state.issueDate = int(time.time()) - - # Encrypt the protocol state. - aes = NewAESCipher(self.symmTicketKey, self.IV) - state = repr(self.state) - assert (len(state) % AES_BLOCK_SIZE) == 0 - cryptedState = aes.encrypt(state) - - # Authenticate the encrypted state and the IV. - hmac = hmac_sha256_digest(self.hmacTicketKey, self.IV + cryptedState) - - finalTicket = self.IV + cryptedState + hmac - logging.debug("Returning %d-byte ticket." % len(finalTicket)) - - return finalTicket - - -# Alias class name in order to provide a more intuitive API. -new = SessionTicket diff --git a/pupy/network/lib/transports/scramblesuit/uniformdh.py b/pupy/network/lib/transports/scramblesuit/uniformdh.py deleted file mode 100644 index 74ea404f..00000000 --- a/pupy/network/lib/transports/scramblesuit/uniformdh.py +++ /dev/null @@ -1,210 +0,0 @@ -""" -This module implements a class to deal with Uniform Diffie-Hellman handshakes. - -The class `UniformDH' is used by the server as well as by the client to handle -the Uniform Diffie-Hellman handshake used by ScrambleSuit. -""" - -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -from . import const - -import random - -from ..cryptoutils import SHA256, get_random - -from . import util -from . import mycrypto - -from ..obfs3 import obfs3_dh - -import logging -log = logging - - -class UniformDH(object): - - """ - Provide methods to deal with Uniform Diffie-Hellman handshakes. - - The class provides methods to extract public keys and to generate public - keys wrapped in a valid UniformDH handshake. - """ - - def __init__(self, sharedSecret, weAreServer): - """ - Initialise a UniformDH object. - """ - - # `True' if we are the server; `False' otherwise. - self.weAreServer = weAreServer - - # The shared UniformDH secret. - self.sharedSecret = sharedSecret - - # Cache a UniformDH public key until it's added to the replay table. - self.remotePublicKey = None - - # Uniform Diffie-Hellman object (implemented in obfs3_dh.py). - self.udh = None - - # Used by the server so it can simply echo the client's epoch. - self.echoEpoch = None - - def getRemotePublicKey(self): - """ - Return the cached remote UniformDH public key. - """ - - return self.remotePublicKey - - def receivePublicKey(self, data, callback, srvState=None): - """ - Extract the public key and invoke a callback with the master secret. - - First, the UniformDH public key is extracted out of `data'. Then, the - shared master secret is computed and `callback' is invoked with the - master secret as argument. If any of this fails, `False' is returned. - """ - - # Extract the public key sent by the remote host. - remotePublicKey = self.extractPublicKey(data, srvState) - if not remotePublicKey: - return False - - if self.weAreServer: - self.remotePublicKey = remotePublicKey - # As server, we need a DH object; as client, we already have one. - self.udh = obfs3_dh.UniformDH() - - assert self.udh is not None - - try: - uniformDHSecret = self.udh.get_secret(remotePublicKey) - except ValueError: - raise ValueError("Corrupted public key.") - - # First, hash the 4096-bit UniformDH secret to obtain the master key. - masterKey = SHA256.new(uniformDHSecret).digest() - - # Second, session keys are now derived from the master key. - callback(masterKey) - - return True - - def extractPublicKey(self, data, srvState=None): - """ - Extract and return a UniformDH public key out of `data'. - - Before the public key is touched, the HMAC is verified. If the HMAC is - invalid or some other error occurs, `False' is returned. Otherwise, - the public key is returned. The extracted data is finally drained from - the given `data' object. - """ - - assert self.sharedSecret is not None - - # Do we already have the minimum amount of data? - if len(data) < (const.PUBLIC_KEY_LENGTH + const.MARK_LENGTH + \ - const.HMAC_SHA256_128_LENGTH): - return False - - log.debug("Attempting to extract the remote machine's UniformDH " - "public key out of %d bytes of data." % len(data)) - - handshake = data.peek() - - # First, find the mark to efficiently locate the HMAC. - publicKey = handshake[:const.PUBLIC_KEY_LENGTH] - mark = mycrypto.HMAC_SHA256_128(self.sharedSecret, publicKey) - - index = util.locateMark(mark, handshake) - if not index: - return False - - # Now that we know where the authenticating HMAC is: verify it. - hmacStart = index + const.MARK_LENGTH - existingHMAC = handshake[hmacStart: - (hmacStart + const.HMAC_SHA256_128_LENGTH)] - - authenticated = False - for epoch in util.expandedEpoch(): - myHMAC = mycrypto.HMAC_SHA256_128(self.sharedSecret, - handshake[0: hmacStart] + epoch) - - if util.isValidHMAC(myHMAC, existingHMAC, self.sharedSecret): - self.echoEpoch = epoch - authenticated = True - break - - log.debug("HMAC invalid. Trying next epoch value.") - - if not authenticated: - log.warning("Could not verify the authentication message's HMAC.") - return False - - # Do nothing if the ticket is replayed. Immediately closing the - # connection would be suspicious. - if srvState is not None and srvState.isReplayed(existingHMAC): - log.warning("The HMAC was already present in the replay table.") - return False - - data.drain(index + const.MARK_LENGTH + const.HMAC_SHA256_128_LENGTH) - - if srvState is not None: - log.debug( - "Adding the HMAC authenticating the UniformDH message." - ) - srvState.registerKey(existingHMAC) - - return handshake[:const.PUBLIC_KEY_LENGTH] - - def createHandshake(self, srvState=None): - """ - Create and return a ready-to-be-sent UniformDH handshake. - - The returned handshake data includes the public key, pseudo-random - padding, the mark and the HMAC. If a UniformDH object has not been - initialised yet, a new instance is created. - """ - - assert self.sharedSecret is not None - - log.debug("Creating UniformDH handshake message.") - - if self.udh is None: - self.udh = obfs3_dh.UniformDH() - publicKey = self.udh.get_public() - - assert (const.MAX_PADDING_LENGTH - const.PUBLIC_KEY_LENGTH) >= 0 - - # Subtract the length of the public key to make the handshake on - # average as long as a redeemed ticket. That should thwart statistical - # length-based attacks. - padding = get_random( - random.randint(0, const.MAX_PADDING_LENGTH - const.PUBLIC_KEY_LENGTH)) - - # Add a mark which enables efficient location of the HMAC. - mark = mycrypto.HMAC_SHA256_128(self.sharedSecret, publicKey) - - if self.echoEpoch is None: - epoch = util.getEpoch() - else: - epoch = self.echoEpoch - log.debug("Echoing epoch rather than recreating it.") - - # Authenticate the handshake including the current approximate epoch. - mac = mycrypto.HMAC_SHA256_128(self.sharedSecret, - publicKey + padding + mark + epoch) - - if self.weAreServer and (srvState is not None): - log.debug("Adding the HMAC authenticating the server's UniformDH.") - srvState.registerKey(mac) - - return publicKey + padding + mark + mac - -# Alias class name in order to provide a more intuitive API. -new = UniformDH diff --git a/pupy/network/lib/transports/scramblesuit/util.py b/pupy/network/lib/transports/scramblesuit/util.py deleted file mode 100644 index cb37ff33..00000000 --- a/pupy/network/lib/transports/scramblesuit/util.py +++ /dev/null @@ -1,144 +0,0 @@ -# Original file edited by contact@n1nj4.eu to avoid writing state to disk contact -""" -This module implements several commonly used utility functions. - -The implemented functions can be used to swap variables, write and read data -from files and to convert a number to raw text. -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -import logging -import os -import time -from . import const - -from . import mycrypto - -log = logging - -def setStateLocation(stateLocation): - """ - Set the constant `STATE_LOCATION' to the given `stateLocation'. - - The variable `stateLocation' determines where persistent information (such - as the server's key material) is stored. If `stateLocation' is `None', it - remains to be the current directory. In general, however, it should be a - subdirectory of Tor's data directory. - """ - - if stateLocation is None: - return - - if not stateLocation.endswith('/'): - stateLocation += '/' - - # To be polite, we create a subdirectory inside wherever we are asked to - # store data in. - stateLocation += (const.TRANSPORT_NAME).lower() + '/' - - # ...and if it does not exist yet, we attempt to create the full - # directory path. - if not os.path.exists(stateLocation): - log.info("Creating directory path `%s'." % stateLocation) - os.makedirs(stateLocation) - - log.debug("Setting the state location to `%s'." % stateLocation) - const.STATE_LOCATION = stateLocation - - -def isValidHMAC(hmac1, hmac2, key): - """ - Compares `hmac1' and `hmac2' after HMACing them again using `key'. - - The arguments `hmac1' and `hmac2' are compared. If they are equal, `True' - is returned and otherwise `False'. To prevent timing attacks, double HMAC - verification is used meaning that the two arguments are HMACed again before - (variable-time) string comparison. The idea is taken from: - https://www.isecpartners.com/blog/2011/february/double-hmac-verification.aspx - """ - - assert len(hmac1) == len(hmac2) - - # HMAC the arguments again to prevent timing attacks. - doubleHmac1 = mycrypto.HMAC_SHA256_128(key, hmac1) - doubleHmac2 = mycrypto.HMAC_SHA256_128(key, hmac2) - - if doubleHmac1 != doubleHmac2: - return False - - log.debug("The computed HMAC is valid.") - - return True - - -def locateMark(mark, payload): - """ - Locate the given `mark' in `payload' and return its index. - - The `mark' is placed before the HMAC of a ScrambleSuit authentication - mechanism and makes it possible to efficiently locate the HMAC. If the - `mark' could not be found, `None' is returned. - """ - - index = payload.find(mark, 0, const.MAX_PADDING_LENGTH + const.MARK_LENGTH) - if index < 0: - log.debug("Could not find the mark just yet.") - return None - - if (len(payload) - index - const.MARK_LENGTH) < \ - const.HMAC_SHA256_128_LENGTH: - log.debug("Found the mark but the HMAC is still incomplete.") - return None - - log.debug("Successfully located the mark.") - - return index - - -def getEpoch(): - """ - Return the Unix epoch divided by a constant as string. - - This function returns a coarse-grained version of the Unix epoch. The - seconds passed since the epoch are divided by the constant - `EPOCH_GRANULARITY'. - """ - - return str(int(time.time()) // const.EPOCH_GRANULARITY) - - -def expandedEpoch(): - """ - Return [epoch, epoch-1, epoch+1]. - """ - - epoch = int(getEpoch()) - - return [str(epoch), str(epoch - 1), str(epoch + 1)] - - -def sanitiseBase32(data): - """ - Try to sanitise a Base32 string if it's slightly wrong. - - ScrambleSuit's shared secret might be distributed verbally which could - cause mistakes. This function fixes simple mistakes, e.g., when a user - noted "1" rather than "I". - """ - - data = data.upper() - - if "1" in data: - log.info("Found a \"1\" in Base32-encoded \"%s\". Assuming " \ - "it's actually \"I\"." % data) - data = data.replace("1", "I") - - if "0" in data: - log.info("Found a \"0\" in Base32-encoded \"%s\". Assuming " \ - "it's actually \"O\"." % data) - data = data.replace("0", "O") - - return data diff --git a/pupy/network/transports/scramblesuit/__init__.py b/pupy/network/transports/scramblesuit/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pupy/network/transports/scramblesuit/conf.py b/pupy/network/transports/scramblesuit/conf.py deleted file mode 100644 index 13403f0f..00000000 --- a/pupy/network/transports/scramblesuit/conf.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2015, Nicolas VERDIER (contact@n1nj4.eu) -# Pupy is under the BSD 3-Clause license. see the LICENSE file at the root of -# the project for the detailed licence terms - -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals -from pupy.network.transports import Transport, LAUNCHER_TYPE_BIND -from pupy.network.lib import PupyTCPServer, PupyTCPClient, PupySocketStream -from pupy.network.lib import RSA_AESClient, RSA_AESServer -from pupy.network.lib import chain_transports -from pupy.network.lib.transports.scramblesuit.scramblesuit import ScrambleSuitClient, ScrambleSuitServer - - -class TransportConf(Transport): - info = "TCP transport using obfsproxy's obfs3 transport with a extra rsa+aes layer" - name = "scramblesuit" - server = PupyTCPServer - client = PupyTCPClient - stream = PupySocketStream - credentials = ['SIMPLE_RSA_PRIV_KEY', 'SIMPLE_RSA_PUB_KEY', 'SCRAMBLESUIT_PASSWD'] - - def __init__(self, *args, **kwargs): - Transport.__init__(self, *args, **kwargs) - try: - import pupy_credentials - RSA_PUB_KEY = pupy_credentials.SIMPLE_RSA_PUB_KEY - RSA_PRIV_KEY = pupy_credentials.SIMPLE_RSA_PUB_KEY - SCRAMBLESUIT_PASSWD = pupy_credentials.SCRAMBLESUIT_PASSWD - - except ImportError: - from pupy.pupylib.PupyCredentials import Credentials - credentials = Credentials() - RSA_PUB_KEY = credentials['SIMPLE_RSA_PUB_KEY'] - RSA_PRIV_KEY = credentials['SIMPLE_RSA_PRIV_KEY'] - SCRAMBLESUIT_PASSWD = credentials['SCRAMBLESUIT_PASSWD'] - - self.client_transport_kwargs = {'password': SCRAMBLESUIT_PASSWD} - self.server_transport_kwargs = {'password': SCRAMBLESUIT_PASSWD} - - if self.launcher_type == LAUNCHER_TYPE_BIND: - self.client_transport = chain_transports( - ScrambleSuitClient, - RSA_AESServer.custom(privkey=RSA_PRIV_KEY, rsa_key_size=4096, aes_size=256) - ) - self.server_transport = chain_transports( - ScrambleSuitServer, - RSA_AESClient.custom(pubkey=RSA_PUB_KEY, rsa_key_size=4096, aes_size=256) - ) - - else: - self.client_transport = chain_transports( - ScrambleSuitClient, - RSA_AESClient.custom(pubkey=RSA_PUB_KEY, rsa_key_size=4096, aes_size=256) - ) - self.server_transport = chain_transports( - ScrambleSuitServer, - RSA_AESServer.custom(privkey=RSA_PRIV_KEY, rsa_key_size=4096, aes_size=256) - )