def validate_external_mode_cli(cls, args): """ Assign the given command line arguments to local variables. """ uniformDHSecret = None try: uniformDHSecret = base64.b32decode( util.sanitiseBase32(args.uniformDHSecret)) except (TypeError, AttributeError) as error: log.error(error.message) raise base.PluggableTransportError("Given password '%s' is not " \ "valid Base32! Run 'generate_password.py' to generate " \ "a good password." % args.uniformDHSecret) parentalApproval = super(ScrambleSuitTransport, cls).validate_external_mode_cli(args) if not parentalApproval: # XXX not very descriptive nor helpful, but the parent class only # returns a boolean without telling us what's wrong. raise base.PluggableTransportError( "Pluggable Transport args invalid: %s" % args) if uniformDHSecret: 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: cls.uniformDHSecret = uniformDHSecret
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 == self.payloadLen == self.flags == None: self.totalLen = pack.ntohs(aes.decrypt(self.recvBuf[16:18])) self.payloadLen = pack.ntohs(aes.decrypt(self.recvBuf[18:20])) self.flags = ord(aes.decrypt(self.recvBuf[20])) if not isSane(self.totalLen, self.payloadLen, self.flags): raise base.PluggableTransportError("Invalid header.") # 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
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 base.PluggableTransportError("Corrupted public key.") # First, hash the 4096-bit UniformDH secret to obtain the master key. masterKey = Crypto.Hash.SHA256.new(uniformDHSecret).digest() # Second, session keys are now derived from the master key. callback(masterKey) return True
def _scan_for_magic(self, data): """ Scan 'data' for the magic string. If found, drain it and all the padding before it. Then open the connection. """ log_prefix = "obfs3:_scan_for_magic()" log.debug("%s: Searching for magic." % log_prefix) assert (self.other_magic_value) chunk = data.peek() index = chunk.find(self.other_magic_value) if index < 0: if (len(data) > MAX_PADDING + HASHLEN): raise base.PluggableTransportError( "obfs3: Too much padding (%d)!" % len(data)) log.debug("%s: Did not find magic this time (%d)." % (log_prefix, len(data))) return index += len(self.other_magic_value) log.debug("%s: Found magic. Draining %d bytes." % (log_prefix, index)) data.drain(index) self.state = ST_OPEN
def unpack(self, data, aes): # Input buffer which is not yet processed and forwarded. self.recvBuf += data fwdBuf = "" # Keep trying to unpack as long as there seems to be enough data. while len(self.recvBuf) >= const.HDR_LENGTH: # Extract length fields if we don't have them already. if self.totalLen == None: self.totalLen = pack.ntohs(aes.decrypt(self.recvBuf[16:18])) self.payloadLen = pack.ntohs(aes.decrypt(self.recvBuf[18:20])) # Abort immediately if the extracted lengths do not make sense. if not message.saneLengths(self.totalLen, self.payloadLen): raise base.PluggableTransportError("Invalid message " \ "length(s): totalLen=%d, payloadLen=%d." % \ (self.totalLen, self.payloadLen)) log.debug("Message header: totalLen=%d, payloadLen=%d." % \ (self.totalLen, self.payloadLen)) if (len(self.recvBuf) - const.HDR_LENGTH) < self.totalLen: return fwdBuf # We have a full message; let's extract it. else: log.debug("Extracting fully received protocol message.") rcvdHMAC = self.recvBuf[0:const.HMAC_LENGTH] vrfyHMAC = mycrypto.MyHMAC_SHA256_128(self.recvHMAC, \ self.recvBuf[const.HMAC_LENGTH:(self.totalLen + \ const.HDR_LENGTH)]) # Abort immediately if the HMAC is invalid. if rcvdHMAC != vrfyHMAC: raise base.PluggableTransportError("Invalid HMAC!") fwdBuf += aes.decrypt(self.recvBuf[const.HDR_LENGTH: \ (self.totalLen+const.HDR_LENGTH)])[:self.payloadLen] self.recvBuf = self.recvBuf[const.HDR_LENGTH + self.totalLen:] # Protocol message extracted - resetting length fields. self.totalLen = self.payloadLen = None log.debug("Unpacked %d bytes of data: 0x%s..." % \ (len(fwdBuf), fwdBuf[:10].encode('hex'))) return fwdBuf
def handle_socks_args(self, args): """ Receive arguments `args' passed over a SOCKS connection. The SOCKS authentication mechanism is (ab)used to pass arguments to pluggable transports. This method receives these arguments and parses them. As argument, we only expect a UniformDH shared secret. """ log.debug("Received the following arguments over SOCKS: %s." % args) if len(args) != 1: raise base.SOCKSArgsError("Too many SOCKS arguments " "(expected 1 but got %d)." % len(args)) # The ScrambleSuit specification defines that the shared secret is # called "password". if not args[0].startswith("password="******"The SOCKS argument must start with " "`password='******'=')[1].strip())) except TypeError as error: log.error(error.message) raise base.PluggableTransportError("Given password '%s' is not " \ "valid Base32! Run 'generate_password.py' to generate " \ "a good password." % args[0].split('=')[1].strip()) rawLength = len(self.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)) self.uniformdh = uniformdh.new(self.uniformDHSecret, self.weAreServer)
def parseControlFields(self): """Extract control message fields.""" # Parse `opcode` if self.opcode == None: self.opcode = self.getOpCode() # Sanity check of the opcode if not isOpCodeSane(self.opcode): raise base.PluggableTransportError("Invalid control opcode: %s" % self.opcode) # Parse args self.argsLen = self.getargsLen() self.args += self.getMessageField(const.ARGS_POS, self.argsLen)
def parseMinHeaderFields(self): """Extract common header fields, if necessary.""" if not self.totalLen == self.payloadLen == self.flags == None: return # Parse common header fields self.totalLen = self.getTotalLen() self.payloadLen = self.getPayloadLen() self.flags = self.getFlags() # Sanity check of the fields if not isSane(self.totalLen, self.payloadLen, self.flags): log.error("TotalLen: %s, PayloadLen: %s, Flags: %s", self.totalLen, self.payloadLen, self.flags) raise base.PluggableTransportError("Invalid header field.")
def __init__(self, payload="", 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 _read_handshake(self, data, circuit): """ Read handshake message, parse the other peer's public key and set up our crypto. """ log_prefix = "obfs3:_read_handshake()" if len(data) < PUBKEY_LEN: log.debug("%s: Not enough bytes for key (%d)." % (log_prefix, len(data))) return log.debug("%s: Got %d bytes of handshake data (waiting for key)." % (log_prefix, len(data))) # Get the public key from the handshake message, do the DH and # get the shared secret. other_pubkey = data.read(PUBKEY_LEN) try: self.shared_secret = self.dh.get_secret(other_pubkey) except ValueError: raise base.PluggableTransportError( "obfs3: Corrupted public key '%s'" % repr(other_pubkey)) log.debug("Got public key: %s.\nGot shared secret: %s" % (repr(other_pubkey), repr(self.shared_secret))) # Set up our crypto. self.send_crypto = self._derive_crypto(self.send_keytype) self.recv_crypto = self._derive_crypto(self.recv_keytype) self.other_magic_value = hmac_sha256.hmac_sha256_digest( self.shared_secret, self.recv_magic_const) # Send our magic value to the remote end and append the queued outgoing data. # Padding is prepended so that the server does not just send the 32-byte magic # in a single TCP segment. padding_length = random.randint(0, MAX_PADDING / 2) magic = hmac_sha256.hmac_sha256_digest(self.shared_secret, self.send_magic_const) message = rand.random_bytes( padding_length) + magic + self.send_crypto.crypt(self.queued_data) self.queued_data = '' log.debug("%s: Transmitting %d bytes (with magic)." % (log_prefix, len(message))) circuit.downstream.write(message) self.state = ST_SEARCHING_MAGIC
def __init__(self, payload='', paddingLen=0, flags=const.FLAG_DATA, opcode=None, args=""): self.payload = payload self.payloadLen = len(self.payload) self.totalLen = self.payloadLen + paddingLen if (self.totalLen) > const.MPU: raise base.PluggableTransportError( "The transport created a message longer than TCP's MTU.") self.sndTime = 0 self.rcvTime = 0 self.flags = flags self.opcode = opcode self.argsLen = len(args) self.args = args
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 expand(self): """Returns the expanded output key material which is calculated based on the given PRK, info and L.""" tmp = "" # 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.") while self.length > len(self.T): tmp = Crypto.Hash.HMAC.new(self.prk, tmp + self.info + \ chr(self.ctr), Crypto.Hash.SHA256).digest() self.T += tmp self.ctr += 1 return self.T[:self.length]
def setup( cls, transportConfig ): """ Called once when obfsproxy starts. """ 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 as error: log.error(error.message) raise base.PluggableTransportError("Given password '%s' " \ "is not valid Base32! Run " \ "'generate_password.py' to generate a good " \ "password." % cfg["password"]) 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 state.writeServerDescriptor(cls.uniformDHSecret, transportConfig.getBindAddr(), cls.weAreExternal)
def receivedDownstream(self, data): """ Got data from downstream. We need to de-obfuscate them and proxy them upstream. """ log_prefix = "obfs2 receivedDownstream" # used in logs if self.state == ST_WAIT_FOR_KEY: log.debug("%s: Waiting for key." % log_prefix) if len(data) < SEED_LENGTH + 8: log.debug("%s: Not enough bytes for key (%d)." % (log_prefix, len(data))) return data # incomplete if self.we_are_initiator: self.responder_seed = data.read(SEED_LENGTH) else: self.initiator_seed = data.read(SEED_LENGTH) # Now that we got the other seed, let's set up our crypto. self.send_crypto = self._derive_crypto(self.send_keytype) self.recv_crypto = self._derive_crypto(self.recv_keytype) self.recv_padding_crypto = \ self._derive_padding_crypto(self.responder_seed if self.we_are_initiator else self.initiator_seed, self.recv_pad_keytype) # XXX maybe faster with a single d() instead of two. magic = srlz.ntohl(self.recv_padding_crypto.crypt(data.read(4))) padding_length = srlz.ntohl(self.recv_padding_crypto.crypt(data.read(4))) log.debug("%s: Got %d bytes of handshake data (padding_length: %d, magic: %s)" % \ (log_prefix, len(data), padding_length, hex(magic))) if magic != MAGIC_VALUE: raise base.PluggableTransportError("obfs2: Corrupted magic value '%s'" % hex(magic)) if padding_length > MAX_PADDING: raise base.PluggableTransportError("obfs2: Too big padding length '%s'" % padding_length) self.padding_left_to_read = padding_length self.state = ST_WAIT_FOR_PADDING while self.padding_left_to_read: if not data: return n_to_drain = self.padding_left_to_read if (self.padding_left_to_read > len(data)): n_to_drain = len(data) data.drain(n_to_drain) self.padding_left_to_read -= n_to_drain log.debug("%s: Consumed %d bytes of padding, %d still to come (%d).", log_prefix, n_to_drain, self.padding_left_to_read, len(data)) self.state = ST_OPEN log.debug("%s: Processing %d bytes of application data.", log_prefix, len(data)) if self.pending_data_to_send: log.debug("%s: We got pending data to send and our crypto is ready. Pushing!" % log_prefix) self.receivedUpstream(self.circuit.upstream.buffer) # XXX touching guts of network.py self.pending_data_to_send = False self.circuit.upstream.write(self.recv_crypto.crypt(data.read()))
def newControl(self, opcode, args="", payload="", paddingLen=0): """Shortcut to create a single control message.""" if len(args) > const.MPU: raise base.PluggableTransportError("Arguments are too long.") return self.new(payload, paddingLen, const.FLAG_CONTROL, opcode, args)