commit 561bf0682668d6ce41a0ca43eeb9ea0a7b6586c5 Author: Arsen Musayelyan Date: Thu Dec 3 02:12:43 2020 -0800 Golang Rewrite 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