From 561bf0682668d6ce41a0ca43eeb9ea0a7b6586c5 Mon Sep 17 00:00:00 2001 From: Arsen Musayelyan Date: Thu, 3 Dec 2020 02:12:43 -0800 Subject: [PATCH] Golang Rewrite --- .gitignore | 3 + config.go | 113 ++++++++++++++++++++++++++++++ deviceDiscovery.go | 59 ++++++++++++++++ fileCrypto.go | 127 ++++++++++++++++++++++++++++++++++ files.go | 166 +++++++++++++++++++++++++++++++++++++++++++++ go.mod | 9 +++ go.sum | 36 ++++++++++ keyCrypto.go | 75 ++++++++++++++++++++ keyExchange.go | 65 ++++++++++++++++++ main.go | 137 +++++++++++++++++++++++++++++++++++++ 10 files changed, 790 insertions(+) create mode 100644 .gitignore create mode 100644 config.go create mode 100644 deviceDiscovery.go create mode 100644 fileCrypto.go create mode 100644 files.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 keyCrypto.go create mode 100644 keyExchange.go create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a028b7f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea/ +opensend +opensend-arm \ No newline at end of file diff --git a/config.go b/config.go new file mode 100644 index 0000000..02a8890 --- /dev/null +++ b/config.go @@ -0,0 +1,113 @@ +package main + +import ( + "encoding/json" + "github.com/pkg/browser" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "io" + "io/ioutil" + "os" + "path/filepath" + "strconv" +) + +// Create config type to store action type and data +type Config struct { + ActionType string + ActionData string +} + +// Instantiate and return a new Config struct +func NewConfig(actionType string, actionData string) *Config { + return &Config{ActionType: actionType, ActionData: actionData} +} + +// Create config file +func (config *Config) CreateFile(dir string) { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + // Create config file at given directory + configFile, err := os.Create(dir + "/config.json") + if err != nil { log.Fatal().Err(err).Msg("Error creating config file") } + // Close config file at the end of this function + defer configFile.Close() + // Marshal given Config struct into a []byte + jsonData, err := json.Marshal(config) + if err != nil { log.Fatal().Err(err).Msg("Error encoding JSON") } + // Write []byte to previously created config file + bytesWritten, err := configFile.Write(jsonData) + if err != nil { log.Fatal().Err(err).Msg("Error writing JSON to file") } + // Log bytes written + log.Info().Str("file", "config.json").Msg("Wrote " + strconv.Itoa(bytesWritten) + " bytes") +} + +// Collect all required files into given directory +func (config *Config) CollectFiles(dir string) { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + // If action type is file + if config.ActionType == "file" { + // Open file path in config.ActionData + src, err := os.Open(config.ActionData) + if err != nil { log.Fatal().Err(err).Msg("Error opening file from config") } + // Close source file at the end of this function + defer src.Close() + // Create new file with the same name at given directory + dst, err := os.Create(dir + "/" + filepath.Base(config.ActionData)) + if err != nil { log.Fatal().Err(err).Msg("Error creating file") } + // Close new file at the end of this function + defer dst.Close() + // Copy data from source file to destination file + _, err = io.Copy(dst, src) + if err != nil { log.Fatal().Err(err).Msg("Error copying data to file") } + // Replace file path in config.ActionData with file name + config.ActionData = filepath.Base(config.ActionData) + } +} + +// Read config file at given file path +func (config *Config) ReadFile(filePath string) { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + // Read file at filePath + fileData, err := ioutil.ReadFile(filePath) + if err != nil { log.Fatal().Err(err).Msg("Error reading config file") } + // Unmarshal data from JSON into config struct + err = json.Unmarshal(fileData, config) + if err != nil { log.Fatal().Err(err).Msg("Error decoding JSON") } +} + +// Execute action specified in config +func (config *Config) ExecuteAction(srcDir string) { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + // If action is file + if config.ActionType == "file" { + // Open file from config at given directory + src, err := os.Open(srcDir + "/" + config.ActionData) + if err != nil { log.Fatal().Err(err).Msg("Error reading file from config") } + // Close source file at the end of this function + defer src.Close() + // Get user's home directory + homeDir, err := os.UserHomeDir() + if err != nil { log.Fatal().Err(err).Msg("Error getting home directory") } + // Create file in user's Downloads directory + dst, err := os.Create(homeDir + "/Downloads/" + config.ActionData) + if err != nil { log.Fatal().Err(err).Msg("Error creating file") } + // Close destination file at the end of this function + defer dst.Close() + // Copy data from source file to destination file + _, err = io.Copy(dst, src) + if err != nil { log.Fatal().Err(err).Msg("Error copying data to file") } + // If action is url + } else if config.ActionType == "url" { + // Attempt to open URL in browser + err := browser.OpenURL(config.ActionData) + if err != nil { log.Fatal().Err(err).Msg("Error opening browser") } + // Catchall + } else { + // Log unknown action type + log.Fatal().Msg("Unknown action type " + config.ActionType) + } +} \ No newline at end of file diff --git a/deviceDiscovery.go b/deviceDiscovery.go new file mode 100644 index 0000000..fd45a00 --- /dev/null +++ b/deviceDiscovery.go @@ -0,0 +1,59 @@ +package main + +import ( + "context" + "github.com/grandcat/zeroconf" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "os" + "time" +) + +// Discover opensend receivers on the network +func DiscoverReceivers() ([]string, []string) { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + // Create zeroconf resolver + resolver, err := zeroconf.NewResolver(nil) + if err != nil { log.Fatal().Err(err).Msg("Error creating zeroconf resolver") } + // Create channel for zeroconf entries + entries := make(chan *zeroconf.ServiceEntry) + // Create slice to store hostnames of discovered receivers + var discoveredReceivers []string + // Create slice to store IPs of discovered receivers + var discoveredReceiverIPs []string + // Concurrently run mDNS query + go func(results <-chan *zeroconf.ServiceEntry) { + // For each entry + for entry := range results { + // Append hostname to discoveredReceivers + discoveredReceivers = append(discoveredReceivers, entry.HostName) + // Append IP to discoveredReceiverIPs + discoveredReceiverIPs = append(discoveredReceiverIPs, entry.AddrIPv4[0].String()) + } + }(entries) + + // Create context with 4 second timeout + ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) + // Cancel context at the end of this function + defer cancel() + // Browse for mDNS entries + err = resolver.Browse(ctx, "_opensend._tcp", "local.", entries) + if err != nil { log.Fatal().Err(err).Msg("Error browsing zeroconf services") } + + // Send Done signal to context + <-ctx.Done() + // Return discovered receiver slices + return discoveredReceivers, discoveredReceiverIPs +} + +// Register opensend zeroconf service on the network +func RegisterService() func() { + // Get computer hostname + hostname, _ := os.Hostname() + // Register zeroconf service {hostname}._opensend._tcp.local. + server, err := zeroconf.Register(hostname, "_opensend._tcp", "local.", 9797, []string{"txtv=0", "lo=1", "la=2"}, nil) + if err != nil { log.Fatal().Err(err).Msg("Error registering zeroconf service") } + // Return server.Shutdown() function to allow for shutdown in main() + return server.Shutdown +} \ No newline at end of file diff --git a/fileCrypto.go b/fileCrypto.go new file mode 100644 index 0000000..8d2078a --- /dev/null +++ b/fileCrypto.go @@ -0,0 +1,127 @@ +package main + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/md5" + "crypto/rand" + "encoding/hex" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "io" + "io/ioutil" + "os" + "path/filepath" + "strconv" + "strings" +) + +// Encrypt given file using the shared key +func EncryptFile(filePath string, newFilePath string, sharedKey string) { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + // Read data from file + data, err := ioutil.ReadFile(filePath) + if err != nil { log.Fatal().Err(err).Msg("Error reading file") } + // Create md5 hash of password in order to make it the required size + md5Hash := md5.New() + md5Hash.Write([]byte(sharedKey)) + // Encode md5 hash bytes into hexadecimal + hashedKey := hex.EncodeToString(md5Hash.Sum(nil)) + // Create new AES cipher + block, _ := aes.NewCipher([]byte(hashedKey)) + // Create GCM for AES cipher + gcm, err := cipher.NewGCM(block) + if err != nil { log.Fatal().Err(err).Msg("Error creating GCM") } + // Make byte slice for nonce + nonce := make([]byte, gcm.NonceSize()) + // Read random bytes into nonce slice + _, err = io.ReadFull(rand.Reader, nonce) + if err != nil { log.Fatal().Err(err).Msg("Error creating nonce") } + // Encrypt data + ciphertext := gcm.Seal(nonce, nonce, data, nil) + // Create new file + newFile, err := os.Create(newFilePath) + if err != nil { log.Fatal().Err(err).Msg("Error creating file") } + // Defer file close + defer newFile.Close() + // Write ciphertext to new file + bytesWritten, err := newFile.Write(ciphertext) + // Log bytes written and to which file + log.Info().Str("file", filepath.Base(newFilePath)).Msg("Wrote " + strconv.Itoa(bytesWritten) + " bytes") +} + +// Decrypt given file using the shared key +func DecryptFile(filePath string, newFilePath string, sharedKey string) { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + // Read data from file + data, err := ioutil.ReadFile(filePath) + if err != nil { log.Fatal().Err(err).Msg("Error reading file") } + // Create md5 hash of password in order to make it the required size + md5Hash := md5.New() + md5Hash.Write([]byte(sharedKey)) + hashedKey := hex.EncodeToString(md5Hash.Sum(nil)) + // Create new AES cipher + block, _ := aes.NewCipher([]byte(hashedKey)) + // Create GCM for AES cipher + gcm, err := cipher.NewGCM(block) + if err != nil { log.Fatal().Err(err).Msg("Error creating GCM") } + // Get standard GCM nonce size + nonceSize := gcm.NonceSize() + // Get nonce and ciphertext from data + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + // Decrypt data + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { log.Fatal().Err(err).Msg("Error decrypting data") } + // Create new file + newFile, err := os.Create(newFilePath) + if err != nil { log.Fatal().Err(err).Msg("Error creating file") } + // Defer file close + defer newFile.Close() + // Write ciphertext to new file + bytesWritten, err := newFile.Write(plaintext) + // Log bytes written and to which file + log.Info().Str("file", filepath.Base(newFilePath)).Msg("Wrote " + strconv.Itoa(bytesWritten) + " bytes") +} + +// Encrypt files in given directory using shared key +func EncryptFiles(dir string, sharedKey string) { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + // Walk given directory + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + // If error reading, return err + if err != nil { return err } + // If file is not a directory and is not the key + if !info.IsDir() && !strings.Contains(path, "aesKey"){ + // Encrypt the file using shared key, appending .enc + EncryptFile(path, path + ".enc", sharedKey) + // Remove unencrypted file + err := os.Remove(path) + if err != nil {return err} + } + // Return nil if no error occurs + return nil + }) + if err != nil { log.Fatal().Err(err).Msg("Error encrypting files") } +} + +// Decrypt files in given directory using shared key +func DecryptFiles(dir string, sharedKey string) { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + // Walk given directory + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + // If error reading, return err + if err != nil { return err } + // If file is not a directory and is encrypted + if !info.IsDir() && strings.Contains(path, ".enc") { + // Decrypt the file using the shared key, removing .enc + DecryptFile(path, strings.TrimSuffix(path, ".enc"), sharedKey) + } + // Return nil if no errors occurred + return nil + }) + if err != nil { log.Fatal().Err(err).Msg("Error decrypting files") } +} \ No newline at end of file diff --git a/files.go b/files.go new file mode 100644 index 0000000..6936767 --- /dev/null +++ b/files.go @@ -0,0 +1,166 @@ +package main + +import ( + "context" + "fmt" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "io" + "io/ioutil" + "net" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" +) + +// Save encrypted key to file +func SaveEncryptedKey(encryptedKey []byte, filePath string) { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + // Create file at given file path + keyFile, err := os.Create(filePath) + if err != nil { log.Fatal().Err(err).Msg("Error creating file") } + // Close file at the end of this function + defer keyFile.Close() + // Write encrypted key to file + bytesWritten, err := keyFile.Write(encryptedKey) + if err != nil { log.Fatal().Err(err).Msg("Error writing key to file") } + // Log bytes written + log.Info().Str("file", filepath.Base(filePath)).Msg("Wrote " + strconv.Itoa(bytesWritten) + " bytes") +} + +// Create HTTP server to transmit files +func SendFiles(dir string) { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + // Instantiate http.Server struct + srv := &http.Server{} + // Listen on all ipv4 addresses on port 9898 + listener, err := net.Listen("tcp4", ":9898") + if err != nil { log.Fatal().Err(err).Msg("Error starting listener") } + + // If client connects to /:filePath + http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) { + // Set file to first path components of URL, excluding first / + file := req.URL.Path[1:] + // Read file at specified location + fileData, err := ioutil.ReadFile(dir + "/" + file) + // If there was an error reading + if err != nil { + // Warn user of error + log.Warn().Err(err).Msg("Error reading file") + // Otherwise + } else { + // Inform user client has requested a file + log.Info().Str("file", file).Msg("GET File") + } + // Write file to ResponseWriter + _, err = fmt.Fprint(res, string(fileData)) + if err != nil { log.Fatal().Err(err).Msg("Error writing response") } + }) + + // If client connects to /index + http.HandleFunc("/index", func(res http.ResponseWriter, req *http.Request) { + // Inform user a client has requested the file index + log.Info().Msg("GET Index") + // Get directory listing + dirListing, err := ioutil.ReadDir(dir) + if err != nil { log.Fatal().Err(err).Msg("Error reading directory") } + // Create new slice to house filenames for index + var indexSlice []string + // For each file in listing + for _, file := range dirListing { + // If the file is not the key + if !strings.Contains(file.Name(), "savedKey.aesKey") { + // Append the file path to indexSlice + indexSlice = append(indexSlice, dir + "/" + file.Name()) + } + } + // Join index slice into string + indexStr := strings.Join(indexSlice, ";") + // Write index to ResponseWriter + _, err = fmt.Fprint(res, indexStr) + if err != nil { log.Fatal().Err(err).Msg("Error writing response") } + }) + + // If client connects to /key + http.HandleFunc("/key", func(res http.ResponseWriter, req *http.Request) { + // Inform user a client has requested the key + log.Info().Msg("GET Key") + // Read saved key + key, err := ioutil.ReadFile(dir + "/savedKey.aesKey") + if err != nil { log.Fatal().Err(err).Msg("Error reading key") } + // Write saved key to ResponseWriter + _, err = fmt.Fprint(res, string(key)) + if err != nil { log.Fatal().Err(err).Msg("Error writing response") } + }) + + // If client connects to /stop + http.HandleFunc("/stop", func(res http.ResponseWriter, req *http.Request) { + // Inform user a client has requested server shutdown + log.Info().Msg("GET Stop") + log.Info().Msg("Shutdown signal received") + // Shutdown server and send to empty context + err := srv.Shutdown(context.TODO()) + if err != nil { log.Fatal().Err(err).Msg("Error stopping server") } + }) + + // Start HTTP Server + _ = srv.Serve(listener) +} + +// Get files from sender +func RecvFiles(dir string, senderAddr string) { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + // Get server address by getting the IP without the port, prepending http:// and appending :9898 + serverAddr := "http://" + strings.Split(senderAddr, ":")[0] + ":9898" + // GET /index on sender's HTTP server + response, err := http.Get(serverAddr + "/index") + if err != nil { log.Fatal().Err(err).Msg("Error getting index") } + // Close response body at the end of this function + defer response.Body.Close() + // Create index slice for storage of file index + var index []string + // If server responded with 200 OK + if response.StatusCode == http.StatusOK { + // Read response body + body, err := ioutil.ReadAll(response.Body) + if err != nil { log.Fatal().Err(err).Msg("Error reading HTTP response") } + // Get string from body + bodyStr := string(body) + // Split string to form index + index = strings.Split(bodyStr, ";") + } + // For each file in the index + for _, file := range index { + // GET current file in index + response, err := http.Get(serverAddr + "/" + filepath.Base(file)) + if err != nil { log.Fatal().Err(err).Msg("Error getting file") } + // If server responded with 200 OK + if response.StatusCode == http.StatusOK { + // Create new file at index filepath + newFile, err := os.Create(file) + if err != nil { log.Fatal().Err(err).Msg("Error creating file") } + // Copy response body to new file + bytesWritten, err := io.Copy(newFile, response.Body) + if err != nil { log.Fatal().Err(err).Msg("Error writing to file") } + // Log bytes written + log.Info().Str("file", filepath.Base(file)).Msg("Wrote " + strconv.Itoa(int(bytesWritten)) + " bytes") + // Close new file + newFile.Close() + } + // Close response body + response.Body.Close() + } +} + +// Send stop signal to sender's HTTP server +func SendSrvStopSignal(senderAddr string) { + // Get server address by getting the IP without the port, prepending http:// and appending :9898 + serverAddr := "http://" + strings.Split(senderAddr, ":")[0] + ":9898" + // GET /stop on sender's HTTP servers ignoring any errors + _, _ = http.Get(serverAddr + "/stop") +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..26e9f9a --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module opensend + +go 1.15 + +require ( + github.com/grandcat/zeroconf v1.0.0 + github.com/pkg/browser v0.0.0-20201112035734-206646e67786 + github.com/rs/zerolog v1.20.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..dfef2ce --- /dev/null +++ b/go.sum @@ -0,0 +1,36 @@ +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE= +github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs= +github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM= +github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/pkg/browser v0.0.0-20201112035734-206646e67786 h1:4Gk0Dsp90g2YwfsxDOjvkEIgKGh+2R9FlvormRycveA= +github.com/pkg/browser v0.0.0-20201112035734-206646e67786/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs= +github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/keyCrypto.go b/keyCrypto.go new file mode 100644 index 0000000..89f62ac --- /dev/null +++ b/keyCrypto.go @@ -0,0 +1,75 @@ +package main + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "io/ioutil" + "net/http" + "os" + "strings" +) + +// Generate RSA keypair +func GenerateRSAKeypair() (*rsa.PrivateKey, *rsa.PublicKey) { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + // Generate private/public RSA keypair + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { log.Fatal().Err(err).Msg("Error generating RSA keypair") } + // Get public key + publicKey := privateKey.PublicKey + // Return keypair + return privateKey, &publicKey +} + +// Get public key from sender +func GetKey(senderAddr string) []byte { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + // Get server address by getting the IP without the port, prepending http:// and appending :9898 + serverAddr := "http://" + strings.Split(senderAddr, ":")[0] + ":9898" + // GET /key on the sender's HTTP server + response, err := http.Get(serverAddr + "/key") + if err != nil { log.Fatal().Err(err).Msg("Error getting key") } + // Close response body at the end of this function + defer response.Body.Close() + // If server responded with 200 OK + if response.StatusCode == http.StatusOK { + // Read response body into key + key, err := ioutil.ReadAll(response.Body) + if err != nil { log.Fatal().Err(err).Msg("Error reading HTTP response") } + // Return key + return key + // Otherwise + } else { + // Fatally log status code + if err != nil { log.Fatal().Int("code", response.StatusCode).Msg("HTTP Error Response Code Received") } + } + // Return nil if all else fails + return nil +} + +// Encrypt shared key with received public key +func EncryptKey(sharedKey string, recvPubKey *rsa.PublicKey) []byte { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + // Encrypt shared key using RSA + encryptedSharedKey, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, recvPubKey, []byte(sharedKey), nil) + if err != nil { log.Fatal().Err(err).Msg("Error encrypting shared key") } + // Return encrypted key + return encryptedSharedKey +} + +// Decrypt shared key using private RSA key +func DecryptKey(encryptedKey []byte, privateKey *rsa.PrivateKey) string { + // Decrypt shared key using RSA + decryptedKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, encryptedKey, nil) + if err != nil { log.Fatal().Err(err).Msg("Error decrypting shared key") } + // Get string of decrypted key + sharedKey := string(decryptedKey) + // Return shared key + return sharedKey +} \ No newline at end of file diff --git a/keyExchange.go b/keyExchange.go new file mode 100644 index 0000000..db55d1c --- /dev/null +++ b/keyExchange.go @@ -0,0 +1,65 @@ +package main + +import ( + "crypto/rsa" + "encoding/gob" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "net" + "os" +) + +// Exchange keys with sender +func ReceiverKeyExchange(key *rsa.PublicKey) string { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + // Create TCP listener on port 9797 + listener, err := net.Listen("tcp", ":9797") + if err != nil { log.Fatal().Err(err).Msg("Error starting listener") } + // Create string for sender address + var senderAddr string + // Create channel to send break signal + breakChannel := make(chan bool) + for { + // Accept connection on listener + connection, err := listener.Accept() + // Get sender address and store it in senderAddr + senderAddr = connection.RemoteAddr().String() + if err != nil { log.Fatal().Err(err).Msg("Error accepting connections") } + // Concurrently handle connection + go func(conn net.Conn) { + // Create gob encoder with connection as io.Writer + encoder := gob.NewEncoder(conn) + // Encode key into connection + err := encoder.Encode(key) + if err != nil { log.Fatal().Err(err).Msg("Error encoding key") } + // Send signal to breakChannel + breakChannel <- true + }(connection) + // Wait for break signal + select { + // When break signal arrives + case _ = <-breakChannel: + // Return sender address + return senderAddr + } + } +} + +// Exchange keys with receiver +func SenderKeyExchange(receiverIP string) *rsa.PublicKey { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + // Connect to TCP socket on receiver IP port 9797 + connection, err := net.Dial("tcp", receiverIP + ":9797") + if err != nil { log.Fatal().Err(err).Msg("Error connecting to sender") } + // Create gob decoder + decoder := gob.NewDecoder(connection) + // Instantiate rsa.PublicKey struct + recvPubKey := &rsa.PublicKey{} + // Decode key + err = decoder.Decode(recvPubKey) + if err != nil { log.Fatal().Err(err).Msg("Error decoding key") } + // Return received key + return recvPubKey +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..9ed96e1 --- /dev/null +++ b/main.go @@ -0,0 +1,137 @@ +package main + +import ( + "bufio" + "crypto/rand" + "encoding/hex" + "flag" + "fmt" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "io" + "os" + "os/signal" + "strconv" + "strings" + "syscall" + "time" +) + +func main() { + // Use ConsoleWriter logger + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + + // Create 32 byte buffer + sharedKeyBytes := make([]byte, 32) + // Read random bytes into buffer + _, err := io.ReadFull(rand.Reader, sharedKeyBytes) + if err != nil { log.Fatal().Err(err).Msg("Error generating random bytes") } + // Encode random bytes to hexadecimal + sharedKey := hex.EncodeToString(sharedKeyBytes) + // Get user's home directory + homeDir, err := os.UserHomeDir() + if err != nil { log.Fatal().Err(err).Msg("Error getting home directory") } + // Define opensend directory as ~/.opensend + opensendDir := homeDir + "/.opensend" + + // Create channel for signals + sig := make(chan os.Signal, 1) + // Send message on channel upon reception of SIGINT or SIGTERM + signal.Notify(sig, os.Interrupt, syscall.SIGTERM) + // Intercept signal + go func() { + select { + // Wait for sig to be written + case <-sig: + // Warn user that a signal has been received and that opensend is shutting down + log.Warn().Msg("Signal received. Shutting down.") + // Remove opensend directory to avoid future conflicts + _ = os.RemoveAll(opensendDir) + // Exit with code 0 + os.Exit(0) + } + }() + + // Create -t flag for type + actionType := flag.String("t", "","Type of data being sent") + // Create -d flag for data + actionData := flag.String("d", "", "Data to send") + // Create -s flag for sending + sendFlag := flag.Bool("s", false, "Send data") + // Create -r flag for receiving + recvFlag := flag.Bool("r", false, "Receive data") + // Parse flags + flag.Parse() + + // Create opensend dir ignoring errors + _ = os.Mkdir(opensendDir, 0755) + // If -s given + if *sendFlag { + // Discover all _opensend._tcp.local. mDNS services + discoveredReceivers, discoveredIPs := DiscoverReceivers() + // Create reader for STDIN + reader := bufio.NewReader(os.Stdin) + // Print hostnames of each receiver + for index, receiver := range discoveredReceivers { + // Print hostname and index+1 + fmt.Println("[" + strconv.Itoa(index + 1) + "]", receiver) + } + // Prompt user for choice + fmt.Print("Choose a receiver: ") + choiceStr, _ := reader.ReadString('\n') + // Convert input to int after trimming spaces + choiceInt, err := strconv.Atoi(strings.TrimSpace(choiceStr)) + if err != nil { log.Fatal().Err(err).Msg("Error converting choice to int") } + // Set choiceIndex to choiceInt-1 to allow for indexing + choiceIndex := choiceInt - 1 + // Get IP of chosen receiver + choiceIP := discoveredIPs[choiceIndex] + // Exchange RSA keys with receiver + rawKey := SenderKeyExchange(choiceIP) + // Encrypt shared key using RSA public key + key := EncryptKey(sharedKey, rawKey) + // Save encrypted key in opensend directory as savedKey.aesKey + SaveEncryptedKey(key, opensendDir + "/savedKey.aesKey") + // Instantiate Config object + config := NewConfig(*actionType, *actionData) + // Collect any files that may be required for transaction into opensend directory + config.CollectFiles(opensendDir) + // Create config file in opensend directory + config.CreateFile(opensendDir) + // Encrypt all files in opensend directory using shared key + EncryptFiles(opensendDir, sharedKey) + // Send all files in opensend directory using an HTTP server on port 9898 + SendFiles(opensendDir) + // If -r given + } else if *recvFlag { + // Register {hostname}._opensend._tcp.local. mDNS service and pass shutdown function + zeroconfShutdown := RegisterService() + // Shutdown zeroconf server at the end of main() + defer zeroconfShutdown() + // Generate keypair + privateKey, publicKey := GenerateRSAKeypair() + // Exchange keys with sender + senderIP := ReceiverKeyExchange(publicKey) + // Sleep 300ms to allow sender time to start HTTP server + time.Sleep(300*time.Millisecond) + // Get files from sender and place them into the opensend directory + RecvFiles(opensendDir, senderIP) + // Get encrypted shared key from sender + encryptedKey := GetKey(senderIP) + // Send stop signal to sender's HTTP server + SendSrvStopSignal(senderIP) + // Decrypt shared key + sharedKey := DecryptKey(encryptedKey, privateKey) + // Decrypt all files in opensend directory using shared key + DecryptFiles(opensendDir, sharedKey) + // Instantiate Config + config := &Config{} + // Read config file in opensend directory + config.ReadFile(opensendDir + "/config.json") + // Execute JSON action using files within opensend directory + config.ExecuteAction(opensendDir) + } + // Remove opensend directory + err = os.RemoveAll(opensendDir) + if err != nil { log.Fatal().Err(err).Msg("Error remove opensend dir") } +} \ No newline at end of file