├── 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 |  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 |