Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/cagerton/dropthat

Client side encryption can be part of this complete breakfast. Also Lua.
https://github.com/cagerton/dropthat

Last synced: 23 days ago
JSON representation

Client side encryption can be part of this complete breakfast. Also Lua.

Awesome Lists containing this project

README

        

Building an OpenResty events server
===================================

[DropTh.at](https://dropth.at/) is a chat service demo built with OpenResty. It secures your messages using a combination of traditional https and clientside crypto. Chat rooms are randomly generated by the client and can be shared by sharing the room's full url. The encryption keys are stored in the #fragment-identifier and are not sent to the server. That means the url is the room password. The core server and client demo are [open source](https://github.com/cagerton/dropthat).

Reality check: It still relies on a trustworthy host and on SSL to deliver the initial html+js. It is just a is a demo. See [Below](#on-javascript-cryptography).

### Context: Javascript EventSource API:

I'm using the [EventSource API](http://www.w3.org/TR/eventsource/) for delivering messages. It is [supported](http://caniuse.com/#feat=eventsource) by all the browsers that I currently care about (and can be shimmed into some by Microsoft). The protocol is just a chunked HTTP stream with content-type "text/event-source" and formatted event messages. It has a very low overhead and it plays nicely with the pub-sub pattern.

Here are the headers and the first event in a stream (as rendered by curl):
```http
< HTTP/1.1 200 OK
< Content-Type: text/event-stream
< Server: dropth.at
< Connection: keep-alive
< Transfer-Encoding: chunked
<
event:count
data:count=2

```
And here's how you could capture it:
```javascript
var events = new EventSource( '' );
events.addEventListener('count', function(c){ console.log("Got ", c); });
```
>Got count=2

### Nginx, Lua, and OpenResty:

The server is built with [OpenResty](http://openresty.org/); that's Nginx, Lua, and a few extra modules. Leafo has a [nice intro](http://leafo.net/posts/creating_an_image_server.html) to building with OpenResty if you're not familiar. I highly suggest you check it out.

I'll assume you understand the basics of Nginx and Lua from this point. Here are my core pub/sub locations in the [nginx config](https://github.com/cagerton/dropthat/blob/master/chat.conf).
```nginx
worker_processes 1;
http {
init_by_lua 'chat = require "chat"';

server {
listen 80 so_keepalive=20s:3s:6;
set_by_lua $dontcare 'chat.check_init()';

location ~ "^/sub/(?P[a-zA-Z\d_-]+)$" {
lua_socket_log_errors off;
lua_check_client_abort on;
content_by_lua 'chat.event_source_location(ngx.var.channel)';
}

location ~ "^/pub/(?P[a-zA-Z\d_-]+)$" {
client_max_body_size 32k;
content_by_lua '
ngx.req.read_body()
local message = ngx.escape_uri(ngx.var.request_body)
chat.publish_event(ngx.var.channel, nil, nil, message)
';
}
```
* I've loaded the chat module as a thread global (happens once during while loading the config).
* I'm start the a light background thread by calling chat.check_init()
* The /sub/<channel-id> location calls chat.event_source_location() function.
* Likewise, the pub location really just hits chat.publish_event() function.

Now that the special parts of the Nginx config file are out of the way, we'll jump in to [chat.lua](https://github.com/cagerton/dropthat/blob/master/chat.lua). The first thing we need is a way to format the event stream.
```lua
local http_header = "HTTP/1.1 200 OK\r\n"..
"Content-Type: text/event-stream\r\n"..
"Server: dropth.at\r\n"..
"Connection: keep-alive\r\n"..
"Transfer-Encoding: chunked\r\n\r\n"

local function format_http_chunk(message)
-- http chunk format is hex-encoded length, newline, data, newline
local len = string.format("%x\r\n", string.len(message))
return len..message.."\r\n"
end

local function format_event(id, event, message)
local buff = "data:"..message.."\r\n\r\n"
if event then
buff = "event:"..event.."\r\n"..buff
end
if id then
buff = "id:"..id.."\r\n"..buff
end
return format_http_chunk(buff)
end
```
Well, that's pretty simple. Next we'll handle the incoming subscriber connections.

```lua
function event_source_location(channel_id)
local sock, err = ngx.req.socket(true) -- hijack the request socket
sock:send(http_header)

local function cleanup()
remove_socket(channel_id, sock)
ngx.exit(499)
end
local ok, err = ngx.on_abort(cleanup) -- handle disconnect

add_socket(channel_id, sock) -- add socket to data structure
local loops=0
while 1 do
ngx.sleep(19.31)
send_blank(sock) -- periodic tick message :\r\n
end
end

local function add_socket(channel_id, socket)
local channel = get_or_create_channel(channel_id)
channel.sockets[socket] = ngx.now()
update_channel(channel_id)
end

local function update_channel(channel_id)
local channel = channels[channel_id]
if not channel.announce_queued then
push_update_queue(channel_id)
channel.announce_queued = true
end
end
```
For reference, [ngx.sleep](https://github.com/chaoslawful/lua-nginx-module#ngxsleep) is nonblocking. In this case, the lua "light thread" handling this request wakes up every 19ish seconds just send a simple no-op message to keep the connection alive.

When a web client posts a message, our Nginx config hands off to here:
```lua
function publish_event(channel_id, event_id, event, message)
local channel = channels[channel_id]
if channel then
local chunk = format_event(event_id, event, message) --chunk & event-stream format
for sock,start_time in pairs(channel.sockets) do
local bytes, err = sock:send(chunk)
if bytes ~= string.len(chunk) then
-- handle error here.
ngx.log(ngx.ERR, "failed to write event", err)
end
end
end
end
```
The server sends count events when the number of subscibers for a channel changes. I found out the hard way that these need to be rate limited while load testing with 20k clients on a virtual machine. Here's how the notification thread works:
```lua
function check_init()
ngx.timer.at(0, notify_thread) -- ngx.timer.at creates a new light-thread
check_init = function() end -- we only need that to happen once
end

local function notify_thread(premature)
local loops = 0
while true do
local channel_id = pop_update_queue() -- just has channel_ids
if channel_id then
local now = ngx.now();
local channel = channels[channel_id]
if channel and now < 0.373 + channel.last_announce then
ngx.sleep(0.373) -- oversleeps a tiny bit,
-- but queue order means it's ok.
end
channel = channels[channel_id]
if channel then
channel.announce_queued = false
publish_channel_count(channel_id) -- just counts clients & sends event
end
else
ngx.sleep(.127)
end
end
end
```
Pretty cool, huh? Try it out. Here's a [Dockerfile](https://github.com/cagerton/dropthat/blob/master/Dockerfile).

I test this on a local VM.
```bash
$ curl localhost:8080/sub/0000000000000000000000
event:count
data:count=1

event:count
data:count=4663

event:count
data:count=15001

event:count
data:count=13977

event:count
data:count=13465

event:count
data:count=1

```
the other tab:
```bash
cda@ubuntu:~$ sudo bash -c 'ulimit -n 16384 && ab -n 15000 -c 15000 localhost:8080/sub/0000000000000000000000'
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
^C

Server Software: dropth.at
Server Hostname: localhost
Server Port: 8080

Document Path: /sub/0000000000000000000000
Document Length: 0 bytes

Concurrency Level: 15000
Time taken for tests: 6.447 seconds
Complete requests: 0
Failed requests: 0
Write errors: 0
Total transferred: 2607156 bytes
HTML transferred: 762156 bytes
cda@ubuntu:~$
```

On Javascript Cryptography
==========================

### Preface

Javascript Cryptography isn't [inherently doomed (Matasano)](http://www.matasano.com/articles/javascript-cryptography/), it's just useful for different types of problems. (JS Crypto can be an important part of this complete breakfast!)

### Intro

My usage of encryption on the DropTh.at demo is very simple. The interesting crypto is handled by [SJCL](http://bitwiseshiftleft.github.io/sjcl/), the keys are stored in the fragment-identifier of the url (that part that doesn't get sent with the request; handle that url like a password), and the room channel ids are derived from a one way SHA hash of the room keys. Messages and images are encrypted using the same default settings in SJCL. It handles building random IVs, ensuring availability of sufficent entropy and encrypting with AES-CCM mode. Remember that none of this is secure unless you can safely get the javascript and html (over SSL) in the first place. There's lots of room for improvement here.

Here are some of the things that JS crypto can add to traditional HTTPS.

### Client Crypto is an implicit contract:

When web services let clients encrypt their data without giving up their keys, it gives the clients an expectation of pricavy and forms an implicit contract like this:
* We won't casually read your stuff on our severs.
* We won't target ads based on your content.
* We won't leak information by [de-duplicating](http://paranoia.dubfire.net/2011/04/how-dropbox-sacrifices-user-privacy-for.html) your data.
* You won't delete your private media because of a bogus DMCA takedown on another user with the same files.
* Our backups will never contain your plaintext.
* Our cross-data-center links will never expose your unencrypted data.
* We won't report your private data [to the police](http://sacramento.cbslocal.com/2013/11/21/googles-role-in-woodland-child-pornography-arrest-raises-privacy-concerns/)(Okay this time, but how about leaked NSA docs?)

Normal cloud companies could get away with any of those things, but a client-encrypted service would lose all credibility over the first violation.

### As a legal protection for the host from the client data:

Lets take [Mega](https://mega.co.nz/) as an example (I haven't verified this completely): they use client encryption and never see media files that are uploaded to them. Since they're insulated from the data, it'll be hard to argue that they are complicity in copyright violations.

### Untrusted Public CDNs:

We like Public CDNs (bootstrapcdn, cdnjs, googlecdn, etc), but we shouldn't have to trust them. I'm sure they've all got great security, but an attack on a big public cdn could have wide reaching implications. You can use Javascript to verify assets (both shared and private) before using/running them. Proof of concept: https://dropth.at/cors-cdn-demo

### Password Hashing:

You all use BCrypt/SCrypt/PBKDF2, right? These take up lots of CPU time by design and can be targeted for a DoS attack on your server. Rate limiting can help, but it's not a silver bullet - especially if you're chewing through 100ms+ of cpu time for each attempt. They also make RasPi servers cry. With the careful application of client-side js crypto, you can offload some of the expensive work from your server to the client during heavy load (or all the time). I'd reccomend wrapping the result again with a SHA and random salt on the server. [Previously](https://gist.github.com/cagerton/5485241#file-1crazy-md)

PBKDF2 Sha256 with 25000 iterations:
```javascript
var b64 = sjcl.codec.base64, sha2 = sjcl.hash.sha256, kdf = sjcl.misc.pbkdf2;
function preHash(site, username, password){
var salt = b64.fromBits(sha2.hash(site + username)).slice(0,10);
return = b64.fromBits(kdf(password, salt, 25000));
}
var username='cda', password='password', start = Date.now(),
output = preHash('dropth.at', username, password);
console.log("Hashing took approx ",Date.now()-start, "ms");
```
> Chrome: 168ms, Firefox: 150ms, Safari: 1260ms, Chrome on my Galaxy Nexus: 1343ms, Android browser: 1628ms.

> django.utils.crypto.pbkdf2: 105ms on my laptop.

TODO
============
* iPhone canvas problem? Meh. Don't have one to test with; works on iPad.
* Needs responsive layout. Sucks on a small screen.
* Add disclaimer about old and/or shitty browsers.
* Some misc fixups. Maybe CSRF (less important since rooms are secret) + asset domains?

### Some ways you'll get boned:
* Someone will try to run this without https and you'll get boned.
* Someone will intercept the key when you send the room link...
* You'll share the link#key with the wrong person...
* Someone will dig through your history and get your key...
* Someone will pwn my server or find an xss hold and inject js to steal the key....
* Someone will get a "trusted" CA to issue a cert then mitm you. Then you'll get boned.
* http://xkcd.com/538/ (Could be me + a court order to inject js to steal keys).