mirror of https://github.com/n1nj4sec/pupy.git
removing deprecated python2 scramblesuit transport
This commit is contained in:
parent
f7da2be7a0
commit
cf623e3217
|
@ -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
|
||||
|
|
|
@ -18,6 +18,7 @@ import sys
|
|||
|
||||
if sys.version_info.major > 2:
|
||||
xrange = range
|
||||
long = int
|
||||
|
||||
def to_byte(x):
|
||||
return bytes((x,))
|
||||
|
|
|
@ -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"
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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]
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
)
|
Loading…
Reference in New Issue