From 339c63f0c8cd4374f6fa26484498eb6fa91b7bca Mon Sep 17 00:00:00 2001 From: Yawning Angel Date: Sun, 17 Aug 2014 17:11:03 +0000 Subject: Massive cleanup/code reorg. * Changed obfs4proxy to be more like obfsproxy in terms of design, including being an easy framework for developing new TCP/IP style pluggable transports. * Added support for also acting as an obfs2/obfs3 client or bridge as a transition measure (and because the code itself is trivial). * Massively cleaned up the obfs4 and related code to be easier to read, and more idiomatic Go-like in style. * To ease deployment, obfs4proxy will now autogenerate the node-id, curve25519 keypair, and drbg seed if none are specified, and save them to a JSON file in the pt_state directory (Fixes Tor bug #12605). --- transports/obfs3/obfs3.go | 358 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 transports/obfs3/obfs3.go (limited to 'transports/obfs3') diff --git a/transports/obfs3/obfs3.go b/transports/obfs3/obfs3.go new file mode 100644 index 0000000..7844443 --- /dev/null +++ b/transports/obfs3/obfs3.go @@ -0,0 +1,358 @@ +/* + * Copyright (c) 2014, Yawning Angel + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +// Package obfs3 provides an implementation of the Tor Project's obfs3 +// obfuscation protocol. +package obfs3 + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/sha256" + "errors" + "io" + "net" + "time" + + "git.torproject.org/pluggable-transports/goptlib.git" + "git.torproject.org/pluggable-transports/obfs4.git/common/csrand" + "git.torproject.org/pluggable-transports/obfs4.git/common/uniformdh" + "git.torproject.org/pluggable-transports/obfs4.git/transports/base" +) + +const ( + transportName = "obfs3" + + clientHandshakeTimeout = time.Duration(30) * time.Second + serverHandshakeTimeout = time.Duration(30) * time.Second + + initiatorKdfString = "Initiator obfuscated data" + responderKdfString = "Responder obfuscated data" + initiatorMagicString = "Initiator magic" + responderMagicString = "Responder magic" + maxPadding = 8194 + keyLen = 16 +) + +// Transport is the obfs3 implementation of the base.Transport interface. +type Transport struct{} + +// Name returns the name of the obfs3 transport protocol. +func (t *Transport) Name() string { + return transportName +} + +// ClientFactory returns a new obfs3ClientFactory instance. +func (t *Transport) ClientFactory(stateDir string) (base.ClientFactory, error) { + cf := &obfs3ClientFactory{transport: t} + return cf, nil +} + +// ServerFactory returns a new obfs3ServerFactory instance. +func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFactory, error) { + sf := &obfs3ServerFactory{transport: t} + return sf, nil +} + +type obfs3ClientFactory struct { + transport base.Transport +} + +func (cf *obfs3ClientFactory) Transport() base.Transport { + return cf.transport +} + +func (cf *obfs3ClientFactory) ParseArgs(args *pt.Args) (interface{}, error) { + return nil, nil +} + +func (cf *obfs3ClientFactory) WrapConn(conn net.Conn, args interface{}) (net.Conn, error) { + return newObfs3ClientConn(conn) +} + +type obfs3ServerFactory struct { + transport base.Transport +} + +func (sf *obfs3ServerFactory) Transport() base.Transport { + return sf.transport +} + +func (sf *obfs3ServerFactory) Args() *pt.Args { + return nil +} + +func (sf *obfs3ServerFactory) WrapConn(conn net.Conn) (net.Conn, error) { + return newObfs3ServerConn(conn) +} + +type obfs3Conn struct { + net.Conn + + isInitiator bool + rxMagic []byte + txMagic []byte + rxBuf *bytes.Buffer + + rx *cipher.StreamReader + tx *cipher.StreamWriter +} + +func newObfs3ClientConn(conn net.Conn) (c *obfs3Conn, err error) { + // Initialize a client connection, and start the handshake timeout. + c = &obfs3Conn{conn, true, nil, nil, new(bytes.Buffer), nil, nil} + deadline := time.Now().Add(clientHandshakeTimeout) + if err = c.SetDeadline(deadline); err != nil { + return nil, err + } + + // Handshake. + if err = c.handshake(); err != nil { + return nil, err + } + + // Disarm the handshake timer. + if err = c.SetDeadline(time.Time{}); err != nil { + return nil, err + } + + return +} + +func newObfs3ServerConn(conn net.Conn) (c *obfs3Conn, err error) { + // Initialize a server connection, and start the handshake timeout. + c = &obfs3Conn{conn, false, nil, nil, new(bytes.Buffer), nil, nil} + deadline := time.Now().Add(serverHandshakeTimeout) + if err = c.SetDeadline(deadline); err != nil { + return nil, err + } + + // Handshake. + if err = c.handshake(); err != nil { + return nil, err + } + + // Disarm the handshake timer. + if err = c.SetDeadline(time.Time{}); err != nil { + return nil, err + } + + return +} + +func (conn *obfs3Conn) handshake() (err error) { + // The party who opens the connection is the 'initiator'; the one who + // accepts it is the 'responder'. Each begins by generating a + // UniformDH keypair, and a random number PADLEN in [0, MAX_PADDING/2]. + // Both parties then send: + // + // PUB_KEY | WR(PADLEN) + var privateKey *uniformdh.PrivateKey + if privateKey, err = uniformdh.GenerateKey(csrand.Reader); err != nil { + return + } + padLen := csrand.IntRange(0, maxPadding/2) + blob := make([]byte, uniformdh.Size+padLen) + var publicKey []byte + if publicKey, err = privateKey.PublicKey.Bytes(); err != nil { + return + } + copy(blob[0:], publicKey) + if err = csrand.Bytes(blob[uniformdh.Size:]); err != nil { + return + } + if _, err = conn.Conn.Write(blob); err != nil { + return + } + + // Read the public key from the peer. + rawPeerPublicKey := make([]byte, uniformdh.Size) + if _, err = io.ReadFull(conn.Conn, rawPeerPublicKey); err != nil { + return + } + var peerPublicKey uniformdh.PublicKey + if err = peerPublicKey.SetBytes(rawPeerPublicKey); err != nil { + return + } + + // After retrieving the public key of the other end, each party + // completes the DH key exchange and generates a shared-secret for the + // session (named SHARED_SECRET). + var sharedSecret []byte + if sharedSecret, err = uniformdh.Handshake(privateKey, &peerPublicKey); err != nil { + return + } + if err = conn.kdf(sharedSecret); err != nil { + return + } + + return +} + +func (conn *obfs3Conn) kdf(sharedSecret []byte) (err error) { + // Using that shared-secret each party derives its encryption keys as + // follows: + // + // INIT_SECRET = HMAC(SHARED_SECRET, "Initiator obfuscated data") + // RESP_SECRET = HMAC(SHARED_SECRET, "Responder obfuscated data") + // INIT_KEY = INIT_SECRET[:KEYLEN] + // INIT_COUNTER = INIT_SECRET[KEYLEN:] + // RESP_KEY = RESP_SECRET[:KEYLEN] + // RESP_COUNTER = RESP_SECRET[KEYLEN:] + initHmac := hmac.New(sha256.New, sharedSecret) + initHmac.Write([]byte(initiatorKdfString)) + initSecret := initHmac.Sum(nil) + initHmac.Reset() + initHmac.Write([]byte(initiatorMagicString)) + initMagic := initHmac.Sum(nil) + + respHmac := hmac.New(sha256.New, sharedSecret) + respHmac.Write([]byte(responderKdfString)) + respSecret := respHmac.Sum(nil) + respHmac.Reset() + respHmac.Write([]byte(responderMagicString)) + respMagic := respHmac.Sum(nil) + + // The INIT_KEY value keys a block cipher (in CTR mode) used to + // encrypt values from initiator to responder thereafter. The counter + // mode's initial counter value is INIT_COUNTER. The RESP_KEY value + // keys a block cipher (in CTR mode) used to encrypt values from + // responder to initiator thereafter. That counter mode's initial + // counter value is RESP_COUNTER. + // + // Note: To have this be the last place where the shared secret is used, + // also generate the magic value to send/scan for here. + var initBlock cipher.Block + if initBlock, err = aes.NewCipher(initSecret[:keyLen]); err != nil { + return err + } + initStream := cipher.NewCTR(initBlock, initSecret[keyLen:]) + + var respBlock cipher.Block + if respBlock, err = aes.NewCipher(respSecret[:keyLen]); err != nil { + return err + } + respStream := cipher.NewCTR(respBlock, respSecret[keyLen:]) + + if conn.isInitiator { + conn.tx = &cipher.StreamWriter{initStream, conn.Conn, nil} + conn.rx = &cipher.StreamReader{respStream, conn.rxBuf} + conn.txMagic = initMagic + conn.rxMagic = respMagic + } else { + conn.tx = &cipher.StreamWriter{respStream, conn.Conn, nil} + conn.rx = &cipher.StreamReader{initStream, conn.rxBuf} + conn.txMagic = respMagic + conn.rxMagic = initMagic + } + + return +} + +func (conn *obfs3Conn) findPeerMagic() error { + var hsBuf [maxPadding + sha256.Size]byte + for { + n, err := conn.Conn.Read(hsBuf[:]) + if err != nil { + // Yes, Read can return partial data and an error, but continuing + // past that is nonsensical. + return err + } + conn.rxBuf.Write(hsBuf[:n]) + + pos := bytes.Index(conn.rxBuf.Bytes(), conn.rxMagic) + if pos == -1 { + if conn.rxBuf.Len() >= maxPadding+sha256.Size { + return errors.New("failed to find peer magic value") + } + continue + } else if pos > maxPadding { + return errors.New("peer sent too much pre-magic-padding") + } + + // Discard the padding/MAC. + pos += len(conn.rxMagic) + _ = conn.rxBuf.Next(pos) + + return nil + } +} + +func (conn *obfs3Conn) Read(b []byte) (n int, err error) { + // If this is the first time we read data post handshake, scan for the + // magic value. + if conn.rxMagic != nil { + if err = conn.findPeerMagic(); err != nil { + conn.Close() + return + } + conn.rxMagic = nil + } + + // If the handshake receive buffer is still present... + if conn.rxBuf != nil { + // And it is empty... + if conn.rxBuf.Len() == 0 { + // There is no more trailing data left from the handshake process, + // so rewire the cipher.StreamReader to pull data from the network + // instead of the temporary receive buffer. + conn.rx.R = conn.Conn + conn.rxBuf = nil + } + } + + return conn.rx.Read(b) +} + +func (conn *obfs3Conn) Write(b []byte) (n int, err error) { + // If this is the first time we write data post handshake, send the + // padding/magic value. + if conn.txMagic != nil { + padLen := csrand.IntRange(0, maxPadding/2) + blob := make([]byte, padLen+len(conn.txMagic)) + if err = csrand.Bytes(blob[:padLen]); err != nil { + conn.Close() + return + } + copy(blob[padLen:], conn.txMagic) + if _, err = conn.Conn.Write(blob); err != nil { + conn.Close() + return + } + conn.txMagic = nil + } + + return conn.tx.Write(b) +} + + +var _ base.ClientFactory = (*obfs3ClientFactory)(nil) +var _ base.ServerFactory = (*obfs3ServerFactory)(nil) +var _ base.Transport = (*Transport)(nil) +var _ net.Conn = (*obfs3Conn)(nil) -- cgit v1.2.3