diff --git a/.github/default.txt b/.github/default.txt
index 9f035513..ec6da57a 100644
--- a/.github/default.txt
+++ b/.github/default.txt
@@ -33,7 +33,7 @@ install_croc()
croc_os="unsupported"
croc_arch="unknown"
croc_arm=""
- croc_version="4.0.8"
+ croc_version="4.1.0"
# Termux on Android has $PREFIX set which already ends with /usr
diff --git a/.github/issue_template.md b/.github/issue_template.md
new file mode 100644
index 00000000..d2eb4680
--- /dev/null
+++ b/.github/issue_template.md
@@ -0,0 +1,17 @@
+## Expected Behavior
+
+
+## Actual Behavior
+
+
+## Steps to Reproduce the Problem
+
+ 1.
+ 1.
+ 1.
+
+## Specifications
+
+ - Version (`croc -version` or let me know latest commit in git log if using dev):
+ - Platform:
+ - Subsystem:
diff --git a/.travis.yml b/.travis.yml
index a294f6c3..987919ae 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,7 @@
language: go
go:
- - tip
+ - "1.11"
env:
- "PATH=/home/travis/gopath/bin:$PATH"
diff --git a/README.md b/README.md
index 3a72924f..c09668ed 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
src="https://user-images.githubusercontent.com/6550035/46709024-9b23ad00-cbf6-11e8-9fb2-ca8b20b7dbec.jpg"
width="408px" border="0" alt="croc">
-
+
## Overview
-*croc* uses "code phrases" to securely transfer files. A code phrase is a combination of three random words (mnemonicoded 4 bytes) which the sender shares with the recipient. The code phrase is used by the sender and recipient for password authenticated key exchange ([PAKE](https://github.com/schollz/pake)) to validate parties and generate a secure session key for end-to-end encryption. Since a code phrase can only be used once between two parties, an attacker has a chance of less than 1 in *4 billion* to guess the right code phrase to steal the file. Any attacker with the wrong code phrase will fail the PAKE and the sender will be notified. Only two people with the right code phrase will be able to computers transfer encrypted data through a relay.
+**Transmit encrypted data with a code phrase**
+
+*croc* securely transfers data using *code phrases* - a combination of three random words (mnemonicoded 4 bytes). The code phrase is shared between the sender and the recipient for password authenticated key exchange ([PAKE](https://github.com/schollz/pake)), a cryptographic method to use a shared weak key (the "code phrase") to generate a strong key for secure end-to-end encryption. By default, a code phrase can only be used once between two parties so an attacker would have a chance of less than 1 in *4 billion* to guess the code phrase correctly to steal the data. An attacker with the wrong code phrase will fail the PAKE and the sender will be notified without any data transfering. Only two people with the right code phrase will be able to computers transfer encrypted data through a relay.
+
+**Fast data transfer through TCP**
The actual data transfer is accomplished using a relay, either using raw TCP sockets or websockets. If both computers are on the LAN network then *croc* will use a local relay, otherwise a public relay is used. All the data going through the relay is encrypted using the PAKE-generated session key, so the relay can't spy on information passing through it. The data is transferred in blocks, where each block is compressed and encrypted, and the recipient keeps track of blocks received so that it can resume the transfer if interrupted.
-My motivation to write *croc*, as stupid as it sounds, is because I wanted to create a program that made it easy to send a 3GB+ PBS documentary to my friend in a different country. My friend has a Windows computer and is not comfortable using a terminal. So I wanted to write a program that, while secure, is simple to receive a file. *croc* accomplishes this, and now I find myself using it almost everyday at work. To receive a file you can just download the executable and double click on it (sending a file requires opening a terminal still, though).
+**Why another data transfer utility?**
+
+My motivation to write *croc*, as stupid as it sounds, is because I wanted to create a program that made it easy to send a 3GB+ PBS documentary to my friend in a different country. My friend has a Windows computer and is not comfortable using a terminal. So I wanted to write a program that, while secure, is simple to receive a file. *croc* accomplishes this, and now I find myself using it almost everyday at work. To receive a file you can just download the executable and double click on it (sending a file requires opening a terminal still, though). The name is inspired by the [fable of the frog and the crocodile](https://web.archive.org/web/20180926035731/http://allaboutfrogs.org/stories/crocodile.html).
## Examples
@@ -128,6 +134,17 @@ You can send files using your relay by entering `-relay` to change the relay tha
$ croc -relay "ws://myrelay.example.com" send [filename]
```
+### Configuration file
+
+You can also make some paramters static by using a configuration file. To get started with the config file just do
+
+```
+$ croc config
+```
+
+which will generate the file that you can edit.
+Any changes you make to the configuration file will be applied *before* the command-line flags, if any.
+
## License
diff --git a/go.mod b/go.mod
index 9aa3ab68..96aa8805 100644
--- a/go.mod
+++ b/go.mod
@@ -1,24 +1,23 @@
module github.com/schollz/croc
require (
+ github.com/BurntSushi/toml v0.3.1
github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575
- github.com/dustin/go-humanize v0.0.0-20180713052910-9f541cc9db5d
+ github.com/dustin/go-humanize v1.0.0
github.com/fatih/color v1.7.0 // indirect
github.com/gorilla/websocket v1.4.0
- github.com/mars9/crypt v0.0.0-20150406101210-65899cf653ff // indirect
github.com/mattn/go-colorable v0.0.9 // indirect
github.com/mattn/go-isatty v0.0.4 // indirect
+ github.com/mitchellh/go-homedir v1.0.0
github.com/pkg/errors v0.8.0
github.com/schollz/mnemonicode v1.0.1
- github.com/schollz/pake v1.0.2
+ github.com/schollz/pake v1.1.0
github.com/schollz/peerdiscovery v1.2.2
github.com/schollz/progressbar/v2 v2.6.0
github.com/schollz/spinner v0.0.0-20180925172146-6bbc5f7804f9
- github.com/schollz/utils v1.0.0
github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c
github.com/stretchr/testify v1.2.2
- github.com/tscholl2/siec v0.0.0-20180721101609-21667da05937
github.com/urfave/cli v1.20.0
- golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941
- golang.org/x/net v0.0.0-20181005035420-146acd28ed58 // indirect
+ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793
+ golang.org/x/net v0.0.0-20181017193950-04a2e542c03f // indirect
)
diff --git a/src/cli/cli.go b/src/cli/cli.go
index dee007fe..344357e7 100644
--- a/src/cli/cli.go
+++ b/src/cli/cli.go
@@ -8,6 +8,7 @@ import (
"log"
"os"
"path/filepath"
+ "runtime"
"strings"
"time"
@@ -19,10 +20,10 @@ import (
)
var Version string
-var codePhrase string
var cr *croc.Croc
func Run() {
+ runtime.GOMAXPROCS(runtime.NumCPU())
app := cli.NewApp()
app.Name = "croc"
if Version == "" {
@@ -60,6 +61,16 @@ func Run() {
return relay(c)
},
},
+ {
+ Name: "config",
+ Usage: "generates a config file",
+ Description: "the croc config can be used to set static parameters",
+ Flags: []cli.Flag{},
+ HelpName: "croc config",
+ Action: func(c *cli.Context) error {
+ return saveDefaultConfig(c)
+ },
+ },
}
app.Flags = []cli.Flag{
cli.StringFlag{Name: "addr", Value: "croc4.schollz.com", Usage: "address of the public relay"},
@@ -74,7 +85,8 @@ func Run() {
cli.BoolFlag{Name: "force-web", Usage: "force websockets"},
cli.StringFlag{Name: "port", Value: "8153", Usage: "port that the websocket listens on"},
cli.StringFlag{Name: "tcp-port", Value: "8154,8155,8156,8157,8158,8159,8160,8161", Usage: "ports that the tcp server listens on"},
- cli.StringFlag{Name: "curve", Value: "siec", Usage: "specify elliptic curve to use (p224, p256, p384, p521, siec)"},
+ cli.StringFlag{Name: "curve", Value: "siec", Usage: "specify elliptic curve to use for PAKE (p256, p384, p521, siec)"},
+ cli.StringFlag{Name: "out", Value: ".", Usage: "specify an output folder to receive the file"},
}
app.EnableBashCompletion = true
app.HideHelp = false
@@ -87,6 +99,7 @@ func Run() {
}
app.Before = func(c *cli.Context) error {
cr = croc.Init(c.GlobalBool("debug"))
+ cr.Version = Version
cr.AllowLocalDiscovery = true
cr.Address = c.GlobalString("addr")
cr.AddressTCPPorts = strings.Split(c.GlobalString("addr-tcp"), ",")
@@ -114,6 +127,10 @@ func Run() {
}
}
+func saveDefaultConfig(c *cli.Context) error {
+ return croc.SaveDefaultConfig()
+}
+
func send(c *cli.Context) error {
stat, _ := os.Stdin.Stat()
var fname string
@@ -146,11 +163,12 @@ func send(c *cli.Context) error {
cr.UseCompression = !c.Bool("no-compress")
cr.UseEncryption = !c.Bool("no-encrypt")
if c.String("code") != "" {
- codePhrase = c.String("code")
+ cr.Codephrase = c.String("code")
}
- if len(codePhrase) == 0 {
+ cr.LoadConfig()
+ if len(cr.Codephrase) == 0 {
// generate code phrase
- codePhrase = utils.GetRandomName()
+ cr.Codephrase = utils.GetRandomName()
}
// print the text
@@ -175,28 +193,32 @@ func send(c *cli.Context) error {
humanize.Bytes(uint64(fsize)),
fileOrFolder,
filename,
- codePhrase,
- codePhrase,
+ cr.Codephrase,
+ cr.Codephrase,
)
- return cr.Send(fname, codePhrase)
+ return cr.Send(fname, cr.Codephrase)
}
func receive(c *cli.Context) error {
if c.GlobalString("code") != "" {
- codePhrase = c.GlobalString("code")
+ cr.Codephrase = c.GlobalString("code")
}
if c.Args().First() != "" {
- codePhrase = c.Args().First()
+ cr.Codephrase = c.Args().First()
}
+ if c.GlobalString("out") != "" {
+ os.Chdir(c.GlobalString("out"))
+ }
+ cr.LoadConfig()
openFolder := false
if len(os.Args) == 1 {
// open folder since they didn't give any arguments
openFolder = true
}
- if codePhrase == "" {
- codePhrase = utils.GetInput("Enter receive code: ")
+ if cr.Codephrase == "" {
+ cr.Codephrase = utils.GetInput("Enter receive code: ")
}
- err := cr.Receive(codePhrase)
+ err := cr.Receive(cr.Codephrase)
if err == nil && openFolder {
cwd, _ := os.Getwd()
open.Run(cwd)
diff --git a/src/compress/compress.go b/src/compress/compress.go
index 36102e55..e23dd3e2 100644
--- a/src/compress/compress.go
+++ b/src/compress/compress.go
@@ -6,6 +6,13 @@ import (
"io"
)
+// CompressWithOption returns compressed data using the specified level
+func CompressWithOption(src []byte, level int) []byte {
+ compressedData := new(bytes.Buffer)
+ compress(src, compressedData, level)
+ return compressedData.Bytes()
+}
+
// Compress returns a compressed byte slice.
func Compress(src []byte) []byte {
compressedData := new(bytes.Buffer)
diff --git a/src/compress/compress_test.go b/src/compress/compress_test.go
new file mode 100644
index 00000000..563883df
--- /dev/null
+++ b/src/compress/compress_test.go
@@ -0,0 +1,78 @@
+package compress
+
+import (
+ "crypto/rand"
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var fable = []byte(`The Frog and the Crocodile
+Once, there was a frog who lived in the middle of a swamp. His entire family had lived in that swamp for generations, but this particular frog decided that he had had quite enough wetness to last him a lifetime. He decided that he was going to find a dry place to live instead.
+
+The only thing that separated him from dry land was a swampy, muddy, swiftly flowing river. But the river was home to all sorts of slippery, slittering snakes that loved nothing better than a good, plump frog for dinner, so Frog didn't dare try to swim across.
+
+So for many days, the frog stayed put, hopping along the bank, trying to think of a way to get across.
+
+The snakes hissed and jeered at him, daring him to come closer, but he refused. Occasionally they would slither closer, jaws open to attack, but the frog always leaped out of the way. But no matter how far upstream he searched or how far downstream, the frog wasn't able to find a way across the water.
+
+He had felt certain that there would be a bridge, or a place where the banks came together, yet all he found was more reeds and water. After a while, even the snakes stopped teasing him and went off in search of easier prey.
+
+The frog sighed in frustration and sat to sulk in the rushes. Suddenly, he spotted two big eyes staring at him from the water. The giant log-shaped animal opened its mouth and asked him, "What are you doing, Frog? Surely there are enough flies right there for a meal."
+
+The frog croaked in surprise and leaped away from the crocodile. That creature could swallow him whole in a moment without thinking about it! Once he was a satisfied that he was a safe distance away, he answered. "I'm tired of living in swampy waters, and I want to travel to the other side of the river. But if I swim across, the snakes will eat me."
+
+The crocodile harrumphed in agreement and sat, thinking, for a while. "Well, if you're afraid of the snakes, I could give you a ride across," he suggested.
+
+"Oh no, I don't think so," Frog answered quickly. "You'd eat me on the way over, or go underwater so the snakes could get me!"
+
+"Now why would I let the snakes get you? I think they're a terrible nuisance with all their hissing and slithering! The river would be much better off without them altogether! Anyway, if you're so worried that I might eat you, you can ride on my tail."
+
+The frog considered his offer. He did want to get to dry ground very badly, and there didn't seem to be any other way across the river. He looked at the crocodile from his short, squat buggy eyes and wondered about the crocodile's motives. But if he rode on the tail, the croc couldn't eat him anyway. And he was right about the snakes--no self-respecting crocodile would give a meal to the snakes.
+
+"Okay, it sounds like a good plan to me. Turn around so I can hop on your tail."
+
+The crocodile flopped his tail into the marshy mud and let the frog climb on, then he waddled out to the river. But he couldn't stick his tail into the water as a rudder because the frog was on it -- and if he put his tail in the water, the snakes would eat the frog. They clumsily floated downstream for a ways, until the crocodile said, "Hop onto my back so I can steer straight with my tail." The frog moved, and the journey smoothed out.
+
+From where he was sitting, the frog couldn't see much except the back of Crocodile's head. "Why don't you hop up on my head so you can see everything around us?" Crocodile invited. `)
+
+func BenchmarkCompressLevelMinusTwo(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ CompressWithOption(fable, -2)
+ }
+}
+
+func BenchmarkCompressLevelNine(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ CompressWithOption(fable, 9)
+ }
+}
+
+func BenchmarkCompressLevelMinusTwoBinary(b *testing.B) {
+ data := make([]byte, 1000000)
+ rand.Read(data)
+ for i := 0; i < b.N; i++ {
+ CompressWithOption(data, -2)
+ }
+}
+
+func BenchmarkCompressLevelNineBinary(b *testing.B) {
+ data := make([]byte, 1000000)
+ rand.Read(data)
+ for i := 0; i < b.N; i++ {
+ CompressWithOption(data, 9)
+ }
+}
+
+func TestCompress(t *testing.T) {
+ compressedB := CompressWithOption(fable, 9)
+ dataRateSavings := 100 * (1.0 - float64(len(compressedB))/float64(len(fable)))
+ fmt.Printf("Level 9: %2.0f%% percent space savings\n", dataRateSavings)
+ assert.True(t, len(compressedB) < len(fable))
+
+ compressedB = CompressWithOption(fable, -2)
+ dataRateSavings = 100 * (1.0 - float64(len(compressedB))/float64(len(fable)))
+ fmt.Printf("Level -2: %2.0f%% percent space savings\n", dataRateSavings)
+ assert.True(t, len(compressedB) < len(fable))
+}
diff --git a/src/croc/config.go b/src/croc/config.go
new file mode 100644
index 00000000..b1750599
--- /dev/null
+++ b/src/croc/config.go
@@ -0,0 +1,187 @@
+package croc
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path"
+ "path/filepath"
+ "time"
+
+ "github.com/BurntSushi/toml"
+ homedir "github.com/mitchellh/go-homedir"
+ "github.com/schollz/croc/src/utils"
+)
+
+type Config struct {
+ // Relay parameters
+ RelayWebsocketPort string
+ RelayTCPPorts []string
+
+ // Sender parameters
+ CurveType string
+
+ // Options for connecting to server
+ PublicServerIP string
+ AddressTCPPorts []string
+ AddressWebsocketPort string
+ Timeout time.Duration
+ LocalOnly bool
+ NoLocal bool
+
+ // Options for file transfering
+ UseEncryption bool
+ UseCompression bool
+ AllowLocalDiscovery bool
+ NoRecipientPrompt bool
+ ForceTCP bool
+ ForceWebsockets bool
+ Codephrase string
+}
+
+func defaultConfig() Config {
+ c := Config{}
+ cr := Init(false)
+ c.RelayWebsocketPort = cr.RelayWebsocketPort
+ c.RelayTCPPorts = cr.RelayTCPPorts
+ c.CurveType = cr.CurveType
+ c.PublicServerIP = cr.Address
+ c.AddressTCPPorts = cr.AddressTCPPorts
+ c.AddressWebsocketPort = cr.AddressWebsocketPort
+ c.Timeout = cr.Timeout
+ c.LocalOnly = cr.LocalOnly
+ c.NoLocal = cr.NoLocal
+ c.UseEncryption = cr.UseEncryption
+ c.UseCompression = cr.UseCompression
+ c.AllowLocalDiscovery = cr.AllowLocalDiscovery
+ c.NoRecipientPrompt = cr.NoRecipientPrompt
+ c.ForceTCP = false
+ c.ForceWebsockets = false
+ c.Codephrase = ""
+ return c
+}
+
+func SaveDefaultConfig() error {
+ homedir, err := homedir.Dir()
+ if err != nil {
+ return err
+ }
+ os.MkdirAll(path.Join(homedir, ".config", "croc"), 0644)
+ c := defaultConfig()
+ buf := new(bytes.Buffer)
+ toml.NewEncoder(buf).Encode(c)
+ confTOML := buf.String()
+ err = ioutil.WriteFile(path.Join(homedir, ".config", "croc", "config.toml"), []byte(confTOML), 0644)
+ if err == nil {
+ fmt.Printf("Default config file written at '%s'", filepath.Clean(path.Join(homedir, ".config", "croc", "config.toml")))
+ }
+ return err
+}
+
+// LoadConfig will override parameters
+func (cr *Croc) LoadConfig() (err error) {
+ homedir, err := homedir.Dir()
+ if err != nil {
+ return err
+ }
+ pathToConfig := path.Join(homedir, ".config", "croc", "config.toml")
+ if !utils.Exists(pathToConfig) {
+ // ignore if doesn't exist
+ return nil
+ }
+
+ var c Config
+ _, err = toml.DecodeFile(pathToConfig, &c)
+ if err != nil {
+ return
+ }
+
+ cDefault := defaultConfig()
+ // only load if things are different than defaults
+ // just in case the CLI parameters are used
+ if c.RelayWebsocketPort != cDefault.RelayWebsocketPort && cr.RelayWebsocketPort == cDefault.RelayWebsocketPort {
+ cr.RelayWebsocketPort = c.RelayWebsocketPort
+ fmt.Printf("loaded RelayWebsocketPort from config\n")
+ }
+ if !slicesEqual(c.RelayTCPPorts, cDefault.RelayTCPPorts) && slicesEqual(cr.RelayTCPPorts, cDefault.RelayTCPPorts) {
+ cr.RelayTCPPorts = c.RelayTCPPorts
+ fmt.Printf("loaded RelayTCPPorts from config\n")
+ }
+ if c.CurveType != cDefault.CurveType && cr.CurveType == cDefault.CurveType {
+ cr.CurveType = c.CurveType
+ fmt.Printf("loaded CurveType from config\n")
+ }
+ if c.PublicServerIP != cDefault.PublicServerIP && cr.Address == cDefault.PublicServerIP {
+ cr.Address = c.PublicServerIP
+ fmt.Printf("loaded Address from config\n")
+ }
+ if !slicesEqual(c.AddressTCPPorts, cDefault.AddressTCPPorts) {
+ cr.AddressTCPPorts = c.AddressTCPPorts
+ fmt.Printf("loaded AddressTCPPorts from config\n")
+ }
+ if c.AddressWebsocketPort != cDefault.AddressWebsocketPort && cr.AddressWebsocketPort == cDefault.AddressWebsocketPort {
+ cr.AddressWebsocketPort = c.AddressWebsocketPort
+ fmt.Printf("loaded AddressWebsocketPort from config\n")
+ }
+ if c.Timeout != cDefault.Timeout && cr.Timeout == cDefault.Timeout {
+ cr.Timeout = c.Timeout
+ fmt.Printf("loaded Timeout from config\n")
+ }
+ if c.LocalOnly != cDefault.LocalOnly && cr.LocalOnly == cDefault.LocalOnly {
+ cr.LocalOnly = c.LocalOnly
+ fmt.Printf("loaded LocalOnly from config\n")
+ }
+ if c.NoLocal != cDefault.NoLocal && cr.NoLocal == cDefault.NoLocal {
+ cr.NoLocal = c.NoLocal
+ fmt.Printf("loaded NoLocal from config\n")
+ }
+ if c.UseEncryption != cDefault.UseEncryption && cr.UseEncryption == cDefault.UseEncryption {
+ cr.UseEncryption = c.UseEncryption
+ fmt.Printf("loaded UseEncryption from config\n")
+ }
+ if c.UseCompression != cDefault.UseCompression && cr.UseCompression == cDefault.UseCompression {
+ cr.UseCompression = c.UseCompression
+ fmt.Printf("loaded UseCompression from config\n")
+ }
+ if c.AllowLocalDiscovery != cDefault.AllowLocalDiscovery && cr.AllowLocalDiscovery == cDefault.AllowLocalDiscovery {
+ cr.AllowLocalDiscovery = c.AllowLocalDiscovery
+ fmt.Printf("loaded AllowLocalDiscovery from config\n")
+ }
+ if c.NoRecipientPrompt != cDefault.NoRecipientPrompt && cr.NoRecipientPrompt == cDefault.NoRecipientPrompt {
+ cr.NoRecipientPrompt = c.NoRecipientPrompt
+ fmt.Printf("loaded NoRecipientPrompt from config\n")
+ }
+ if c.ForceWebsockets {
+ cr.ForceSend = 1
+ }
+ if c.ForceTCP {
+ cr.ForceSend = 2
+ }
+ if c.Codephrase != cDefault.Codephrase && cr.Codephrase == cDefault.Codephrase {
+ cr.Codephrase = c.Codephrase
+ fmt.Printf("loaded Codephrase from config\n")
+ }
+ return
+}
+
+// slicesEqual checcks if two slices are equal
+// from https://stackoverflow.com/a/15312097
+func slicesEqual(a, b []string) bool {
+ // If one is nil, the other must also be nil.
+ if (a == nil) != (b == nil) {
+ return false
+ }
+
+ if len(a) != len(b) {
+ return false
+ }
+
+ for i := range a {
+ if a[i] != b[i] {
+ return false
+ }
+ }
+
+ return true
+}
diff --git a/src/croc/croc.go b/src/croc/croc.go
index 11442c4c..fd11e207 100644
--- a/src/croc/croc.go
+++ b/src/croc/croc.go
@@ -17,6 +17,8 @@ func init() {
// Croc options
type Croc struct {
+ // Version is the version of croc
+ Version string
// Options for all
Debug bool
// ShowText will display text on the stderr
diff --git a/src/croc/models.go b/src/croc/models.go
new file mode 100644
index 00000000..de2650a3
--- /dev/null
+++ b/src/croc/models.go
@@ -0,0 +1,7 @@
+package croc
+
+type WebSocketMessage struct {
+ messageType int
+ message []byte
+ err error
+}
diff --git a/src/croc/recipient.go b/src/croc/recipient.go
index f05a9f6c..d41cf758 100644
--- a/src/croc/recipient.go
+++ b/src/croc/recipient.go
@@ -4,7 +4,6 @@ import (
"bufio"
"bytes"
"encoding/json"
- "errors"
"fmt"
"io/ioutil"
"os"
@@ -16,6 +15,7 @@ import (
log "github.com/cihub/seelog"
humanize "github.com/dustin/go-humanize"
"github.com/gorilla/websocket"
+ "github.com/pkg/errors"
"github.com/schollz/croc/src/comm"
"github.com/schollz/croc/src/compress"
"github.com/schollz/croc/src/crypt"
@@ -26,7 +26,6 @@ import (
"github.com/schollz/pake"
"github.com/schollz/progressbar/v2"
"github.com/schollz/spinner"
- "github.com/tscholl2/siec"
)
var DebugLevel string
@@ -38,6 +37,10 @@ func (cr *Croc) startRecipient(forceSend int, serverAddress string, tcpPorts []s
if err != nil {
if !strings.HasPrefix(err.Error(), "websocket: close 100") {
fmt.Fprintf(os.Stderr, "\n"+err.Error())
+ cr.StateString = err.Error()
+ err = errors.Wrap(err, "error in recipient:")
+ c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
+ time.Sleep(50 * time.Millisecond)
}
}
done <- struct{}{}
@@ -50,6 +53,8 @@ func (cr *Croc) receive(forceSend int, serverAddress string, tcpPorts []string,
var progressFile string
var resumeFile bool
var tcpConnections []comm.Comm
+ var Q *pake.Pake
+
dataChan := make(chan []byte, 1024*1024)
isConnectedIfUsingTCP := make(chan bool)
blocks := []string{}
@@ -74,20 +79,29 @@ func (cr *Croc) receive(forceSend int, serverAddress string, tcpPorts []string,
spin.Start()
defer spin.Stop()
- // pick an elliptic curve
- curve := siec.SIEC255()
// both parties should have a weak key
pw := []byte(codephrase)
- // initialize recipient Q ("1" indicates recipient)
- Q, err := pake.Init(pw, 1, curve, 1*time.Millisecond)
- if err != nil {
- return
- }
+ // start the reader
+ websocketMessages := make(chan WebSocketMessage, 1024)
+ go func() {
+ defer func() {
+ if r := recover(); r != nil {
+ log.Debugf("recovered from %s", r)
+ }
+ }()
+ for {
+ messageType, message, err := c.ReadMessage()
+ websocketMessages <- WebSocketMessage{messageType, message, err}
+ }
+ }()
step := 0
for {
- messageType, message, err := c.ReadMessage()
+ websocketMessage := <-websocketMessages
+ messageType := websocketMessage.messageType
+ message := websocketMessage.message
+ err := websocketMessage.err
if err != nil {
return err
}
@@ -101,20 +115,44 @@ func (cr *Croc) receive(forceSend int, serverAddress string, tcpPorts []string,
log.Debugf("got %d: %s", messageType, message)
switch step {
case 0:
- // sender has initiated, sends their ip address
- cr.OtherIP = string(message)
+ // sender has initiated, sends their initial data
+ var initialData models.Initial
+ err = json.Unmarshal(message, &initialData)
+ if err != nil {
+ err = errors.Wrap(err, "incompatible versions of croc")
+ return err
+ }
+ cr.OtherIP = initialData.IPAddress
log.Debugf("sender IP: %s", cr.OtherIP)
- // recipient begins by sending address
+ // check whether the version strings are compatible
+ versionStringsOther := strings.Split(initialData.VersionString, ".")
+ versionStringsSelf := strings.Split(cr.Version, ".")
+ if len(versionStringsOther) == 3 && len(versionStringsSelf) == 3 {
+ if versionStringsSelf[0] != versionStringsOther[0] || versionStringsSelf[1] != versionStringsOther[1] {
+ return fmt.Errorf("version sender %s is not compatible with recipient %s", cr.Version, initialData.VersionString)
+ }
+ }
+
+ // initialize the PAKE with the curve sent from the sender
+ Q, err = pake.InitCurve(pw, 1, initialData.CurveType, 1*time.Millisecond)
+ if err != nil {
+ err = errors.Wrap(err, "incompatible curve type")
+ return err
+ }
+
+ // recipient begins by sending back initial data to sender
ip := ""
if isLocal {
ip = utils.LocalIP()
} else {
ip, _ = utils.PublicIP()
}
- c.WriteMessage(websocket.BinaryMessage, []byte(ip))
+ initialData.VersionString = cr.Version
+ initialData.IPAddress = ip
+ bInitialData, _ := json.Marshal(initialData)
+ c.WriteMessage(websocket.BinaryMessage, bInitialData)
case 1:
-
// Q receives u
log.Debugf("[%d] Q computes k, sends H(k), v back to P", step)
if err := Q.Update(message); err != nil {
@@ -130,20 +168,27 @@ func (cr *Croc) receive(forceSend int, serverAddress string, tcpPorts []string,
// initialize TCP connections if using (possible, but unlikely, race condition)
go func() {
+ log.Debug("initializing TCP connections")
if !useWebsockets {
log.Debugf("connecting to server")
tcpConnections = make([]comm.Comm, len(tcpPorts))
+ var wg sync.WaitGroup
+ wg.Add(len(tcpPorts))
for i, tcpPort := range tcpPorts {
- log.Debugf("connecting to %d", i)
- var message string
- tcpConnections[i], message, err = connectToTCPServer(utils.SHA256(fmt.Sprintf("%d%x", i, sessionKey)), serverAddress+":"+tcpPort)
- if err != nil {
- log.Error(err)
- }
- if message != "recipient" {
- log.Errorf("got wrong message: %s", message)
- }
+ go func(i int, tcpPort string) {
+ defer wg.Done()
+ log.Debugf("connecting to %d", i)
+ var message string
+ tcpConnections[i], message, err = connectToTCPServer(utils.SHA256(fmt.Sprintf("%d%x", i, sessionKey)), serverAddress+":"+tcpPort)
+ if err != nil {
+ log.Error(err)
+ }
+ if message != "recipient" {
+ log.Errorf("got wrong message: %s", message)
+ }
+ }(i, tcpPort)
}
+ wg.Wait()
log.Debugf("fully connected")
}
isConnectedIfUsingTCP <- true
@@ -385,59 +430,101 @@ func (cr *Croc) receive(forceSend int, serverAddress string, tcpPorts []string,
startTime := time.Now()
if useWebsockets {
for {
- var messageType int
// read from websockets
- messageType, message, err = c.ReadMessage()
- if messageType != websocket.BinaryMessage {
+ websocketMessageData := <-websocketMessages
+ if bytes.HasPrefix(websocketMessageData.message, []byte("error")) {
+ return fmt.Errorf("%s", websocketMessageData.message)
+ }
+ if websocketMessageData.messageType != websocket.BinaryMessage {
continue
}
if err != nil {
log.Error(err)
return err
}
- if bytes.Equal(message, []byte("magic")) {
+ if bytes.Equal(websocketMessageData.message, []byte("magic")) {
log.Debug("got magic")
break
}
- dataChan <- message
- // select {
- // case dataChan <- message:
- // default:
- // log.Debug("blocked")
- // // no message sent
- // // block
- // dataChan <- message
- // }
+ dataChan <- websocketMessageData.message
}
} else {
log.Debugf("starting listening with tcp with %d connections", len(tcpConnections))
- // using TCP
- var wg sync.WaitGroup
- wg.Add(len(tcpConnections))
- for i := range tcpConnections {
- defer func(i int) {
- log.Debugf("closing connection %d", i)
- tcpConnections[i].Close()
- }(i)
- go func(wg *sync.WaitGroup, j int) {
- defer wg.Done()
- for {
- log.Debugf("waiting to read on %d", j)
- // read from TCP connection
- message, _, _, err := tcpConnections[j].Read()
- // log.Debugf("message: %s", message)
- if err != nil {
- panic(err)
- }
- if bytes.Equal(message, []byte("magic")) {
- log.Debugf("%d got magic, leaving", j)
+
+ // check to see if any messages are sent
+ stopMessageSignal := make(chan bool, 1)
+ errorsDuringTransfer := make(chan error, 24)
+ go func() {
+ for {
+ select {
+ case sig := <-stopMessageSignal:
+ errorsDuringTransfer <- nil
+ log.Debugf("got message signal: %+v", sig)
+ return
+ case wsMessage := <-websocketMessages:
+ log.Debugf("got message: %s", wsMessage.message)
+ if bytes.HasPrefix(wsMessage.message, []byte("error")) {
+ log.Debug("stopping transfer")
+ for i := 0; i < len(tcpConnections)+1; i++ {
+ errorsDuringTransfer <- fmt.Errorf("%s", wsMessage.message)
+ }
return
}
- dataChan <- message
+ default:
+ continue
}
- }(&wg, i)
+ }
+ }()
+
+ // using TCP
+ go func() {
+ var wg sync.WaitGroup
+ wg.Add(len(tcpConnections))
+ for i := range tcpConnections {
+ defer func(i int) {
+ log.Debugf("closing connection %d", i)
+ tcpConnections[i].Close()
+ }(i)
+ go func(wg *sync.WaitGroup, j int) {
+ defer wg.Done()
+ for {
+ select {
+ case _ = <-errorsDuringTransfer:
+ log.Debugf("%d got stop", i)
+ return
+ default:
+ }
+
+ log.Debugf("waiting to read on %d", j)
+ // read from TCP connection
+ message, _, _, err := tcpConnections[j].Read()
+ // log.Debugf("message: %s", message)
+ if err != nil {
+ panic(err)
+ }
+ if bytes.Equal(message, []byte("magic")) {
+ log.Debugf("%d got magic, leaving", j)
+ return
+ }
+ dataChan <- message
+ }
+ }(&wg, i)
+ }
+ log.Debug("waiting for tcp goroutines")
+ wg.Wait()
+ errorsDuringTransfer <- nil
+ }()
+
+ // block until this is done
+
+ log.Debug("waiting for error")
+ errorDuringTransfer := <-errorsDuringTransfer
+ log.Debug("sending stop message signal")
+ stopMessageSignal <- true
+ if errorDuringTransfer != nil {
+ log.Debugf("got error during transfer: %s", errorDuringTransfer.Error())
+ return errorDuringTransfer
}
- wg.Wait()
}
_ = <-finished
diff --git a/src/croc/sender.go b/src/croc/sender.go
index 7e671115..b6f4248d 100644
--- a/src/croc/sender.go
+++ b/src/croc/sender.go
@@ -26,7 +26,6 @@ import (
"github.com/schollz/pake"
progressbar "github.com/schollz/progressbar/v2"
"github.com/schollz/spinner"
- "github.com/tscholl2/siec"
)
// Send is the async call to send data
@@ -35,10 +34,14 @@ func (cr *Croc) startSender(forceSend int, serverAddress string, tcpPorts []stri
log.Debugf("sending %s", fname)
err := cr.send(forceSend, serverAddress, tcpPorts, isLocal, c, fname, codephrase, useCompression, useEncryption)
if err != nil {
+ log.Debug(err)
if !strings.HasPrefix(err.Error(), "websocket: close 100") {
fmt.Fprintf(os.Stderr, "\n"+err.Error())
+ err = errors.Wrap(err, "error in sender:")
+ c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
+ time.Sleep(50 * time.Millisecond)
+ cr.StateString = err.Error()
}
- cr.StateString = err.Error()
}
done <- struct{}{}
@@ -91,19 +94,34 @@ func (cr *Croc) send(forceSend int, serverAddress string, tcpPorts []string, isL
spin.Writer = os.Stderr
defer spin.Stop()
- // pick an elliptic curve
- curve := siec.SIEC255()
// both parties should have a weak key
pw := []byte(codephrase)
// initialize sender P ("0" indicates sender)
- P, err := pake.Init(pw, 0, curve, 1*time.Millisecond)
+ P, err := pake.InitCurve(pw, 0, cr.CurveType, 1*time.Millisecond)
if err != nil {
return
}
+ // start the reader
+ websocketMessages := make(chan WebSocketMessage, 1024)
+ go func() {
+ defer func() {
+ if r := recover(); r != nil {
+ log.Debugf("recovered from %s", r)
+ }
+ }()
+ for {
+ messageType, message, err := c.ReadMessage()
+ websocketMessages <- WebSocketMessage{messageType, message, err}
+ }
+ }()
+
step := 0
for {
- messageType, message, errRead := c.ReadMessage()
+ websocketMessage := <-websocketMessages
+ messageType := websocketMessage.messageType
+ message := websocketMessage.message
+ errRead := websocketMessage.err
if errRead != nil {
return errRead
}
@@ -113,6 +131,9 @@ func (cr *Croc) send(forceSend int, serverAddress string, tcpPorts []string, isL
if messageType == websocket.TextMessage && bytes.Equal(message, []byte("interrupt")) {
return errors.New("\rinterrupted by other party")
}
+ if messageType == websocket.TextMessage && bytes.HasPrefix(message, []byte("err")) {
+ return errors.New("\r" + string(message))
+ }
log.Debugf("got %d: %s", messageType, message)
switch step {
case 0:
@@ -123,11 +144,24 @@ func (cr *Croc) send(forceSend int, serverAddress string, tcpPorts []string, isL
} else {
ip, _ = utils.PublicIP()
}
- // send my IP address
- c.WriteMessage(websocket.BinaryMessage, []byte(ip))
+
+ initialData := models.Initial{
+ CurveType: cr.CurveType,
+ IPAddress: ip,
+ VersionString: cr.Version, // version should match
+ }
+ bInitialData, _ := json.Marshal(initialData)
+ // send the initial data
+ c.WriteMessage(websocket.BinaryMessage, bInitialData)
case 1:
- // first receive the IP address from the sender
- cr.OtherIP = string(message)
+ // first receive the initial data from the recipient
+ var initialData models.Initial
+ err = json.Unmarshal(message, &initialData)
+ if err != nil {
+ err = errors.Wrap(err, "incompatible versions of croc")
+ return
+ }
+ cr.OtherIP = initialData.IPAddress
log.Debugf("recipient IP: %s", cr.OtherIP)
go func() {
@@ -250,17 +284,23 @@ func (cr *Croc) send(forceSend int, serverAddress string, tcpPorts []string, isL
go func() {
if !useWebsockets {
log.Debugf("connecting to server")
+ var wg sync.WaitGroup
+ wg.Add(len(tcpPorts))
for i, tcpPort := range tcpPorts {
- log.Debugf("connecting to %s on connection %d", tcpPort, i)
- var message string
- tcpConnections[i], message, err = connectToTCPServer(utils.SHA256(fmt.Sprintf("%d%x", i, sessionKey)), serverAddress+":"+tcpPort)
- if err != nil {
- log.Error(err)
- }
- if message != "sender" {
- log.Errorf("got wrong message: %s", message)
- }
+ go func(i int, tcpPort string) {
+ defer wg.Done()
+ log.Debugf("connecting to %s on connection %d", tcpPort, i)
+ var message string
+ tcpConnections[i], message, err = connectToTCPServer(utils.SHA256(fmt.Sprintf("%d%x", i, sessionKey)), serverAddress+":"+tcpPort)
+ if err != nil {
+ log.Error(err)
+ }
+ if message != "sender" {
+ log.Errorf("got wrong message: %s", message)
+ }
+ }(i, tcpPort)
}
+ wg.Wait()
}
isConnectedIfUsingTCP <- true
}()
@@ -395,6 +435,32 @@ func (cr *Croc) send(forceSend int, serverAddress string, tcpPorts []string, isL
} else {
_ = <-isConnectedIfUsingTCP
log.Debug("connected and ready to send on tcp")
+
+ // check to see if any messages are sent
+ stopMessageSignal := make(chan bool, 1)
+ errorsDuringTransfer := make(chan error, 24)
+ go func() {
+ for {
+ select {
+ case sig := <-stopMessageSignal:
+ errorsDuringTransfer <- nil
+ log.Debugf("got message signal: %+v", sig)
+ return
+ case wsMessage := <-websocketMessages:
+ log.Debugf("got message: %s", wsMessage.message)
+ if bytes.HasPrefix(wsMessage.message, []byte("error")) {
+ log.Debug("stopping transfer")
+ for i := 0; i < len(tcpConnections)+1; i++ {
+ errorsDuringTransfer <- fmt.Errorf("%s", wsMessage.message)
+ }
+ return
+ }
+ default:
+ continue
+ }
+ }
+ }()
+
var wg sync.WaitGroup
wg.Add(len(tcpConnections))
for i := range tcpConnections {
@@ -405,16 +471,23 @@ func (cr *Croc) send(forceSend int, serverAddress string, tcpPorts []string, isL
go func(i int, wg *sync.WaitGroup, dataChan <-chan DataChan) {
defer wg.Done()
for data := range dataChan {
+ select {
+ case _ = <-errorsDuringTransfer:
+ log.Debugf("%d got stop", i)
+ return
+ default:
+ }
if data.err != nil {
log.Error(data.err)
return
}
cr.Bar.Add(data.bytesRead)
// write data to tcp connection
- _, err = tcpConnections[i].Write(data.b)
- if err != nil {
- err = errors.Wrap(err, "problem writing message")
- log.Error(err)
+ _, errTcp := tcpConnections[i].Write(data.b)
+ if errTcp != nil {
+ errTcp = errors.Wrap(errTcp, "problem writing message")
+ log.Debug(errTcp)
+ errorsDuringTransfer <- errTcp
return
}
if bytes.Equal(data.b, []byte("magic")) {
@@ -424,7 +497,18 @@ func (cr *Croc) send(forceSend int, serverAddress string, tcpPorts []string, isL
}
}(i, &wg, dataChan)
}
+
+ // block until this is done
+ log.Debug("waiting for tcp goroutines")
wg.Wait()
+ log.Debug("sending stop message signal")
+ stopMessageSignal <- true
+ log.Debug("waiting for error")
+ errorDuringTransfer := <-errorsDuringTransfer
+ if errorDuringTransfer != nil {
+ log.Debugf("got error during transfer: %s", errorDuringTransfer.Error())
+ return errorDuringTransfer
+ }
}
cr.Bar.Finish()
diff --git a/src/croc/sending.go b/src/croc/sending.go
index 74d972e2..1e2834fb 100644
--- a/src/croc/sending.go
+++ b/src/croc/sending.go
@@ -12,8 +12,8 @@ import (
log "github.com/cihub/seelog"
"github.com/gorilla/websocket"
"github.com/schollz/croc/src/relay"
+ "github.com/schollz/croc/src/utils"
"github.com/schollz/peerdiscovery"
- "github.com/schollz/utils"
)
// Send the file
@@ -96,7 +96,7 @@ func (c *Croc) Receive(codephrase string) (err error) {
log.Debug(errDiscover)
}
if len(discovered) > 0 {
- if discovered[0].Address == utils.GetLocalIP() {
+ if discovered[0].Address == utils.LocalIP() {
discovered[0].Address = "localhost"
}
log.Debugf("discovered %s:%s", discovered[0].Address, discovered[0].Payload)
@@ -153,6 +153,7 @@ func (c *Croc) sendReceive(address, websocketPort string, tcpPorts []string, fna
log.Debugf("connecting to %s", websocketAddress)
sock, _, err := websocket.DefaultDialer.Dial(websocketAddress, nil)
if err != nil {
+ log.Error(err)
return
}
defer sock.Close()
@@ -160,6 +161,7 @@ func (c *Croc) sendReceive(address, websocketPort string, tcpPorts []string, fna
// tell the websockets we are connected
err = sock.WriteMessage(websocket.BinaryMessage, []byte("connected"))
if err != nil {
+ log.Error(err)
return err
}
@@ -172,13 +174,14 @@ func (c *Croc) sendReceive(address, websocketPort string, tcpPorts []string, fna
for {
select {
case <-done:
+ log.Debug("received done signal")
return nil
case <-interrupt:
if !c.Debug {
SetDebugLevel("critical")
}
log.Debug("interrupt")
- err = sock.WriteMessage(websocket.TextMessage, []byte("interrupt"))
+ err = sock.WriteMessage(websocket.TextMessage, []byte("error: interrupted by other party"))
if err != nil {
return err
}
diff --git a/src/models/initial.go b/src/models/initial.go
new file mode 100644
index 00000000..75b0877b
--- /dev/null
+++ b/src/models/initial.go
@@ -0,0 +1,7 @@
+package models
+
+type Initial struct {
+ CurveType string
+ IPAddress string
+ VersionString string
+}