summaryrefslogtreecommitdiff
path: root/transports
diff options
context:
space:
mode:
authorYawning Angel <yawning@torproject.org>2014-08-17 17:11:03 +0000
committerYawning Angel <yawning@torproject.org>2014-08-17 17:11:03 +0000
commit339c63f0c8cd4374f6fa26484498eb6fa91b7bca (patch)
treeedef1bebc1a40a653b2b9f0bd02f53c8c4923ac3 /transports
parent8a3eb4b30965975951a92dde8f68ce17cb08ac8e (diff)
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).
Diffstat (limited to 'transports')
-rw-r--r--transports/base/base.go88
-rw-r--r--transports/obfs2/obfs2.go367
-rw-r--r--transports/obfs3/obfs3.go358
-rw-r--r--transports/obfs4/framing/framing.go308
-rw-r--r--transports/obfs4/framing/framing_test.go169
-rw-r--r--transports/obfs4/handshake_ntor.go426
-rw-r--r--transports/obfs4/handshake_ntor_test.go222
-rw-r--r--transports/obfs4/obfs4.go579
-rw-r--r--transports/obfs4/packet.go179
-rw-r--r--transports/obfs4/statefile.go156
-rw-r--r--transports/transports.go91
11 files changed, 2943 insertions, 0 deletions
diff --git a/transports/base/base.go b/transports/base/base.go
new file mode 100644
index 0000000..e81ea03
--- /dev/null
+++ b/transports/base/base.go
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * 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 base provides the common interface that each supported transport
+// protocol must implement.
+package base
+
+import (
+ "net"
+
+ "git.torproject.org/pluggable-transports/goptlib.git"
+)
+
+// ClientFactory is the interface that defines the factory for creating
+// pluggable transport protocol client instances.
+type ClientFactory interface {
+ // Transport returns the Transport instance that this ClientFactory belongs
+ // to.
+ Transport() Transport
+
+ // ParseArgs parses the supplied arguments into an internal representation
+ // for use with WrapConn. This routine is called before the outgoing
+ // TCP/IP connection is created to allow doing things (like keypair
+ // generation) to be hidden from third parties.
+ ParseArgs(args *pt.Args) (interface{}, error)
+
+ // WrapConn wraps the provided net.Conn with a transport protocol
+ // implementation, and does whatever is required (eg: handshaking) to get
+ // the connection to a point where it is ready to relay data.
+ WrapConn(conn net.Conn, args interface{}) (net.Conn, error)
+}
+
+// ServerFactory is the interface that defines the factory for creating
+// plugable transport protocol server instances. As the arguments are the
+// property of the factory, validation is done at factory creation time.
+type ServerFactory interface {
+ // Transport returns the Transport instance that this ServerFactory belongs
+ // to.
+ Transport() Transport
+
+ // Args returns the Args required on the client side to handshake with
+ // server connections created by this factory.
+ Args() *pt.Args
+
+ // WrapConn wraps the provided net.Conn with a transport protocol
+ // implementation, and does whatever is required (eg: handshaking) to get
+ // the connection to a point where it is ready to relay data.
+ WrapConn(conn net.Conn) (net.Conn, error)
+}
+
+// Transport is an interface that defines a pluggable transport protocol.
+type Transport interface {
+ // Name returns the name of the transport protocol. It MUST be a valid C
+ // identifier.
+ Name() string
+
+ // ClientFactory returns a ClientFactory instance for this transport
+ // protocol.
+ ClientFactory(stateDir string) (ClientFactory, error)
+
+ // ServerFactory returns a ServerFactory instance for this transport
+ // protocol. This can fail if the provided arguments are invalid.
+ ServerFactory(stateDir string, args *pt.Args) (ServerFactory, error)
+}
diff --git a/transports/obfs2/obfs2.go b/transports/obfs2/obfs2.go
new file mode 100644
index 0000000..3490646
--- /dev/null
+++ b/transports/obfs2/obfs2.go
@@ -0,0 +1,367 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * 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 obfs2 provides an implementation of the Tor Project's obfs2
+// obfuscation protocol. This protocol is considered trivially broken by most
+// sophisticated adversaries.
+package obfs2
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/sha256"
+ "encoding/binary"
+ "fmt"
+ "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/transports/base"
+)
+
+const (
+ transportName = "obfs2"
+ sharedSecretArg = "shared-secret"
+
+ clientHandshakeTimeout = time.Duration(30) * time.Second
+ serverHandshakeTimeout = time.Duration(30) * time.Second
+
+ magicValue = 0x2bf5ca7e
+ initiatorPadString = "Initiator obfuscation padding"
+ responderPadString = "Responder obfuscation padding"
+ initiatorKdfString = "Initiator obfuscated data"
+ responderKdfString = "Responder obfuscated data"
+ maxPadding = 8192
+ keyLen = 16
+ seedLen = 16
+ hsLen = 4 + 4
+)
+
+func validateArgs(args *pt.Args) error {
+ if _, ok := args.Get(sharedSecretArg); ok {
+ // "shared-secret" is something no bridges use in practice and is thus
+ // unimplemented.
+ return fmt.Errorf("unsupported argument '%s'", sharedSecretArg)
+ }
+ return nil
+}
+
+// Transport is the obfs2 implementation of the base.Transport interface.
+type Transport struct{}
+
+// Name returns the name of the obfs2 transport protocol.
+func (t *Transport) Name() string {
+ return transportName
+}
+
+// ClientFactory returns a new obfs2ClientFactory instance.
+func (t *Transport) ClientFactory(stateDir string) (base.ClientFactory, error) {
+ cf := &obfs2ClientFactory{transport: t}
+ return cf, nil
+}
+
+// ServerFactory returns a new obfs2ServerFactory instance.
+func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFactory, error) {
+ if err := validateArgs(args); err != nil {
+ return nil, err
+ }
+
+ sf := &obfs2ServerFactory{t}
+ return sf, nil
+}
+
+type obfs2ClientFactory struct {
+ transport base.Transport
+}
+
+func (cf *obfs2ClientFactory) Transport() base.Transport {
+ return cf.transport
+}
+
+func (cf *obfs2ClientFactory) ParseArgs(args *pt.Args) (interface{}, error) {
+ return nil, validateArgs(args)
+}
+
+func (cf *obfs2ClientFactory) WrapConn(conn net.Conn, args interface{}) (net.Conn, error) {
+ return newObfs2ClientConn(conn)
+}
+
+type obfs2ServerFactory struct {
+ transport base.Transport
+}
+
+func (sf *obfs2ServerFactory) Transport() base.Transport {
+ return sf.transport
+}
+
+func (sf *obfs2ServerFactory) Args() *pt.Args {
+ return nil
+}
+
+func (sf *obfs2ServerFactory) WrapConn(conn net.Conn) (net.Conn, error) {
+ return newObfs2ServerConn(conn)
+}
+
+type obfs2Conn struct {
+ net.Conn
+
+ isInitiator bool
+
+ rx *cipher.StreamReader
+ tx *cipher.StreamWriter
+}
+
+func (conn *obfs2Conn) Read(b []byte) (int, error) {
+ return conn.rx.Read(b)
+}
+
+func (conn *obfs2Conn) Write(b []byte) (int, error) {
+ return conn.tx.Write(b)
+}
+
+func newObfs2ClientConn(conn net.Conn) (c *obfs2Conn, err error) {
+ // Initialize a client connection, and start the handshake timeout.
+ c = &obfs2Conn{conn, true, 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 newObfs2ServerConn(conn net.Conn) (c *obfs2Conn, err error) {
+ // Initialize a server connection, and start the handshake timeout.
+ c = &obfs2Conn{conn, false, 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 *obfs2Conn) handshake() (err error) {
+ // Each begins by generating a seed and a padding key as follows.
+ // The initiator generates:
+ //
+ // INIT_SEED = SR(SEED_LENGTH)
+ // INIT_PAD_KEY = MAC("Initiator obfuscation padding", INIT_SEED)[:KEYLEN]
+ //
+ // And the responder generates:
+ //
+ // RESP_SEED = SR(SEED_LENGTH)
+ // RESP_PAD_KEY = MAC("Responder obfuscation padding", INIT_SEED)[:KEYLEN]
+ //
+ // Each then generates a random number PADLEN in range from 0 through
+ // MAX_PADDING (inclusive).
+ var seed [seedLen]byte
+ if err = csrand.Bytes(seed[:]); err != nil {
+ return
+ }
+ var padMagic []byte
+ if conn.isInitiator {
+ padMagic = []byte(initiatorPadString)
+ } else {
+ padMagic = []byte(responderPadString)
+ }
+ padKey, padIV := hsKdf(padMagic, seed[:], conn.isInitiator)
+ padLen := uint32(csrand.IntRange(0, maxPadding))
+
+ hsBlob := make([]byte, hsLen+padLen)
+ binary.BigEndian.PutUint32(hsBlob[0:4], magicValue)
+ binary.BigEndian.PutUint32(hsBlob[4:8], padLen)
+ if padLen > 0 {
+ if err = csrand.Bytes(hsBlob[8:]); err != nil {
+ return
+ }
+ }
+
+ // The initiator then sends:
+ //
+ // INIT_SEED | E(INIT_PAD_KEY, UINT32(MAGIC_VALUE) | UINT32(PADLEN) | WR(PADLEN))
+ //
+ // and the responder sends:
+ //
+ // RESP_SEED | E(RESP_PAD_KEY, UINT32(MAGIC_VALUE) | UINT32(PADLEN) | WR(PADLEN))
+ var txBlock cipher.Block
+ if txBlock, err = aes.NewCipher(padKey); err != nil {
+ return
+ }
+ txStream := cipher.NewCTR(txBlock, padIV)
+ conn.tx = &cipher.StreamWriter{txStream, conn.Conn, nil}
+ if _, err = conn.Conn.Write(seed[:]); err != nil {
+ return
+ }
+ if _, err = conn.Write(hsBlob); err != nil {
+ return
+ }
+
+ // Upon receiving the SEED from the other party, each party derives
+ // the other party's padding key value as above, and decrypts the next
+ // 8 bytes of the key establishment message.
+ var peerSeed [seedLen]byte
+ if _, err = io.ReadFull(conn.Conn, peerSeed[:]); err != nil {
+ return
+ }
+ var peerPadMagic []byte
+ if conn.isInitiator {
+ peerPadMagic = []byte(responderPadString)
+ } else {
+ peerPadMagic = []byte(initiatorPadString)
+ }
+ peerKey, peerIV := hsKdf(peerPadMagic, peerSeed[:], !conn.isInitiator)
+ var rxBlock cipher.Block
+ if rxBlock, err = aes.NewCipher(peerKey); err != nil {
+ return
+ }
+ rxStream := cipher.NewCTR(rxBlock, peerIV)
+ conn.rx = &cipher.StreamReader{rxStream, conn.Conn}
+ hsHdr := make([]byte, hsLen)
+ if _, err = io.ReadFull(conn, hsHdr[:]); err != nil {
+ return
+ }
+
+ // If the MAGIC_VALUE does not match, or the PADLEN value is greater than
+ // MAX_PADDING, the party receiving it should close the connection
+ // immediately.
+ if peerMagic := binary.BigEndian.Uint32(hsHdr[0:4]); peerMagic != magicValue {
+ err = fmt.Errorf("invalid magic value: %x", peerMagic)
+ return
+ }
+ padLen = binary.BigEndian.Uint32(hsHdr[4:8])
+ if padLen > maxPadding {
+ err = fmt.Errorf("padlen too long: %d", padLen)
+ return
+ }
+
+ // Otherwise, it should read the remaining PADLEN bytes of padding data
+ // and discard them.
+ tmp := make([]byte, padLen)
+ if _, err = io.ReadFull(conn.Conn, tmp); err != nil { // Note: Skips AES.
+ return
+ }
+
+ // Derive the actual keys.
+ if err = conn.kdf(seed[:], peerSeed[:]); err != nil {
+ return
+ }
+
+ return
+}
+
+func (conn *obfs2Conn) kdf(seed, peerSeed []byte) (err error) {
+ // Additional keys are then derived as:
+ //
+ // INIT_SECRET = MAC("Initiator obfuscated data", INIT_SEED|RESP_SEED)
+ // RESP_SECRET = MAC("Responder obfuscated data", INIT_SEED|RESP_SEED)
+ // INIT_KEY = INIT_SECRET[:KEYLEN]
+ // INIT_IV = INIT_SECRET[KEYLEN:]
+ // RESP_KEY = RESP_SECRET[:KEYLEN]
+ // RESP_IV = RESP_SECRET[KEYLEN:]
+ combSeed := make([]byte, 0, seedLen*2)
+ if conn.isInitiator {
+ combSeed = append(combSeed, seed...)
+ combSeed = append(combSeed, peerSeed...)
+ } else {
+ combSeed = append(combSeed, peerSeed...)
+ combSeed = append(combSeed, seed...)
+ }
+
+ initKey, initIV := hsKdf([]byte(initiatorKdfString), combSeed, true)
+ var initBlock cipher.Block
+ if initBlock, err = aes.NewCipher(initKey); err != nil {
+ return
+ }
+ initStream := cipher.NewCTR(initBlock, initIV)
+
+ respKey, respIV := hsKdf([]byte(responderKdfString), combSeed, false)
+ var respBlock cipher.Block
+ if respBlock, err = aes.NewCipher(respKey); err != nil {
+ return
+ }
+ respStream := cipher.NewCTR(respBlock, respIV)
+
+ if conn.isInitiator {
+ conn.tx.S = initStream
+ conn.rx.S = respStream
+ } else {
+ conn.tx.S = respStream
+ conn.rx.S = initStream
+ }
+
+ return
+}
+
+func hsKdf(magic, seed []byte, isInitiator bool) (padKey, padIV []byte) {
+ // The actual key/IV is derived in the form of:
+ // m = MAC(magic, seed)
+ // KEY = m[:KEYLEN]
+ // IV = m[KEYLEN:]
+ m := mac(magic, seed)
+ padKey = m[:keyLen]
+ padIV = m[keyLen:]
+
+ return
+}
+
+func mac(s, x []byte) []byte {
+ // H(x) is SHA256 of x.
+ // MAC(s, x) = H(s | x | s)
+ h := sha256.New()
+ h.Write(s)
+ h.Write(x)
+ h.Write(s)
+ return h.Sum(nil)
+}
+
+var _ base.ClientFactory = (*obfs2ClientFactory)(nil)
+var _ base.ServerFactory = (*obfs2ServerFactory)(nil)
+var _ base.Transport = (*Transport)(nil)
+var _ net.Conn = (*obfs2Conn)(nil)
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 <yawning at torproject dot org>
+ * 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)
diff --git a/transports/obfs4/framing/framing.go b/transports/obfs4/framing/framing.go
new file mode 100644
index 0000000..04e788f
--- /dev/null
+++ b/transports/obfs4/framing/framing.go
@@ -0,0 +1,308 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * 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 framing implements the obfs4 link framing and cryptography.
+//
+// The Encoder/Decoder shared secret format is:
+// uint8_t[32] NaCl secretbox key
+// uint8_t[16] NaCl Nonce prefix
+// uint8_t[16] SipHash-2-4 key (used to obfsucate length)
+// uint8_t[8] SipHash-2-4 IV
+//
+// The frame format is:
+// uint16_t length (obfsucated, big endian)
+// NaCl secretbox (Poly1305/XSalsa20) containing:
+// uint8_t[16] tag (Part of the secretbox construct)
+// uint8_t[] payload
+//
+// The length field is length of the NaCl secretbox XORed with the truncated
+// SipHash-2-4 digest ran in OFB mode.
+//
+// Initialize K, IV[0] with values from the shared secret.
+// On each packet, IV[n] = H(K, IV[n - 1])
+// mask[n] = IV[n][0:2]
+// obfsLen = length ^ mask[n]
+//
+// The NaCl secretbox (Poly1305/XSalsa20) nonce format is:
+// uint8_t[24] prefix (Fixed)
+// uint64_t counter (Big endian)
+//
+// The counter is initialized to 1, and is incremented on each frame. Since
+// the protocol is designed to be used over a reliable medium, the nonce is not
+// transmitted over the wire as both sides of the conversation know the prefix
+// and the initial counter value. It is imperative that the counter does not
+// wrap, and sessions MUST terminate before 2^64 frames are sent.
+//
+package framing
+
+import (
+ "bytes"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+
+ "code.google.com/p/go.crypto/nacl/secretbox"
+
+ "git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/drbg"
+)
+
+const (
+ // MaximumSegmentLength is the length of the largest possible segment
+ // including overhead.
+ MaximumSegmentLength = 1500 - (40 + 12)
+
+ // FrameOverhead is the length of the framing overhead.
+ FrameOverhead = lengthLength + secretbox.Overhead
+
+ // MaximumFramePayloadLength is the length of the maximum allowed payload
+ // per frame.
+ MaximumFramePayloadLength = MaximumSegmentLength - FrameOverhead
+
+ // KeyLength is the length of the Encoder/Decoder secret key.
+ KeyLength = keyLength + noncePrefixLength + drbg.SeedLength
+
+ maxFrameLength = MaximumSegmentLength - lengthLength
+ minFrameLength = FrameOverhead - lengthLength
+
+ keyLength = 32
+
+ noncePrefixLength = 16
+ nonceCounterLength = 8
+ nonceLength = noncePrefixLength + nonceCounterLength
+
+ lengthLength = 2
+)
+
+// Error returned when Decoder.Decode() requires more data to continue.
+var ErrAgain = errors.New("framing: More data needed to decode")
+
+// Error returned when Decoder.Decode() failes to authenticate a frame.
+var ErrTagMismatch = errors.New("framing: Poly1305 tag mismatch")
+
+// Error returned when the NaCl secretbox nonce's counter wraps (FATAL).
+var ErrNonceCounterWrapped = errors.New("framing: Nonce counter wrapped")
+
+// InvalidPayloadLengthError is the error returned when Encoder.Encode()
+// rejects the payload length.
+type InvalidPayloadLengthError int
+
+func (e InvalidPayloadLengthError) Error() string {
+ return fmt.Sprintf("framing: Invalid payload length: %d", int(e))
+}
+
+type boxNonce struct {
+ prefix [noncePrefixLength]byte
+ counter uint64
+}
+
+func (nonce *boxNonce) init(prefix []byte) {
+ if noncePrefixLength != len(prefix) {
+ panic(fmt.Sprintf("BUG: Nonce prefix length invalid: %d", len(prefix)))
+ }
+
+ copy(nonce.prefix[:], prefix)
+ nonce.counter = 1
+}
+
+func (nonce boxNonce) bytes(out *[nonceLength]byte) error {
+ // The security guarantee of Poly1305 is broken if a nonce is ever reused
+ // for a given key. Detect this by checking for counter wraparound since
+ // we start each counter at 1. If it ever happens that more than 2^64 - 1
+ // frames are transmitted over a given connection, support for rekeying
+ // will be neccecary, but that's unlikely to happen.
+ if nonce.counter == 0 {
+ return ErrNonceCounterWrapped
+ }
+
+ copy(out[:], nonce.prefix[:])
+ binary.BigEndian.PutUint64(out[noncePrefixLength:], nonce.counter)
+
+ return nil
+}
+
+// Encoder is a frame encoder instance.
+type Encoder struct {
+ key [keyLength]byte
+ nonce boxNonce
+ drbg *drbg.HashDrbg
+}
+
+// NewEncoder creates a new Encoder instance. It must be supplied a slice
+// containing exactly KeyLength bytes of keying material.
+func NewEncoder(key []byte) *Encoder {
+ if len(key) != KeyLength {
+ panic(fmt.Sprintf("BUG: Invalid encoder key length: %d", len(key)))
+ }
+
+ encoder := new(Encoder)
+ copy(encoder.key[:], key[0:keyLength])
+ encoder.nonce.init(key[keyLength : keyLength+noncePrefixLength])
+ seed, err := drbg.SeedFromBytes(key[keyLength+noncePrefixLength:])
+ if err != nil {
+ panic(fmt.Sprintf("BUG: Failed to initialize DRBG: %s", err))
+ }
+ encoder.drbg, _ = drbg.NewHashDrbg(seed)
+
+ return encoder
+}
+
+// Encode encodes a single frame worth of payload and returns the encoded
+// length. InvalidPayloadLengthError is recoverable, all other errors MUST be
+// treated as fatal and the session aborted.
+func (encoder *Encoder) Encode(frame, payload []byte) (n int, err error) {
+ payloadLen := len(payload)
+ if MaximumFramePayloadLength < payloadLen {
+ return 0, InvalidPayloadLengthError(payloadLen)
+ }
+ if len(frame) < payloadLen+FrameOverhead {
+ return 0, io.ErrShortBuffer
+ }
+
+ // Generate a new nonce.
+ var nonce [nonceLength]byte
+ err = encoder.nonce.bytes(&nonce)
+ if err != nil {
+ return 0, err
+ }
+ encoder.nonce.counter++
+
+ // Encrypt and MAC payload.
+ box := secretbox.Seal(frame[:lengthLength], payload, &nonce, &encoder.key)
+
+ // Obfuscate the length.
+ length := uint16(len(box) - lengthLength)
+ lengthMask := encoder.drbg.NextBlock()
+ length ^= binary.BigEndian.Uint16(lengthMask)
+ binary.BigEndian.PutUint16(frame[:2], length)
+
+ // Return the frame.
+ return len(box), nil
+}
+
+// Decoder is a frame decoder instance.
+type Decoder struct {
+ key [keyLength]byte
+ nonce boxNonce
+ drbg *drbg.HashDrbg
+
+ nextNonce [nonceLength]byte
+ nextLength uint16
+ nextLengthInvalid bool
+}
+
+// NewDecoder creates a new Decoder instance. It must be supplied a slice
+// containing exactly KeyLength bytes of keying material.
+func NewDecoder(key []byte) *Decoder {
+ if len(key) != KeyLength {
+ panic(fmt.Sprintf("BUG: Invalid decoder key length: %d", len(key)))
+ }
+
+ decoder := new(Decoder)
+ copy(decoder.key[:], key[0:keyLength])
+ decoder.nonce.init(key[keyLength : keyLength+noncePrefixLength])
+ seed, err := drbg.SeedFromBytes(key[keyLength+noncePrefixLength:])
+ if err != nil {
+ panic(fmt.Sprintf("BUG: Failed to initialize DRBG: %s", err))
+ }
+ decoder.drbg, _ = drbg.NewHashDrbg(seed)
+
+ return decoder
+}
+
+// Decode decodes a stream of data and returns the length if any. ErrAgain is
+// a temporary failure, all other errors MUST be treated as fatal and the
+// session aborted.
+func (decoder *Decoder) Decode(data []byte, frames *bytes.Buffer) (int, error) {
+ // A length of 0 indicates that we do not know how big the next frame is
+ // going to be.
+ if decoder.nextLength == 0 {
+ // Attempt to pull out the next frame length.
+ if lengthLength > frames.Len() {
+ return 0, ErrAgain
+ }
+
+ // Remove the length field from the buffer.
+ var obfsLen [lengthLength]byte
+ _, err := io.ReadFull(frames, obfsLen[:])
+ if err != nil {
+ return 0, err
+ }
+
+ // Derive the nonce the peer used.
+ err = decoder.nonce.bytes(&decoder.nextNonce)
+ if err != nil {
+ return 0, err
+ }
+
+ // Deobfuscate the length field.
+ length := binary.BigEndian.Uint16(obfsLen[:])
+ lengthMask := decoder.drbg.NextBlock()
+ length ^= binary.BigEndian.Uint16(lengthMask)
+ if maxFrameLength < length || minFrameLength > length {
+ // Per "Plaintext Recovery Attacks Against SSH" by
+ // Martin R. Albrecht, Kenneth G. Paterson and Gaven J. Watson,
+ // there are a class of attacks againt protocols that use similar
+ // sorts of framing schemes.
+ //
+ // While obfs4 should not allow plaintext recovery (CBC mode is
+ // not used), attempt to mitigate out of bound frame length errors
+ // by pretending that the length was a random valid range as per
+ // the countermeasure suggested by Denis Bider in section 6 of the
+ // paper.
+
+ decoder.nextLengthInvalid = true
+ length = uint16(csrand.IntRange(minFrameLength, maxFrameLength))
+ }
+ decoder.nextLength = length
+ }
+
+ if int(decoder.nextLength) > frames.Len() {
+ return 0, ErrAgain
+ }
+
+ // Unseal the frame.
+ var box [maxFrameLength]byte
+ n, err := io.ReadFull(frames, box[:decoder.nextLength])
+ if err != nil {
+ return 0, err
+ }
+ out, ok := secretbox.Open(data[:0], box[:n], &decoder.nextNonce, &decoder.key)
+ if !ok || decoder.nextLengthInvalid {
+ // When a random length is used (on length error) the tag should always
+ // mismatch, but be paranoid.
+ return 0, ErrTagMismatch
+ }
+
+ // Clean up and prepare for the next frame.
+ decoder.nextLength = 0
+ decoder.nonce.counter++
+
+ return len(out), nil
+}
diff --git a/transports/obfs4/framing/framing_test.go b/transports/obfs4/framing/framing_test.go
new file mode 100644
index 0000000..03e0d3b
--- /dev/null
+++ b/transports/obfs4/framing/framing_test.go
@@ -0,0 +1,169 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * 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 framing
+
+import (
+ "bytes"
+ "crypto/rand"
+ "testing"
+)
+
+func generateRandomKey() []byte {
+ key := make([]byte, KeyLength)
+
+ _, err := rand.Read(key)
+ if err != nil {
+ panic(err)
+ }
+
+ return key
+}
+
+func newEncoder(t *testing.T) *Encoder {
+ // Generate a key to use.
+ key := generateRandomKey()
+
+ encoder := NewEncoder(key)
+ if encoder == nil {
+ t.Fatalf("NewEncoder returned nil")
+ }
+
+ return encoder
+}
+
+// TestNewEncoder tests the Encoder ctor.
+func TestNewEncoder(t *testing.T) {
+ encoder := newEncoder(t)
+ _ = encoder
+}
+
+// TestEncoder_Encode tests Encoder.Encode.
+func TestEncoder_Encode(t *testing.T) {
+ encoder := newEncoder(t)
+
+ buf := make([]byte, MaximumFramePayloadLength)
+ _, _ = rand.Read(buf) // YOLO
+ for i := 0; i <= MaximumFramePayloadLength; i++ {
+ var frame [MaximumSegmentLength]byte
+ n, err := encoder.Encode(frame[:], buf[0:i])
+ if err != nil {
+ t.Fatalf("Encoder.encode([%d]byte), failed: %s", i, err)
+ }
+ if n != i+FrameOverhead {
+ t.Fatalf("Unexpected encoded framesize: %d, expecting %d", n, i+
+ FrameOverhead)
+ }
+ }
+}
+
+// TestEncoder_Encode_Oversize tests oversized frame rejection.
+func TestEncoder_Encode_Oversize(t *testing.T) {
+ encoder := newEncoder(t)
+
+ var frame [MaximumSegmentLength]byte
+ var buf [MaximumFramePayloadLength + 1]byte
+ _, _ = rand.Read(buf[:]) // YOLO
+ _, err := encoder.Encode(frame[:], buf[:])
+ if _, ok := err.(InvalidPayloadLengthError); !ok {
+ t.Error("Encoder.encode() returned unexpected error:", err)
+ }
+}
+
+// TestNewDecoder tests the Decoder ctor.
+func TestNewDecoder(t *testing.T) {
+ key := generateRandomKey()
+ decoder := NewDecoder(key)
+ if decoder == nil {
+ t.Fatalf("NewDecoder returned nil")
+ }
+}
+
+// TestDecoder_Decode tests Decoder.Decode.
+func TestDecoder_Decode(t *testing.T) {
+ key := generateRandomKey()
+
+ encoder := NewEncoder(key)
+ decoder := NewDecoder(key)
+
+ var buf [MaximumFramePayloadLength]byte
+ _, _ = rand.Read(buf[:]) // YOLO
+ for i := 0; i <= MaximumFramePayloadLength; i++ {
+ var frame [MaximumSegmentLength]byte
+ encLen, err := encoder.Encode(frame[:], buf[0:i])
+ if err != nil {
+ t.Fatalf("Encoder.encode([%d]byte), failed: %s", i, err)
+ }
+ if encLen != i+FrameOverhead {
+ t.Fatalf("Unexpected encoded framesize: %d, expecting %d", encLen,
+ i+FrameOverhead)
+ }
+
+ var decoded [MaximumFramePayloadLength]byte
+
+ decLen, err := decoder.Decode(decoded[:], bytes.NewBuffer(frame[:encLen]))
+ if err != nil {
+ t.Fatalf("Decoder.decode([%d]byte), failed: %s", i, err)
+ }
+ if decLen != i {
+ t.Fatalf("Unexpected decoded framesize: %d, expecting %d",
+ decLen, i)
+ }
+
+ if 0 != bytes.Compare(decoded[:decLen], buf[:i]) {
+ t.Fatalf("Frame %d does not match encoder input", i)
+ }
+ }
+}
+
+// BencharkEncoder_Encode benchmarks Encoder.Encode processing 1 MiB
+// of payload.
+func BenchmarkEncoder_Encode(b *testing.B) {
+ var chopBuf [MaximumFramePayloadLength]byte
+ var frame [MaximumSegmentLength]byte
+ payload := make([]byte, 1024*1024)
+ encoder := NewEncoder(generateRandomKey())
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ transfered := 0
+ buffer := bytes.NewBuffer(payload)
+ for 0 < buffer.Len() {
+ n, err := buffer.Read(chopBuf[:])
+ if err != nil {
+ b.Fatal("buffer.Read() failed:", err)
+ }
+
+ n, err = encoder.Encode(frame[:], chopBuf[:n])
+ transfered += n - FrameOverhead
+ }
+ if transfered != len(payload) {
+ b.Fatalf("Transfered length mismatch: %d != %d", transfered,
+ len(payload))
+ }
+ }
+}
diff --git a/transports/obfs4/handshake_ntor.go b/transports/obfs4/handshake_ntor.go
new file mode 100644
index 0000000..8dcf0c8
--- /dev/null
+++ b/transports/obfs4/handshake_ntor.go
@@ -0,0 +1,426 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * 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 obfs4
+
+import (
+ "bytes"
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "hash"
+ "strconv"
+ "time"
+
+ "git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/ntor"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/replayfilter"
+ "git.torproject.org/pluggable-transports/obfs4.git/transports/obfs4/framing"
+)
+
+const (
+ maxHandshakeLength = 8192
+
+ clientMinPadLength = (serverMinHandshakeLength + inlineSeedFrameLength) -
+ clientMinHandshakeLength
+ clientMaxPadLength = maxHandshakeLength - clientMinHandshakeLength
+ clientMinHandshakeLength = ntor.RepresentativeLength + markLength + macLength
+
+ serverMinPadLength = 0
+ serverMaxPadLength = maxHandshakeLength - (serverMinHandshakeLength +
+ inlineSeedFrameLength)
+ serverMinHandshakeLength = ntor.RepresentativeLength + ntor.AuthLength +
+ markLength + macLength
+
+ markLength = sha256.Size / 2
+ macLength = sha256.Size / 2
+
+ inlineSeedFrameLength = framing.FrameOverhead + packetOverhead + seedPacketPayloadLength
+)
+
+// ErrMarkNotFoundYet is the error returned when the obfs4 handshake is
+// incomplete and requires more data to continue. This error is non-fatal and
+// is the equivalent to EAGAIN/EWOULDBLOCK.
+var ErrMarkNotFoundYet = errors.New("handshake: M_[C,S] not found yet")
+
+// ErrInvalidHandshake is the error returned when the obfs4 handshake fails due
+// to the peer not sending the correct mark. This error is fatal and the
+// connection MUST be dropped.
+var ErrInvalidHandshake = errors.New("handshake: Failed to find M_[C,S]")
+
+// ErrReplayedHandshake is the error returned when the obfs4 handshake fails
+// due it being replayed. This error is fatal and the connection MUST be
+// dropped.
+var ErrReplayedHandshake = errors.New("handshake: Replay detected")
+
+// ErrNtorFailed is the error returned when the ntor handshake fails. This
+// error is fatal and the connection MUST be dropped.
+var ErrNtorFailed = errors.New("handshake: ntor handshake failure")
+
+// InvalidMacError is the error returned when the handshake MACs do not match.
+// This error is fatal and the connection MUST be dropped.
+type InvalidMacError struct {
+ Derived []byte
+ Received []byte
+}
+
+func (e *InvalidMacError) Error() string {
+ return fmt.Sprintf("handshake: MAC mismatch: Dervied: %s Received: %s.",
+ hex.EncodeToString(e.Derived), hex.EncodeToString(e.Received))
+}
+
+// InvalidAuthError is the error returned when the ntor AUTH tags do not match.
+// This error is fatal and the connection MUST be dropped.
+type InvalidAuthError struct {
+ Derived *ntor.Auth
+ Received *ntor.Auth
+}
+
+func (e *InvalidAuthError) Error() string {
+ return fmt.Sprintf("handshake: ntor AUTH mismatch: Derived: %s Received:%s.",
+ hex.EncodeToString(e.Derived.Bytes()[:]),
+ hex.EncodeToString(e.Received.Bytes()[:]))
+}
+
+type clientHandshake struct {
+ keypair *ntor.Keypair
+ nodeID *ntor.NodeID
+ serverIdentity *ntor.PublicKey
+ epochHour []byte
+
+ padLen int
+ mac hash.Hash
+
+ serverRepresentative *ntor.Representative
+ serverAuth *ntor.Auth
+ serverMark []byte
+}
+
+func newClientHandshake(nodeID *ntor.NodeID, serverIdentity *ntor.PublicKey, sessionKey *ntor.Keypair) *clientHandshake {
+ hs := new(clientHandshake)
+ hs.keypair = sessionKey
+ hs.nodeID = nodeID
+ hs.serverIdentity = serverIdentity
+ hs.padLen = csrand.IntRange(clientMinPadLength, clientMaxPadLength)
+ hs.mac = hmac.New(sha256.New, append(hs.serverIdentity.Bytes()[:], hs.nodeID.Bytes()[:]...))
+
+ return hs
+}
+
+func (hs *clientHandshake) generateHandshake() ([]byte, error) {
+ var buf bytes.Buffer
+
+ hs.mac.Reset()
+ hs.mac.Write(hs.keypair.Representative().Bytes()[:])
+ mark := hs.mac.Sum(nil)[:markLength]
+
+ // The client handshake is X | P_C | M_C | MAC(X | P_C | M_C | E) where:
+ // * X is the client's ephemeral Curve25519 public key representative.
+ // * P_C is [clientMinPadLength,clientMaxPadLength] bytes of random padding.
+ // * M_C is HMAC-SHA256-128(serverIdentity | NodeID, X)
+ // * MAC is HMAC-SHA256-128(serverIdentity | NodeID, X .... E)
+ // * E is the string representation of the number of hours since the UNIX
+ // epoch.
+
+ // Generate the padding
+ pad, err := makePad(hs.padLen)
+ if err != nil {
+ return nil, err
+ }
+
+ // Write X, P_C, M_C.
+ buf.Write(hs.keypair.Representative().Bytes()[:])
+ buf.Write(pad)
+ buf.Write(mark)
+
+ // Calculate and write the MAC.
+ hs.mac.Reset()
+ hs.mac.Write(buf.Bytes())
+ hs.epochHour = []byte(strconv.FormatInt(getEpochHour(), 10))
+ hs.mac.Write(hs.epochHour)
+ buf.Write(hs.mac.Sum(nil)[:macLength])
+
+ return buf.Bytes(), nil
+}
+
+func (hs *clientHandshake) parseServerHandshake(resp []byte) (int, []byte, error) {
+ // No point in examining the data unless the miminum plausible response has
+ // been received.
+ if serverMinHandshakeLength > len(resp) {
+ return 0, nil, ErrMarkNotFoundYet
+ }
+
+ if hs.serverRepresentative == nil || hs.serverAuth == nil {
+ // Pull out the representative/AUTH. (XXX: Add ctors to ntor)
+ hs.serverRepresentative = new(ntor.Representative)
+ copy(hs.serverRepresentative.Bytes()[:], resp[0:ntor.RepresentativeLength])
+ hs.serverAuth = new(ntor.Auth)
+ copy(hs.serverAuth.Bytes()[:], resp[ntor.RepresentativeLength:])
+
+ // Derive the mark.
+ hs.mac.Reset()
+ hs.mac.Write(hs.serverRepresentative.Bytes()[:])
+ hs.serverMark = hs.mac.Sum(nil)[:markLength]
+ }
+
+ // Attempt to find the mark + MAC.
+ pos := findMarkMac(hs.serverMark, resp, ntor.RepresentativeLength+ntor.AuthLength+serverMinPadLength,
+ maxHandshakeLength, false)
+ if pos == -1 {
+ if len(resp) >= maxHandshakeLength {
+ return 0, nil, ErrInvalidHandshake
+ }
+ return 0, nil, ErrMarkNotFoundYet
+ }
+
+ // Validate the MAC.
+ hs.mac.Reset()
+ hs.mac.Write(resp[:pos+markLength])
+ hs.mac.Write(hs.epochHour)
+ macCmp := hs.mac.Sum(nil)[:macLength]
+ macRx := resp[pos+markLength : pos+markLength+macLength]
+ if !hmac.Equal(macCmp, macRx) {
+ return 0, nil, &InvalidMacError{macCmp, macRx}
+ }
+
+ // Complete the handshake.
+ serverPublic := hs.serverRepresentative.ToPublic()
+ ok, seed, auth := ntor.ClientHandshake(hs.keypair, serverPublic,
+ hs.serverIdentity, hs.nodeID)
+ if !ok {
+ return 0, nil, ErrNtorFailed
+ }
+ if !ntor.CompareAuth(auth, hs.serverAuth.Bytes()[:]) {
+ return 0, nil, &InvalidAuthError{auth, hs.serverAuth}
+ }
+
+ return pos + markLength + macLength, seed.Bytes()[:], nil
+}
+
+type serverHandshake struct {
+ keypair *ntor.Keypair
+ nodeID *ntor.NodeID
+ serverIdentity *ntor.Keypair
+ epochHour []byte
+ serverAuth *ntor.Auth
+
+ padLen int
+ mac hash.Hash
+
+ clientRepresentative *ntor.Representative
+ clientMark []byte
+}
+
+func newServerHandshake(nodeID *ntor.NodeID, serverIdentity *ntor.Keypair, sessionKey *ntor.Keypair) *serverHandshake {
+ hs := new(serverHandshake)
+ hs.keypair = sessionKey
+ hs.nodeID = nodeID
+ hs.serverIdentity = serverIdentity
+ hs.padLen = csrand.IntRange(serverMinPadLength, serverMaxPadLength)
+ hs.mac = hmac.New(sha256.New, append(hs.serverIdentity.Public().Bytes()[:], hs.nodeID.Bytes()[:]...))
+
+ return hs
+}
+
+func (hs *serverHandshake) parseClientHandshake(filter *replayfilter.ReplayFilter, resp []byte) ([]byte, error) {
+ // No point in examining the data unless the miminum plausible response has
+ // been received.
+ if clientMinHandshakeLength > len(resp) {
+ return nil, ErrMarkNotFoundYet
+ }
+
+ if hs.clientRepresentative == nil {
+ // Pull out the representative/AUTH. (XXX: Add ctors to ntor)
+ hs.clientRepresentative = new(ntor.Representative)
+ copy(hs.clientRepresentative.Bytes()[:], resp[0:ntor.RepresentativeLength])
+
+ // Derive the mark.
+ hs.mac.Reset()
+ hs.mac.Write(hs.clientRepresentative.Bytes()[:])
+ hs.clientMark = hs.mac.Sum(nil)[:markLength]
+ }
+
+ // Attempt to find the mark + MAC.
+ pos := findMarkMac(hs.clientMark, resp, ntor.RepresentativeLength+clientMinPadLength,
+ maxHandshakeLength, true)
+ if pos == -1 {
+ if len(resp) >= maxHandshakeLength {
+ return nil, ErrInvalidHandshake
+ }
+ return nil, ErrMarkNotFoundYet
+ }
+
+ // Validate the MAC.
+ macFound := false
+ for _, off := range []int64{0, -1, 1} {
+ // Allow epoch to be off by up to a hour in either direction.
+ epochHour := []byte(strconv.FormatInt(getEpochHour()+int64(off), 10))
+ hs.mac.Reset()
+ hs.mac.Write(resp[:pos+markLength])
+ hs.mac.Write(epochHour)
+ macCmp := hs.mac.Sum(nil)[:macLength]
+ macRx := resp[pos+markLength : pos+markLength+macLength]
+ if hmac.Equal(macCmp, macRx) {
+ // Ensure that this handshake has not been seen previously.
+ if filter.TestAndSet(time.Now(), macRx) {
+ // The client either happened to generate exactly the same
+ // session key and padding, or someone is replaying a previous
+ // handshake. In either case, fuck them.
+ return nil, ErrReplayedHandshake
+ }
+
+ macFound = true
+ hs.epochHour = epochHour
+
+ // We could break out here, but in the name of reducing timing
+ // variation, evaluate all 3 MACs.
+ }
+ }
+ if !macFound {
+ // This probably should be an InvalidMacError, but conveying the 3 MACS
+ // that would be accepted is annoying so just return a generic fatal
+ // failure.
+ return nil, ErrInvalidHandshake
+ }
+
+ // Client should never sent trailing garbage.
+ if len(resp) != pos+markLength+macLength {
+ return nil, ErrInvalidHandshake
+ }
+
+ clientPublic := hs.clientRepresentative.ToPublic()
+ ok, seed, auth := ntor.ServerHandshake(clientPublic, hs.keypair,
+ hs.serverIdentity, hs.nodeID)
+ if !ok {
+ return nil, ErrNtorFailed
+ }
+ hs.serverAuth = auth
+
+ return seed.Bytes()[:], nil
+}
+
+func (hs *serverHandshake) generateHandshake() ([]byte, error) {
+ var buf bytes.Buffer
+
+ hs.mac.Reset()
+ hs.mac.Write(hs.keypair.Representative().Bytes()[:])
+ mark := hs.mac.Sum(nil)[:markLength]
+
+ // The server handshake is Y | AUTH | P_S | M_S | MAC(Y | AUTH | P_S | M_S | E) where:
+ // * Y is the server's ephemeral Curve25519 public key representative.
+ // * AUTH is the ntor handshake AUTH value.
+ // * P_S is [serverMinPadLength,serverMaxPadLength] bytes of random padding.
+ // * M_S is HMAC-SHA256-128(serverIdentity | NodeID, Y)
+ // * MAC is HMAC-SHA256-128(serverIdentity | NodeID, Y .... E)
+ // * E is the string representation of the number of hours since the UNIX
+ // epoch.
+
+ // Generate the padding
+ pad, err := makePad(hs.padLen)
+ if err != nil {
+ return nil, err
+ }
+
+ // Write Y, AUTH, P_S, M_S.
+ buf.Write(hs.keypair.Representative().Bytes()[:])
+ buf.Write(hs.serverAuth.Bytes()[:])
+ buf.Write(pad)
+ buf.Write(mark)
+
+ // Calculate and write the MAC.
+ hs.mac.Reset()
+ hs.mac.Write(buf.Bytes())
+ hs.epochHour = []byte(strconv.FormatInt(getEpochHour(), 10))
+ hs.mac.Write(hs.epochHour)
+ buf.Write(hs.mac.Sum(nil)[:macLength])
+
+ return buf.Bytes(), nil
+}
+
+// getEpochHour returns the number of hours since the UNIX epoch.
+func getEpochHour() int64 {
+ return time.Now().Unix() / 3600
+}
+
+func findMarkMac(mark, buf []byte, startPos, maxPos int, fromTail bool) (pos int) {
+ if len(mark) != markLength {
+ panic(fmt.Sprintf("BUG: Invalid mark length: %d", len(mark)))
+ }
+
+ endPos := len(buf)
+ if startPos > len(buf) {
+ return -1
+ }
+ if endPos > maxPos {
+ endPos = maxPos
+ }
+ if endPos-startPos < markLength+macLength {
+ return -1
+ }
+
+ if fromTail {
+ // The server can optimize the search process by only examining the
+ // tail of the buffer. The client can't send valid data past M_C |
+ // MAC_C as it does not have the server's public key yet.
+ pos = endPos - (markLength + macLength)
+ if !hmac.Equal(buf[pos:pos+markLength], mark) {
+ return -1
+ }
+
+ return
+ }
+
+ // The client has to actually do a substring search since the server can
+ // and will send payload trailing the response.
+ //
+ // XXX: bytes.Index() uses a naive search, which kind of sucks.
+ pos = bytes.Index(buf[startPos:endPos], mark)
+ if pos == -1 {
+ return -1
+ }
+
+ // Ensure that there is enough trailing data for the MAC.
+ if startPos+pos+markLength+macLength > endPos {
+ return -1
+ }
+
+ // Return the index relative to the start of the slice.
+ pos += startPos
+ return
+}
+
+func makePad(padLen int) ([]byte, error) {
+ pad := make([]byte, padLen)
+ err := csrand.Bytes(pad)
+ if err != nil {
+ return nil, err
+ }
+
+ return pad, err
+}
diff --git a/transports/obfs4/handshake_ntor_test.go b/transports/obfs4/handshake_ntor_test.go
new file mode 100644
index 0000000..fa03420
--- /dev/null
+++ b/transports/obfs4/handshake_ntor_test.go
@@ -0,0 +1,222 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * 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 obfs4
+
+import (
+ "bytes"
+ "testing"
+
+ "git.torproject.org/pluggable-transports/obfs4.git/common/ntor"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/replayfilter"
+)
+
+func TestHandshakeNtor(t *testing.T) {
+ // Generate the server node id and id keypair.
+ nodeID, _ := ntor.NewNodeID([]byte("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13"))
+ idKeypair, _ := ntor.NewKeypair(false)
+ serverFilter, _ := replayfilter.New(replayTTL)
+
+ // Test client handshake padding.
+ for l := clientMinPadLength; l <= clientMaxPadLength; l++ {
+ // Generate the client state and override the pad length.
+ clientKeypair, err := ntor.NewKeypair(true)
+ if err != nil {
+ t.Fatalf("[%d:0] ntor.NewKeypair failed: %s", l, err)
+ }
+ clientHs := newClientHandshake(nodeID, idKeypair.Public(), clientKeypair)
+ clientHs.padLen = l
+
+ // Generate what the client will send to the server.
+ clientBlob, err := clientHs.generateHandshake()
+ if err != nil {
+ t.Fatalf("[%d:0] clientHandshake.generateHandshake() failed: %s", l, err)
+ }
+ if len(clientBlob) > maxHandshakeLength {
+ t.Fatalf("[%d:0] Generated client body is oversized: %d", l, len(clientBlob))
+ }
+ if len(clientBlob) < clientMinHandshakeLength {
+ t.Fatalf("[%d:0] Generated client body is undersized: %d", l, len(clientBlob))
+ }
+ if len(clientBlob) != clientMinHandshakeLength+l {
+ t.Fatalf("[%d:0] Generated client body incorrect size: %d", l, len(clientBlob))
+ }
+
+ // Generate the server state and override the pad length.
+ serverKeypair, err := ntor.NewKeypair(true)
+ if err != nil {
+ t.Fatalf("[%d:0] ntor.NewKeypair failed: %s", l, err)
+ }
+ serverHs := newServerHandshake(nodeID, idKeypair, serverKeypair)
+ serverHs.padLen = serverMinPadLength
+
+ // Parse the client handshake message.
+ serverSeed, err := serverHs.parseClientHandshake(serverFilter, clientBlob)
+ if err != nil {
+ t.Fatalf("[%d:0] serverHandshake.parseClientHandshake() failed: %s", l, err)
+ }
+
+ // Genrate what the server will send to the client.
+ serverBlob, err := serverHs.generateHandshake()
+ if err != nil {
+ t.Fatal("[%d:0]: serverHandshake.generateHandshake() failed: %s", l, err)
+ }
+
+ // Parse the server handshake message.
+ clientHs.serverRepresentative = nil
+ n, clientSeed, err := clientHs.parseServerHandshake(serverBlob)
+ if err != nil {
+ t.Fatalf("[%d:0] clientHandshake.parseServerHandshake() failed: %s", l, err)
+ }
+ if n != len(serverBlob) {
+ t.Fatalf("[%d:0] clientHandshake.parseServerHandshake() has bytes remaining: %d", l, n)
+ }
+
+ // Ensure the derived shared secret is the same.
+ if 0 != bytes.Compare(clientSeed, serverSeed) {
+ t.Fatalf("[%d:0] client/server seed mismatch", l)
+ }
+ }
+
+ // Test server handshake padding.
+ for l := serverMinPadLength; l <= serverMaxPadLength+inlineSeedFrameLength; l++ {
+ // Generate the client state and override the pad length.
+ clientKeypair, err := ntor.NewKeypair(true)
+ if err != nil {
+ t.Fatalf("[%d:0] ntor.NewKeypair failed: %s", l, err)
+ }
+ clientHs := newClientHandshake(nodeID, idKeypair.Public(), clientKeypair)
+ clientHs.padLen = clientMinPadLength
+
+ // Generate what the client will send to the server.
+ clientBlob, err := clientHs.generateHandshake()
+ if err != nil {
+ t.Fatalf("[%d:1] clientHandshake.generateHandshake() failed: %s", l, err)
+ }
+ if len(clientBlob) > maxHandshakeLength {
+ t.Fatalf("[%d:1] Generated client body is oversized: %d", l, len(clientBlob))
+ }
+
+ // Generate the server state and override the pad length.
+ serverKeypair, err := ntor.NewKeypair(true)
+ if err != nil {
+ t.Fatalf("[%d:0] ntor.NewKeypair failed: %s", l, err)
+ }
+ serverHs := newServerHandshake(nodeID, idKeypair, serverKeypair)
+ serverHs.padLen = l
+
+ // Parse the client handshake message.
+ serverSeed, err := serverHs.parseClientHandshake(serverFilter, clientBlob)
+ if err != nil {
+ t.Fatalf("[%d:1] serverHandshake.parseClientHandshake() failed: %s", l, err)
+ }
+
+ // Genrate what the server will send to the client.
+ serverBlob, err := serverHs.generateHandshake()
+ if err != nil {
+ t.Fatal("[%d:1]: serverHandshake.generateHandshake() failed: %s", l, err)
+ }
+
+ // Parse the server handshake message.
+ n, clientSeed, err := clientHs.parseServerHandshake(serverBlob)
+ if err != nil {
+ t.Fatalf("[%d:1] clientHandshake.parseServerHandshake() failed: %s", l, err)
+ }
+ if n != len(serverBlob) {
+ t.Fatalf("[%d:1] clientHandshake.parseServerHandshake() has bytes remaining: %d", l, n)
+ }
+
+ // Ensure the derived shared secret is the same.
+ if 0 != bytes.Compare(clientSeed, serverSeed) {
+ t.Fatalf("[%d:1] client/server seed mismatch", l)
+ }
+ }
+
+ // Test oversized client padding.
+ clientKeypair, err := ntor.NewKeypair(true)
+ if err != nil {
+ t.Fatalf("ntor.NewKeypair failed: %s", err)
+ }
+ clientHs := newClientHandshake(nodeID, idKeypair.Public(), clientKeypair)
+ if err != nil {
+ t.Fatalf("newClientHandshake failed: %s", err)
+ }
+
+ clientHs.padLen = clientMaxPadLength + 1
+ clientBlob, err := clientHs.generateHandshake()
+ if err != nil {
+ t.Fatalf("clientHandshake.generateHandshake() (forced oversize) failed: %s", err)
+ }
+ serverKeypair, err := ntor.NewKeypair(true)
+ if err != nil {
+ t.Fatalf("ntor.NewKeypair failed: %s", err)
+ }
+ serverHs := newServerHandshake(nodeID, idKeypair, serverKeypair)
+ _, err = serverHs.parseClientHandshake(serverFilter, clientBlob)
+ if err == nil {
+ t.Fatalf("serverHandshake.parseClientHandshake() succeded (oversized)")
+ }
+
+ // Test undersized client padding.
+ clientHs.padLen = clientMinPadLength - 1
+ clientBlob, err = clientHs.generateHandshake()
+ if err != nil {
+ t.Fatalf("clientHandshake.generateHandshake() (forced undersize) failed: %s", err)
+ }
+ serverHs = newServerHandshake(nodeID, idKeypair, serverKeypair)
+ _, err = serverHs.parseClientHandshake(serverFilter, clientBlob)
+ if err == nil {
+ t.Fatalf("serverHandshake.parseClientHandshake() succeded (undersized)")
+ }
+
+ // Test oversized server padding.
+ //
+ // NB: serverMaxPadLength isn't the real maxPadLength that triggers client
+ // rejection, because the implementation is written with the asusmption
+ // that/ the PRNG_SEED is also inlined with the response. Thus the client
+ // actually accepts longer padding. The server handshake test and this
+ // test adjust around that.
+ clientHs.padLen = clientMinPadLength
+ clientBlob, err = clientHs.generateHandshake()
+ if err != nil {
+ t.Fatalf("clientHandshake.generateHandshake() failed: %s", err)
+ }
+ serverHs = newServerHandshake(nodeID, idKeypair, serverKeypair)
+ serverHs.padLen = serverMaxPadLength + inlineSeedFrameLength + 1
+ _, err = serverHs.parseClientHandshake(serverFilter, clientBlob)
+ if err != nil {
+ t.Fatalf("serverHandshake.parseClientHandshake() failed: %s", err)
+ }
+ serverBlob, err := serverHs.generateHandshake()
+ if err != nil {
+ t.Fatal("serverHandshake.generateHandshake() (forced oversize) failed: %s", err)
+ }
+ _, _, err = clientHs.parseServerHandshake(serverBlob)
+ if err == nil {
+ t.Fatalf("clientHandshake.parseServerHandshake() succeded (oversized)")
+ }
+}
diff --git a/transports/obfs4/obfs4.go b/transports/obfs4/obfs4.go
new file mode 100644
index 0000000..7af7224
--- /dev/null
+++ b/transports/obfs4/obfs4.go
@@ -0,0 +1,579 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * 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 obfs4 provides an implementation of the Tor Project's obfs4
+// obfuscation protocol.
+package obfs4
+
+import (
+ "bytes"
+ "crypto/sha256"
+ "fmt"
+ "math/rand"
+ "net"
+ "syscall"
+ "time"
+
+ "git.torproject.org/pluggable-transports/goptlib.git"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/drbg"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/ntor"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/probdist"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/replayfilter"
+ "git.torproject.org/pluggable-transports/obfs4.git/transports/base"
+ "git.torproject.org/pluggable-transports/obfs4.git/transports/obfs4/framing"
+)
+
+const (
+ transportName = "obfs4"
+
+ nodeIDArg = "node-id"
+ publicKeyArg = "public-key"
+ privateKeyArg = "private-key"
+ seedArg = "drbg-seed"
+
+ seedLength = 32
+ headerLength = framing.FrameOverhead + packetOverhead
+ clientHandshakeTimeout = time.Duration(60) * time.Second
+ serverHandshakeTimeout = time.Duration(30) * time.Second
+ replayTTL = time.Duration(3) * time.Hour
+
+ // Use a ScrambleSuit style biased probability table.
+ biasedDist = false
+
+ // Use IAT obfuscation.
+ iatObfuscation = false
+
+ // Maximum IAT delay (100 usec increments).
+ maxIATDelay = 100
+
+ maxCloseDelayBytes = maxHandshakeLength
+ maxCloseDelay = 60
+)
+
+type obfs4ClientArgs struct {
+ nodeID *ntor.NodeID
+ publicKey *ntor.PublicKey
+ sessionKey *ntor.Keypair
+}
+
+// Transport is the obfs4 implementation of the base.Transport interface.
+type Transport struct{}
+
+// Name returns the name of the obfs4 transport protocol.
+func (t *Transport) Name() string {
+ return transportName
+}
+
+// ClientFactory returns a new obfs4ClientFactory instance.
+func (t *Transport) ClientFactory(stateDir string) (base.ClientFactory, error) {
+ cf := &obfs4ClientFactory{transport: t}
+ return cf, nil
+}
+
+// ServerFactory returns a new obfs4ServerFactory instance.
+func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFactory, error) {
+ var err error
+
+ var st *obfs4ServerState
+ if st, err = serverStateFromArgs(stateDir, args); err != nil {
+ return nil, err
+ }
+
+ var iatSeed *drbg.Seed
+ if iatObfuscation {
+ iatSeedSrc := sha256.Sum256(st.drbgSeed.Bytes()[:])
+ iatSeed, err = drbg.SeedFromBytes(iatSeedSrc[:])
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // Store the arguments that should appear in our descriptor for the clients.
+ ptArgs := pt.Args{}
+ ptArgs.Add(nodeIDArg, st.nodeID.Base64())
+ ptArgs.Add(publicKeyArg, st.identityKey.Public().Base64())
+
+ // Initialize the replay filter.
+ filter, err := replayfilter.New(replayTTL)
+ if err != nil {
+ return nil, err
+ }
+
+ // Initialize the close thresholds for failed connections.
+ drbg, err := drbg.NewHashDrbg(st.drbgSeed)
+ if err != nil {
+ return nil, err
+ }
+ rng := rand.New(drbg)
+
+ sf := &obfs4ServerFactory{t, &ptArgs, st.nodeID, st.identityKey, st.drbgSeed, iatSeed, filter, rng.Intn(maxCloseDelayBytes), rng.Intn(maxCloseDelay)}
+ return sf, nil
+}
+
+type obfs4ClientFactory struct {
+ transport base.Transport
+}
+
+func (cf *obfs4ClientFactory) Transport() base.Transport {
+ return cf.transport
+}
+
+func (cf *obfs4ClientFactory) ParseArgs(args *pt.Args) (interface{}, error) {
+ var err error
+
+ // Handle the arguments.
+ nodeIDStr, ok := args.Get(nodeIDArg)
+ if !ok {
+ return nil, fmt.Errorf("missing argument '%s'", nodeIDArg)
+ }
+ var nodeID *ntor.NodeID
+ if nodeID, err = ntor.NodeIDFromBase64(nodeIDStr); err != nil {
+ return nil, err
+ }
+
+ publicKeyStr, ok := args.Get(publicKeyArg)
+ if !ok {
+ return nil, fmt.Errorf("missing argument '%s'", publicKeyArg)
+ }
+ var publicKey *ntor.PublicKey
+ if publicKey, err = ntor.PublicKeyFromBase64(publicKeyStr); err != nil {
+ return nil, err
+ }
+
+ // Generate the session key pair before connectiong to hide the Elligator2
+ // rejection sampling from network observers.
+ sessionKey, err := ntor.NewKeypair(true)
+ if err != nil {
+ return nil, err
+ }
+
+ return &obfs4ClientArgs{nodeID, publicKey, sessionKey}, nil
+}
+
+func (cf *obfs4ClientFactory) WrapConn(conn net.Conn, args interface{}) (net.Conn, error) {
+ ca, ok := args.(*obfs4ClientArgs)
+ if !ok {
+ return nil, fmt.Errorf("invalid argument type for args")
+ }
+
+ return newObfs4ClientConn(conn, ca)
+}
+
+type obfs4ServerFactory struct {
+ transport base.Transport
+ args *pt.Args
+
+ nodeID *ntor.NodeID
+ identityKey *ntor.Keypair
+ lenSeed *drbg.Seed
+ iatSeed *drbg.Seed
+ replayFilter *replayfilter.ReplayFilter
+
+ closeDelayBytes int
+ closeDelay int
+}
+
+func (sf *obfs4ServerFactory) Transport() base.Transport {
+ return sf.transport
+}
+
+func (sf *obfs4ServerFactory) Args() *pt.Args {
+ return sf.args
+}
+
+func (sf *obfs4ServerFactory) WrapConn(conn net.Conn) (net.Conn, error) {
+ // Not much point in having a separate newObfs4ServerConn routine when
+ // wrapping requires using values from the factory instance.
+
+ // Generate the session keypair *before* consuming data from the peer, to
+ // attempt to mask the rejection sampling due to use of Elligator2. This
+ // might be futile, but the timing differential isn't very large on modern
+ // hardware, and there are far easier statistical attacks that can be
+ // mounted as a distinguisher.
+ sessionKey, err := ntor.NewKeypair(true)
+ if err != nil {
+ return nil, err
+ }
+
+ lenDist := probdist.New(sf.lenSeed, 0, framing.MaximumSegmentLength, biasedDist)
+ var iatDist *probdist.WeightedDist
+ if sf.iatSeed != nil {
+ iatDist = probdist.New(sf.iatSeed, 0, maxIATDelay, biasedDist)
+ }
+
+ c := &obfs4Conn{conn, true, lenDist, iatDist, bytes.NewBuffer(nil), bytes.NewBuffer(nil), nil, nil}
+
+ startTime := time.Now()
+
+ if err = c.serverHandshake(sf, sessionKey); err != nil {
+ c.closeAfterDelay(sf, startTime)
+ return nil, err
+ }
+
+ return c, nil
+}
+
+type obfs4Conn struct {
+ net.Conn
+
+ isServer bool
+
+ lenDist *probdist.WeightedDist
+ iatDist *probdist.WeightedDist
+
+ receiveBuffer *bytes.Buffer
+ receiveDecodedBuffer *bytes.Buffer
+
+ encoder *framing.Encoder
+ decoder *framing.Decoder
+}
+
+func newObfs4ClientConn(conn net.Conn, args *obfs4ClientArgs) (c *obfs4Conn, err error) {
+ // Generate the initial protocol polymorphism distribution(s).
+ var seed *drbg.Seed
+ if seed, err = drbg.NewSeed(); err != nil {
+ return
+ }
+ lenDist := probdist.New(seed, 0, framing.MaximumSegmentLength, biasedDist)
+ var iatDist *probdist.WeightedDist
+ if iatObfuscation {
+ var iatSeed *drbg.Seed
+ iatSeedSrc := sha256.Sum256(seed.Bytes()[:])
+ if iatSeed, err = drbg.SeedFromBytes(iatSeedSrc[:]); err != nil {
+ return
+ }
+ iatDist = probdist.New(iatSeed, 0, maxIATDelay, biasedDist)
+ }
+
+ // Allocate the client structure.
+ c = &obfs4Conn{conn, false, lenDist, iatDist, bytes.NewBuffer(nil), bytes.NewBuffer(nil), nil, nil}
+
+ // Start the handshake timeout.
+ deadline := time.Now().Add(clientHandshakeTimeout)
+ if err = conn.SetDeadline(deadline); err != nil {
+ return nil, err
+ }
+
+ if err = c.clientHandshake(args.nodeID, args.publicKey, args.sessionKey); err != nil {
+ return nil, err
+ }
+
+ // Stop the handshake timeout.
+ if err = conn.SetDeadline(time.Time{}); err != nil {
+ return nil, err
+ }
+
+ return
+}
+
+func (conn *obfs4Conn) clientHandshake(nodeID *ntor.NodeID, peerIdentityKey *ntor.PublicKey, sessionKey *ntor.Keypair) error {
+ if conn.isServer {
+ return fmt.Errorf("clientHandshake called on server connection")
+ }
+
+ // Generate and send the client handshake.
+ hs := newClientHandshake(nodeID, peerIdentityKey, sessionKey)
+ blob, err := hs.generateHandshake()
+ if err != nil {
+ return err
+ }
+ if _, err = conn.Conn.Write(blob); err != nil {
+ return err
+ }
+
+ // Consume the server handshake.
+ var hsBuf [maxHandshakeLength]byte
+ for {
+ var n int
+ if n, err = conn.Conn.Read(hsBuf[:]); err != nil {
+ // The Read() could have returned data and an error, but there is
+ // no point in continuing on an EOF or whatever.
+ return err
+ }
+ conn.receiveBuffer.Write(hsBuf[:n])
+
+ var seed []byte
+ n, seed, err = hs.parseServerHandshake(conn.receiveBuffer.Bytes())
+ if err == ErrMarkNotFoundYet {
+ continue
+ } else if err != nil {
+ return err
+ }
+ _ = conn.receiveBuffer.Next(n)
+
+ // Use the derived key material to intialize the link crypto.
+ okm := ntor.Kdf(seed, framing.KeyLength*2)
+ conn.encoder = framing.NewEncoder(okm[:framing.KeyLength])
+ conn.decoder = framing.NewDecoder(okm[framing.KeyLength:])
+
+ return nil
+ }
+}
+
+func (conn *obfs4Conn) serverHandshake(sf *obfs4ServerFactory, sessionKey *ntor.Keypair) (err error) {
+ if !conn.isServer {
+ return fmt.Errorf("serverHandshake called on client connection")
+ }
+
+ // Generate the server handshake, and arm the base timeout.
+ hs := newServerHandshake(sf.nodeID, sf.identityKey, sessionKey)
+ if err = conn.Conn.SetDeadline(time.Now().Add(serverHandshakeTimeout)); err != nil {
+ return
+ }
+
+ // Consume the client handshake.
+ var hsBuf [maxHandshakeLength]byte
+ for {
+ var n int
+ if n, err = conn.Conn.Read(hsBuf[:]); err != nil {
+ // The Read() could have returned data and an error, but there is
+ // no point in continuing on an EOF or whatever.
+ return
+ }
+ conn.receiveBuffer.Write(hsBuf[:n])
+
+ var seed []byte
+ seed, err = hs.parseClientHandshake(sf.replayFilter, conn.receiveBuffer.Bytes())
+ if err == ErrMarkNotFoundYet {
+ continue
+ } else if err != nil {
+ return
+ }
+ conn.receiveBuffer.Reset()
+
+ if err = conn.Conn.SetDeadline(time.Time{}); err != nil {
+ return
+ }
+
+ // Use the derived key material to intialize the link crypto.
+ okm := ntor.Kdf(seed, framing.KeyLength*2)
+ conn.encoder = framing.NewEncoder(okm[framing.KeyLength:])
+ conn.decoder = framing.NewDecoder(okm[:framing.KeyLength])
+
+ break
+ }
+
+ // Since the current and only implementation always sends a PRNG seed for
+ // the length obfuscation, this makes the amount of data received from the
+ // server inconsistent with the length sent from the client.
+ //
+ // Rebalance this by tweaking the client mimimum padding/server maximum
+ // padding, and sending the PRNG seed unpadded (As in, treat the PRNG seed
+ // as part of the server response). See inlineSeedFrameLength in
+ // handshake_ntor.go.
+
+ // Generate/send the response.
+ var blob []byte
+ blob, err = hs.generateHandshake()
+ if err != nil {
+ return
+ }
+ var frameBuf bytes.Buffer
+ _, err = frameBuf.Write(blob)
+ if err != nil {
+ return
+ }
+
+ // Send the PRNG seed as the first packet.
+ if err = conn.makePacket(&frameBuf, packetTypePrngSeed, sf.lenSeed.Bytes()[:], 0); err != nil {
+ return
+ }
+ if _, err = conn.Conn.Write(frameBuf.Bytes()); err != nil {
+ return
+ }
+
+ return
+}
+
+func (conn *obfs4Conn) Read(b []byte) (n int, err error) {
+ // If there is no payload from the previous Read() calls, consume data off
+ // the network. Not all data received is guaranteed to be usable payload,
+ // so do this in a loop till data is present or an error occurs.
+ for conn.receiveDecodedBuffer.Len() == 0 {
+ err = conn.readPackets()
+ if err == framing.ErrAgain {
+ // Don't proagate this back up the call stack if we happen to break
+ // out of the loop.
+ err = nil
+ continue
+ } else if err != nil {
+ break
+ }
+ }
+
+ // Even if err is set, attempt to do the read anyway so that all decoded
+ // data gets relayed before the connection is torn down.
+ if conn.receiveDecodedBuffer.Len() > 0 {
+ var berr error
+ n, berr = conn.receiveDecodedBuffer.Read(b)
+ if err == nil {
+ // Only propagate berr if there are not more important (fatal)
+ // errors from the network/crypto/packet processing.
+ err = berr
+ }
+ }
+
+ return
+}
+
+func (conn *obfs4Conn) Write(b []byte) (n int, err error) {
+ chopBuf := bytes.NewBuffer(b)
+ var payload [maxPacketPayloadLength]byte
+ var frameBuf bytes.Buffer
+
+ // Chop the pending data into payload frames.
+ for chopBuf.Len() > 0 {
+ // Send maximum sized frames.
+ rdLen := 0
+ rdLen, err = chopBuf.Read(payload[:])
+ if err != nil {
+ return 0, err
+ } else if rdLen == 0 {
+ panic(fmt.Sprintf("BUG: Write(), chopping length was 0"))
+ }
+ n += rdLen
+
+ err = conn.makePacket(&frameBuf, packetTypePayload, payload[:rdLen], 0)
+ if err != nil {
+ return 0, err
+ }
+ }
+
+ // Add the length obfuscation padding. In theory, this could be inlined
+ // with the last chopped packet for certain (most?) payload lenghts, but
+ // this is simpler.
+
+ if err = conn.padBurst(&frameBuf); err != nil {
+ return 0, err
+ }
+
+ // Write the pending data onto the network. Partial writes are fatal,
+ // because the frame encoder state is advanced, and the code doesn't keep
+ // frameBuf around. In theory, write timeouts and whatnot could be
+ // supported if this wasn't the case, but that complicates the code.
+
+ if conn.iatDist != nil {
+ var iatFrame [framing.MaximumSegmentLength]byte
+ for frameBuf.Len() > 0 {
+ iatWrLen := 0
+ iatWrLen, err = frameBuf.Read(iatFrame[:])
+ if err != nil {
+ return 0, err
+ } else if iatWrLen == 0 {
+ panic(fmt.Sprintf("BUG: Write(), iat length was 0"))
+ }
+
+ // Calculate the delay. The delay resolution is 100 usec, leading
+ // to a maximum delay of 10 msec.
+ iatDelta := time.Duration(conn.iatDist.Sample() * 100)
+
+ // Write then sleep.
+ _, err = conn.Conn.Write(iatFrame[:iatWrLen])
+ if err != nil {
+ return 0, err
+ }
+ time.Sleep(iatDelta * time.Microsecond)
+ }
+ } else {
+ _, err = conn.Conn.Write(frameBuf.Bytes())
+ }
+
+ return
+}
+
+func (conn *obfs4Conn) SetDeadline(t time.Time) error {
+ return syscall.ENOTSUP
+}
+
+func (conn *obfs4Conn) SetWriteDeadline(t time.Time) error {
+ return syscall.ENOTSUP
+}
+
+func (conn *obfs4Conn) closeAfterDelay(sf *obfs4ServerFactory, startTime time.Time) {
+ // I-it's not like I w-wanna handshake with you or anything. B-b-baka!
+ defer conn.Conn.Close()
+
+ delay := time.Duration(sf.closeDelay)*time.Second + serverHandshakeTimeout
+ deadline := startTime.Add(delay)
+ if time.Now().After(deadline) {
+ return
+ }
+
+ if err := conn.Conn.SetReadDeadline(deadline); err != nil {
+ return
+ }
+
+ // Consume and discard data on this connection until either the specified
+ // interval passes or a certain size has been reached.
+ discarded := 0
+ var buf [framing.MaximumSegmentLength]byte
+ for discarded < int(sf.closeDelayBytes) {
+ n, err := conn.Conn.Read(buf[:])
+ if err != nil {
+ return
+ }
+ discarded += n
+ }
+}
+
+func (conn *obfs4Conn) padBurst(burst *bytes.Buffer) (err error) {
+ tailLen := burst.Len() % framing.MaximumSegmentLength
+ toPadTo := conn.lenDist.Sample()
+
+ padLen := 0
+ if toPadTo >= tailLen {
+ padLen = toPadTo - tailLen
+ } else {
+ padLen = (framing.MaximumSegmentLength - tailLen) + toPadTo
+ }
+
+ if padLen > headerLength {
+ err = conn.makePacket(burst, packetTypePayload, []byte{},
+ uint16(padLen-headerLength))
+ if err != nil {
+ return
+ }
+ } else if padLen > 0 {
+ err = conn.makePacket(burst, packetTypePayload, []byte{},
+ maxPacketPayloadLength)
+ if err != nil {
+ return
+ }
+ err = conn.makePacket(burst, packetTypePayload, []byte{},
+ uint16(padLen))
+ if err != nil {
+ return
+ }
+ }
+
+ return
+}
+
+var _ base.ClientFactory = (*obfs4ClientFactory)(nil)
+var _ base.ServerFactory = (*obfs4ServerFactory)(nil)
+var _ base.Transport = (*Transport)(nil)
+var _ net.Conn = (*obfs4Conn)(nil)
diff --git a/transports/obfs4/packet.go b/transports/obfs4/packet.go
new file mode 100644
index 0000000..9865c82
--- /dev/null
+++ b/transports/obfs4/packet.go
@@ -0,0 +1,179 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * 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 obfs4
+
+import (
+ "crypto/sha256"
+ "encoding/binary"
+ "fmt"
+ "io"
+
+ "git.torproject.org/pluggable-transports/obfs4.git/common/drbg"
+ "git.torproject.org/pluggable-transports/obfs4.git/transports/obfs4/framing"
+)
+
+const (
+ packetOverhead = 2 + 1
+ maxPacketPayloadLength = framing.MaximumFramePayloadLength - packetOverhead
+ maxPacketPaddingLength = maxPacketPayloadLength
+ seedPacketPayloadLength = seedLength
+
+ consumeReadSize = framing.MaximumSegmentLength * 16
+)
+
+const (
+ packetTypePayload = iota
+ packetTypePrngSeed
+)
+
+// InvalidPacketLengthError is the error returned when decodePacket detects a
+// invalid packet length/
+type InvalidPacketLengthError int
+
+func (e InvalidPacketLengthError) Error() string {
+ return fmt.Sprintf("packet: Invalid packet length: %d", int(e))
+}
+
+// InvalidPayloadLengthError is the error returned when decodePacket rejects the
+// payload length.
+type InvalidPayloadLengthError int
+
+func (e InvalidPayloadLengthError) Error() string {
+ return fmt.Sprintf("packet: Invalid payload length: %d", int(e))
+}
+
+var zeroPadBytes [maxPacketPaddingLength]byte
+
+func (conn *obfs4Conn) makePacket(w io.Writer, pktType uint8, data []byte, padLen uint16) (err error) {
+ var pkt [framing.MaximumFramePayloadLength]byte
+
+ if len(data)+int(padLen) > maxPacketPayloadLength {
+ panic(fmt.Sprintf("BUG: makePacket() len(data) + padLen > maxPacketPayloadLength: %d + %d > %d",
+ len(data), padLen, maxPacketPayloadLength))
+ }
+
+ // Packets are:
+ // uint8_t type packetTypePayload (0x00)
+ // uint16_t length Length of the payload (Big Endian).
+ // uint8_t[] payload Data payload.
+ // uint8_t[] padding Padding.
+ pkt[0] = pktType
+ binary.BigEndian.PutUint16(pkt[1:], uint16(len(data)))
+ if len(data) > 0 {
+ copy(pkt[3:], data[:])
+ }
+ copy(pkt[3+len(data):], zeroPadBytes[:padLen])
+
+ pktLen := packetOverhead + len(data) + int(padLen)
+
+ // Encode the packet in an AEAD frame.
+ var frame [framing.MaximumSegmentLength]byte
+ frameLen := 0
+ frameLen, err = conn.encoder.Encode(frame[:], pkt[:pktLen])
+ if err != nil {
+ // All encoder errors are fatal.
+ return
+ }
+ var wrLen int
+ wrLen, err = w.Write(frame[:frameLen])
+ if err != nil {
+ return
+ } else if wrLen < frameLen {
+ err = io.ErrShortWrite
+ return
+ }
+
+ return
+}
+
+func (conn *obfs4Conn) readPackets() (err error) {
+ // Attempt to read off the network.
+ var buf [consumeReadSize]byte
+ rdLen, rdErr := conn.Conn.Read(buf[:])
+ conn.receiveBuffer.Write(buf[:rdLen])
+
+ var decoded [framing.MaximumFramePayloadLength]byte
+ for conn.receiveBuffer.Len() > 0 {
+ // Decrypt an AEAD frame.
+ decLen := 0
+ decLen, err = conn.decoder.Decode(decoded[:], conn.receiveBuffer)
+ if err == framing.ErrAgain {
+ break
+ } else if err != nil {
+ break
+ } else if decLen < packetOverhead {
+ err = InvalidPacketLengthError(decLen)
+ break
+ }
+
+ // Decode the packet.
+ pkt := decoded[0:decLen]
+ pktType := pkt[0]
+ payloadLen := binary.BigEndian.Uint16(pkt[1:])
+ if int(payloadLen) > len(pkt)-packetOverhead {
+ err = InvalidPayloadLengthError(int(payloadLen))
+ break
+ }
+ payload := pkt[3 : 3+payloadLen]
+
+ switch pktType {
+ case packetTypePayload:
+ if payloadLen > 0 {
+ conn.receiveDecodedBuffer.Write(payload)
+ }
+ case packetTypePrngSeed:
+ // Only regenerate the distribution if we are the client.
+ if len(payload) == seedPacketPayloadLength && !conn.isServer {
+ var seed *drbg.Seed
+ seed, err = drbg.SeedFromBytes(payload)
+ if err != nil {
+ break
+ }
+ conn.lenDist.Reset(seed)
+ if conn.iatDist != nil {
+ iatSeedSrc := sha256.Sum256(seed.Bytes()[:])
+ iatSeed, err := drbg.SeedFromBytes(iatSeedSrc[:])
+ if err != nil {
+ break
+ }
+ conn.iatDist.Reset(iatSeed)
+ }
+ }
+ default:
+ // Ignore unknown packet types.
+ }
+ }
+
+ // Read errors (all fatal) take priority over various frame processing
+ // errors.
+ if rdErr != nil {
+ return rdErr
+ }
+
+ return
+}
diff --git a/transports/obfs4/statefile.go b/transports/obfs4/statefile.go
new file mode 100644
index 0000000..814a545
--- /dev/null
+++ b/transports/obfs4/statefile.go
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * 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 obfs4
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path"
+
+ "git.torproject.org/pluggable-transports/goptlib.git"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/drbg"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/ntor"
+)
+
+const (
+ stateFile = "obfs4_state.json"
+)
+
+type jsonServerState struct {
+ NodeID string `json:"node-id"`
+ PrivateKey string `json:"private-key"`
+ PublicKey string `json:"public-key"`
+ DrbgSeed string `json:"drbgSeed"`
+}
+
+type obfs4ServerState struct {
+ nodeID *ntor.NodeID
+ identityKey *ntor.Keypair
+ drbgSeed *drbg.Seed
+}
+
+func serverStateFromArgs(stateDir string, args *pt.Args) (*obfs4ServerState, error) {
+ var js jsonServerState
+ var nodeIDOk, privKeyOk, seedOk bool
+
+ js.NodeID, nodeIDOk = args.Get(nodeIDArg)
+ js.PrivateKey, privKeyOk = args.Get(privateKeyArg)
+ js.DrbgSeed, seedOk = args.Get(seedArg)
+
+ if !privKeyOk && !nodeIDOk && !seedOk {
+ if err := jsonServerStateFromFile(stateDir, &js); err != nil {
+ return nil, err
+ }
+ } else if !privKeyOk {
+ return nil, fmt.Errorf("missing argument '%s'", privateKeyArg)
+ } else if !nodeIDOk {
+ return nil, fmt.Errorf("missing argument '%s'", nodeIDArg)
+ } else if !seedOk {
+ return nil, fmt.Errorf("missing argument '%s'", seedArg)
+ }
+
+ return serverStateFromJSONServerState(&js)
+}
+
+func serverStateFromJSONServerState(js *jsonServerState) (*obfs4ServerState, error) {
+ var err error
+
+ st := new(obfs4ServerState)
+ if st.nodeID, err = ntor.NodeIDFromBase64(js.NodeID); err != nil {
+ return nil, err
+ }
+ if st.identityKey, err = ntor.KeypairFromBase64(js.PrivateKey); err != nil {
+ return nil, err
+ }
+ var rawSeed []byte
+ if rawSeed, err = base64.StdEncoding.DecodeString(js.DrbgSeed); err != nil {
+ return nil, err
+ }
+ if st.drbgSeed, err = drbg.SeedFromBytes(rawSeed); err != nil {
+ return nil, err
+ }
+
+ return st, nil
+}
+
+func jsonServerStateFromFile(stateDir string, js *jsonServerState) error {
+ f, err := ioutil.ReadFile(path.Join(stateDir, stateFile))
+ if err != nil {
+ if os.IsNotExist(err) {
+ if err = newJSONServerState(stateDir, js); err == nil {
+ return nil
+ }
+ }
+ return err
+ }
+
+ if err = json.Unmarshal(f, js); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func newJSONServerState(stateDir string, js *jsonServerState) (err error) {
+ // Generate everything a server needs, using the cryptographic PRNG.
+ var st obfs4ServerState
+ rawID := make([]byte, ntor.NodeIDLength)
+ if err = csrand.Bytes(rawID); err != nil {
+ return
+ }
+ if st.nodeID, err = ntor.NewNodeID(rawID); err != nil {
+ return
+ }
+ if st.identityKey, err = ntor.NewKeypair(false); err != nil {
+ return
+ }
+ if st.drbgSeed, err = drbg.NewSeed(); err != nil {
+ return
+ }
+
+ // Encode it into JSON format and write the state file.
+ js.NodeID = st.nodeID.Base64()
+ js.PrivateKey = st.identityKey.Private().Base64()
+ js.PublicKey = st.identityKey.Public().Base64()
+ js.DrbgSeed = st.drbgSeed.Base64()
+
+ var encoded []byte
+ if encoded, err = json.Marshal(js); err != nil {
+ return
+ }
+
+ if err = ioutil.WriteFile(path.Join(stateDir, stateFile), encoded, 0600); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/transports/transports.go b/transports/transports.go
new file mode 100644
index 0000000..6b80bdc
--- /dev/null
+++ b/transports/transports.go
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * 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 transports provides a interface to query supported pluggable
+// transports.
+package transports
+
+import (
+ "fmt"
+ "sync"
+
+ "git.torproject.org/pluggable-transports/obfs4.git/transports/base"
+ "git.torproject.org/pluggable-transports/obfs4.git/transports/obfs2"
+ "git.torproject.org/pluggable-transports/obfs4.git/transports/obfs3"
+ "git.torproject.org/pluggable-transports/obfs4.git/transports/obfs4"
+)
+
+var transportMapLock sync.Mutex
+var transportMap map[string]base.Transport
+
+// Register registers a transport protocol.
+func Register(transport base.Transport) error {
+ transportMapLock.Lock()
+ defer transportMapLock.Unlock()
+
+ name := transport.Name()
+ _, registered := transportMap[name]
+ if registered {
+ return fmt.Errorf("transport '%s' already registered", name)
+ }
+ transportMap[name] = transport
+
+ return nil
+}
+
+// Transports returns the list of registered transport protocols.
+func Transports() []string {
+ transportMapLock.Lock()
+ defer transportMapLock.Unlock()
+
+ var ret []string
+ for name := range transportMap {
+ ret = append(ret, name)
+ }
+
+ return ret
+}
+
+// Get returns a transport protocol implementation by name.
+func Get(name string) base.Transport {
+ transportMapLock.Lock()
+ defer transportMapLock.Unlock()
+
+ t := transportMap[name]
+
+ return t
+}
+
+func init() {
+ // Initialize the transport list.
+ transportMap = make(map[string]base.Transport)
+
+ // Register all the currently supported transports.
+ Register(new(obfs2.Transport))
+ Register(new(obfs3.Transport))
+ Register(new(obfs4.Transport))
+}