removing deprecated python2 scramblesuit transport

This commit is contained in:
n1nj4sec 2022-11-06 23:11:22 +01:00
parent f7da2be7a0
commit cf623e3217
16 changed files with 1 additions and 2297 deletions

View File

@ -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

View File

@ -18,6 +18,7 @@ import sys
if sys.version_info.major > 2:
xrange = range
long = int
def to_byte(x):
return bytes((x,))

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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