[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [obfs4/master] Massive cleanup/code reorg.
commit 339c63f0c8cd4374f6fa26484498eb6fa91b7bca
Author: Yawning Angel <yawning@xxxxxxxxxxxxxx>
Date: Sun Aug 17 17:11:03 2014 +0000
Massive cleanup/code reorg.
* Changed obfs4proxy to be more like obfsproxy in terms of design,
including being an easy framework for developing new TCP/IP style
pluggable transports.
* Added support for also acting as an obfs2/obfs3 client or bridge
as a transition measure (and because the code itself is trivial).
* Massively cleaned up the obfs4 and related code to be easier to
read, and more idiomatic Go-like in style.
* To ease deployment, obfs4proxy will now autogenerate the node-id,
curve25519 keypair, and drbg seed if none are specified, and save
them to a JSON file in the pt_state directory (Fixes Tor bug #12605).
---
README.md | 23 +-
common/csrand/csrand.go | 101 ++++
common/drbg/hash_drbg.go | 149 ++++++
common/ntor/ntor.go | 432 ++++++++++++++++
common/ntor/ntor_test.go | 180 +++++++
common/probdist/weighted_dist.go | 220 +++++++++
common/probdist/weighted_dist_test.go | 80 +++
common/replayfilter/replay_filter.go | 147 ++++++
common/replayfilter/replay_filter_test.go | 95 ++++
common/uniformdh/uniformdh.go | 183 +++++++
common/uniformdh/uniformdh_test.go | 220 +++++++++
csrand/csrand.go | 102 ----
drbg/hash_drbg.go | 147 ------
framing/framing.go | 310 ------------
framing/framing_test.go | 171 -------
handshake_ntor.go | 427 ----------------
handshake_ntor_test.go | 221 ---------
ntor/ntor.go | 437 -----------------
ntor/ntor_test.go | 182 -------
obfs4.go | 758 -----------------------------
obfs4proxy/obfs4proxy.go | 553 ++++++++++-----------
obfs4proxy/proxy_extras.go | 51 --
obfs4proxy/proxy_http.go | 3 -
obfs4proxy/proxy_socks4.go | 2 -
obfs4proxy/pt_extras.go | 23 +-
packet.go | 212 --------
replay_filter.go | 145 ------
replay_filter_test.go | 92 ----
transports/base/base.go | 88 ++++
transports/obfs2/obfs2.go | 367 ++++++++++++++
transports/obfs3/obfs3.go | 358 ++++++++++++++
transports/obfs4/framing/framing.go | 308 ++++++++++++
transports/obfs4/framing/framing_test.go | 169 +++++++
transports/obfs4/handshake_ntor.go | 426 ++++++++++++++++
transports/obfs4/handshake_ntor_test.go | 222 +++++++++
transports/obfs4/obfs4.go | 579 ++++++++++++++++++++++
transports/obfs4/packet.go | 179 +++++++
transports/obfs4/statefile.go | 156 ++++++
transports/transports.go | 91 ++++
weighted_dist.go | 212 --------
weighted_dist_test.go | 82 ----
41 files changed, 5014 insertions(+), 3889 deletions(-)
diff --git a/README.md b/README.md
index c97588a..3ee9c0c 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,6 @@
## obfs4 - The obfourscator
#### Yawning Angel (yawning at torproject dot org)
-### WARNING
-
-This is pre-alpha. Don't expect any security or wire protocol stability yet.
-If you want to use something like this, you should currently probably be looking
-at ScrambleSuit.
-
### What?
This is a look-like nothing obfuscation protocol that incorporates ideas and
@@ -22,6 +16,9 @@ The notable differences between ScrambleSuit and obfs4:
obfuscated via the Elligator 2 mapping.
* The link layer encryption uses NaCl secret boxes (Poly1305/XSalsa20).
+As an added bonus, obfs4proxy also supports acting as an obfs2/3 client and
+bridge to ease the transition to the new protocol.
+
### Why not extend ScrambleSuit?
It's my protocol and I'll obfuscate if I want to.
@@ -43,20 +40,6 @@ listed for clarity.
* SipHash-2-4 (https://github.com/dchest/siphash)
* goptlib (https://git.torproject.org/pluggable-transports/goptlib.git)
-### TODO
-
- * Code cleanups.
- * Write more unit tests.
- * Optimize further.
-
-### WON'T DO
-
- * I do not care that much about standalone mode. Patches *MAY* be accepted,
- especially if they are clean and are useful to Tor users.
- * Yes, I use a bunch of code from the borg^w^wGoogle. If that bothers you
- feel free to write your own implementation.
- * I do not care about older versions of the go runtime.
-
### Thanks
* David Fifield for goptlib.
diff --git a/common/csrand/csrand.go b/common/csrand/csrand.go
new file mode 100644
index 0000000..45849d3
--- /dev/null
+++ b/common/csrand/csrand.go
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Package csrand implements the math/rand interface over crypto/rand, along
+// with some utility functions for common random number/byte related tasks.
+//
+// Not all of the convinience routines are replicated, only those that are
+// immediately useful. The Rand variable provides access to the full math/rand
+// API.
+package csrand
+
+import (
+ cryptRand "crypto/rand"
+ "encoding/binary"
+ "fmt"
+ "io"
+ "math/rand"
+)
+
+var (
+ csRandSourceInstance csRandSource
+
+ // Rand is a math/rand instance backed by crypto/rand CSPRNG.
+ Rand = rand.New(csRandSourceInstance)
+)
+
+type csRandSource struct {
+ // This does not keep any state as it is backed by crypto/rand.
+}
+
+func (r csRandSource) Int63() int64 {
+ var src [8]byte
+ if err := Bytes(src[:]); err != nil {
+ panic(err)
+ }
+ val := binary.BigEndian.Uint64(src[:])
+ val &= (1<<63 - 1)
+
+ return int64(val)
+}
+
+func (r csRandSource) Seed(seed int64) {
+ // No-op.
+}
+
+// Intn returns, as a int, a pseudo random number in [0, n).
+func Intn(n int) int {
+ return Rand.Intn(n)
+}
+
+// Float64 returns, as a float64, a pesudo random number in [0.0,1.0).
+func Float64() float64 {
+ return Rand.Float64()
+}
+
+// IntRange returns a uniformly distributed int [min, max].
+func IntRange(min, max int) int {
+ if max < min {
+ panic(fmt.Sprintf("IntRange: min > max (%d, %d)", min, max))
+ }
+
+ r := (max + 1) - min
+ ret := Rand.Intn(r)
+ return ret + min
+}
+
+// Bytes fills the slice with random data.
+func Bytes(buf []byte) error {
+ if _, err := io.ReadFull(cryptRand.Reader, buf); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// Reader is a alias of rand.Reader.
+var Reader = cryptRand.Reader
diff --git a/common/drbg/hash_drbg.go b/common/drbg/hash_drbg.go
new file mode 100644
index 0000000..5329828
--- /dev/null
+++ b/common/drbg/hash_drbg.go
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Package drbg implements a minimalistic DRBG based off SipHash-2-4 in OFB
+// mode.
+package drbg
+
+import (
+ "encoding/base64"
+ "encoding/binary"
+ "fmt"
+ "hash"
+
+ "github.com/dchest/siphash"
+
+ "git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
+)
+
+// Size is the length of the HashDrbg output.
+const Size = siphash.Size
+
+// SeedLength is the length of the HashDrbg seed.
+const SeedLength = 16 + Size
+
+// Seed is the initial state for a HashDrbg. It consists of a SipHash-2-4
+// key, and 8 bytes of initial data.
+type Seed [SeedLength]byte
+
+// Bytes returns a pointer to the raw HashDrbg seed.
+func (seed *Seed) Bytes() *[SeedLength]byte {
+ return (*[SeedLength]byte)(seed)
+}
+
+// Base64 returns the Base64 representation of the seed.
+func (seed *Seed) Base64() string {
+ return base64.StdEncoding.EncodeToString(seed.Bytes()[:])
+}
+
+// NewSeed returns a Seed initialized with the runtime CSPRNG.
+func NewSeed() (seed *Seed, err error) {
+ seed = new(Seed)
+ if err = csrand.Bytes(seed.Bytes()[:]); err != nil {
+ return nil, err
+ }
+
+ return
+}
+
+// SeedFromBytes creates a Seed from the raw bytes, truncating to SeedLength as
+// appropriate.
+func SeedFromBytes(src []byte) (seed *Seed, err error) {
+ if len(src) < SeedLength {
+ return nil, InvalidSeedLengthError(len(src))
+ }
+
+ seed = new(Seed)
+ copy(seed.Bytes()[:], src)
+
+ return
+}
+
+// SeedFromBase64 creates a Seed from the Base64 representation, truncating to
+// SeedLength as appropriate.
+func SeedFromBase64(encoded string) (seed *Seed, err error) {
+ var raw []byte
+ if raw, err = base64.StdEncoding.DecodeString(encoded); err != nil {
+ return nil, err
+ }
+
+ return SeedFromBytes(raw)
+}
+
+// InvalidSeedLengthError is the error returned when the seed provided to the
+// DRBG is an invalid length.
+type InvalidSeedLengthError int
+
+func (e InvalidSeedLengthError) Error() string {
+ return fmt.Sprintf("invalid seed length: %d", int(e))
+}
+
+// HashDrbg is a CSDRBG based off of SipHash-2-4 in OFB mode.
+type HashDrbg struct {
+ sip hash.Hash64
+ ofb [Size]byte
+}
+
+// NewHashDrbg makes a HashDrbg instance based off an optional seed. The seed
+// is truncated to SeedLength.
+func NewHashDrbg(seed *Seed) (*HashDrbg, error) {
+ drbg := new(HashDrbg)
+ if seed == nil {
+ var err error
+ if seed, err = NewSeed(); err != nil {
+ return nil, err
+ }
+ }
+ drbg.sip = siphash.New(seed.Bytes()[:16])
+ copy(drbg.ofb[:], seed.Bytes()[16:])
+
+ return drbg, nil
+}
+
+// Int63 returns a uniformly distributed random integer [0, 1 << 63).
+func (drbg *HashDrbg) Int63() int64 {
+ block := drbg.NextBlock()
+ ret := binary.BigEndian.Uint64(block)
+ ret &= (1<<63 - 1)
+
+ return int64(ret)
+}
+
+// Seed does nothing, call NewHashDrbg if you want to reseed.
+func (drbg *HashDrbg) Seed(seed int64) {
+ // No-op.
+}
+
+// NextBlock returns the next 8 byte DRBG block.
+func (drbg *HashDrbg) NextBlock() []byte {
+ drbg.sip.Write(drbg.ofb[:])
+ copy(drbg.ofb[:], drbg.sip.Sum(nil))
+
+ ret := make([]byte, Size)
+ copy(ret, drbg.ofb[:])
+ return ret
+}
diff --git a/common/ntor/ntor.go b/common/ntor/ntor.go
new file mode 100644
index 0000000..37cfe88
--- /dev/null
+++ b/common/ntor/ntor.go
@@ -0,0 +1,432 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Package ntor implements the Tor Project's ntor handshake as defined in
+// proposal 216 "Improved circuit-creation key exchange". It also supports
+// using Elligator to transform the Curve25519 public keys sent over the wire
+// to a form that is indistinguishable from random strings.
+//
+// Before using this package, it is strongly recommended that the specification
+// is read and understood.
+package ntor
+
+import (
+ "bytes"
+ "crypto/hmac"
+ "crypto/sha256"
+ "crypto/subtle"
+ "encoding/base64"
+ "fmt"
+ "io"
+
+ "code.google.com/p/go.crypto/curve25519"
+ "code.google.com/p/go.crypto/hkdf"
+
+ "github.com/agl/ed25519/extra25519"
+
+ "git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
+)
+
+const (
+ // PublicKeyLength is the length of a Curve25519 public key.
+ PublicKeyLength = 32
+
+ // RepresentativeLength is the length of an Elligator representative.
+ RepresentativeLength = 32
+
+ // PrivateKeyLength is the length of a Curve25519 private key.
+ PrivateKeyLength = 32
+
+ // SharedSecretLength is the length of a Curve25519 shared secret.
+ SharedSecretLength = 32
+
+ // NodeIDLength is the length of a ntor node identifier.
+ NodeIDLength = 20
+
+ // KeySeedLength is the length of the derived KEY_SEED.
+ KeySeedLength = sha256.Size
+
+ // AuthLength is the lenght of the derived AUTH.
+ AuthLength = sha256.Size
+)
+
+var protoID = []byte("ntor-curve25519-sha256-1")
+var tMac = append(protoID, []byte(":mac")...)
+var tKey = append(protoID, []byte(":key_extract")...)
+var tVerify = append(protoID, []byte(":key_verify")...)
+var mExpand = append(protoID, []byte(":key_expand")...)
+
+// PublicKeyLengthError is the error returned when the public key being
+// imported is an invalid length.
+type PublicKeyLengthError int
+
+func (e PublicKeyLengthError) Error() string {
+ return fmt.Sprintf("ntor: Invalid Curve25519 public key length: %d",
+ int(e))
+}
+
+// PrivateKeyLengthError is the error returned when the private key being
+// imported is an invalid length.
+type PrivateKeyLengthError int
+
+func (e PrivateKeyLengthError) Error() string {
+ return fmt.Sprintf("ntor: Invalid Curve25519 private key length: %d",
+ int(e))
+}
+
+// NodeIDLengthError is the error returned when the node ID being imported is
+// an invalid length.
+type NodeIDLengthError int
+
+func (e NodeIDLengthError) Error() string {
+ return fmt.Sprintf("ntor: Invalid NodeID length: %d", int(e))
+}
+
+// KeySeed is the key material that results from a handshake (KEY_SEED).
+type KeySeed [KeySeedLength]byte
+
+// Bytes returns a pointer to the raw key material.
+func (key_seed *KeySeed) Bytes() *[KeySeedLength]byte {
+ return (*[KeySeedLength]byte)(key_seed)
+}
+
+// Auth is the verifier that results from a handshake (AUTH).
+type Auth [AuthLength]byte
+
+// Bytes returns a pointer to the raw auth.
+func (auth *Auth) Bytes() *[AuthLength]byte {
+ return (*[AuthLength]byte)(auth)
+}
+
+// NodeID is a ntor node identifier.
+type NodeID [NodeIDLength]byte
+
+// NewNodeID creates a NodeID from the raw bytes.
+func NewNodeID(raw []byte) (*NodeID, error) {
+ if len(raw) != NodeIDLength {
+ return nil, NodeIDLengthError(len(raw))
+ }
+
+ nodeID := new(NodeID)
+ copy(nodeID[:], raw)
+
+ return nodeID, nil
+}
+
+// NodeIDFromBase64 creates a new NodeID from the Base64 encoded representation.
+func NodeIDFromBase64(encoded string) (*NodeID, error) {
+ raw, err := base64.StdEncoding.DecodeString(encoded)
+ if err != nil {
+ return nil, err
+ }
+
+ return NewNodeID(raw)
+}
+// Bytes returns a pointer to the raw NodeID.
+func (id *NodeID) Bytes() *[NodeIDLength]byte {
+ return (*[NodeIDLength]byte)(id)
+}
+
+// Base64 returns the Base64 representation of the NodeID.
+func (id *NodeID) Base64() string {
+ return base64.StdEncoding.EncodeToString(id[:])
+}
+
+// PublicKey is a Curve25519 public key in little-endian byte order.
+type PublicKey [PublicKeyLength]byte
+
+// Bytes returns a pointer to the raw Curve25519 public key.
+func (public *PublicKey) Bytes() *[PublicKeyLength]byte {
+ return (*[PublicKeyLength]byte)(public)
+}
+
+// Base64 returns the Base64 representation of the Curve25519 public key.
+func (public *PublicKey) Base64() string {
+ return base64.StdEncoding.EncodeToString(public.Bytes()[:])
+}
+
+// NewPublicKey creates a PublicKey from the raw bytes.
+func NewPublicKey(raw []byte) (*PublicKey, error) {
+ if len(raw) != PublicKeyLength {
+ return nil, PublicKeyLengthError(len(raw))
+ }
+
+ pubKey := new(PublicKey)
+ copy(pubKey[:], raw)
+
+ return pubKey, nil
+}
+
+// PublicKeyFromBase64 returns a PublicKey from a Base64 representation.
+func PublicKeyFromBase64(encoded string) (*PublicKey, error) {
+ raw, err := base64.StdEncoding.DecodeString(encoded)
+ if err != nil {
+ return nil, err
+ }
+
+ return NewPublicKey(raw)
+}
+
+// Representative is an Elligator representative of a Curve25519 public key
+// in little-endian byte order.
+type Representative [RepresentativeLength]byte
+
+// Bytes returns a pointer to the raw Elligator representative.
+func (repr *Representative) Bytes() *[RepresentativeLength]byte {
+ return (*[RepresentativeLength]byte)(repr)
+}
+
+// ToPublic converts a Elligator representative to a Curve25519 public key.
+func (repr *Representative) ToPublic() *PublicKey {
+ pub := new(PublicKey)
+
+ extra25519.RepresentativeToPublicKey(pub.Bytes(), repr.Bytes())
+ return pub
+}
+
+// PrivateKey is a Curve25519 private key in little-endian byte order.
+type PrivateKey [PrivateKeyLength]byte
+
+// Bytes returns a pointer to the raw Curve25519 private key.
+func (private *PrivateKey) Bytes() *[PrivateKeyLength]byte {
+ return (*[PrivateKeyLength]byte)(private)
+}
+
+// Base64 returns the Base64 representation of the Curve25519 private key.
+func (private *PrivateKey) Base64() string {
+ return base64.StdEncoding.EncodeToString(private.Bytes()[:])
+}
+
+// Keypair is a Curve25519 keypair with an optional Elligator representative.
+// As only certain Curve25519 keys can be obfuscated with Elligator, the
+// representative must be generated along with the keypair.
+type Keypair struct {
+ public *PublicKey
+ private *PrivateKey
+ representative *Representative
+}
+
+// Public returns the Curve25519 public key belonging to the Keypair.
+func (keypair *Keypair) Public() *PublicKey {
+ return keypair.public
+}
+
+// Private returns the Curve25519 private key belonging to the Keypair.
+func (keypair *Keypair) Private() *PrivateKey {
+ return keypair.private
+}
+
+// Representative returns the Elligator representative of the public key
+// belonging to the Keypair.
+func (keypair *Keypair) Representative() *Representative {
+ return keypair.representative
+}
+
+// HasElligator returns true if the Keypair has an Elligator representative.
+func (keypair *Keypair) HasElligator() bool {
+ return nil != keypair.representative
+}
+
+// NewKeypair generates a new Curve25519 keypair, and optionally also generates
+// an Elligator representative of the public key.
+func NewKeypair(elligator bool) (*Keypair, error) {
+ keypair := new(Keypair)
+ keypair.private = new(PrivateKey)
+ keypair.public = new(PublicKey)
+ if elligator {
+ keypair.representative = new(Representative)
+ }
+
+ for {
+ // Generate a Curve25519 private key. Like everyone who does this,
+ // run the CSPRNG output through SHA256 for extra tinfoil hattery.
+ priv := keypair.private.Bytes()[:]
+ if err := csrand.Bytes(priv); err != nil {
+ return nil, err
+ }
+ digest := sha256.Sum256(priv)
+ digest[0] &= 248
+ digest[31] &= 127
+ digest[31] |= 64
+ copy(priv, digest[:])
+
+ if elligator {
+ // Apply the Elligator transform. This fails ~50% of the time.
+ if !extra25519.ScalarBaseMult(keypair.public.Bytes(),
+ keypair.representative.Bytes(),
+ keypair.private.Bytes()) {
+ continue
+ }
+ } else {
+ // Generate the corresponding Curve25519 public key.
+ curve25519.ScalarBaseMult(keypair.public.Bytes(),
+ keypair.private.Bytes())
+ }
+
+ return keypair, nil
+ }
+}
+
+// KeypairFromBase64 returns a Keypair from a Base64 representation of the
+// private key.
+func KeypairFromBase64(encoded string) (*Keypair, error) {
+ raw, err := base64.StdEncoding.DecodeString(encoded)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(raw) != PrivateKeyLength {
+ return nil, PrivateKeyLengthError(len(raw))
+ }
+
+ keypair := new(Keypair)
+ keypair.private = new(PrivateKey)
+ keypair.public = new(PublicKey)
+
+ copy(keypair.private[:], raw)
+ curve25519.ScalarBaseMult(keypair.public.Bytes(),
+ keypair.private.Bytes())
+
+ return keypair, nil
+}
+
+// ServerHandshake does the server side of a ntor handshake and returns status,
+// KEY_SEED, and AUTH. If status is not true, the handshake MUST be aborted.
+func ServerHandshake(clientPublic *PublicKey, serverKeypair *Keypair, idKeypair *Keypair, id *NodeID) (ok bool, keySeed *KeySeed, auth *Auth) {
+ var notOk int
+ var secretInput bytes.Buffer
+
+ // Server side uses EXP(X,y) | EXP(X,b)
+ var exp [SharedSecretLength]byte
+ curve25519.ScalarMult(&exp, serverKeypair.private.Bytes(),
+ clientPublic.Bytes())
+ notOk |= constantTimeIsZero(exp[:])
+ secretInput.Write(exp[:])
+
+ curve25519.ScalarMult(&exp, idKeypair.private.Bytes(),
+ clientPublic.Bytes())
+ notOk |= constantTimeIsZero(exp[:])
+ secretInput.Write(exp[:])
+
+ keySeed, auth = ntorCommon(secretInput, id, idKeypair.public,
+ clientPublic, serverKeypair.public)
+ return notOk == 0, keySeed, auth
+}
+
+// ClientHandshake does the client side of a ntor handshake and returnes
+// status, KEY_SEED, and AUTH. If status is not true or AUTH does not match
+// the value recieved from the server, the handshake MUST be aborted.
+func ClientHandshake(clientKeypair *Keypair, serverPublic *PublicKey, idPublic *PublicKey, id *NodeID) (ok bool, keySeed *KeySeed, auth *Auth) {
+ var notOk int
+ var secretInput bytes.Buffer
+
+ // Client side uses EXP(Y,x) | EXP(B,x)
+ var exp [SharedSecretLength]byte
+ curve25519.ScalarMult(&exp, clientKeypair.private.Bytes(),
+ serverPublic.Bytes())
+ notOk |= constantTimeIsZero(exp[:])
+ secretInput.Write(exp[:])
+
+ curve25519.ScalarMult(&exp, clientKeypair.private.Bytes(),
+ idPublic.Bytes())
+ notOk |= constantTimeIsZero(exp[:])
+ secretInput.Write(exp[:])
+
+ keySeed, auth = ntorCommon(secretInput, id, idPublic,
+ clientKeypair.public, serverPublic)
+ return notOk == 0, keySeed, auth
+}
+
+// CompareAuth does a constant time compare of a Auth and a byte slice
+// (presumably received over a network).
+func CompareAuth(auth1 *Auth, auth2 []byte) bool {
+ auth1Bytes := auth1.Bytes()
+ return hmac.Equal(auth1Bytes[:], auth2)
+}
+
+func ntorCommon(secretInput bytes.Buffer, id *NodeID, b *PublicKey, x *PublicKey, y *PublicKey) (*KeySeed, *Auth) {
+ keySeed := new(KeySeed)
+ auth := new(Auth)
+
+ // secret_input/auth_input use this common bit, build it once.
+ suffix := bytes.NewBuffer(b.Bytes()[:])
+ suffix.Write(b.Bytes()[:])
+ suffix.Write(x.Bytes()[:])
+ suffix.Write(y.Bytes()[:])
+ suffix.Write(protoID)
+ suffix.Write(id[:])
+
+ // At this point secret_input has the 2 exponents, concatenated, append the
+ // client/server common suffix.
+ secretInput.Write(suffix.Bytes())
+
+ // KEY_SEED = H(secret_input, t_key)
+ h := hmac.New(sha256.New, tKey)
+ h.Write(secretInput.Bytes())
+ tmp := h.Sum(nil)
+ copy(keySeed[:], tmp)
+
+ // verify = H(secret_input, t_verify)
+ h = hmac.New(sha256.New, tVerify)
+ h.Write(secretInput.Bytes())
+ verify := h.Sum(nil)
+
+ // auth_input = verify | ID | B | Y | X | PROTOID | "Server"
+ authInput := bytes.NewBuffer(verify)
+ authInput.Write(suffix.Bytes())
+ authInput.Write([]byte("Server"))
+ h = hmac.New(sha256.New, tMac)
+ h.Write(authInput.Bytes())
+ tmp = h.Sum(nil)
+ copy(auth[:], tmp)
+
+ return keySeed, auth
+}
+
+func constantTimeIsZero(x []byte) int {
+ var ret byte
+ for _, v := range x {
+ ret |= v
+ }
+
+ return subtle.ConstantTimeByteEq(ret, 0)
+}
+
+// Kdf extracts and expands KEY_SEED via HKDF-SHA256 and returns `okm_len` bytes
+// of key material.
+func Kdf(keySeed []byte, okmLen int) []byte {
+ kdf := hkdf.New(sha256.New, keySeed, tKey, mExpand)
+ okm := make([]byte, okmLen)
+ n, err := io.ReadFull(kdf, okm)
+ if err != nil {
+ panic(fmt.Sprintf("BUG: Failed HKDF: %s", err.Error()))
+ } else if n != len(okm) {
+ panic(fmt.Sprintf("BUG: Got truncated HKDF output: %d", n))
+ }
+
+ return okm
+}
diff --git a/common/ntor/ntor_test.go b/common/ntor/ntor_test.go
new file mode 100644
index 0000000..c92c04e
--- /dev/null
+++ b/common/ntor/ntor_test.go
@@ -0,0 +1,180 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package ntor
+
+import (
+ "bytes"
+ "testing"
+)
+
+// TestNewKeypair tests Curve25519/Elligator keypair generation.
+func TestNewKeypair(t *testing.T) {
+ // Test standard Curve25519 first.
+ keypair, err := NewKeypair(false)
+ if err != nil {
+ t.Fatal("NewKeypair(false) failed:", err)
+ }
+ if keypair == nil {
+ t.Fatal("NewKeypair(false) returned nil")
+ }
+ if keypair.HasElligator() {
+ t.Fatal("NewKeypair(false) has a Elligator representative")
+ }
+
+ // Test Elligator generation.
+ keypair, err = NewKeypair(true)
+ if err != nil {
+ t.Fatal("NewKeypair(true) failed:", err)
+ }
+ if keypair == nil {
+ t.Fatal("NewKeypair(true) returned nil")
+ }
+ if !keypair.HasElligator() {
+ t.Fatal("NewKeypair(true) mising an Elligator representative")
+ }
+}
+
+// Test Client/Server handshake.
+func TestHandshake(t *testing.T) {
+ clientKeypair, err := NewKeypair(true)
+ if err != nil {
+ t.Fatal("Failed to generate client keypair:", err)
+ }
+ if clientKeypair == nil {
+ t.Fatal("Client keypair is nil")
+ }
+
+ serverKeypair, err := NewKeypair(true)
+ if err != nil {
+ t.Fatal("Failed to generate server keypair:", err)
+ }
+ if serverKeypair == nil {
+ t.Fatal("Server keypair is nil")
+ }
+
+ idKeypair, err := NewKeypair(false)
+ if err != nil {
+ t.Fatal("Failed to generate identity keypair:", err)
+ }
+ if idKeypair == nil {
+ t.Fatal("Identity keypair is nil")
+ }
+
+ nodeID, err := NewNodeID([]byte("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13"))
+ if err != nil {
+ t.Fatal("Failed to load NodeId:", err)
+ }
+
+ // ServerHandshake
+ clientPublic := clientKeypair.Representative().ToPublic()
+ ok, serverSeed, serverAuth := ServerHandshake(clientPublic,
+ serverKeypair, idKeypair, nodeID)
+ if !ok {
+ t.Fatal("ServerHandshake failed")
+ }
+ if serverSeed == nil {
+ t.Fatal("ServerHandshake returned nil KEY_SEED")
+ }
+ if serverAuth == nil {
+ t.Fatal("ServerHandshake returned nil AUTH")
+ }
+
+ // ClientHandshake
+ ok, clientSeed, clientAuth := ClientHandshake(clientKeypair,
+ serverKeypair.Public(), idKeypair.Public(), nodeID)
+ if !ok {
+ t.Fatal("ClientHandshake failed")
+ }
+ if clientSeed == nil {
+ t.Fatal("ClientHandshake returned nil KEY_SEED")
+ }
+ if clientAuth == nil {
+ t.Fatal("ClientHandshake returned nil AUTH")
+ }
+
+ // WARNING: Use a constant time comparison in actual code.
+ if 0 != bytes.Compare(clientSeed.Bytes()[:], serverSeed.Bytes()[:]) {
+ t.Fatal("KEY_SEED mismatched between client/server")
+ }
+ if 0 != bytes.Compare(clientAuth.Bytes()[:], serverAuth.Bytes()[:]) {
+ t.Fatal("AUTH mismatched between client/server")
+ }
+}
+
+// Benchmark Client/Server handshake. The actual time taken that will be
+// observed on either the Client or Server is half the reported time per
+// operation since the benchmark does both sides.
+func BenchmarkHandshake(b *testing.B) {
+ // Generate the "long lasting" identity key and NodeId.
+ idKeypair, err := NewKeypair(false)
+ if err != nil || idKeypair == nil {
+ b.Fatal("Failed to generate identity keypair")
+ }
+ nodeID, err := NewNodeID([]byte("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13"))
+ if err != nil {
+ b.Fatal("Failed to load NodeId:", err)
+ }
+ b.ResetTimer()
+
+ // Start the actual benchmark.
+ for i := 0; i < b.N; i++ {
+ // Generate the keypairs.
+ serverKeypair, err := NewKeypair(true)
+ if err != nil || serverKeypair == nil {
+ b.Fatal("Failed to generate server keypair")
+ }
+
+ clientKeypair, err := NewKeypair(true)
+ if err != nil || clientKeypair == nil {
+ b.Fatal("Failed to generate client keypair")
+ }
+
+ // Server handshake.
+ clientPublic := clientKeypair.Representative().ToPublic()
+ ok, serverSeed, serverAuth := ServerHandshake(clientPublic,
+ serverKeypair, idKeypair, nodeID)
+ if !ok || serverSeed == nil || serverAuth == nil {
+ b.Fatal("ServerHandshake failed")
+ }
+
+ // Client handshake.
+ serverPublic := serverKeypair.Representative().ToPublic()
+ ok, clientSeed, clientAuth := ClientHandshake(clientKeypair,
+ serverPublic, idKeypair.Public(), nodeID)
+ if !ok || clientSeed == nil || clientAuth == nil {
+ b.Fatal("ClientHandshake failed")
+ }
+
+ // Validate the authenticator. Real code would pass the AUTH read off
+ // the network as a slice to CompareAuth here.
+ if !CompareAuth(clientAuth, serverAuth.Bytes()[:]) ||
+ !CompareAuth(serverAuth, clientAuth.Bytes()[:]) {
+ b.Fatal("AUTH mismatched between client/server")
+ }
+ }
+}
diff --git a/common/probdist/weighted_dist.go b/common/probdist/weighted_dist.go
new file mode 100644
index 0000000..2386bbe
--- /dev/null
+++ b/common/probdist/weighted_dist.go
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Package probdist implements a weighted probability distribution suitable for
+// protocol parameterization. To allow for easy reproduction of a given
+// distribution, the drbg package is used as the random number source.
+package probdist
+
+import (
+ "container/list"
+ "fmt"
+ "math/rand"
+
+ "git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/drbg"
+)
+
+const (
+ minValues = 1
+ maxValues = 100
+)
+
+// WeightedDist is a weighted distribution.
+type WeightedDist struct {
+ minValue int
+ maxValue int
+ biased bool
+ values []int
+ weights []float64
+
+ alias []int
+ prob []float64
+}
+
+// New creates a weighted distribution of values ranging from min to max
+// based on a HashDrbg initialized with seed. Optionally, bias the weight
+// generation to match the ScrambleSuit non-uniform distribution from
+// obfsproxy.
+func New(seed *drbg.Seed, min, max int, biased bool) (w *WeightedDist) {
+ w = &WeightedDist{minValue: min, maxValue: max, biased: biased}
+
+ if max <= min {
+ panic(fmt.Sprintf("wDist.Reset(): min >= max (%d, %d)", min, max))
+ }
+
+ w.Reset(seed)
+
+ return
+}
+
+// genValues creates a slice containing a random number of random values
+// that when scaled by adding minValue will fall into [min, max].
+func (w *WeightedDist) genValues(rng *rand.Rand) {
+ nValues := (w.maxValue + 1) - w.minValue
+ values := rng.Perm(nValues)
+ if nValues < minValues {
+ nValues = minValues
+ }
+ if nValues > maxValues {
+ nValues = maxValues
+ }
+ nValues = rng.Intn(nValues) + 1
+ w.values = values[:nValues]
+}
+
+// genBiasedWeights generates a non-uniform weight list, similar to the
+// ScrambleSuit prob_dist module.
+func (w *WeightedDist) genBiasedWeights(rng *rand.Rand) {
+ w.weights = make([]float64, len(w.values))
+
+ culmProb := 0.0
+ for i := range w.weights {
+ p := (1.0 - culmProb) * rng.Float64()
+ w.weights[i] = p
+ culmProb += p
+ }
+}
+
+// genUniformWeights generates a uniform weight list.
+func (w *WeightedDist) genUniformWeights(rng *rand.Rand) {
+ w.weights = make([]float64, len(w.values))
+ for i := range w.weights {
+ w.weights[i] = rng.Float64()
+ }
+}
+
+// genTables calculates the alias and prob tables used for Vose's Alias method.
+// Algorithm taken from http://www.keithschwarz.com/darts-dice-coins/
+func (w *WeightedDist) genTables() {
+ n := len(w.weights)
+ var sum float64
+ for _, weight := range w.weights {
+ sum += weight
+ }
+
+ // Create arrays $Alias$ and $Prob$, each of size $n$.
+ alias := make([]int, n)
+ prob := make([]float64, n)
+
+ // Create two worklists, $Small$ and $Large$.
+ small := list.New()
+ large := list.New()
+
+ scaled := make([]float64, n)
+ for i, weight := range w.weights {
+ // Multiply each probability by $n$.
+ p_i := weight * float64(n) / sum
+ scaled[i] = p_i
+
+ // For each scaled probability $p_i$:
+ if scaled[i] < 1.0 {
+ // If $p_i < 1$, add $i$ to $Small$.
+ small.PushBack(i)
+ } else {
+ // Otherwise ($p_i \ge 1$), add $i$ to $Large$.
+ large.PushBack(i)
+ }
+ }
+
+ // While $Small$ and $Large$ are not empty: ($Large$ might be emptied first)
+ for small.Len() > 0 && large.Len() > 0 {
+ // Remove the first element from $Small$; call it $l$.
+ l := small.Remove(small.Front()).(int)
+ // Remove the first element from $Large$; call it $g$.
+ g := large.Remove(large.Front()).(int)
+
+ // Set $Prob[l] = p_l$.
+ prob[l] = scaled[l]
+ // Set $Alias[l] = g$.
+ alias[l] = g
+
+ // Set $p_g := (p_g + p_l) - 1$. (This is a more numerically stable option.)
+ scaled[g] = (scaled[g] + scaled[l]) - 1.0
+
+ if scaled[g] < 1.0 {
+ // If $p_g < 1$, add $g$ to $Small$.
+ small.PushBack(g)
+ } else {
+ // Otherwise ($p_g \ge 1$), add $g$ to $Large$.
+ large.PushBack(g)
+ }
+ }
+
+ // While $Large$ is not empty:
+ for large.Len() > 0 {
+ // Remove the first element from $Large$; call it $g$.
+ g := large.Remove(large.Front()).(int)
+ // Set $Prob[g] = 1$.
+ prob[g] = 1.0
+ }
+
+ // While $Small$ is not empty: This is only possible due to numerical instability.
+ for small.Len() > 0 {
+ // Remove the first element from $Small$; call it $l$.
+ l := small.Remove(small.Front()).(int)
+ // Set $Prob[l] = 1$.
+ prob[l] = 1.0
+ }
+
+ w.prob = prob
+ w.alias = alias
+}
+
+// Reset generates a new distribution with the same min/max based on a new
+// seed.
+func (w *WeightedDist) Reset(seed *drbg.Seed) {
+ // Initialize the deterministic random number generator.
+ drbg, _ := drbg.NewHashDrbg(seed)
+ rng := rand.New(drbg)
+
+ w.genValues(rng)
+ if w.biased {
+ w.genBiasedWeights(rng)
+ } else {
+ w.genUniformWeights(rng)
+ }
+ w.genTables()
+}
+
+// Sample generates a random value according to the distribution.
+func (w *WeightedDist) Sample() int {
+ var idx int
+
+ // Generate a fair die roll from an $n$-sided die; call the side $i$.
+ i := csrand.Intn(len(w.values))
+ // Flip a biased coin that comes up heads with probability $Prob[i]$.
+ if csrand.Float64() <= w.prob[i] {
+ // If the coin comes up "heads," return $i$.
+ idx = i
+ } else {
+ // Otherwise, return $Alias[i]$.
+ idx = w.alias[i]
+ }
+
+ return w.minValue + w.values[idx]
+}
diff --git a/common/probdist/weighted_dist_test.go b/common/probdist/weighted_dist_test.go
new file mode 100644
index 0000000..b705add
--- /dev/null
+++ b/common/probdist/weighted_dist_test.go
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package probdist
+
+import (
+ "fmt"
+ "testing"
+
+ "git.torproject.org/pluggable-transports/obfs4.git/common/drbg"
+)
+
+const debug = false
+
+func TestWeightedDist(t *testing.T) {
+ seed, err := drbg.NewSeed()
+ if err != nil {
+ t.Fatal("failed to generate a DRBG seed:", err)
+ }
+
+ const nrTrials = 1000000
+
+ hist := make([]int, 1000)
+
+ w := New(seed, 0, 999, true)
+ if debug {
+ // Dump a string representation of the probability table.
+ fmt.Println("Table:")
+ var sum float64
+ for _, weight := range w.weights {
+ sum += weight
+ }
+ for i, weight := range w.weights {
+ p := weight / sum
+ if p > 0.000001 { // Filter out tiny values.
+ fmt.Printf(" [%d]: %f\n", w.minValue+w.values[i], p)
+ }
+ }
+ fmt.Println()
+ }
+
+ for i := 0; i < nrTrials; i++ {
+ value := w.Sample()
+ hist[value]++
+ }
+
+ if debug {
+ fmt.Println("Generated:")
+ for value, count := range hist {
+ if count != 0 {
+ p := float64(count) / float64(nrTrials)
+ fmt.Printf(" [%d]: %f (%d)\n", value, p, count)
+ }
+ }
+ }
+}
diff --git a/common/replayfilter/replay_filter.go b/common/replayfilter/replay_filter.go
new file mode 100644
index 0000000..95cc5d6
--- /dev/null
+++ b/common/replayfilter/replay_filter.go
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Package replayfilter implements a generic replay detection filter with a
+// caller specifiable time-to-live. It only detects if a given byte sequence
+// has been seen before based on the SipHash-2-4 digest of the sequence.
+// Collisions are treated as positive matches, though the probability of this
+// happening is negligible.
+package replayfilter
+
+import (
+ "container/list"
+ "encoding/binary"
+ "sync"
+ "time"
+
+ "github.com/dchest/siphash"
+
+ "git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
+)
+
+// maxFilterSize is the maximum capacity of a replay filter. This value is
+// more as a safeguard to prevent runaway filter growth, and is sized to be
+// serveral orders of magnitude greater than the number of connections a busy
+// bridge sees in one day, so in practice should never be reached.
+const maxFilterSize = 100 * 1024
+
+type entry struct {
+ digest uint64
+ firstSeen time.Time
+ element *list.Element
+}
+
+// ReplayFilter is a simple filter designed only to detect if a given byte
+// sequence has been seen before.
+type ReplayFilter struct {
+ sync.Mutex
+
+ filter map[uint64]*entry
+ fifo *list.List
+
+ key [2]uint64
+ ttl time.Duration
+}
+
+// New creates a new ReplayFilter instance.
+func New(ttl time.Duration) (filter *ReplayFilter, err error) {
+ // Initialize the SipHash-2-4 instance with a random key.
+ var key [16]byte
+ if err = csrand.Bytes(key[:]); err != nil {
+ return
+ }
+
+ filter = new(ReplayFilter)
+ filter.filter = make(map[uint64]*entry)
+ filter.fifo = list.New()
+ filter.key[0] = binary.BigEndian.Uint64(key[0:8])
+ filter.key[1] = binary.BigEndian.Uint64(key[8:16])
+ filter.ttl = ttl
+
+ return
+}
+
+// TestAndSet queries the filter for a given byte sequence, inserts the
+// sequence, and returns if it was present before the insertion operation.
+func (f *ReplayFilter) TestAndSet(now time.Time, buf []byte) bool {
+ digest := siphash.Hash(f.key[0], f.key[1], buf)
+
+ f.Lock()
+ defer f.Unlock()
+
+ f.compactFilter(now)
+
+ if e := f.filter[digest]; e != nil {
+ // Hit. Just return.
+ return true
+ }
+
+ // Miss. Add a new entry.
+ e := new(entry)
+ e.digest = digest
+ e.firstSeen = now
+ e.element = f.fifo.PushBack(e)
+ f.filter[digest] = e
+
+ return false
+}
+
+func (f *ReplayFilter) compactFilter(now time.Time) {
+ e := f.fifo.Front()
+ for e != nil {
+ ent, _ := e.Value.(*entry)
+
+ // If the filter is not full, only purge entries that exceed the TTL,
+ // otherwise purge at least one entry, then revert to TTL based
+ // compaction.
+ if f.fifo.Len() < maxFilterSize && f.ttl > 0 {
+ deltaT := now.Sub(ent.firstSeen)
+ if deltaT < 0 {
+ // Aeeeeeee, the system time jumped backwards, potentially by
+ // a lot. This will eventually self-correct, but "eventually"
+ // could be a long time. As much as this sucks, jettison the
+ // entire filter.
+ f.reset()
+ return
+ } else if deltaT < f.ttl {
+ return
+ }
+ }
+
+ // Remove the eldest entry.
+ eNext := e.Next()
+ delete(f.filter, ent.digest)
+ f.fifo.Remove(ent.element)
+ ent.element = nil
+ e = eNext
+ }
+}
+
+func (f *ReplayFilter) reset() {
+ f.filter = make(map[uint64]*entry)
+ f.fifo = list.New()
+}
diff --git a/common/replayfilter/replay_filter_test.go b/common/replayfilter/replay_filter_test.go
new file mode 100644
index 0000000..884e4fb
--- /dev/null
+++ b/common/replayfilter/replay_filter_test.go
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package replayfilter
+
+import (
+ "testing"
+ "time"
+)
+
+func TestReplayFilter(t *testing.T) {
+ ttl := 10 * time.Second
+
+ f, err := New(ttl)
+ if err != nil {
+ t.Fatal("newReplayFilter failed:", err)
+ }
+
+ buf := []byte("This is a test of the Emergency Broadcast System.")
+ now := time.Now()
+
+ // testAndSet into empty filter, returns false (not present).
+ set := f.TestAndSet(now, buf)
+ if set {
+ t.Fatal("TestAndSet empty filter returned true")
+ }
+
+ // testAndSet into filter containing entry, should return true(present).
+ set = f.TestAndSet(now, buf)
+ if !set {
+ t.Fatal("testAndSet populated filter (replayed) returned false")
+ }
+
+ buf2 := []byte("This concludes this test of the Emergency Broadcast System.")
+ now = now.Add(ttl)
+
+ // testAndSet with time advanced.
+ set = f.TestAndSet(now, buf2)
+ if set {
+ t.Fatal("testAndSet populated filter, 2nd entry returned true")
+ }
+ set = f.TestAndSet(now, buf2)
+ if !set {
+ t.Fatal("testAndSet populated filter, 2nd entry (replayed) returned false")
+ }
+
+ // Ensure that the first entry has been removed by compact.
+ set = f.TestAndSet(now, buf)
+ if set {
+ t.Fatal("testAndSet populated filter, compact check returned true")
+ }
+
+ // Ensure that the filter gets reaped if the clock jumps backwards.
+ now = time.Time{}
+ set = f.TestAndSet(now, buf)
+ if set {
+ t.Fatal("testAndSet populated filter, backward time jump returned true")
+ }
+ if len(f.filter) != 1 {
+ t.Fatal("filter map has a unexpected number of entries:", len(f.filter))
+ }
+ if f.fifo.Len() != 1 {
+ t.Fatal("filter fifo has a unexpected number of entries:", f.fifo.Len())
+ }
+
+ // Ensure that the entry is properly added after reaping.
+ set = f.TestAndSet(now, buf)
+ if !set {
+ t.Fatal("testAndSet populated filter, post-backward clock jump (replayed) returned false")
+ }
+}
diff --git a/common/uniformdh/uniformdh.go b/common/uniformdh/uniformdh.go
new file mode 100644
index 0000000..ab94a2e
--- /dev/null
+++ b/common/uniformdh/uniformdh.go
@@ -0,0 +1,183 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Package uniformdh implements the Tor Project's UniformDH key exchange
+// mechanism as defined in the obfs3 protocol specification. This
+// implementation is suitable for obfuscation but MUST NOT BE USED when strong
+// security is required as it is not constant time.
+package uniformdh
+
+import (
+ "fmt"
+ "io"
+ "math/big"
+)
+
+const (
+ // Size is the size of a UniformDH key or shared secret in bytes.
+ Size = 1536 / 8
+
+ // modpStr is the RFC3526 1536-bit MODP Group (Group 5).
+ modpStr = "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" +
+ "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" +
+ "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" +
+ "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" +
+ "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" +
+ "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" +
+ "83655D23DCA3AD961C62F356208552BB9ED529077096966D" +
+ "670C354E4ABC9804F1746C08CA237327FFFFFFFFFFFFFFFF"
+
+ g = 2
+)
+
+var modpGroup *big.Int
+var gen *big.Int
+
+// A PrivateKey represents a UniformDH private key.
+type PrivateKey struct {
+ PublicKey
+ privateKey *big.Int
+}
+
+// A PublicKey represents a UniformDH public key.
+type PublicKey struct {
+ bytes []byte
+ publicKey *big.Int
+}
+
+// Bytes returns the byte representation of a PublicKey.
+func (pub *PublicKey) Bytes() (pubBytes []byte, err error) {
+ if len(pub.bytes) != Size || pub.bytes == nil {
+ return nil, fmt.Errorf("public key is not initialized")
+ }
+ pubBytes = make([]byte, Size)
+ copy(pubBytes, pub.bytes)
+
+ return
+}
+
+// SetBytes sets the PublicKey from a byte slice.
+func (pub *PublicKey) SetBytes(pubBytes []byte) error {
+ if len(pubBytes) != Size {
+ return fmt.Errorf("public key length %d is not %d", len(pubBytes), Size)
+ }
+ pub.bytes = make([]byte, Size)
+ copy(pub.bytes, pubBytes)
+ pub.publicKey = new(big.Int).SetBytes(pub.bytes)
+
+ return nil
+}
+
+// GenerateKey generates a UniformDH keypair using the random source random.
+func GenerateKey(random io.Reader) (priv *PrivateKey, err error) {
+ privBytes := make([]byte, Size)
+ if _, err = io.ReadFull(random, privBytes); err != nil {
+ return
+ }
+ priv, err = generateKey(privBytes)
+
+ return
+}
+
+func generateKey(privBytes []byte) (priv *PrivateKey, err error) {
+ // This function does all of the actual heavy lifting of creating a public
+ // key from a raw 192 byte private key. It is split so that the KAT tests
+ // can be written easily, and not exposed since non-ephemeral keys are a
+ // terrible idea.
+
+ if len(privBytes) != Size {
+ return nil, fmt.Errorf("invalid private key size %d", len(privBytes))
+ }
+
+ // To pick a private UniformDH key, we pick a random 1536-bit number,
+ // and make it even by setting its low bit to 0
+ privBn := new(big.Int).SetBytes(privBytes)
+ wasEven := privBn.Bit(0) == 0
+ privBn = privBn.SetBit(privBn, 0, 0)
+
+ // Let x be that private key, and X = g^x (mod p).
+ pubBn := new(big.Int).Exp(gen, privBn, modpGroup)
+ pubAlt := new(big.Int).Sub(modpGroup, pubBn)
+
+ // When someone sends her public key to the other party, she randomly
+ // decides whether to send X or p-X. Use the lowest most bit of the
+ // private key here as the random coin flip since it is masked out and not
+ // used.
+ //
+ // Note: The spec doesn't explicitly specify it, but here we prepend zeros
+ // to the key so that it is always exactly Size bytes.
+ pubBytes := make([]byte, Size)
+ if wasEven {
+ err = prependZeroBytes(pubBytes, pubBn.Bytes())
+ } else {
+ err = prependZeroBytes(pubBytes, pubAlt.Bytes())
+ }
+ if err != nil {
+ return
+ }
+
+ priv = new(PrivateKey)
+ priv.PublicKey.bytes = pubBytes
+ priv.PublicKey.publicKey = pubBn
+ priv.privateKey = privBn
+
+ return
+}
+
+// Handshake generates a shared secret given a PrivateKey and PublicKey.
+func Handshake(privateKey *PrivateKey, publicKey *PublicKey) (sharedSecret []byte, err error) {
+ // When a party wants to calculate the shared secret, she raises the
+ // foreign public key to her private key.
+ secretBn := new(big.Int).Exp(publicKey.publicKey, privateKey.privateKey, modpGroup)
+ sharedSecret = make([]byte, Size)
+ err = prependZeroBytes(sharedSecret, secretBn.Bytes())
+
+ return
+}
+
+func prependZeroBytes(dst, src []byte) error {
+ zeros := len(dst) - len(src)
+ if zeros < 0 {
+ return fmt.Errorf("src length is greater than destination: %d", zeros)
+ }
+ for i := 0; i < zeros; i++ {
+ dst[i] = 0
+ }
+ copy(dst[zeros:], src)
+
+ return nil
+}
+
+func init() {
+ // Load the MODP group and the generator.
+ var ok bool
+ modpGroup, ok = new(big.Int).SetString(modpStr, 16)
+ if !ok {
+ panic("Failed to load the RFC3526 MODP Group")
+ }
+ gen = big.NewInt(g)
+}
diff --git a/common/uniformdh/uniformdh_test.go b/common/uniformdh/uniformdh_test.go
new file mode 100644
index 0000000..d705d66
--- /dev/null
+++ b/common/uniformdh/uniformdh_test.go
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package uniformdh
+
+import (
+ "bytes"
+ "crypto/rand"
+ "encoding/hex"
+ "testing"
+)
+
+const (
+ xPrivStr = "6f592d676f536874746f20686e6b776f" +
+ "20736874206561676574202e6f592d67" +
+ "6f536874746f20687369742065686720" +
+ "74612e655920676f532d746f6f686874" +
+ "6920207368742065656b20796e612064" +
+ "7567726169646e616f20206668742065" +
+ "61676574202e61507473202c72707365" +
+ "6e652c746620747572752c6561206c6c" +
+ "612065726f20656e6920206e6f592d67" +
+ "6f536874746f2e68482020656e6b776f" +
+ "2073687772652065687420656c4f2064" +
+ "6e4f736562206f72656b74207268756f"
+
+ xPubStr = "76a3d17d5c55b03e865fa3e8267990a7" +
+ "24baa24b0bdd0cc4af93be8de30be120" +
+ "d5533c91bf63ef923b02edcb84b74438" +
+ "3f7de232cca6eb46d07cad83dcaa317f" +
+ "becbc68ca13e2c4019e6a36531067450" +
+ "04aecc0be1dff0a78733fb0e7d5cb7c4" +
+ "97cab77b1331bf347e5f3a7847aa0bc0" +
+ "f4bc64146b48407fed7b931d16972d25" +
+ "fb4da5e6dc074ce2a58daa8de7624247" +
+ "cdf2ebe4e4dfec6d5989aac778c87559" +
+ "d3213d6040d4111ce3a2acae19f9ee15" +
+ "32509e037f69b252fdc30243cbbce9d0"
+
+ yPrivStr = "736562206f72656b74207268756f6867" +
+ "6f2020666c6f2c646120646e77206568" +
+ "657254206568207968736c61206c7262" +
+ "6165206b68746f726775206867616961" +
+ "2e6e482020656e6b776f207368777265" +
+ "2065685479656820766120657274646f" +
+ "652072616874732766206569646c2c73" +
+ "6120646e772065686572542065682079" +
+ "74736c69206c72746165206468746d65" +
+ "202c6e612064687720796f6e6f20656e" +
+ "63206e61622068656c6f206468546d65" +
+ "61202073685479657420657264610a2e"
+
+ yPubStr = "d04e156e554c37ffd7aba749df662350" +
+ "1e4ff4466cb12be055617c1a36872237" +
+ "36d2c3fdce9ee0f9b27774350849112a" +
+ "a5aeb1f126811c9c2f3a9cb13d2f0c3a" +
+ "7e6fa2d3bf71baf50d839171534f227e" +
+ "fbb2ce4227a38c25abdc5ba7fc430111" +
+ "3a2cb2069c9b305faac4b72bf21fec71" +
+ "578a9c369bcac84e1a7dcf0754e342f5" +
+ "bc8fe4917441b88254435e2abaf297e9" +
+ "3e1e57968672d45bd7d4c8ba1bc3d314" +
+ "889b5bc3d3e4ea33d4f2dfdd34e5e5a7" +
+ "2ff24ee46316d4757dad09366a0b66b3"
+
+ ssStr = "78afaf5f457f1fdb832bebc397644a33" +
+ "038be9dba10ca2ce4a076f327f3a0ce3" +
+ "151d477b869ee7ac467755292ad8a77d" +
+ "b9bd87ffbbc39955bcfb03b1583888c8" +
+ "fd037834ff3f401d463c10f899aa6378" +
+ "445140b7f8386a7d509e7b9db19b677f" +
+ "062a7a1a4e1509604d7a0839ccd5da61" +
+ "73e10afd9eab6dda74539d60493ca37f" +
+ "a5c98cd9640b409cd8bb3be2bc5136fd" +
+ "42e764fc3f3c0ddb8db3d87abcf2e659" +
+ "8d2b101bef7a56f50ebc658f9df1287d" +
+ "a81359543e77e4a4cfa7598a4152e4c0"
+)
+
+var xPriv, xPub, yPriv, yPub, ss []byte
+
+// TestGenerateKeyOdd tests creating a UniformDH keypair with a odd private
+// key.
+func TestGenerateKeyOdd(t *testing.T) {
+ xX, err := generateKey(xPriv)
+ if err != nil {
+ t.Fatal("generateKey(xPriv) failed:", err)
+ }
+
+ xPubGen, err := xX.PublicKey.Bytes()
+ if err != nil {
+ t.Fatal("xX.PublicKey.Bytes() failed:", err)
+ }
+ if 0 != bytes.Compare(xPubGen, xPub) {
+ t.Fatal("Generated public key does not match known answer")
+ }
+}
+
+// TestGenerateKeyEven tests creating a UniformDH keypair with a even private
+// key.
+func TestGenerateKeyEven(t *testing.T) {
+ yY, err := generateKey(yPriv)
+ if err != nil {
+ t.Fatal("generateKey(yPriv) failed:", err)
+ }
+
+ yPubGen, err := yY.PublicKey.Bytes()
+ if err != nil {
+ t.Fatal("yY.PublicKey.Bytes() failed:", err)
+ }
+ if 0 != bytes.Compare(yPubGen, yPub) {
+ t.Fatal("Generated public key does not match known answer")
+ }
+}
+
+// TestHandshake tests conductiong a UniformDH handshake with know values.
+func TestHandshake(t *testing.T) {
+ xX, err := generateKey(xPriv)
+ if err != nil {
+ t.Fatal("generateKey(xPriv) failed:", err)
+ }
+ yY, err := generateKey(yPriv)
+ if err != nil {
+ t.Fatal("generateKey(yPriv) failed:", err)
+ }
+
+ xY, err := Handshake(xX, &yY.PublicKey)
+ if err != nil {
+ t.Fatal("Handshake(xX, yY.PublicKey) failed:", err)
+ }
+ yX, err := Handshake(yY, &xX.PublicKey)
+ if err != nil {
+ t.Fatal("Handshake(yY, xX.PublicKey) failed:", err)
+ }
+
+ if 0 != bytes.Compare(xY, yX) {
+ t.Fatal("Generated shared secrets do not match between peers")
+ }
+ if 0 != bytes.Compare(xY, ss) {
+ t.Fatal("Generated shared secret does not match known value")
+ }
+}
+
+// Benchmark UniformDH key generation + exchange. THe actual time taken per
+// peer is half of the reported time as this does 2 key generation an
+// handshake operations.
+func BenchmarkHandshake(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ xX, err := GenerateKey(rand.Reader)
+ if err != nil {
+ b.Fatal("Failed to generate xX keypair", err)
+ }
+
+ yY, err := GenerateKey(rand.Reader)
+ if err != nil {
+ b.Fatal("Failed to generate yY keypair", err)
+ }
+
+ xY, err := Handshake(xX, &yY.PublicKey)
+ if err != nil {
+ b.Fatal("Handshake(xX, yY.PublicKey) failed:", err)
+ }
+ yX, err := Handshake(yY, &xX.PublicKey)
+ if err != nil {
+ b.Fatal("Handshake(yY, xX.PublicKey) failed:", err)
+ }
+
+ _ = xY
+ _ = yX
+ }
+}
+
+func init() {
+ // Load the test vectors into byte slices.
+ var err error
+ xPriv, err = hex.DecodeString(xPrivStr)
+ if err != nil {
+ panic("hex.DecodeString(xPrivStr) failed")
+ }
+ xPub, err = hex.DecodeString(xPubStr)
+ if err != nil {
+ panic("hex.DecodeString(xPubStr) failed")
+ }
+ yPriv, err = hex.DecodeString(yPrivStr)
+ if err != nil {
+ panic("hex.DecodeString(yPrivStr) failed")
+ }
+ yPub, err = hex.DecodeString(yPubStr)
+ if err != nil {
+ panic("hex.DecodeString(yPubStr) failed")
+ }
+ ss, err = hex.DecodeString(ssStr)
+ if err != nil {
+ panic("hex.DecodeString(ssStr) failed")
+ }
+}
diff --git a/csrand/csrand.go b/csrand/csrand.go
deleted file mode 100644
index b059ed0..0000000
--- a/csrand/csrand.go
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * * Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-// Package csrand implements the math/rand interface over crypto/rand, along
-// with some utility functions for common random number/byte related tasks.
-//
-// Not all of the convinience routines are replicated, only those that are
-// useful for obfs4. The CsRand variable provides access to the full math/rand
-// API.
-package csrand
-
-import (
- cryptRand "crypto/rand"
- "encoding/binary"
- "fmt"
- "io"
- "math/rand"
-)
-
-var (
- csRandSourceInstance csRandSource
-
- // CsRand is a math/rand instance backed by crypto/rand CSPRNG.
- CsRand = rand.New(csRandSourceInstance)
-)
-
-type csRandSource struct {
- // This does not keep any state as it is backed by crypto/rand.
-}
-
-func (r csRandSource) Int63() int64 {
- var src [8]byte
- err := Bytes(src[:])
- if err != nil {
- panic(err)
- }
- val := binary.BigEndian.Uint64(src[:])
- val &= (1<<63 - 1)
-
- return int64(val)
-}
-
-func (r csRandSource) Seed(seed int64) {
- // No-op.
-}
-
-// Intn returns, as a int, a pseudo random number in [0, n).
-func Intn(n int) int {
- return CsRand.Intn(n)
-}
-
-// Float64 returns, as a float64, a pesudo random number in [0.0,1.0).
-func Float64() float64 {
- return CsRand.Float64()
-}
-
-// IntRange returns a uniformly distributed int [min, max].
-func IntRange(min, max int) int {
- if max < min {
- panic(fmt.Sprintf("IntRange: min > max (%d, %d)", min, max))
- }
-
- r := (max + 1) - min
- ret := CsRand.Intn(r)
- return ret + min
-}
-
-// Bytes fills the slice with random data.
-func Bytes(buf []byte) error {
- _, err := io.ReadFull(cryptRand.Reader, buf)
- if err != nil {
- return err
- }
-
- return nil
-}
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/drbg/hash_drbg.go b/drbg/hash_drbg.go
deleted file mode 100644
index c94902a..0000000
--- a/drbg/hash_drbg.go
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * * Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-// Package drbg implements a minimalistic DRBG based off SipHash-2-4 in OFB
-// mode.
-package drbg
-
-import (
- "encoding/base64"
- "encoding/binary"
- "fmt"
- "hash"
-
- "github.com/dchest/siphash"
-
- "git.torproject.org/pluggable-transports/obfs4.git/csrand"
-)
-
-// Size is the length of the HashDrbg output.
-const Size = siphash.Size
-
-// SeedLength is the length of the HashDrbg seed.
-const SeedLength = 16 + Size
-
-// Seed is the initial state for a HashDrbg. It consists of a SipHash-2-4
-// key, and 8 bytes of initial data.
-type Seed [SeedLength]byte
-
-// Bytes returns a pointer to the raw HashDrbg seed.
-func (seed *Seed) Bytes() *[SeedLength]byte {
- return (*[SeedLength]byte)(seed)
-}
-
-// Base64 returns the Base64 representation of the seed.
-func (seed *Seed) Base64() string {
- return base64.StdEncoding.EncodeToString(seed.Bytes()[:])
-}
-
-// NewSeed returns a Seed initialized with the runtime CSPRNG.
-func NewSeed() (seed *Seed, err error) {
- seed = new(Seed)
- err = csrand.Bytes(seed.Bytes()[:])
- if err != nil {
- return nil, err
- }
-
- return
-}
-
-// SeedFromBytes creates a Seed from the raw bytes, truncating to SeedLength as
-// appropriate.
-func SeedFromBytes(src []byte) (seed *Seed, err error) {
- if len(src) < SeedLength {
- return nil, InvalidSeedLengthError(len(src))
- }
-
- seed = new(Seed)
- copy(seed.Bytes()[:], src)
-
- return
-}
-
-// SeedFromBase64 creates a Seed from the Base64 representation, truncating to
-// SeedLength as appropriate.
-func SeedFromBase64(encoded string) (seed *Seed, err error) {
- var raw []byte
- raw, err = base64.StdEncoding.DecodeString(encoded)
- if err != nil {
- return nil, err
- }
-
- return SeedFromBytes(raw)
-}
-
-// InvalidSeedLengthError is the error returned when the seed provided to the
-// DRBG is an invalid length.
-type InvalidSeedLengthError int
-
-func (e InvalidSeedLengthError) Error() string {
- return fmt.Sprintf("invalid seed length: %d", int(e))
-}
-
-// HashDrbg is a CSDRBG based off of SipHash-2-4 in OFB mode.
-type HashDrbg struct {
- sip hash.Hash64
- ofb [Size]byte
-}
-
-// NewHashDrbg makes a HashDrbg instance based off an optional seed. The seed
-// is truncated to SeedLength.
-func NewHashDrbg(seed *Seed) *HashDrbg {
- drbg := new(HashDrbg)
- drbg.sip = siphash.New(seed.Bytes()[:16])
- copy(drbg.ofb[:], seed.Bytes()[16:])
-
- return drbg
-}
-
-// Int63 returns a uniformly distributed random integer [0, 1 << 63).
-func (drbg *HashDrbg) Int63() int64 {
- block := drbg.NextBlock()
- ret := binary.BigEndian.Uint64(block)
- ret &= (1<<63 - 1)
-
- return int64(ret)
-}
-
-// Seed does nothing, call NewHashDrbg if you want to reseed.
-func (drbg *HashDrbg) Seed(seed int64) {
- // No-op.
-}
-
-// NextBlock returns the next 8 byte DRBG block.
-func (drbg *HashDrbg) NextBlock() []byte {
- drbg.sip.Write(drbg.ofb[:])
- copy(drbg.ofb[:], drbg.sip.Sum(nil))
-
- ret := make([]byte, Size)
- copy(ret, drbg.ofb[:])
- return ret
-}
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/framing/framing.go b/framing/framing.go
deleted file mode 100644
index 725a762..0000000
--- a/framing/framing.go
+++ /dev/null
@@ -1,310 +0,0 @@
-/*
- * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * * Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-//
-// Package framing implements the obfs4 link framing and cryptography.
-//
-// The Encoder/Decoder shared secret format is:
-// uint8_t[32] NaCl secretbox key
-// uint8_t[16] NaCl Nonce prefix
-// uint8_t[16] SipHash-2-4 key (used to obfsucate length)
-// uint8_t[8] SipHash-2-4 IV
-//
-// The frame format is:
-// uint16_t length (obfsucated, big endian)
-// NaCl secretbox (Poly1305/XSalsa20) containing:
-// uint8_t[16] tag (Part of the secretbox construct)
-// uint8_t[] payload
-//
-// The length field is length of the NaCl secretbox XORed with the truncated
-// SipHash-2-4 digest ran in OFB mode.
-//
-// Initialize K, IV[0] with values from the shared secret.
-// On each packet, IV[n] = H(K, IV[n - 1])
-// mask[n] = IV[n][0:2]
-// obfsLen = length ^ mask[n]
-//
-// The NaCl secretbox (Poly1305/XSalsa20) nonce format is:
-// uint8_t[24] prefix (Fixed)
-// uint64_t counter (Big endian)
-//
-// The counter is initialized to 1, and is incremented on each frame. Since
-// the protocol is designed to be used over a reliable medium, the nonce is not
-// transmitted over the wire as both sides of the conversation know the prefix
-// and the initial counter value. It is imperative that the counter does not
-// wrap, and sessions MUST terminate before 2^64 frames are sent.
-//
-package framing
-
-import (
- "bytes"
- "encoding/binary"
- "errors"
- "fmt"
- "io"
-
- "code.google.com/p/go.crypto/nacl/secretbox"
-
- "git.torproject.org/pluggable-transports/obfs4.git/csrand"
- "git.torproject.org/pluggable-transports/obfs4.git/drbg"
-)
-
-const (
- // MaximumSegmentLength is the length of the largest possible segment
- // including overhead.
- MaximumSegmentLength = 1500 - (40 + 12)
-
- // FrameOverhead is the length of the framing overhead.
- FrameOverhead = lengthLength + secretbox.Overhead
-
- // MaximumFramePayloadLength is the length of the maximum allowed payload
- // per frame.
- MaximumFramePayloadLength = MaximumSegmentLength - FrameOverhead
-
- // KeyLength is the length of the Encoder/Decoder secret key.
- KeyLength = keyLength + noncePrefixLength + drbg.SeedLength
-
- maxFrameLength = MaximumSegmentLength - lengthLength
- minFrameLength = FrameOverhead - lengthLength
-
- keyLength = 32
-
- noncePrefixLength = 16
- nonceCounterLength = 8
- nonceLength = noncePrefixLength + nonceCounterLength
-
- lengthLength = 2
-)
-
-// Error returned when Decoder.Decode() requires more data to continue.
-var ErrAgain = errors.New("framing: More data needed to decode")
-
-// Error returned when Decoder.Decode() failes to authenticate a frame.
-var ErrTagMismatch = errors.New("framing: Poly1305 tag mismatch")
-
-// Error returned when the NaCl secretbox nonce's counter wraps (FATAL).
-var ErrNonceCounterWrapped = errors.New("framing: Nonce counter wrapped")
-
-// InvalidPayloadLengthError is the error returned when Encoder.Encode()
-// rejects the payload length.
-type InvalidPayloadLengthError int
-
-func (e InvalidPayloadLengthError) Error() string {
- return fmt.Sprintf("framing: Invalid payload length: %d", int(e))
-}
-
-type boxNonce struct {
- prefix [noncePrefixLength]byte
- counter uint64
-}
-
-func (nonce *boxNonce) init(prefix []byte) {
- if noncePrefixLength != len(prefix) {
- panic(fmt.Sprintf("BUG: Nonce prefix length invalid: %d", len(prefix)))
- }
-
- copy(nonce.prefix[:], prefix)
- nonce.counter = 1
-}
-
-func (nonce boxNonce) bytes(out *[nonceLength]byte) error {
- // The security guarantee of Poly1305 is broken if a nonce is ever reused
- // for a given key. Detect this by checking for counter wraparound since
- // we start each counter at 1. If it ever happens that more than 2^64 - 1
- // frames are transmitted over a given connection, support for rekeying
- // will be neccecary, but that's unlikely to happen.
- if nonce.counter == 0 {
- return ErrNonceCounterWrapped
- }
-
- copy(out[:], nonce.prefix[:])
- binary.BigEndian.PutUint64(out[noncePrefixLength:], nonce.counter)
-
- return nil
-}
-
-// Encoder is a frame encoder instance.
-type Encoder struct {
- key [keyLength]byte
- nonce boxNonce
- drbg *drbg.HashDrbg
-}
-
-// NewEncoder creates a new Encoder instance. It must be supplied a slice
-// containing exactly KeyLength bytes of keying material.
-func NewEncoder(key []byte) *Encoder {
- if len(key) != KeyLength {
- panic(fmt.Sprintf("BUG: Invalid encoder key length: %d", len(key)))
- }
-
- encoder := new(Encoder)
- copy(encoder.key[:], key[0:keyLength])
- encoder.nonce.init(key[keyLength : keyLength+noncePrefixLength])
- seed, err := drbg.SeedFromBytes(key[keyLength+noncePrefixLength:])
- if err != nil {
- panic(fmt.Sprintf("BUG: Failed to initialize DRBG: %s", err))
- }
- encoder.drbg = drbg.NewHashDrbg(seed)
-
- return encoder
-}
-
-// Encode encodes a single frame worth of payload and returns the encoded
-// length. InvalidPayloadLengthError is recoverable, all other errors MUST be
-// treated as fatal and the session aborted.
-func (encoder *Encoder) Encode(frame, payload []byte) (n int, err error) {
- payloadLen := len(payload)
- if MaximumFramePayloadLength < payloadLen {
- return 0, InvalidPayloadLengthError(payloadLen)
- }
- if len(frame) < payloadLen+FrameOverhead {
- return 0, io.ErrShortBuffer
- }
-
- // Generate a new nonce.
- var nonce [nonceLength]byte
- err = encoder.nonce.bytes(&nonce)
- if err != nil {
- return 0, err
- }
- encoder.nonce.counter++
-
- // Encrypt and MAC payload.
- box := secretbox.Seal(frame[:lengthLength], payload, &nonce, &encoder.key)
-
- // Obfuscate the length.
- length := uint16(len(box) - lengthLength)
- lengthMask := encoder.drbg.NextBlock()
- length ^= binary.BigEndian.Uint16(lengthMask)
- binary.BigEndian.PutUint16(frame[:2], length)
-
- // Return the frame.
- return len(box), nil
-}
-
-// Decoder is a frame decoder instance.
-type Decoder struct {
- key [keyLength]byte
- nonce boxNonce
- drbg *drbg.HashDrbg
-
- nextNonce [nonceLength]byte
- nextLength uint16
- nextLengthInvalid bool
-}
-
-// NewDecoder creates a new Decoder instance. It must be supplied a slice
-// containing exactly KeyLength bytes of keying material.
-func NewDecoder(key []byte) *Decoder {
- if len(key) != KeyLength {
- panic(fmt.Sprintf("BUG: Invalid decoder key length: %d", len(key)))
- }
-
- decoder := new(Decoder)
- copy(decoder.key[:], key[0:keyLength])
- decoder.nonce.init(key[keyLength : keyLength+noncePrefixLength])
- seed, err := drbg.SeedFromBytes(key[keyLength+noncePrefixLength:])
- if err != nil {
- panic(fmt.Sprintf("BUG: Failed to initialize DRBG: %s", err))
- }
- decoder.drbg = drbg.NewHashDrbg(seed)
-
- return decoder
-}
-
-// Decode decodes a stream of data and returns the length if any. ErrAgain is
-// a temporary failure, all other errors MUST be treated as fatal and the
-// session aborted.
-func (decoder *Decoder) Decode(data []byte, frames *bytes.Buffer) (int, error) {
- // A length of 0 indicates that we do not know how big the next frame is
- // going to be.
- if decoder.nextLength == 0 {
- // Attempt to pull out the next frame length.
- if lengthLength > frames.Len() {
- return 0, ErrAgain
- }
-
- // Remove the length field from the buffer.
- var obfsLen [lengthLength]byte
- _, err := io.ReadFull(frames, obfsLen[:])
- if err != nil {
- return 0, err
- }
-
- // Derive the nonce the peer used.
- err = decoder.nonce.bytes(&decoder.nextNonce)
- if err != nil {
- return 0, err
- }
-
- // Deobfuscate the length field.
- length := binary.BigEndian.Uint16(obfsLen[:])
- lengthMask := decoder.drbg.NextBlock()
- length ^= binary.BigEndian.Uint16(lengthMask)
- if maxFrameLength < length || minFrameLength > length {
- // Per "Plaintext Recovery Attacks Against SSH" by
- // Martin R. Albrecht, Kenneth G. Paterson and Gaven J. Watson,
- // there are a class of attacks againt protocols that use similar
- // sorts of framing schemes.
- //
- // While obfs4 should not allow plaintext recovery (CBC mode is
- // not used), attempt to mitigate out of bound frame length errors
- // by pretending that the length was a random valid range as per
- // the countermeasure suggested by Denis Bider in section 6 of the
- // paper.
-
- decoder.nextLengthInvalid = true
- length = uint16(csrand.IntRange(minFrameLength, maxFrameLength))
- }
- decoder.nextLength = length
- }
-
- if int(decoder.nextLength) > frames.Len() {
- return 0, ErrAgain
- }
-
- // Unseal the frame.
- var box [maxFrameLength]byte
- n, err := io.ReadFull(frames, box[:decoder.nextLength])
- if err != nil {
- return 0, err
- }
- out, ok := secretbox.Open(data[:0], box[:n], &decoder.nextNonce, &decoder.key)
- if !ok || decoder.nextLengthInvalid {
- // When a random length is used (on length error) the tag should always
- // mismatch, but be paranoid.
- return 0, ErrTagMismatch
- }
-
- // Clean up and prepare for the next frame.
- decoder.nextLength = 0
- decoder.nonce.counter++
-
- return len(out), nil
-}
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/framing/framing_test.go b/framing/framing_test.go
deleted file mode 100644
index 7df0e28..0000000
--- a/framing/framing_test.go
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * * Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-package framing
-
-import (
- "bytes"
- "crypto/rand"
- "testing"
-)
-
-func generateRandomKey() []byte {
- key := make([]byte, KeyLength)
-
- _, err := rand.Read(key)
- if err != nil {
- panic(err)
- }
-
- return key
-}
-
-func newEncoder(t *testing.T) *Encoder {
- // Generate a key to use.
- key := generateRandomKey()
-
- encoder := NewEncoder(key)
- if encoder == nil {
- t.Fatalf("NewEncoder returned nil")
- }
-
- return encoder
-}
-
-// TestNewEncoder tests the Encoder ctor.
-func TestNewEncoder(t *testing.T) {
- encoder := newEncoder(t)
- _ = encoder
-}
-
-// TestEncoder_Encode tests Encoder.Encode.
-func TestEncoder_Encode(t *testing.T) {
- encoder := newEncoder(t)
-
- buf := make([]byte, MaximumFramePayloadLength)
- _, _ = rand.Read(buf) // YOLO
- for i := 0; i <= MaximumFramePayloadLength; i++ {
- var frame [MaximumSegmentLength]byte
- n, err := encoder.Encode(frame[:], buf[0:i])
- if err != nil {
- t.Fatalf("Encoder.encode([%d]byte), failed: %s", i, err)
- }
- if n != i+FrameOverhead {
- t.Fatalf("Unexpected encoded framesize: %d, expecting %d", n, i+
- FrameOverhead)
- }
- }
-}
-
-// TestEncoder_Encode_Oversize tests oversized frame rejection.
-func TestEncoder_Encode_Oversize(t *testing.T) {
- encoder := newEncoder(t)
-
- var frame [MaximumSegmentLength]byte
- var buf [MaximumFramePayloadLength + 1]byte
- _, _ = rand.Read(buf[:]) // YOLO
- _, err := encoder.Encode(frame[:], buf[:])
- if _, ok := err.(InvalidPayloadLengthError); !ok {
- t.Error("Encoder.encode() returned unexpected error:", err)
- }
-}
-
-// TestNewDecoder tests the Decoder ctor.
-func TestNewDecoder(t *testing.T) {
- key := generateRandomKey()
- decoder := NewDecoder(key)
- if decoder == nil {
- t.Fatalf("NewDecoder returned nil")
- }
-}
-
-// TestDecoder_Decode tests Decoder.Decode.
-func TestDecoder_Decode(t *testing.T) {
- key := generateRandomKey()
-
- encoder := NewEncoder(key)
- decoder := NewDecoder(key)
-
- var buf [MaximumFramePayloadLength]byte
- _, _ = rand.Read(buf[:]) // YOLO
- for i := 0; i <= MaximumFramePayloadLength; i++ {
- var frame [MaximumSegmentLength]byte
- encLen, err := encoder.Encode(frame[:], buf[0:i])
- if err != nil {
- t.Fatalf("Encoder.encode([%d]byte), failed: %s", i, err)
- }
- if encLen != i+FrameOverhead {
- t.Fatalf("Unexpected encoded framesize: %d, expecting %d", encLen,
- i+FrameOverhead)
- }
-
- var decoded [MaximumFramePayloadLength]byte
-
- decLen, err := decoder.Decode(decoded[:], bytes.NewBuffer(frame[:encLen]))
- if err != nil {
- t.Fatalf("Decoder.decode([%d]byte), failed: %s", i, err)
- }
- if decLen != i {
- t.Fatalf("Unexpected decoded framesize: %d, expecting %d",
- decLen, i)
- }
-
- if 0 != bytes.Compare(decoded[:decLen], buf[:i]) {
- t.Fatalf("Frame %d does not match encoder input", i)
- }
- }
-}
-
-// BencharkEncoder_Encode benchmarks Encoder.Encode processing 1 MiB
-// of payload.
-func BenchmarkEncoder_Encode(b *testing.B) {
- var chopBuf [MaximumFramePayloadLength]byte
- var frame [MaximumSegmentLength]byte
- payload := make([]byte, 1024*1024)
- encoder := NewEncoder(generateRandomKey())
- b.ResetTimer()
-
- for i := 0; i < b.N; i++ {
- transfered := 0
- buffer := bytes.NewBuffer(payload)
- for 0 < buffer.Len() {
- n, err := buffer.Read(chopBuf[:])
- if err != nil {
- b.Fatal("buffer.Read() failed:", err)
- }
-
- n, err = encoder.Encode(frame[:], chopBuf[:n])
- transfered += n - FrameOverhead
- }
- if transfered != len(payload) {
- b.Fatalf("Transfered length mismatch: %d != %d", transfered,
- len(payload))
- }
- }
-}
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/handshake_ntor.go b/handshake_ntor.go
deleted file mode 100644
index 8192494..0000000
--- a/handshake_ntor.go
+++ /dev/null
@@ -1,427 +0,0 @@
-/*
- * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * * Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-package obfs4
-
-import (
- "bytes"
- "crypto/hmac"
- "crypto/sha256"
- "encoding/hex"
- "errors"
- "fmt"
- "hash"
- "strconv"
- "time"
-
- "git.torproject.org/pluggable-transports/obfs4.git/csrand"
- "git.torproject.org/pluggable-transports/obfs4.git/framing"
- "git.torproject.org/pluggable-transports/obfs4.git/ntor"
-)
-
-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, 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().Unix(), macRx) {
- // The client either happened to generate exactly the same
- // session key and padding, or someone is replaying a previous
- // handshake. In either case, fuck them.
- return nil, ErrReplayedHandshake
- }
-
- macFound = true
- hs.epochHour = epochHour
-
- // We could break out here, but in the name of reducing timing
- // variation, evaluate all 3 MACs.
- }
- }
- if !macFound {
- // This probably should be an InvalidMacError, but conveying the 3 MACS
- // that would be accepted is annoying so just return a generic fatal
- // failure.
- return nil, ErrInvalidHandshake
- }
-
- // Client should never sent trailing garbage.
- if len(resp) != pos+markLength+macLength {
- return nil, ErrInvalidHandshake
- }
-
- clientPublic := hs.clientRepresentative.ToPublic()
- ok, seed, auth := ntor.ServerHandshake(clientPublic, hs.keypair,
- hs.serverIdentity, hs.nodeID)
- if !ok {
- return nil, ErrNtorFailed
- }
- hs.serverAuth = auth
-
- return seed.Bytes()[:], nil
-}
-
-func (hs *serverHandshake) generateHandshake() ([]byte, error) {
- var buf bytes.Buffer
-
- hs.mac.Reset()
- hs.mac.Write(hs.keypair.Representative().Bytes()[:])
- mark := hs.mac.Sum(nil)[:markLength]
-
- // The server handshake is Y | AUTH | P_S | M_S | MAC(Y | AUTH | P_S | M_S | E) where:
- // * Y is the server's ephemeral Curve25519 public key representative.
- // * AUTH is the ntor handshake AUTH value.
- // * P_S is [serverMinPadLength,serverMaxPadLength] bytes of random padding.
- // * M_S is HMAC-SHA256-128(serverIdentity | NodeID, Y)
- // * MAC is HMAC-SHA256-128(serverIdentity | NodeID, Y .... E)
- // * E is the string representation of the number of hours since the UNIX
- // epoch.
-
- // Generate the padding
- pad, err := makePad(hs.padLen)
- if err != nil {
- return nil, err
- }
-
- // Write Y, AUTH, P_S, M_S.
- buf.Write(hs.keypair.Representative().Bytes()[:])
- buf.Write(hs.serverAuth.Bytes()[:])
- buf.Write(pad)
- buf.Write(mark)
-
- // Calculate and write the MAC.
- hs.mac.Reset()
- hs.mac.Write(buf.Bytes())
- hs.epochHour = []byte(strconv.FormatInt(getEpochHour(), 10))
- hs.mac.Write(hs.epochHour)
- buf.Write(hs.mac.Sum(nil)[:macLength])
-
- return buf.Bytes(), nil
-}
-
-// getEpochHour returns the number of hours since the UNIX epoch.
-func getEpochHour() int64 {
- return time.Now().Unix() / 3600
-}
-
-func findMarkMac(mark, buf []byte, startPos, maxPos int, fromTail bool) (pos int) {
- if len(mark) != markLength {
- panic(fmt.Sprintf("BUG: Invalid mark length: %d", len(mark)))
- }
-
- endPos := len(buf)
- if startPos > len(buf) {
- return -1
- }
- if endPos > maxPos {
- endPos = maxPos
- }
- if endPos-startPos < markLength+macLength {
- return -1
- }
-
- if fromTail {
- // The server can optimize the search process by only examining the
- // tail of the buffer. The client can't send valid data past M_C |
- // MAC_C as it does not have the server's public key yet.
- pos = endPos - (markLength + macLength)
- if !hmac.Equal(buf[pos:pos+markLength], mark) {
- return -1
- }
-
- return
- }
-
- // The client has to actually do a substring search since the server can
- // and will send payload trailing the response.
- //
- // XXX: bytes.Index() uses a naive search, which kind of sucks.
- pos = bytes.Index(buf[startPos:endPos], mark)
- if pos == -1 {
- return -1
- }
-
- // Ensure that there is enough trailing data for the MAC.
- if startPos+pos+markLength+macLength > endPos {
- return -1
- }
-
- // Return the index relative to the start of the slice.
- pos += startPos
- return
-}
-
-func makePad(padLen int) ([]byte, error) {
- pad := make([]byte, padLen)
- err := csrand.Bytes(pad)
- if err != nil {
- return nil, err
- }
-
- return pad, err
-}
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/handshake_ntor_test.go b/handshake_ntor_test.go
deleted file mode 100644
index 94d2a7f..0000000
--- a/handshake_ntor_test.go
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * * Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-package obfs4
-
-import (
- "bytes"
- "testing"
-
- "git.torproject.org/pluggable-transports/obfs4.git/ntor"
-)
-
-func TestHandshakeNtor(t *testing.T) {
- // Generate the server node id and id keypair.
- nodeID, _ := ntor.NewNodeID([]byte("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13"))
- idKeypair, _ := ntor.NewKeypair(false)
- serverFilter, _ := newReplayFilter()
-
- // Test client handshake padding.
- for l := clientMinPadLength; l <= clientMaxPadLength; l++ {
- // Generate the client state and override the pad length.
- clientKeypair, err := ntor.NewKeypair(true)
- if err != nil {
- t.Fatalf("[%d:0] ntor.NewKeypair failed: %s", l, err)
- }
- clientHs := newClientHandshake(nodeID, idKeypair.Public(), clientKeypair)
- clientHs.padLen = l
-
- // Generate what the client will send to the server.
- clientBlob, err := clientHs.generateHandshake()
- if err != nil {
- t.Fatalf("[%d:0] clientHandshake.generateHandshake() failed: %s", l, err)
- }
- if len(clientBlob) > maxHandshakeLength {
- t.Fatalf("[%d:0] Generated client body is oversized: %d", l, len(clientBlob))
- }
- if len(clientBlob) < clientMinHandshakeLength {
- t.Fatalf("[%d:0] Generated client body is undersized: %d", l, len(clientBlob))
- }
- if len(clientBlob) != clientMinHandshakeLength+l {
- t.Fatalf("[%d:0] Generated client body incorrect size: %d", l, len(clientBlob))
- }
-
- // Generate the server state and override the pad length.
- serverKeypair, err := ntor.NewKeypair(true)
- if err != nil {
- t.Fatalf("[%d:0] ntor.NewKeypair failed: %s", l, err)
- }
- serverHs := newServerHandshake(nodeID, idKeypair, serverKeypair)
- serverHs.padLen = serverMinPadLength
-
- // Parse the client handshake message.
- serverSeed, err := serverHs.parseClientHandshake(serverFilter, clientBlob)
- if err != nil {
- t.Fatalf("[%d:0] serverHandshake.parseClientHandshake() failed: %s", l, err)
- }
-
- // Genrate what the server will send to the client.
- serverBlob, err := serverHs.generateHandshake()
- if err != nil {
- t.Fatal("[%d:0]: serverHandshake.generateHandshake() failed: %s", l, err)
- }
-
- // Parse the server handshake message.
- clientHs.serverRepresentative = nil
- n, clientSeed, err := clientHs.parseServerHandshake(serverBlob)
- if err != nil {
- t.Fatalf("[%d:0] clientHandshake.parseServerHandshake() failed: %s", l, err)
- }
- if n != len(serverBlob) {
- t.Fatalf("[%d:0] clientHandshake.parseServerHandshake() has bytes remaining: %d", l, n)
- }
-
- // Ensure the derived shared secret is the same.
- if 0 != bytes.Compare(clientSeed, serverSeed) {
- t.Fatalf("[%d:0] client/server seed mismatch", l)
- }
- }
-
- // Test server handshake padding.
- for l := serverMinPadLength; l <= serverMaxPadLength+inlineSeedFrameLength; l++ {
- // Generate the client state and override the pad length.
- clientKeypair, err := ntor.NewKeypair(true)
- if err != nil {
- t.Fatalf("[%d:0] ntor.NewKeypair failed: %s", l, err)
- }
- clientHs := newClientHandshake(nodeID, idKeypair.Public(), clientKeypair)
- clientHs.padLen = clientMinPadLength
-
- // Generate what the client will send to the server.
- clientBlob, err := clientHs.generateHandshake()
- if err != nil {
- t.Fatalf("[%d:1] clientHandshake.generateHandshake() failed: %s", l, err)
- }
- if len(clientBlob) > maxHandshakeLength {
- t.Fatalf("[%d:1] Generated client body is oversized: %d", l, len(clientBlob))
- }
-
- // Generate the server state and override the pad length.
- serverKeypair, err := ntor.NewKeypair(true)
- if err != nil {
- t.Fatalf("[%d:0] ntor.NewKeypair failed: %s", l, err)
- }
- serverHs := newServerHandshake(nodeID, idKeypair, serverKeypair)
- serverHs.padLen = l
-
- // Parse the client handshake message.
- serverSeed, err := serverHs.parseClientHandshake(serverFilter, clientBlob)
- if err != nil {
- t.Fatalf("[%d:1] serverHandshake.parseClientHandshake() failed: %s", l, err)
- }
-
- // Genrate what the server will send to the client.
- serverBlob, err := serverHs.generateHandshake()
- if err != nil {
- t.Fatal("[%d:1]: serverHandshake.generateHandshake() failed: %s", l, err)
- }
-
- // Parse the server handshake message.
- n, clientSeed, err := clientHs.parseServerHandshake(serverBlob)
- if err != nil {
- t.Fatalf("[%d:1] clientHandshake.parseServerHandshake() failed: %s", l, err)
- }
- if n != len(serverBlob) {
- t.Fatalf("[%d:1] clientHandshake.parseServerHandshake() has bytes remaining: %d", l, n)
- }
-
- // Ensure the derived shared secret is the same.
- if 0 != bytes.Compare(clientSeed, serverSeed) {
- t.Fatalf("[%d:1] client/server seed mismatch", l)
- }
- }
-
- // Test oversized client padding.
- clientKeypair, err := ntor.NewKeypair(true)
- if err != nil {
- t.Fatalf("ntor.NewKeypair failed: %s", err)
- }
- clientHs := newClientHandshake(nodeID, idKeypair.Public(), clientKeypair)
- if err != nil {
- t.Fatalf("newClientHandshake failed: %s", err)
- }
-
- clientHs.padLen = clientMaxPadLength + 1
- clientBlob, err := clientHs.generateHandshake()
- if err != nil {
- t.Fatalf("clientHandshake.generateHandshake() (forced oversize) failed: %s", err)
- }
- serverKeypair, err := ntor.NewKeypair(true)
- if err != nil {
- t.Fatalf("ntor.NewKeypair failed: %s", err)
- }
- serverHs := newServerHandshake(nodeID, idKeypair, serverKeypair)
- _, err = serverHs.parseClientHandshake(serverFilter, clientBlob)
- if err == nil {
- t.Fatalf("serverHandshake.parseClientHandshake() succeded (oversized)")
- }
-
- // Test undersized client padding.
- clientHs.padLen = clientMinPadLength - 1
- clientBlob, err = clientHs.generateHandshake()
- if err != nil {
- t.Fatalf("clientHandshake.generateHandshake() (forced undersize) failed: %s", err)
- }
- serverHs = newServerHandshake(nodeID, idKeypair, serverKeypair)
- _, err = serverHs.parseClientHandshake(serverFilter, clientBlob)
- if err == nil {
- t.Fatalf("serverHandshake.parseClientHandshake() succeded (undersized)")
- }
-
- // Test oversized server padding.
- //
- // NB: serverMaxPadLength isn't the real maxPadLength that triggers client
- // rejection, because the implementation is written with the asusmption
- // that/ the PRNG_SEED is also inlined with the response. Thus the client
- // actually accepts longer padding. The server handshake test and this
- // test adjust around that.
- clientHs.padLen = clientMinPadLength
- clientBlob, err = clientHs.generateHandshake()
- if err != nil {
- t.Fatalf("clientHandshake.generateHandshake() failed: %s", err)
- }
- serverHs = newServerHandshake(nodeID, idKeypair, serverKeypair)
- serverHs.padLen = serverMaxPadLength + inlineSeedFrameLength + 1
- _, err = serverHs.parseClientHandshake(serverFilter, clientBlob)
- if err != nil {
- t.Fatalf("serverHandshake.parseClientHandshake() failed: %s", err)
- }
- serverBlob, err := serverHs.generateHandshake()
- if err != nil {
- t.Fatal("serverHandshake.generateHandshake() (forced oversize) failed: %s", err)
- }
- _, _, err = clientHs.parseServerHandshake(serverBlob)
- if err == nil {
- t.Fatalf("clientHandshake.parseServerHandshake() succeded (oversized)")
- }
-}
diff --git a/ntor/ntor.go b/ntor/ntor.go
deleted file mode 100644
index b178454..0000000
--- a/ntor/ntor.go
+++ /dev/null
@@ -1,437 +0,0 @@
-/*
- * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * * Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-//
-// Package ntor implements the Tor Project's ntor handshake as defined in
-// proposal 216 "Improved circuit-creation key exchange". It also supports
-// using Elligator to transform the Curve25519 public keys sent over the wire
-// to a form that is indistinguishable from random strings.
-//
-// Before using this package, it is strongly recommended that the specification
-// is read and understood.
-//
-package ntor
-
-import (
- "bytes"
- "crypto/hmac"
- "crypto/sha256"
- "crypto/subtle"
- "encoding/base64"
- "fmt"
- "io"
-
- "code.google.com/p/go.crypto/curve25519"
- "code.google.com/p/go.crypto/hkdf"
-
- "github.com/agl/ed25519/extra25519"
-
- "git.torproject.org/pluggable-transports/obfs4.git/csrand"
-)
-
-const (
- // PublicKeyLength is the length of a Curve25519 public key.
- PublicKeyLength = 32
-
- // RepresentativeLength is the length of an Elligator representative.
- RepresentativeLength = 32
-
- // PrivateKeyLength is the length of a Curve25519 private key.
- PrivateKeyLength = 32
-
- // SharedSecretLength is the length of a Curve25519 shared secret.
- SharedSecretLength = 32
-
- // NodeIDLength is the length of a ntor node identifier.
- NodeIDLength = 20
-
- // KeySeedLength is the length of the derived KEY_SEED.
- KeySeedLength = sha256.Size
-
- // AuthLength is the lenght of the derived AUTH.
- AuthLength = sha256.Size
-)
-
-var protoID = []byte("ntor-curve25519-sha256-1")
-var tMac = append(protoID, []byte(":mac")...)
-var tKey = append(protoID, []byte(":key_extract")...)
-var tVerify = append(protoID, []byte(":key_verify")...)
-var mExpand = append(protoID, []byte(":key_expand")...)
-
-// PublicKeyLengthError is the error returned when the public key being
-// imported is an invalid length.
-type PublicKeyLengthError int
-
-func (e PublicKeyLengthError) Error() string {
- return fmt.Sprintf("ntor: Invalid Curve25519 public key length: %d",
- int(e))
-}
-
-// PrivateKeyLengthError is the error returned when the private key being
-// imported is an invalid length.
-type PrivateKeyLengthError int
-
-func (e PrivateKeyLengthError) Error() string {
- return fmt.Sprintf("ntor: Invalid Curve25519 private key length: %d",
- int(e))
-}
-
-// NodeIDLengthError is the error returned when the node ID being imported is
-// an invalid length.
-type NodeIDLengthError int
-
-func (e NodeIDLengthError) Error() string {
- return fmt.Sprintf("ntor: Invalid NodeID length: %d", int(e))
-}
-
-// KeySeed is the key material that results from a handshake (KEY_SEED).
-type KeySeed [KeySeedLength]byte
-
-// Bytes returns a pointer to the raw key material.
-func (key_seed *KeySeed) Bytes() *[KeySeedLength]byte {
- return (*[KeySeedLength]byte)(key_seed)
-}
-
-// Auth is the verifier that results from a handshake (AUTH).
-type Auth [AuthLength]byte
-
-// Bytes returns a pointer to the raw auth.
-func (auth *Auth) Bytes() *[AuthLength]byte {
- return (*[AuthLength]byte)(auth)
-}
-
-// NodeID is a ntor node identifier.
-type NodeID [NodeIDLength]byte
-
-// NewNodeID creates a NodeID from the raw bytes.
-func NewNodeID(raw []byte) (*NodeID, error) {
- if len(raw) != NodeIDLength {
- return nil, NodeIDLengthError(len(raw))
- }
-
- nodeID := new(NodeID)
- copy(nodeID[:], raw)
-
- return nodeID, nil
-}
-
-// NodeIDFromBase64 creates a new NodeID from the Base64 encoded representation.
-func NodeIDFromBase64(encoded string) (*NodeID, error) {
- raw, err := base64.StdEncoding.DecodeString(encoded)
- if err != nil {
- return nil, err
- }
-
- return NewNodeID(raw)
-}
-// Bytes returns a pointer to the raw NodeID.
-func (id *NodeID) Bytes() *[NodeIDLength]byte {
- return (*[NodeIDLength]byte)(id)
-}
-
-// Base64 returns the Base64 representation of the NodeID.
-func (id *NodeID) Base64() string {
- return base64.StdEncoding.EncodeToString(id[:])
-}
-
-// PublicKey is a Curve25519 public key in little-endian byte order.
-type PublicKey [PublicKeyLength]byte
-
-// Bytes returns a pointer to the raw Curve25519 public key.
-func (public *PublicKey) Bytes() *[PublicKeyLength]byte {
- return (*[PublicKeyLength]byte)(public)
-}
-
-// Base64 returns the Base64 representation of the Curve25519 public key.
-func (public *PublicKey) Base64() string {
- return base64.StdEncoding.EncodeToString(public.Bytes()[:])
-}
-
-// NewPublicKey creates a PublicKey from the raw bytes.
-func NewPublicKey(raw []byte) (*PublicKey, error) {
- if len(raw) != PublicKeyLength {
- return nil, PublicKeyLengthError(len(raw))
- }
-
- pubKey := new(PublicKey)
- copy(pubKey[:], raw)
-
- return pubKey, nil
-}
-
-// PublicKeyFromBase64 returns a PublicKey from a Base64 representation.
-func PublicKeyFromBase64(encoded string) (*PublicKey, error) {
- raw, err := base64.StdEncoding.DecodeString(encoded)
- if err != nil {
- return nil, err
- }
-
- return NewPublicKey(raw)
-}
-
-// Representative is an Elligator representative of a Curve25519 public key
-// in little-endian byte order.
-type Representative [RepresentativeLength]byte
-
-// Bytes returns a pointer to the raw Elligator representative.
-func (repr *Representative) Bytes() *[RepresentativeLength]byte {
- return (*[RepresentativeLength]byte)(repr)
-}
-
-// ToPublic converts a Elligator representative to a Curve25519 public key.
-func (repr *Representative) ToPublic() *PublicKey {
- pub := new(PublicKey)
-
- extra25519.RepresentativeToPublicKey(pub.Bytes(), repr.Bytes())
- return pub
-}
-
-// PrivateKey is a Curve25519 private key in little-endian byte order.
-type PrivateKey [PrivateKeyLength]byte
-
-// Bytes returns a pointer to the raw Curve25519 private key.
-func (private *PrivateKey) Bytes() *[PrivateKeyLength]byte {
- return (*[PrivateKeyLength]byte)(private)
-}
-
-// Base64 returns the Base64 representation of the Curve25519 private key.
-func (private *PrivateKey) Base64() string {
- return base64.StdEncoding.EncodeToString(private.Bytes()[:])
-}
-
-// Keypair is a Curve25519 keypair with an optional Elligator representative.
-// As only certain Curve25519 keys can be obfuscated with Elligator, the
-// representative must be generated along with the keypair.
-type Keypair struct {
- public *PublicKey
- private *PrivateKey
- representative *Representative
-}
-
-// Public returns the Curve25519 public key belonging to the Keypair.
-func (keypair *Keypair) Public() *PublicKey {
- return keypair.public
-}
-
-// Private returns the Curve25519 private key belonging to the Keypair.
-func (keypair *Keypair) Private() *PrivateKey {
- return keypair.private
-}
-
-// Representative returns the Elligator representative of the public key
-// belonging to the Keypair.
-func (keypair *Keypair) Representative() *Representative {
- return keypair.representative
-}
-
-// HasElligator returns true if the Keypair has an Elligator representative.
-func (keypair *Keypair) HasElligator() bool {
- return nil != keypair.representative
-}
-
-// NewKeypair generates a new Curve25519 keypair, and optionally also generates
-// an Elligator representative of the public key.
-func NewKeypair(elligator bool) (*Keypair, error) {
- keypair := new(Keypair)
- keypair.private = new(PrivateKey)
- keypair.public = new(PublicKey)
- if elligator {
- keypair.representative = new(Representative)
- }
-
- for {
- // Generate a Curve25519 private key. Like everyone who does this,
- // run the CSPRNG output through SHA256 for extra tinfoil hattery.
- priv := keypair.private.Bytes()[:]
- err := csrand.Bytes(priv)
- if err != nil {
- return nil, err
- }
- digest := sha256.Sum256(priv)
- digest[0] &= 248
- digest[31] &= 127
- digest[31] |= 64
- copy(priv, digest[:])
-
- if elligator {
- // Apply the Elligator transform. This fails ~50% of the time.
- if !extra25519.ScalarBaseMult(keypair.public.Bytes(),
- keypair.representative.Bytes(),
- keypair.private.Bytes()) {
- continue
- }
- } else {
- // Generate the corresponding Curve25519 public key.
- curve25519.ScalarBaseMult(keypair.public.Bytes(),
- keypair.private.Bytes())
- }
-
- return keypair, nil
- }
-}
-
-// KeypairFromBase64 returns a Keypair from a Base64 representation of the
-// private key.
-func KeypairFromBase64(encoded string) (*Keypair, error) {
- raw, err := base64.StdEncoding.DecodeString(encoded)
- if err != nil {
- return nil, err
- }
-
- if len(raw) != PrivateKeyLength {
- return nil, PrivateKeyLengthError(len(raw))
- }
-
- keypair := new(Keypair)
- keypair.private = new(PrivateKey)
- keypair.public = new(PublicKey)
-
- copy(keypair.private[:], raw)
- curve25519.ScalarBaseMult(keypair.public.Bytes(),
- keypair.private.Bytes())
-
- return keypair, nil
-}
-
-// ServerHandshake does the server side of a ntor handshake and returns status,
-// KEY_SEED, and AUTH. If status is not true, the handshake MUST be aborted.
-func ServerHandshake(clientPublic *PublicKey, serverKeypair *Keypair, idKeypair *Keypair, id *NodeID) (ok bool, keySeed *KeySeed, auth *Auth) {
- var notOk int
- var secretInput bytes.Buffer
-
- // Server side uses EXP(X,y) | EXP(X,b)
- var exp [SharedSecretLength]byte
- curve25519.ScalarMult(&exp, serverKeypair.private.Bytes(),
- clientPublic.Bytes())
- notOk |= constantTimeIsZero(exp[:])
- secretInput.Write(exp[:])
-
- curve25519.ScalarMult(&exp, idKeypair.private.Bytes(),
- clientPublic.Bytes())
- notOk |= constantTimeIsZero(exp[:])
- secretInput.Write(exp[:])
-
- keySeed, auth = ntorCommon(secretInput, id, idKeypair.public,
- clientPublic, serverKeypair.public)
- return notOk == 0, keySeed, auth
-}
-
-// ClientHandshake does the client side of a ntor handshake and returnes
-// status, KEY_SEED, and AUTH. If status is not true or AUTH does not match
-// the value recieved from the server, the handshake MUST be aborted.
-func ClientHandshake(clientKeypair *Keypair, serverPublic *PublicKey, idPublic *PublicKey, id *NodeID) (ok bool, keySeed *KeySeed, auth *Auth) {
- var notOk int
- var secretInput bytes.Buffer
-
- // Client side uses EXP(Y,x) | EXP(B,x)
- var exp [SharedSecretLength]byte
- curve25519.ScalarMult(&exp, clientKeypair.private.Bytes(),
- serverPublic.Bytes())
- notOk |= constantTimeIsZero(exp[:])
- secretInput.Write(exp[:])
-
- curve25519.ScalarMult(&exp, clientKeypair.private.Bytes(),
- idPublic.Bytes())
- notOk |= constantTimeIsZero(exp[:])
- secretInput.Write(exp[:])
-
- keySeed, auth = ntorCommon(secretInput, id, idPublic,
- clientKeypair.public, serverPublic)
- return notOk == 0, keySeed, auth
-}
-
-// CompareAuth does a constant time compare of a Auth and a byte slice
-// (presumably received over a network).
-func CompareAuth(auth1 *Auth, auth2 []byte) bool {
- auth1Bytes := auth1.Bytes()
- return hmac.Equal(auth1Bytes[:], auth2)
-}
-
-func ntorCommon(secretInput bytes.Buffer, id *NodeID, b *PublicKey, x *PublicKey, y *PublicKey) (*KeySeed, *Auth) {
- keySeed := new(KeySeed)
- auth := new(Auth)
-
- // secret_input/auth_input use this common bit, build it once.
- suffix := bytes.NewBuffer(b.Bytes()[:])
- suffix.Write(b.Bytes()[:])
- suffix.Write(x.Bytes()[:])
- suffix.Write(y.Bytes()[:])
- suffix.Write(protoID)
- suffix.Write(id[:])
-
- // At this point secret_input has the 2 exponents, concatenated, append the
- // client/server common suffix.
- secretInput.Write(suffix.Bytes())
-
- // KEY_SEED = H(secret_input, t_key)
- h := hmac.New(sha256.New, tKey)
- h.Write(secretInput.Bytes())
- tmp := h.Sum(nil)
- copy(keySeed[:], tmp)
-
- // verify = H(secret_input, t_verify)
- h = hmac.New(sha256.New, tVerify)
- h.Write(secretInput.Bytes())
- verify := h.Sum(nil)
-
- // auth_input = verify | ID | B | Y | X | PROTOID | "Server"
- authInput := bytes.NewBuffer(verify)
- authInput.Write(suffix.Bytes())
- authInput.Write([]byte("Server"))
- h = hmac.New(sha256.New, tMac)
- h.Write(authInput.Bytes())
- tmp = h.Sum(nil)
- copy(auth[:], tmp)
-
- return keySeed, auth
-}
-
-func constantTimeIsZero(x []byte) int {
- var ret byte
- for _, v := range x {
- ret |= v
- }
-
- return subtle.ConstantTimeByteEq(ret, 0)
-}
-
-// Kdf extracts and expands KEY_SEED via HKDF-SHA256 and returns `okm_len` bytes
-// of key material.
-func Kdf(keySeed []byte, okmLen int) []byte {
- kdf := hkdf.New(sha256.New, keySeed, tKey, mExpand)
- okm := make([]byte, okmLen)
- n, err := io.ReadFull(kdf, okm)
- if err != nil {
- panic(fmt.Sprintf("BUG: Failed HKDF: %s", err.Error()))
- } else if n != len(okm) {
- panic(fmt.Sprintf("BUG: Got truncated HKDF output: %d", n))
- }
-
- return okm
-}
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/ntor/ntor_test.go b/ntor/ntor_test.go
deleted file mode 100644
index 9d7c687..0000000
--- a/ntor/ntor_test.go
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * * Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-package ntor
-
-import (
- "bytes"
- "testing"
-)
-
-// TestNewKeypair tests Curve25519/Elligator keypair generation.
-func TestNewKeypair(t *testing.T) {
- // Test standard Curve25519 first.
- keypair, err := NewKeypair(false)
- if err != nil {
- t.Fatal("NewKeypair(false) failed:", err)
- }
- if keypair == nil {
- t.Fatal("NewKeypair(false) returned nil")
- }
- if keypair.HasElligator() {
- t.Fatal("NewKeypair(false) has a Elligator representative")
- }
-
- // Test Elligator generation.
- keypair, err = NewKeypair(true)
- if err != nil {
- t.Fatal("NewKeypair(true) failed:", err)
- }
- if keypair == nil {
- t.Fatal("NewKeypair(true) returned nil")
- }
- if !keypair.HasElligator() {
- t.Fatal("NewKeypair(true) mising an Elligator representative")
- }
-}
-
-// Test Client/Server handshake.
-func TestHandshake(t *testing.T) {
- clientKeypair, err := NewKeypair(true)
- if err != nil {
- t.Fatal("Failed to generate client keypair:", err)
- }
- if clientKeypair == nil {
- t.Fatal("Client keypair is nil")
- }
-
- serverKeypair, err := NewKeypair(true)
- if err != nil {
- t.Fatal("Failed to generate server keypair:", err)
- }
- if serverKeypair == nil {
- t.Fatal("Server keypair is nil")
- }
-
- idKeypair, err := NewKeypair(false)
- if err != nil {
- t.Fatal("Failed to generate identity keypair:", err)
- }
- if idKeypair == nil {
- t.Fatal("Identity keypair is nil")
- }
-
- nodeID, err := NewNodeID([]byte("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13"))
- if err != nil {
- t.Fatal("Failed to load NodeId:", err)
- }
-
- // ServerHandshake
- clientPublic := clientKeypair.Representative().ToPublic()
- ok, serverSeed, serverAuth := ServerHandshake(clientPublic,
- serverKeypair, idKeypair, nodeID)
- if !ok {
- t.Fatal("ServerHandshake failed")
- }
- if serverSeed == nil {
- t.Fatal("ServerHandshake returned nil KEY_SEED")
- }
- if serverAuth == nil {
- t.Fatal("ServerHandshake returned nil AUTH")
- }
-
- // ClientHandshake
- ok, clientSeed, clientAuth := ClientHandshake(clientKeypair,
- serverKeypair.Public(), idKeypair.Public(), nodeID)
- if !ok {
- t.Fatal("ClientHandshake failed")
- }
- if clientSeed == nil {
- t.Fatal("ClientHandshake returned nil KEY_SEED")
- }
- if clientAuth == nil {
- t.Fatal("ClientHandshake returned nil AUTH")
- }
-
- // WARNING: Use a constant time comparison in actual code.
- if 0 != bytes.Compare(clientSeed.Bytes()[:], serverSeed.Bytes()[:]) {
- t.Fatal("KEY_SEED mismatched between client/server")
- }
- if 0 != bytes.Compare(clientAuth.Bytes()[:], serverAuth.Bytes()[:]) {
- t.Fatal("AUTH mismatched between client/server")
- }
-}
-
-// Benchmark Client/Server handshake. The actual time taken that will be
-// observed on either the Client or Server is half the reported time per
-// operation since the benchmark does both sides.
-func BenchmarkHandshake(b *testing.B) {
- // Generate the "long lasting" identity key and NodeId.
- idKeypair, err := NewKeypair(false)
- if err != nil || idKeypair == nil {
- b.Fatal("Failed to generate identity keypair")
- }
- nodeID, err := NewNodeID([]byte("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13"))
- if err != nil {
- b.Fatal("Failed to load NodeId:", err)
- }
- b.ResetTimer()
-
- // Start the actual benchmark.
- for i := 0; i < b.N; i++ {
- // Generate the keypairs.
- serverKeypair, err := NewKeypair(true)
- if err != nil || serverKeypair == nil {
- b.Fatal("Failed to generate server keypair")
- }
-
- clientKeypair, err := NewKeypair(true)
- if err != nil || clientKeypair == nil {
- b.Fatal("Failed to generate client keypair")
- }
-
- // Server handshake.
- clientPublic := clientKeypair.Representative().ToPublic()
- ok, serverSeed, serverAuth := ServerHandshake(clientPublic,
- serverKeypair, idKeypair, nodeID)
- if !ok || serverSeed == nil || serverAuth == nil {
- b.Fatal("ServerHandshake failed")
- }
-
- // Client handshake.
- serverPublic := serverKeypair.Representative().ToPublic()
- ok, clientSeed, clientAuth := ClientHandshake(clientKeypair,
- serverPublic, idKeypair.Public(), nodeID)
- if !ok || clientSeed == nil || clientAuth == nil {
- b.Fatal("ClientHandshake failed")
- }
-
- // Validate the authenticator. Real code would pass the AUTH read off
- // the network as a slice to CompareAuth here.
- if !CompareAuth(clientAuth, serverAuth.Bytes()[:]) ||
- !CompareAuth(serverAuth, clientAuth.Bytes()[:]) {
- b.Fatal("AUTH mismatched between client/server")
- }
- }
-}
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/obfs4.go b/obfs4.go
deleted file mode 100644
index 4cc159c..0000000
--- a/obfs4.go
+++ /dev/null
@@ -1,758 +0,0 @@
-/*
- * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * * Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-// Package obfs4 implements the obfs4 protocol. For the most part, obfs4
-// connections are exposed via the net.Conn and net.Listener interface, though
-// accepting connections as a server requires calling ServerHandshake on the
-// conn to finish connection establishment.
-package obfs4
-
-import (
- "bytes"
- "crypto/sha256"
- "encoding/base64"
- "fmt"
- "io"
- "math/rand"
- "net"
- "syscall"
- "time"
-
- "git.torproject.org/pluggable-transports/obfs4.git/drbg"
- "git.torproject.org/pluggable-transports/obfs4.git/framing"
- "git.torproject.org/pluggable-transports/obfs4.git/ntor"
-)
-
-const (
- // SeedLength is the length of the obfs4 polymorphism seed.
- SeedLength = 32
- headerLength = framing.FrameOverhead + packetOverhead
- connectionTimeout = time.Duration(30) * time.Second
-
- maxCloseDelayBytes = maxHandshakeLength
- maxCloseDelay = 60
-
- maxIatDelay = 100
-)
-
-type connState int
-
-const (
- stateInit connState = iota
- stateEstablished
- stateBroken
- stateClosed
-)
-
-// Obfs4Conn is the implementation of the net.Conn interface for obfs4
-// connections.
-type Obfs4Conn struct {
- conn net.Conn
-
- sessionKey *ntor.Keypair
-
- lenProbDist *wDist
- iatProbDist *wDist
-
- encoder *framing.Encoder
- decoder *framing.Decoder
-
- receiveBuffer bytes.Buffer
- receiveDecodedBuffer bytes.Buffer
-
- state connState
- isServer bool
-
- // Server side state.
- listener *Obfs4Listener
- startTime time.Time
-}
-
-func (c *Obfs4Conn) padBurst(burst *bytes.Buffer) (err error) {
- tailLen := burst.Len() % framing.MaximumSegmentLength
- toPadTo := c.lenProbDist.sample()
-
- padLen := 0
- if toPadTo >= tailLen {
- padLen = toPadTo - tailLen
- } else {
- padLen = (framing.MaximumSegmentLength - tailLen) + toPadTo
- }
-
- if padLen > headerLength {
- err = c.producePacket(burst, packetTypePayload, []byte{},
- uint16(padLen-headerLength))
- if err != nil {
- return
- }
- } else if padLen > 0 {
- err = c.producePacket(burst, packetTypePayload, []byte{},
- maxPacketPayloadLength)
- if err != nil {
- return
- }
- err = c.producePacket(burst, packetTypePayload, []byte{},
- uint16(padLen))
- if err != nil {
- return
- }
- }
-
- return
-}
-
-func (c *Obfs4Conn) closeAfterDelay() {
- // I-it's not like I w-wanna handshake with you or anything. B-b-baka!
- defer c.conn.Close()
-
- delay := time.Duration(c.listener.closeDelay)*time.Second + connectionTimeout
- deadline := c.startTime.Add(delay)
- if time.Now().After(deadline) {
- return
- }
-
- err := c.conn.SetReadDeadline(deadline)
- if 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(c.listener.closeDelayBytes) {
- n, err := c.conn.Read(buf[:])
- if err != nil {
- return
- }
- discarded += n
- }
-}
-
-func (c *Obfs4Conn) setBroken() {
- c.state = stateBroken
-}
-
-func (c *Obfs4Conn) clientHandshake(nodeID *ntor.NodeID, publicKey *ntor.PublicKey) (err error) {
- if c.isServer {
- panic(fmt.Sprintf("BUG: clientHandshake() called for server connection"))
- }
-
- defer func() {
- c.sessionKey = nil
- if err != nil {
- c.setBroken()
- }
- }()
-
- // Generate/send the client handshake.
- var hs *clientHandshake
- var blob []byte
- hs = newClientHandshake(nodeID, publicKey, c.sessionKey)
- blob, err = hs.generateHandshake()
- if err != nil {
- return
- }
-
- err = c.conn.SetDeadline(time.Now().Add(connectionTimeout * 2))
- if err != nil {
- return
- }
-
- _, err = c.conn.Write(blob)
- if err != nil {
- return
- }
-
- // Consume the server handshake.
- var hsBuf [maxHandshakeLength]byte
- for {
- var n int
- n, err = c.conn.Read(hsBuf[:])
- if err != nil {
- // Yes, just bail out of handshaking even if the Read could have
- // returned data, no point in continuing on EOF/etc.
- return
- }
- c.receiveBuffer.Write(hsBuf[:n])
-
- var seed []byte
- n, seed, err = hs.parseServerHandshake(c.receiveBuffer.Bytes())
- if err == ErrMarkNotFoundYet {
- continue
- } else if err != nil {
- return
- }
- _ = c.receiveBuffer.Next(n)
-
- err = c.conn.SetDeadline(time.Time{})
- if err != nil {
- return
- }
-
- // Use the derived key material to intialize the link crypto.
- okm := ntor.Kdf(seed, framing.KeyLength*2)
- c.encoder = framing.NewEncoder(okm[:framing.KeyLength])
- c.decoder = framing.NewDecoder(okm[framing.KeyLength:])
-
- c.state = stateEstablished
-
- return nil
- }
-}
-
-func (c *Obfs4Conn) serverHandshake(nodeID *ntor.NodeID, keypair *ntor.Keypair) (err error) {
- if !c.isServer {
- panic(fmt.Sprintf("BUG: serverHandshake() called for client connection"))
- }
-
- defer func() {
- c.sessionKey = nil
- if err != nil {
- c.setBroken()
- }
- }()
-
- hs := newServerHandshake(nodeID, keypair, c.sessionKey)
- err = c.conn.SetDeadline(time.Now().Add(connectionTimeout))
- if err != nil {
- return
- }
-
- // Consume the client handshake.
- var hsBuf [maxHandshakeLength]byte
- for {
- var n int
- n, err = c.conn.Read(hsBuf[:])
- if err != nil {
- // Yes, just bail out of handshaking even if the Read could have
- // returned data, no point in continuing on EOF/etc.
- return
- }
- c.receiveBuffer.Write(hsBuf[:n])
-
- var seed []byte
- seed, err = hs.parseClientHandshake(c.listener.filter, c.receiveBuffer.Bytes())
- if err == ErrMarkNotFoundYet {
- continue
- } else if err != nil {
- return
- }
- c.receiveBuffer.Reset()
-
- err = c.conn.SetDeadline(time.Time{})
- if err != nil {
- return
- }
-
- // Use the derived key material to intialize the link crypto.
- okm := ntor.Kdf(seed, framing.KeyLength*2)
- c.encoder = framing.NewEncoder(okm[framing.KeyLength:])
- c.decoder = framing.NewDecoder(okm[:framing.KeyLength])
-
- break
- }
-
- //
- // Since the current and only implementation always sends a PRNG seed for
- // the length obfuscation, this makes the amount of data received from the
- // server inconsistent with the length sent from the client.
- //
- // Rebalance this by tweaking the client mimimum padding/server maximum
- // padding, and sending the PRNG seed unpadded (As in, treat the PRNG seed
- // as part of the server response). See inlineSeedFrameLength in
- // handshake_ntor.go.
- //
-
- // Generate/send the response.
- var blob []byte
- blob, err = hs.generateHandshake()
- if err != nil {
- return
- }
- var frameBuf bytes.Buffer
- _, err = frameBuf.Write(blob)
- if err != nil {
- return
- }
- c.state = stateEstablished
-
- // Send the PRNG seed as the first packet.
- err = c.producePacket(&frameBuf, packetTypePrngSeed, c.listener.rawSeed, 0)
- if err != nil {
- return
- }
- _, err = c.conn.Write(frameBuf.Bytes())
- if err != nil {
- return
- }
-
- return
-}
-
-// CanHandshake queries the connection state to see if it is appropriate to
-// call ServerHandshake to complete connection establishment.
-func (c *Obfs4Conn) CanHandshake() bool {
- return c.state == stateInit
-}
-
-// CanReadWrite queries the connection state to see if it is possible to read
-// and write data.
-func (c *Obfs4Conn) CanReadWrite() bool {
- return c.state == stateEstablished
-}
-
-// ServerHandshake completes the server side of the obfs4 handshake. Servers
-// are required to call this after accepting a connection. ServerHandshake
-// will treat errors encountered during the handshake as fatal and drop the
-// connection before returning.
-func (c *Obfs4Conn) ServerHandshake() error {
- // Handshakes when already established are a no-op.
- if c.CanReadWrite() {
- return nil
- } else if !c.CanHandshake() {
- return syscall.EINVAL
- }
-
- if !c.isServer {
- panic(fmt.Sprintf("BUG: ServerHandshake() called for client connection"))
- }
-
- // Complete the handshake.
- err := c.serverHandshake(c.listener.nodeID, c.listener.keyPair)
- if err != nil {
- c.closeAfterDelay()
- }
- c.listener = nil
-
- return err
-}
-
-// Read implements the net.Conn Read method.
-func (c *Obfs4Conn) Read(b []byte) (n int, err error) {
- if !c.CanReadWrite() {
- return 0, syscall.EINVAL
- }
-
- for c.receiveDecodedBuffer.Len() == 0 {
- _, err = c.consumeFramedPackets(nil)
- if err == framing.ErrAgain {
- continue
- } else if err != nil {
- return
- }
- }
-
- n, err = c.receiveDecodedBuffer.Read(b)
- return
-}
-
-// WriteTo implements the io.WriterTo WriteTo method.
-func (c *Obfs4Conn) WriteTo(w io.Writer) (n int64, err error) {
- if !c.CanReadWrite() {
- return 0, syscall.EINVAL
- }
-
- // If there is buffered payload from earlier Read() calls, write.
- wrLen := 0
- if c.receiveDecodedBuffer.Len() > 0 {
- wrLen, err = w.Write(c.receiveDecodedBuffer.Bytes())
- if err != nil {
- c.setBroken()
- return int64(wrLen), err
- } else if wrLen < int(c.receiveDecodedBuffer.Len()) {
- c.setBroken()
- return int64(wrLen), io.ErrShortWrite
- }
- c.receiveDecodedBuffer.Reset()
- }
-
- for {
- wrLen, err = c.consumeFramedPackets(w)
- n += int64(wrLen)
- if err == framing.ErrAgain {
- continue
- } else if err != nil {
- // io.EOF is treated as not an error.
- if err == io.EOF {
- err = nil
- }
- break
- }
- }
-
- return
-}
-
-// Write implements the net.Conn Write method. The obfs4 lengt obfuscation is
-// done based on the amount of data passed to Write (each call to Write results
-// in up to 2 frames of padding). Passing excessively short buffers to Write
-// will result in significant overhead.
-func (c *Obfs4Conn) Write(b []byte) (n int, err error) {
- if !c.CanReadWrite() {
- return 0, syscall.EINVAL
- }
-
- defer func() {
- if err != nil {
- c.setBroken()
- }
- }()
-
- // TODO: Change this to write directly to c.conn skipping frameBuf.
- chopBuf := bytes.NewBuffer(b)
- var payload [maxPacketPayloadLength]byte
- var frameBuf bytes.Buffer
-
- 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 = c.producePacket(&frameBuf, packetTypePayload, payload[:rdLen], 0)
- if err != nil {
- return 0, err
- }
- }
-
- // Insert random padding. In theory for some padding lengths, this can be
- // inlined with the payload, but doing it this way simplifies the code
- // significantly.
- err = c.padBurst(&frameBuf)
- if err != nil {
- return 0, err
- }
-
- // Spit frame(s) onto the network.
- //
- // Partial writes are fatal because the frame encoder state is advanced
- // at this point. It's possible to keep frameBuf around, but fuck it.
- // Someone that wants write timeouts can change this.
- if c.iatProbDist != nil {
- var iatFrame [framing.MaximumSegmentLength]byte
- for frameBuf.Len() > 0 {
- iatWrLen := 0
- iatWrLen, err = frameBuf.Read(iatFrame[:])
- if err != nil {
- return 0, err
- } else if iatWrLen == 0 {
- panic(fmt.Sprintf("BUG: Write(), iat length was 0"))
- }
-
- // Calculate the delay. The delay resolution is 100 usec, leading
- // to a maximum delay of 10 msec.
- iatDelta := time.Duration(c.iatProbDist.sample() * 100)
-
- // Write then sleep.
- _, err = c.conn.Write(iatFrame[:iatWrLen])
- if err != nil {
- return 0, err
- }
- time.Sleep(iatDelta * time.Microsecond)
- }
- } else {
- _, err = c.conn.Write(frameBuf.Bytes())
- if err != nil {
- return 0, err
- }
- }
-
- return
-}
-
-// Close closes the connection.
-func (c *Obfs4Conn) Close() error {
- if c.conn == nil {
- return syscall.EINVAL
- }
-
- c.state = stateClosed
-
- return c.conn.Close()
-}
-
-// LocalAddr returns the local network address.
-func (c *Obfs4Conn) LocalAddr() net.Addr {
- if c.state == stateClosed {
- return nil
- }
-
- return c.conn.LocalAddr()
-}
-
-// RemoteAddr returns the remote network address.
-func (c *Obfs4Conn) RemoteAddr() net.Addr {
- if c.state == stateClosed {
- return nil
- }
-
- return c.conn.RemoteAddr()
-}
-
-// SetDeadline is a convoluted way to get syscall.ENOTSUP.
-func (c *Obfs4Conn) SetDeadline(t time.Time) error {
- return syscall.ENOTSUP
-}
-
-// SetReadDeadline implements the net.Conn SetReadDeadline method. Connections
-// must be in the established state (CanReadWrite).
-func (c *Obfs4Conn) SetReadDeadline(t time.Time) error {
- if !c.CanReadWrite() {
- return syscall.EINVAL
- }
-
- return c.conn.SetReadDeadline(t)
-}
-
-// SetWriteDeadline is a convoluted way to get syscall.ENOTSUP.
-func (c *Obfs4Conn) SetWriteDeadline(t time.Time) error {
- return syscall.ENOTSUP
-}
-
-// DialFn is a function pointer to a dial routine that matches the
-// net.Dialer.Dial routine.
-type DialFn func(string, string) (net.Conn, error)
-
-// DialObfs4 connects to the remote address on the network, and handshakes with
-// the peer's obfs4 Node ID and Identity Public Key. nodeID and publicKey are
-// expected as strings containing the Base64 encoded values.
-func DialObfs4(network, address, nodeID, publicKey string, iatObfuscation bool) (*Obfs4Conn, error) {
-
- return DialObfs4DialFn(net.Dial, network, address, nodeID, publicKey, iatObfuscation)
-}
-
-// DialObfs4DialFn connects to the remote address on the network via DialFn,
-// and handshakes with the peers' obfs4 Node ID and Identity Public Key.
-func DialObfs4DialFn(dialFn DialFn, network, address, nodeID, publicKey string, iatObfuscation bool) (*Obfs4Conn, error) {
- // Decode the node_id/public_key.
- pub, err := ntor.PublicKeyFromBase64(publicKey)
- if err != nil {
- return nil, err
- }
- id, err := ntor.NodeIDFromBase64(nodeID)
- if err != nil {
- return nil, err
- }
-
- // Generate the initial length obfuscation distribution.
- seed, err := drbg.NewSeed()
- if err != nil {
- return nil, err
- }
-
- // Generate the Obfs4Conn.
- c := new(Obfs4Conn)
- c.lenProbDist = newWDist(seed, 0, framing.MaximumSegmentLength)
- if iatObfuscation {
- iatSeedSrc := sha256.Sum256(seed.Bytes()[:])
- iatSeed, err := drbg.SeedFromBytes(iatSeedSrc[:])
- if err != nil {
- return nil, err
- }
- c.iatProbDist = newWDist(iatSeed, 0, maxIatDelay)
- }
-
- // Generate the session keypair *before* connecting to the remote peer.
- c.sessionKey, err = ntor.NewKeypair(true)
- if err != nil {
- return nil, err
- }
-
- // Connect to the remote peer.
- c.conn, err = dialFn(network, address)
- if err != nil {
- return nil, err
- }
-
- // Handshake.
- err = c.clientHandshake(id, pub)
- if err != nil {
- c.conn.Close()
- return nil, err
- }
-
- return c, nil
-}
-
-// Obfs4Listener is the implementation of the net.Listener interface for obfs4
-// connections.
-type Obfs4Listener struct {
- listener net.Listener
-
- filter *replayFilter
-
- keyPair *ntor.Keypair
- nodeID *ntor.NodeID
-
- rawSeed []byte
- seed *drbg.Seed
- iatSeed *drbg.Seed
- iatObfuscation bool
-
- closeDelayBytes int
- closeDelay int
-}
-
-// Accept implements the Accept method of the net.Listener interface; it waits
-// for the next call and returns a generic net.Conn. Callers are responsible
-// for completing the handshake by calling Obfs4Conn.ServerHandshake().
-func (l *Obfs4Listener) Accept() (net.Conn, error) {
- conn, err := l.AcceptObfs4()
- if err != nil {
- return nil, err
- }
-
- return conn, nil
-}
-
-// AcceptObfs4 accepts the next incoming call and returns a new connection.
-// Callers are responsible for completing the handshake by calling
-// Obfs4Conn.ServerHandshake().
-func (l *Obfs4Listener) AcceptObfs4() (*Obfs4Conn, error) {
- // Accept a connection.
- c, err := l.listener.Accept()
- if err != nil {
- return nil, err
- }
-
- // Allocate the obfs4 connection state.
- cObfs := new(Obfs4Conn)
-
- // Generate the session keypair *before* consuming data from the peer, to
- // add more noise to the keypair generation time. The idea is that jitter
- // here is masked by network latency (the time it takes for a server to
- // accept a socket out of the backlog should not be fixed, and the client
- // needs to send the public key).
- cObfs.sessionKey, err = ntor.NewKeypair(true)
- if err != nil {
- return nil, err
- }
-
- cObfs.conn = c
- cObfs.isServer = true
- cObfs.listener = l
- cObfs.lenProbDist = newWDist(l.seed, 0, framing.MaximumSegmentLength)
- if l.iatObfuscation {
- cObfs.iatProbDist = newWDist(l.iatSeed, 0, maxIatDelay)
- }
- if err != nil {
- c.Close()
- return nil, err
- }
- cObfs.startTime = time.Now()
-
- return cObfs, nil
-}
-
-// Close stops listening on the Obfs4 endpoint. Already Accepted connections
-// are not closed.
-func (l *Obfs4Listener) Close() error {
- return l.listener.Close()
-}
-
-// Addr returns the listener's network address.
-func (l *Obfs4Listener) Addr() net.Addr {
- return l.listener.Addr()
-}
-
-// PublicKey returns the listener's Identity Public Key, a Base64 encoded
-// obfs4.ntor.PublicKey.
-func (l *Obfs4Listener) PublicKey() string {
- if l.keyPair == nil {
- return ""
- }
-
- return l.keyPair.Public().Base64()
-}
-
-// NodeID returns the listener's NodeID, a Base64 encoded obfs4.ntor.NodeID.
-func (l *Obfs4Listener) NodeID() string {
- if l.nodeID == nil {
- return ""
- }
-
- return l.nodeID.Base64()
-}
-
-// ListenObfs4 annnounces on the network and address, and returns and
-// Obfs4Listener. nodeId, privateKey and seed are expected as strings
-// containing the Base64 encoded values.
-func ListenObfs4(network, laddr, nodeID, privateKey, seed string, iatObfuscation bool) (*Obfs4Listener, error) {
- var err error
-
- // Decode node_id/private_key.
- l := new(Obfs4Listener)
- l.keyPair, err = ntor.KeypairFromBase64(privateKey)
- if err != nil {
- return nil, err
- }
- l.nodeID, err = ntor.NodeIDFromBase64(nodeID)
- if err != nil {
- return nil, err
- }
- l.rawSeed, err = base64.StdEncoding.DecodeString(seed)
- if err != nil {
- return nil, err
- }
- l.seed, err = drbg.SeedFromBytes(l.rawSeed)
- if err != nil {
- return nil, err
- }
- l.iatObfuscation = iatObfuscation
- if l.iatObfuscation {
- iatSeedSrc := sha256.Sum256(l.seed.Bytes()[:])
- l.iatSeed, err = drbg.SeedFromBytes(iatSeedSrc[:])
- if err != nil {
- return nil, err
- }
- }
-
- l.filter, err = newReplayFilter()
- if err != nil {
- return nil, err
- }
-
- rng := rand.New(drbg.NewHashDrbg(l.seed))
- l.closeDelayBytes = rng.Intn(maxCloseDelayBytes)
- l.closeDelay = rng.Intn(maxCloseDelay)
-
- // Start up the listener.
- l.listener, err = net.Listen(network, laddr)
- if err != nil {
- return nil, err
- }
-
- return l, nil
-}
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/obfs4proxy/obfs4proxy.go b/obfs4proxy/obfs4proxy.go
index c49f3d0..d3490bf 100644
--- a/obfs4proxy/obfs4proxy.go
+++ b/obfs4proxy/obfs4proxy.go
@@ -23,31 +23,13 @@
* 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.
- *
- * This file is based off goptlib's dummy-[client,server].go files.
*/
-// obfs4 pluggable transport. Works only as a managed proxy.
-//
-// Client usage (in torrc):
-// UseBridges 1
-// Bridge obfs4 X.X.X.X:YYYY <Fingerprint> public-key=<Base64 Bridge Public Key> node-id=<Base64 Bridge Node ID>
-// ClientTransportPlugin obfs4 exec obfs4proxy
-//
-// Server usage (in torrc):
-// BridgeRelay 1
-// ORPort 9001
-// ExtORPort 6669
-// ServerTransportPlugin obfs4 exec obfs4proxy
-// ServerTransportOptions obfs4 private-key=<Base64 Bridge Private Key> node-id=<Base64 Node ID> drbg-seed=<Base64 DRBG Seed>
-//
-// Because the pluggable transport requires arguments, obfs4proxy requires
-// tor-0.2.5.x to be useful.
+// Go language Tor Pluggable Transport suite. Works only as a managed
+// client/server.
package main
import (
- "encoding/base64"
- "encoding/hex"
"flag"
"fmt"
"io"
@@ -61,292 +43,307 @@ import (
"sync"
"syscall"
+ "code.google.com/p/go.net/proxy"
+
"git.torproject.org/pluggable-transports/goptlib.git"
- "git.torproject.org/pluggable-transports/obfs4.git"
- "git.torproject.org/pluggable-transports/obfs4.git/csrand"
- "git.torproject.org/pluggable-transports/obfs4.git/ntor"
+ "git.torproject.org/pluggable-transports/obfs4.git/transports"
+ "git.torproject.org/pluggable-transports/obfs4.git/transports/base"
)
const (
- obfs4Method = "obfs4"
- obfs4LogFile = "obfs4proxy.log"
+ obfs4proxyLogFile = "obfs4proxy.log"
+ socksAddr = "127.0.0.1:0"
+ elidedAddr = "[scrubbed]"
)
var enableLogging bool
var unsafeLogging bool
-var iatObfuscation bool
-var ptListeners []net.Listener
+var stateDir string
+var handlerChan chan int
-// When a connection handler starts, +1 is written to this channel; when it
-// ends, -1 is written.
-var handlerChan = make(chan int)
+// DialFn is a function pointer to a function that matches the net.Dialer.Dial
+// interface.
+type DialFn func(string, string) (net.Conn, error)
-func logAndRecover(conn *obfs4.Obfs4Conn) {
- if err := recover(); err != nil {
- log.Printf("[ERROR] %p: Panic: %s", conn, err)
+func elideAddr(addrStr string) string {
+ if unsafeLogging {
+ return addrStr
}
+
+ if addr, err := resolveAddrStr(addrStr); err == nil {
+ // Only scrub off the address so that it's slightly easier to follow
+ // the logs by looking at the port.
+ return fmt.Sprintf("%s:%d", elidedAddr, addr.Port)
+ }
+
+ return elidedAddr
}
-func copyLoop(a net.Conn, b *obfs4.Obfs4Conn) {
- var wg sync.WaitGroup
- wg.Add(2)
+func clientSetup() (launched bool, listeners []net.Listener) {
+ ptClientInfo, err := pt.ClientSetup(transports.Transports())
+ if err != nil {
+ log.Fatal(err)
+ }
- go func() {
- defer logAndRecover(b)
- defer wg.Done()
- defer b.Close()
- defer a.Close()
+ ptClientProxy, err := ptGetProxy()
+ if err != nil {
+ log.Fatal(err)
+ } else if ptClientProxy != nil {
+ ptProxyDone()
+ }
- _, err := io.Copy(b, a)
- if err != nil {
- log.Printf("[WARN] copyLoop: %p: Connection closed: %s", b, err)
+ // Launch each of the client listeners.
+ for _, name := range ptClientInfo.MethodNames {
+ t := transports.Get(name)
+ if t == nil {
+ pt.CmethodError(name, "no such transport is supported")
+ continue
}
- }()
- go func() {
- defer logAndRecover(b)
- defer wg.Done()
- defer a.Close()
- defer b.Close()
- _, err := io.Copy(a, b)
+ f, err := t.ClientFactory(stateDir)
if err != nil {
- log.Printf("[WARN] copyLoop: %p: Connection closed: %s", b, err)
+ pt.CmethodError(name, "failed to get ClientFactory")
+ continue
}
- }()
-
- wg.Wait()
-}
-
-func serverHandler(conn *obfs4.Obfs4Conn, info *pt.ServerInfo) error {
- defer conn.Close()
- defer logAndRecover(conn)
-
- handlerChan <- 1
- defer func() {
- handlerChan <- -1
- }()
- var addr string
- if unsafeLogging {
- addr = conn.RemoteAddr().String()
- } else {
- addr = "[scrubbed]"
- }
+ ln, err := pt.ListenSocks("tcp", socksAddr)
+ if err != nil {
+ pt.CmethodError(name, err.Error())
+ continue
+ }
- log.Printf("[INFO] server: %p: New connection from %s", conn, addr)
+ go clientAcceptLoop(f, ln, ptClientProxy)
+ pt.Cmethod(name, ln.Version(), ln.Addr())
- // Handshake with the client.
- err := conn.ServerHandshake()
- if err != nil {
- log.Printf("[WARN] server: %p: Handshake failed: %s", conn, err)
- return err
- }
+ log.Printf("[INFO]: %s - registered listener: %s", name, ln.Addr())
- or, err := pt.DialOr(info, conn.RemoteAddr().String(), obfs4Method)
- if err != nil {
- log.Printf("[ERROR] server: %p: DialOr failed: %s", conn, err)
- return err
+ listeners = append(listeners, ln)
+ launched = true
}
- defer or.Close()
-
- copyLoop(or, conn)
+ pt.CmethodsDone()
- return nil
+ return
}
-func serverAcceptLoop(ln *obfs4.Obfs4Listener, info *pt.ServerInfo) error {
+func clientAcceptLoop(f base.ClientFactory, ln *pt.SocksListener, proxyURI *url.URL) error {
defer ln.Close()
for {
- conn, err := ln.AcceptObfs4()
+ conn, err := ln.AcceptSocks()
if err != nil {
if e, ok := err.(net.Error); ok && !e.Temporary() {
return err
}
continue
}
- go serverHandler(conn, info)
+ go clientHandler(f, conn, proxyURI)
}
}
-func serverSetup() (launched bool) {
- // Initialize pt logging.
- err := ptInitializeLogging(enableLogging)
+func clientHandler(f base.ClientFactory, conn *pt.SocksConn, proxyURI *url.URL) {
+ defer conn.Close()
+ handlerChan <- 1
+ defer func() {
+ handlerChan <- -1
+ }()
+
+ name := f.Transport().Name()
+ addrStr := elideAddr(conn.Req.Target)
+ log.Printf("[INFO]: %s(%s) - new connection", name, addrStr)
+
+ // Deal with arguments.
+ args, err := f.ParseArgs(&conn.Req.Args)
if err != nil {
+ log.Printf("[ERROR]: %s(%s) - invalid arguments: %s", name, addrStr, err)
+ conn.Reject()
return
}
- ptServerInfo, err := pt.ServerSetup([]string{obfs4Method})
+ // Obtain the proxy dialer if any, and create the outgoing TCP connection.
+ var dialFn DialFn
+ if proxyURI == nil {
+ dialFn = proxy.Direct.Dial
+ } else {
+ // This is unlikely to happen as the proxy protocol is verified during
+ // the configuration phase.
+ dialer, err := proxy.FromURL(proxyURI, proxy.Direct)
+ if err != nil {
+ log.Printf("[ERROR]: %s(%s) - failed to obtain proxy dialer: %s", name, addrStr, err)
+ conn.Reject()
+ return
+ }
+ dialFn = dialer.Dial
+ }
+ remoteConn, err := dialFn("tcp", conn.Req.Target) // XXX: Allow UDP?
if err != nil {
+ // Note: The error message returned from the dialer can include the IP
+ // address/port of the remote peer.
+ if unsafeLogging {
+ log.Printf("[ERROR]: %s(%s) - outgoing connection failed: %s", name, addrStr, err)
+ } else {
+ log.Printf("[ERROR]: %s(%s) - outgoing connection failed", name, addrStr)
+ }
+ conn.Reject()
return
}
+ defer remoteConn.Close()
- for _, bindaddr := range ptServerInfo.Bindaddrs {
- switch bindaddr.MethodName {
- case obfs4Method:
- // Handle the mandetory arguments.
- privateKey, ok := bindaddr.Options.Get("private-key")
- if !ok {
- pt.SmethodError(bindaddr.MethodName, "needs a private-key option")
- break
- }
- nodeID, ok := bindaddr.Options.Get("node-id")
- if !ok {
- pt.SmethodError(bindaddr.MethodName, "needs a node-id option")
- break
- }
- seed, ok := bindaddr.Options.Get("drbg-seed")
- if !ok {
- pt.SmethodError(bindaddr.MethodName, "needs a drbg-seed option")
- break
- }
-
- // Initialize the listener.
- ln, err := obfs4.ListenObfs4("tcp", bindaddr.Addr.String(), nodeID,
- privateKey, seed, iatObfuscation)
- if err != nil {
- pt.SmethodError(bindaddr.MethodName, err.Error())
- break
- }
+ // Instantiate the client transport method, handshake, and start pushing
+ // bytes back and forth.
+ remote, err := f.WrapConn(remoteConn, args)
+ if err != nil {
+ log.Printf("[ERROR]: %s(%s) - handshake failed: %s", name, addrStr, err)
+ conn.Reject()
+ return
+ }
+ err = conn.Grant(remoteConn.RemoteAddr().(*net.TCPAddr))
+ if err != nil {
+ log.Printf("[ERROR]: %s(%s) - SOCKS grant failed: %s", name, addrStr, err)
+ return
+ }
- // Report the SMETHOD including the parameters.
- args := pt.Args{}
- args.Add("node-id", nodeID)
- args.Add("public-key", ln.PublicKey())
- go serverAcceptLoop(ln, &ptServerInfo)
- pt.SmethodArgs(bindaddr.MethodName, ln.Addr(), args)
- ptListeners = append(ptListeners, ln)
- launched = true
- default:
- pt.SmethodError(bindaddr.MethodName, "no such method")
- }
+ err = copyLoop(conn, remote)
+ if err != nil {
+ log.Printf("[INFO]: %s(%s) - closed connection: %s", name, addrStr, err)
+ } else {
+ log.Printf("[INFO]: %s(%s) - closed connection", name, addrStr)
}
- pt.SmethodsDone()
return
}
-func clientHandler(conn *pt.SocksConn, proxyURI *url.URL) error {
- defer conn.Close()
-
- var addr string
- if unsafeLogging {
- addr = conn.Req.Target
- } else {
- addr = "[scrubbed]"
+func serverSetup() (launched bool, listeners []net.Listener) {
+ ptServerInfo, err := pt.ServerSetup(transports.Transports())
+ if err != nil {
+ log.Fatal(err)
}
- log.Printf("[INFO] client: New connection to %s", addr)
+ for _, bindaddr := range ptServerInfo.Bindaddrs {
+ name := bindaddr.MethodName
+ t := transports.Get(name)
+ if t == nil {
+ pt.SmethodError(name, "no such transport is supported")
+ continue
+ }
- // Extract the peer's node ID and public key.
- nodeID, ok := conn.Req.Args.Get("node-id")
- if !ok {
- log.Printf("[ERROR] client: missing node-id argument")
- conn.Reject()
- return nil
- }
- publicKey, ok := conn.Req.Args.Get("public-key")
- if !ok {
- log.Printf("[ERROR] client: missing public-key argument")
- conn.Reject()
- return nil
- }
+ f, err := t.ServerFactory(stateDir, &bindaddr.Options)
+ if err != nil {
+ pt.SmethodError(name, err.Error())
+ continue
+ }
- handlerChan <- 1
- defer func() {
- handlerChan <- -1
- }()
+ ln, err := net.ListenTCP("tcp", bindaddr.Addr)
+ if err != nil {
+ pt.SmethodError(name, err.Error())
+ }
- defer logAndRecover(nil)
- dialFn, err := getProxyDialer(proxyURI)
- if err != nil {
- log.Printf("[ERROR] client: failed to get proxy dialer: %s", err)
- conn.Reject()
- return err
- }
- remote, err := obfs4.DialObfs4DialFn(dialFn, "tcp", conn.Req.Target, nodeID, publicKey, iatObfuscation)
- if err != nil {
- log.Printf("[ERROR] client: %p: Handshake failed: %s", remote, err)
- conn.Reject()
- return err
- }
- defer remote.Close()
- err = conn.Grant(remote.RemoteAddr().(*net.TCPAddr))
- if err != nil {
- return err
- }
+ go serverAcceptLoop(f, ln, &ptServerInfo)
+ if args := f.Args(); args != nil {
+ pt.SmethodArgs(name, ln.Addr(), *args)
+ } else {
+ pt.SmethodArgs(name, ln.Addr(), nil)
+ }
- copyLoop(conn, remote)
+ log.Printf("[INFO]: %s - registered listener: %s", name, elideAddr(ln.Addr().String()))
- return nil
+ listeners = append(listeners, ln)
+ launched = true
+ }
+ pt.SmethodsDone()
+
+ return
}
-func clientAcceptLoop(ln *pt.SocksListener, proxyURI *url.URL) error {
+func serverAcceptLoop(f base.ServerFactory, ln net.Listener, info *pt.ServerInfo) error {
defer ln.Close()
for {
- conn, err := ln.AcceptSocks()
+ conn, err := ln.Accept()
if err != nil {
if e, ok := err.(net.Error); ok && !e.Temporary() {
return err
}
continue
}
- go clientHandler(conn, proxyURI)
+ go serverHandler(f, conn, info)
}
}
-func clientSetup() (launched bool) {
- // Initialize pt logging.
- err := ptInitializeLogging(enableLogging)
+func serverHandler(f base.ServerFactory, conn net.Conn, info *pt.ServerInfo) {
+ defer conn.Close()
+ handlerChan <- 1
+ defer func() {
+ handlerChan <- -1
+ }()
+
+ name := f.Transport().Name()
+ addrStr := elideAddr(conn.RemoteAddr().String())
+ log.Printf("[INFO]: %s(%s) - new connection", name, addrStr)
+
+ // Instantiate the server transport method and handshake.
+ remote, err := f.WrapConn(conn)
if err != nil {
+ log.Printf("[ERROR]: %s(%s) - handshake failed: %s", name, addrStr, err)
return
}
- ptClientInfo, err := pt.ClientSetup([]string{obfs4Method})
+ // Connect to the orport.
+ orConn, err := pt.DialOr(info, conn.RemoteAddr().String(), name)
if err != nil {
- log.Fatal(err)
+ log.Printf("[ERROR]: %s(%s) - failed to connect to ORPort: %s", name, addrStr, err)
+ return
}
+ defer orConn.Close()
- ptClientProxy, err := ptGetProxy()
+ err = copyLoop(orConn, remote)
if err != nil {
- log.Fatal(err)
- } else if ptClientProxy != nil {
- ptProxyDone()
+ log.Printf("[INFO]: %s(%s) - closed connection: %s", name, addrStr, err)
+ } else {
+ log.Printf("[INFO]: %s(%s) - closed connection", name, addrStr)
}
- for _, methodName := range ptClientInfo.MethodNames {
- switch methodName {
- case obfs4Method:
- ln, err := pt.ListenSocks("tcp", "127.0.0.1:0")
- if err != nil {
- pt.CmethodError(methodName, err.Error())
- break
- }
- go clientAcceptLoop(ln, ptClientProxy)
- pt.Cmethod(methodName, ln.Version(), ln.Addr())
- ptListeners = append(ptListeners, ln)
- launched = true
- default:
- pt.CmethodError(methodName, "no such method")
- }
+ return
+}
+
+func copyLoop(a net.Conn, b net.Conn) error {
+ // Note: b is always the pt connection. a is the SOCKS/ORPort connection.
+ errChan := make(chan error, 2)
+
+ var wg sync.WaitGroup
+ wg.Add(2)
+
+ go func() {
+ defer wg.Done()
+ defer b.Close()
+ defer a.Close()
+ _, err := io.Copy(b, a)
+ errChan <- err
+ }()
+ go func() {
+ defer wg.Done()
+ defer a.Close()
+ defer b.Close()
+ _, err := io.Copy(a, b)
+ errChan <- err
+ }()
+
+ // Wait for both upstream and downstream to close. Since one side
+ // terminating closes the other, the second error in the channel will be
+ // something like EINVAL (though io.Copy() will swallow EOF), so only the
+ // first error is returned.
+ wg.Wait()
+ if len(errChan) > 0 {
+ return <-errChan
}
- pt.CmethodsDone()
- return
+ return nil
}
func ptInitializeLogging(enable bool) error {
if enable {
- // pt.MakeStateDir will ENV-ERROR for us.
- dir, err := pt.MakeStateDir()
- if err != nil {
- return err
- }
-
// While we could just exit, log an ENV-ERROR so it will propagate to
// the tor log.
- f, err := os.OpenFile(path.Join(dir, obfs4LogFile), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
+ f, err := os.OpenFile(path.Join(stateDir, obfs4proxyLogFile), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
- return ptEnvError(fmt.Sprintf("Failed to open log file: %s\n", err))
+ return ptEnvError(fmt.Sprintf("failed to open log file: %s\n", err))
}
log.SetOutput(f)
} else {
@@ -356,122 +353,74 @@ func ptInitializeLogging(enable bool) error {
return nil
}
-func generateServerParams(id string) {
- idIsFP := id != ""
- var rawID []byte
-
- if idIsFP {
- var err error
- rawID, err = hex.DecodeString(id)
- if err != nil {
- fmt.Println("Failed to hex decode id:", err)
- return
- }
- } else {
- rawID = make([]byte, ntor.NodeIDLength)
- err := csrand.Bytes(rawID)
- if err != nil {
- fmt.Println("Failed to generate random node-id:", err)
- return
- }
- }
- parsedID, err := ntor.NewNodeID(rawID)
- if err != nil {
- fmt.Println("Failed to parse id:", err)
- return
- }
-
- fmt.Println("Generated node-id:", parsedID.Base64())
-
- keypair, err := ntor.NewKeypair(false)
- if err != nil {
- fmt.Println("Failed to generate keypair:", err)
- return
- }
-
- seed := make([]byte, obfs4.SeedLength)
- err = csrand.Bytes(seed)
- if err != nil {
- fmt.Println("Failed to generate DRBG seed:", err)
- return
- }
- seedBase64 := base64.StdEncoding.EncodeToString(seed)
-
- fmt.Println("Generated private-key:", keypair.Private().Base64())
- fmt.Println("Generated public-key:", keypair.Public().Base64())
- fmt.Println("Generated drbg-seed:", seedBase64)
- fmt.Println()
- fmt.Println("Client config: ")
- if idIsFP {
- fmt.Printf(" Bridge obfs4 <IP Address:Port> %s node-id=%s public-key=%s\n",
- id, parsedID.Base64(), keypair.Public().Base64())
- } else {
- fmt.Printf(" Bridge obfs4 <IP Address:Port> <Fingerprint> node-id=%s public-key=%s\n",
- parsedID.Base64(), keypair.Public().Base64())
- }
- fmt.Println()
- fmt.Println("Server config:")
- fmt.Printf(" ServerTransportOptions obfs4 node-id=%s private-key=%s drbg-seed=%s\n",
- parsedID.Base64(), keypair.Private().Base64(), seedBase64)
-}
-
func main() {
- // Some command line args.
- genParams := flag.Bool("genServerParams", false, "Generate Bridge operator torrc parameters")
- genParamsFP := flag.String("genServerParamsFP", "", "Optional bridge fingerprint for genServerParams")
- flag.BoolVar(&enableLogging, "enableLogging", false, "Log to TOR_PT_STATE_LOCATION/obfs4proxy.log")
- flag.BoolVar(&iatObfuscation, "iatObfuscation", false, "Enable IAT obufscation (EXPENSIVE)")
+ // Handle the command line arguments.
+ _, execName := path.Split(os.Args[0])
+ flag.BoolVar(&enableLogging, "enableLogging", false, "Log to TOR_PT_STATE_LOCATION/"+obfs4proxyLogFile)
flag.BoolVar(&unsafeLogging, "unsafeLogging", false, "Disable the address scrubber")
flag.Parse()
- if *genParams {
- generateServerParams(*genParamsFP)
- return
- }
- // Go through the pt protocol and initialize client or server mode.
+ // Determine if this is a client or server, initialize logging, and finish
+ // the pt configuration.
+ var ptListeners []net.Listener
+ handlerChan = make(chan int)
launched := false
isClient, err := ptIsClient()
if err != nil {
- log.Fatal("[ERROR] obfs4proxy must be run as a managed transport or server")
- } else if isClient {
- launched = clientSetup()
+ log.Fatalf("[ERROR]: %s - must be run as a managed transport", execName)
+ }
+ if stateDir, err = pt.MakeStateDir(); err != nil {
+ log.Fatalf("[ERROR]: %s - No state directory: %s", execName, err)
+ }
+ if err = ptInitializeLogging(enableLogging); err != nil {
+ log.Fatalf("[ERROR]: %s - failed to initialize logging", execName)
+ }
+ if isClient {
+ log.Printf("[INFO]: %s - initializing client transport listeners", execName)
+ launched, ptListeners = clientSetup()
} else {
- launched = serverSetup()
+ log.Printf("[INFO]: %s - initializing server transport listeners", execName)
+ launched, ptListeners = serverSetup()
}
if !launched {
- // Something must have failed in client/server setup, just bail.
+ // Initialization failed, the client or server setup routines should
+ // have logged, so just exit here.
os.Exit(-1)
}
- log.Println("[INFO] obfs4proxy - Launched and listening")
+ log.Printf("[INFO]: %s - launched and accepting connections", execName)
defer func() {
- log.Println("[INFO] obfs4proxy - Terminated")
+ log.Printf("[INFO]: %s - terminated", execName)
}()
- // Handle termination notification.
- numHandlers := 0
- var sig os.Signal
+ // At this point, the pt config protocol is finished, and incoming
+ // connections will be processed. Per the pt spec, on sane platforms
+ // termination is signaled via SIGINT (or SIGTERM), so wait on tor to
+ // request a shutdown of some sort.
+
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
- // wait for first signal
- sig = nil
+ // Wait for the first SIGINT (close listeners).
+ var sig os.Signal
+ numHandlers := 0
for sig == nil {
select {
case n := <-handlerChan:
numHandlers += n
case sig = <-sigChan:
+ if sig == syscall.SIGTERM {
+ // SIGTERM causes immediate termination.
+ return
+ }
}
}
for _, ln := range ptListeners {
ln.Close()
}
- if sig == syscall.SIGTERM {
- return
- }
-
- // wait for second signal or no more handlers
+ // Wait for the 2nd SIGINT (or a SIGTERM), or for all current sessions to
+ // finish.
sig = nil
for sig == nil && numHandlers != 0 {
select {
@@ -481,5 +430,3 @@ func main() {
}
}
}
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/obfs4proxy/proxy_extras.go b/obfs4proxy/proxy_extras.go
deleted file mode 100644
index 5ead3b8..0000000
--- a/obfs4proxy/proxy_extras.go
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * * Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-package main
-
-import (
- "net/url"
-
- "code.google.com/p/go.net/proxy"
-
- "git.torproject.org/pluggable-transports/obfs4.git"
-)
-
-// getProxyDialer is a trival wrapper around the go.net/proxy package to avoid
-// having it as a dependency for anything else.
-func getProxyDialer(uri *url.URL) (obfs4.DialFn, error) {
- if uri == nil {
- return proxy.Direct.Dial, nil
- }
-
- dialer, err := proxy.FromURL(uri, proxy.Direct)
- if err != nil {
- return nil, err
- }
-
- return dialer.Dial, nil
-}
diff --git a/obfs4proxy/proxy_http.go b/obfs4proxy/proxy_http.go
index c7b926a..2db6ca0 100644
--- a/obfs4proxy/proxy_http.go
+++ b/obfs4proxy/proxy_http.go
@@ -108,7 +108,6 @@ func (s *httpProxy) Dial(network, addr string) (net.Conn, error) {
return conn, nil
}
-// httpConn is the mountain of bullshit we need to do just for staleReader.
type httpConn struct {
remoteAddr *net.TCPAddr
httpConn *httputil.ClientConn
@@ -157,5 +156,3 @@ func (c *httpConn) SetWriteDeadline(t time.Time) error {
func init() {
proxy.RegisterDialerType("http", newHTTP)
}
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/obfs4proxy/proxy_socks4.go b/obfs4proxy/proxy_socks4.go
index 95cc7b6..9d6bd4d 100644
--- a/obfs4proxy/proxy_socks4.go
+++ b/obfs4proxy/proxy_socks4.go
@@ -162,5 +162,3 @@ func init() {
// Despite the scheme name, this really is SOCKS4.
proxy.RegisterDialerType("socks4a", newSOCKS4)
}
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/obfs4proxy/pt_extras.go b/obfs4proxy/pt_extras.go
index 2d09cc3..9eddd26 100644
--- a/obfs4proxy/pt_extras.go
+++ b/obfs4proxy/pt_extras.go
@@ -124,7 +124,7 @@ func ptGetProxy() (*url.URL, error) {
return nil, ptProxyError(fmt.Sprintf("proxy URI has invalid scheme: %s", spec.Scheme))
}
- err = validateAddrStr(spec.Host)
+ _, err = resolveAddrStr(spec.Host)
if err != nil {
return nil, ptProxyError(fmt.Sprintf("proxy URI has invalid host: %s", err))
}
@@ -135,27 +135,26 @@ func ptGetProxy() (*url.URL, error) {
// Sigh, pt.resolveAddr() isn't exported. Include our own getto version that
// doesn't work around #7011, because we don't work with pre-0.2.5.x tor, and
// all we care about is validation anyway.
-func validateAddrStr(addrStr string) error {
+func resolveAddrStr(addrStr string) (*net.TCPAddr, error) {
ipStr, portStr, err := net.SplitHostPort(addrStr)
if err != nil {
- return err
+ return nil, err
}
if ipStr == "" {
- return net.InvalidAddrError(fmt.Sprintf("address string %q lacks a host part", addrStr))
+ return nil, net.InvalidAddrError(fmt.Sprintf("address string %q lacks a host part", addrStr))
}
if portStr == "" {
- return net.InvalidAddrError(fmt.Sprintf("address string %q lacks a port part", addrStr))
+ return nil, net.InvalidAddrError(fmt.Sprintf("address string %q lacks a port part", addrStr))
}
- if net.ParseIP(ipStr) == nil {
- return net.InvalidAddrError(fmt.Sprintf("not an IP string: %q", ipStr))
+ ip := net.ParseIP(ipStr)
+ if ip == nil {
+ return nil, net.InvalidAddrError(fmt.Sprintf("not an IP string: %q", ipStr))
}
- _, err = strconv.ParseUint(portStr, 10, 16)
+ port, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
- return net.InvalidAddrError(fmt.Sprintf("not a Port string: %q", portStr))
+ return nil, net.InvalidAddrError(fmt.Sprintf("not a Port string: %q", portStr))
}
- return nil
+ return &net.TCPAddr{IP: ip, Port: int(port), Zone: ""}, nil
}
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/packet.go b/packet.go
deleted file mode 100644
index 58a5877..0000000
--- a/packet.go
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * * Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-package obfs4
-
-import (
- "crypto/sha256"
- "encoding/binary"
- "fmt"
- "io"
- "syscall"
-
- "git.torproject.org/pluggable-transports/obfs4.git/drbg"
- "git.torproject.org/pluggable-transports/obfs4.git/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 (c *Obfs4Conn) producePacket(w io.Writer, pktType uint8, data []byte, padLen uint16) (err error) {
- var pkt [framing.MaximumFramePayloadLength]byte
-
- if !c.CanReadWrite() {
- return syscall.EINVAL
- }
-
- if len(data)+int(padLen) > maxPacketPayloadLength {
- panic(fmt.Sprintf("BUG: makePacket() len(data) + padLen > maxPacketPayloadLength: %d + %d > %d",
- len(data), padLen, maxPacketPayloadLength))
- }
-
- defer func() {
- if err != nil {
- c.setBroken()
- }
- }()
-
- // Packets are:
- // uint8_t type packetTypePayload (0x00)
- // uint16_t length Length of the payload (Big Endian).
- // uint8_t[] payload Data payload.
- // uint8_t[] padding Padding.
- pkt[0] = pktType
- binary.BigEndian.PutUint16(pkt[1:], uint16(len(data)))
- if len(data) > 0 {
- copy(pkt[3:], data[:])
- }
- copy(pkt[3+len(data):], zeroPadBytes[:padLen])
-
- pktLen := packetOverhead + len(data) + int(padLen)
-
- // Encode the packet in an AEAD frame.
- var frame [framing.MaximumSegmentLength]byte
- frameLen := 0
- frameLen, err = c.encoder.Encode(frame[:], pkt[:pktLen])
- if err != nil {
- // All encoder errors are fatal.
- return
- }
- var wrLen int
- wrLen, err = w.Write(frame[:frameLen])
- if err != nil {
- return
- } else if wrLen < frameLen {
- err = io.ErrShortWrite
- return
- }
-
- return
-}
-
-func (c *Obfs4Conn) consumeFramedPackets(w io.Writer) (n int, err error) {
- if !c.CanReadWrite() {
- return n, syscall.EINVAL
- }
-
- var buf [consumeReadSize]byte
- rdLen, rdErr := c.conn.Read(buf[:])
- c.receiveBuffer.Write(buf[:rdLen])
- var decoded [framing.MaximumFramePayloadLength]byte
- for c.receiveBuffer.Len() > 0 {
- // Decrypt an AEAD frame.
- decLen := 0
- decLen, err = c.decoder.Decode(decoded[:], &c.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 {
- if w != nil {
- // c.WriteTo() skips buffering in c.receiveDecodedBuffer
- var wrLen int
- wrLen, err = w.Write(payload)
- n += wrLen
- if err != nil {
- break
- } else if wrLen < int(payloadLen) {
- err = io.ErrShortWrite
- break
- }
- } else {
- // c.Read() stashes decoded payload in receiveDecodedBuffer
- c.receiveDecodedBuffer.Write(payload)
- n += int(payloadLen)
- }
- }
- case packetTypePrngSeed:
- // Only regenerate the distribution if we are the client.
- if len(payload) == seedPacketPayloadLength && !c.isServer {
- var seed *drbg.Seed
- seed, err = drbg.SeedFromBytes(payload)
- if err != nil {
- break
- }
- c.lenProbDist.reset(seed)
- if c.iatProbDist != nil {
- iatSeedSrc := sha256.Sum256(seed.Bytes()[:])
- iatSeed, err := drbg.SeedFromBytes(iatSeedSrc[:])
- if err != nil {
- break
- }
- c.iatProbDist.reset(iatSeed)
- }
- }
- default:
- // Ignore unrecognised packet types.
- }
- }
-
- // Read errors and non-framing.ErrAgain errors are all fatal.
- if (err != nil && err != framing.ErrAgain) || rdErr != nil {
- // Propagate read errors correctly.
- if err == nil && rdErr != nil {
- err = rdErr
- }
- c.setBroken()
- }
-
- return
-}
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/replay_filter.go b/replay_filter.go
deleted file mode 100644
index b8f284a..0000000
--- a/replay_filter.go
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * * Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-package obfs4
-
-import (
- "container/list"
- "encoding/binary"
- "sync"
-
- "github.com/dchest/siphash"
-
- "git.torproject.org/pluggable-transports/obfs4.git/csrand"
-)
-
-// maxFilterSize is the maximum capacity of the replay filter. The busiest
-// bridge I know about processes something along the order of 3000 connections
-// per day. The maximum timespan any entry can live in the filter is 2 hours,
-// so this value should be sufficient.
-const maxFilterSize = 100 * 1024
-
-// replayFilter is a simple filter designed only to answer if it has seen a
-// given byte sequence before. It is based around comparing the SipHash-2-4
-// digest of data to match against. Collisions are treated as positive matches
-// however, the probability of such occurences is negligible.
-type replayFilter struct {
- lock sync.Mutex
- key [2]uint64
- filter map[uint64]*filterEntry
- fifo *list.List
-}
-
-type filterEntry struct {
- firstSeen int64
- hash uint64
- element *list.Element
-}
-
-// newReplayFilter creates a new replayFilter instance.
-func newReplayFilter() (filter *replayFilter, err error) {
- // Initialize the SipHash-2-4 instance with a random key.
- var key [16]byte
- err = csrand.Bytes(key[:])
- if err != nil {
- return
- }
-
- filter = new(replayFilter)
- filter.key[0] = binary.BigEndian.Uint64(key[0:8])
- filter.key[1] = binary.BigEndian.Uint64(key[8:16])
- filter.filter = make(map[uint64]*filterEntry)
- filter.fifo = list.New()
-
- return
-}
-
-// testAndSet queries the filter for buf, adds it if it was not present and
-// returns if it has added the entry or not. This method is threadsafe.
-func (f *replayFilter) testAndSet(now int64, buf []byte) bool {
- hash := siphash.Hash(f.key[0], f.key[1], buf)
-
- f.lock.Lock()
- defer f.lock.Unlock()
-
- f.compactFilter(now)
-
- entry := f.filter[hash]
- if entry != nil {
- return true
- }
-
- entry = new(filterEntry)
- entry.hash = hash
- entry.firstSeen = now
- entry.element = f.fifo.PushBack(entry)
- f.filter[hash] = entry
-
- return false
-}
-
-// compactFilter purges entries that are too old to be relevant. If the filter
-// is filled to maxFilterCapacity, it will force purge a single entry. This
-// method is NOT threadsafe.
-func (f *replayFilter) compactFilter(now int64) {
- e := f.fifo.Front()
- for e != nil {
- entry, _ := e.Value.(*filterEntry)
-
- // If the filter is at max capacity, force purge at least one entry.
- if f.fifo.Len() < maxFilterSize {
- deltaT := now - entry.firstSeen
- if deltaT < 0 {
- // Aeeeeeee, the system time jumped backwards, potentially by
- // a lot. This will eventually self-correct, but "eventually"
- // could be a long time. As much as this sucks, jettison the
- // entire filter.
- f.reset()
- return
- }
- if deltaT < 3600*2 {
- // Why yes, this is 2 hours. The MAC code includes a hour
- // resolution timestamp, but to deal with clock skew, it
- // accepts time +- 1 hour.
- break
- }
- }
- eNext := e.Next()
- delete(f.filter, entry.hash)
- f.fifo.Remove(entry.element)
- entry.element = nil
- e = eNext
- }
-}
-
-// reset purges the entire filter. This methoid is NOT threadsafe.
-func (f *replayFilter) reset() {
- f.filter = make(map[uint64]*filterEntry)
- f.fifo = list.New()
-}
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/replay_filter_test.go b/replay_filter_test.go
deleted file mode 100644
index 09337c0..0000000
--- a/replay_filter_test.go
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * * Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-package obfs4
-
-import (
- "testing"
-)
-
-func TestReplayFilter(t *testing.T) {
- f, err := newReplayFilter()
- if err != nil {
- t.Fatal("newReplayFilter failed:", err)
- }
-
- buf := []byte("This is a test of the Emergency Broadcast System.")
- var now int64 = 3600
-
- // testAndSet into empty filter, returns false (not present).
- set := f.testAndSet(now, buf)
- if set {
- t.Fatal("testAndSet empty filter returned true")
- }
-
- // testAndSet into filter containing entry, should return true(present).
- set = f.testAndSet(now, buf)
- if !set {
- t.Fatal("testAndSet populated filter (replayed) returned false")
- }
-
- buf2 := []byte("This concludes this test of the Emergency Broadcast System.")
- now += 3600 * 2
-
- // testAndSet with time advanced.
- set = f.testAndSet(now, buf2)
- if set {
- t.Fatal("testAndSet populated filter, 2nd entry returned true")
- }
- set = f.testAndSet(now, buf2)
- if !set {
- t.Fatal("testAndSet populated filter, 2nd entry (replayed) returned false")
- }
-
- // Ensure that the first entry has been removed by compact.
- set = f.testAndSet(now, buf)
- if set {
- t.Fatal("testAndSet populated filter, compact check returned true")
- }
-
- // Ensure that the filter gets reaped if the clock jumps backwards.
- now = 0
- set = f.testAndSet(now, buf)
- if set {
- t.Fatal("testAndSet populated filter, backward time jump returned true")
- }
- if len(f.filter) != 1 {
- t.Fatal("filter map has a unexpected number of entries:", len(f.filter))
- }
- if f.fifo.Len() != 1 {
- t.Fatal("filter fifo has a unexpected number of entries:", f.fifo.Len())
- }
-
- // Ensure that the entry is properly added after reaping.
- set = f.testAndSet(now, buf)
- if !set {
- t.Fatal("testAndSet populated filter, post-backward clock jump (replayed) returned false")
- }
-}
diff --git a/transports/base/base.go b/transports/base/base.go
new file mode 100644
index 0000000..e81ea03
--- /dev/null
+++ b/transports/base/base.go
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Package base provides the common interface that each supported transport
+// protocol must implement.
+package base
+
+import (
+ "net"
+
+ "git.torproject.org/pluggable-transports/goptlib.git"
+)
+
+// ClientFactory is the interface that defines the factory for creating
+// pluggable transport protocol client instances.
+type ClientFactory interface {
+ // Transport returns the Transport instance that this ClientFactory belongs
+ // to.
+ Transport() Transport
+
+ // ParseArgs parses the supplied arguments into an internal representation
+ // for use with WrapConn. This routine is called before the outgoing
+ // TCP/IP connection is created to allow doing things (like keypair
+ // generation) to be hidden from third parties.
+ ParseArgs(args *pt.Args) (interface{}, error)
+
+ // WrapConn wraps the provided net.Conn with a transport protocol
+ // implementation, and does whatever is required (eg: handshaking) to get
+ // the connection to a point where it is ready to relay data.
+ WrapConn(conn net.Conn, args interface{}) (net.Conn, error)
+}
+
+// ServerFactory is the interface that defines the factory for creating
+// plugable transport protocol server instances. As the arguments are the
+// property of the factory, validation is done at factory creation time.
+type ServerFactory interface {
+ // Transport returns the Transport instance that this ServerFactory belongs
+ // to.
+ Transport() Transport
+
+ // Args returns the Args required on the client side to handshake with
+ // server connections created by this factory.
+ Args() *pt.Args
+
+ // WrapConn wraps the provided net.Conn with a transport protocol
+ // implementation, and does whatever is required (eg: handshaking) to get
+ // the connection to a point where it is ready to relay data.
+ WrapConn(conn net.Conn) (net.Conn, error)
+}
+
+// Transport is an interface that defines a pluggable transport protocol.
+type Transport interface {
+ // Name returns the name of the transport protocol. It MUST be a valid C
+ // identifier.
+ Name() string
+
+ // ClientFactory returns a ClientFactory instance for this transport
+ // protocol.
+ ClientFactory(stateDir string) (ClientFactory, error)
+
+ // ServerFactory returns a ServerFactory instance for this transport
+ // protocol. This can fail if the provided arguments are invalid.
+ ServerFactory(stateDir string, args *pt.Args) (ServerFactory, error)
+}
diff --git a/transports/obfs2/obfs2.go b/transports/obfs2/obfs2.go
new file mode 100644
index 0000000..3490646
--- /dev/null
+++ b/transports/obfs2/obfs2.go
@@ -0,0 +1,367 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Package obfs2 provides an implementation of the Tor Project's obfs2
+// obfuscation protocol. This protocol is considered trivially broken by most
+// sophisticated adversaries.
+package obfs2
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/sha256"
+ "encoding/binary"
+ "fmt"
+ "io"
+ "net"
+ "time"
+
+ "git.torproject.org/pluggable-transports/goptlib.git"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
+ "git.torproject.org/pluggable-transports/obfs4.git/transports/base"
+)
+
+const (
+ transportName = "obfs2"
+ sharedSecretArg = "shared-secret"
+
+ clientHandshakeTimeout = time.Duration(30) * time.Second
+ serverHandshakeTimeout = time.Duration(30) * time.Second
+
+ magicValue = 0x2bf5ca7e
+ initiatorPadString = "Initiator obfuscation padding"
+ responderPadString = "Responder obfuscation padding"
+ initiatorKdfString = "Initiator obfuscated data"
+ responderKdfString = "Responder obfuscated data"
+ maxPadding = 8192
+ keyLen = 16
+ seedLen = 16
+ hsLen = 4 + 4
+)
+
+func validateArgs(args *pt.Args) error {
+ if _, ok := args.Get(sharedSecretArg); ok {
+ // "shared-secret" is something no bridges use in practice and is thus
+ // unimplemented.
+ return fmt.Errorf("unsupported argument '%s'", sharedSecretArg)
+ }
+ return nil
+}
+
+// Transport is the obfs2 implementation of the base.Transport interface.
+type Transport struct{}
+
+// Name returns the name of the obfs2 transport protocol.
+func (t *Transport) Name() string {
+ return transportName
+}
+
+// ClientFactory returns a new obfs2ClientFactory instance.
+func (t *Transport) ClientFactory(stateDir string) (base.ClientFactory, error) {
+ cf := &obfs2ClientFactory{transport: t}
+ return cf, nil
+}
+
+// ServerFactory returns a new obfs2ServerFactory instance.
+func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFactory, error) {
+ if err := validateArgs(args); err != nil {
+ return nil, err
+ }
+
+ sf := &obfs2ServerFactory{t}
+ return sf, nil
+}
+
+type obfs2ClientFactory struct {
+ transport base.Transport
+}
+
+func (cf *obfs2ClientFactory) Transport() base.Transport {
+ return cf.transport
+}
+
+func (cf *obfs2ClientFactory) ParseArgs(args *pt.Args) (interface{}, error) {
+ return nil, validateArgs(args)
+}
+
+func (cf *obfs2ClientFactory) WrapConn(conn net.Conn, args interface{}) (net.Conn, error) {
+ return newObfs2ClientConn(conn)
+}
+
+type obfs2ServerFactory struct {
+ transport base.Transport
+}
+
+func (sf *obfs2ServerFactory) Transport() base.Transport {
+ return sf.transport
+}
+
+func (sf *obfs2ServerFactory) Args() *pt.Args {
+ return nil
+}
+
+func (sf *obfs2ServerFactory) WrapConn(conn net.Conn) (net.Conn, error) {
+ return newObfs2ServerConn(conn)
+}
+
+type obfs2Conn struct {
+ net.Conn
+
+ isInitiator bool
+
+ rx *cipher.StreamReader
+ tx *cipher.StreamWriter
+}
+
+func (conn *obfs2Conn) Read(b []byte) (int, error) {
+ return conn.rx.Read(b)
+}
+
+func (conn *obfs2Conn) Write(b []byte) (int, error) {
+ return conn.tx.Write(b)
+}
+
+func newObfs2ClientConn(conn net.Conn) (c *obfs2Conn, err error) {
+ // Initialize a client connection, and start the handshake timeout.
+ c = &obfs2Conn{conn, true, nil, nil}
+ deadline := time.Now().Add(clientHandshakeTimeout)
+ if err = c.SetDeadline(deadline); err != nil {
+ return nil, err
+ }
+
+ // Handshake.
+ if err = c.handshake(); err != nil {
+ return nil, err
+ }
+
+ // Disarm the handshake timer.
+ if err = c.SetDeadline(time.Time{}); err != nil {
+ return nil, err
+ }
+
+ return
+}
+
+func newObfs2ServerConn(conn net.Conn) (c *obfs2Conn, err error) {
+ // Initialize a server connection, and start the handshake timeout.
+ c = &obfs2Conn{conn, false, nil, nil}
+ deadline := time.Now().Add(serverHandshakeTimeout)
+ if err = c.SetDeadline(deadline); err != nil {
+ return nil, err
+ }
+
+ // Handshake.
+ if err = c.handshake(); err != nil {
+ return nil, err
+ }
+
+ // Disarm the handshake timer.
+ if err = c.SetDeadline(time.Time{}); err != nil {
+ return nil, err
+ }
+
+ return
+}
+
+func (conn *obfs2Conn) handshake() (err error) {
+ // Each begins by generating a seed and a padding key as follows.
+ // The initiator generates:
+ //
+ // INIT_SEED = SR(SEED_LENGTH)
+ // INIT_PAD_KEY = MAC("Initiator obfuscation padding", INIT_SEED)[:KEYLEN]
+ //
+ // And the responder generates:
+ //
+ // RESP_SEED = SR(SEED_LENGTH)
+ // RESP_PAD_KEY = MAC("Responder obfuscation padding", INIT_SEED)[:KEYLEN]
+ //
+ // Each then generates a random number PADLEN in range from 0 through
+ // MAX_PADDING (inclusive).
+ var seed [seedLen]byte
+ if err = csrand.Bytes(seed[:]); err != nil {
+ return
+ }
+ var padMagic []byte
+ if conn.isInitiator {
+ padMagic = []byte(initiatorPadString)
+ } else {
+ padMagic = []byte(responderPadString)
+ }
+ padKey, padIV := hsKdf(padMagic, seed[:], conn.isInitiator)
+ padLen := uint32(csrand.IntRange(0, maxPadding))
+
+ hsBlob := make([]byte, hsLen+padLen)
+ binary.BigEndian.PutUint32(hsBlob[0:4], magicValue)
+ binary.BigEndian.PutUint32(hsBlob[4:8], padLen)
+ if padLen > 0 {
+ if err = csrand.Bytes(hsBlob[8:]); err != nil {
+ return
+ }
+ }
+
+ // The initiator then sends:
+ //
+ // INIT_SEED | E(INIT_PAD_KEY, UINT32(MAGIC_VALUE) | UINT32(PADLEN) | WR(PADLEN))
+ //
+ // and the responder sends:
+ //
+ // RESP_SEED | E(RESP_PAD_KEY, UINT32(MAGIC_VALUE) | UINT32(PADLEN) | WR(PADLEN))
+ var txBlock cipher.Block
+ if txBlock, err = aes.NewCipher(padKey); err != nil {
+ return
+ }
+ txStream := cipher.NewCTR(txBlock, padIV)
+ conn.tx = &cipher.StreamWriter{txStream, conn.Conn, nil}
+ if _, err = conn.Conn.Write(seed[:]); err != nil {
+ return
+ }
+ if _, err = conn.Write(hsBlob); err != nil {
+ return
+ }
+
+ // Upon receiving the SEED from the other party, each party derives
+ // the other party's padding key value as above, and decrypts the next
+ // 8 bytes of the key establishment message.
+ var peerSeed [seedLen]byte
+ if _, err = io.ReadFull(conn.Conn, peerSeed[:]); err != nil {
+ return
+ }
+ var peerPadMagic []byte
+ if conn.isInitiator {
+ peerPadMagic = []byte(responderPadString)
+ } else {
+ peerPadMagic = []byte(initiatorPadString)
+ }
+ peerKey, peerIV := hsKdf(peerPadMagic, peerSeed[:], !conn.isInitiator)
+ var rxBlock cipher.Block
+ if rxBlock, err = aes.NewCipher(peerKey); err != nil {
+ return
+ }
+ rxStream := cipher.NewCTR(rxBlock, peerIV)
+ conn.rx = &cipher.StreamReader{rxStream, conn.Conn}
+ hsHdr := make([]byte, hsLen)
+ if _, err = io.ReadFull(conn, hsHdr[:]); err != nil {
+ return
+ }
+
+ // If the MAGIC_VALUE does not match, or the PADLEN value is greater than
+ // MAX_PADDING, the party receiving it should close the connection
+ // immediately.
+ if peerMagic := binary.BigEndian.Uint32(hsHdr[0:4]); peerMagic != magicValue {
+ err = fmt.Errorf("invalid magic value: %x", peerMagic)
+ return
+ }
+ padLen = binary.BigEndian.Uint32(hsHdr[4:8])
+ if padLen > maxPadding {
+ err = fmt.Errorf("padlen too long: %d", padLen)
+ return
+ }
+
+ // Otherwise, it should read the remaining PADLEN bytes of padding data
+ // and discard them.
+ tmp := make([]byte, padLen)
+ if _, err = io.ReadFull(conn.Conn, tmp); err != nil { // Note: Skips AES.
+ return
+ }
+
+ // Derive the actual keys.
+ if err = conn.kdf(seed[:], peerSeed[:]); err != nil {
+ return
+ }
+
+ return
+}
+
+func (conn *obfs2Conn) kdf(seed, peerSeed []byte) (err error) {
+ // Additional keys are then derived as:
+ //
+ // INIT_SECRET = MAC("Initiator obfuscated data", INIT_SEED|RESP_SEED)
+ // RESP_SECRET = MAC("Responder obfuscated data", INIT_SEED|RESP_SEED)
+ // INIT_KEY = INIT_SECRET[:KEYLEN]
+ // INIT_IV = INIT_SECRET[KEYLEN:]
+ // RESP_KEY = RESP_SECRET[:KEYLEN]
+ // RESP_IV = RESP_SECRET[KEYLEN:]
+ combSeed := make([]byte, 0, seedLen*2)
+ if conn.isInitiator {
+ combSeed = append(combSeed, seed...)
+ combSeed = append(combSeed, peerSeed...)
+ } else {
+ combSeed = append(combSeed, peerSeed...)
+ combSeed = append(combSeed, seed...)
+ }
+
+ initKey, initIV := hsKdf([]byte(initiatorKdfString), combSeed, true)
+ var initBlock cipher.Block
+ if initBlock, err = aes.NewCipher(initKey); err != nil {
+ return
+ }
+ initStream := cipher.NewCTR(initBlock, initIV)
+
+ respKey, respIV := hsKdf([]byte(responderKdfString), combSeed, false)
+ var respBlock cipher.Block
+ if respBlock, err = aes.NewCipher(respKey); err != nil {
+ return
+ }
+ respStream := cipher.NewCTR(respBlock, respIV)
+
+ if conn.isInitiator {
+ conn.tx.S = initStream
+ conn.rx.S = respStream
+ } else {
+ conn.tx.S = respStream
+ conn.rx.S = initStream
+ }
+
+ return
+}
+
+func hsKdf(magic, seed []byte, isInitiator bool) (padKey, padIV []byte) {
+ // The actual key/IV is derived in the form of:
+ // m = MAC(magic, seed)
+ // KEY = m[:KEYLEN]
+ // IV = m[KEYLEN:]
+ m := mac(magic, seed)
+ padKey = m[:keyLen]
+ padIV = m[keyLen:]
+
+ return
+}
+
+func mac(s, x []byte) []byte {
+ // H(x) is SHA256 of x.
+ // MAC(s, x) = H(s | x | s)
+ h := sha256.New()
+ h.Write(s)
+ h.Write(x)
+ h.Write(s)
+ return h.Sum(nil)
+}
+
+var _ base.ClientFactory = (*obfs2ClientFactory)(nil)
+var _ base.ServerFactory = (*obfs2ServerFactory)(nil)
+var _ base.Transport = (*Transport)(nil)
+var _ net.Conn = (*obfs2Conn)(nil)
diff --git a/transports/obfs3/obfs3.go b/transports/obfs3/obfs3.go
new file mode 100644
index 0000000..7844443
--- /dev/null
+++ b/transports/obfs3/obfs3.go
@@ -0,0 +1,358 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Package obfs3 provides an implementation of the Tor Project's obfs3
+// obfuscation protocol.
+package obfs3
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/hmac"
+ "crypto/sha256"
+ "errors"
+ "io"
+ "net"
+ "time"
+
+ "git.torproject.org/pluggable-transports/goptlib.git"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/uniformdh"
+ "git.torproject.org/pluggable-transports/obfs4.git/transports/base"
+)
+
+const (
+ transportName = "obfs3"
+
+ clientHandshakeTimeout = time.Duration(30) * time.Second
+ serverHandshakeTimeout = time.Duration(30) * time.Second
+
+ initiatorKdfString = "Initiator obfuscated data"
+ responderKdfString = "Responder obfuscated data"
+ initiatorMagicString = "Initiator magic"
+ responderMagicString = "Responder magic"
+ maxPadding = 8194
+ keyLen = 16
+)
+
+// Transport is the obfs3 implementation of the base.Transport interface.
+type Transport struct{}
+
+// Name returns the name of the obfs3 transport protocol.
+func (t *Transport) Name() string {
+ return transportName
+}
+
+// ClientFactory returns a new obfs3ClientFactory instance.
+func (t *Transport) ClientFactory(stateDir string) (base.ClientFactory, error) {
+ cf := &obfs3ClientFactory{transport: t}
+ return cf, nil
+}
+
+// ServerFactory returns a new obfs3ServerFactory instance.
+func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFactory, error) {
+ sf := &obfs3ServerFactory{transport: t}
+ return sf, nil
+}
+
+type obfs3ClientFactory struct {
+ transport base.Transport
+}
+
+func (cf *obfs3ClientFactory) Transport() base.Transport {
+ return cf.transport
+}
+
+func (cf *obfs3ClientFactory) ParseArgs(args *pt.Args) (interface{}, error) {
+ return nil, nil
+}
+
+func (cf *obfs3ClientFactory) WrapConn(conn net.Conn, args interface{}) (net.Conn, error) {
+ return newObfs3ClientConn(conn)
+}
+
+type obfs3ServerFactory struct {
+ transport base.Transport
+}
+
+func (sf *obfs3ServerFactory) Transport() base.Transport {
+ return sf.transport
+}
+
+func (sf *obfs3ServerFactory) Args() *pt.Args {
+ return nil
+}
+
+func (sf *obfs3ServerFactory) WrapConn(conn net.Conn) (net.Conn, error) {
+ return newObfs3ServerConn(conn)
+}
+
+type obfs3Conn struct {
+ net.Conn
+
+ isInitiator bool
+ rxMagic []byte
+ txMagic []byte
+ rxBuf *bytes.Buffer
+
+ rx *cipher.StreamReader
+ tx *cipher.StreamWriter
+}
+
+func newObfs3ClientConn(conn net.Conn) (c *obfs3Conn, err error) {
+ // Initialize a client connection, and start the handshake timeout.
+ c = &obfs3Conn{conn, true, nil, nil, new(bytes.Buffer), nil, nil}
+ deadline := time.Now().Add(clientHandshakeTimeout)
+ if err = c.SetDeadline(deadline); err != nil {
+ return nil, err
+ }
+
+ // Handshake.
+ if err = c.handshake(); err != nil {
+ return nil, err
+ }
+
+ // Disarm the handshake timer.
+ if err = c.SetDeadline(time.Time{}); err != nil {
+ return nil, err
+ }
+
+ return
+}
+
+func newObfs3ServerConn(conn net.Conn) (c *obfs3Conn, err error) {
+ // Initialize a server connection, and start the handshake timeout.
+ c = &obfs3Conn{conn, false, nil, nil, new(bytes.Buffer), nil, nil}
+ deadline := time.Now().Add(serverHandshakeTimeout)
+ if err = c.SetDeadline(deadline); err != nil {
+ return nil, err
+ }
+
+ // Handshake.
+ if err = c.handshake(); err != nil {
+ return nil, err
+ }
+
+ // Disarm the handshake timer.
+ if err = c.SetDeadline(time.Time{}); err != nil {
+ return nil, err
+ }
+
+ return
+}
+
+func (conn *obfs3Conn) handshake() (err error) {
+ // The party who opens the connection is the 'initiator'; the one who
+ // accepts it is the 'responder'. Each begins by generating a
+ // UniformDH keypair, and a random number PADLEN in [0, MAX_PADDING/2].
+ // Both parties then send:
+ //
+ // PUB_KEY | WR(PADLEN)
+ var privateKey *uniformdh.PrivateKey
+ if privateKey, err = uniformdh.GenerateKey(csrand.Reader); err != nil {
+ return
+ }
+ padLen := csrand.IntRange(0, maxPadding/2)
+ blob := make([]byte, uniformdh.Size+padLen)
+ var publicKey []byte
+ if publicKey, err = privateKey.PublicKey.Bytes(); err != nil {
+ return
+ }
+ copy(blob[0:], publicKey)
+ if err = csrand.Bytes(blob[uniformdh.Size:]); err != nil {
+ return
+ }
+ if _, err = conn.Conn.Write(blob); err != nil {
+ return
+ }
+
+ // Read the public key from the peer.
+ rawPeerPublicKey := make([]byte, uniformdh.Size)
+ if _, err = io.ReadFull(conn.Conn, rawPeerPublicKey); err != nil {
+ return
+ }
+ var peerPublicKey uniformdh.PublicKey
+ if err = peerPublicKey.SetBytes(rawPeerPublicKey); err != nil {
+ return
+ }
+
+ // After retrieving the public key of the other end, each party
+ // completes the DH key exchange and generates a shared-secret for the
+ // session (named SHARED_SECRET).
+ var sharedSecret []byte
+ if sharedSecret, err = uniformdh.Handshake(privateKey, &peerPublicKey); err != nil {
+ return
+ }
+ if err = conn.kdf(sharedSecret); err != nil {
+ return
+ }
+
+ return
+}
+
+func (conn *obfs3Conn) kdf(sharedSecret []byte) (err error) {
+ // Using that shared-secret each party derives its encryption keys as
+ // follows:
+ //
+ // INIT_SECRET = HMAC(SHARED_SECRET, "Initiator obfuscated data")
+ // RESP_SECRET = HMAC(SHARED_SECRET, "Responder obfuscated data")
+ // INIT_KEY = INIT_SECRET[:KEYLEN]
+ // INIT_COUNTER = INIT_SECRET[KEYLEN:]
+ // RESP_KEY = RESP_SECRET[:KEYLEN]
+ // RESP_COUNTER = RESP_SECRET[KEYLEN:]
+ initHmac := hmac.New(sha256.New, sharedSecret)
+ initHmac.Write([]byte(initiatorKdfString))
+ initSecret := initHmac.Sum(nil)
+ initHmac.Reset()
+ initHmac.Write([]byte(initiatorMagicString))
+ initMagic := initHmac.Sum(nil)
+
+ respHmac := hmac.New(sha256.New, sharedSecret)
+ respHmac.Write([]byte(responderKdfString))
+ respSecret := respHmac.Sum(nil)
+ respHmac.Reset()
+ respHmac.Write([]byte(responderMagicString))
+ respMagic := respHmac.Sum(nil)
+
+ // The INIT_KEY value keys a block cipher (in CTR mode) used to
+ // encrypt values from initiator to responder thereafter. The counter
+ // mode's initial counter value is INIT_COUNTER. The RESP_KEY value
+ // keys a block cipher (in CTR mode) used to encrypt values from
+ // responder to initiator thereafter. That counter mode's initial
+ // counter value is RESP_COUNTER.
+ //
+ // Note: To have this be the last place where the shared secret is used,
+ // also generate the magic value to send/scan for here.
+ var initBlock cipher.Block
+ if initBlock, err = aes.NewCipher(initSecret[:keyLen]); err != nil {
+ return err
+ }
+ initStream := cipher.NewCTR(initBlock, initSecret[keyLen:])
+
+ var respBlock cipher.Block
+ if respBlock, err = aes.NewCipher(respSecret[:keyLen]); err != nil {
+ return err
+ }
+ respStream := cipher.NewCTR(respBlock, respSecret[keyLen:])
+
+ if conn.isInitiator {
+ conn.tx = &cipher.StreamWriter{initStream, conn.Conn, nil}
+ conn.rx = &cipher.StreamReader{respStream, conn.rxBuf}
+ conn.txMagic = initMagic
+ conn.rxMagic = respMagic
+ } else {
+ conn.tx = &cipher.StreamWriter{respStream, conn.Conn, nil}
+ conn.rx = &cipher.StreamReader{initStream, conn.rxBuf}
+ conn.txMagic = respMagic
+ conn.rxMagic = initMagic
+ }
+
+ return
+}
+
+func (conn *obfs3Conn) findPeerMagic() error {
+ var hsBuf [maxPadding + sha256.Size]byte
+ for {
+ n, err := conn.Conn.Read(hsBuf[:])
+ if err != nil {
+ // Yes, Read can return partial data and an error, but continuing
+ // past that is nonsensical.
+ return err
+ }
+ conn.rxBuf.Write(hsBuf[:n])
+
+ pos := bytes.Index(conn.rxBuf.Bytes(), conn.rxMagic)
+ if pos == -1 {
+ if conn.rxBuf.Len() >= maxPadding+sha256.Size {
+ return errors.New("failed to find peer magic value")
+ }
+ continue
+ } else if pos > maxPadding {
+ return errors.New("peer sent too much pre-magic-padding")
+ }
+
+ // Discard the padding/MAC.
+ pos += len(conn.rxMagic)
+ _ = conn.rxBuf.Next(pos)
+
+ return nil
+ }
+}
+
+func (conn *obfs3Conn) Read(b []byte) (n int, err error) {
+ // If this is the first time we read data post handshake, scan for the
+ // magic value.
+ if conn.rxMagic != nil {
+ if err = conn.findPeerMagic(); err != nil {
+ conn.Close()
+ return
+ }
+ conn.rxMagic = nil
+ }
+
+ // If the handshake receive buffer is still present...
+ if conn.rxBuf != nil {
+ // And it is empty...
+ if conn.rxBuf.Len() == 0 {
+ // There is no more trailing data left from the handshake process,
+ // so rewire the cipher.StreamReader to pull data from the network
+ // instead of the temporary receive buffer.
+ conn.rx.R = conn.Conn
+ conn.rxBuf = nil
+ }
+ }
+
+ return conn.rx.Read(b)
+}
+
+func (conn *obfs3Conn) Write(b []byte) (n int, err error) {
+ // If this is the first time we write data post handshake, send the
+ // padding/magic value.
+ if conn.txMagic != nil {
+ padLen := csrand.IntRange(0, maxPadding/2)
+ blob := make([]byte, padLen+len(conn.txMagic))
+ if err = csrand.Bytes(blob[:padLen]); err != nil {
+ conn.Close()
+ return
+ }
+ copy(blob[padLen:], conn.txMagic)
+ if _, err = conn.Conn.Write(blob); err != nil {
+ conn.Close()
+ return
+ }
+ conn.txMagic = nil
+ }
+
+ return conn.tx.Write(b)
+}
+
+
+var _ base.ClientFactory = (*obfs3ClientFactory)(nil)
+var _ base.ServerFactory = (*obfs3ServerFactory)(nil)
+var _ base.Transport = (*Transport)(nil)
+var _ net.Conn = (*obfs3Conn)(nil)
diff --git a/transports/obfs4/framing/framing.go b/transports/obfs4/framing/framing.go
new file mode 100644
index 0000000..04e788f
--- /dev/null
+++ b/transports/obfs4/framing/framing.go
@@ -0,0 +1,308 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+//
+// Package framing implements the obfs4 link framing and cryptography.
+//
+// The Encoder/Decoder shared secret format is:
+// uint8_t[32] NaCl secretbox key
+// uint8_t[16] NaCl Nonce prefix
+// uint8_t[16] SipHash-2-4 key (used to obfsucate length)
+// uint8_t[8] SipHash-2-4 IV
+//
+// The frame format is:
+// uint16_t length (obfsucated, big endian)
+// NaCl secretbox (Poly1305/XSalsa20) containing:
+// uint8_t[16] tag (Part of the secretbox construct)
+// uint8_t[] payload
+//
+// The length field is length of the NaCl secretbox XORed with the truncated
+// SipHash-2-4 digest ran in OFB mode.
+//
+// Initialize K, IV[0] with values from the shared secret.
+// On each packet, IV[n] = H(K, IV[n - 1])
+// mask[n] = IV[n][0:2]
+// obfsLen = length ^ mask[n]
+//
+// The NaCl secretbox (Poly1305/XSalsa20) nonce format is:
+// uint8_t[24] prefix (Fixed)
+// uint64_t counter (Big endian)
+//
+// The counter is initialized to 1, and is incremented on each frame. Since
+// the protocol is designed to be used over a reliable medium, the nonce is not
+// transmitted over the wire as both sides of the conversation know the prefix
+// and the initial counter value. It is imperative that the counter does not
+// wrap, and sessions MUST terminate before 2^64 frames are sent.
+//
+package framing
+
+import (
+ "bytes"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+
+ "code.google.com/p/go.crypto/nacl/secretbox"
+
+ "git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/drbg"
+)
+
+const (
+ // MaximumSegmentLength is the length of the largest possible segment
+ // including overhead.
+ MaximumSegmentLength = 1500 - (40 + 12)
+
+ // FrameOverhead is the length of the framing overhead.
+ FrameOverhead = lengthLength + secretbox.Overhead
+
+ // MaximumFramePayloadLength is the length of the maximum allowed payload
+ // per frame.
+ MaximumFramePayloadLength = MaximumSegmentLength - FrameOverhead
+
+ // KeyLength is the length of the Encoder/Decoder secret key.
+ KeyLength = keyLength + noncePrefixLength + drbg.SeedLength
+
+ maxFrameLength = MaximumSegmentLength - lengthLength
+ minFrameLength = FrameOverhead - lengthLength
+
+ keyLength = 32
+
+ noncePrefixLength = 16
+ nonceCounterLength = 8
+ nonceLength = noncePrefixLength + nonceCounterLength
+
+ lengthLength = 2
+)
+
+// Error returned when Decoder.Decode() requires more data to continue.
+var ErrAgain = errors.New("framing: More data needed to decode")
+
+// Error returned when Decoder.Decode() failes to authenticate a frame.
+var ErrTagMismatch = errors.New("framing: Poly1305 tag mismatch")
+
+// Error returned when the NaCl secretbox nonce's counter wraps (FATAL).
+var ErrNonceCounterWrapped = errors.New("framing: Nonce counter wrapped")
+
+// InvalidPayloadLengthError is the error returned when Encoder.Encode()
+// rejects the payload length.
+type InvalidPayloadLengthError int
+
+func (e InvalidPayloadLengthError) Error() string {
+ return fmt.Sprintf("framing: Invalid payload length: %d", int(e))
+}
+
+type boxNonce struct {
+ prefix [noncePrefixLength]byte
+ counter uint64
+}
+
+func (nonce *boxNonce) init(prefix []byte) {
+ if noncePrefixLength != len(prefix) {
+ panic(fmt.Sprintf("BUG: Nonce prefix length invalid: %d", len(prefix)))
+ }
+
+ copy(nonce.prefix[:], prefix)
+ nonce.counter = 1
+}
+
+func (nonce boxNonce) bytes(out *[nonceLength]byte) error {
+ // The security guarantee of Poly1305 is broken if a nonce is ever reused
+ // for a given key. Detect this by checking for counter wraparound since
+ // we start each counter at 1. If it ever happens that more than 2^64 - 1
+ // frames are transmitted over a given connection, support for rekeying
+ // will be neccecary, but that's unlikely to happen.
+ if nonce.counter == 0 {
+ return ErrNonceCounterWrapped
+ }
+
+ copy(out[:], nonce.prefix[:])
+ binary.BigEndian.PutUint64(out[noncePrefixLength:], nonce.counter)
+
+ return nil
+}
+
+// Encoder is a frame encoder instance.
+type Encoder struct {
+ key [keyLength]byte
+ nonce boxNonce
+ drbg *drbg.HashDrbg
+}
+
+// NewEncoder creates a new Encoder instance. It must be supplied a slice
+// containing exactly KeyLength bytes of keying material.
+func NewEncoder(key []byte) *Encoder {
+ if len(key) != KeyLength {
+ panic(fmt.Sprintf("BUG: Invalid encoder key length: %d", len(key)))
+ }
+
+ encoder := new(Encoder)
+ copy(encoder.key[:], key[0:keyLength])
+ encoder.nonce.init(key[keyLength : keyLength+noncePrefixLength])
+ seed, err := drbg.SeedFromBytes(key[keyLength+noncePrefixLength:])
+ if err != nil {
+ panic(fmt.Sprintf("BUG: Failed to initialize DRBG: %s", err))
+ }
+ encoder.drbg, _ = drbg.NewHashDrbg(seed)
+
+ return encoder
+}
+
+// Encode encodes a single frame worth of payload and returns the encoded
+// length. InvalidPayloadLengthError is recoverable, all other errors MUST be
+// treated as fatal and the session aborted.
+func (encoder *Encoder) Encode(frame, payload []byte) (n int, err error) {
+ payloadLen := len(payload)
+ if MaximumFramePayloadLength < payloadLen {
+ return 0, InvalidPayloadLengthError(payloadLen)
+ }
+ if len(frame) < payloadLen+FrameOverhead {
+ return 0, io.ErrShortBuffer
+ }
+
+ // Generate a new nonce.
+ var nonce [nonceLength]byte
+ err = encoder.nonce.bytes(&nonce)
+ if err != nil {
+ return 0, err
+ }
+ encoder.nonce.counter++
+
+ // Encrypt and MAC payload.
+ box := secretbox.Seal(frame[:lengthLength], payload, &nonce, &encoder.key)
+
+ // Obfuscate the length.
+ length := uint16(len(box) - lengthLength)
+ lengthMask := encoder.drbg.NextBlock()
+ length ^= binary.BigEndian.Uint16(lengthMask)
+ binary.BigEndian.PutUint16(frame[:2], length)
+
+ // Return the frame.
+ return len(box), nil
+}
+
+// Decoder is a frame decoder instance.
+type Decoder struct {
+ key [keyLength]byte
+ nonce boxNonce
+ drbg *drbg.HashDrbg
+
+ nextNonce [nonceLength]byte
+ nextLength uint16
+ nextLengthInvalid bool
+}
+
+// NewDecoder creates a new Decoder instance. It must be supplied a slice
+// containing exactly KeyLength bytes of keying material.
+func NewDecoder(key []byte) *Decoder {
+ if len(key) != KeyLength {
+ panic(fmt.Sprintf("BUG: Invalid decoder key length: %d", len(key)))
+ }
+
+ decoder := new(Decoder)
+ copy(decoder.key[:], key[0:keyLength])
+ decoder.nonce.init(key[keyLength : keyLength+noncePrefixLength])
+ seed, err := drbg.SeedFromBytes(key[keyLength+noncePrefixLength:])
+ if err != nil {
+ panic(fmt.Sprintf("BUG: Failed to initialize DRBG: %s", err))
+ }
+ decoder.drbg, _ = drbg.NewHashDrbg(seed)
+
+ return decoder
+}
+
+// Decode decodes a stream of data and returns the length if any. ErrAgain is
+// a temporary failure, all other errors MUST be treated as fatal and the
+// session aborted.
+func (decoder *Decoder) Decode(data []byte, frames *bytes.Buffer) (int, error) {
+ // A length of 0 indicates that we do not know how big the next frame is
+ // going to be.
+ if decoder.nextLength == 0 {
+ // Attempt to pull out the next frame length.
+ if lengthLength > frames.Len() {
+ return 0, ErrAgain
+ }
+
+ // Remove the length field from the buffer.
+ var obfsLen [lengthLength]byte
+ _, err := io.ReadFull(frames, obfsLen[:])
+ if err != nil {
+ return 0, err
+ }
+
+ // Derive the nonce the peer used.
+ err = decoder.nonce.bytes(&decoder.nextNonce)
+ if err != nil {
+ return 0, err
+ }
+
+ // Deobfuscate the length field.
+ length := binary.BigEndian.Uint16(obfsLen[:])
+ lengthMask := decoder.drbg.NextBlock()
+ length ^= binary.BigEndian.Uint16(lengthMask)
+ if maxFrameLength < length || minFrameLength > length {
+ // Per "Plaintext Recovery Attacks Against SSH" by
+ // Martin R. Albrecht, Kenneth G. Paterson and Gaven J. Watson,
+ // there are a class of attacks againt protocols that use similar
+ // sorts of framing schemes.
+ //
+ // While obfs4 should not allow plaintext recovery (CBC mode is
+ // not used), attempt to mitigate out of bound frame length errors
+ // by pretending that the length was a random valid range as per
+ // the countermeasure suggested by Denis Bider in section 6 of the
+ // paper.
+
+ decoder.nextLengthInvalid = true
+ length = uint16(csrand.IntRange(minFrameLength, maxFrameLength))
+ }
+ decoder.nextLength = length
+ }
+
+ if int(decoder.nextLength) > frames.Len() {
+ return 0, ErrAgain
+ }
+
+ // Unseal the frame.
+ var box [maxFrameLength]byte
+ n, err := io.ReadFull(frames, box[:decoder.nextLength])
+ if err != nil {
+ return 0, err
+ }
+ out, ok := secretbox.Open(data[:0], box[:n], &decoder.nextNonce, &decoder.key)
+ if !ok || decoder.nextLengthInvalid {
+ // When a random length is used (on length error) the tag should always
+ // mismatch, but be paranoid.
+ return 0, ErrTagMismatch
+ }
+
+ // Clean up and prepare for the next frame.
+ decoder.nextLength = 0
+ decoder.nonce.counter++
+
+ return len(out), nil
+}
diff --git a/transports/obfs4/framing/framing_test.go b/transports/obfs4/framing/framing_test.go
new file mode 100644
index 0000000..03e0d3b
--- /dev/null
+++ b/transports/obfs4/framing/framing_test.go
@@ -0,0 +1,169 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package framing
+
+import (
+ "bytes"
+ "crypto/rand"
+ "testing"
+)
+
+func generateRandomKey() []byte {
+ key := make([]byte, KeyLength)
+
+ _, err := rand.Read(key)
+ if err != nil {
+ panic(err)
+ }
+
+ return key
+}
+
+func newEncoder(t *testing.T) *Encoder {
+ // Generate a key to use.
+ key := generateRandomKey()
+
+ encoder := NewEncoder(key)
+ if encoder == nil {
+ t.Fatalf("NewEncoder returned nil")
+ }
+
+ return encoder
+}
+
+// TestNewEncoder tests the Encoder ctor.
+func TestNewEncoder(t *testing.T) {
+ encoder := newEncoder(t)
+ _ = encoder
+}
+
+// TestEncoder_Encode tests Encoder.Encode.
+func TestEncoder_Encode(t *testing.T) {
+ encoder := newEncoder(t)
+
+ buf := make([]byte, MaximumFramePayloadLength)
+ _, _ = rand.Read(buf) // YOLO
+ for i := 0; i <= MaximumFramePayloadLength; i++ {
+ var frame [MaximumSegmentLength]byte
+ n, err := encoder.Encode(frame[:], buf[0:i])
+ if err != nil {
+ t.Fatalf("Encoder.encode([%d]byte), failed: %s", i, err)
+ }
+ if n != i+FrameOverhead {
+ t.Fatalf("Unexpected encoded framesize: %d, expecting %d", n, i+
+ FrameOverhead)
+ }
+ }
+}
+
+// TestEncoder_Encode_Oversize tests oversized frame rejection.
+func TestEncoder_Encode_Oversize(t *testing.T) {
+ encoder := newEncoder(t)
+
+ var frame [MaximumSegmentLength]byte
+ var buf [MaximumFramePayloadLength + 1]byte
+ _, _ = rand.Read(buf[:]) // YOLO
+ _, err := encoder.Encode(frame[:], buf[:])
+ if _, ok := err.(InvalidPayloadLengthError); !ok {
+ t.Error("Encoder.encode() returned unexpected error:", err)
+ }
+}
+
+// TestNewDecoder tests the Decoder ctor.
+func TestNewDecoder(t *testing.T) {
+ key := generateRandomKey()
+ decoder := NewDecoder(key)
+ if decoder == nil {
+ t.Fatalf("NewDecoder returned nil")
+ }
+}
+
+// TestDecoder_Decode tests Decoder.Decode.
+func TestDecoder_Decode(t *testing.T) {
+ key := generateRandomKey()
+
+ encoder := NewEncoder(key)
+ decoder := NewDecoder(key)
+
+ var buf [MaximumFramePayloadLength]byte
+ _, _ = rand.Read(buf[:]) // YOLO
+ for i := 0; i <= MaximumFramePayloadLength; i++ {
+ var frame [MaximumSegmentLength]byte
+ encLen, err := encoder.Encode(frame[:], buf[0:i])
+ if err != nil {
+ t.Fatalf("Encoder.encode([%d]byte), failed: %s", i, err)
+ }
+ if encLen != i+FrameOverhead {
+ t.Fatalf("Unexpected encoded framesize: %d, expecting %d", encLen,
+ i+FrameOverhead)
+ }
+
+ var decoded [MaximumFramePayloadLength]byte
+
+ decLen, err := decoder.Decode(decoded[:], bytes.NewBuffer(frame[:encLen]))
+ if err != nil {
+ t.Fatalf("Decoder.decode([%d]byte), failed: %s", i, err)
+ }
+ if decLen != i {
+ t.Fatalf("Unexpected decoded framesize: %d, expecting %d",
+ decLen, i)
+ }
+
+ if 0 != bytes.Compare(decoded[:decLen], buf[:i]) {
+ t.Fatalf("Frame %d does not match encoder input", i)
+ }
+ }
+}
+
+// BencharkEncoder_Encode benchmarks Encoder.Encode processing 1 MiB
+// of payload.
+func BenchmarkEncoder_Encode(b *testing.B) {
+ var chopBuf [MaximumFramePayloadLength]byte
+ var frame [MaximumSegmentLength]byte
+ payload := make([]byte, 1024*1024)
+ encoder := NewEncoder(generateRandomKey())
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ transfered := 0
+ buffer := bytes.NewBuffer(payload)
+ for 0 < buffer.Len() {
+ n, err := buffer.Read(chopBuf[:])
+ if err != nil {
+ b.Fatal("buffer.Read() failed:", err)
+ }
+
+ n, err = encoder.Encode(frame[:], chopBuf[:n])
+ transfered += n - FrameOverhead
+ }
+ if transfered != len(payload) {
+ b.Fatalf("Transfered length mismatch: %d != %d", transfered,
+ len(payload))
+ }
+ }
+}
diff --git a/transports/obfs4/handshake_ntor.go b/transports/obfs4/handshake_ntor.go
new file mode 100644
index 0000000..8dcf0c8
--- /dev/null
+++ b/transports/obfs4/handshake_ntor.go
@@ -0,0 +1,426 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package obfs4
+
+import (
+ "bytes"
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "hash"
+ "strconv"
+ "time"
+
+ "git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/ntor"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/replayfilter"
+ "git.torproject.org/pluggable-transports/obfs4.git/transports/obfs4/framing"
+)
+
+const (
+ maxHandshakeLength = 8192
+
+ clientMinPadLength = (serverMinHandshakeLength + inlineSeedFrameLength) -
+ clientMinHandshakeLength
+ clientMaxPadLength = maxHandshakeLength - clientMinHandshakeLength
+ clientMinHandshakeLength = ntor.RepresentativeLength + markLength + macLength
+
+ serverMinPadLength = 0
+ serverMaxPadLength = maxHandshakeLength - (serverMinHandshakeLength +
+ inlineSeedFrameLength)
+ serverMinHandshakeLength = ntor.RepresentativeLength + ntor.AuthLength +
+ markLength + macLength
+
+ markLength = sha256.Size / 2
+ macLength = sha256.Size / 2
+
+ inlineSeedFrameLength = framing.FrameOverhead + packetOverhead + seedPacketPayloadLength
+)
+
+// ErrMarkNotFoundYet is the error returned when the obfs4 handshake is
+// incomplete and requires more data to continue. This error is non-fatal and
+// is the equivalent to EAGAIN/EWOULDBLOCK.
+var ErrMarkNotFoundYet = errors.New("handshake: M_[C,S] not found yet")
+
+// ErrInvalidHandshake is the error returned when the obfs4 handshake fails due
+// to the peer not sending the correct mark. This error is fatal and the
+// connection MUST be dropped.
+var ErrInvalidHandshake = errors.New("handshake: Failed to find M_[C,S]")
+
+// ErrReplayedHandshake is the error returned when the obfs4 handshake fails
+// due it being replayed. This error is fatal and the connection MUST be
+// dropped.
+var ErrReplayedHandshake = errors.New("handshake: Replay detected")
+
+// ErrNtorFailed is the error returned when the ntor handshake fails. This
+// error is fatal and the connection MUST be dropped.
+var ErrNtorFailed = errors.New("handshake: ntor handshake failure")
+
+// InvalidMacError is the error returned when the handshake MACs do not match.
+// This error is fatal and the connection MUST be dropped.
+type InvalidMacError struct {
+ Derived []byte
+ Received []byte
+}
+
+func (e *InvalidMacError) Error() string {
+ return fmt.Sprintf("handshake: MAC mismatch: Dervied: %s Received: %s.",
+ hex.EncodeToString(e.Derived), hex.EncodeToString(e.Received))
+}
+
+// InvalidAuthError is the error returned when the ntor AUTH tags do not match.
+// This error is fatal and the connection MUST be dropped.
+type InvalidAuthError struct {
+ Derived *ntor.Auth
+ Received *ntor.Auth
+}
+
+func (e *InvalidAuthError) Error() string {
+ return fmt.Sprintf("handshake: ntor AUTH mismatch: Derived: %s Received:%s.",
+ hex.EncodeToString(e.Derived.Bytes()[:]),
+ hex.EncodeToString(e.Received.Bytes()[:]))
+}
+
+type clientHandshake struct {
+ keypair *ntor.Keypair
+ nodeID *ntor.NodeID
+ serverIdentity *ntor.PublicKey
+ epochHour []byte
+
+ padLen int
+ mac hash.Hash
+
+ serverRepresentative *ntor.Representative
+ serverAuth *ntor.Auth
+ serverMark []byte
+}
+
+func newClientHandshake(nodeID *ntor.NodeID, serverIdentity *ntor.PublicKey, sessionKey *ntor.Keypair) *clientHandshake {
+ hs := new(clientHandshake)
+ hs.keypair = sessionKey
+ hs.nodeID = nodeID
+ hs.serverIdentity = serverIdentity
+ hs.padLen = csrand.IntRange(clientMinPadLength, clientMaxPadLength)
+ hs.mac = hmac.New(sha256.New, append(hs.serverIdentity.Bytes()[:], hs.nodeID.Bytes()[:]...))
+
+ return hs
+}
+
+func (hs *clientHandshake) generateHandshake() ([]byte, error) {
+ var buf bytes.Buffer
+
+ hs.mac.Reset()
+ hs.mac.Write(hs.keypair.Representative().Bytes()[:])
+ mark := hs.mac.Sum(nil)[:markLength]
+
+ // The client handshake is X | P_C | M_C | MAC(X | P_C | M_C | E) where:
+ // * X is the client's ephemeral Curve25519 public key representative.
+ // * P_C is [clientMinPadLength,clientMaxPadLength] bytes of random padding.
+ // * M_C is HMAC-SHA256-128(serverIdentity | NodeID, X)
+ // * MAC is HMAC-SHA256-128(serverIdentity | NodeID, X .... E)
+ // * E is the string representation of the number of hours since the UNIX
+ // epoch.
+
+ // Generate the padding
+ pad, err := makePad(hs.padLen)
+ if err != nil {
+ return nil, err
+ }
+
+ // Write X, P_C, M_C.
+ buf.Write(hs.keypair.Representative().Bytes()[:])
+ buf.Write(pad)
+ buf.Write(mark)
+
+ // Calculate and write the MAC.
+ hs.mac.Reset()
+ hs.mac.Write(buf.Bytes())
+ hs.epochHour = []byte(strconv.FormatInt(getEpochHour(), 10))
+ hs.mac.Write(hs.epochHour)
+ buf.Write(hs.mac.Sum(nil)[:macLength])
+
+ return buf.Bytes(), nil
+}
+
+func (hs *clientHandshake) parseServerHandshake(resp []byte) (int, []byte, error) {
+ // No point in examining the data unless the miminum plausible response has
+ // been received.
+ if serverMinHandshakeLength > len(resp) {
+ return 0, nil, ErrMarkNotFoundYet
+ }
+
+ if hs.serverRepresentative == nil || hs.serverAuth == nil {
+ // Pull out the representative/AUTH. (XXX: Add ctors to ntor)
+ hs.serverRepresentative = new(ntor.Representative)
+ copy(hs.serverRepresentative.Bytes()[:], resp[0:ntor.RepresentativeLength])
+ hs.serverAuth = new(ntor.Auth)
+ copy(hs.serverAuth.Bytes()[:], resp[ntor.RepresentativeLength:])
+
+ // Derive the mark.
+ hs.mac.Reset()
+ hs.mac.Write(hs.serverRepresentative.Bytes()[:])
+ hs.serverMark = hs.mac.Sum(nil)[:markLength]
+ }
+
+ // Attempt to find the mark + MAC.
+ pos := findMarkMac(hs.serverMark, resp, ntor.RepresentativeLength+ntor.AuthLength+serverMinPadLength,
+ maxHandshakeLength, false)
+ if pos == -1 {
+ if len(resp) >= maxHandshakeLength {
+ return 0, nil, ErrInvalidHandshake
+ }
+ return 0, nil, ErrMarkNotFoundYet
+ }
+
+ // Validate the MAC.
+ hs.mac.Reset()
+ hs.mac.Write(resp[:pos+markLength])
+ hs.mac.Write(hs.epochHour)
+ macCmp := hs.mac.Sum(nil)[:macLength]
+ macRx := resp[pos+markLength : pos+markLength+macLength]
+ if !hmac.Equal(macCmp, macRx) {
+ return 0, nil, &InvalidMacError{macCmp, macRx}
+ }
+
+ // Complete the handshake.
+ serverPublic := hs.serverRepresentative.ToPublic()
+ ok, seed, auth := ntor.ClientHandshake(hs.keypair, serverPublic,
+ hs.serverIdentity, hs.nodeID)
+ if !ok {
+ return 0, nil, ErrNtorFailed
+ }
+ if !ntor.CompareAuth(auth, hs.serverAuth.Bytes()[:]) {
+ return 0, nil, &InvalidAuthError{auth, hs.serverAuth}
+ }
+
+ return pos + markLength + macLength, seed.Bytes()[:], nil
+}
+
+type serverHandshake struct {
+ keypair *ntor.Keypair
+ nodeID *ntor.NodeID
+ serverIdentity *ntor.Keypair
+ epochHour []byte
+ serverAuth *ntor.Auth
+
+ padLen int
+ mac hash.Hash
+
+ clientRepresentative *ntor.Representative
+ clientMark []byte
+}
+
+func newServerHandshake(nodeID *ntor.NodeID, serverIdentity *ntor.Keypair, sessionKey *ntor.Keypair) *serverHandshake {
+ hs := new(serverHandshake)
+ hs.keypair = sessionKey
+ hs.nodeID = nodeID
+ hs.serverIdentity = serverIdentity
+ hs.padLen = csrand.IntRange(serverMinPadLength, serverMaxPadLength)
+ hs.mac = hmac.New(sha256.New, append(hs.serverIdentity.Public().Bytes()[:], hs.nodeID.Bytes()[:]...))
+
+ return hs
+}
+
+func (hs *serverHandshake) parseClientHandshake(filter *replayfilter.ReplayFilter, resp []byte) ([]byte, error) {
+ // No point in examining the data unless the miminum plausible response has
+ // been received.
+ if clientMinHandshakeLength > len(resp) {
+ return nil, ErrMarkNotFoundYet
+ }
+
+ if hs.clientRepresentative == nil {
+ // Pull out the representative/AUTH. (XXX: Add ctors to ntor)
+ hs.clientRepresentative = new(ntor.Representative)
+ copy(hs.clientRepresentative.Bytes()[:], resp[0:ntor.RepresentativeLength])
+
+ // Derive the mark.
+ hs.mac.Reset()
+ hs.mac.Write(hs.clientRepresentative.Bytes()[:])
+ hs.clientMark = hs.mac.Sum(nil)[:markLength]
+ }
+
+ // Attempt to find the mark + MAC.
+ pos := findMarkMac(hs.clientMark, resp, ntor.RepresentativeLength+clientMinPadLength,
+ maxHandshakeLength, true)
+ if pos == -1 {
+ if len(resp) >= maxHandshakeLength {
+ return nil, ErrInvalidHandshake
+ }
+ return nil, ErrMarkNotFoundYet
+ }
+
+ // Validate the MAC.
+ macFound := false
+ for _, off := range []int64{0, -1, 1} {
+ // Allow epoch to be off by up to a hour in either direction.
+ epochHour := []byte(strconv.FormatInt(getEpochHour()+int64(off), 10))
+ hs.mac.Reset()
+ hs.mac.Write(resp[:pos+markLength])
+ hs.mac.Write(epochHour)
+ macCmp := hs.mac.Sum(nil)[:macLength]
+ macRx := resp[pos+markLength : pos+markLength+macLength]
+ if hmac.Equal(macCmp, macRx) {
+ // Ensure that this handshake has not been seen previously.
+ if filter.TestAndSet(time.Now(), macRx) {
+ // The client either happened to generate exactly the same
+ // session key and padding, or someone is replaying a previous
+ // handshake. In either case, fuck them.
+ return nil, ErrReplayedHandshake
+ }
+
+ macFound = true
+ hs.epochHour = epochHour
+
+ // We could break out here, but in the name of reducing timing
+ // variation, evaluate all 3 MACs.
+ }
+ }
+ if !macFound {
+ // This probably should be an InvalidMacError, but conveying the 3 MACS
+ // that would be accepted is annoying so just return a generic fatal
+ // failure.
+ return nil, ErrInvalidHandshake
+ }
+
+ // Client should never sent trailing garbage.
+ if len(resp) != pos+markLength+macLength {
+ return nil, ErrInvalidHandshake
+ }
+
+ clientPublic := hs.clientRepresentative.ToPublic()
+ ok, seed, auth := ntor.ServerHandshake(clientPublic, hs.keypair,
+ hs.serverIdentity, hs.nodeID)
+ if !ok {
+ return nil, ErrNtorFailed
+ }
+ hs.serverAuth = auth
+
+ return seed.Bytes()[:], nil
+}
+
+func (hs *serverHandshake) generateHandshake() ([]byte, error) {
+ var buf bytes.Buffer
+
+ hs.mac.Reset()
+ hs.mac.Write(hs.keypair.Representative().Bytes()[:])
+ mark := hs.mac.Sum(nil)[:markLength]
+
+ // The server handshake is Y | AUTH | P_S | M_S | MAC(Y | AUTH | P_S | M_S | E) where:
+ // * Y is the server's ephemeral Curve25519 public key representative.
+ // * AUTH is the ntor handshake AUTH value.
+ // * P_S is [serverMinPadLength,serverMaxPadLength] bytes of random padding.
+ // * M_S is HMAC-SHA256-128(serverIdentity | NodeID, Y)
+ // * MAC is HMAC-SHA256-128(serverIdentity | NodeID, Y .... E)
+ // * E is the string representation of the number of hours since the UNIX
+ // epoch.
+
+ // Generate the padding
+ pad, err := makePad(hs.padLen)
+ if err != nil {
+ return nil, err
+ }
+
+ // Write Y, AUTH, P_S, M_S.
+ buf.Write(hs.keypair.Representative().Bytes()[:])
+ buf.Write(hs.serverAuth.Bytes()[:])
+ buf.Write(pad)
+ buf.Write(mark)
+
+ // Calculate and write the MAC.
+ hs.mac.Reset()
+ hs.mac.Write(buf.Bytes())
+ hs.epochHour = []byte(strconv.FormatInt(getEpochHour(), 10))
+ hs.mac.Write(hs.epochHour)
+ buf.Write(hs.mac.Sum(nil)[:macLength])
+
+ return buf.Bytes(), nil
+}
+
+// getEpochHour returns the number of hours since the UNIX epoch.
+func getEpochHour() int64 {
+ return time.Now().Unix() / 3600
+}
+
+func findMarkMac(mark, buf []byte, startPos, maxPos int, fromTail bool) (pos int) {
+ if len(mark) != markLength {
+ panic(fmt.Sprintf("BUG: Invalid mark length: %d", len(mark)))
+ }
+
+ endPos := len(buf)
+ if startPos > len(buf) {
+ return -1
+ }
+ if endPos > maxPos {
+ endPos = maxPos
+ }
+ if endPos-startPos < markLength+macLength {
+ return -1
+ }
+
+ if fromTail {
+ // The server can optimize the search process by only examining the
+ // tail of the buffer. The client can't send valid data past M_C |
+ // MAC_C as it does not have the server's public key yet.
+ pos = endPos - (markLength + macLength)
+ if !hmac.Equal(buf[pos:pos+markLength], mark) {
+ return -1
+ }
+
+ return
+ }
+
+ // The client has to actually do a substring search since the server can
+ // and will send payload trailing the response.
+ //
+ // XXX: bytes.Index() uses a naive search, which kind of sucks.
+ pos = bytes.Index(buf[startPos:endPos], mark)
+ if pos == -1 {
+ return -1
+ }
+
+ // Ensure that there is enough trailing data for the MAC.
+ if startPos+pos+markLength+macLength > endPos {
+ return -1
+ }
+
+ // Return the index relative to the start of the slice.
+ pos += startPos
+ return
+}
+
+func makePad(padLen int) ([]byte, error) {
+ pad := make([]byte, padLen)
+ err := csrand.Bytes(pad)
+ if err != nil {
+ return nil, err
+ }
+
+ return pad, err
+}
diff --git a/transports/obfs4/handshake_ntor_test.go b/transports/obfs4/handshake_ntor_test.go
new file mode 100644
index 0000000..fa03420
--- /dev/null
+++ b/transports/obfs4/handshake_ntor_test.go
@@ -0,0 +1,222 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package obfs4
+
+import (
+ "bytes"
+ "testing"
+
+ "git.torproject.org/pluggable-transports/obfs4.git/common/ntor"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/replayfilter"
+)
+
+func TestHandshakeNtor(t *testing.T) {
+ // Generate the server node id and id keypair.
+ nodeID, _ := ntor.NewNodeID([]byte("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13"))
+ idKeypair, _ := ntor.NewKeypair(false)
+ serverFilter, _ := replayfilter.New(replayTTL)
+
+ // Test client handshake padding.
+ for l := clientMinPadLength; l <= clientMaxPadLength; l++ {
+ // Generate the client state and override the pad length.
+ clientKeypair, err := ntor.NewKeypair(true)
+ if err != nil {
+ t.Fatalf("[%d:0] ntor.NewKeypair failed: %s", l, err)
+ }
+ clientHs := newClientHandshake(nodeID, idKeypair.Public(), clientKeypair)
+ clientHs.padLen = l
+
+ // Generate what the client will send to the server.
+ clientBlob, err := clientHs.generateHandshake()
+ if err != nil {
+ t.Fatalf("[%d:0] clientHandshake.generateHandshake() failed: %s", l, err)
+ }
+ if len(clientBlob) > maxHandshakeLength {
+ t.Fatalf("[%d:0] Generated client body is oversized: %d", l, len(clientBlob))
+ }
+ if len(clientBlob) < clientMinHandshakeLength {
+ t.Fatalf("[%d:0] Generated client body is undersized: %d", l, len(clientBlob))
+ }
+ if len(clientBlob) != clientMinHandshakeLength+l {
+ t.Fatalf("[%d:0] Generated client body incorrect size: %d", l, len(clientBlob))
+ }
+
+ // Generate the server state and override the pad length.
+ serverKeypair, err := ntor.NewKeypair(true)
+ if err != nil {
+ t.Fatalf("[%d:0] ntor.NewKeypair failed: %s", l, err)
+ }
+ serverHs := newServerHandshake(nodeID, idKeypair, serverKeypair)
+ serverHs.padLen = serverMinPadLength
+
+ // Parse the client handshake message.
+ serverSeed, err := serverHs.parseClientHandshake(serverFilter, clientBlob)
+ if err != nil {
+ t.Fatalf("[%d:0] serverHandshake.parseClientHandshake() failed: %s", l, err)
+ }
+
+ // Genrate what the server will send to the client.
+ serverBlob, err := serverHs.generateHandshake()
+ if err != nil {
+ t.Fatal("[%d:0]: serverHandshake.generateHandshake() failed: %s", l, err)
+ }
+
+ // Parse the server handshake message.
+ clientHs.serverRepresentative = nil
+ n, clientSeed, err := clientHs.parseServerHandshake(serverBlob)
+ if err != nil {
+ t.Fatalf("[%d:0] clientHandshake.parseServerHandshake() failed: %s", l, err)
+ }
+ if n != len(serverBlob) {
+ t.Fatalf("[%d:0] clientHandshake.parseServerHandshake() has bytes remaining: %d", l, n)
+ }
+
+ // Ensure the derived shared secret is the same.
+ if 0 != bytes.Compare(clientSeed, serverSeed) {
+ t.Fatalf("[%d:0] client/server seed mismatch", l)
+ }
+ }
+
+ // Test server handshake padding.
+ for l := serverMinPadLength; l <= serverMaxPadLength+inlineSeedFrameLength; l++ {
+ // Generate the client state and override the pad length.
+ clientKeypair, err := ntor.NewKeypair(true)
+ if err != nil {
+ t.Fatalf("[%d:0] ntor.NewKeypair failed: %s", l, err)
+ }
+ clientHs := newClientHandshake(nodeID, idKeypair.Public(), clientKeypair)
+ clientHs.padLen = clientMinPadLength
+
+ // Generate what the client will send to the server.
+ clientBlob, err := clientHs.generateHandshake()
+ if err != nil {
+ t.Fatalf("[%d:1] clientHandshake.generateHandshake() failed: %s", l, err)
+ }
+ if len(clientBlob) > maxHandshakeLength {
+ t.Fatalf("[%d:1] Generated client body is oversized: %d", l, len(clientBlob))
+ }
+
+ // Generate the server state and override the pad length.
+ serverKeypair, err := ntor.NewKeypair(true)
+ if err != nil {
+ t.Fatalf("[%d:0] ntor.NewKeypair failed: %s", l, err)
+ }
+ serverHs := newServerHandshake(nodeID, idKeypair, serverKeypair)
+ serverHs.padLen = l
+
+ // Parse the client handshake message.
+ serverSeed, err := serverHs.parseClientHandshake(serverFilter, clientBlob)
+ if err != nil {
+ t.Fatalf("[%d:1] serverHandshake.parseClientHandshake() failed: %s", l, err)
+ }
+
+ // Genrate what the server will send to the client.
+ serverBlob, err := serverHs.generateHandshake()
+ if err != nil {
+ t.Fatal("[%d:1]: serverHandshake.generateHandshake() failed: %s", l, err)
+ }
+
+ // Parse the server handshake message.
+ n, clientSeed, err := clientHs.parseServerHandshake(serverBlob)
+ if err != nil {
+ t.Fatalf("[%d:1] clientHandshake.parseServerHandshake() failed: %s", l, err)
+ }
+ if n != len(serverBlob) {
+ t.Fatalf("[%d:1] clientHandshake.parseServerHandshake() has bytes remaining: %d", l, n)
+ }
+
+ // Ensure the derived shared secret is the same.
+ if 0 != bytes.Compare(clientSeed, serverSeed) {
+ t.Fatalf("[%d:1] client/server seed mismatch", l)
+ }
+ }
+
+ // Test oversized client padding.
+ clientKeypair, err := ntor.NewKeypair(true)
+ if err != nil {
+ t.Fatalf("ntor.NewKeypair failed: %s", err)
+ }
+ clientHs := newClientHandshake(nodeID, idKeypair.Public(), clientKeypair)
+ if err != nil {
+ t.Fatalf("newClientHandshake failed: %s", err)
+ }
+
+ clientHs.padLen = clientMaxPadLength + 1
+ clientBlob, err := clientHs.generateHandshake()
+ if err != nil {
+ t.Fatalf("clientHandshake.generateHandshake() (forced oversize) failed: %s", err)
+ }
+ serverKeypair, err := ntor.NewKeypair(true)
+ if err != nil {
+ t.Fatalf("ntor.NewKeypair failed: %s", err)
+ }
+ serverHs := newServerHandshake(nodeID, idKeypair, serverKeypair)
+ _, err = serverHs.parseClientHandshake(serverFilter, clientBlob)
+ if err == nil {
+ t.Fatalf("serverHandshake.parseClientHandshake() succeded (oversized)")
+ }
+
+ // Test undersized client padding.
+ clientHs.padLen = clientMinPadLength - 1
+ clientBlob, err = clientHs.generateHandshake()
+ if err != nil {
+ t.Fatalf("clientHandshake.generateHandshake() (forced undersize) failed: %s", err)
+ }
+ serverHs = newServerHandshake(nodeID, idKeypair, serverKeypair)
+ _, err = serverHs.parseClientHandshake(serverFilter, clientBlob)
+ if err == nil {
+ t.Fatalf("serverHandshake.parseClientHandshake() succeded (undersized)")
+ }
+
+ // Test oversized server padding.
+ //
+ // NB: serverMaxPadLength isn't the real maxPadLength that triggers client
+ // rejection, because the implementation is written with the asusmption
+ // that/ the PRNG_SEED is also inlined with the response. Thus the client
+ // actually accepts longer padding. The server handshake test and this
+ // test adjust around that.
+ clientHs.padLen = clientMinPadLength
+ clientBlob, err = clientHs.generateHandshake()
+ if err != nil {
+ t.Fatalf("clientHandshake.generateHandshake() failed: %s", err)
+ }
+ serverHs = newServerHandshake(nodeID, idKeypair, serverKeypair)
+ serverHs.padLen = serverMaxPadLength + inlineSeedFrameLength + 1
+ _, err = serverHs.parseClientHandshake(serverFilter, clientBlob)
+ if err != nil {
+ t.Fatalf("serverHandshake.parseClientHandshake() failed: %s", err)
+ }
+ serverBlob, err := serverHs.generateHandshake()
+ if err != nil {
+ t.Fatal("serverHandshake.generateHandshake() (forced oversize) failed: %s", err)
+ }
+ _, _, err = clientHs.parseServerHandshake(serverBlob)
+ if err == nil {
+ t.Fatalf("clientHandshake.parseServerHandshake() succeded (oversized)")
+ }
+}
diff --git a/transports/obfs4/obfs4.go b/transports/obfs4/obfs4.go
new file mode 100644
index 0000000..7af7224
--- /dev/null
+++ b/transports/obfs4/obfs4.go
@@ -0,0 +1,579 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Package obfs4 provides an implementation of the Tor Project's obfs4
+// obfuscation protocol.
+package obfs4
+
+import (
+ "bytes"
+ "crypto/sha256"
+ "fmt"
+ "math/rand"
+ "net"
+ "syscall"
+ "time"
+
+ "git.torproject.org/pluggable-transports/goptlib.git"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/drbg"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/ntor"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/probdist"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/replayfilter"
+ "git.torproject.org/pluggable-transports/obfs4.git/transports/base"
+ "git.torproject.org/pluggable-transports/obfs4.git/transports/obfs4/framing"
+)
+
+const (
+ transportName = "obfs4"
+
+ nodeIDArg = "node-id"
+ publicKeyArg = "public-key"
+ privateKeyArg = "private-key"
+ seedArg = "drbg-seed"
+
+ seedLength = 32
+ headerLength = framing.FrameOverhead + packetOverhead
+ clientHandshakeTimeout = time.Duration(60) * time.Second
+ serverHandshakeTimeout = time.Duration(30) * time.Second
+ replayTTL = time.Duration(3) * time.Hour
+
+ // Use a ScrambleSuit style biased probability table.
+ biasedDist = false
+
+ // Use IAT obfuscation.
+ iatObfuscation = false
+
+ // Maximum IAT delay (100 usec increments).
+ maxIATDelay = 100
+
+ maxCloseDelayBytes = maxHandshakeLength
+ maxCloseDelay = 60
+)
+
+type obfs4ClientArgs struct {
+ nodeID *ntor.NodeID
+ publicKey *ntor.PublicKey
+ sessionKey *ntor.Keypair
+}
+
+// Transport is the obfs4 implementation of the base.Transport interface.
+type Transport struct{}
+
+// Name returns the name of the obfs4 transport protocol.
+func (t *Transport) Name() string {
+ return transportName
+}
+
+// ClientFactory returns a new obfs4ClientFactory instance.
+func (t *Transport) ClientFactory(stateDir string) (base.ClientFactory, error) {
+ cf := &obfs4ClientFactory{transport: t}
+ return cf, nil
+}
+
+// ServerFactory returns a new obfs4ServerFactory instance.
+func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFactory, error) {
+ var err error
+
+ var st *obfs4ServerState
+ if st, err = serverStateFromArgs(stateDir, args); err != nil {
+ return nil, err
+ }
+
+ var iatSeed *drbg.Seed
+ if iatObfuscation {
+ iatSeedSrc := sha256.Sum256(st.drbgSeed.Bytes()[:])
+ iatSeed, err = drbg.SeedFromBytes(iatSeedSrc[:])
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // Store the arguments that should appear in our descriptor for the clients.
+ ptArgs := pt.Args{}
+ ptArgs.Add(nodeIDArg, st.nodeID.Base64())
+ ptArgs.Add(publicKeyArg, st.identityKey.Public().Base64())
+
+ // Initialize the replay filter.
+ filter, err := replayfilter.New(replayTTL)
+ if err != nil {
+ return nil, err
+ }
+
+ // Initialize the close thresholds for failed connections.
+ drbg, err := drbg.NewHashDrbg(st.drbgSeed)
+ if err != nil {
+ return nil, err
+ }
+ rng := rand.New(drbg)
+
+ sf := &obfs4ServerFactory{t, &ptArgs, st.nodeID, st.identityKey, st.drbgSeed, iatSeed, filter, rng.Intn(maxCloseDelayBytes), rng.Intn(maxCloseDelay)}
+ return sf, nil
+}
+
+type obfs4ClientFactory struct {
+ transport base.Transport
+}
+
+func (cf *obfs4ClientFactory) Transport() base.Transport {
+ return cf.transport
+}
+
+func (cf *obfs4ClientFactory) ParseArgs(args *pt.Args) (interface{}, error) {
+ var err error
+
+ // Handle the arguments.
+ nodeIDStr, ok := args.Get(nodeIDArg)
+ if !ok {
+ return nil, fmt.Errorf("missing argument '%s'", nodeIDArg)
+ }
+ var nodeID *ntor.NodeID
+ if nodeID, err = ntor.NodeIDFromBase64(nodeIDStr); err != nil {
+ return nil, err
+ }
+
+ publicKeyStr, ok := args.Get(publicKeyArg)
+ if !ok {
+ return nil, fmt.Errorf("missing argument '%s'", publicKeyArg)
+ }
+ var publicKey *ntor.PublicKey
+ if publicKey, err = ntor.PublicKeyFromBase64(publicKeyStr); err != nil {
+ return nil, err
+ }
+
+ // Generate the session key pair before connectiong to hide the Elligator2
+ // rejection sampling from network observers.
+ sessionKey, err := ntor.NewKeypair(true)
+ if err != nil {
+ return nil, err
+ }
+
+ return &obfs4ClientArgs{nodeID, publicKey, sessionKey}, nil
+}
+
+func (cf *obfs4ClientFactory) WrapConn(conn net.Conn, args interface{}) (net.Conn, error) {
+ ca, ok := args.(*obfs4ClientArgs)
+ if !ok {
+ return nil, fmt.Errorf("invalid argument type for args")
+ }
+
+ return newObfs4ClientConn(conn, ca)
+}
+
+type obfs4ServerFactory struct {
+ transport base.Transport
+ args *pt.Args
+
+ nodeID *ntor.NodeID
+ identityKey *ntor.Keypair
+ lenSeed *drbg.Seed
+ iatSeed *drbg.Seed
+ replayFilter *replayfilter.ReplayFilter
+
+ closeDelayBytes int
+ closeDelay int
+}
+
+func (sf *obfs4ServerFactory) Transport() base.Transport {
+ return sf.transport
+}
+
+func (sf *obfs4ServerFactory) Args() *pt.Args {
+ return sf.args
+}
+
+func (sf *obfs4ServerFactory) WrapConn(conn net.Conn) (net.Conn, error) {
+ // Not much point in having a separate newObfs4ServerConn routine when
+ // wrapping requires using values from the factory instance.
+
+ // Generate the session keypair *before* consuming data from the peer, to
+ // attempt to mask the rejection sampling due to use of Elligator2. This
+ // might be futile, but the timing differential isn't very large on modern
+ // hardware, and there are far easier statistical attacks that can be
+ // mounted as a distinguisher.
+ sessionKey, err := ntor.NewKeypair(true)
+ if err != nil {
+ return nil, err
+ }
+
+ lenDist := probdist.New(sf.lenSeed, 0, framing.MaximumSegmentLength, biasedDist)
+ var iatDist *probdist.WeightedDist
+ if sf.iatSeed != nil {
+ iatDist = probdist.New(sf.iatSeed, 0, maxIATDelay, biasedDist)
+ }
+
+ c := &obfs4Conn{conn, true, lenDist, iatDist, bytes.NewBuffer(nil), bytes.NewBuffer(nil), nil, nil}
+
+ startTime := time.Now()
+
+ if err = c.serverHandshake(sf, sessionKey); err != nil {
+ c.closeAfterDelay(sf, startTime)
+ return nil, err
+ }
+
+ return c, nil
+}
+
+type obfs4Conn struct {
+ net.Conn
+
+ isServer bool
+
+ lenDist *probdist.WeightedDist
+ iatDist *probdist.WeightedDist
+
+ receiveBuffer *bytes.Buffer
+ receiveDecodedBuffer *bytes.Buffer
+
+ encoder *framing.Encoder
+ decoder *framing.Decoder
+}
+
+func newObfs4ClientConn(conn net.Conn, args *obfs4ClientArgs) (c *obfs4Conn, err error) {
+ // Generate the initial protocol polymorphism distribution(s).
+ var seed *drbg.Seed
+ if seed, err = drbg.NewSeed(); err != nil {
+ return
+ }
+ lenDist := probdist.New(seed, 0, framing.MaximumSegmentLength, biasedDist)
+ var iatDist *probdist.WeightedDist
+ if iatObfuscation {
+ var iatSeed *drbg.Seed
+ iatSeedSrc := sha256.Sum256(seed.Bytes()[:])
+ if iatSeed, err = drbg.SeedFromBytes(iatSeedSrc[:]); err != nil {
+ return
+ }
+ iatDist = probdist.New(iatSeed, 0, maxIATDelay, biasedDist)
+ }
+
+ // Allocate the client structure.
+ c = &obfs4Conn{conn, false, lenDist, iatDist, bytes.NewBuffer(nil), bytes.NewBuffer(nil), nil, nil}
+
+ // Start the handshake timeout.
+ deadline := time.Now().Add(clientHandshakeTimeout)
+ if err = conn.SetDeadline(deadline); err != nil {
+ return nil, err
+ }
+
+ if err = c.clientHandshake(args.nodeID, args.publicKey, args.sessionKey); err != nil {
+ return nil, err
+ }
+
+ // Stop the handshake timeout.
+ if err = conn.SetDeadline(time.Time{}); err != nil {
+ return nil, err
+ }
+
+ return
+}
+
+func (conn *obfs4Conn) clientHandshake(nodeID *ntor.NodeID, peerIdentityKey *ntor.PublicKey, sessionKey *ntor.Keypair) error {
+ if conn.isServer {
+ return fmt.Errorf("clientHandshake called on server connection")
+ }
+
+ // Generate and send the client handshake.
+ hs := newClientHandshake(nodeID, peerIdentityKey, sessionKey)
+ blob, err := hs.generateHandshake()
+ if err != nil {
+ return err
+ }
+ if _, err = conn.Conn.Write(blob); err != nil {
+ return err
+ }
+
+ // Consume the server handshake.
+ var hsBuf [maxHandshakeLength]byte
+ for {
+ var n int
+ if n, err = conn.Conn.Read(hsBuf[:]); err != nil {
+ // The Read() could have returned data and an error, but there is
+ // no point in continuing on an EOF or whatever.
+ return err
+ }
+ conn.receiveBuffer.Write(hsBuf[:n])
+
+ var seed []byte
+ n, seed, err = hs.parseServerHandshake(conn.receiveBuffer.Bytes())
+ if err == ErrMarkNotFoundYet {
+ continue
+ } else if err != nil {
+ return err
+ }
+ _ = conn.receiveBuffer.Next(n)
+
+ // Use the derived key material to intialize the link crypto.
+ okm := ntor.Kdf(seed, framing.KeyLength*2)
+ conn.encoder = framing.NewEncoder(okm[:framing.KeyLength])
+ conn.decoder = framing.NewDecoder(okm[framing.KeyLength:])
+
+ return nil
+ }
+}
+
+func (conn *obfs4Conn) serverHandshake(sf *obfs4ServerFactory, sessionKey *ntor.Keypair) (err error) {
+ if !conn.isServer {
+ return fmt.Errorf("serverHandshake called on client connection")
+ }
+
+ // Generate the server handshake, and arm the base timeout.
+ hs := newServerHandshake(sf.nodeID, sf.identityKey, sessionKey)
+ if err = conn.Conn.SetDeadline(time.Now().Add(serverHandshakeTimeout)); err != nil {
+ return
+ }
+
+ // Consume the client handshake.
+ var hsBuf [maxHandshakeLength]byte
+ for {
+ var n int
+ if n, err = conn.Conn.Read(hsBuf[:]); err != nil {
+ // The Read() could have returned data and an error, but there is
+ // no point in continuing on an EOF or whatever.
+ return
+ }
+ conn.receiveBuffer.Write(hsBuf[:n])
+
+ var seed []byte
+ seed, err = hs.parseClientHandshake(sf.replayFilter, conn.receiveBuffer.Bytes())
+ if err == ErrMarkNotFoundYet {
+ continue
+ } else if err != nil {
+ return
+ }
+ conn.receiveBuffer.Reset()
+
+ if err = conn.Conn.SetDeadline(time.Time{}); err != nil {
+ return
+ }
+
+ // Use the derived key material to intialize the link crypto.
+ okm := ntor.Kdf(seed, framing.KeyLength*2)
+ conn.encoder = framing.NewEncoder(okm[framing.KeyLength:])
+ conn.decoder = framing.NewDecoder(okm[:framing.KeyLength])
+
+ break
+ }
+
+ // Since the current and only implementation always sends a PRNG seed for
+ // the length obfuscation, this makes the amount of data received from the
+ // server inconsistent with the length sent from the client.
+ //
+ // Rebalance this by tweaking the client mimimum padding/server maximum
+ // padding, and sending the PRNG seed unpadded (As in, treat the PRNG seed
+ // as part of the server response). See inlineSeedFrameLength in
+ // handshake_ntor.go.
+
+ // Generate/send the response.
+ var blob []byte
+ blob, err = hs.generateHandshake()
+ if err != nil {
+ return
+ }
+ var frameBuf bytes.Buffer
+ _, err = frameBuf.Write(blob)
+ if err != nil {
+ return
+ }
+
+ // Send the PRNG seed as the first packet.
+ if err = conn.makePacket(&frameBuf, packetTypePrngSeed, sf.lenSeed.Bytes()[:], 0); err != nil {
+ return
+ }
+ if _, err = conn.Conn.Write(frameBuf.Bytes()); err != nil {
+ return
+ }
+
+ return
+}
+
+func (conn *obfs4Conn) Read(b []byte) (n int, err error) {
+ // If there is no payload from the previous Read() calls, consume data off
+ // the network. Not all data received is guaranteed to be usable payload,
+ // so do this in a loop till data is present or an error occurs.
+ for conn.receiveDecodedBuffer.Len() == 0 {
+ err = conn.readPackets()
+ if err == framing.ErrAgain {
+ // Don't proagate this back up the call stack if we happen to break
+ // out of the loop.
+ err = nil
+ continue
+ } else if err != nil {
+ break
+ }
+ }
+
+ // Even if err is set, attempt to do the read anyway so that all decoded
+ // data gets relayed before the connection is torn down.
+ if conn.receiveDecodedBuffer.Len() > 0 {
+ var berr error
+ n, berr = conn.receiveDecodedBuffer.Read(b)
+ if err == nil {
+ // Only propagate berr if there are not more important (fatal)
+ // errors from the network/crypto/packet processing.
+ err = berr
+ }
+ }
+
+ return
+}
+
+func (conn *obfs4Conn) Write(b []byte) (n int, err error) {
+ chopBuf := bytes.NewBuffer(b)
+ var payload [maxPacketPayloadLength]byte
+ var frameBuf bytes.Buffer
+
+ // Chop the pending data into payload frames.
+ for chopBuf.Len() > 0 {
+ // Send maximum sized frames.
+ rdLen := 0
+ rdLen, err = chopBuf.Read(payload[:])
+ if err != nil {
+ return 0, err
+ } else if rdLen == 0 {
+ panic(fmt.Sprintf("BUG: Write(), chopping length was 0"))
+ }
+ n += rdLen
+
+ err = conn.makePacket(&frameBuf, packetTypePayload, payload[:rdLen], 0)
+ if err != nil {
+ return 0, err
+ }
+ }
+
+ // Add the length obfuscation padding. In theory, this could be inlined
+ // with the last chopped packet for certain (most?) payload lenghts, but
+ // this is simpler.
+
+ if err = conn.padBurst(&frameBuf); err != nil {
+ return 0, err
+ }
+
+ // Write the pending data onto the network. Partial writes are fatal,
+ // because the frame encoder state is advanced, and the code doesn't keep
+ // frameBuf around. In theory, write timeouts and whatnot could be
+ // supported if this wasn't the case, but that complicates the code.
+
+ if conn.iatDist != nil {
+ var iatFrame [framing.MaximumSegmentLength]byte
+ for frameBuf.Len() > 0 {
+ iatWrLen := 0
+ iatWrLen, err = frameBuf.Read(iatFrame[:])
+ if err != nil {
+ return 0, err
+ } else if iatWrLen == 0 {
+ panic(fmt.Sprintf("BUG: Write(), iat length was 0"))
+ }
+
+ // Calculate the delay. The delay resolution is 100 usec, leading
+ // to a maximum delay of 10 msec.
+ iatDelta := time.Duration(conn.iatDist.Sample() * 100)
+
+ // Write then sleep.
+ _, err = conn.Conn.Write(iatFrame[:iatWrLen])
+ if err != nil {
+ return 0, err
+ }
+ time.Sleep(iatDelta * time.Microsecond)
+ }
+ } else {
+ _, err = conn.Conn.Write(frameBuf.Bytes())
+ }
+
+ return
+}
+
+func (conn *obfs4Conn) SetDeadline(t time.Time) error {
+ return syscall.ENOTSUP
+}
+
+func (conn *obfs4Conn) SetWriteDeadline(t time.Time) error {
+ return syscall.ENOTSUP
+}
+
+func (conn *obfs4Conn) closeAfterDelay(sf *obfs4ServerFactory, startTime time.Time) {
+ // I-it's not like I w-wanna handshake with you or anything. B-b-baka!
+ defer conn.Conn.Close()
+
+ delay := time.Duration(sf.closeDelay)*time.Second + serverHandshakeTimeout
+ deadline := startTime.Add(delay)
+ if time.Now().After(deadline) {
+ return
+ }
+
+ if err := conn.Conn.SetReadDeadline(deadline); err != nil {
+ return
+ }
+
+ // Consume and discard data on this connection until either the specified
+ // interval passes or a certain size has been reached.
+ discarded := 0
+ var buf [framing.MaximumSegmentLength]byte
+ for discarded < int(sf.closeDelayBytes) {
+ n, err := conn.Conn.Read(buf[:])
+ if err != nil {
+ return
+ }
+ discarded += n
+ }
+}
+
+func (conn *obfs4Conn) padBurst(burst *bytes.Buffer) (err error) {
+ tailLen := burst.Len() % framing.MaximumSegmentLength
+ toPadTo := conn.lenDist.Sample()
+
+ padLen := 0
+ if toPadTo >= tailLen {
+ padLen = toPadTo - tailLen
+ } else {
+ padLen = (framing.MaximumSegmentLength - tailLen) + toPadTo
+ }
+
+ if padLen > headerLength {
+ err = conn.makePacket(burst, packetTypePayload, []byte{},
+ uint16(padLen-headerLength))
+ if err != nil {
+ return
+ }
+ } else if padLen > 0 {
+ err = conn.makePacket(burst, packetTypePayload, []byte{},
+ maxPacketPayloadLength)
+ if err != nil {
+ return
+ }
+ err = conn.makePacket(burst, packetTypePayload, []byte{},
+ uint16(padLen))
+ if err != nil {
+ return
+ }
+ }
+
+ return
+}
+
+var _ base.ClientFactory = (*obfs4ClientFactory)(nil)
+var _ base.ServerFactory = (*obfs4ServerFactory)(nil)
+var _ base.Transport = (*Transport)(nil)
+var _ net.Conn = (*obfs4Conn)(nil)
diff --git a/transports/obfs4/packet.go b/transports/obfs4/packet.go
new file mode 100644
index 0000000..9865c82
--- /dev/null
+++ b/transports/obfs4/packet.go
@@ -0,0 +1,179 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package obfs4
+
+import (
+ "crypto/sha256"
+ "encoding/binary"
+ "fmt"
+ "io"
+
+ "git.torproject.org/pluggable-transports/obfs4.git/common/drbg"
+ "git.torproject.org/pluggable-transports/obfs4.git/transports/obfs4/framing"
+)
+
+const (
+ packetOverhead = 2 + 1
+ maxPacketPayloadLength = framing.MaximumFramePayloadLength - packetOverhead
+ maxPacketPaddingLength = maxPacketPayloadLength
+ seedPacketPayloadLength = seedLength
+
+ consumeReadSize = framing.MaximumSegmentLength * 16
+)
+
+const (
+ packetTypePayload = iota
+ packetTypePrngSeed
+)
+
+// InvalidPacketLengthError is the error returned when decodePacket detects a
+// invalid packet length/
+type InvalidPacketLengthError int
+
+func (e InvalidPacketLengthError) Error() string {
+ return fmt.Sprintf("packet: Invalid packet length: %d", int(e))
+}
+
+// InvalidPayloadLengthError is the error returned when decodePacket rejects the
+// payload length.
+type InvalidPayloadLengthError int
+
+func (e InvalidPayloadLengthError) Error() string {
+ return fmt.Sprintf("packet: Invalid payload length: %d", int(e))
+}
+
+var zeroPadBytes [maxPacketPaddingLength]byte
+
+func (conn *obfs4Conn) makePacket(w io.Writer, pktType uint8, data []byte, padLen uint16) (err error) {
+ var pkt [framing.MaximumFramePayloadLength]byte
+
+ if len(data)+int(padLen) > maxPacketPayloadLength {
+ panic(fmt.Sprintf("BUG: makePacket() len(data) + padLen > maxPacketPayloadLength: %d + %d > %d",
+ len(data), padLen, maxPacketPayloadLength))
+ }
+
+ // Packets are:
+ // uint8_t type packetTypePayload (0x00)
+ // uint16_t length Length of the payload (Big Endian).
+ // uint8_t[] payload Data payload.
+ // uint8_t[] padding Padding.
+ pkt[0] = pktType
+ binary.BigEndian.PutUint16(pkt[1:], uint16(len(data)))
+ if len(data) > 0 {
+ copy(pkt[3:], data[:])
+ }
+ copy(pkt[3+len(data):], zeroPadBytes[:padLen])
+
+ pktLen := packetOverhead + len(data) + int(padLen)
+
+ // Encode the packet in an AEAD frame.
+ var frame [framing.MaximumSegmentLength]byte
+ frameLen := 0
+ frameLen, err = conn.encoder.Encode(frame[:], pkt[:pktLen])
+ if err != nil {
+ // All encoder errors are fatal.
+ return
+ }
+ var wrLen int
+ wrLen, err = w.Write(frame[:frameLen])
+ if err != nil {
+ return
+ } else if wrLen < frameLen {
+ err = io.ErrShortWrite
+ return
+ }
+
+ return
+}
+
+func (conn *obfs4Conn) readPackets() (err error) {
+ // Attempt to read off the network.
+ var buf [consumeReadSize]byte
+ rdLen, rdErr := conn.Conn.Read(buf[:])
+ conn.receiveBuffer.Write(buf[:rdLen])
+
+ var decoded [framing.MaximumFramePayloadLength]byte
+ for conn.receiveBuffer.Len() > 0 {
+ // Decrypt an AEAD frame.
+ decLen := 0
+ decLen, err = conn.decoder.Decode(decoded[:], conn.receiveBuffer)
+ if err == framing.ErrAgain {
+ break
+ } else if err != nil {
+ break
+ } else if decLen < packetOverhead {
+ err = InvalidPacketLengthError(decLen)
+ break
+ }
+
+ // Decode the packet.
+ pkt := decoded[0:decLen]
+ pktType := pkt[0]
+ payloadLen := binary.BigEndian.Uint16(pkt[1:])
+ if int(payloadLen) > len(pkt)-packetOverhead {
+ err = InvalidPayloadLengthError(int(payloadLen))
+ break
+ }
+ payload := pkt[3 : 3+payloadLen]
+
+ switch pktType {
+ case packetTypePayload:
+ if payloadLen > 0 {
+ conn.receiveDecodedBuffer.Write(payload)
+ }
+ case packetTypePrngSeed:
+ // Only regenerate the distribution if we are the client.
+ if len(payload) == seedPacketPayloadLength && !conn.isServer {
+ var seed *drbg.Seed
+ seed, err = drbg.SeedFromBytes(payload)
+ if err != nil {
+ break
+ }
+ conn.lenDist.Reset(seed)
+ if conn.iatDist != nil {
+ iatSeedSrc := sha256.Sum256(seed.Bytes()[:])
+ iatSeed, err := drbg.SeedFromBytes(iatSeedSrc[:])
+ if err != nil {
+ break
+ }
+ conn.iatDist.Reset(iatSeed)
+ }
+ }
+ default:
+ // Ignore unknown packet types.
+ }
+ }
+
+ // Read errors (all fatal) take priority over various frame processing
+ // errors.
+ if rdErr != nil {
+ return rdErr
+ }
+
+ return
+}
diff --git a/transports/obfs4/statefile.go b/transports/obfs4/statefile.go
new file mode 100644
index 0000000..814a545
--- /dev/null
+++ b/transports/obfs4/statefile.go
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package obfs4
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path"
+
+ "git.torproject.org/pluggable-transports/goptlib.git"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/drbg"
+ "git.torproject.org/pluggable-transports/obfs4.git/common/ntor"
+)
+
+const (
+ stateFile = "obfs4_state.json"
+)
+
+type jsonServerState struct {
+ NodeID string `json:"node-id"`
+ PrivateKey string `json:"private-key"`
+ PublicKey string `json:"public-key"`
+ DrbgSeed string `json:"drbgSeed"`
+}
+
+type obfs4ServerState struct {
+ nodeID *ntor.NodeID
+ identityKey *ntor.Keypair
+ drbgSeed *drbg.Seed
+}
+
+func serverStateFromArgs(stateDir string, args *pt.Args) (*obfs4ServerState, error) {
+ var js jsonServerState
+ var nodeIDOk, privKeyOk, seedOk bool
+
+ js.NodeID, nodeIDOk = args.Get(nodeIDArg)
+ js.PrivateKey, privKeyOk = args.Get(privateKeyArg)
+ js.DrbgSeed, seedOk = args.Get(seedArg)
+
+ if !privKeyOk && !nodeIDOk && !seedOk {
+ if err := jsonServerStateFromFile(stateDir, &js); err != nil {
+ return nil, err
+ }
+ } else if !privKeyOk {
+ return nil, fmt.Errorf("missing argument '%s'", privateKeyArg)
+ } else if !nodeIDOk {
+ return nil, fmt.Errorf("missing argument '%s'", nodeIDArg)
+ } else if !seedOk {
+ return nil, fmt.Errorf("missing argument '%s'", seedArg)
+ }
+
+ return serverStateFromJSONServerState(&js)
+}
+
+func serverStateFromJSONServerState(js *jsonServerState) (*obfs4ServerState, error) {
+ var err error
+
+ st := new(obfs4ServerState)
+ if st.nodeID, err = ntor.NodeIDFromBase64(js.NodeID); err != nil {
+ return nil, err
+ }
+ if st.identityKey, err = ntor.KeypairFromBase64(js.PrivateKey); err != nil {
+ return nil, err
+ }
+ var rawSeed []byte
+ if rawSeed, err = base64.StdEncoding.DecodeString(js.DrbgSeed); err != nil {
+ return nil, err
+ }
+ if st.drbgSeed, err = drbg.SeedFromBytes(rawSeed); err != nil {
+ return nil, err
+ }
+
+ return st, nil
+}
+
+func jsonServerStateFromFile(stateDir string, js *jsonServerState) error {
+ f, err := ioutil.ReadFile(path.Join(stateDir, stateFile))
+ if err != nil {
+ if os.IsNotExist(err) {
+ if err = newJSONServerState(stateDir, js); err == nil {
+ return nil
+ }
+ }
+ return err
+ }
+
+ if err = json.Unmarshal(f, js); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func newJSONServerState(stateDir string, js *jsonServerState) (err error) {
+ // Generate everything a server needs, using the cryptographic PRNG.
+ var st obfs4ServerState
+ rawID := make([]byte, ntor.NodeIDLength)
+ if err = csrand.Bytes(rawID); err != nil {
+ return
+ }
+ if st.nodeID, err = ntor.NewNodeID(rawID); err != nil {
+ return
+ }
+ if st.identityKey, err = ntor.NewKeypair(false); err != nil {
+ return
+ }
+ if st.drbgSeed, err = drbg.NewSeed(); err != nil {
+ return
+ }
+
+ // Encode it into JSON format and write the state file.
+ js.NodeID = st.nodeID.Base64()
+ js.PrivateKey = st.identityKey.Private().Base64()
+ js.PublicKey = st.identityKey.Public().Base64()
+ js.DrbgSeed = st.drbgSeed.Base64()
+
+ var encoded []byte
+ if encoded, err = json.Marshal(js); err != nil {
+ return
+ }
+
+ if err = ioutil.WriteFile(path.Join(stateDir, stateFile), encoded, 0600); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/transports/transports.go b/transports/transports.go
new file mode 100644
index 0000000..6b80bdc
--- /dev/null
+++ b/transports/transports.go
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Package transports provides a interface to query supported pluggable
+// transports.
+package transports
+
+import (
+ "fmt"
+ "sync"
+
+ "git.torproject.org/pluggable-transports/obfs4.git/transports/base"
+ "git.torproject.org/pluggable-transports/obfs4.git/transports/obfs2"
+ "git.torproject.org/pluggable-transports/obfs4.git/transports/obfs3"
+ "git.torproject.org/pluggable-transports/obfs4.git/transports/obfs4"
+)
+
+var transportMapLock sync.Mutex
+var transportMap map[string]base.Transport
+
+// Register registers a transport protocol.
+func Register(transport base.Transport) error {
+ transportMapLock.Lock()
+ defer transportMapLock.Unlock()
+
+ name := transport.Name()
+ _, registered := transportMap[name]
+ if registered {
+ return fmt.Errorf("transport '%s' already registered", name)
+ }
+ transportMap[name] = transport
+
+ return nil
+}
+
+// Transports returns the list of registered transport protocols.
+func Transports() []string {
+ transportMapLock.Lock()
+ defer transportMapLock.Unlock()
+
+ var ret []string
+ for name := range transportMap {
+ ret = append(ret, name)
+ }
+
+ return ret
+}
+
+// Get returns a transport protocol implementation by name.
+func Get(name string) base.Transport {
+ transportMapLock.Lock()
+ defer transportMapLock.Unlock()
+
+ t := transportMap[name]
+
+ return t
+}
+
+func init() {
+ // Initialize the transport list.
+ transportMap = make(map[string]base.Transport)
+
+ // Register all the currently supported transports.
+ Register(new(obfs2.Transport))
+ Register(new(obfs3.Transport))
+ Register(new(obfs4.Transport))
+}
diff --git a/weighted_dist.go b/weighted_dist.go
deleted file mode 100644
index 4f1f2a5..0000000
--- a/weighted_dist.go
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * * Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-package obfs4
-
-import (
- "container/list"
- "fmt"
- "math/rand"
-
- "git.torproject.org/pluggable-transports/obfs4.git/csrand"
- "git.torproject.org/pluggable-transports/obfs4.git/drbg"
-)
-
-const (
- minValues = 1
- maxValues = 100
-)
-
-// wDist is a weighted distribution.
-type wDist struct {
- minValue int
- maxValue int
- values []int
- weights []float64
-
- alias []int
- prob []float64
-}
-
-// newWDist creates a weighted distribution of values ranging from min to max
-// based on a HashDrbg initialized with seed.
-func newWDist(seed *drbg.Seed, min, max int) (w *wDist) {
- w = &wDist{minValue: min, maxValue: max}
-
- if max <= min {
- panic(fmt.Sprintf("wDist.Reset(): min >= max (%d, %d)", min, max))
- }
-
- w.reset(seed)
-
- return
-}
-
-// genValues creates a slice containing a random number of random values
-// that when scaled by adding minValue will fall into [min, max].
-func (w *wDist) genValues(rng *rand.Rand) {
- nValues := (w.maxValue + 1) - w.minValue
- values := rng.Perm(nValues)
- if nValues < minValues {
- nValues = minValues
- }
- if nValues > maxValues {
- nValues = maxValues
- }
- nValues = rng.Intn(nValues) + 1
- w.values = values[:nValues]
-}
-
-// genBiasedWeights generates a non-uniform weight list, similar to the
-// ScrambleSuit prob_dist module.
-func (w *wDist) genBiasedWeights(rng *rand.Rand) {
- w.weights = make([]float64, len(w.values))
-
- culmProb := 0.0
- for i := range w.values {
- p := (1.0 - culmProb) * rng.Float64()
- w.weights[i] = p
- culmProb += p
- }
-}
-
-// genUniformWeights generates a uniform weight list.
-func (w *wDist) genUniformWeights(rng *rand.Rand) {
- w.weights = make([]float64, len(w.values))
- for i := range w.weights {
- w.weights[i] = rng.Float64()
- }
-}
-
-// genTables calculates the alias and prob tables used for Vose's Alias method.
-// Algorithm taken from http://www.keithschwarz.com/darts-dice-coins/
-func (w *wDist) genTables() {
- n := len(w.weights)
- var sum float64
- for _, weight := range w.weights {
- sum += weight
- }
-
- // Create arrays $Alias$ and $Prob$, each of size $n$.
- alias := make([]int, n)
- prob := make([]float64, n)
-
- // Create two worklists, $Small$ and $Large$.
- small := list.New()
- large := list.New()
-
- scaled := make([]float64, n)
- for i, weight := range w.weights {
- // Multiply each probability by $n$.
- p_i := weight * float64(n) / sum
- scaled[i] = p_i
-
- // For each scaled probability $p_i$:
- if scaled[i] < 1.0 {
- // If $p_i < 1$, add $i$ to $Small$.
- small.PushBack(i)
- } else {
- // Otherwise ($p_i \ge 1$), add $i$ to $Large$.
- large.PushBack(i)
- }
- }
-
- // While $Small$ and $Large$ are not empty: ($Large$ might be emptied first)
- for small.Len() > 0 && large.Len() > 0 {
- // Remove the first element from $Small$; call it $l$.
- l := small.Remove(small.Front()).(int)
- // Remove the first element from $Large$; call it $g$.
- g := large.Remove(large.Front()).(int)
-
- // Set $Prob[l] = p_l$.
- prob[l] = scaled[l]
- // Set $Alias[l] = g$.
- alias[l] = g
-
- // Set $p_g := (p_g + p_l) - 1$. (This is a more numerically stable option.)
- scaled[g] = (scaled[g] + scaled[l]) - 1.0
-
- if scaled[g] < 1.0 {
- // If $p_g < 1$, add $g$ to $Small$.
- small.PushBack(g)
- } else {
- // Otherwise ($p_g \ge 1$), add $g$ to $Large$.
- large.PushBack(g)
- }
- }
-
- // While $Large$ is not empty:
- for large.Len() > 0 {
- // Remove the first element from $Large$; call it $g$.
- g := large.Remove(large.Front()).(int)
- // Set $Prob[g] = 1$.
- prob[g] = 1.0
- }
-
- // While $Small$ is not empty: This is only possible due to numerical instability.
- for small.Len() > 0 {
- // Remove the first element from $Small$; call it $l$.
- l := small.Remove(small.Front()).(int)
- // Set $Prob[l] = 1$.
- prob[l] = 1.0
- }
-
- w.prob = prob
- w.alias = alias
-}
-
-// reset generates a new distribution with the same min/max based on a new seed.
-func (w *wDist) reset(seed *drbg.Seed) {
- // Initialize the deterministic random number generator.
- drbg := drbg.NewHashDrbg(seed)
- rng := rand.New(drbg)
-
- w.genValues(rng)
- //w.genBiasedWeights(rng)
- w.genUniformWeights(rng)
- w.genTables()
-}
-
-// sample generates a random value according to the distribution.
-func (w *wDist) sample() int {
- var idx int
-
- // Generate a fair die roll from an $n$-sided die; call the side $i$.
- i := csrand.Intn(len(w.values))
- // Flip a biased coin that comes up heads with probability $Prob[i]$.
- if csrand.Float64() <= w.prob[i] {
- // If the coin comes up "heads," return $i$.
- idx = i
- } else {
- // Otherwise, return $Alias[i]$.
- idx = w.alias[i]
- }
-
- return w.minValue + w.values[idx]
-}
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/weighted_dist_test.go b/weighted_dist_test.go
deleted file mode 100644
index 16b93c4..0000000
--- a/weighted_dist_test.go
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * * Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-package obfs4
-
-import (
- "fmt"
- "testing"
-
- "git.torproject.org/pluggable-transports/obfs4.git/drbg"
-)
-
-const debug = false
-
-func TestWeightedDist(t *testing.T) {
- seed, err := drbg.NewSeed()
- if err != nil {
- t.Fatal("failed to generate a DRBG seed:", err)
- }
-
- const nrTrials = 1000000
-
- hist := make([]int, 1000)
-
- w := newWDist(seed, 0, 999)
- if debug {
- // Dump a string representation of the probability table.
- fmt.Println("Table:")
- var sum float64
- for _, weight := range w.weights {
- sum += weight
- }
- for i, weight := range w.weights {
- p := weight / sum
- if p > 0.000001 { // Filter out tiny values.
- fmt.Printf(" [%d]: %f\n", w.minValue+w.values[i], p)
- }
- }
- fmt.Println()
- }
-
- for i := 0; i < nrTrials; i++ {
- value := w.sample()
- hist[value]++
- }
-
- if debug {
- fmt.Println("Generated:")
- for value, count := range hist {
- if count != 0 {
- p := float64(count) / float64(nrTrials)
- fmt.Printf(" [%d]: %f (%d)\n", value, p, count)
- }
- }
- }
-}
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits