obfs4/transports/obfs2/obfs2.go
Yawning Angel efdc692691 obfs4: Clean up and modernize the codebase
While the thought of dealing with this codebase makes me reach for the
Benzodiazepines, I might as well clean this up.
2023-07-23 12:12:56 +09:00

374 lines
10 KiB
Go

/*
* Copyright (c) 2015, 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 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 "gitlab.com/yawning/obfs4.git/transports/obfs2"
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/binary"
"fmt"
"io"
"net"
"time"
"gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/goptlib"
"gitlab.com/yawning/obfs4.git/common/csrand"
"gitlab.com/yawning/obfs4.git/transports/base"
)
const (
transportName = "obfs2"
sharedSecretArg = "shared-secret"
clientHandshakeTimeout = time.Duration(30) * time.Second
serverHandshakeTimeout = time.Duration(30) * time.Second
magicValue = 0x2bf5ca7e
initiatorPadString = "Initiator obfuscation padding"
responderPadString = "Responder obfuscation padding"
initiatorKdfString = "Initiator obfuscated data"
responderKdfString = "Responder obfuscated data"
maxPadding = 8192
keyLen = 16
seedLen = 16
hsLen = 4 + 4
)
func validateArgs(args *pt.Args) error {
if _, ok := args.Get(sharedSecretArg); ok {
// "shared-secret" is something no bridges use in practice and is thus
// unimplemented.
return fmt.Errorf("unsupported argument '%s'", sharedSecretArg)
}
return nil
}
// Transport is the obfs2 implementation of the base.Transport interface.
type Transport struct{}
// Name returns the name of the obfs2 transport protocol.
func (t *Transport) Name() string {
return transportName
}
// ClientFactory returns a new obfs2ClientFactory instance.
func (t *Transport) ClientFactory(_ string) (base.ClientFactory, error) {
cf := &obfs2ClientFactory{transport: t}
return cf, nil
}
// ServerFactory returns a new obfs2ServerFactory instance.
func (t *Transport) ServerFactory(_ 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) (any, error) {
return nil, validateArgs(args)
}
func (cf *obfs2ClientFactory) 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 = 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) (*obfs2Conn, 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 c, nil
}
func newObfs2ServerConn(conn net.Conn) (*obfs2Conn, 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 c, nil
}
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[:])
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[:])
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.
return conn.kdf(seed[:], peerSeed[:])
}
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)
initBlock, err := aes.NewCipher(initKey)
if err != nil {
return err
}
initStream := cipher.NewCTR(initBlock, initIV)
respKey, respIV := hsKdf([]byte(responderKdfString), combSeed)
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) ([]byte, []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 padKey, padIV
}
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)
_ base.ServerFactory = (*obfs2ServerFactory)(nil)
_ base.Transport = (*Transport)(nil)
_ net.Conn = (*obfs2Conn)(nil)
)