diff --git a/go.sum b/go.sum index bc8b68f..12a5b48 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= -github.com/kalafut/imohash v1.0.0 h1:LgCJ+p/BwM2HKpOxFopkeddpzVCfm15EtXMroXD1SYE= -github.com/kalafut/imohash v1.0.0/go.mod h1:c3RHT80ZAp5C/aYgQI92ZlrOymqkZnRDprU87kg75HI= github.com/kalafut/imohash v1.0.2 h1:j/cUPa15YvXv7abJlM+kdJIycbBMpmO7WqhPl4YB76I= github.com/kalafut/imohash v1.0.2/go.mod h1:PjHBF0vpo1q7zMqiTn0qwSTQU2wDn5QIe8S8sFQuZS8= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= diff --git a/src/cli/cli.go b/src/cli/cli.go index d35a745..81737ce 100644 --- a/src/cli/cli.go +++ b/src/cli/cli.go @@ -16,6 +16,7 @@ import ( "github.com/schollz/cli/v2" "github.com/schollz/croc/v9/src/comm" "github.com/schollz/croc/v9/src/croc" + "github.com/schollz/croc/v9/src/crypt" "github.com/schollz/croc/v9/src/models" "github.com/schollz/croc/v9/src/tcp" "github.com/schollz/croc/v9/src/utils" @@ -75,6 +76,7 @@ func Run() (err error) { }, Flags: []cli.Flag{ &cli.StringFlag{Name: "ports", Value: "9009,9010,9011,9012,9013", Usage: "ports of the relay"}, + &cli.StringFlag{Name: "identity", Value: "", Usage: "identity file with credentials"}, }, }, } @@ -93,7 +95,8 @@ func Run() (err error) { &cli.StringFlag{Name: "relay", Value: models.DEFAULT_RELAY, Usage: "address of the relay", EnvVars: []string{"CROC_RELAY"}}, &cli.StringFlag{Name: "relay6", Value: models.DEFAULT_RELAY6, Usage: "ipv6 address of the relay", EnvVars: []string{"CROC_RELAY6"}}, &cli.StringFlag{Name: "out", Value: ".", Usage: "specify an output folder to receive the file"}, - &cli.StringFlag{Name: "pass", Value: models.DEFAULT_PASSPHRASE, Usage: "password for the relay", EnvVars: []string{"CROC_PASS"}}, + &cli.StringFlag{Name: "relay-pass", Value: models.DEFAULT_RELAY_PASSWORD, Usage: "password for the relay (defaults to public relay)", EnvVars: []string{"CROC_PASS"}}, + &cli.StringFlag{Name: "relay-key", Value: models.DEFAULT_RELAY_KEYPUBLIC, Usage: "public key for the relay (defaults to public relay)", EnvVars: []string{"CROC_KEY"}}, &cli.StringFlag{Name: "socks5", Value: "", Usage: "add a socks5 proxy", EnvVars: []string{"SOCKS5_PROXY"}}, } app.EnableBashCompletion = true @@ -167,15 +170,6 @@ func getConfigFile() string { return path.Join(configFile, "send.json") } -func determinePass(c *cli.Context) (pass string) { - pass = c.String("pass") - b, err := ioutil.ReadFile(pass) - if err == nil { - pass = strings.TrimSpace(string(b)) - } - return -} - func send(c *cli.Context) (err error) { setDebugLevel(c) comm.Socks5Proxy = c.String("socks5") @@ -193,7 +187,8 @@ func send(c *cli.Context) (err error) { RelayPorts: strings.Split(c.String("ports"), ","), Ask: c.Bool("ask"), NoMultiplexing: c.Bool("no-multi"), - RelayPassword: determinePass(c), + RelayPassword: c.String("relay-pass"), + RelayKeyPublic: c.String("relay-key"), SendingText: c.String("text") != "", NoCompress: c.Bool("no-compress"), Overwrite: c.Bool("overwrite"), @@ -226,9 +221,12 @@ func send(c *cli.Context) (err error) { if !c.IsSet("code") { crocOptions.SharedSecret = rememberedOptions.SharedSecret } - if !c.IsSet("pass") { + if !c.IsSet("relay-pass") { crocOptions.RelayPassword = rememberedOptions.RelayPassword } + if !c.IsSet("relay-key") { + crocOptions.RelayKeyPublic = rememberedOptions.RelayKeyPublic + } } var fnames []string @@ -383,20 +381,21 @@ func saveConfig(c *cli.Context, crocOptions croc.Options) { func receive(c *cli.Context) (err error) { comm.Socks5Proxy = c.String("socks5") crocOptions := croc.Options{ - SharedSecret: c.String("code"), - IsSender: false, - Debug: c.Bool("debug"), - NoPrompt: c.Bool("yes"), - RelayAddress: c.String("relay"), - RelayAddress6: c.String("relay6"), - Stdout: c.Bool("stdout"), - Ask: c.Bool("ask"), - RelayPassword: determinePass(c), - OnlyLocal: c.Bool("local"), - IP: c.String("ip"), - Overwrite: c.Bool("overwrite"), - Curve: c.String("curve"), - HashAlgorithm: "xxhash", + SharedSecret: c.String("code"), + IsSender: false, + Debug: c.Bool("debug"), + NoPrompt: c.Bool("yes"), + RelayAddress: c.String("relay"), + RelayAddress6: c.String("relay6"), + Stdout: c.Bool("stdout"), + Ask: c.Bool("ask"), + RelayPassword: c.String("relay-pass"), + RelayKeyPublic: c.String("relay-key"), + OnlyLocal: c.Bool("local"), + IP: c.String("ip"), + Overwrite: c.Bool("overwrite"), + Curve: c.String("curve"), + HashAlgorithm: "xxhash", } if crocOptions.RelayAddress != models.DEFAULT_RELAY { crocOptions.RelayAddress6 = "" @@ -481,23 +480,40 @@ func receive(c *cli.Context) (err error) { } func relay(c *cli.Context) (err error) { - log.Infof("starting croc relay version %v", Version) + identityFile := "identity.croc" + if c.String("identity") != "" { + identityFile = c.String("identity") + } + if !utils.Exists(identityFile) { + err = crypt.GenerateIdentityAndPassword(identityFile) + if err != nil { + return + } + } + keyPrivate, keyPublic, password, err := crypt.LoadIdentityAndPassword(identityFile) + if err != nil { + return + } + ports := strings.Split(c.String("ports"), ",") + tcpPorts := strings.Join(ports[1:], ",") + fmt.Printf("starting croc relay version %v\n", Version) + identityFileAbs, _ := filepath.Abs(identityFile) + fmt.Printf("identity file = %s\n", identityFileAbs) + fmt.Printf("\n\nexample to send files:\n\n croc --relay localhost:%s --relay-pass %s --relay-key %s send file.txt", ports[0], password, keyPublic) debugString := "info" if c.Bool("debug") { debugString = "debug" } - ports := strings.Split(c.String("ports"), ",") - tcpPorts := strings.Join(ports[1:], ",") for i, port := range ports { if i == 0 { continue } go func(portStr string) { - err = tcp.Run(debugString, portStr, determinePass(c)) + err = tcp.Run(debugString, portStr, password, keyPublic, keyPrivate) if err != nil { panic(err) } }(port) } - return tcp.Run(debugString, ports[0], determinePass(c), tcpPorts) + return tcp.Run(debugString, ports[0], password, keyPublic, keyPrivate, tcpPorts) } diff --git a/src/croc/croc.go b/src/croc/croc.go index 10ea2b8..b9f4d24 100644 --- a/src/croc/croc.go +++ b/src/croc/croc.go @@ -48,26 +48,31 @@ func Debug(debug bool) { // Options specifies user specific options type Options struct { - IsSender bool - SharedSecret string - Debug bool - RelayAddress string - RelayAddress6 string - RelayPorts []string - RelayPassword string - Stdout bool - NoPrompt bool - NoMultiplexing bool - DisableLocal bool - OnlyLocal bool - IgnoreStdin bool - Ask bool - SendingText bool - NoCompress bool - IP string - Overwrite bool - Curve string - HashAlgorithm string + IsSender bool + SharedSecret string + Debug bool + RelayAddress string + RelayAddress6 string + RelayPorts []string + RelayPassword string + RelayKeyPrivate string + RelayKeyPublic string + LocalRelayPassword string + LocalRelayKeyPrivate string + LocalRelayKeyPublic string + Stdout bool + NoPrompt bool + NoMultiplexing bool + DisableLocal bool + OnlyLocal bool + IgnoreStdin bool + Ask bool + SendingText bool + NoCompress bool + IP string + Overwrite bool + Curve string + HashAlgorithm string } // Client holds the state of the croc transfer @@ -288,7 +293,7 @@ func (c *Client) setupLocalRelay() { if c.Options.Debug { debugString = "debug" } - err := tcp.Run(debugString, portStr, c.Options.RelayPassword, strings.Join(c.Options.RelayPorts[1:], ",")) + err := tcp.Run(debugString, portStr, c.Options.LocalRelayPassword, c.Options.LocalRelayKeyPublic, c.Options.LocalRelayKeyPrivate, strings.Join(c.Options.RelayPorts[1:], ",")) if err != nil { panic(err) } @@ -320,7 +325,7 @@ func (c *Client) transferOverLocalRelay(options TransferOptions, errchan chan<- time.Sleep(500 * time.Millisecond) log.Debug("establishing connection") var banner string - conn, banner, ipaddr, err := tcp.ConnectToTCPServer("localhost:"+c.Options.RelayPorts[0], c.Options.RelayPassword, c.Options.SharedSecret[:3]) + conn, banner, ipaddr, err := tcp.ConnectToTCPServer("localhost:"+c.Options.RelayPorts[0], c.Options.LocalRelayPassword, c.Options.LocalRelayKeyPublic, c.Options.SharedSecret[:3]) log.Debugf("banner: %s", banner) if err != nil { err = fmt.Errorf("could not connect to localhost:%s: %w", c.Options.RelayPorts[0], err) @@ -361,8 +366,9 @@ func (c *Client) Send(options TransferOptions) (err error) { if c.Options.RelayAddress != models.DEFAULT_RELAY { flags.WriteString("--relay " + c.Options.RelayAddress + " ") } - if c.Options.RelayPassword != models.DEFAULT_PASSPHRASE { - flags.WriteString("--pass " + c.Options.RelayPassword + " ") + if c.Options.RelayPassword != models.DEFAULT_RELAY_PASSWORD || c.Options.RelayKeyPublic != models.DEFAULT_RELAY_KEYPUBLIC { + flags.WriteString("--relay-pass " + c.Options.RelayPassword + " ") + flags.WriteString("--relay-key " + c.Options.RelayKeyPublic + " ") } fmt.Fprintf(os.Stderr, "Code is: %[1]s\nOn the other computer run\n\ncroc %[2]s%[1]s\n", c.Options.SharedSecret, flags.String()) if c.Options.Ask { @@ -376,6 +382,8 @@ func (c *Client) Send(options TransferOptions) (err error) { errchan := make(chan error, 1) if !c.Options.DisableLocal { + c.Options.LocalRelayKeyPublic, c.Options.LocalRelayKeyPrivate, _ = crypt.NewAge() + c.Options.LocalRelayPassword, _ = crypt.GenerateRandomString(6) // add two things to the error channel errchan = make(chan error, 2) c.setupLocalRelay() @@ -405,7 +413,7 @@ func (c *Client) Send(options TransferOptions) (err error) { log.Debugf("got host '%v' and port '%v'", host, port) address = net.JoinHostPort(host, port) log.Debugf("trying connection to %s", address) - conn, banner, ipaddr, err = tcp.ConnectToTCPServer(address, c.Options.RelayPassword, c.Options.SharedSecret[:3], durations[i]) + conn, banner, ipaddr, err = tcp.ConnectToTCPServer(address, c.Options.RelayPassword, c.Options.RelayKeyPublic, c.Options.SharedSecret[:3], durations[i]) if err == nil { c.Options.RelayAddress = address break @@ -599,7 +607,7 @@ func (c *Client) Receive() (err error) { log.Debugf("got host '%v' and port '%v'", host, port) address = net.JoinHostPort(host, port) log.Debugf("trying connection to %s", address) - c.conn[0], banner, c.ExternalIP, err = tcp.ConnectToTCPServer(address, c.Options.RelayPassword, c.Options.SharedSecret[:3], durations[i]) + c.conn[0], banner, c.ExternalIP, err = tcp.ConnectToTCPServer(address, c.Options.RelayPassword, c.Options.RelayKeyPublic, c.Options.SharedSecret[:3], durations[i]) if err == nil { c.Options.RelayAddress = address break @@ -652,7 +660,7 @@ func (c *Client) Receive() (err error) { } serverTry := fmt.Sprintf("%s:%s", ip, port) - conn, banner2, externalIP, errConn := tcp.ConnectToTCPServer(serverTry, c.Options.RelayPassword, c.Options.SharedSecret[:3], 50*time.Millisecond) + conn, banner2, externalIP, errConn := tcp.ConnectToTCPServer(serverTry, c.Options.RelayPassword, c.Options.RelayKeyPublic, c.Options.SharedSecret[:3], 50*time.Millisecond) if errConn != nil { log.Debugf("could not connect to " + serverTry) continue @@ -909,6 +917,7 @@ func (c *Client) processMessagePake(m message.Message) (err error) { c.conn[j+1], _, _, err = tcp.ConnectToTCPServer( server, c.Options.RelayPassword, + c.Options.RelayKeyPublic, fmt.Sprintf("%s-%d", utils.SHA256(c.Options.SharedSecret[:5])[:6], j), ) if err != nil { diff --git a/src/crypt/crypt.go b/src/crypt/crypt.go index 712eba1..171b277 100644 --- a/src/crypt/crypt.go +++ b/src/crypt/crypt.go @@ -8,7 +8,10 @@ import ( "crypto/sha256" "fmt" "io" + "io/ioutil" "log" + "math/big" + "strings" "filippo.io/age" "golang.org/x/crypto/argon2" @@ -76,6 +79,64 @@ func Decrypt(encrypted []byte, key []byte) (plaintext []byte, err error) { return } +// GenerateIdentityAndPassword will generate a file with a public age identity and password +func GenerateIdentityAndPassword(fname string) (err error) { + keyPublic, keyPrivate, err := NewAge() + if err != nil { + return + } + password, err := GenerateRandomString(6) + if err != nil { + return + } + err = ioutil.WriteFile(fname, []byte(fmt.Sprintf("%s\n%s\n%s", keyPrivate, keyPublic, password)), 0644) + return +} + +// LoadIdentityAndPassword will load a file with a public age identity and password +func LoadIdentityAndPassword(fname string) (keyPrivate string, keyPublic string, password string, err error) { + b, err := ioutil.ReadFile(fname) + if err != nil { + return + } + foo := strings.Fields(string(b)) + if len(foo) < 3 { + err = fmt.Errorf("malformed file") + return + } + keyPrivate = foo[0] + keyPublic = foo[1] + password = foo[2] + + _, err = age.ParseX25519Identity(keyPrivate) + if err != nil { + return + } + _, err = age.ParseX25519Recipient(keyPublic) + if err != nil { + return + } + return +} + +// GenerateRandomString returns a securely generated random string. +// It will return an error if the system's secure random +// number generator fails to function correctly, in which +// case the caller should not continue. +func GenerateRandomString(n int) (string, error) { + const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + ret := make([]byte, n) + for i := 0; i < n; i++ { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) + if err != nil { + return "", err + } + ret[i] = letters[num.Int64()] + } + + return string(ret), nil +} + func NewAge() (pubkey string, privkey string, err error) { identity, err := age.GenerateX25519Identity() if err != nil { diff --git a/src/crypt/crypt_test.go b/src/crypt/crypt_test.go index eed6fc7..b95f979 100644 --- a/src/crypt/crypt_test.go +++ b/src/crypt/crypt_test.go @@ -2,8 +2,10 @@ package crypt import ( "fmt" + "os" "testing" + "github.com/schollz/croc/v9/src/utils" "github.com/stretchr/testify/assert" ) @@ -115,3 +117,19 @@ func TestEncryptionAge(t *testing.T) { assert.Nil(t, err) assert.Equal(t, msg, dec) } + +func TestGenerate(t *testing.T) { + err := GenerateIdentityAndPassword("test") + assert.Nil(t, err) + assert.True(t, utils.Exists("test")) + keyPrivate, keyPublic, password, err := LoadIdentityAndPassword("test") + assert.Nil(t, err) + fmt.Println(keyPrivate) + fmt.Println(keyPublic) + fmt.Println(password) + _, _, _, err = LoadIdentityAndPassword("crypt.go") + assert.NotNil(t, err) + _, _, _, err = LoadIdentityAndPassword("doesntexist") + assert.NotNil(t, err) + os.Remove("test") +} diff --git a/src/models/constants.go b/src/models/constants.go index dd5b933..429aef2 100644 --- a/src/models/constants.go +++ b/src/models/constants.go @@ -9,12 +9,13 @@ import ( // TCP_BUFFER_SIZE is the maximum packet size const TCP_BUFFER_SIZE = 1024 * 64 -// DEFAULT_RELAY is the default relay used (can be set using --relay) +// DEFAULT_RELAY is the default relay used (can be set using --relay-pass) var ( - DEFAULT_RELAY = "croc.schollz.com" - DEFAULT_RELAY6 = "croc6.schollz.com" - DEFAULT_PORT = "9009" - DEFAULT_PASSPHRASE = "pass123" + DEFAULT_RELAY = "croc.schollz.com" + DEFAULT_RELAY6 = "croc6.schollz.com" + DEFAULT_PORT = "9009" + DEFAULT_RELAY_PASSWORD = "ZiNO9Y" + DEFAULT_RELAY_KEYPUBLIC = "age10yrxthzjrcr0e59nucg0epgnn0qpjv9rhsxqs90rdn335edgnueqrtdnyh" ) func init() { diff --git a/src/tcp/tcp.go b/src/tcp/tcp.go index 4457d4c..8ee658c 100644 --- a/src/tcp/tcp.go +++ b/src/tcp/tcp.go @@ -90,7 +90,7 @@ func (s *server) start() (err error) { } func (s *server) run() (err error) { - log.Infof("starting TCP server on " + s.port) + log.Debugf("starting TCP server on " + s.port) server, err := net.Listen("tcp", ":"+s.port) if err != nil { return fmt.Errorf("error listening on %s: %w", s.port, err)