tor-commits
Threads by month
- ----- 2025 -----
- July
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
August 2014
- 21 participants
- 1599 discussions

[translation/abouttor-homepage] Update translations for abouttor-homepage
by translation@torproject.org 17 Aug '14
by translation@torproject.org 17 Aug '14
17 Aug '14
commit e36487d72a823dc8b921db1cfc12c05cd95d6f0e
Author: Translation commit bot <translation(a)torproject.org>
Date: Sun Aug 17 18:45:37 2014 +0000
Update translations for abouttor-homepage
---
et/aboutTor.dtd | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/et/aboutTor.dtd b/et/aboutTor.dtd
index 7128bb8..8de1a0b 100644
--- a/et/aboutTor.dtd
+++ b/et/aboutTor.dtd
@@ -21,7 +21,7 @@
<!ENTITY aboutTor.failure3Link "help(a)rt.torproject.org">
<!ENTITY aboutTor.failure3suffix.label ".">
-<!ENTITY aboutTor.search.label "Search">
+<!ENTITY aboutTor.search.label "Otsing">
<!ENTITY aboutTor.searchSPPost.link "https://startpage.com/rth/search">
<!ENTITY aboutTor.searchDDGPost.link "https://duckduckgo.com/html/">
1
0

[translation/https_everywhere] Update translations for https_everywhere
by translation@torproject.org 17 Aug '14
by translation@torproject.org 17 Aug '14
17 Aug '14
commit 84c06f7f63bee3e578ae030f3b54bf2f569bf029
Author: Translation commit bot <translation(a)torproject.org>
Date: Sun Aug 17 18:45:14 2014 +0000
Update translations for https_everywhere
---
et/https-everywhere.dtd | 40 ++++++++++++++++++++--------------------
1 file changed, 20 insertions(+), 20 deletions(-)
diff --git a/et/https-everywhere.dtd b/et/https-everywhere.dtd
index 5bf01c0..85841df 100644
--- a/et/https-everywhere.dtd
+++ b/et/https-everywhere.dtd
@@ -1,8 +1,8 @@
<!ENTITY https-everywhere.about.title "About HTTPS Everywhere">
<!ENTITY https-everywhere.about.ext_name "HTTPS Everywhere">
<!ENTITY https-everywhere.about.ext_description "Encrypt the Web! Automatically use HTTPS security on many sites.">
-<!ENTITY https-everywhere.about.version "Version">
-<!ENTITY https-everywhere.about.created_by "Created by">
+<!ENTITY https-everywhere.about.version "Versioon">
+<!ENTITY https-everywhere.about.created_by "Loodud">
<!ENTITY https-everywhere.about.librarians "Ruleset Librarians">
<!ENTITY https-everywhere.about.thanks "Thanks to">
<!ENTITY https-everywhere.about.contribute "If you like HTTPS Everywhere, you might consider">
@@ -17,27 +17,27 @@
<!ENTITY https-everywhere.menu.showCounter "Show Counter">
<!ENTITY https-everywhere.prefs.title "HTTPS Everywhere Preferences">
-<!ENTITY https-everywhere.prefs.enable_all "Enable All">
-<!ENTITY https-everywhere.prefs.disable_all "Disable All">
-<!ENTITY https-everywhere.prefs.reset_defaults "Reset to Defaults">
-<!ENTITY https-everywhere.prefs.search "Search">
-<!ENTITY https-everywhere.prefs.site "Site">
-<!ENTITY https-everywhere.prefs.notes "Notes">
-<!ENTITY https-everywhere.prefs.list_caption "Which HTTPS redirection rules should apply?">
-<!ENTITY https-everywhere.prefs.enabled "Enabled">
-<!ENTITY https-everywhere.prefs.ruleset_howto "You can learn how to write your own rulesets (to add support for other web sites)">
-<!ENTITY https-everywhere.prefs.here_link "here">
-<!ENTITY https-everywhere.prefs.toggle "Toggle">
-<!ENTITY https-everywhere.prefs.reset_default "Reset to Default">
-<!ENTITY https-everywhere.prefs.view_xml_source "View XML Source">
+<!ENTITY https-everywhere.prefs.enable_all "Luba kõik">
+<!ENTITY https-everywhere.prefs.disable_all "Keela kõik">
+<!ENTITY https-everywhere.prefs.reset_defaults "Taasta algseaded">
+<!ENTITY https-everywhere.prefs.search "Otsing">
+<!ENTITY https-everywhere.prefs.site "Leht">
+<!ENTITY https-everywhere.prefs.notes "Märkmed">
+<!ENTITY https-everywhere.prefs.list_caption "Millised HTTPS ümbersuunamis reeglid peaksid olema aktiivsed?">
+<!ENTITY https-everywhere.prefs.enabled "Lubatud">
+<!ENTITY https-everywhere.prefs.ruleset_howto "Sa võid kirjutada isiklikke reegleid (et lisada tuge teistele lehtedele)">
+<!ENTITY https-everywhere.prefs.here_link "siin">
+<!ENTITY https-everywhere.prefs.toggle "lülita">
+<!ENTITY https-everywhere.prefs.reset_default "Taasta vaikeväärtused">
+<!ENTITY https-everywhere.prefs.view_xml_source "Vaata XML lähtekoodi">
-<!ENTITY https-everywhere.source.downloading "Downloading">
-<!ENTITY https-everywhere.source.filename "Filename">
-<!ENTITY https-everywhere.source.unable_to_download "Unable to download source.">
+<!ENTITY https-everywhere.source.downloading "Allalaadimine">
+<!ENTITY https-everywhere.source.filename "Failinimi">
+<!ENTITY https-everywhere.source.unable_to_download "Lähtekoodi allalaadimine ebaõnnestus">
<!ENTITY https-everywhere.popup.title "HTTPS Everywhere 4.0development.11 notification">
-<!ENTITY https-everywhere.popup.paragraph1 "Oops. You were using the stable version of HTTPS Everywhere, but we might have accidentally upgraded you to the development version in our last release.">
-<!ENTITY https-everywhere.popup.paragraph2 "Would you like to go back to stable?">
+<!ENTITY https-everywhere.popup.paragraph1 "Oih. Sa kasutasid HTTPS Everwhere stabiilset versiooni aga oleme ekslikult uuendanud sind arendus versioonile.">
+<!ENTITY https-everywhere.popup.paragraph2 "Kas sa soovid minna tagasi stabiilsele versioonile?">
<!ENTITY https-everywhere.popup.paragraph3 "We'd love it if you continued using our development release and helped us make HTTPS Everywhere better! You might find there are a few more bugs here and there, which you can report to https-everywhere(a)eff.org. Sorry about the inconvenience, and thank you for using HTTPS Everywhere.">
<!ENTITY https-everywhere.popup.keep "Keep me on the development version">
<!ENTITY https-everywhere.popup.revert "Download the latest stable version">
1
0

r26920: {website} "How to use meek PT" blog post in announcements. (website/trunk/en)
by Matt Pagan 17 Aug '14
by Matt Pagan 17 Aug '14
17 Aug '14
Author: mttp
Date: 2014-08-17 18:39:36 +0000 (Sun, 17 Aug 2014)
New Revision: 26920
Modified:
website/trunk/en/index.wml
Log:
"How to use meek PT" blog post in announcements.
Modified: website/trunk/en/index.wml
===================================================================
--- website/trunk/en/index.wml 2014-08-15 00:18:25 UTC (rev 26919)
+++ website/trunk/en/index.wml 2014-08-17 18:39:36 UTC (rev 26920)
@@ -158,6 +158,12 @@
<table>
<tr>
<td>
+ <div class="calendar"><span class="month">Aug</span><br><span class="day">15</span></div>
+ <p>How to use the <a href="https://blog.torproject.org/blog/how-use-%E2%80%9Cmeek%E2%80%9D-pluggable-t…">"meek" pluggable transport</a>.</p>
+ </td>
+ </tr>
+ <tr>
+ <td>
<div class="calendar"><span class="month">Aug</span><br><span class="day">12</span></div>
<p>New <a
href="https://blog.torproject.org/blog/tor-browser-364-and-40-alpha-1-are-released">
@@ -180,12 +186,6 @@
bad relays</a>.</p>
</td>
</tr>
- <tr>
- <td>
- <div class="calendar"><span class="month">Jul</span><br><span class="day">26</span></div>
- <p>Transparency, Openness, and our <a href="https://blog.torproject.org/blog/transparency-openness-and-our-2013-financi…">2013 Financials</a>.</p>
- </td>
- </tr>
</table>
</div>
<div id="home-users">
1
0

[doctor/master] Minor wording change for missing consensus signatures
by atagar@torproject.org 17 Aug '14
by atagar@torproject.org 17 Aug '14
17 Aug '14
commit 22c2797c93e686a1c7e10199d97e246364fdc5b6
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sun Aug 17 11:26:57 2014 -0700
Minor wording change for missing consensus signatures
Request from Sebastian G.
https://lists.torproject.org/pipermail/tor-talk/2014-August/034431.html
---
data/consensus_health.cfg | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/data/consensus_health.cfg b/data/consensus_health.cfg
index ceb54ed..af7c200 100644
--- a/data/consensus_health.cfg
+++ b/data/consensus_health.cfg
@@ -6,7 +6,7 @@ msg DIFFERENT_RECOMMENDED_VERSION => The following directory authorities recomme
msg UNKNOWN_CONSENSUS_PARAMETERS => The following directory authorities set unknown consensus parameters: {parameters}
msg MISMATCH_CONSENSUS_PARAMETERS => The following directory authorities set conflicting consensus parameters: {parameters}
msg CERTIFICATE_ABOUT_TO_EXPIRE => The certificate of the following directory authority expires within the next {duration}: {authority}
-msg MISSING_SIGNATURE => Consensus belonging to {consensus_of} was missing the following authority signatures: {authorities}
+msg MISSING_SIGNATURE => Consensus fetched from {consensus_of} was missing the following authority signatures: {authorities}
msg MISSING_BANDWIDTH_SCANNERS => The following directory authorities are not reporting bandwidth scanner results: {authorities}
msg EXTRA_BANDWIDTH_SCANNERS => The following directory authorities were not expected to report bandwidth scanner results: {authorities}
msg MISSING_VOTES => The consensuses downloaded from the following authorities are missing votes that are contained in consensuses downloaded from other authorities: {authorities}
1
0
commit 339c63f0c8cd4374f6fa26484498eb6fa91b7bca
Author: Yawning Angel <yawning(a)torproject.org>
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 : */
1
0

[translation/gettor_completed] Update translations for gettor_completed
by translation@torproject.org 17 Aug '14
by translation@torproject.org 17 Aug '14
17 Aug '14
commit 29072f56c00c452a4db284e68e556f953d68a7eb
Author: Translation commit bot <translation(a)torproject.org>
Date: Sun Aug 17 16:45:08 2014 +0000
Update translations for gettor_completed
---
de/gettor.po | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/de/gettor.po b/de/gettor.po
index d0bb05f..5bdad52 100644
--- a/de/gettor.po
+++ b/de/gettor.po
@@ -17,7 +17,7 @@ msgstr ""
"Project-Id-Version: The Tor Project\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-01-19 13:40+0100\n"
-"PO-Revision-Date: 2014-08-17 16:10+0000\n"
+"PO-Revision-Date: 2014-08-17 16:21+0000\n"
"Last-Translator: Tobias Bannert\n"
"Language-Team: German (http://www.transifex.com/projects/p/torproject/language/de/)\n"
"MIME-Version: 1.0\n"
@@ -345,13 +345,13 @@ msgid ""
".zip. For example, if you recevied a file called \"windows.z\", rename it to \n"
"\"windows.zip\". You should then be able to extract the archive with common \n"
"file archiver programs that probably are already installed on your computer."
-msgstr "Eine Alternative um die .z Dateien zu entpacken, besteht darin diese in .zip umzubenennen. Hast du eine Dateie erhalten die \"windows.z\" heißt, kannst du diese in \"windows.zip\" umbenennen. Du solltest nun in der Lage sein das Archiv mit einem herkömmlichen Archivprogramm das bereits auf deinem Computer installiert ist, zu entpacken."
+msgstr "Eine Alternative, um .z-Dateien zu entpacken, besteht darin, diese in .zip umzubenennen. \nHaben Sie eine Datei erhalten, die »windows.z« heißt, können Sie diese in »windows.zip« umbenennen. \nSie sollten nun in der Lage sein das Archiv mit einem herkömmlichen Archivprogramm, \ndas bereits auf Ihrem Rechner installiert ist, zu entpacken."
#: lib/gettor/i18n.py:219
msgid ""
"Please reply to this mail, and tell me a single package name anywhere\n"
"in your reply. Here's a short explanation of what these packages are:"
-msgstr "Bitte beantworte diese mail und gib mir einen einzigen Paketnamen irgendwo in deiner Antwort an. Hier ist eine kurze Erklärung was das für Pakete sind:"
+msgstr "Bitte diese Nachricht beantworten und mir einen einzelnen Paketnamen, irgendwo in Ihrer Antwort, angeben. \nHier ist eine kurze Erklärung was das für Pakete sind:"
#: lib/gettor/i18n.py:222
msgid ""
@@ -359,7 +359,7 @@ msgid ""
"The Tor Browser Bundle package for Windows operating systems. If you're \n"
"running some version of Windows, like Windows XP, Windows Vista or \n"
"Windows 7, this is the package you should get."
-msgstr "windows:\nDas Tor Browser Bundle Paket für Windows Betriebssysteme. Benutzt du eine Version von Windows, wie z.B. Windows XP, Windows Vista oder Windows 7, ist dies das Paket, das du benötigst."
+msgstr "windows:\nDas Tor-Browser-Paket für Windows-Betriebssysteme. Benutzen Sie eine Version von Windows, wie z.B. Windows XP, Windows Vista oder Windows 7, ist dieses das Paket, das Sie benötigen."
#: lib/gettor/i18n.py:227
msgid ""
1
0

17 Aug '14
commit 7c94d12e5c2c6e0b150d424fc998cca8b98d06e8
Author: Translation commit bot <translation(a)torproject.org>
Date: Sun Aug 17 16:45:05 2014 +0000
Update translations for gettor
---
de/gettor.po | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/de/gettor.po b/de/gettor.po
index d0bb05f..5bdad52 100644
--- a/de/gettor.po
+++ b/de/gettor.po
@@ -17,7 +17,7 @@ msgstr ""
"Project-Id-Version: The Tor Project\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-01-19 13:40+0100\n"
-"PO-Revision-Date: 2014-08-17 16:10+0000\n"
+"PO-Revision-Date: 2014-08-17 16:21+0000\n"
"Last-Translator: Tobias Bannert\n"
"Language-Team: German (http://www.transifex.com/projects/p/torproject/language/de/)\n"
"MIME-Version: 1.0\n"
@@ -345,13 +345,13 @@ msgid ""
".zip. For example, if you recevied a file called \"windows.z\", rename it to \n"
"\"windows.zip\". You should then be able to extract the archive with common \n"
"file archiver programs that probably are already installed on your computer."
-msgstr "Eine Alternative um die .z Dateien zu entpacken, besteht darin diese in .zip umzubenennen. Hast du eine Dateie erhalten die \"windows.z\" heißt, kannst du diese in \"windows.zip\" umbenennen. Du solltest nun in der Lage sein das Archiv mit einem herkömmlichen Archivprogramm das bereits auf deinem Computer installiert ist, zu entpacken."
+msgstr "Eine Alternative, um .z-Dateien zu entpacken, besteht darin, diese in .zip umzubenennen. \nHaben Sie eine Datei erhalten, die »windows.z« heißt, können Sie diese in »windows.zip« umbenennen. \nSie sollten nun in der Lage sein das Archiv mit einem herkömmlichen Archivprogramm, \ndas bereits auf Ihrem Rechner installiert ist, zu entpacken."
#: lib/gettor/i18n.py:219
msgid ""
"Please reply to this mail, and tell me a single package name anywhere\n"
"in your reply. Here's a short explanation of what these packages are:"
-msgstr "Bitte beantworte diese mail und gib mir einen einzigen Paketnamen irgendwo in deiner Antwort an. Hier ist eine kurze Erklärung was das für Pakete sind:"
+msgstr "Bitte diese Nachricht beantworten und mir einen einzelnen Paketnamen, irgendwo in Ihrer Antwort, angeben. \nHier ist eine kurze Erklärung was das für Pakete sind:"
#: lib/gettor/i18n.py:222
msgid ""
@@ -359,7 +359,7 @@ msgid ""
"The Tor Browser Bundle package for Windows operating systems. If you're \n"
"running some version of Windows, like Windows XP, Windows Vista or \n"
"Windows 7, this is the package you should get."
-msgstr "windows:\nDas Tor Browser Bundle Paket für Windows Betriebssysteme. Benutzt du eine Version von Windows, wie z.B. Windows XP, Windows Vista oder Windows 7, ist dies das Paket, das du benötigst."
+msgstr "windows:\nDas Tor-Browser-Paket für Windows-Betriebssysteme. Benutzen Sie eine Version von Windows, wie z.B. Windows XP, Windows Vista oder Windows 7, ist dieses das Paket, das Sie benötigen."
#: lib/gettor/i18n.py:227
msgid ""
1
0

[translation/gettor_completed] Update translations for gettor_completed
by translation@torproject.org 17 Aug '14
by translation@torproject.org 17 Aug '14
17 Aug '14
commit 7bd910f1fe4aff1873c79d23b2e016e89c7b6ec2
Author: Translation commit bot <translation(a)torproject.org>
Date: Sun Aug 17 16:15:15 2014 +0000
Update translations for gettor_completed
---
de/gettor.po | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/de/gettor.po b/de/gettor.po
index b88dd60..d0bb05f 100644
--- a/de/gettor.po
+++ b/de/gettor.po
@@ -17,7 +17,7 @@ msgstr ""
"Project-Id-Version: The Tor Project\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-01-19 13:40+0100\n"
-"PO-Revision-Date: 2014-08-17 14:50+0000\n"
+"PO-Revision-Date: 2014-08-17 16:10+0000\n"
"Last-Translator: Tobias Bannert\n"
"Language-Team: German (http://www.transifex.com/projects/p/torproject/language/de/)\n"
"MIME-Version: 1.0\n"
@@ -331,13 +331,13 @@ msgid ""
"your computer yet, you can download it here:\n"
"\n"
" http://www.7-zip.org/"
-msgstr "Der einfachste Weg um die erhaltenen Dateien zu entpacken, besteht darin 7-zip zu installieren, ein freies Programm zu packen/entpacken. Sollte es bisher nicht auf deinem Computer installiert sein, kannst du es hier herunterladen:\n\n http://www.7-zip.org/"
+msgstr "Der einfachste Weg, um die erhaltenen Dateien zu entpacken, besteht darin 7-zip zu installieren, \nein freies Programm zum packen/entpacken von Dateien. \nSollte es bisher nicht auf Ihrem Rechner installiert sein, \nkönnen Sie es hier herunterladen:\n\n http://www.7-zip.org/"
#: lib/gettor/i18n.py:211
msgid ""
"When 7-Zip is installed, you can open the .z archive you received from\n"
"us by double-clicking on it."
-msgstr "Sobald 7-zip installiert ist, kannst du die .z Archive die du von uns erhalten hast durch einen Doppel-Klick öffnen."
+msgstr "Sobald 7-zip installiert ist, können Sie die .z-Archive, \ndie Sie von uns erhalten haben, durch einen Doppelklick öffnen."
#: lib/gettor/i18n.py:214
msgid ""
1
0

17 Aug '14
commit 23a6b8ee1522f6e1257a10e410ed371ca8556e8d
Author: Translation commit bot <translation(a)torproject.org>
Date: Sun Aug 17 16:15:09 2014 +0000
Update translations for gettor
---
de/gettor.po | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/de/gettor.po b/de/gettor.po
index b88dd60..d0bb05f 100644
--- a/de/gettor.po
+++ b/de/gettor.po
@@ -17,7 +17,7 @@ msgstr ""
"Project-Id-Version: The Tor Project\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-01-19 13:40+0100\n"
-"PO-Revision-Date: 2014-08-17 14:50+0000\n"
+"PO-Revision-Date: 2014-08-17 16:10+0000\n"
"Last-Translator: Tobias Bannert\n"
"Language-Team: German (http://www.transifex.com/projects/p/torproject/language/de/)\n"
"MIME-Version: 1.0\n"
@@ -331,13 +331,13 @@ msgid ""
"your computer yet, you can download it here:\n"
"\n"
" http://www.7-zip.org/"
-msgstr "Der einfachste Weg um die erhaltenen Dateien zu entpacken, besteht darin 7-zip zu installieren, ein freies Programm zu packen/entpacken. Sollte es bisher nicht auf deinem Computer installiert sein, kannst du es hier herunterladen:\n\n http://www.7-zip.org/"
+msgstr "Der einfachste Weg, um die erhaltenen Dateien zu entpacken, besteht darin 7-zip zu installieren, \nein freies Programm zum packen/entpacken von Dateien. \nSollte es bisher nicht auf Ihrem Rechner installiert sein, \nkönnen Sie es hier herunterladen:\n\n http://www.7-zip.org/"
#: lib/gettor/i18n.py:211
msgid ""
"When 7-Zip is installed, you can open the .z archive you received from\n"
"us by double-clicking on it."
-msgstr "Sobald 7-zip installiert ist, kannst du die .z Archive die du von uns erhalten hast durch einen Doppel-Klick öffnen."
+msgstr "Sobald 7-zip installiert ist, können Sie die .z-Archive, \ndie Sie von uns erhalten haben, durch einen Doppelklick öffnen."
#: lib/gettor/i18n.py:214
msgid ""
1
0

[translation/gettor_completed] Update translations for gettor_completed
by translation@torproject.org 17 Aug '14
by translation@torproject.org 17 Aug '14
17 Aug '14
commit 63868513f633d50a4c4a6949d4a1fa969ae4b634
Author: Translation commit bot <translation(a)torproject.org>
Date: Sun Aug 17 15:15:10 2014 +0000
Update translations for gettor_completed
---
de/gettor.po | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/de/gettor.po b/de/gettor.po
index e8c728f..b88dd60 100644
--- a/de/gettor.po
+++ b/de/gettor.po
@@ -17,7 +17,7 @@ msgstr ""
"Project-Id-Version: The Tor Project\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-01-19 13:40+0100\n"
-"PO-Revision-Date: 2014-08-17 14:44+0000\n"
+"PO-Revision-Date: 2014-08-17 14:50+0000\n"
"Last-Translator: Tobias Bannert\n"
"Language-Team: German (http://www.transifex.com/projects/p/torproject/language/de/)\n"
"MIME-Version: 1.0\n"
@@ -302,13 +302,13 @@ msgstr "Es wurde erfolgreich verstanden. Ihre Anfrage wird zur Zeit bearbeitet.\
msgid ""
"If it doesn't arrive, the package might be too big for your mail provider.\n"
"Try resending the mail from a GMAIL.COM, YAHOO.CN or YAHOO.COM account."
-msgstr "Sollte es nicht ankommen, könnte es daran liegen das dass Packet zu groß für deinen Email Provider ist. Versuche die Anfrage erneut über ein Konto bei GMAIL.COM, YAHOO.CN oder YAHOO.COM per Email zu senden."
+msgstr "Sollte es nicht ankommen, könnte es daran liegen, dass das Paket zu groß für Ihren E-Mail-Anbieter ist. \nDie Anfrage bitte erneut über ein Konto von GMAIL.COM, YAHOO.CN oder YAHOO.COM per E-Mail versenden."
#: lib/gettor/i18n.py:194
msgid ""
"Unfortunately we are currently experiencing problems and we can't fulfill\n"
"your request right now. Please be patient as we try to resolve this issue."
-msgstr "Leider treten aktuell Probleme auf, die uns daran hindern, Ihre Anfrage zeitnah zu beantworten. Bitte gedulden Sie sich, während wir versuchen das Problem zu lösen."
+msgstr "Leider treten aktuell Probleme auf, die uns daran hindern, Ihre Anfrage zeitnah zu beantworten. \nBitte gedulden Sie sich, während wir versuchen das Problem zu lösen."
#: lib/gettor/i18n.py:197
msgid ""
@@ -316,7 +316,7 @@ msgid ""
"requested. Please send us another package name or request the same package \n"
"again, but remove the 'split' keyword. In that case we'll send you the whole \n"
"package. Make sure this is what you want."
-msgstr "Leider ist für das von Ihnen angefragte Paket keine Paketaufteilung möglich. Bitte fordern Sie ein anderes Paket an oder erneuern Ihre Anfrage ohne das Stichwort 'split'. In diesem Fall würden wir Ihnen das gesamte Paket zuschicken. Überprüfen Sie zuvor die Identität dieses Packetes."
+msgstr "Leider ist für das von Ihnen angefragte Paket keine Paketaufteilung möglich. \nBitte fordern Sie ein anderes Paket an oder erneuern Sie Ihre Anfrage ohne das Stichwort »split« zu benutzen. \nIn diesem Fall würden wir Ihnen das gesamte Paket zuschicken. \nBitte überprüfen Sie zuvor die Identität dieses Paketes."
#: lib/gettor/i18n.py:202
msgid ""
1
0