├── README.md ├── ephemeral.png ├── main.go ├── scripts.sql └── static ├── create.html ├── ephemeral.js ├── error.html ├── favicon.png ├── home.html ├── style.css ├── viewClient.html └── viewServer.html /README.md: -------------------------------------------------------------------------------- 1 | Temporary encrypted messaging 2 | 3 | ![ephemeral.png](ephemeral.png) 4 | -------------------------------------------------------------------------------- /ephemeral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergeron/ephemeral/f0069dc4e9036c50946db5f60513842116563dd9/ephemeral.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* main.go */ 2 | 3 | package main 4 | import ( 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/rand" 8 | "database/sql" 9 | "encoding/base64" 10 | "encoding/hex" 11 | "errors" 12 | "fmt" 13 | "html/template" 14 | "io" 15 | "log" 16 | "net/http" 17 | "os" 18 | "strconv" 19 | "strings" 20 | "sync" 21 | _ "github.com/go-sql-driver/mysql" 22 | ) 23 | 24 | var db *sql.DB = connectDb() 25 | 26 | func main() { 27 | http.HandleFunc("/create/server/", createServerHandler) 28 | http.HandleFunc("/create/client/", createClientHandler) 29 | http.HandleFunc("/view/server/", viewServerHandler) 30 | http.HandleFunc("/view/client/", viewClientHandler) 31 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 32 | http.ServeFile(w, r, "static/home.html") 33 | }) 34 | 35 | fs := http.FileServer(http.Dir("static")) 36 | http.Handle("/static/", http.StripPrefix("/static/", fs)) 37 | 38 | err := http.ListenAndServe(":11994", nil) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | } 43 | 44 | func connectDb() (*sql.DB){ 45 | tablename := "Ephemeral" 46 | username := os.Getenv("ephemeralUsername") 47 | password := os.Getenv("ephemeralPassword") 48 | 49 | db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@/%s", username, password, tablename)) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | 54 | /* Test connection */ 55 | err = db.Ping() 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | return db 60 | } 61 | 62 | /* Write the given error message as HTML */ 63 | func writeError(w http.ResponseWriter, message string){ 64 | type Out struct { 65 | Message string 66 | } 67 | template.Must(template.ParseFiles("static/error.html")).Execute(w, Out{message}) 68 | } 69 | 70 | /* 128 bit AES */ 71 | func encrypt(key, text []byte) ([]byte, error) { 72 | block, err := aes.NewCipher(key) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | b := base64.StdEncoding.EncodeToString(text) 78 | ciphertext := make([]byte, aes.BlockSize+len(b)) 79 | iv := ciphertext[:aes.BlockSize] 80 | if _, err := io.ReadFull(rand.Reader, iv); err != nil { 81 | return nil, err 82 | } 83 | cfb := cipher.NewCFBEncrypter(block, iv) 84 | cfb.XORKeyStream(ciphertext[aes.BlockSize:], []byte(b)) 85 | return ciphertext, nil 86 | } 87 | 88 | /* 128 bit AES */ 89 | func decrypt(key, text []byte) ([]byte, error) { 90 | block, err := aes.NewCipher(key) 91 | if err != nil { 92 | return nil, err 93 | } 94 | if len(text) < aes.BlockSize { 95 | return nil, errors.New("Ciphertext too short") 96 | } 97 | iv := text[:aes.BlockSize] 98 | text = text[aes.BlockSize:] 99 | cfb := cipher.NewCFBDecrypter(block, iv) 100 | cfb.XORKeyStream(text, text) 101 | data, err := base64.StdEncoding.DecodeString(string(text)) 102 | if err != nil { 103 | return nil, err 104 | } 105 | return data, nil 106 | } 107 | 108 | /* POST /create/server */ 109 | func createServerHandler(w http.ResponseWriter, r *http.Request) { 110 | if r.Method != http.MethodPost { 111 | http.Error(w, "POST required", http.StatusMethodNotAllowed) 112 | return 113 | } 114 | 115 | text := r.PostFormValue("text") 116 | expireMinutes, err := strconv.Atoi(r.PostFormValue("expireMinutes")) 117 | if err != nil { 118 | expireMinutes = 43200 /* Default expire in 30 days */ 119 | } 120 | 121 | if len(text) > 16000{ 122 | writeError(w, "Message too long. Max character length is 16000.") 123 | return 124 | } 125 | 126 | /* Generate 128 bit key */ 127 | key128bits := make([]byte, 16) 128 | _, err = rand.Read(key128bits) 129 | if err != nil { 130 | http.Error(w, "Something went wrong :(", http.StatusInternalServerError) 131 | return 132 | } 133 | 134 | /* Encrypt the text */ 135 | encryptedtextBytes, err := encrypt(key128bits, []byte(text)) 136 | if err != nil { 137 | http.Error(w, "Something went wrong :(", http.StatusInternalServerError) 138 | return 139 | } 140 | 141 | msgId := generateMsgId(db) 142 | _, err = db.Exec("insert into messages values (?, ?, ?, UNIX_TIMESTAMP(), ?, ?)", 143 | msgId, hex.EncodeToString(encryptedtextBytes), nil, expireMinutes, true) 144 | 145 | if err != nil { 146 | http.Error(w, "Something went wrong :(", http.StatusInternalServerError) 147 | return 148 | } 149 | 150 | type Out struct { 151 | MsgId string 152 | Key string 153 | } 154 | 155 | tmpl := template.Must(template.ParseFiles("static/create.html")) 156 | tmpl.Execute(w, Out{msgId, hex.EncodeToString(key128bits)}) 157 | } 158 | 159 | /* POST /create/client */ 160 | func createClientHandler(w http.ResponseWriter, r *http.Request) { 161 | if r.Method != http.MethodPost { 162 | http.Error(w, "POST required", http.StatusMethodNotAllowed) 163 | return 164 | } 165 | 166 | encryptedText := r.PostFormValue("text") 167 | salt := r.PostFormValue("salt") 168 | expireMinutes, err := strconv.Atoi(r.PostFormValue("expireMinutes")) 169 | if err != nil { 170 | expireMinutes = 43200 /* Default expire in 30 days */ 171 | } 172 | 173 | if len(encryptedText) > 16000{ 174 | writeError(w, "Message too long. Max character length is 16000.") 175 | return 176 | } 177 | 178 | msgId := generateMsgId(db) 179 | _, err = db.Exec("insert into messages values (?, ?, ?, UNIX_TIMESTAMP(), ?, ?)", 180 | msgId, encryptedText, salt, expireMinutes, false) 181 | 182 | if err != nil { 183 | http.Error(w, "Something went wrong :(", http.StatusInternalServerError) 184 | return 185 | } 186 | 187 | w.Write([]byte("https://ephemeral.pw/view/client/" + msgId)) 188 | } 189 | 190 | /* GET /view/server */ 191 | func viewServerHandler(w http.ResponseWriter, r *http.Request) { 192 | if r.Method != http.MethodGet { 193 | http.Error(w, "GET required", http.StatusMethodNotAllowed) 194 | return 195 | } 196 | 197 | /* Blacklist sites that GET the url before sending to recipient */ 198 | blacklist := [...]string{"facebook"} 199 | for _,e := range blacklist { 200 | if strings.Contains(r.UserAgent(), e) { 201 | fmt.Fprintf(w, "Go away %s! This is only for the recipient!", e) 202 | return 203 | } 204 | } 205 | 206 | /* ephemeral.pw/view/server/msgId/key/ */ 207 | queryString := strings.TrimSuffix(r.URL.Path[len("/view/server/"):],"/") 208 | params := strings.Split(queryString, "/") 209 | if len(params) != 2 { 210 | writeError(w, "Message not found. It may have been deleted.") 211 | return 212 | } 213 | 214 | msgId := params[0] 215 | keyString := params[1] 216 | keyBytes, err := hex.DecodeString(keyString) 217 | if err != nil { 218 | /* Key is not hex */ 219 | writeError(w, "Message not found. It may have been deleted.") 220 | return 221 | } 222 | 223 | var m sync.Mutex 224 | m.Lock() /* ONLY ONE THREAD IN HERE AT A TIME */ 225 | 226 | var encryptedText string 227 | err = db.QueryRow("SELECT encrypted_text FROM messages WHERE id = ?", msgId).Scan(&encryptedText) 228 | if err != nil { 229 | writeError(w, "Message not found. It may have been deleted.") 230 | return 231 | } 232 | 233 | /* Decrypt message */ 234 | encryptedtextBytes , err := hex.DecodeString(encryptedText) 235 | if err != nil { 236 | writeError(w, "Message not found. It may have been deleted.") 237 | return 238 | } 239 | messageBytes, err := decrypt(keyBytes, []byte(encryptedtextBytes)) 240 | if err != nil { 241 | /* Valid msgId, but invalid key */ 242 | writeError(w, "Message not found. It may have been deleted.") 243 | return 244 | } 245 | 246 | db.Exec("DELETE FROM messages WHERE id = ? LIMIT 1", msgId) 247 | 248 | m.Unlock() 249 | 250 | type Out struct { 251 | Message []string 252 | } 253 | 254 | message := template.HTMLEscapeString(string(messageBytes)) /* no XSS */ 255 | tmpl := template.Must(template.ParseFiles("static/viewServer.html")) 256 | tmpl.Execute(w, Out{strings.Split(message, "\n")}) 257 | } 258 | 259 | /* GET /view/client */ 260 | func viewClientHandler(w http.ResponseWriter, r *http.Request) { 261 | if r.Method != http.MethodGet { 262 | http.Error(w, "GET required", http.StatusMethodNotAllowed) 263 | return 264 | } 265 | 266 | /* Blacklist sites that GET the url before sending to recipient */ 267 | blacklist := [...]string{"facebook"} 268 | for _,e := range blacklist { 269 | if strings.Contains(r.UserAgent(), e) { 270 | fmt.Fprintf(w, "Go away %s! This is only for the recipient!", e) 271 | return 272 | } 273 | } 274 | 275 | /* ephemeral.pw/view/client/msgId */ 276 | queryString := strings.TrimSuffix(r.URL.Path[len("/view/client/"):],"/") 277 | params := strings.Split(queryString, "/") 278 | if len(params) != 1 { 279 | writeError(w, "Message not found. It may have been deleted.") 280 | return 281 | } 282 | msgId := params[0] 283 | 284 | var m sync.Mutex 285 | m.Lock() /* ONLY ONE THREAD IN HERE AT A TIME */ 286 | 287 | var encryptedText string 288 | var salt string 289 | err := db.QueryRow("SELECT encrypted_text, salt FROM messages WHERE id = ?", msgId).Scan(&encryptedText, &salt) 290 | if err != nil { 291 | writeError(w, "Message not found. It may have been deleted.") 292 | return 293 | } 294 | 295 | db.Exec("DELETE FROM messages WHERE id = ? LIMIT 1", msgId) 296 | 297 | m.Unlock() 298 | 299 | type Out struct { 300 | Message string 301 | Salt string 302 | } 303 | 304 | tmpl := template.Must(template.ParseFiles("static/viewClient.html")) 305 | tmpl.Execute(w, Out{encryptedText, salt}) 306 | } 307 | 308 | /* Generate unique 64 random bits */ 309 | func generateMsgId(db *sql.DB) string { 310 | 311 | rand64bits := make([]byte, 8) 312 | _, err := rand.Read(rand64bits) 313 | if err != nil { 314 | return generateMsgId(db) 315 | } 316 | id := hex.EncodeToString(rand64bits) 317 | 318 | /* Check for collision */ 319 | var available bool 320 | db.QueryRow("SELECT COUNT(*) = 0 FROM messages WHERE id = ?", id).Scan(&available) 321 | 322 | if(available){ 323 | return id 324 | } else { 325 | return generateMsgId(db) 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /scripts.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS `messages`; 2 | CREATE TABLE `messages` ( 3 | `id` VARBINARY(32) NOT NULL, 4 | `encrypted_text` VARBINARY(43688) NOT NULL, 5 | `salt` VARCHAR(255), 6 | `dt_created_epoch` BIGINT NOT NULL, 7 | `expire_minutes` INT, 8 | `server_encrypted` BOOLEAN NOT NULL 9 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1; 10 | 11 | -- 12 | -- Self destruct expired messages 13 | -- 14 | SET GLOBAL event_scheduler = ON; 15 | DROP EVENT IF EXISTS message_reaper; 16 | 17 | CREATE EVENT message_reaper ON SCHEDULE EVERY 1 MINUTE DO 18 | DELETE FROM messages WHERE (dt_created_epoch + (60 * expire_minutes)) < UNIX_TIMESTAMP(); 19 | 20 | -------------------------------------------------------------------------------- /static/create.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ephemeral 6 | 7 | 8 | 13 | 14 | 15 | 16 |
17 | Ephemeral 18 |
19 |
20 |

Give this URL to the recipient:

21 |
22 |
23 | 26 |
27 |

28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /static/ephemeral.js: -------------------------------------------------------------------------------- 1 | /* ephemeral.js */ 2 | 3 | $(document).ready(function(){ 4 | $('#options').find('input[type=checkbox]').on('click', function(){ 5 | $(this).closest('.option').children('input').prop('disabled', function(i, v){ 6 | if(!v){$(this).val('');} 7 | return !v; 8 | }); 9 | }); 10 | 11 | /* iOS hack to force "cut copy paste" keyboard */ 12 | if(/iPhone|iPad|iPod/i.test(navigator.userAgent)){ 13 | $('.link').parent().attr('contenteditable', 'true'); 14 | } 15 | 16 | $('.link').on('click', function () { 17 | var sel = window.getSelection(); 18 | var range = document.createRange(); 19 | range.selectNodeContents(this); 20 | sel.removeAllRanges(); 21 | sel.addRange(range); 22 | }); 23 | }); 24 | 25 | function create(message){ 26 | if($('#pwdCheck').is(':checked')){ 27 | encrypt(message, $('#pwd').val()); 28 | return false; 29 | } else { 30 | return true; 31 | } 32 | } 33 | 34 | function encrypt(message, password){ 35 | if(!message){ 36 | message = " "; 37 | } 38 | 39 | var salt = CryptoJS.lib.WordArray.random(128/8).toString(); 40 | var key = CryptoJS.PBKDF2(password, salt, { keySize: 128/32 }).toString(); 41 | var encryptedText = CryptoJS.AES.encrypt(message, key).toString(); 42 | var expireMinutes = $('#expire').val(); 43 | 44 | $.ajax({ 45 | method: 'POST', 46 | url: '/create/client/', 47 | data: { 48 | text: encryptedText, 49 | expireMinutes: expireMinutes, 50 | salt: salt 51 | } 52 | }) 53 | .done(function(data) { 54 | $('#clientEncryptedUrl').html(data); 55 | $('#result').show(); 56 | $(window).scrollTop(10000); 57 | }) 58 | .fail(function(status, err){ 59 | console.log(status + ' ' + err); 60 | }); 61 | } 62 | 63 | function decrypt(password){ 64 | try { 65 | var key = CryptoJS.PBKDF2(password, salt, { keySize: 128/32 }).toString(); 66 | var decrypted = CryptoJS.AES.decrypt(encryptedText, key).toString(CryptoJS.enc.Utf8).split("\n").join("
"); 67 | 68 | if(decrypted){ 69 | $('#decryptedMessage').html(''); 70 | var lines = decrypted.split('
'); 71 | for(var i=0; i < lines.length; i++){ 72 | $('#decryptedMessage').append(escapeHtml(lines[i]) + '
'); 73 | } 74 | } 75 | } catch (e) {return false;} /* Error when incorrect key doesn't generate UTF8 */ 76 | 77 | return false; 78 | } 79 | 80 | function escapeHtml(str) { 81 | var div = document.createElement('div'); 82 | div.appendChild(document.createTextNode(str)); 83 | return div.innerHTML; 84 | } 85 | -------------------------------------------------------------------------------- /static/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ephemeral 6 | 7 | 8 | 9 | 10 |
11 | Ephemeral 12 |
13 |
14 |
15 |

{{.Message}}

16 |
17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergeron/ephemeral/f0069dc4e9036c50946db5f60513842116563dd9/static/favicon.png -------------------------------------------------------------------------------- /static/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ephemeral 6 | 7 | 8 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | Ephemeral 20 |
21 |
22 |
23 |

1. Write a message

24 |
25 |

2. Send URL to recipient

26 |
27 |

3. Message destroyed when read

28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 |
36 | 40 | 41 |
42 |

43 |
44 | 48 | 49 |
50 |
51 |
52 | 55 |
56 |
57 |
58 |
59 | Give this URL to the recipient:
60 |
61 | 63 |
64 |
65 |

66 | 67 | 68 | -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | html{ 2 | margin:0; 3 | padding:0; 4 | background-color: #efefef; 5 | font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif; 6 | } 7 | 8 | body{ 9 | margin:0; 10 | padding:0; 11 | } 12 | 13 | textarea { 14 | overflow: auto; 15 | } 16 | 17 | .center{ 18 | display: block; 19 | margin-left: auto; 20 | margin-right: auto; 21 | } 22 | 23 | a:hover{ 24 | color: #36D7B7; 25 | } 26 | 27 | #title{ 28 | padding: 5px 0px 13px 10px; 29 | font-size: 60px; 30 | } 31 | 32 | #title a{ 33 | color: black; 34 | text-decoration: none; 35 | } 36 | 37 | #title a:hover{ 38 | color: #36D7B7; 39 | } 40 | 41 | #whiteStrip{ 42 | padding-top: 15px; 43 | padding-bottom: 15px; 44 | background-color: white; 45 | } 46 | 47 | #whiteStrip h1{ 48 | font-size: 23px; 49 | font-weight: normal; 50 | margin:0; 51 | padding:0; 52 | } 53 | 54 | #msgText{ 55 | font-size: 20px; 56 | width: 512px; 57 | height: 161px; /* So Chrome mobile wont resize the box when typing */ 58 | } 59 | 60 | .well { 61 | font-size: 20px; 62 | font-family: monospace; 63 | word-wrap: break-word; 64 | color: white; 65 | min-height: 20px; 66 | padding: 15px; 67 | background-color: black; 68 | border: 4px solid #36D7B7; 69 | -moz-border-radius: 15px; 70 | border-radius: 15px; 71 | outline: none; 72 | box-sizing: border-box; 73 | } 74 | 75 | .link { 76 | font-size: 12px; 77 | width: 100%; 78 | } 79 | 80 | .submitBtn { 81 | color: white; 82 | background: black; 83 | font-size: 20px; 84 | padding: 15px 30px; 85 | margin-bottom: 2px; 86 | outline: none; 87 | border: 5px solid #36D7B7; 88 | cursor: pointer; 89 | } 90 | 91 | .submitBtn:hover { 92 | color: black; 93 | background: #36D7B7; 94 | } 95 | 96 | #options{ 97 | max-width:470px; 98 | height: 100px; 99 | text-align: center; 100 | font-size: 20px; 101 | } 102 | 103 | #options input{ 104 | font-size: 20px; 105 | } 106 | 107 | .option{ 108 | float: left; 109 | } 110 | 111 | #result{ 112 | display: none; 113 | text-align: center; 114 | } 115 | 116 | #decryptInput{ 117 | font-size: 20px; 118 | width: 40%; 119 | margin-right:20px; 120 | } 121 | -------------------------------------------------------------------------------- /static/viewClient.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ephemeral 6 | 7 | 8 | 13 | 14 | 15 | 16 | 20 | 21 | 22 |
23 | Ephemeral 24 |
25 |
26 |
27 |
28 |
29 | {{.Message}} 30 |
31 |
32 |
33 |
34 | 35 | 36 |
37 |
38 |



39 |

This message has been deleted and cannot be viewed again.

40 |
41 |
42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /static/viewServer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ephemeral 6 | 7 | 8 | 13 | 14 | 22 | 23 | 24 |
25 | Ephemeral 26 |
27 |
28 |
29 |
30 |
31 |
32 |

33 |

This message has been deleted and cannot be viewed again.

34 |
35 |
36 |
37 | 38 | 39 | --------------------------------------------------------------------------------