0
0
Fork 0
mirror of https://github.com/schollz/croc.git synced 2025-10-11 13:21:00 +02:00

Merge pull request #105 from schollz/4.1-sync

4.1 sync
This commit is contained in:
Zack 2018-10-23 05:30:59 -07:00 committed by GitHub
commit 8d43a1f63a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 628 additions and 111 deletions

2
.github/default.txt vendored
View file

@ -33,7 +33,7 @@ install_croc()
croc_os="unsupported" croc_os="unsupported"
croc_arch="unknown" croc_arch="unknown"
croc_arm="" croc_arm=""
croc_version="4.0.8" croc_version="4.1.0"
# Termux on Android has $PREFIX set which already ends with /usr # Termux on Android has $PREFIX set which already ends with /usr

17
.github/issue_template.md vendored Normal file
View file

@ -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:

View file

@ -1,7 +1,7 @@
language: go language: go
go: go:
- tip - "1.11"
env: env:
- "PATH=/home/travis/gopath/bin:$PATH" - "PATH=/home/travis/gopath/bin:$PATH"

View file

@ -4,7 +4,7 @@
src="https://user-images.githubusercontent.com/6550035/46709024-9b23ad00-cbf6-11e8-9fb2-ca8b20b7dbec.jpg" src="https://user-images.githubusercontent.com/6550035/46709024-9b23ad00-cbf6-11e8-9fb2-ca8b20b7dbec.jpg"
width="408px" border="0" alt="croc"> width="408px" border="0" alt="croc">
<br> <br>
<a href="https://github.com/schollz/croc/releases/latest"><img src="https://img.shields.io/badge/version-4.0.8-brightgreen.svg?style=flat-square" alt="Version"></a> <a href="https://github.com/schollz/croc/releases/latest"><img src="https://img.shields.io/badge/version-4.1.0-brightgreen.svg?style=flat-square" alt="Version"></a>
<img src="https://img.shields.io/badge/coverage-77%25-brightgreen.svg?style=flat-square" alt="Code coverage"> <img src="https://img.shields.io/badge/coverage-77%25-brightgreen.svg?style=flat-square" alt="Code coverage">
<a href="https://travis-ci.org/schollz/croc"><img <a href="https://travis-ci.org/schollz/croc"><img
src="https://img.shields.io/travis-ci/schollz/croc.svg?style=flat-square" alt="Build src="https://img.shields.io/travis-ci/schollz/croc.svg?style=flat-square" alt="Build
@ -19,11 +19,17 @@ Status"></a>
## Overview ## 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. 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 ## 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] $ 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 ## License

13
go.mod
View file

@ -1,24 +1,23 @@
module github.com/schollz/croc module github.com/schollz/croc
require ( require (
github.com/BurntSushi/toml v0.3.1
github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 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/fatih/color v1.7.0 // indirect
github.com/gorilla/websocket v1.4.0 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-colorable v0.0.9 // indirect
github.com/mattn/go-isatty v0.0.4 // 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/pkg/errors v0.8.0
github.com/schollz/mnemonicode v1.0.1 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/peerdiscovery v1.2.2
github.com/schollz/progressbar/v2 v2.6.0 github.com/schollz/progressbar/v2 v2.6.0
github.com/schollz/spinner v0.0.0-20180925172146-6bbc5f7804f9 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/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c
github.com/stretchr/testify v1.2.2 github.com/stretchr/testify v1.2.2
github.com/tscholl2/siec v0.0.0-20180721101609-21667da05937
github.com/urfave/cli v1.20.0 github.com/urfave/cli v1.20.0
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793
golang.org/x/net v0.0.0-20181005035420-146acd28ed58 // indirect golang.org/x/net v0.0.0-20181017193950-04a2e542c03f // indirect
) )

View file

@ -8,6 +8,7 @@ import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"time" "time"
@ -19,10 +20,10 @@ import (
) )
var Version string var Version string
var codePhrase string
var cr *croc.Croc var cr *croc.Croc
func Run() { func Run() {
runtime.GOMAXPROCS(runtime.NumCPU())
app := cli.NewApp() app := cli.NewApp()
app.Name = "croc" app.Name = "croc"
if Version == "" { if Version == "" {
@ -60,6 +61,16 @@ func Run() {
return relay(c) 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{ app.Flags = []cli.Flag{
cli.StringFlag{Name: "addr", Value: "croc4.schollz.com", Usage: "address of the public relay"}, 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.BoolFlag{Name: "force-web", Usage: "force websockets"},
cli.StringFlag{Name: "port", Value: "8153", Usage: "port that the websocket listens on"}, 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: "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.EnableBashCompletion = true
app.HideHelp = false app.HideHelp = false
@ -87,6 +99,7 @@ func Run() {
} }
app.Before = func(c *cli.Context) error { app.Before = func(c *cli.Context) error {
cr = croc.Init(c.GlobalBool("debug")) cr = croc.Init(c.GlobalBool("debug"))
cr.Version = Version
cr.AllowLocalDiscovery = true cr.AllowLocalDiscovery = true
cr.Address = c.GlobalString("addr") cr.Address = c.GlobalString("addr")
cr.AddressTCPPorts = strings.Split(c.GlobalString("addr-tcp"), ",") 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 { func send(c *cli.Context) error {
stat, _ := os.Stdin.Stat() stat, _ := os.Stdin.Stat()
var fname string var fname string
@ -146,11 +163,12 @@ func send(c *cli.Context) error {
cr.UseCompression = !c.Bool("no-compress") cr.UseCompression = !c.Bool("no-compress")
cr.UseEncryption = !c.Bool("no-encrypt") cr.UseEncryption = !c.Bool("no-encrypt")
if c.String("code") != "" { 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 // generate code phrase
codePhrase = utils.GetRandomName() cr.Codephrase = utils.GetRandomName()
} }
// print the text // print the text
@ -175,28 +193,32 @@ func send(c *cli.Context) error {
humanize.Bytes(uint64(fsize)), humanize.Bytes(uint64(fsize)),
fileOrFolder, fileOrFolder,
filename, filename,
codePhrase, cr.Codephrase,
codePhrase, cr.Codephrase,
) )
return cr.Send(fname, codePhrase) return cr.Send(fname, cr.Codephrase)
} }
func receive(c *cli.Context) error { func receive(c *cli.Context) error {
if c.GlobalString("code") != "" { if c.GlobalString("code") != "" {
codePhrase = c.GlobalString("code") cr.Codephrase = c.GlobalString("code")
} }
if c.Args().First() != "" { 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 openFolder := false
if len(os.Args) == 1 { if len(os.Args) == 1 {
// open folder since they didn't give any arguments // open folder since they didn't give any arguments
openFolder = true openFolder = true
} }
if codePhrase == "" { if cr.Codephrase == "" {
codePhrase = utils.GetInput("Enter receive code: ") cr.Codephrase = utils.GetInput("Enter receive code: ")
} }
err := cr.Receive(codePhrase) err := cr.Receive(cr.Codephrase)
if err == nil && openFolder { if err == nil && openFolder {
cwd, _ := os.Getwd() cwd, _ := os.Getwd()
open.Run(cwd) open.Run(cwd)

View file

@ -6,6 +6,13 @@ import (
"io" "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. // Compress returns a compressed byte slice.
func Compress(src []byte) []byte { func Compress(src []byte) []byte {
compressedData := new(bytes.Buffer) compressedData := new(bytes.Buffer)

View file

@ -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))
}

187
src/croc/config.go Normal file
View file

@ -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
}

View file

@ -17,6 +17,8 @@ func init() {
// Croc options // Croc options
type Croc struct { type Croc struct {
// Version is the version of croc
Version string
// Options for all // Options for all
Debug bool Debug bool
// ShowText will display text on the stderr // ShowText will display text on the stderr

7
src/croc/models.go Normal file
View file

@ -0,0 +1,7 @@
package croc
type WebSocketMessage struct {
messageType int
message []byte
err error
}

View file

@ -4,7 +4,6 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@ -16,6 +15,7 @@ import (
log "github.com/cihub/seelog" log "github.com/cihub/seelog"
humanize "github.com/dustin/go-humanize" humanize "github.com/dustin/go-humanize"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/pkg/errors"
"github.com/schollz/croc/src/comm" "github.com/schollz/croc/src/comm"
"github.com/schollz/croc/src/compress" "github.com/schollz/croc/src/compress"
"github.com/schollz/croc/src/crypt" "github.com/schollz/croc/src/crypt"
@ -26,7 +26,6 @@ import (
"github.com/schollz/pake" "github.com/schollz/pake"
"github.com/schollz/progressbar/v2" "github.com/schollz/progressbar/v2"
"github.com/schollz/spinner" "github.com/schollz/spinner"
"github.com/tscholl2/siec"
) )
var DebugLevel string var DebugLevel string
@ -38,6 +37,10 @@ func (cr *Croc) startRecipient(forceSend int, serverAddress string, tcpPorts []s
if err != nil { if err != nil {
if !strings.HasPrefix(err.Error(), "websocket: close 100") { if !strings.HasPrefix(err.Error(), "websocket: close 100") {
fmt.Fprintf(os.Stderr, "\n"+err.Error()) 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{}{} done <- struct{}{}
@ -50,6 +53,8 @@ func (cr *Croc) receive(forceSend int, serverAddress string, tcpPorts []string,
var progressFile string var progressFile string
var resumeFile bool var resumeFile bool
var tcpConnections []comm.Comm var tcpConnections []comm.Comm
var Q *pake.Pake
dataChan := make(chan []byte, 1024*1024) dataChan := make(chan []byte, 1024*1024)
isConnectedIfUsingTCP := make(chan bool) isConnectedIfUsingTCP := make(chan bool)
blocks := []string{} blocks := []string{}
@ -74,20 +79,29 @@ func (cr *Croc) receive(forceSend int, serverAddress string, tcpPorts []string,
spin.Start() spin.Start()
defer spin.Stop() defer spin.Stop()
// pick an elliptic curve
curve := siec.SIEC255()
// both parties should have a weak key // both parties should have a weak key
pw := []byte(codephrase) pw := []byte(codephrase)
// initialize recipient Q ("1" indicates recipient) // start the reader
Q, err := pake.Init(pw, 1, curve, 1*time.Millisecond) websocketMessages := make(chan WebSocketMessage, 1024)
if err != nil { go func() {
return 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 step := 0
for { for {
messageType, message, err := c.ReadMessage() websocketMessage := <-websocketMessages
messageType := websocketMessage.messageType
message := websocketMessage.message
err := websocketMessage.err
if err != nil { if err != nil {
return err return err
} }
@ -101,20 +115,44 @@ func (cr *Croc) receive(forceSend int, serverAddress string, tcpPorts []string,
log.Debugf("got %d: %s", messageType, message) log.Debugf("got %d: %s", messageType, message)
switch step { switch step {
case 0: case 0:
// sender has initiated, sends their ip address // sender has initiated, sends their initial data
cr.OtherIP = string(message) 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) 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 := "" ip := ""
if isLocal { if isLocal {
ip = utils.LocalIP() ip = utils.LocalIP()
} else { } else {
ip, _ = utils.PublicIP() 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: case 1:
// Q receives u // Q receives u
log.Debugf("[%d] Q computes k, sends H(k), v back to P", step) log.Debugf("[%d] Q computes k, sends H(k), v back to P", step)
if err := Q.Update(message); err != nil { if err := Q.Update(message); err != nil {
@ -130,10 +168,15 @@ func (cr *Croc) receive(forceSend int, serverAddress string, tcpPorts []string,
// initialize TCP connections if using (possible, but unlikely, race condition) // initialize TCP connections if using (possible, but unlikely, race condition)
go func() { go func() {
log.Debug("initializing TCP connections")
if !useWebsockets { if !useWebsockets {
log.Debugf("connecting to server") log.Debugf("connecting to server")
tcpConnections = make([]comm.Comm, len(tcpPorts)) tcpConnections = make([]comm.Comm, len(tcpPorts))
var wg sync.WaitGroup
wg.Add(len(tcpPorts))
for i, tcpPort := range tcpPorts { for i, tcpPort := range tcpPorts {
go func(i int, tcpPort string) {
defer wg.Done()
log.Debugf("connecting to %d", i) log.Debugf("connecting to %d", i)
var message string var message string
tcpConnections[i], message, err = connectToTCPServer(utils.SHA256(fmt.Sprintf("%d%x", i, sessionKey)), serverAddress+":"+tcpPort) tcpConnections[i], message, err = connectToTCPServer(utils.SHA256(fmt.Sprintf("%d%x", i, sessionKey)), serverAddress+":"+tcpPort)
@ -143,7 +186,9 @@ func (cr *Croc) receive(forceSend int, serverAddress string, tcpPorts []string,
if message != "recipient" { if message != "recipient" {
log.Errorf("got wrong message: %s", message) log.Errorf("got wrong message: %s", message)
} }
}(i, tcpPort)
} }
wg.Wait()
log.Debugf("fully connected") log.Debugf("fully connected")
} }
isConnectedIfUsingTCP <- true isConnectedIfUsingTCP <- true
@ -385,33 +430,54 @@ func (cr *Croc) receive(forceSend int, serverAddress string, tcpPorts []string,
startTime := time.Now() startTime := time.Now()
if useWebsockets { if useWebsockets {
for { for {
var messageType int
// read from websockets // read from websockets
messageType, message, err = c.ReadMessage() websocketMessageData := <-websocketMessages
if messageType != websocket.BinaryMessage { if bytes.HasPrefix(websocketMessageData.message, []byte("error")) {
return fmt.Errorf("%s", websocketMessageData.message)
}
if websocketMessageData.messageType != websocket.BinaryMessage {
continue continue
} }
if err != nil { if err != nil {
log.Error(err) log.Error(err)
return err return err
} }
if bytes.Equal(message, []byte("magic")) { if bytes.Equal(websocketMessageData.message, []byte("magic")) {
log.Debug("got magic") log.Debug("got magic")
break break
} }
dataChan <- message dataChan <- websocketMessageData.message
// select {
// case dataChan <- message:
// default:
// log.Debug("blocked")
// // no message sent
// // block
// dataChan <- message
// }
} }
} else { } else {
log.Debugf("starting listening with tcp with %d connections", len(tcpConnections)) log.Debugf("starting listening with tcp with %d connections", len(tcpConnections))
// 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
}
}
}()
// using TCP // using TCP
go func() {
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(len(tcpConnections)) wg.Add(len(tcpConnections))
for i := range tcpConnections { for i := range tcpConnections {
@ -422,6 +488,13 @@ func (cr *Croc) receive(forceSend int, serverAddress string, tcpPorts []string,
go func(wg *sync.WaitGroup, j int) { go func(wg *sync.WaitGroup, j int) {
defer wg.Done() defer wg.Done()
for { for {
select {
case _ = <-errorsDuringTransfer:
log.Debugf("%d got stop", i)
return
default:
}
log.Debugf("waiting to read on %d", j) log.Debugf("waiting to read on %d", j)
// read from TCP connection // read from TCP connection
message, _, _, err := tcpConnections[j].Read() message, _, _, err := tcpConnections[j].Read()
@ -437,7 +510,21 @@ func (cr *Croc) receive(forceSend int, serverAddress string, tcpPorts []string,
} }
}(&wg, i) }(&wg, i)
} }
log.Debug("waiting for tcp goroutines")
wg.Wait() 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
}
} }
_ = <-finished _ = <-finished

View file

@ -26,7 +26,6 @@ import (
"github.com/schollz/pake" "github.com/schollz/pake"
progressbar "github.com/schollz/progressbar/v2" progressbar "github.com/schollz/progressbar/v2"
"github.com/schollz/spinner" "github.com/schollz/spinner"
"github.com/tscholl2/siec"
) )
// Send is the async call to send data // Send is the async call to send data
@ -35,11 +34,15 @@ func (cr *Croc) startSender(forceSend int, serverAddress string, tcpPorts []stri
log.Debugf("sending %s", fname) log.Debugf("sending %s", fname)
err := cr.send(forceSend, serverAddress, tcpPorts, isLocal, c, fname, codephrase, useCompression, useEncryption) err := cr.send(forceSend, serverAddress, tcpPorts, isLocal, c, fname, codephrase, useCompression, useEncryption)
if err != nil { if err != nil {
log.Debug(err)
if !strings.HasPrefix(err.Error(), "websocket: close 100") { if !strings.HasPrefix(err.Error(), "websocket: close 100") {
fmt.Fprintf(os.Stderr, "\n"+err.Error()) 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{}{} done <- struct{}{}
} }
@ -91,19 +94,34 @@ func (cr *Croc) send(forceSend int, serverAddress string, tcpPorts []string, isL
spin.Writer = os.Stderr spin.Writer = os.Stderr
defer spin.Stop() defer spin.Stop()
// pick an elliptic curve
curve := siec.SIEC255()
// both parties should have a weak key // both parties should have a weak key
pw := []byte(codephrase) pw := []byte(codephrase)
// initialize sender P ("0" indicates sender) // 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 { if err != nil {
return 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 step := 0
for { for {
messageType, message, errRead := c.ReadMessage() websocketMessage := <-websocketMessages
messageType := websocketMessage.messageType
message := websocketMessage.message
errRead := websocketMessage.err
if errRead != nil { if errRead != nil {
return errRead 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")) { if messageType == websocket.TextMessage && bytes.Equal(message, []byte("interrupt")) {
return errors.New("\rinterrupted by other party") 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) log.Debugf("got %d: %s", messageType, message)
switch step { switch step {
case 0: case 0:
@ -123,11 +144,24 @@ func (cr *Croc) send(forceSend int, serverAddress string, tcpPorts []string, isL
} else { } else {
ip, _ = utils.PublicIP() 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: case 1:
// first receive the IP address from the sender // first receive the initial data from the recipient
cr.OtherIP = string(message) 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) log.Debugf("recipient IP: %s", cr.OtherIP)
go func() { go func() {
@ -250,7 +284,11 @@ func (cr *Croc) send(forceSend int, serverAddress string, tcpPorts []string, isL
go func() { go func() {
if !useWebsockets { if !useWebsockets {
log.Debugf("connecting to server") log.Debugf("connecting to server")
var wg sync.WaitGroup
wg.Add(len(tcpPorts))
for i, tcpPort := range tcpPorts { for i, tcpPort := range tcpPorts {
go func(i int, tcpPort string) {
defer wg.Done()
log.Debugf("connecting to %s on connection %d", tcpPort, i) log.Debugf("connecting to %s on connection %d", tcpPort, i)
var message string var message string
tcpConnections[i], message, err = connectToTCPServer(utils.SHA256(fmt.Sprintf("%d%x", i, sessionKey)), serverAddress+":"+tcpPort) tcpConnections[i], message, err = connectToTCPServer(utils.SHA256(fmt.Sprintf("%d%x", i, sessionKey)), serverAddress+":"+tcpPort)
@ -260,7 +298,9 @@ func (cr *Croc) send(forceSend int, serverAddress string, tcpPorts []string, isL
if message != "sender" { if message != "sender" {
log.Errorf("got wrong message: %s", message) log.Errorf("got wrong message: %s", message)
} }
}(i, tcpPort)
} }
wg.Wait()
} }
isConnectedIfUsingTCP <- true isConnectedIfUsingTCP <- true
}() }()
@ -395,6 +435,32 @@ func (cr *Croc) send(forceSend int, serverAddress string, tcpPorts []string, isL
} else { } else {
_ = <-isConnectedIfUsingTCP _ = <-isConnectedIfUsingTCP
log.Debug("connected and ready to send on tcp") 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 var wg sync.WaitGroup
wg.Add(len(tcpConnections)) wg.Add(len(tcpConnections))
for i := range 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) { go func(i int, wg *sync.WaitGroup, dataChan <-chan DataChan) {
defer wg.Done() defer wg.Done()
for data := range dataChan { for data := range dataChan {
select {
case _ = <-errorsDuringTransfer:
log.Debugf("%d got stop", i)
return
default:
}
if data.err != nil { if data.err != nil {
log.Error(data.err) log.Error(data.err)
return return
} }
cr.Bar.Add(data.bytesRead) cr.Bar.Add(data.bytesRead)
// write data to tcp connection // write data to tcp connection
_, err = tcpConnections[i].Write(data.b) _, errTcp := tcpConnections[i].Write(data.b)
if err != nil { if errTcp != nil {
err = errors.Wrap(err, "problem writing message") errTcp = errors.Wrap(errTcp, "problem writing message")
log.Error(err) log.Debug(errTcp)
errorsDuringTransfer <- errTcp
return return
} }
if bytes.Equal(data.b, []byte("magic")) { 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) }(i, &wg, dataChan)
} }
// block until this is done
log.Debug("waiting for tcp goroutines")
wg.Wait() 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() cr.Bar.Finish()

View file

@ -12,8 +12,8 @@ import (
log "github.com/cihub/seelog" log "github.com/cihub/seelog"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/schollz/croc/src/relay" "github.com/schollz/croc/src/relay"
"github.com/schollz/croc/src/utils"
"github.com/schollz/peerdiscovery" "github.com/schollz/peerdiscovery"
"github.com/schollz/utils"
) )
// Send the file // Send the file
@ -96,7 +96,7 @@ func (c *Croc) Receive(codephrase string) (err error) {
log.Debug(errDiscover) log.Debug(errDiscover)
} }
if len(discovered) > 0 { if len(discovered) > 0 {
if discovered[0].Address == utils.GetLocalIP() { if discovered[0].Address == utils.LocalIP() {
discovered[0].Address = "localhost" discovered[0].Address = "localhost"
} }
log.Debugf("discovered %s:%s", discovered[0].Address, discovered[0].Payload) 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) log.Debugf("connecting to %s", websocketAddress)
sock, _, err := websocket.DefaultDialer.Dial(websocketAddress, nil) sock, _, err := websocket.DefaultDialer.Dial(websocketAddress, nil)
if err != nil { if err != nil {
log.Error(err)
return return
} }
defer sock.Close() defer sock.Close()
@ -160,6 +161,7 @@ func (c *Croc) sendReceive(address, websocketPort string, tcpPorts []string, fna
// tell the websockets we are connected // tell the websockets we are connected
err = sock.WriteMessage(websocket.BinaryMessage, []byte("connected")) err = sock.WriteMessage(websocket.BinaryMessage, []byte("connected"))
if err != nil { if err != nil {
log.Error(err)
return err return err
} }
@ -172,13 +174,14 @@ func (c *Croc) sendReceive(address, websocketPort string, tcpPorts []string, fna
for { for {
select { select {
case <-done: case <-done:
log.Debug("received done signal")
return nil return nil
case <-interrupt: case <-interrupt:
if !c.Debug { if !c.Debug {
SetDebugLevel("critical") SetDebugLevel("critical")
} }
log.Debug("interrupt") log.Debug("interrupt")
err = sock.WriteMessage(websocket.TextMessage, []byte("interrupt")) err = sock.WriteMessage(websocket.TextMessage, []byte("error: interrupted by other party"))
if err != nil { if err != nil {
return err return err
} }

7
src/models/initial.go Normal file
View file

@ -0,0 +1,7 @@
package models
type Initial struct {
CurveType string
IPAddress string
VersionString string
}