Skip to content

Instantly share code, notes, and snippets.

@bellbind
Last active August 29, 2015 14:18
Show Gist options
  • Save bellbind/e6a3d72946e1b1c6daea to your computer and use it in GitHub Desktop.
Save bellbind/e6a3d72946e1b1c6daea to your computer and use it in GitHub Desktop.
[webcrypto][websocket][usermedia]captured photo sending with specified with pubkey
;(function () {
"use strict";
// helpers for ArrayBuffer representation
var s2b = function (str) {
return new Promise(function (fulfill, reject) {
var file = new FileReader();
file.addEventListener("load", function (ev) {
fulfill(ev.target.result);
}, false);
file.addEventListener("error", reject, false);
file.addEventListener("abort", reject, false);
file.readAsArrayBuffer(new Blob([str]));
});
};
var b2s = function (buffer) {
return new Promise(function (fulfill, reject) {
var file = new FileReader();
file.addEventListener("load", function (ev) {
fulfill(ev.target.result);
}, false);
file.addEventListener("error", reject, false);
file.addEventListener("abort", reject, false);
file.readAsText(new Blob([buffer]));
});
};
var b2h = function (buffer, conn) {
conn = conn === undefined ? "" : conn;
return [].map.call(new Uint8Array(buffer), function (b, i) {
var c = b.toString(16);
return c.length === 1 ? "0" + c : c;
}).join(conn);
};
var h2b = function (hex, conn) {
var hexes = conn ? hex.split(conn) : hex.match(/.{1,2}/g);
return new Uint8Array(hexes.map(function (h) {
return parseInt(h, 16);
}));
};
// polyfill ES6 Object.assign()
var assign = function (target) {
for (var i = 1; i < arguments.length; i++) {
var s = arguments[i];
Object.keys(s).forEach(function (k) {
var d = Object.getOwnPropertyDescriptor(s, k);
if (d && d.enumerable) target[k] = s[k];
});
}
return target;
};
// crypto specs
var conf = {};
conf.system = {
name: "ECDH",
namedCurve: "P-256",
};
conf.spec = {
name: conf.system.name,
namedCurve: conf.system.namedCurve,
};
conf.alg = {
name: conf.system.name,
namedCurve: conf.system.namedCurve,
};
conf.secSpec = {
name: "AES-CBC",
length: 256,
};
conf.fmt = "jwk";
conf.digest = {name: "SHA-256"};
var initMe = function () {
return crypto.subtle.generateKey(conf.system, true, [
"deriveKey",
]).then(function (keys) {
return Promise.all([
keys.privateKey,
crypto.subtle.exportKey(conf.fmt, keys.publicKey)]);
}).then(function (keys) {
return Promise.all([
keys[0], keys[1],
s2b(JSON.stringify(keys[1])).then(function (buf) {
return crypto.subtle.digest(conf.digest, buf);
}).then(b2h),
]);
}).then(function (keys) {
return {id: keys[2], priv: keys[0], pub: keys[1]};
});
};
var importKey = function (jwk) {
return crypto.subtle.importKey(conf.fmt, jwk, conf.spec, false, []);
};
var encrypt = function (me, pubkey, text) {
var iv = crypto.getRandomValues(new Uint8Array(16));
return Promise.all([
crypto.subtle.deriveKey(
assign({}, conf.system, {public: pubkey}),
me.priv, conf.secSpec, false, ["encrypt"]),
s2b(text),
]).then(function (km) {
return crypto.subtle.encrypt(
assign({}, conf.secSpec, {iv: iv}), km[0], km[1]);
}).then(function (cipher) {
console.log("byteLength", cipher.byteLength);
var iva = Array.apply(null, new Uint8Array(iv));
//var body = Array.apply(null, new Uint8Array(cipher));
var body = b2h(cipher);
return {key: me.pub, iv: iva, body: body};
});
};
var decrypt = function (me, msg) {
var iv = new Uint8Array(msg.iv);
//var cipher = new Uint8Array(msg.body);
var cipher = h2b(msg.body);
return importKey(msg.key).then(function (pubkey) {
return crypto.subtle.deriveKey(
assign({}, conf.system, {public: pubkey}),
me.priv, conf.secSpec, false, ["decrypt"]);
}).then(function (skey) {
return crypto.subtle.decrypt(
assign({}, conf.secSpec, {iv: iv}),
skey, cipher);
}).then(b2s);
};
window.CryptoSystem = {
initMe: initMe,
importKey: importKey,
encrypt: encrypt,
decrypt: decrypt,
};
})();
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>WebCrypto and WebSocket and getUserMedia</title>
<script src="crypto.js"></script>
<script src="script.js"></script>
<style>
.block {
display: inline-block;
margin: 0px;
padding: 0px;
width: 45%;
vertical-align: top;
}
</style>
</head>
<body>
<div>
[<input id="name" type="text" value="anon" style="width: 100px"
readonly="readonly" />]
send to <select id="targets"></select> (when tap camera)
</div>
<div style="height: 80%;">
<div id="camera" class="block"></div>
<div id="block" class="block" style="overflow: scroll;">
<div id="log"></div>
</div>
</div>
</body>
</html>
{
"name": "webcrypto-camera",
"version": "0.0.1",
"description": "crypted shot send with websocket server (ECDH/AES-CBC)",
"dependencies": {
"faye-websocket": "*",
"mime-types": "*"
},
"scripts": {
"start": "iojs ws-server.js"
},
"engines": {
"iojs": "1.x"
}
}
web: npm start
;window.addEventListener("load", function () {
"use strict";
var getUserMedia = function (conf) {
if (navigator.mediaDevices) {
return navigator.mediaDevices.getUserMedia(conf);
}
if (navigator.webkitGetUserMedia) return new Promise(function (f, r) {
navigator.webkitGetUserMedia(conf, f, r);
});
return Promise.reject();
};
var createObjectURL = function (stream) {
if (typeof URL !== "undefined")
return URL.createObjectURL(stream);
if (typeof webkitURL !== "undefined")
return webkitURL.createObjectURL(stream);
return "";
};
var capture = function (video) {
var canvas = document.createElement("canvas");
//console.log(video.width, video.height);
canvas.width = video.clientWidth;
canvas.height = video.clientHeight;
var c2d = canvas.getContext("2d");
c2d.drawImage(video, 0, 0, canvas.width, canvas.height);
var uri = canvas.toDataURL("image/png");
return uri;
};
getUserMedia({video: true}).then(function (stream) {
var video = document.createElement("video");
document.getElementById("camera").appendChild(video);
// define video screen size beacuse
// browsers not yet support MediaStreamTrack.getConstraints() API
//window.track = stream.getVideoTracks()[0]
video.style.width = "100%";
video.src = createObjectURL(stream);
video.play();
return video;
}).then(function (video) {
CryptoSystem.initMe().then(function openSocket(me) {
var name = document.getElementById("name");
var log = document.getElementById("log");
var targets = document.getElementById("targets");
console.log("clientHeight", video.clientHeight);
var block = document.getElementById("block");
block.style.height = (0| window.innerHeight / 2) + "px";
var pubkeys = [];
while (targets.firstChild) targets.removeChild(targets.firstChild);
name.value = me.id.substring(0, 8);
var wsurl = location.origin.replace(/^http/, "ws") + "/bs/";
var socket = new WebSocket(wsurl);
socket.addEventListener("open", function (ev) {
console.log("[open]");
// send my pubkey
socket.send(JSON.stringify({
t: "hello", id: me.id, key: me.pub}));
video.addEventListener("click", sender, false);
}, false);
socket.addEventListener("message", function (ev) {
var msg = JSON.parse(ev.data);
if (msg.t === "hello") return acceptHello(msg);
if (msg.t === "chat") return acceptChat(msg);
if (msg.t === "bye") return acceptBye(msg);
}, false);
socket.addEventListener("close", function (ev) {
console.log("[close]");
video.removeEventListener("click", sender, false);
// reconnect
requestAnimationFrame(openSocket.bind(null, me));
}, false);
var sender = function (ev) {
var pubkey = pubkeys[targets.selectedIndex];
if (!pubkey) return;
console.log("[send]");
var uri = capture(video);
console.log("uri length", uri.length);
// prepare key and encrypt a message to the specified receiver
CryptoSystem.encrypt(
me, pubkey.key, uri
).then(function (msg) {
msg.t = "chat";
socket.send(JSON.stringify(msg));
var item = document.createElement("div");
var img = document.createElement("img");
img.src = uri;
item.appendChild(img);
log.insertBefore(item, log.firstChild);
}).catch(console.log.bind(console, "<<encrypt>>"));
};
var acceptHello = function (msg) {
CryptoSystem.importKey(msg.key).then(function (pubkey) {
console.log("[import key]", pubkey);
pubkeys.push({id: msg.id, key: pubkey});
var opt = document.createElement("option");
opt.textContent = msg.id;
opt.id = msg.id;
targets.add(opt);
}).catch(console.log.bind(console, "<<import key>>"));
};
var acceptChat = function (msg) {
CryptoSystem.decrypt(me, msg).then(function (uri) {
console.log("[message] " + uri);
var item = document.createElement("div");
var img = document.createElement("img");
img.src = uri;
item.appendChild(img);
log.insertBefore(item, log.firstChild);
}).catch(console.log.bind(console, "<<decrypt>>"));
};
var acceptBye = function (msg) {
// remove receiver list
for (var i = 0; i < pubkeys.length; i++) {
if (pubkeys[i].id == msg.id) {
pubkeys.splice(i, 1);
break;
}
}
targets.removeChild(document.getElementById(msg.id));
};
}).catch(console.log.bind(console, "<<init me>>"));
});
}, false);
// Simple Broadcast WebSocket server
// $ npm install faya-websocket mime-types
// $ iojs ws-server.js
var http = require("http");
var fs = require("fs");
var path = require("path");
// 3rd party libs
var WebSocket = require("faye-websocket"); // W3C API compliant interface
var mime = require("mime-types");
var fileServer = function (req, res) {
console.log("[file server]", req.method, req.url);
if (req.method !== "GET") {
res.writeHead(405, {allow: "GET"});
return res.end();
}
var loc = req.url.endsWith("/") ? req.url + "index.html" : req.url;
var file = path.join(__dirname, loc);
fs.createReadStream(file).once("readable", function () {
console.log("[exist]", file);
var mtype = mime.lookup(loc) || "application/octet-stream";
var charset = mime.charset(mtype);
var ctype = mtype + (charset ? "; charset=" + charset : "");
res.writeHead(200, {"content-type": ctype});
this.pipe(res);
}).once("error", function (er) {
console.log("[not exist]", file);
res.writeHead(404);
res.end();
});
};
// single broadcast hub
var BroadcastServer = function () {
this.sockets = new Map();
};
BroadcastServer.prototype.onUpgrade = function (req, socket, head) {
console.log("[upgrade]", req.method, req.url);
var sockets = this.sockets;
var ws = new WebSocket(req, socket, head);
ws.addEventListener("open", function (ev) {
console.log("[open]", ws.url);
for (var val of sockets.values()) {
//console.log(val);
if (val.hello) ws.send(val.hello);
}
sockets.set(ws, {});
}, false);
ws.addEventListener("message", function (ev) {
console.log("[message]", ev.data);
var msg = JSON.parse(ev.data);
if (msg.t === "hello") {
console.log("[hello]", msg.id);
sockets.set(ws, {id: msg.id, hello: ev.data});
}
for (var socket of sockets.keys()) {
socket.send(ev.data);
}
}, false);
ws.addEventListener("close", function (ev) {
console.log("[close]", ws.url);
var id = sockets.get(ws).id;
sockets.delete(ws);
for (var socket of sockets.keys()) {
socket.send(JSON.stringify({t: "bye", id: id}));
}
console.log("[bye]", id);
}, false);
};
var bs = new BroadcastServer();
var server = http.createServer(fileServer);
server.on("upgrade", bs.onUpgrade.bind(bs));
var port = process.env.PORT || 8080;
server.listen(port);
console.log("open http://localhost:" + port + "/");
@bellbind
Copy link
Author

bellbind commented Apr 2, 2015

@bellbind
Copy link
Author

bellbind commented Apr 3, 2015

Note for updating to DHE style encryption

Current implementation is that the sender use me.priv and chosed pubkey.

Instead of using the me.priv and me.pub, use a newly generated ECDH key pairs in each send event.
It make a cipher body with the temporal privkey, send the cipher body and the temporal pubkey,
then drop the keys after encryption.

DHE can anonymize the sender identity.

  • Alice use the Bob's permanent pubkey for deriveKey
  • Alice make a temporal DH key pair
  • Alice encrypt a text with the permanent pubkey and the temporal privkey
  • Alice send the cipher text and temporal pubkey to Bob
  • Bob decrypt the cipher text with the permanent privkey and the temporal pubkey

DHE+DSA uses both DSA pubkey and DHE pubkey.

Make and send a sign of the temporal pubkey with DSA privkey,
It assures the Sender Identity lost by DHE system.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment