From 8a7de7e1e252a73d786bafab042eedde8c025ad6 Mon Sep 17 00:00:00 2001 From: Brandon Wiley Date: Tue, 15 Nov 2016 14:52:04 -0600 Subject: Removed transports from shapeshifter-dispatcher (now located in shapeshifter-transports) --- transports/base/base.go | 90 ---- transports/meeklite/base.go | 89 ---- transports/meeklite/meek.go | 373 -------------- transports/obfs2/obfs2.go | 374 -------------- transports/obfs3/obfs3.go | 366 -------------- transports/obfs4/framing/framing.go | 306 ------------ transports/obfs4/framing/framing_test.go | 169 ------- transports/obfs4/handshake_ntor.go | 425 ---------------- transports/obfs4/handshake_ntor_test.go | 248 ---------- transports/obfs4/obfs4.go | 646 ------------------------- transports/obfs4/packet.go | 176 ------- transports/obfs4/statefile.go | 252 ---------- transports/scramblesuit/base.go | 99 ---- transports/scramblesuit/conn.go | 523 -------------------- transports/scramblesuit/handshake_ticket.go | 228 --------- transports/scramblesuit/handshake_uniformdh.go | 173 ------- transports/scramblesuit/hkdf_expand.go | 67 --- 17 files changed, 4604 deletions(-) delete mode 100644 transports/base/base.go delete mode 100644 transports/meeklite/base.go delete mode 100644 transports/meeklite/meek.go delete mode 100644 transports/obfs2/obfs2.go delete mode 100644 transports/obfs3/obfs3.go delete mode 100644 transports/obfs4/framing/framing.go delete mode 100644 transports/obfs4/framing/framing_test.go delete mode 100644 transports/obfs4/handshake_ntor.go delete mode 100644 transports/obfs4/handshake_ntor_test.go delete mode 100644 transports/obfs4/obfs4.go delete mode 100644 transports/obfs4/packet.go delete mode 100644 transports/obfs4/statefile.go delete mode 100644 transports/scramblesuit/base.go delete mode 100644 transports/scramblesuit/conn.go delete mode 100644 transports/scramblesuit/handshake_ticket.go delete mode 100644 transports/scramblesuit/handshake_uniformdh.go delete mode 100644 transports/scramblesuit/hkdf_expand.go (limited to 'transports') diff --git a/transports/base/base.go b/transports/base/base.go deleted file mode 100644 index bb0902e..0000000 --- a/transports/base/base.go +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2014, Yawning Angel - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -// Package base provides the common interface that each supported transport -// protocol must implement. -package base - -import ( - "net" - - "git.torproject.org/pluggable-transports/goptlib.git" -) - -type DialFunc func(string, string) (net.Conn, error) - -// 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) - - // Dial creates an outbound net.Conn, and does whatever is required - // (eg: handshaking) to get the connection to the point where it is - // ready to relay data. - Dial(network, address string, dialFn DialFunc, 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/meeklite/base.go b/transports/meeklite/base.go deleted file mode 100644 index 6401885..0000000 --- a/transports/meeklite/base.go +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2015, Yawning Angel - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -// Package meeklite provides an implementation of the Meek circumvention -// protocol. Only a client implementation is provided, and no effort is -// made to normalize the TLS fingerprint. -// -// It borrows quite liberally from the real meek-client code. -package meeklite - -import ( - "fmt" - "net" - - "git.torproject.org/pluggable-transports/goptlib.git" - "github.com/OperatorFoundation/obfs4/transports/base" -) - -const transportName = "meek_lite" - -// Transport is the Meek implementation of the base.Transport interface. -type Transport struct{} - -// Name returns the name of the Meek transport protocol. -func (t *Transport) Name() string { - return transportName -} - -// ClientFactory returns a new meekClientFactory instance. -func (t *Transport) ClientFactory(stateDir string) (base.ClientFactory, error) { - cf := &meekClientFactory{transport: t} - return cf, nil -} - -// ServerFactory will one day return a new meekServerFactory instance. -func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFactory, error) { - // TODO: Fill this in eventually, though for servers people should - // just use the real thing. - return nil, fmt.Errorf("server not supported") -} - -type meekClientFactory struct { - transport base.Transport -} - -func (cf *meekClientFactory) Transport() base.Transport { - return cf.transport -} - -func (cf *meekClientFactory) ParseArgs(args *pt.Args) (interface{}, error) { - return newClientArgs(args) -} - -func (cf *meekClientFactory) Dial(network, addr string, dialFn base.DialFunc, args interface{}) (net.Conn, error) { - // Validate args before opening outgoing connection. - ca, ok := args.(*meekClientArgs) - if !ok { - return nil, fmt.Errorf("invalid argument type for args") - } - - return newMeekConn(network, addr, dialFn, ca) -} - -var _ base.ClientFactory = (*meekClientFactory)(nil) -var _ base.Transport = (*Transport)(nil) diff --git a/transports/meeklite/meek.go b/transports/meeklite/meek.go deleted file mode 100644 index 79012ac..0000000 --- a/transports/meeklite/meek.go +++ /dev/null @@ -1,373 +0,0 @@ -/* - * Copyright (c) 2015, Yawning Angel - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -package meeklite - -import ( - "bytes" - "crypto/rand" - "crypto/sha256" - "encoding/hex" - "errors" - "fmt" - "io" - "io/ioutil" - "net" - "net/http" - gourl "net/url" - "runtime" - "sync" - "time" - - "git.torproject.org/pluggable-transports/goptlib.git" - "github.com/OperatorFoundation/obfs4/transports/base" -) - -const ( - urlArg = "url" - frontArg = "front" - - maxChanBacklog = 16 - - // Constants shamelessly stolen from meek-client.go... - maxPayloadLength = 0x10000 - initPollInterval = 100 * time.Millisecond - maxPollInterval = 5 * time.Second - pollIntervalMultiplier = 1.5 - maxRetries = 10 - retryDelay = 30 * time.Second -) - -var ( - // ErrNotSupported is the error returned for a unsupported operation. - ErrNotSupported = errors.New("meek_lite: operation not supported") - - loopbackAddr = net.IPv4(127, 0, 0, 1) -) - -type meekClientArgs struct { - url *gourl.URL - front string -} - -func (ca *meekClientArgs) Network() string { - return transportName -} - -func (ca *meekClientArgs) String() string { - return transportName + ":" + ca.front + ":" + ca.url.String() -} - -func newClientArgs(args *pt.Args) (ca *meekClientArgs, err error) { - ca = &meekClientArgs{} - - // Parse the URL argument. - str, ok := args.Get(urlArg) - if !ok { - return nil, fmt.Errorf("missing argument '%s'", urlArg) - } - ca.url, err = gourl.Parse(str) - if err != nil { - return nil, fmt.Errorf("malformed url: '%s'", str) - } - switch ca.url.Scheme { - case "http", "https": - default: - return nil, fmt.Errorf("invalid scheme: '%s'", ca.url.Scheme) - } - - // Parse the (optional) front argument. - ca.front, _ = args.Get(frontArg) - - return ca, nil -} - -type meekConn struct { - sync.Mutex - - args *meekClientArgs - sessionID string - transport *http.Transport - - workerRunning bool - workerWrChan chan []byte - workerRdChan chan []byte - workerCloseChan chan bool - rdBuf *bytes.Buffer -} - -func (c *meekConn) Read(p []byte) (n int, err error) { - // If there is data left over from the previous read, - // service the request using the buffered data. - if c.rdBuf != nil { - if c.rdBuf.Len() == 0 { - panic("empty read buffer") - } - n, err = c.rdBuf.Read(p) - if c.rdBuf.Len() == 0 { - c.rdBuf = nil - } - return - } - - // Wait for the worker to enqueue more incoming data. - b, ok := <-c.workerRdChan - if !ok { - // Close() was called and the worker's shutting down. - return 0, io.ErrClosedPipe - } - - // Ew, an extra copy, but who am I kidding, it's meek. - buf := bytes.NewBuffer(b) - n, err = buf.Read(p) - if buf.Len() > 0 { - // If there's data pending, stash the buffer so the next - // Read() call will use it to fulfuill the Read(). - c.rdBuf = buf - } - return -} - -func (c *meekConn) Write(b []byte) (n int, err error) { - // Check to see if the connection is actually open. - c.Lock() - closed := !c.workerRunning - c.Unlock() - if closed { - return 0, io.ErrClosedPipe - } - - if len(b) == 0 { - return 0, nil - } - - // Copy the data to be written to a new slice, since - // we return immediately after queuing and the peer can - // happily reuse `b` before data has been sent. - toWrite := len(b) - b2 := make([]byte, toWrite) - copy(b2, b) - if ok := c.enqueueWrite(b2); !ok { - // Technically we did enqueue data, but the worker's - // got closed out from under us. - return 0, io.ErrClosedPipe - } - runtime.Gosched() - return len(b), nil -} - -func (c *meekConn) Close() error { - // Ensure that we do this once and only once. - c.Lock() - defer c.Unlock() - if !c.workerRunning { - return nil - } - - // Tear down the worker. - c.workerRunning = false - c.workerCloseChan <- true - - return nil -} - -func (c *meekConn) LocalAddr() net.Addr { - return &net.IPAddr{IP: loopbackAddr} -} - -func (c *meekConn) RemoteAddr() net.Addr { - return c.args -} - -func (c *meekConn) SetDeadline(t time.Time) error { - return ErrNotSupported -} - -func (c *meekConn) SetReadDeadline(t time.Time) error { - return ErrNotSupported -} - -func (c *meekConn) SetWriteDeadline(t time.Time) error { - return ErrNotSupported -} - -func (c *meekConn) enqueueWrite(b []byte) (ok bool) { - defer func() { recover() }() - c.workerWrChan <- b - return true -} - -func (c *meekConn) roundTrip(sndBuf []byte) (recvBuf []byte, err error) { - var req *http.Request - var resp *http.Response - - for retries := 0; retries < maxRetries; retries++ { - url := *c.args.url - host := url.Host - if c.args.front != "" { - url.Host = c.args.front - } - req, err = http.NewRequest("POST", url.String(), bytes.NewReader(sndBuf)) - if err != nil { - return nil, err - } - if c.args.front != "" { - req.Host = host - } - req.Header.Set("X-Session-Id", c.sessionID) - req.Header.Set("User-Agent", "") - - resp, err = c.transport.RoundTrip(req) - if err != nil { - return nil, err - } - if resp.StatusCode != http.StatusOK { - err = fmt.Errorf("status code was %d, not %d", resp.StatusCode, http.StatusOK) - time.Sleep(retryDelay) - } else { - defer resp.Body.Close() - recvBuf, err = ioutil.ReadAll(io.LimitReader(resp.Body, maxPayloadLength)) - return - } - } - return -} - -func (c *meekConn) ioWorker() { - interval := initPollInterval - var sndBuf, leftBuf []byte -loop: - - for { - sndBuf = nil - select { - case <-time.After(interval): - // If the poll interval has elapsed, issue a request. - case sndBuf = <-c.workerWrChan: - // If there is data pending a send, issue a request. - case _ = <-c.workerCloseChan: - break loop - } - - // Combine short writes as long as data is available to be - // sent immediately and it will not put us over the max - // payload limit. Any excess data is stored and dispatched - // as the next request). - sndBuf = append(leftBuf, sndBuf...) - wrSz := len(sndBuf) - for len(c.workerWrChan) > 0 && wrSz < maxPayloadLength { - b := <-c.workerWrChan - sndBuf = append(sndBuf, b...) - wrSz = len(sndBuf) - } - if wrSz > maxPayloadLength { - wrSz = maxPayloadLength - } - - // Issue a request. - rdBuf, err := c.roundTrip(sndBuf[:wrSz]) - if err != nil { - // Welp, something went horrifically wrong. - break loop - } - - // Stash the remaining payload if any. - leftBuf = sndBuf[wrSz:] // Store the remaining data - if len(leftBuf) == 0 { - leftBuf = nil - } - - // Determine the next poll interval. - if len(rdBuf) > 0 { - // Received data, enqueue the read. - c.workerRdChan <- rdBuf - - // And poll immediately. - interval = 0 - } else if wrSz > 0 { - // Sent data, poll immediately. - interval = 0 - } else if interval == 0 { - // Neither sent nor received data, initialize the delay. - interval = initPollInterval - } else { - // Apply a multiplicative backoff. - interval = time.Duration(float64(interval) * pollIntervalMultiplier) - if interval > maxPollInterval { - interval = maxPollInterval - } - } - - runtime.Gosched() - } - - // Unblock callers waiting in Read() for data that will never arrive, - // and callers waiting in Write() for data that will never get sent. - close(c.workerRdChan) - close(c.workerWrChan) - - // In case the close was done on an error condition, update the state - // variable so that further calls to Write() will fail. - c.Lock() - defer c.Unlock() - c.workerRunning = false -} - -func newMeekConn(network, addr string, dialFn base.DialFunc, ca *meekClientArgs) (net.Conn, error) { - id, err := newSessionID() - if err != nil { - return nil, err - } - - tr := &http.Transport{Dial: dialFn} - conn := &meekConn{ - args: ca, - sessionID: id, - transport: tr, - workerRunning: true, - workerWrChan: make(chan []byte, maxChanBacklog), - workerRdChan: make(chan []byte, maxChanBacklog), - workerCloseChan: make(chan bool), - } - - // Start the I/O worker. - go conn.ioWorker() - - return conn, nil -} - -func newSessionID() (string, error) { - var b [64]byte - if _, err := rand.Read(b[:]); err != nil { - return "", err - } - h := sha256.Sum256(b[:]) - return hex.EncodeToString(h[:16]), nil -} - -var _ net.Conn = (*meekConn)(nil) -var _ net.Addr = (*meekClientArgs)(nil) diff --git a/transports/obfs2/obfs2.go b/transports/obfs2/obfs2.go deleted file mode 100644 index 8bbdbaa..0000000 --- a/transports/obfs2/obfs2.go +++ /dev/null @@ -1,374 +0,0 @@ -/* - * Copyright (c) 2014, Yawning Angel - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -// Package 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" - "github.com/OperatorFoundation/obfs4/common/csrand" - "github.com/OperatorFoundation/obfs4/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) Dial(network, addr string, dialFn base.DialFunc, args interface{}) (net.Conn, error) { - conn, err := dialFn(network, addr) - if err != nil { - return nil, err - } - dialConn := conn - if conn, err = newObfs2ClientConn(conn); err != nil { - dialConn.Close() - return nil, err - } - return conn, nil -} - -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() 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 err - } - 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 err - } - } - - // 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)) - txBlock, err := aes.NewCipher(padKey) - if err != nil { - return err - } - txStream := cipher.NewCTR(txBlock, padIV) - conn.tx = &cipher.StreamWriter{S: txStream, W: conn.Conn} - if _, err := conn.Conn.Write(seed[:]); err != nil { - return err - } - if _, err := conn.Write(hsBlob); err != nil { - return err - } - - // 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 err - } - var peerPadMagic []byte - if conn.isInitiator { - peerPadMagic = []byte(responderPadString) - } else { - peerPadMagic = []byte(initiatorPadString) - } - peerKey, peerIV := hsKdf(peerPadMagic, peerSeed[:], !conn.isInitiator) - rxBlock, err := aes.NewCipher(peerKey) - if err != nil { - return err - } - rxStream := cipher.NewCTR(rxBlock, peerIV) - conn.rx = &cipher.StreamReader{S: rxStream, R: conn.Conn} - hsHdr := make([]byte, hsLen) - if _, err := io.ReadFull(conn, hsHdr[:]); err != nil { - return err - } - - // 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 { - return fmt.Errorf("invalid magic value: %x", peerMagic) - } - padLen = binary.BigEndian.Uint32(hsHdr[4:8]) - if padLen > maxPadding { - return fmt.Errorf("padlen too long: %d", padLen) - } - - // 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 err - } - - // Derive the actual keys. - if err := conn.kdf(seed[:], peerSeed[:]); err != nil { - return err - } - - return nil -} - -func (conn *obfs2Conn) kdf(seed, peerSeed []byte) 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) - initBlock, err := aes.NewCipher(initKey) - if err != nil { - return err - } - initStream := cipher.NewCTR(initBlock, initIV) - - respKey, respIV := hsKdf([]byte(responderKdfString), combSeed, false) - respBlock, err := aes.NewCipher(respKey) - if err != nil { - return err - } - 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 nil -} - -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 deleted file mode 100644 index c872129..0000000 --- a/transports/obfs3/obfs3.go +++ /dev/null @@ -1,366 +0,0 @@ -/* - * Copyright (c) 2014, Yawning Angel - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -// Package obfs3 provides an implementation of the Tor Project's obfs3 -// obfuscation protocol. -package obfs3 - -import ( - "bytes" - "crypto/aes" - "crypto/cipher" - "crypto/hmac" - "crypto/sha256" - "errors" - "io" - "net" - "time" - - "git.torproject.org/pluggable-transports/goptlib.git" - "github.com/OperatorFoundation/obfs4/common/csrand" - "github.com/OperatorFoundation/obfs4/common/uniformdh" - "github.com/OperatorFoundation/obfs4/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) Dial(network, addr string, dialFn base.DialFunc, args interface{}) (net.Conn, error) { - conn, err := dialFn(network, addr) - if err != nil { - return nil, err - } - dialConn := conn - if conn, err = newObfs3ClientConn(conn); err != nil { - dialConn.Close() - return nil, err - } - return conn, nil -} - -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() 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) - privateKey, err := uniformdh.GenerateKey(csrand.Reader) - if err != nil { - return err - } - padLen := csrand.IntRange(0, maxPadding/2) - blob := make([]byte, uniformdh.Size+padLen) - publicKey, err := privateKey.PublicKey.Bytes() - if err != nil { - return err - } - copy(blob[0:], publicKey) - if err := csrand.Bytes(blob[uniformdh.Size:]); err != nil { - return err - } - if _, err := conn.Conn.Write(blob); err != nil { - return err - } - - // Read the public key from the peer. - rawPeerPublicKey := make([]byte, uniformdh.Size) - if _, err := io.ReadFull(conn.Conn, rawPeerPublicKey); err != nil { - return err - } - var peerPublicKey uniformdh.PublicKey - if err := peerPublicKey.SetBytes(rawPeerPublicKey); err != nil { - return err - } - - // 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). - sharedSecret, err := uniformdh.Handshake(privateKey, &peerPublicKey) - if err != nil { - return err - } - if err := conn.kdf(sharedSecret); err != nil { - return err - } - - return nil -} - -func (conn *obfs3Conn) kdf(sharedSecret []byte) 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. - initBlock, err := aes.NewCipher(initSecret[:keyLen]) - if err != nil { - return err - } - initStream := cipher.NewCTR(initBlock, initSecret[keyLen:]) - - respBlock, err := aes.NewCipher(respSecret[:keyLen]) - if err != nil { - return err - } - respStream := cipher.NewCTR(respBlock, respSecret[keyLen:]) - - if conn.isInitiator { - conn.tx = &cipher.StreamWriter{S: initStream, W: conn.Conn} - conn.rx = &cipher.StreamReader{S: respStream, R: conn.rxBuf} - conn.txMagic = initMagic - conn.rxMagic = respMagic - } else { - conn.tx = &cipher.StreamWriter{S: respStream, W: conn.Conn} - conn.rx = &cipher.StreamReader{S: initStream, R: conn.rxBuf} - conn.txMagic = respMagic - conn.rxMagic = initMagic - } - - return nil -} - -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 deleted file mode 100644 index 9eaba69..0000000 --- a/transports/obfs4/framing/framing.go +++ /dev/null @@ -1,306 +0,0 @@ -/* - * Copyright (c) 2014, Yawning Angel - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -// -// Package 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" - - "golang.org/x/crypto/nacl/secretbox" - - "github.com/OperatorFoundation/obfs4/common/csrand" - "github.com/OperatorFoundation/obfs4/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 - if err = encoder.nonce.bytes(&nonce); 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. - if err = decoder.nonce.bytes(&decoder.nextNonce); 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 deleted file mode 100644 index 03e0d3b..0000000 --- a/transports/obfs4/framing/framing_test.go +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (c) 2014, Yawning Angel - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -package 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 deleted file mode 100644 index ed7c114..0000000 --- a/transports/obfs4/handshake_ntor.go +++ /dev/null @@ -1,425 +0,0 @@ -/* - * Copyright (c) 2014, Yawning Angel - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -package obfs4 - -import ( - "bytes" - "crypto/hmac" - "crypto/sha256" - "encoding/hex" - "errors" - "fmt" - "hash" - "strconv" - "time" - - "github.com/OperatorFoundation/obfs4/common/csrand" - "github.com/OperatorFoundation/obfs4/common/ntor" - "github.com/OperatorFoundation/obfs4/common/replayfilter" - "github.com/OperatorFoundation/obfs4/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) - if err := csrand.Bytes(pad); err != nil { - return nil, err - } - - return pad, nil -} diff --git a/transports/obfs4/handshake_ntor_test.go b/transports/obfs4/handshake_ntor_test.go deleted file mode 100644 index 04afcef..0000000 --- a/transports/obfs4/handshake_ntor_test.go +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright (c) 2014, Yawning Angel - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -package obfs4 - -import ( - "bytes" - "testing" - - "github.com/OperatorFoundation/obfs4/common/ntor" - "github.com/OperatorFoundation/obfs4/common/replayfilter" -) - -func TestHandshakeNtorClient(t *testing.T) { - // Generate the server node id and id keypair, and ephemeral session keys. - 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) - clientKeypair, err := ntor.NewKeypair(true) - if err != nil { - t.Fatalf("client: ntor.NewKeypair failed: %s", err) - } - serverKeypair, err := ntor.NewKeypair(true) - if err != nil { - t.Fatalf("server: ntor.NewKeypair failed: %s", err) - } - - // Test client handshake padding. - for l := clientMinPadLength; l <= clientMaxPadLength; l++ { - // Generate the client state and override the pad length. - 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. - 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.Fatalf("[%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 oversized client padding. - 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) - } - 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)") - } -} - -func TestHandshakeNtorServer(t *testing.T) { - // Generate the server node id and id keypair, and ephemeral session keys. - 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) - clientKeypair, err := ntor.NewKeypair(true) - if err != nil { - t.Fatalf("client: ntor.NewKeypair failed: %s", err) - } - serverKeypair, err := ntor.NewKeypair(true) - if err != nil { - t.Fatalf("server: ntor.NewKeypair failed: %s", err) - } - - // Test server handshake padding. - for l := serverMinPadLength; l <= serverMaxPadLength+inlineSeedFrameLength; l++ { - // Generate the client state and override the pad length. - 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. - 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.Fatalf("[%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. - 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) - } - 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.Fatalf("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 deleted file mode 100644 index 269e1c6..0000000 --- a/transports/obfs4/obfs4.go +++ /dev/null @@ -1,646 +0,0 @@ -/* - * Copyright (c) 2014, Yawning Angel - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -// Package obfs4 provides an implementation of the Tor Project's obfs4 -// obfuscation protocol. -package obfs4 - -import ( - "bytes" - "crypto/sha256" - "flag" - "fmt" - "math/rand" - "net" - "strconv" - "syscall" - "time" - - "git.torproject.org/pluggable-transports/goptlib.git" - "github.com/OperatorFoundation/obfs4/common/drbg" - "github.com/OperatorFoundation/obfs4/common/ntor" - "github.com/OperatorFoundation/obfs4/common/probdist" - "github.com/OperatorFoundation/obfs4/common/replayfilter" - "github.com/OperatorFoundation/obfs4/transports/base" - "github.com/OperatorFoundation/obfs4/transports/obfs4/framing" -) - -const ( - transportName = "obfs4" - - nodeIDArg = "node-id" - publicKeyArg = "public-key" - privateKeyArg = "private-key" - seedArg = "drbg-seed" - iatArg = "iat-mode" - certArg = "cert" - - biasCmdArg = "obfs4-distBias" - - seedLength = drbg.SeedLength - headerLength = framing.FrameOverhead + packetOverhead - clientHandshakeTimeout = time.Duration(60) * time.Second - serverHandshakeTimeout = time.Duration(30) * time.Second - replayTTL = time.Duration(3) * time.Hour - - maxIATDelay = 100 - maxCloseDelayBytes = maxHandshakeLength - maxCloseDelay = 60 -) - -const ( - iatNone = iota - iatEnabled - iatParanoid -) - -// biasedDist controls if the probability table will be ScrambleSuit style or -// uniformly distributed. -var biasedDist bool - -type obfs4ClientArgs struct { - nodeID *ntor.NodeID - publicKey *ntor.PublicKey - sessionKey *ntor.Keypair - iatMode int -} - -// 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) { - st, err := serverStateFromArgs(stateDir, args) - if err != nil { - return nil, err - } - - var iatSeed *drbg.Seed - if st.iatMode != iatNone { - iatSeedSrc := sha256.Sum256(st.drbgSeed.Bytes()[:]) - var err error - 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(certArg, st.cert.String()) - ptArgs.Add(iatArg, strconv.Itoa(st.iatMode)) - - // 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, st.iatMode, 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 nodeID *ntor.NodeID - var publicKey *ntor.PublicKey - - // The "new" (version >= 0.0.3) bridge lines use a unified "cert" argument - // for the Node ID and Public Key. - certStr, ok := args.Get(certArg) - if ok { - cert, err := serverCertFromString(certStr) - if err != nil { - return nil, err - } - nodeID, publicKey = cert.unpack() - } else { - // The "old" style (version <= 0.0.2) bridge lines use separate Node ID - // and Public Key arguments in Base16 encoding and are a UX disaster. - nodeIDStr, ok := args.Get(nodeIDArg) - if !ok { - return nil, fmt.Errorf("missing argument '%s'", nodeIDArg) - } - var err error - if nodeID, err = ntor.NodeIDFromHex(nodeIDStr); err != nil { - return nil, err - } - - publicKeyStr, ok := args.Get(publicKeyArg) - if !ok { - return nil, fmt.Errorf("missing argument '%s'", publicKeyArg) - } - if publicKey, err = ntor.PublicKeyFromHex(publicKeyStr); err != nil { - return nil, err - } - } - - // IAT config is common across the two bridge line formats. - iatStr, ok := args.Get(iatArg) - if !ok { - return nil, fmt.Errorf("missing argument '%s'", iatArg) - } - iatMode, err := strconv.Atoi(iatStr) - if err != nil || iatMode < iatNone || iatMode > iatParanoid { - return nil, fmt.Errorf("invalid iat-mode '%d'", iatMode) - } - - // 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, iatMode}, nil -} - -func (cf *obfs4ClientFactory) Dial(network, addr string, dialFn base.DialFunc, args interface{}) (net.Conn, error) { - // Validate args before bothering to open connection. - ca, ok := args.(*obfs4ClientArgs) - if !ok { - return nil, fmt.Errorf("invalid argument type for args") - } - conn, err := dialFn(network, addr) - if err != nil { - return nil, err - } - dialConn := conn - if conn, err = newObfs4ClientConn(conn, ca); err != nil { - dialConn.Close() - return nil, err - } - return conn, nil -} - -type obfs4ServerFactory struct { - transport base.Transport - args *pt.Args - - nodeID *ntor.NodeID - identityKey *ntor.Keypair - lenSeed *drbg.Seed - iatSeed *drbg.Seed - iatMode int - 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, sf.iatMode, 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 - iatMode int - - 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 args.iatMode != iatNone { - 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, args.iatMode, 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 { - n, err := conn.Conn.Read(hsBuf[:]) - if 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]) - - 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) 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 err - } - - // Consume the client handshake. - var hsBuf [maxHandshakeLength]byte - for { - n, err := conn.Conn.Read(hsBuf[:]) - if 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]) - - seed, err := hs.parseClientHandshake(sf.replayFilter, conn.receiveBuffer.Bytes()) - if err == ErrMarkNotFoundYet { - continue - } else if err != nil { - return err - } - conn.receiveBuffer.Reset() - - if err := conn.Conn.SetDeadline(time.Time{}); err != nil { - return nil - } - - // 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. - blob, err := hs.generateHandshake() - if err != nil { - return err - } - var frameBuf bytes.Buffer - if _, err = frameBuf.Write(blob); err != nil { - return err - } - - // Send the PRNG seed as the first packet. - if err := conn.makePacket(&frameBuf, packetTypePrngSeed, sf.lenSeed.Bytes()[:], 0); err != nil { - return err - } - if _, err = conn.Conn.Write(frameBuf.Bytes()); err != nil { - return err - } - - return nil -} - -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 - } - } - - if conn.iatMode != iatParanoid { - // For non-paranoid IAT, pad once per burst. Paranoid IAT handles - // things differently. - if err = conn.padBurst(&frameBuf, conn.lenDist.Sample()); 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.iatMode != iatNone { - var iatFrame [framing.MaximumSegmentLength]byte - for frameBuf.Len() > 0 { - iatWrLen := 0 - - switch conn.iatMode { - case iatEnabled: - // Standard (ScrambleSuit-style) IAT obfuscation optimizes for - // bulk transport and will write ~MTU sized frames when - // possible. - iatWrLen, err = frameBuf.Read(iatFrame[:]) - - case iatParanoid: - // Paranoid IAT obfuscation throws performance out of the - // window and will sample the length distribution every time a - // write is scheduled. - targetLen := conn.lenDist.Sample() - if frameBuf.Len() < targetLen { - // There's not enough data buffered for the target write, - // so padding must be inserted. - if err = conn.padBurst(&frameBuf, targetLen); err != nil { - return 0, err - } - if frameBuf.Len() != targetLen { - // Ugh, padding came out to a value that required more - // than one frame, this is relatively unlikely so just - // resample since there's enough data to ensure that - // the next sample will be written. - continue - } - } - iatWrLen, err = frameBuf.Read(iatFrame[:targetLen]) - } - 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, toPadTo int) (err error) { - tailLen := burst.Len() % framing.MaximumSegmentLength - - 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 -} - -func init() { - flag.BoolVar(&biasedDist, biasCmdArg, false, "Enable obfs4 using ScrambleSuit style table generation") -} - -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 deleted file mode 100644 index ac3028a..0000000 --- a/transports/obfs4/packet.go +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (c) 2014, Yawning Angel - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -package obfs4 - -import ( - "crypto/sha256" - "encoding/binary" - "fmt" - "io" - - "github.com/OperatorFoundation/obfs4/common/drbg" - "github.com/OperatorFoundation/obfs4/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) 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, err := conn.encoder.Encode(frame[:], pkt[:pktLen]) - if err != nil { - // All encoder errors are fatal. - return err - } - wrLen, err := w.Write(frame[:frameLen]) - if err != nil { - return err - } else if wrLen < frameLen { - return io.ErrShortWrite - } - - return nil -} - -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 deleted file mode 100644 index 6f89c6c..0000000 --- a/transports/obfs4/statefile.go +++ /dev/null @@ -1,252 +0,0 @@ -/* - * Copyright (c) 2014, Yawning Angel - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -package obfs4 - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "io/ioutil" - "os" - "path" - "strconv" - "strings" - - "git.torproject.org/pluggable-transports/goptlib.git" - "github.com/OperatorFoundation/obfs4/common/csrand" - "github.com/OperatorFoundation/obfs4/common/drbg" - "github.com/OperatorFoundation/obfs4/common/ntor" -) - -const ( - stateFile = "obfs4_state.json" - bridgeFile = "obfs4_bridgeline.txt" - - certSuffix = "==" - certLength = ntor.NodeIDLength + ntor.PublicKeyLength -) - -type jsonServerState struct { - NodeID string `json:"node-id"` - PrivateKey string `json:"private-key"` - PublicKey string `json:"public-key"` - DrbgSeed string `json:"drbg-seed"` - IATMode int `json:"iat-mode"` -} - -type obfs4ServerCert struct { - raw []byte -} - -func (cert *obfs4ServerCert) String() string { - return strings.TrimSuffix(base64.StdEncoding.EncodeToString(cert.raw), certSuffix) -} - -func (cert *obfs4ServerCert) unpack() (*ntor.NodeID, *ntor.PublicKey) { - if len(cert.raw) != certLength { - panic(fmt.Sprintf("cert length %d is invalid", len(cert.raw))) - } - - nodeID, _ := ntor.NewNodeID(cert.raw[:ntor.NodeIDLength]) - pubKey, _ := ntor.NewPublicKey(cert.raw[ntor.NodeIDLength:]) - - return nodeID, pubKey -} - -func serverCertFromString(encoded string) (*obfs4ServerCert, error) { - decoded, err := base64.StdEncoding.DecodeString(encoded + certSuffix) - if err != nil { - return nil, fmt.Errorf("failed to decode cert: %s", err) - } - - if len(decoded) != certLength { - return nil, fmt.Errorf("cert length %d is invalid", len(decoded)) - } - - return &obfs4ServerCert{raw: decoded}, nil -} - -func serverCertFromState(st *obfs4ServerState) *obfs4ServerCert { - cert := new(obfs4ServerCert) - cert.raw = append(st.nodeID.Bytes()[:], st.identityKey.Public().Bytes()[:]...) - return cert -} - -type obfs4ServerState struct { - nodeID *ntor.NodeID - identityKey *ntor.Keypair - drbgSeed *drbg.Seed - iatMode int - - cert *obfs4ServerCert -} - -func (st *obfs4ServerState) clientString() string { - return fmt.Sprintf("%s=%s %s=%d", certArg, st.cert, iatArg, st.iatMode) -} - -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) - iatStr, iatOk := args.Get(iatArg) - - if !privKeyOk && !nodeIDOk && !seedOk && !iatOk { - 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) - } else if !iatOk { - // Disable IAT if not specified. - return nil, fmt.Errorf("missing argument '%s'", iatArg) - } else { - // Parse and validate the iat-mode argument. - iatMode, err := strconv.Atoi(iatStr) - if err != nil { - return nil, fmt.Errorf("malformed iat-mode '%s'", iatStr) - } - js.IATMode = iatMode - } - - return serverStateFromJSONServerState(stateDir, &js) -} - -func serverStateFromJSONServerState(stateDir string, js *jsonServerState) (*obfs4ServerState, error) { - var err error - - st := new(obfs4ServerState) - if st.nodeID, err = ntor.NodeIDFromHex(js.NodeID); err != nil { - return nil, err - } - if st.identityKey, err = ntor.KeypairFromHex(js.PrivateKey); err != nil { - return nil, err - } - if st.drbgSeed, err = drbg.SeedFromHex(js.DrbgSeed); err != nil { - return nil, err - } - if js.IATMode < iatNone || js.IATMode > iatParanoid { - return nil, fmt.Errorf("invalid iat-mode '%d'", js.IATMode) - } - st.iatMode = js.IATMode - st.cert = serverCertFromState(st) - - // Generate a human readable summary of the configured endpoint. - if err = newBridgeFile(stateDir, st); err != nil { - return nil, err - } - - return st, nil -} - -func jsonServerStateFromFile(stateDir string, js *jsonServerState) error { - fPath := path.Join(stateDir, stateFile) - f, err := ioutil.ReadFile(fPath) - 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 fmt.Errorf("failed to load statefile '%s': %s", fPath, 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 - } - st.iatMode = iatNone - - // Encode it into JSON format and write the state file. - js.NodeID = st.nodeID.Hex() - js.PrivateKey = st.identityKey.Private().Hex() - js.PublicKey = st.identityKey.Public().Hex() - js.DrbgSeed = st.drbgSeed.Hex() - js.IATMode = st.iatMode - - 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 -} - -func newBridgeFile(stateDir string, st *obfs4ServerState) error { - const prefix = "# obfs4 torrc client bridge line\n" + - "#\n" + - "# This file is an automatically generated bridge line based on\n" + - "# the current obfs4proxy configuration. EDITING IT WILL HAVE\n" + - "# NO EFFECT.\n" + - "#\n" + - "# Before distributing this Bridge, edit the placeholder fields\n" + - "# to contain the actual values:\n" + - "# - The public IP address of your obfs4 bridge.\n" + - "# - The TCP/IP port of your obfs4 bridge.\n" + - "# - The bridge's fingerprint.\n\n" - - bridgeLine := fmt.Sprintf("Bridge obfs4 : %s\n", - st.clientString()) - - tmp := []byte(prefix + bridgeLine) - if err := ioutil.WriteFile(path.Join(stateDir, bridgeFile), tmp, 0600); err != nil { - return err - } - - return nil -} diff --git a/transports/scramblesuit/base.go b/transports/scramblesuit/base.go deleted file mode 100644 index b6f7cb7..0000000 --- a/transports/scramblesuit/base.go +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2015, Yawning Angel - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -// Package scramblesuit provides an implementation of the ScrambleSuit -// obfuscation protocol. The implementation is client only. -package scramblesuit - -import ( - "fmt" - "net" - - "git.torproject.org/pluggable-transports/goptlib.git" - "github.com/OperatorFoundation/obfs4/transports/base" -) - -const transportName = "scramblesuit" - -// Transport is the ScrambleSuit implementation of the base.Transport interface. -type Transport struct{} - -// Name returns the name of the ScrambleSuit transport protocol. -func (t *Transport) Name() string { - return transportName -} - -// ClientFactory returns a new ssClientFactory instance. -func (t *Transport) ClientFactory(stateDir string) (base.ClientFactory, error) { - tStore, err := loadTicketStore(stateDir) - if err != nil { - return nil, err - } - cf := &ssClientFactory{transport: t, ticketStore: tStore} - return cf, nil -} - -// ServerFactory will one day return a new ssServerFactory instance. -func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFactory, error) { - // TODO: Fill this in eventually, though obfs4 is better. - return nil, fmt.Errorf("server not supported") -} - -type ssClientFactory struct { - transport base.Transport - ticketStore *ssTicketStore -} - -func (cf *ssClientFactory) Transport() base.Transport { - return cf.transport -} - -func (cf *ssClientFactory) ParseArgs(args *pt.Args) (interface{}, error) { - return newClientArgs(args) -} - -func (cf *ssClientFactory) Dial(network, addr string, dialFn base.DialFunc, args interface{}) (net.Conn, error) { - // Validate args before opening outgoing connection. - ca, ok := args.(*ssClientArgs) - if !ok { - return nil, fmt.Errorf("invalid argument type for args") - } - - conn, err := dialFn(network, addr) - if err != nil { - return nil, err - } - dialConn := conn - if conn, err = newScrambleSuitClientConn(conn, cf.ticketStore, ca); err != nil { - dialConn.Close() - return nil, err - } - return conn, nil -} - -var _ base.ClientFactory = (*ssClientFactory)(nil) -var _ base.Transport = (*Transport)(nil) diff --git a/transports/scramblesuit/conn.go b/transports/scramblesuit/conn.go deleted file mode 100644 index 5efd12f..0000000 --- a/transports/scramblesuit/conn.go +++ /dev/null @@ -1,523 +0,0 @@ -/* - * Copyright (c) 2015, Yawning Angel - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -package scramblesuit - -import ( - "bytes" - "crypto/aes" - "crypto/cipher" - "crypto/hmac" - "crypto/sha256" - "encoding/base32" - "encoding/binary" - "errors" - "fmt" - "hash" - "io" - "net" - "time" - - "git.torproject.org/pluggable-transports/goptlib.git" - "github.com/OperatorFoundation/obfs4/common/csrand" - "github.com/OperatorFoundation/obfs4/common/drbg" - "github.com/OperatorFoundation/obfs4/common/probdist" - "github.com/OperatorFoundation/obfs4/common/uniformdh" -) - -const ( - passwordArg = "password" - - maxSegmentLength = 1448 - maxPayloadLength = 1427 - sharedSecretLength = 160 / 8 // k_B - clientHandshakeTimeout = time.Duration(60) * time.Second - - minLenDistLength = 21 - maxLenDistLength = maxSegmentLength - - keyLength = 32 + 8 + 32 - - pktPrngSeedLength = 32 - pktOverhead = macLength + pktHdrLength - pktHdrLength = 2 + 2 + 1 - pktPayload = 1 - pktNewTicket = 1 << 1 - pktPrngSeed = 1 << 2 -) - -var ( - // ErrNotSupported is the error returned for a unsupported operation. - ErrNotSupported = errors.New("scramblesuit: operation not supported") - - // ErrInvalidPacket is the error returned when a invalid packet is received. - ErrInvalidPacket = errors.New("scramblesuit: invalid packet") - - zeroPadBytes [maxPayloadLength]byte -) - -type ssSharedSecret [sharedSecretLength]byte - -type ssClientArgs struct { - kB *ssSharedSecret - sessionKey *uniformdh.PrivateKey -} - -func newClientArgs(args *pt.Args) (ca *ssClientArgs, err error) { - ca = &ssClientArgs{} - if ca.kB, err = parsePasswordArg(args); err != nil { - return nil, err - } - - // Generate the client keypair before opening a connection since the time - // taken is visible to an adversary. This key might not end up being used - // if a session ticket is present, but this doesn't take that long. - if ca.sessionKey, err = uniformdh.GenerateKey(csrand.Reader); err != nil { - return nil, err - } - return -} - -func parsePasswordArg(args *pt.Args) (*ssSharedSecret, error) { - str, ok := args.Get(passwordArg) - if !ok { - return nil, fmt.Errorf("missing argument '%s'", passwordArg) - } - - // To match the obfsproxy behavior, 'str' should contain a Base32 encoded - // shared secret (k_B) used for handshaking. - decoded, err := base32.StdEncoding.DecodeString(str) - if err != nil { - return nil, fmt.Errorf("failed to decode password: %s", err) - } - if len(decoded) != sharedSecretLength { - return nil, fmt.Errorf("password length %d is invalid", len(decoded)) - } - ss := new(ssSharedSecret) - copy(ss[:], decoded) - return ss, nil -} - -type ssCryptoState struct { - s cipher.Stream - mac hash.Hash -} - -func newCryptoState(aesKey []byte, ivPrefix []byte, macKey []byte) (*ssCryptoState, error) { - // The ScrambleSuit CTR-AES256 link crypto uses an 8 byte prefix from the - // KDF, and a 64 bit counter initialized to 1 as the IV. The initial value - // of the counter isn't documented in the spec either. - var initialCtr = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01} - iv := make([]byte, 0, aes.BlockSize) - iv = append(iv, ivPrefix...) - iv = append(iv, initialCtr...) - b, err := aes.NewCipher(aesKey) - if err != nil { - return nil, err - } - s := cipher.NewCTR(b, iv) - mac := hmac.New(sha256.New, macKey) - return &ssCryptoState{s: s, mac: mac}, nil -} - -type ssConn struct { - net.Conn - - isServer bool - - lenDist *probdist.WeightedDist - receiveBuffer *bytes.Buffer - receiveDecodedBuffer *bytes.Buffer - receiveState ssRxState - - txCrypto *ssCryptoState - rxCrypto *ssCryptoState - - ticketStore *ssTicketStore -} - -type ssRxState struct { - mac []byte - hdr []byte - - totalLen int - payloadLen int -} - -func (conn *ssConn) Read(b []byte) (n int, err error) { - // If the receive payload buffer is empty, consume data off the network. - for conn.receiveDecodedBuffer.Len() == 0 { - if err = conn.readPackets(); err != nil { - break - } - } - - // Service the read request using buffered payload. - if conn.receiveDecodedBuffer.Len() > 0 { - n, _ = conn.receiveDecodedBuffer.Read(b) - } - return -} - -func (conn *ssConn) Write(b []byte) (n int, err error) { - var frameBuf bytes.Buffer - p := b - toSend := len(p) - - for toSend > 0 { - // Send as much payload as will fit into each frame as possible. - wrLen := len(p) - if wrLen > maxPayloadLength { - wrLen = maxPayloadLength - } - payload := p[:wrLen] - if err = conn.makePacket(&frameBuf, pktPayload, payload, 0); err != nil { - return 0, err - } - - toSend -= wrLen - p = p[wrLen:] - n += wrLen - } - - // Pad out the burst as appropriate. - if err = conn.padBurst(&frameBuf, conn.lenDist.Sample()); err != nil { - return 0, err - } - - // Write and return. - _, err = conn.Conn.Write(frameBuf.Bytes()) - return -} - -func (conn *ssConn) SetDeadline(t time.Time) error { - return ErrNotSupported -} - -func (conn *ssConn) SetReadDeadline(t time.Time) error { - return ErrNotSupported -} - -func (conn *ssConn) SetWriteDeadline(t time.Time) error { - return ErrNotSupported -} - -func (conn *ssConn) makePacket(w io.Writer, pktType byte, data []byte, padLen int) error { - payloadLen := len(data) - totalLen := payloadLen + padLen - if totalLen > maxPayloadLength { - panic(fmt.Sprintf("BUG: makePacket() len(data) + padLen > maxPayloadLength: %d + %d > %d", len(data), padLen, maxPayloadLength)) - } - - // Build the packet header (total length, payload length, flags), - // and append the payload and padding. - pkt := make([]byte, pktHdrLength, pktHdrLength+payloadLen+padLen) - binary.BigEndian.PutUint16(pkt[0:], uint16(totalLen)) - binary.BigEndian.PutUint16(pkt[2:], uint16(payloadLen)) - pkt[4] = pktType - pkt = append(pkt, data...) - pkt = append(pkt, zeroPadBytes[:padLen]...) - - // Encrypt the packet, and calculate the MAC. - conn.txCrypto.s.XORKeyStream(pkt, pkt) - conn.txCrypto.mac.Reset() - conn.txCrypto.mac.Write(pkt) - mac := conn.txCrypto.mac.Sum(nil)[:macLength] - - // Write out MAC | Packet. Note that this does not go onto the network - // yet, as w is a byte.Buffer (This is done so each call to conn.Write() - // gets padding added). - if _, err := w.Write(mac); err != nil { - return err - } - _, err := w.Write(pkt) - return err -} - -func (conn *ssConn) readPackets() error { - // Consume and buffer up to 1 MSS worth of data. - var buf [maxSegmentLength]byte - rdLen, rdErr := conn.Conn.Read(buf[:]) - conn.receiveBuffer.Write(buf[:rdLen]) - - // Process incoming packets incrementally. conn.receiveState stores - // the results of partial processing. - for conn.receiveBuffer.Len() > 0 { - if conn.receiveState.mac == nil { - // Read and store the packet MAC. - if conn.receiveBuffer.Len() < macLength { - break - } - mac := make([]byte, macLength) - conn.receiveBuffer.Read(mac) - conn.receiveState.mac = mac - } - - if conn.receiveState.hdr == nil { - // Read and store the packet header. - if conn.receiveBuffer.Len() < pktHdrLength { - break - } - hdr := make([]byte, pktHdrLength) - conn.receiveBuffer.Read(hdr) - - // Add the encrypted packet header to the HMAC instance, and then - // decrypt it so that the length of the packet can be determined. - conn.rxCrypto.mac.Reset() - conn.rxCrypto.mac.Write(hdr) - conn.rxCrypto.s.XORKeyStream(hdr, hdr) - - // Store the plaintext packet header, and host byte order length - // values. - totalLen := int(binary.BigEndian.Uint16(hdr[0:])) - payloadLen := int(binary.BigEndian.Uint16(hdr[2:])) - if payloadLen > totalLen || totalLen > maxPayloadLength { - return ErrInvalidPacket - } - conn.receiveState.hdr = hdr - conn.receiveState.totalLen = totalLen - conn.receiveState.payloadLen = payloadLen - } - - var data []byte - if conn.receiveState.totalLen > 0 { - // If the packet actually has payload (including padding), read, - // digest and decrypt it. - if conn.receiveBuffer.Len() < conn.receiveState.totalLen { - break - } - data = make([]byte, conn.receiveState.totalLen) - conn.receiveBuffer.Read(data) - conn.rxCrypto.mac.Write(data) - conn.rxCrypto.s.XORKeyStream(data, data) - } - - // Authenticate the packet, by comparing the received MAC with the one - // calculated over the ciphertext consumed off the network. - cmpMAC := conn.rxCrypto.mac.Sum(nil)[:macLength] - if !hmac.Equal(cmpMAC, conn.receiveState.mac[:]) { - return ErrInvalidPacket - } - - // Based on the packet flags, do something useful with the payload. - data = data[:conn.receiveState.payloadLen] - switch conn.receiveState.hdr[4] { - case pktPayload: - // User data, write it into the decoded payload buffer so that Read - // calls can be serviced. - conn.receiveDecodedBuffer.Write(data) - case pktNewTicket: - // New Session Ticket to be used for future handshakes, store it in - // the Session Ticket store. - if conn.isServer || len(data) != ticketKeyLength+ticketLength { - return ErrInvalidPacket - } - conn.ticketStore.storeTicket(conn.RemoteAddr(), data) - case pktPrngSeed: - // New PRNG_SEED for the protocol polymorphism. Regenerate the - // length obfuscation probability distribution. - if conn.isServer || len(data) != pktPrngSeedLength { - return ErrInvalidPacket - } - seed, err := drbg.SeedFromBytes(data) - if err != nil { - return ErrInvalidPacket - } - conn.lenDist.Reset(seed) - default: - return ErrInvalidPacket - } - - // Done processing a packet, clear the partial state. - conn.receiveState.mac = nil - conn.receiveState.hdr = nil - conn.receiveState.totalLen = 0 - conn.receiveState.payloadLen = 0 - } - return rdErr -} - -func (conn *ssConn) clientHandshake(kB *ssSharedSecret, sessionKey *uniformdh.PrivateKey) error { - if conn.isServer { - return fmt.Errorf("clientHandshake called on server connection") - } - - // Query the Session Ticket store to see if there is a stored session - // ticket. - ticket, err := conn.ticketStore.getTicket(conn.RemoteAddr()) - if err != nil { - return err - } else if ticket != nil { - // Ok, there is an existing ticket, so attempt to do a Session Ticket - // handshake. Until we write to the network, failures are non-fatal as - // we can transition gracefully into doing a UniformDH handshake. - - // Derive the keys from the prestored master key received with the - // ticket. This is done before the actual handshake since the - // handshake uses the outgoing HMAC-SHA256-128 key for authentication. - if err = conn.initCrypto(ticket.key[:]); err != nil { - goto handshakeUDH - } - - // Generate and send the ticket handshake. There is no response, since - // both sides have the keying material. - hs := newTicketClientHandshake(conn.txCrypto.mac, ticket) - blob, err := hs.generateHandshake() - if err != nil { - goto handshakeUDH - } - if _, err = conn.Conn.Write(blob); err != nil { - return err - } - return nil - } - -handshakeUDH: - // No session ticket, so take the slow path and do a UniformDH based - // handshake. - - // Generate and send the client handshake. - hs := newDHClientHandshake(kB, sessionKey) - blob, err := hs.generateHandshake() - if err != nil { - return err - } - if _, err = conn.Conn.Write(blob); err != nil { - return err - } - - // Consume the server handshake. Since we don't actually know the length - // of the respose, we need to consume data off the network till we either - // find the tail marker + MAC digest indicating that a handshake response - // has been received, or the maximum handshake size passes without a valid - // response. - var hsBuf [maxHandshakeLength]byte - for { - var n int - if n, err = conn.Conn.Read(hsBuf[:]); err != nil { - return err - } - conn.receiveBuffer.Write(hsBuf[:n]) - - // Attempt to process all the data seen so far as a response. - var seed []byte - n, seed, err = hs.parseServerHandshake(conn.receiveBuffer.Bytes()) - if err == errMarkNotFoundYet { - // No response found yet, keep trying. - continue - } else if err != nil { - return err - } - - // Ok, done processing the handshake, discard the response, and do the - // key derivation based off the calculated shared secret. - _ = conn.receiveBuffer.Next(n) - err = conn.initCrypto(seed) - return err - } -} - -func (conn *ssConn) initCrypto(seed []byte) error { - // Use HKDF-SHA256 (Expand only, no Extract) to generate session keys from - // initial keying material. - okm := hkdfExpand(sha256.New, seed, nil, kdfSecretLength) - var err error - conn.txCrypto, err = newCryptoState(okm[0:32], okm[32:40], okm[80:112]) - if err != nil { - return err - } - conn.rxCrypto, err = newCryptoState(okm[40:72], okm[72:80], okm[112:144]) - if err != nil { - return err - } - return nil -} - -func (conn *ssConn) padBurst(burst *bytes.Buffer, sampleLen int) error { - // Burst contains the fully encrypted+MACed outgoing payload that will be - // written to the network. Pad it out so that the last segment (based on - // the ScrambleSuit MTU) is sampleLen bytes. - - dataLen := burst.Len() % maxSegmentLength - padLen := 0 - if sampleLen >= dataLen { - padLen = sampleLen - dataLen - } else { - padLen = (maxSegmentLength - dataLen) + sampleLen - } - if padLen < pktOverhead { - // The padLen is less than the MAC + packet header in length, so - // two packets are required. - padLen += maxSegmentLength - } - - if padLen == 0 { - return nil - } - if padLen > maxSegmentLength { - // Note: packetmorpher.py: getPadding is slightly wrong and only - // accounts for one of the two packet headers. - if err := conn.makePacket(burst, pktPayload, nil, 700-pktOverhead); err != nil { - return err - } - return conn.makePacket(burst, pktPayload, nil, padLen-(700+2*pktOverhead)) - } - return conn.makePacket(burst, pktPayload, nil, padLen-pktOverhead) -} - -func newScrambleSuitClientConn(conn net.Conn, tStore *ssTicketStore, ca *ssClientArgs) (net.Conn, error) { - // At this point we have kB and our session key, so we can directly - // start handshaking and seeing what happens. - - // Seed the initial polymorphism distribution. - seed, err := drbg.NewSeed() - if err != nil { - return nil, err - } - dist := probdist.New(seed, minLenDistLength, maxLenDistLength, true) - - // Allocate the client structure. - c := &ssConn{conn, false, dist, bytes.NewBuffer(nil), bytes.NewBuffer(nil), ssRxState{}, nil, nil, tStore} - - // Start the handshake timeout. - deadline := time.Now().Add(clientHandshakeTimeout) - if err := conn.SetDeadline(deadline); err != nil { - return nil, err - } - - // Attempt to handshake. - if err := c.clientHandshake(ca.kB, ca.sessionKey); err != nil { - return nil, err - } - - // Stop the handshake timeout. - if err := conn.SetDeadline(time.Time{}); err != nil { - return nil, err - } - - return c, nil -} diff --git a/transports/scramblesuit/handshake_ticket.go b/transports/scramblesuit/handshake_ticket.go deleted file mode 100644 index c9b97cc..0000000 --- a/transports/scramblesuit/handshake_ticket.go +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright (c) 2015, Yawning Angel - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -package scramblesuit - -import ( - "bytes" - "encoding/base32" - "encoding/json" - "errors" - "fmt" - "hash" - "io/ioutil" - "net" - "os" - "path" - "strconv" - "sync" - "time" - - "github.com/OperatorFoundation/obfs4/common/csrand" -) - -const ( - ticketFile = "scramblesuit_tickets.json" - - ticketKeyLength = 32 - ticketLength = 112 - ticketLifetime = 60 * 60 * 24 * 7 - - ticketMinPadLength = 0 - ticketMaxPadLength = 1388 -) - -var ( - errInvalidTicket = errors.New("scramblesuit: invalid serialized ticket") -) - -type ssTicketStore struct { - sync.Mutex - - filePath string - store map[string]*ssTicket -} - -type ssTicket struct { - key [ticketKeyLength]byte - ticket [ticketLength]byte - issuedAt int64 -} - -type ssTicketJSON struct { - KeyTicket string `json:"key-ticket"` - IssuedAt int64 `json:"issuedAt"` -} - -func (t *ssTicket) isValid() bool { - return t.issuedAt+ticketLifetime > time.Now().Unix() -} - -func newTicket(raw []byte) (*ssTicket, error) { - if len(raw) != ticketKeyLength+ticketLength { - return nil, errInvalidTicket - } - t := &ssTicket{issuedAt: time.Now().Unix()} - copy(t.key[:], raw[0:]) - copy(t.ticket[:], raw[ticketKeyLength:]) - return t, nil -} - -func (s *ssTicketStore) storeTicket(addr net.Addr, rawT []byte) { - t, err := newTicket(rawT) - if err != nil { - // Silently ignore ticket store failures. - return - } - - s.Lock() - defer s.Unlock() - - // Add the ticket to the map, and checkpoint to disk. Serialization errors - // are ignored because the handshake code will just use UniformDH if a - // ticket is not available. - s.store[addr.String()] = t - s.serialize() -} - -func (s *ssTicketStore) getTicket(addr net.Addr) (*ssTicket, error) { - aStr := addr.String() - - s.Lock() - defer s.Unlock() - - t, ok := s.store[aStr] - if ok && t != nil { - // Tickets are one use only, so remove tickets from the map, and - // checkpoint the map to disk. - delete(s.store, aStr) - err := s.serialize() - if !t.isValid() { - // Expired ticket, ignore it. - return nil, err - } - return t, err - } - - // No ticket was found, that's fine. - return nil, nil -} - -func (s *ssTicketStore) serialize() error { - encMap := make(map[string]*ssTicketJSON) - for k, v := range s.store { - kt := make([]byte, 0, ticketKeyLength+ticketLength) - kt = append(kt, v.key[:]...) - kt = append(kt, v.ticket[:]...) - ktStr := base32.StdEncoding.EncodeToString(kt) - jsonObj := &ssTicketJSON{KeyTicket: ktStr, IssuedAt: v.issuedAt} - encMap[k] = jsonObj - } - jsonStr, err := json.Marshal(encMap) - if err != nil { - return err - } - return ioutil.WriteFile(s.filePath, jsonStr, 0600) -} - -func loadTicketStore(stateDir string) (*ssTicketStore, error) { - fPath := path.Join(stateDir, ticketFile) - s := &ssTicketStore{filePath: fPath} - s.store = make(map[string]*ssTicket) - - f, err := ioutil.ReadFile(fPath) - if err != nil { - // No ticket store is fine. - if os.IsNotExist(err) { - return s, nil - } - - // But a file read error is not. - return nil, err - } - - encMap := make(map[string]*ssTicketJSON) - if err = json.Unmarshal(f, &encMap); err != nil { - return nil, fmt.Errorf("failed to load ticket store '%s': '%s'", fPath, err) - } - for k, v := range encMap { - raw, err := base32.StdEncoding.DecodeString(v.KeyTicket) - if err != nil || len(raw) != ticketKeyLength+ticketLength { - // Just silently skip corrupted tickets. - continue - } - t := &ssTicket{issuedAt: v.IssuedAt} - if !t.isValid() { - // Just ignore expired tickets. - continue - } - copy(t.key[:], raw[0:]) - copy(t.ticket[:], raw[ticketKeyLength:]) - s.store[k] = t - } - return s, nil -} - -type ssTicketClientHandshake struct { - mac hash.Hash - ticket *ssTicket - padLen int -} - -func (hs *ssTicketClientHandshake) generateHandshake() ([]byte, error) { - var buf bytes.Buffer - hs.mac.Reset() - - // The client handshake is T | P | M | MAC(T | P | M | E) - hs.mac.Write(hs.ticket.ticket[:]) - m := hs.mac.Sum(nil)[:macLength] - p, err := makePad(hs.padLen) - if err != nil { - return nil, err - } - - // Write T, P, M. - buf.Write(hs.ticket.ticket[:]) - buf.Write(p) - buf.Write(m) - - // Calculate and write the MAC. - e := []byte(strconv.FormatInt(getEpochHour(), 10)) - hs.mac.Write(p) - hs.mac.Write(m) - hs.mac.Write(e) - buf.Write(hs.mac.Sum(nil)[:macLength]) - - hs.mac.Reset() - return buf.Bytes(), nil -} - -func newTicketClientHandshake(mac hash.Hash, ticket *ssTicket) *ssTicketClientHandshake { - hs := &ssTicketClientHandshake{mac: mac, ticket: ticket} - hs.padLen = csrand.IntRange(ticketMinPadLength, ticketMaxPadLength) - return hs -} diff --git a/transports/scramblesuit/handshake_uniformdh.go b/transports/scramblesuit/handshake_uniformdh.go deleted file mode 100644 index 1a577e8..0000000 --- a/transports/scramblesuit/handshake_uniformdh.go +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright (c) 2015, Yawning Angel - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -package scramblesuit - -import ( - "bytes" - "crypto/hmac" - "crypto/sha256" - "errors" - "hash" - "strconv" - "time" - - "github.com/OperatorFoundation/obfs4/common/csrand" - "github.com/OperatorFoundation/obfs4/common/uniformdh" -) - -const ( - minHandshakeLength = uniformdh.Size + macLength*2 - maxHandshakeLength = 1532 - dhMinPadLength = 0 - dhMaxPadLength = 1308 - macLength = 128 / 8 // HMAC-SHA256-128() - - kdfSecretLength = keyLength * 2 -) - -var ( - errMarkNotFoundYet = errors.New("mark not found yet") - - // ErrInvalidHandshake is the error returned when the handshake fails. - ErrInvalidHandshake = errors.New("invalid handshake") -) - -type ssDHClientHandshake struct { - mac hash.Hash - keypair *uniformdh.PrivateKey - epochHour []byte - padLen int - - serverPublicKey *uniformdh.PublicKey - serverMark []byte -} - -func (hs *ssDHClientHandshake) generateHandshake() ([]byte, error) { - var buf bytes.Buffer - hs.mac.Reset() - - // The client handshake is X | P_C | M_C | MAC(X | P_C | M_C | E) - x, err := hs.keypair.PublicKey.Bytes() - if err != nil { - return nil, err - } - hs.mac.Write(x) - mC := hs.mac.Sum(nil)[:macLength] - pC, err := makePad(hs.padLen) - if err != nil { - return nil, err - } - - // Write X, P_C, M_C. - buf.Write(x) - buf.Write(pC) - buf.Write(mC) - - // Calculate and write the MAC. - hs.epochHour = []byte(strconv.FormatInt(getEpochHour(), 10)) - hs.mac.Write(pC) - hs.mac.Write(mC) - hs.mac.Write(hs.epochHour) - buf.Write(hs.mac.Sum(nil)[:macLength]) - - return buf.Bytes(), nil -} - -func (hs *ssDHClientHandshake) parseServerHandshake(resp []byte) (int, []byte, error) { - if len(resp) < minHandshakeLength { - return 0, nil, errMarkNotFoundYet - } - - // The server response is Y | P_S | M_S | MAC(Y | P_S | M_S | E). - if hs.serverPublicKey == nil { - y := resp[:uniformdh.Size] - - // Pull out the public key, and derive the server mark. - hs.serverPublicKey = &uniformdh.PublicKey{} - if err := hs.serverPublicKey.SetBytes(y); err != nil { - return 0, nil, err - } - hs.mac.Reset() - hs.mac.Write(y) - hs.serverMark = hs.mac.Sum(nil)[:macLength] - } - - // Find the mark+MAC, if it exits. - endPos := len(resp) - if endPos > maxHandshakeLength-macLength { - endPos = maxHandshakeLength - macLength - } - pos := bytes.Index(resp[uniformdh.Size:endPos], hs.serverMark) - if pos == -1 { - if len(resp) >= maxHandshakeLength { - // Couldn't find the mark in a maximum length response. - return 0, nil, ErrInvalidHandshake - } - return 0, nil, errMarkNotFoundYet - } else if len(resp) < pos+2*macLength { - // Didn't receive the full M_S. - return 0, nil, errMarkNotFoundYet - } - pos += uniformdh.Size - - // Validate the MAC. - hs.mac.Write(resp[uniformdh.Size : pos+macLength]) - hs.mac.Write(hs.epochHour) - macCmp := hs.mac.Sum(nil)[:macLength] - macRx := resp[pos+macLength : pos+2*macLength] - if !hmac.Equal(macCmp, macRx) { - return 0, nil, ErrInvalidHandshake - } - - // Derive the shared secret. - ss, err := uniformdh.Handshake(hs.keypair, hs.serverPublicKey) - if err != nil { - return 0, nil, err - } - seed := sha256.Sum256(ss) - return pos + 2*macLength, seed[:], nil -} - -func newDHClientHandshake(kB *ssSharedSecret, sessionKey *uniformdh.PrivateKey) *ssDHClientHandshake { - hs := &ssDHClientHandshake{keypair: sessionKey} - hs.mac = hmac.New(sha256.New, kB[:]) - hs.padLen = csrand.IntRange(dhMinPadLength, dhMaxPadLength) - return hs -} - -func getEpochHour() int64 { - return time.Now().Unix() / 3600 -} - -func makePad(padLen int) ([]byte, error) { - pad := make([]byte, padLen) - if err := csrand.Bytes(pad); err != nil { - return nil, err - } - return pad, nil -} diff --git a/transports/scramblesuit/hkdf_expand.go b/transports/scramblesuit/hkdf_expand.go deleted file mode 100644 index 9626b38..0000000 --- a/transports/scramblesuit/hkdf_expand.go +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2015, Yawning Angel - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -package scramblesuit - -import ( - "crypto/hmac" - "hash" -) - -func hkdfExpand(hashFn func() hash.Hash, prk []byte, info []byte, l int) []byte { - // Why, yes. golang.org/x/crypto/hkdf exists, and is a fine - // implementation of HKDF. However it does both the extract - // and expand, while ScrambleSuit only does extract, with no - // way to separate the two steps. - - h := hmac.New(hashFn, prk) - digestSz := h.Size() - if l > 255*digestSz { - panic("hkdf: requested OKM length > 255*HashLen") - } - - var t []byte - okm := make([]byte, 0, l) - toAppend := l - ctr := byte(1) - for toAppend > 0 { - h.Reset() - h.Write(t) - h.Write(info) - h.Write([]byte{ctr}) - t = h.Sum(nil) - ctr++ - - aLen := digestSz - if toAppend < digestSz { - aLen = toAppend - } - okm = append(okm, t[:aLen]...) - toAppend -= aLen - } - return okm -} -- cgit v1.2.3