Skip to content

Instantly share code, notes, and snippets.

@bellbind
Last active August 29, 2015 14:17
Show Gist options
  • Save bellbind/63fd853a8d0119df7848 to your computer and use it in GitHub Desktop.
Save bellbind/63fd853a8d0119df7848 to your computer and use it in GitHub Desktop.
[webcrypto][websocket][iojs] RSA crypted message chat
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>WebCrypto and WebSocket</title>
<script src="script.js"></script>
</head>
<body>
<div>
[<input id="name" type="text" value="anon" style="width: 100px"
readonly="readonly" />]
<input id="message" type="text" style="width: 300px" />
<button id="send" disabled="disabled">send to</button>
<select id="targets"></select>
</div>
<div>
<ul id="log" style="overflow: auto; width: 800px; height: 300px"></ul>
</div>
</body>
</html>
{
"name": "webcrypto-chat",
"version": "0.0.1",
"description": "crypted chat with websocket server",
"dependencies": {
"faye-websocket": "*",
"mime-types": "*"
},
"scripts": {
"start": "iojs ws-server.js"
},
"engines": {
"iojs": "1.x"
}
}
web: npm start
;window.addEventListener("load", 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);
};
// crypto specs and key preparaion
var system = {
name: "RSA-OAEP",
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: {name: "SHA-256"}};
var spec = {
name: system.name,
hash: system.hash,
};
var alg = {
name: system.name,
};
var fmt = "jwk";
crypto.subtle.generateKey(system, true, [
"encrypt", "decrypt"
]).then(function (keys) {
return Promise.all(
[keys.privateKey, crypto.subtle.exportKey(fmt, keys.publicKey)]);
}).then(function (keys) {
return Promise.all([
keys[0], keys[1], s2b(JSON.stringify(keys[1])).then(
crypto.subtle.digest.bind(crypto.subtle, {name: "SHA-256"})
).then(function (raw) {
//return btoa(Array.apply(null, new Uint8Array(raw)));
return b2h(raw);
}),
]);
}).then(function (keys) {
openSocket({id: keys[2], privKey: keys[0], pubJwk: keys[1]});
});
var openSocket = function openSocket(opts) {
// ui and state
var name = document.getElementById("name");
var message = document.getElementById("message");
var log = document.getElementById("log");
var send = document.getElementById("send");
var targets = document.getElementById("targets");
var pubkeys = [];
while (targets.firstChild) targets.removeChild(targets.firstChild);
name.value = opts.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.addEventListener("click", sender, false);
// send my pubkey
socket.send(JSON.stringify({
t: "hello", id: opts.id, key: opts.pubJwk
}));
send.disabled = 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]");
send.removeEventListener("click", sender, false);
// reconnect
requestAnimationFrame(openSocket.bind(null, opts));
}, false);
// actions
var sender = function (ev) {
var pubkey = pubkeys[targets.selectedIndex];
if (!pubkey) return;
console.log("[send]");
var msg = "[" + name.value + "] " + message.value;
// encrypt a message to the specified receiver
s2b(msg).then(
crypto.subtle.encrypt.bind(crypto.subtle, alg, pubkey.key)
).then(function (crypted) {
var array = Array.apply(null, new Uint8Array(crypted));
console.log("[crypted]", array);
socket.send(JSON.stringify({t: "chat", chat: array}));
var li = document.createElement("li");
li.textContent = msg +
" (sent to " + pubkey.id.substring(0, 8) + ")";
log.insertBefore(li, log.firstChild);
});
};
var acceptHello = function (msg) {
// store joined receiver key
crypto.subtle.importKey(fmt, msg.key, spec, false, [
"encrypt"
]).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);
});
};
var acceptChat = function (msg) {
// try to decrypt message
crypto.subtle.decrypt(
alg, opts.privKey, new Uint8Array(msg.chat)
).then(b2s).then(function (message) {
console.log("[message] " + message);
var li = document.createElement("li");
li.textContent = message;
log.insertBefore(li, log.firstChild);
}, function (err) {
// its not for me
console.log("[decrypt]", err);
});
};
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));
};
};
}, 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

Check your browser&platform interoperability

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