├── LICENSE ├── README.md ├── eureka.go ├── eureka.ico ├── folders.go ├── go.mod └── go.sum /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 David Wong 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![EUREKA](https://i.imgur.com/qSscFjx.png) 2 | 3 | Eureka is a simple tool to encrypt files and folders. It works on Windows, Linux and MacOS. 4 | 5 | ## Security and Status 6 | 7 | Eureka is pretty simple, and well commented. Anyone is free to audit the software themselves. 8 | 9 | ## Install 10 | 11 | There are several ways to install Eureka, with more on the way. 12 | 13 | **Binary**. 14 | 15 | [Get a binary here](https://github.com/mimoo/eureka/releases). 16 | 17 | **Go get**. 18 | 19 | If you have [Golang](https://golang.org/) installed and `/usr/local/go/bin` is in your PATH, you should be able to simply get the binary by doing 20 | 21 | ``` 22 | go get github.com/mimoo/eureka 23 | ``` 24 | 25 | **Homebrew**. 26 | 27 | If you are on MacOS, just use Homebrew: 28 | 29 | ``` 30 | brew tap mimoo/eureka && brew install mimoo/eureka/eureka 31 | ``` 32 | 33 | ## Usage 34 | 35 | **1.** You are trying to send *Bob* the file `myfile.txt`.Start by encrypting the file via: 36 | 37 | ``` 38 | eureka myfile.txt 39 | ``` 40 | 41 | which will return a one-time 256-bit AES key and create a new `myfile.txt.encrypted` file: 42 | 43 | ``` 44 | File encrypted at myfile.txt.encrypted 45 | In a different secure channel, pass the following one-time key to your recipient. 46 | 613800fc6cf88f09aa6aeafab3eedd627503e6c5de28040c549efc2c6f80178d 47 | ``` 48 | 49 | **2.** Find a channel to send the encrypted file to *Bob*. It could be via email, or via dropbox, or via google drive, etc. 50 | 51 | **3.** You then need to transmit the one-time key (`613800fc6cf88f09aa6aeafab3eedd627503e6c5de28040c549efc2c6f80178d`) to *Bob* in a **different channel**. For example, if you exchanged the file (or a link to the file) via email, then send this key to *Bob* via WhatsApp. 52 | 53 | **If you send both the encrypted file and the one-time key in the same channel, encryption is useless**. 54 | 55 | **4.** Once *Bob* receives the file and the one-time key from two different channels, he can decrypt the file via this command: 56 | 57 | ``` 58 | eureka myfile.txt.encrypted 59 | ``` 60 | 61 | which will create a new file `myfile.txt` under a `decrypted` folder containing the original content. 62 | -------------------------------------------------------------------------------- /eureka.go: -------------------------------------------------------------------------------- 1 | // 2 | // Eureka 3 | // ====== 4 | // Eureka is a handy utility to encrypt files and folders. It follows several principles: 5 | // 6 | // - I want to encrypt and send a file to someone or myself. 7 | // - Eureka should be easy to install and share. 8 | // - PGP is too cumbersome to use, I want something simple (right-click > encrypt). 9 | // - I already share two separate and secure channels with the recipient (mail + signal for example). 10 | // 11 | // Here how the code is organized: 12 | // 13 | // - eureka.go // the main code to encrypt files 14 | // - folders.go // the code to compress folders 15 | // - ui_windows.go // the code to add right-click encrypt/decrypt on windows 16 | // - ui_macOS.go // the code to add right-click encrypt/decrypt on macOS 17 | // - ui_linux.go // the code to add right-click encrypt/decrypt on linux 18 | 19 | package main 20 | 21 | import ( 22 | "bufio" 23 | "bytes" 24 | "crypto/aes" 25 | "crypto/cipher" 26 | "crypto/rand" 27 | "encoding/hex" 28 | "flag" 29 | "fmt" 30 | "io" 31 | "io/ioutil" 32 | "os" 33 | "os/exec" 34 | "path/filepath" 35 | "runtime" 36 | "strings" 37 | 38 | // cross-platform clipboard 39 | "github.com/atotto/clipboard" 40 | ) 41 | 42 | // open a link in your favorite browser 43 | func openBrowser(url string) { 44 | var err error 45 | 46 | switch runtime.GOOS { 47 | case "linux": 48 | err = exec.Command("xdg-open", url).Start() 49 | case "windows": 50 | err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() 51 | case "darwin": 52 | err = exec.Command("open", url).Start() 53 | default: 54 | err = fmt.Errorf("unsupported platform") 55 | } 56 | if err != nil { 57 | fmt.Println(err) 58 | os.Exit(1) 59 | } 60 | } 61 | 62 | // prompt for the key with a GUI 63 | func promptKey(noClipboard bool) (string, error) { 64 | var key string 65 | reader := bufio.NewReader(os.Stdin) 66 | if !noClipboard { 67 | fmt.Printf("Do you want to use your clipboard as the key? (Y/n): ") 68 | useClipboard, err := reader.ReadString('\n') 69 | if err != nil { 70 | return "", err 71 | } 72 | 73 | // clipboard option 74 | useClipboard = strings.TrimSpace(useClipboard) 75 | if strings.ToLower(useClipboard) != "n" { 76 | key, err := clipboard.ReadAll() 77 | if err != nil { 78 | return "", fmt.Errorf("error: couldn't read the key from clipboard: %s", err) 79 | } 80 | return key, nil 81 | } else { 82 | noClipboard = true 83 | } 84 | } 85 | 86 | if noClipboard { 87 | // terminal option 88 | fmt.Print("Enter 256-bit hexadecimal key: ") 89 | key, err := reader.ReadString('\n') 90 | if err != nil { 91 | return "", fmt.Errorf("couldn't read the key: %s", err) 92 | } 93 | 94 | key = strings.TrimSpace(key) 95 | return key, nil 96 | } 97 | 98 | return key, nil 99 | } 100 | 101 | func main() { 102 | var err error 103 | 104 | // parse flags 105 | about := flag.Bool("about", false, "to get redirected to github.com/mimoo/eureka") 106 | noClipboard := flag.Bool("noclipboard", false, "no clipboard") 107 | 108 | flag.Parse() 109 | 110 | // redirect to github.com/mimoo/eureka 111 | if *about { 112 | openBrowser("https://www.github.com/mimoo/eureka") 113 | return 114 | } 115 | 116 | if len(flag.Args()) == 0 { 117 | fmt.Println("===================ᕙ(⇀‸↼‶)ᕗ===================") 118 | fmt.Println(" Eureka is a tool to help you encrypt/decrypt a file") 119 | fmt.Println(" to encrypt:") 120 | fmt.Println(" eureka your-file") 121 | fmt.Println(" to decrypt:") 122 | fmt.Println(" eureka your-file.encrypted") 123 | fmt.Println("===================ᕙ(⇀‸↼‶)ᕗ===================") 124 | flag.Usage() 125 | return 126 | } 127 | 128 | encrypt, decrypt := new(bool), new(bool) 129 | inFile := &flag.Args()[0] 130 | ext := strings.ToLower(filepath.Ext(*inFile)) 131 | if ext != ".encrypted" { 132 | *encrypt = true 133 | } else { 134 | *decrypt = true 135 | } 136 | 137 | // nonce = 1111... 138 | nonce := bytes.Repeat([]byte{1}, 12) 139 | 140 | // key = ? 141 | var key []byte 142 | 143 | // generate random key if we're encrypting 144 | if *encrypt { 145 | key = make([]byte, 32) 146 | if _, err = io.ReadFull(rand.Reader, key); err != nil { 147 | fmt.Println("error: randomness cannot be generated on your system") 148 | flag.Usage() 149 | os.Exit(1) 150 | } 151 | } 152 | 153 | // get key if we are decrypting 154 | if *decrypt { 155 | // get key 156 | keyHex, err := promptKey(*noClipboard) 157 | if err != nil { 158 | fmt.Printf("eureka: %s\n", err) 159 | os.Exit(1) 160 | } 161 | 162 | // decode and check key 163 | key, err = hex.DecodeString(keyHex) 164 | if err != nil || len(key) != 32 { 165 | fmt.Println("error: the key has to be a 256-bit hexadecimal string") 166 | os.Exit(1) 167 | } 168 | } 169 | 170 | // create AES-GCM instance 171 | cipherAES, err := aes.NewCipher(key) 172 | if err != nil { 173 | fmt.Println("Can't instantiate AES") 174 | os.Exit(1) 175 | } 176 | AESgcm, err := cipher.NewGCM(cipherAES) 177 | if err != nil { 178 | fmt.Println("Can't instantiate GCM") 179 | os.Exit(1) 180 | } 181 | 182 | // encrypt or decrypt 183 | var contentAfter []byte 184 | 185 | if *encrypt { 186 | // compress file or folder 187 | var buf bytes.Buffer 188 | if err := compress(*inFile, &buf); err != nil { 189 | fmt.Println(err) 190 | os.Exit(1) 191 | } 192 | // encrypt compressed content 193 | contentAfter = AESgcm.Seal(nil, nonce, buf.Bytes(), nil) 194 | // write file to disk 195 | _, outFile := filepath.Split(*inFile) 196 | outFile = outFile + ".encrypted" 197 | if err = ioutil.WriteFile(outFile, contentAfter, 0600); err != nil { 198 | fmt.Println(err) 199 | os.Exit(1) 200 | } 201 | // place key in clipboard 202 | stringKey := fmt.Sprintf("%032x", key) 203 | // notification 204 | fmt.Printf("File encrypted at %s\n", outFile) 205 | fmt.Println("Your recipient will need Eureka to decrypt the file: https://github.com/mimoo/eureka") 206 | fmt.Println("In a different secure channel, pass the following one-time key to your recipient.") 207 | 208 | // clipboard option 209 | reader := bufio.NewReader(os.Stdin) 210 | if !*noClipboard { 211 | fmt.Printf("Do you want to copy the key to your clipboard? (Y/n): ") 212 | useClipboard, err := reader.ReadString('\n') 213 | if err != nil { 214 | fmt.Printf("read clipboard input error: %s\nshow key here anyway:\n", err) 215 | fmt.Println(stringKey) 216 | return 217 | } 218 | 219 | useClipboard = strings.TrimSpace(useClipboard) 220 | if strings.ToLower(useClipboard) != "n" { // use clipboard 221 | if err := clipboard.WriteAll(stringKey); err != nil { 222 | fmt.Printf("write clipboard error: %s\nshow key here anyway:\n", err) 223 | fmt.Println(stringKey) 224 | } else { 225 | fmt.Println("key copied to your clipboard") 226 | } 227 | } else { // print to terminal and pause 228 | fmt.Println(stringKey) 229 | } 230 | } else { 231 | fmt.Println(stringKey) 232 | } 233 | 234 | return 235 | } 236 | 237 | if *decrypt { 238 | // open file 239 | content, err := ioutil.ReadFile(*inFile) 240 | if err != nil { 241 | fmt.Println("error: cannot open input file") 242 | flag.Usage() 243 | os.Exit(1) 244 | } 245 | // decrypt 246 | contentAfter, err = AESgcm.Open(nil, nonce, content, nil) 247 | if err != nil { 248 | fmt.Println("error: cannot decrypt. The key is not correct or someone tried to modify your file.") 249 | os.Exit(1) 250 | } 251 | // create a decrypted folder 252 | if _, err := os.Stat("./decrypted"); err != nil { 253 | if err := os.MkdirAll("./decrypted", 0755); err != nil { 254 | fmt.Println("error: cannot create folder 'decrypted'") 255 | os.Exit(1) 256 | } 257 | } else { 258 | fmt.Println("info: the folder 'decrypted' already exists. Decrypting the file could overwrite files.") 259 | return 260 | } 261 | // decompress it 262 | buf := bytes.NewReader(contentAfter) 263 | if err := decompress(buf, "./decrypted"); err != nil { 264 | fmt.Println(err) 265 | os.Exit(1) 266 | } 267 | // notification 268 | fmt.Println("File decrypted at decrypted/\nCheers.") 269 | 270 | return 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /eureka.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimoo/eureka/99112743244fca318cabe54bfb91cf8bf6d7dc33/eureka.ico -------------------------------------------------------------------------------- /folders.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | ) 12 | 13 | func compress(src string, buf io.Writer) error { 14 | // tar > gzip > buf 15 | zr := gzip.NewWriter(buf) 16 | tw := tar.NewWriter(zr) 17 | 18 | // is file a folder? 19 | fi, err := os.Stat(src) 20 | if err != nil { 21 | return err 22 | } 23 | mode := fi.Mode() 24 | if mode.IsRegular() { 25 | // get header 26 | header, err := tar.FileInfoHeader(fi, src) 27 | if err != nil { 28 | return err 29 | } 30 | // write header 31 | if err := tw.WriteHeader(header); err != nil { 32 | return err 33 | } 34 | // get content 35 | data, err := os.Open(src) 36 | if err != nil { 37 | return err 38 | } 39 | if _, err := io.Copy(tw, data); err != nil { 40 | return err 41 | } 42 | } else if mode.IsDir() { // folder 43 | 44 | // walk through every file in the folder 45 | filepath.Walk(src, func(file string, fi os.FileInfo, err error) error { 46 | // generate tar header 47 | header, err := tar.FileInfoHeader(fi, file) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | // must provide real name 53 | // (see https://golang.org/src/archive/tar/common.go?#L626) 54 | header.Name = filepath.ToSlash(file) 55 | 56 | // write header 57 | if err := tw.WriteHeader(header); err != nil { 58 | return err 59 | } 60 | // if not a dir, write file content 61 | if !fi.IsDir() { 62 | data, err := os.Open(file) 63 | if err != nil { 64 | return err 65 | } 66 | if _, err := io.Copy(tw, data); err != nil { 67 | return err 68 | } 69 | } 70 | return nil 71 | }) 72 | } else { 73 | return fmt.Errorf("error: file type not supported") 74 | } 75 | 76 | // produce tar 77 | if err := tw.Close(); err != nil { 78 | return err 79 | } 80 | // produce gzip 81 | if err := zr.Close(); err != nil { 82 | return err 83 | } 84 | // 85 | return nil 86 | } 87 | 88 | // check for path traversal and correct forward slashes 89 | func validRelPath(p string) bool { 90 | if p == "" || strings.Contains(p, `\`) || strings.HasPrefix(p, "/") || strings.Contains(p, "../") { 91 | return false 92 | } 93 | return true 94 | } 95 | 96 | func decompress(src io.Reader, dst string) error { 97 | // ungzip 98 | zr, err := gzip.NewReader(src) 99 | if err != nil { 100 | return err 101 | } 102 | // untar 103 | tr := tar.NewReader(zr) 104 | 105 | // uncompress each element 106 | for { 107 | header, err := tr.Next() 108 | if err == io.EOF { 109 | break // End of archive 110 | } 111 | if err != nil { 112 | return err 113 | } 114 | target := header.Name 115 | 116 | // validate name against path traversal 117 | if !validRelPath(header.Name) { 118 | return fmt.Errorf("tar contained invalid name error %q", target) 119 | } 120 | 121 | // add dst + re-format slashes according to system 122 | target = filepath.Join(dst, header.Name) 123 | // if no join is needed, replace with ToSlash: 124 | // target = filepath.ToSlash(header.Name) 125 | 126 | // check the type 127 | switch header.Typeflag { 128 | 129 | // if its a dir and it doesn't exist create it (with 0755 permission) 130 | case tar.TypeDir: 131 | if _, err := os.Stat(target); err != nil { 132 | if err := os.MkdirAll(target, 0755); err != nil { 133 | return err 134 | } 135 | } 136 | // if it's a file create it (with same permission) 137 | case tar.TypeReg: 138 | fileToWrite, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) 139 | if err != nil { 140 | return err 141 | } 142 | // copy over contents 143 | if _, err := io.Copy(fileToWrite, tr); err != nil { 144 | return err 145 | } 146 | // manually close here after each file operation; defering would cause each file close 147 | // to wait until all operations have completed. 148 | fileToWrite.Close() 149 | } 150 | } 151 | 152 | // 153 | return nil 154 | } 155 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mimoo/eureka 2 | 3 | go 1.14 4 | 5 | require github.com/atotto/clipboard v0.1.2 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY= 2 | github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 3 | --------------------------------------------------------------------------------