diff --git a/main.go b/main.go index 06bb6af0..cf2f223d 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,7 @@ func main() { if *role == -1 { err = c.Relay() } else if *role == 0 { - err = c.Send("foo", *passphrase) + err = c.Send("croc.exe", *passphrase) } else { err = c.Receive(*passphrase) } diff --git a/src/api.go b/src/api.go index 6a0461cc..89573972 100644 --- a/src/api.go +++ b/src/api.go @@ -11,7 +11,7 @@ func (c *Croc) Relay() error { // Send will take an existing file or folder and send it through the croc relay func (c *Croc) Send(fname string, codephrase string) (err error) { - err = c.client(0, codephrase) + err = c.client(0, codephrase, fname) return } diff --git a/src/client.go b/src/client.go index fdb5f377..9bf48de5 100644 --- a/src/client.go +++ b/src/client.go @@ -1,10 +1,17 @@ package croc import ( + "encoding/json" + "io" + "io/ioutil" "net" "net/url" "os" "os/signal" + "path" + "path/filepath" + "strconv" + "strings" "sync" "time" @@ -12,11 +19,23 @@ import ( "github.com/gorilla/websocket" "github.com/pkg/errors" "github.com/schollz/croc/src/pake" + tarinator "github.com/schollz/tarinator-go" ) -func (c *Croc) client(role int, codePhrase string) (err error) { +func (c *Croc) client(role int, codePhrase string, fname ...string) (err error) { defer log.Flush() + if role == 0 { + if len(fname) == 0 { + err = errors.New("must include filename") + return + } + err = c.processFile(fname[0]) + if err != nil { + return + } + } + // initialize the channel data for this client c.cs.Lock() @@ -63,7 +82,7 @@ func (c *Croc) client(role int, codePhrase string) (err error) { log.Debugf("sender read error:", err) return } - log.Debugf("recv: %s", cd) + //log.Debugf("recv: %s", cd.String2()) err = c.processState(ws, cd) if err != nil { log.Warn(err) @@ -125,7 +144,7 @@ func (c *Croc) client(role int, codePhrase string) (err error) { wg.Wait() c.cs.Lock() - if c.cs.channel.FileReceived { + if c.cs.channel.finishedHappy { log.Info("file recieved!") } c.cs.Unlock() @@ -148,6 +167,7 @@ func (c *Croc) processState(ws *websocket.Conn, cd channelData) (err error) { // if file received, then you are all done if cd.FileReceived { c.cs.channel.FileReceived = true + c.cs.channel.finishedHappy = true log.Debug("file recieved!") log.Debug("sending close signal") c.cs.channel.Close = true @@ -155,7 +175,13 @@ func (c *Croc) processState(ws *websocket.Conn, cd channelData) (err error) { return } - // if transfer ready then send file + // otherwise, if ready to read, then set and return + if cd.ReadyToRead { + c.cs.channel.ReadyToRead = true + return + } + + // otherwise, if transfer ready then send file if cd.TransferReady { c.cs.channel.TransferReady = true return @@ -181,11 +207,12 @@ func (c *Croc) processState(ws *websocket.Conn, cd channelData) (err error) { } // copy over the rest of the state c.cs.channel.Ports = cd.Ports + c.cs.channel.EncryptedFileMetaData = cd.EncryptedFileMetaData // update the Pake if cd.Pake != nil && cd.Pake.Role != c.cs.channel.Role { - log.Debugf("updating pake from %d", cd.Pake.Role) if c.cs.channel.Pake.HkA == nil { + log.Debugf("updating pake from %d", cd.Pake.Role) err = c.cs.channel.Pake.Update(cd.Pake.Bytes()) if err != nil { log.Error(err) @@ -204,54 +231,142 @@ func (c *Croc) processState(ws *websocket.Conn, cd channelData) (err error) { c.cs.channel.Update = false } } - if c.cs.channel.Role == 0 && c.cs.channel.Pake.IsVerified() { - go func() { - // encrypt the files - // TODO - c.cs.Lock() - c.cs.channel.fileReady = true - c.cs.Unlock() - }() + if c.cs.channel.Role == 0 && c.cs.channel.Pake.IsVerified() && !c.cs.channel.notSentMetaData { + go c.getFilesReady(ws) } - // TODO // process the client state - if c.cs.channel.Pake.IsVerified() && !c.cs.channel.isReady { + if c.cs.channel.Pake.IsVerified() && !c.cs.channel.isReady && c.cs.channel.EncryptedFileMetaData.Encrypted != nil { + // TODO: + // check if the user still wants to recieve file + + // decrypt the meta data + log.Debugf("encrypted meta data: %+v", c.cs.channel.EncryptedFileMetaData) + var passphrase, metaDataBytes []byte + passphrase, err = c.cs.channel.Pake.SessionKey() + if err != nil { + log.Error(err) + return + } + metaDataBytes, err = c.cs.channel.EncryptedFileMetaData.decrypt(passphrase) + if err != nil { + log.Error(err) + return + } + err = json.Unmarshal(metaDataBytes, &c.cs.channel.fileMetaData) + if err != nil { + log.Error(err) + return + } + log.Debugf("meta data: %+v", c.cs.channel.fileMetaData) + // spawn TCP connections c.cs.channel.isReady = true - go func(role int) { - err = c.dialUp() - if err == nil { - if role == 1 { - c.cs.Lock() - c.cs.channel.Update = true - c.cs.channel.FileReceived = true - log.Debugf("got file successfully") - errWrite := ws.WriteJSON(c.cs.channel) - if errWrite != nil { - log.Error(errWrite) - } - c.cs.channel.Update = false - c.cs.Unlock() - } - } else { - log.Error(err) - } - }(c.cs.channel.Role) + go c.spawnConnections(ws, c.cs.channel.Role) } return } -func (c *Croc) dialUp() (err error) { +func (c *Croc) getFilesReady(ws *websocket.Conn) (err error) { + c.cs.Lock() + defer c.cs.Unlock() + c.cs.channel.notSentMetaData = true + // send metadata + + // wait until data is ready + for { + if c.cs.channel.fileMetaData.Name != "" { + break + } + c.cs.Unlock() + time.Sleep(10 * time.Millisecond) + c.cs.Lock() + } + + // get passphrase + var passphrase []byte + passphrase, err = c.cs.channel.Pake.SessionKey() + if err != nil { + return + } + // encrypt file data + err = encryptFile(path.Join(c.cs.channel.fileMetaData.Path, c.cs.channel.fileMetaData.Name), c.cs.channel.fileMetaData.Name+".enc", passphrase) + if err != nil { + return + } + c.cs.channel.fileMetaData.IsEncrypted = true + // split into pieces to send + if err = splitFile(c.cs.channel.fileMetaData.Name+".enc", len(c.cs.channel.Ports)); err != nil { + return + } + // remove the file now since we still have pieces + if err = os.Remove(c.cs.channel.fileMetaData.Name + ".enc"); err != nil { + return + } + // remove compressed archive + if c.cs.channel.fileMetaData.IsDir { + log.Debug("removing archive: " + c.cs.channel.fileMetaData.Name) + if err = os.Remove(c.cs.channel.fileMetaData.Name); err != nil { + return + } + } + // encrypt meta data + var metaDataBytes []byte + metaDataBytes, err = json.Marshal(c.cs.channel.fileMetaData) + if err != nil { + return + } + c.cs.channel.EncryptedFileMetaData = encrypt(metaDataBytes, passphrase) + + c.cs.channel.Update = true + log.Debugf("updating channel") + errWrite := ws.WriteJSON(c.cs.channel) + if errWrite != nil { + log.Error(errWrite) + } + c.cs.channel.Update = false + go func() { + // encrypt the files + // TODO + c.cs.Lock() + c.cs.channel.fileReady = true + c.cs.Unlock() + }() + return +} + +func (c *Croc) spawnConnections(ws *websocket.Conn, role int) (err error) { + err = c.dialUp(ws) + if err == nil { + if role == 1 { + c.cs.Lock() + c.cs.channel.Update = true + c.cs.channel.finishedHappy = true + c.cs.channel.FileReceived = true + log.Debugf("got file successfully") + errWrite := ws.WriteJSON(c.cs.channel) + if errWrite != nil { + log.Error(errWrite) + } + c.cs.channel.Update = false + c.cs.Unlock() + } + } else { + log.Error(err) + } + return +} + +func (c *Croc) dialUp(ws *websocket.Conn) (err error) { c.cs.Lock() ports := c.cs.channel.Ports channel := c.cs.channel.Channel uuid := c.cs.channel.UUID role := c.cs.channel.Role c.cs.Unlock() - errorChan := make(chan error) + errorChan := make(chan error, len(ports)) for i, port := range ports { - go func(channel, uuid, port string, i int) { + go func(channel, uuid, port string, i int, errorChan chan error) { if i == 0 { log.Debug("dialing up") } @@ -295,14 +410,42 @@ func (c *Croc) dialUp() (err error) { } time.Sleep(10 * time.Millisecond) } + c.cs.RLock() + filename := c.cs.channel.fileMetaData.Name + ".enc." + strconv.Itoa(i) + c.cs.RUnlock() if role == 0 { log.Debug("send file") + for { + c.cs.RLock() + ready := c.cs.channel.ReadyToRead + c.cs.RUnlock() + if ready { + break + } + time.Sleep(10 * time.Millisecond) + } + log.Debug("sending file") + err = sendFile(filename, i, connection) } else { - log.Debug("receive file") + go func() { + time.Sleep(10 * time.Millisecond) + c.cs.Lock() + log.Debugf("updating channel with ready to read") + c.cs.channel.Update = true + c.cs.channel.ReadyToRead = true + errWrite := ws.WriteJSON(c.cs.channel) + if errWrite != nil { + log.Error(errWrite) + } + c.cs.channel.Update = false + c.cs.Unlock() + log.Debug("receive file") + }() + + err = receiveFile(filename, i, connection) } - time.Sleep(3 * time.Second) - errorChan <- nil - }(channel, uuid, port, i) + errorChan <- err + }(channel, uuid, port, i, errorChan) } // collect errors @@ -310,7 +453,173 @@ func (c *Croc) dialUp() (err error) { errOne := <-errorChan if errOne != nil { log.Warn(errOne) + log.Debug("sending close signal") + c.cs.channel.Close = true + ws.WriteJSON(c.cs.channel) } } + log.Debug("leaving dialup") return } + +func (c *Croc) processFile(fname string) (err error) { + + fd := FileMetaData{} + + // first check if it is stdin + if fname == "stdin" { + var f *os.File + f, err = ioutil.TempFile(".", "croc-stdin-") + if err != nil { + return + } + _, err = io.Copy(f, os.Stdin) + if err != nil { + return + } + fname = f.Name() + err = f.Close() + if err != nil { + return + } + fd.DeleteAfterSending = true + } + + fname = filepath.Clean(fname) + // check wether the file is a dir + info, err := os.Stat(fname) + if err != nil { + return + } + + fd.Path, fd.Name = filepath.Split(fname) + if info.Mode().IsDir() { + // tar folder + err = tarinator.Tarinate([]string{fname}, fd.Name+".tar") + if err != nil { + log.Error(err) + return + } + fd.Name = fd.Name + ".tar" + fd.Path = "." + fd.IsDir = true + fname = path.Join(fd.Path, fd.Name) + } + fd.Hash, err = hashFile(fname) + if err != nil { + log.Error(err) + return err + } + fd.Size, err = fileSize(fname) + if err != nil { + err = errors.Wrap(err, "could not determine filesize") + log.Error(err) + return err + } + + c.cs.Lock() + defer c.cs.Unlock() + c.cs.channel.fileMetaData = fd + return +} + +func receiveFile(filename string, id int, connection net.Conn) error { + log.Debug("waiting for chunk size from sender") + fileSizeBuffer := make([]byte, 10) + connection.Read(fileSizeBuffer) + fileDataString := strings.Trim(string(fileSizeBuffer), ":") + fileSizeInt, _ := strconv.Atoi(fileDataString) + chunkSize := int64(fileSizeInt) + log.Debugf("chunk size: %d", chunkSize) + if chunkSize == 0 { + log.Debug(fileSizeBuffer) + return errors.New("chunk size is empty!") + } + + os.Remove(filename) + log.Debug("making " + filename) + newFile, err := os.Create(filename) + if err != nil { + panic(err) + } + defer newFile.Close() + + log.Debug(id, "waiting for file") + var receivedBytes int64 + receivedFirstBytes := false + for { + if (chunkSize - receivedBytes) < bufferSize { + log.Debugf("%d at the end: %d < %d", id, (chunkSize - receivedBytes), bufferSize) + io.CopyN(newFile, connection, (chunkSize - receivedBytes)) + // Empty the remaining bytes that we don't need from the network buffer + if (receivedBytes+bufferSize)-chunkSize < bufferSize { + log.Debug(id, "empty remaining bytes from network buffer") + connection.Read(make([]byte, (receivedBytes+bufferSize)-chunkSize)) + } + break + } + written, _ := io.CopyN(newFile, connection, bufferSize) + receivedBytes += written + if !receivedFirstBytes { + receivedFirstBytes = true + log.Debug(id, "Receieved first bytes!") + } + } + log.Debug(id, "received file") + return nil +} + +func sendFile(filename string, id int, connection net.Conn) error { + // open encrypted file chunk, if it exists + log.Debug("opening encrypted file chunk: " + filename) + file, err := os.Open(filename) + if err != nil { + log.Error(err) + return nil + } + defer file.Close() + + // determine and send the file size to client + fi, err := file.Stat() + if err != nil { + log.Error(err) + return err + } + log.Debugf("sending chunk size: %d", fi.Size()) + log.Debug(connection.RemoteAddr()) + _, err = connection.Write([]byte(fillString(strconv.FormatInt(int64(fi.Size()), 10), 10))) + if err != nil { + return errors.Wrap(err, "Problem sending chunk data: ") + } + + // rate limit the bandwidth + log.Debug("determining rate limiting") + rate := 10000 + throttle := time.NewTicker(time.Second / time.Duration(rate)) + log.Debugf("rate: %+v", rate) + defer throttle.Stop() + + // send the file + sendBuffer := make([]byte, bufferSize) + totalBytesSent := 0 + for range throttle.C { + _, err := file.Read(sendBuffer) + written, _ := connection.Write(sendBuffer) + totalBytesSent += written + // if errWrite != nil { + // errWrite = errors.Wrap(errWrite, "problem writing to connection") + // return errWrite + // } + if err == io.EOF { + //End of file reached, break out of for loop + log.Debug("EOF") + err = nil // not really an error + break + } + } + log.Debug("file is sent") + log.Debug("removing piece") + os.Remove(filename) + + return err +} diff --git a/src/crypto.go b/src/crypto.go index 46916419..8288928d 100644 --- a/src/crypto.go +++ b/src/crypto.go @@ -6,7 +6,6 @@ import ( "crypto/rand" "crypto/sha1" "crypto/sha256" - "encoding/hex" "fmt" mathrand "math/rand" "os" @@ -31,9 +30,17 @@ func getRandomName() string { return strings.Join(result, "-") } -func encrypt(plaintext []byte, passphrase string, dontencrypt ...bool) (encrypted []byte, salt string, iv string) { +type encryption struct { + Encrypted, Salt, IV []byte +} + +func encrypt(plaintext []byte, passphrase []byte, dontencrypt ...bool) encryption { if len(dontencrypt) > 0 && dontencrypt[0] { - return plaintext, "salt", "iv" + return encryption{ + Encrypted: plaintext, + Salt: []byte("salt"), + IV: []byte("iv"), + } } key, saltBytes := deriveKey(passphrase, nil) ivBytes := make([]byte, 12) @@ -42,26 +49,26 @@ func encrypt(plaintext []byte, passphrase string, dontencrypt ...bool) (encrypte rand.Read(ivBytes) b, _ := aes.NewCipher(key) aesgcm, _ := cipher.NewGCM(b) - encrypted = aesgcm.Seal(nil, ivBytes, plaintext, nil) - salt = hex.EncodeToString(saltBytes) - iv = hex.EncodeToString(ivBytes) - return + encrypted := aesgcm.Seal(nil, ivBytes, plaintext, nil) + return encryption{ + Encrypted: encrypted, + Salt: saltBytes, + IV: ivBytes, + } } -func decrypt(data []byte, passphrase string, salt string, iv string, dontencrypt ...bool) (plaintext []byte, err error) { +func (e encryption) decrypt(passphrase []byte, dontencrypt ...bool) (plaintext []byte, err error) { if len(dontencrypt) > 0 && dontencrypt[0] { - return data, nil + return e.Encrypted, nil } - saltBytes, _ := hex.DecodeString(salt) - ivBytes, _ := hex.DecodeString(iv) - key, _ := deriveKey(passphrase, saltBytes) + key, _ := deriveKey(passphrase, e.Salt) b, _ := aes.NewCipher(key) aesgcm, _ := cipher.NewGCM(b) - plaintext, err = aesgcm.Open(nil, ivBytes, data, nil) + plaintext, err = aesgcm.Open(nil, e.IV, e.Encrypted, nil) return } -func deriveKey(passphrase string, salt []byte) ([]byte, []byte) { +func deriveKey(passphrase []byte, salt []byte) ([]byte, []byte) { if salt == nil { salt = make([]byte, 8) // http://www.ietf.org/rfc/rfc2898.txt @@ -80,15 +87,15 @@ func hashBytes(data []byte) string { return fmt.Sprintf("%x", sum) } -func encryptFile(inputFilename string, outputFilename string, password string) error { +func encryptFile(inputFilename string, outputFilename string, password []byte) error { return cryptFile(inputFilename, outputFilename, password, true) } -func decryptFile(inputFilename string, outputFilename string, password string) error { +func decryptFile(inputFilename string, outputFilename string, password []byte) error { return cryptFile(inputFilename, outputFilename, password, false) } -func cryptFile(inputFilename string, outputFilename string, password string, encrypt bool) error { +func cryptFile(inputFilename string, outputFilename string, password []byte, encrypt bool) error { in, err := os.Open(inputFilename) if err != nil { return err @@ -109,7 +116,7 @@ func cryptFile(inputFilename string, outputFilename string, password string, enc c := &crypt.Crypter{ HashFunc: sha1.New, HashSize: sha1.Size, - Key: crypt.NewPbkdf2Key([]byte(password), 32), + Key: crypt.NewPbkdf2Key(password, 32), } if encrypt { if err := c.Encrypt(out, in); err != nil { diff --git a/src/models.go b/src/models.go index 45dc9e3a..93bd959d 100644 --- a/src/models.go +++ b/src/models.go @@ -59,6 +59,18 @@ type clientState struct { sync.RWMutex } +type FileMetaData struct { + TempName string + Name string + Size int + Hash string + Path string + IsDir bool + IsEncrypted bool + IsCompressed bool + DeleteAfterSending bool +} + type channelData struct { // Relay actions // Open set to true when trying to open @@ -81,9 +93,12 @@ type channelData struct { Ports []string `json:"ports"` // Curve is the type of elliptic curve to use Curve string `json:"curve"` - + // FileMetaData is sent after confirmed + EncryptedFileMetaData encryption `json:"encrypted_meta_data"` // FileReceived specifies that everything was done right FileReceived bool `json:"file_received"` + // ReadyToRead means that the recipient is ready to read + ReadyToRead bool `json:"ready_to_read"` // Error is sent if there is an error Error string `json:"error"` @@ -101,9 +116,12 @@ type channelData struct { // passPhrase is used to generate a session key passPhrase string // sessionKey - sessionKey []byte - isReady bool - fileReady bool + sessionKey []byte + isReady bool + fileReady bool + fileMetaData FileMetaData + notSentMetaData bool + finishedHappy bool // relay parameters // isopen determine whether or not the channel has been opened @@ -111,7 +129,7 @@ type channelData struct { // store a UUID of the parties to prevent other parties from joining uuids [2]string // 0 is sender, 1 is recipient // connection information is stored when the clients do connect over TCP - connection [2]net.Conn + connection map[string][2]net.Conn // websocket connections websocketConn [2]*websocket.Conn // startTime is the time that the channel was opened diff --git a/src/relay.go b/src/relay.go index 95646446..77b3ee89 100644 --- a/src/relay.go +++ b/src/relay.go @@ -83,10 +83,19 @@ func (c *Croc) clientCommuncation(port string, connection net.Conn) (err error) if uuid == c.rs.channel[channel].uuids[1] { role = 1 } - c.rs.channel[channel].connection[role] = connection - con1 = c.rs.channel[channel].connection[0] - con2 = c.rs.channel[channel].connection[1] + if _, ok := c.rs.channel[channel].connection[port]; !ok { + c.rs.channel[channel].connection[port] = [2]net.Conn{nil, nil} + } + con1 = c.rs.channel[channel].connection[port][0] + con2 = c.rs.channel[channel].connection[port][1] + if role == 0 { + con1 = connection + } else { + con2 = connection + } + log.Debug(c.rs.channel[channel].connection[port]) + c.rs.channel[channel].connection[port] = [2]net.Conn{con1, con2} ports := c.rs.channel[channel].Ports c.rs.Unlock() diff --git a/src/server.go b/src/server.go index d6335d46..a57e3024 100644 --- a/src/server.go +++ b/src/server.go @@ -1,6 +1,7 @@ package croc import ( + "net" "net/http" "time" @@ -77,6 +78,8 @@ func (c *Croc) updateChannel(cd channelData) (err error) { // update each c.rs.channel[cd.Channel].Error = cd.Error c.rs.channel[cd.Channel].FileReceived = cd.FileReceived + c.rs.channel[cd.Channel].EncryptedFileMetaData = cd.EncryptedFileMetaData + c.rs.channel[cd.Channel].ReadyToRead = cd.ReadyToRead if c.rs.channel[cd.Channel].Pake == nil { c.rs.channel[cd.Channel].Pake = new(pake.Pake) } @@ -91,7 +94,6 @@ func (c *Croc) updateChannel(cd channelData) (err error) { c.rs.channel[cd.Channel].Pake.Xᵥ = cd.Pake.Xᵥ c.rs.channel[cd.Channel].Pake.Yᵤ = cd.Pake.Yᵤ c.rs.channel[cd.Channel].Pake.Yᵥ = cd.Pake.Yᵥ - // TODO return } @@ -122,6 +124,7 @@ func (c *Croc) joinChannel(ws *websocket.Conn, cd channelData) (channel string, log.Debug("creating new channel") if _, ok := c.rs.channel[cd.Channel]; !ok { c.rs.channel[cd.Channel] = new(channelData) + c.rs.channel[cd.Channel].connection = make(map[string][2]net.Conn) } channel = cd.Channel