mirror of https://gitlab.com/yawning/obfs4
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
366 lines
10 KiB
Go
366 lines
10 KiB
Go
/*
|
|
* Copyright (c) 2014, Yawning Angel <yawning at schwanenlied dot me>
|
|
* 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 "gitlab.com/yawning/obfs4.git/transports/obfs3"
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"time"
|
|
|
|
"gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/goptlib"
|
|
|
|
"gitlab.com/yawning/obfs4.git/common/csrand"
|
|
"gitlab.com/yawning/obfs4.git/common/uniformdh"
|
|
"gitlab.com/yawning/obfs4.git/transports/base"
|
|
)
|
|
|
|
const (
|
|
transportName = "obfs3"
|
|
|
|
clientHandshakeTimeout = time.Duration(30) * time.Second
|
|
serverHandshakeTimeout = time.Duration(30) * time.Second
|
|
|
|
initiatorKdfString = "Initiator obfuscated data"
|
|
responderKdfString = "Responder obfuscated data"
|
|
initiatorMagicString = "Initiator magic"
|
|
responderMagicString = "Responder magic"
|
|
maxPadding = 8194
|
|
keyLen = 16
|
|
)
|
|
|
|
// Transport is the obfs3 implementation of the base.Transport interface.
|
|
type Transport struct{}
|
|
|
|
// Name returns the name of the obfs3 transport protocol.
|
|
func (t *Transport) Name() string {
|
|
return transportName
|
|
}
|
|
|
|
// ClientFactory returns a new obfs3ClientFactory instance.
|
|
func (t *Transport) ClientFactory(_ string) (base.ClientFactory, error) {
|
|
cf := &obfs3ClientFactory{transport: t}
|
|
return cf, nil
|
|
}
|
|
|
|
// ServerFactory returns a new obfs3ServerFactory instance.
|
|
func (t *Transport) ServerFactory(_ string, _ *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(_ *pt.Args) (any, error) {
|
|
return nil, nil //nolint:nilnil
|
|
}
|
|
|
|
func (cf *obfs3ClientFactory) Dial(network, addr string, dialFn base.DialFunc, _ any) (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) (*obfs3Conn, 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 c, nil
|
|
}
|
|
|
|
func newObfs3ServerConn(conn net.Conn) (*obfs3Conn, 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 c, nil
|
|
}
|
|
|
|
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
|
|
}
|
|
return conn.kdf(sharedSecret)
|
|
}
|
|
|
|
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) (int, 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 0, err
|
|
}
|
|
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) (int, 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 0, err
|
|
}
|
|
copy(blob[padLen:], conn.txMagic)
|
|
if _, err := conn.Conn.Write(blob); err != nil {
|
|
conn.Close()
|
|
return 0, err
|
|
}
|
|
conn.txMagic = nil
|
|
}
|
|
|
|
return conn.tx.Write(b)
|
|
}
|
|
|
|
var (
|
|
_ base.ClientFactory = (*obfs3ClientFactory)(nil)
|
|
_ base.ServerFactory = (*obfs3ServerFactory)(nil)
|
|
_ base.Transport = (*Transport)(nil)
|
|
_ net.Conn = (*obfs3Conn)(nil)
|
|
)
|