nadachat
Version:
simple secure chat with end to end encyption
530 lines (457 loc) • 21.5 kB
JavaScript
(function(STAMP){ "use strict"; // nadachat js application file [CCBY4]
/* table of contents:
LINE CODE ROLE
10 {app}
200 {api}
215 dom event handlers
300 poll()er for new messages
360 utility functions
460 gather entropy
480 build worker and start booting
*/
var app = { // properties and methods used by the app
// properties:
room: (!window.name && location.hash.slice(1) )|| get64RandomChars().slice(-32), // conversation ID used by server API to dispatch
isAlice: !(!window.name && location.hash.slice(1) ), // is this device the one starting the conversation?
workerURL: "", // used to hold the blobURL generated from fetching the worker code with ajax
pollPeriod: 300, // # of ms to wait before re-connecting an http long-poll
messageIDs: {}, // a look up table of used message ID
readyState: 0, // application lifecycle stage (0-8)
counter: 0, // how many messages have been recieved?
// methods:
BOOT: function() {
location.replace("#");
window.name="";
$(document.body).removeClass("loading");
if(this.isAlice) { // gen ecc curve, ajax to server, update hash and on-screen info from splash to waiting...
app.BOOT_ALICE();
} else { // alice or bob? bob:
app.BOOT_BOB();
} // end if alice/bob?
// update invite URL link and textbox:
$("#pageurl").val(location + "" + app.room);
$("#pageurlLink").prop("href", location + "" + app.room);
// update connection count-down timer:
var dt=Date.now(),
timer = setInterval(function updateTime(){
var est = (Date.now()-dt),
ms=3600000 - est,
et=new Date(ms).toISOString().split("T")[1].split(".")[0].slice(3);
if(ms < 5*60000 ) et = et.fontcolor("red");
if(app.readyState>4) return clearInterval(timer);
$("#spnTimeLeft").html(et);
if(ms < 1000 || app.readyState>4){
app.SET_STATE(9);
clearInterval(timer);
}
}, 2000);
},
SET_STATE: function(numState) {
app.readyState = numState;
app.RENDER();
setTimeout(function() {
$("#taMsg").focus();
}, 200);
if(numState == 5){
$("#btnSend").prop("disabled", false);
location.replace("#");
}
if(numState > 5){
$("#taMsg.btnSend").remove();
location.replace("#");
window.name="";
}
$("#main").prop("scrollTop",0);
},
RENDER: function() {
document.body.dataset.mode = app.readyState;
},
DISCONNECT: function(){
app.SET_STATE(8);
},
SEND_MESSAGE: function(e){
if(app.readyState!=5) return;
var val=$("#taMsg").val();
if(!val) return;
if(val.length>1399) return alert("Messages must be under 1400 characters"), $("#taMsg").focus();
if(val.length<100) val = (val + " ".repeat(100)).slice(0, 100);
$("#btnSend").prop("disabled", true);
var messageIndex=app.counter; // copy index for async process safety
getWorker(function(e){
var msg=JSON.parse(e.data.data); // unpack sjcl's default aes string bundle to strip redundancy
api.send({
i: messageIndex,
iv: msg.iv,
ct: msg.ct
}).then(function(){ // sent, clear+focus message entry box to indicate
var j=$("#taMsg").val("");
if(!navigator.userAgent.match(/mobile/i)) j.focus(); // kb is in the way of inbox on mobile, don't refocus send box
});
}, {
type: "aesenc",
key: getMessageKeyByIndex(messageIndex) ,
data: val,
STAMP: STAMP + rndme._stamp() + (crypto.getRandomValues(new Uint32Array(1))[0]*Math.random()).toString().slice(2,-2).replace(/\D/g,"")
},
app.DISCONNECT
);// end getWorker()
},// end SEND_MESSAGE()
ITEM: function(o) { // templates a LI string from a message object, used to render the inbox list:
var bonus = ((app.isAlice && o.user == 0) || (!app.isAlice && o.user == 1)) ? "" : " >> ";
return "<li class='item new msg " + (bonus ? "you" : "them") + " ' id=msg_" + o.tx + "><div class=heading>" +
"<time>" +
new Date(o.date).toLocaleTimeString() +
"</time>" +
"<a href=# class='xbtn btn-xl xbtn-default rem pull-right '> remove </a>" +
"</div><div class=bod>" + marked(bonus + sanitize(o.data.trim())).trim() + "</div>"+
"</li>";
},
BOOT_ALICE: function(){
// add some entropy, we got important work to attend to...
STAMP+= rndme._stamp();
STAMP+= rndme.crypto("int", 150, Number);
// gen and send pubkey
getWorker(function(e) {
var ob = e.data.data;
app.pubkey = ob;
api.publicKey({
pubkey: {
x: ob.pubx,
y: ob.puby
}
}).then(function() {
location.replace(window.name="#"+app.room);
app.SET_STATE(1);
});
}, {
type: "ecc",
STAMP: (
STAMP +
rndme._stamp() +
[].join.call(crypto.getRandomValues(new Int32Array(32)), "").replace(/\W/g,"")+
rndme.crypto("int", 150, Number) +
Math.random().toString().slice(3)+
rndme._stamp()
)
});// end getWorker()
},
BOOT_BOB: function(){
app.SET_STATE(3)
api.ask().then(function(e) {
try{ // in case the JSON is malformed, we assume it compromised, since that should never happen, bail
e=JSON.parse(e);
}catch(y){
return app.SET_STATE(7);
}
// stop if there's any problem with the response or key
if(!e || !e.data || !e.data.pubkey) return app.SET_STATE(7);
// set pubkey with response:
app.pubkey = e.data.pubkey;
// now send aes key and iv to server/alice
var aes = {
iv: get64RandomChars(),
key: get64RandomChars(),
nonce: String(Array(24)).split("").map(get64RandomChars).join("")
};
app.aes = aes;
app.nonce=expandNonce(app.aes.nonce);
getWorker(function(e) {
// replace with one that's using a worker to ecc the payload:
api.privateKey(e.data).then(function() {
app.SET_STATE(4);
});
}, {
type: "encode",
pub: app.pubkey,
data: aes,
STAMP: STAMP
});
});
},
PRIVATE_KEY_INCOMING: function(lines){
var aes;
lines.filter(function(obj){
return obj.cmd==="privateKey";
}).forEach(function(obj){ // if alice found bobs private key:
aes=obj.data;
});
if(!aes) return; // didn't find a pubkey-protected secret
app.SET_STATE(4);
getWorker(function(evt){
app.aes=JSON.parse(evt.data.data);
app.nonce=expandNonce(app.aes.nonce);
delete app.aes.nonce; // delete orig from bob
api.begin().then(app.SET_STATE.bind(app, 5));
}, {
type: "decode",
ob: app.pubkey,
data: aes.data
});
}
}, // end app definition
api = {};
////////////////////////////////////////////////
// build server interaction API's methods with pre-filled values:
["publicKey", "privateKey", "ask", "send", "fetch", "leave", "begin"].forEach(function(method){
api[method] = function(e){
return $.post( "/api/", {
cmd: method,
room: app.room,
tx: get64RandomChars().slice(-12), // a session-unique message id, used to detect repeated messages, otherwise meaningless
user: +app.isAlice, // 1 or 0
data: e
}); // end post()
};// end apu method
}); // end forEach() of API method names
///////////////////////////////////////////////
// bind ui controls:
$("#btnSend").click(app.SEND_MESSAGE);
// key bindings for compose box:
$("#taMsg").keydown(function(e){
if(e.keyCode==27) return e.target.value=""; // clear on [esc]
if( {10:1, 13:1}[e.keyCode] && !(e.shiftKey||e.ctrlKey) ){ // send message right away if not inserting line and pressing [return] or [enter]
$("#btnSend").click();
e.preventDefault();
return false;
}// end if enter key
});// end keydown()
// cover tracks when clicking the 'exit' link (top-right)
$("#btnLeave").click(function(){
api.leave().then(function(){
location.replace("#"); // stop back button leakage by killing room id in url hash
try{window.close();}catch(y){} // some browsers allow this, which is cool
location.replace("about:blank"); // doesn't make another meaningful history entry to clear
});
});
// select the invite url when clicked:
$("#pageurl").focus(function(){this.select();});
// list item remove capability:
$("#ulList").on("click", ".rem", function(e){
$(e.target.parentNode.parentNode).remove();
e.preventDefault();
return false;
});
// reload buttons on conversation end and error panels:
$(".reload").click(function(e){
location.reload();
});
// focus the send message box when focusing the tab/window:
$(window).on('focus', function() {
setTimeout(function(){$("#taMsg").focus();}, 50);
});
// keep the invite link from accidental clicks by user:
$("#pageurlLink").click(function(e){
e.preventDefault();
e.target.blur();
return false;
}) ;
// toggle about section copy
$("#info").click(function(e){
var div=$("#info");
div.toggleClass("shown");
$(".panel-body", div)[ div.hasClass("shown") ? "fadeIn" : "fadeOut" ]("slow");
});
// toggle the sound feature on and off each click
$("#lnkBeep").click(function(e){
var old=!app.beep;
$("#spnBeep").html( old ? '<span class="glyphicon glyphicon-volume-up" aria-hidden="true"></span>' : '<span class="glyphicon glyphicon-volume-off" aria-hidden="true"></span>');
app.beep=old;
if(app.beep) beeper.play(); // preview a beep sound to adjust vol
e.preventDefault();
});
// window events:
window.addEventListener("offline", app.DISCONNECT, false);
window.onunload=function(){ // send a message that user left when they leave
var fd=new FormData();
fd.append("cmd", "leave");
fd.append("room", app.room);
fd.append("user", +!app.isAlice);
navigator.sendBeacon("/api/", fd);
};
/////////////////////////////////
// fetches messages in the background using long-polling:
(function poll(){
if(app.readyState>5) return;
if(![0,1,0,1,1,1][app.readyState]) return setTimeout(poll, app.readyState===5 ? app.pollPeriod : (app.pollPeriod*3) );
api.fetch(poll.lastDate).then(function(response, status, xhr){
poll.lastDate = xhr.getResponseHeader("Date");
app.pollTimeout=setTimeout(poll, app.pollPeriod);
var respLines=response.trim().split("\n"),
xdate=respLines[0].trim(); // try to suss out a datastamp from the first (padding) line
if(xdate && +new Date(xdate)) poll.lastDate = xdate;
if(response.trim().length < 50 && response.indexOf("#LEFT#")!==-1) return app.SET_STATE( app.readyState==5 ? 6 : 9); // other party left
// turn each line into a js object by parsing as json
var lines=respLines.slice(1).map(function(line){
return line && line[0]==="{" && JSON.parse(line);
}).filter(Boolean);
if(!lines.length) return; // nothing to do
document.body.dataset.empty= app.counter===0; // set body flag if no messages
// if alice is waiting for a bob response instead of a new message:
if(app.readyState===1) return app.PRIVATE_KEY_INCOMING(lines);
// if bob is responding, watch out for begin messages:
if(lines[0] && lines[0].cmd=="begin" && app.readyState!=5) return app.SET_STATE(5);
// append any new messages the view:
lines.filter(function(line){
return line.cmd==="send";
}).forEach(function(line){
if(app.messageIDs[line.tx]) return;
app.messageIDs[line.tx]=1;
app.counter++;
if(app.counter===1) document.body.dataset.empty='false';
getWorker(function(e){
line.data=e.data.data.trim();
if(app.beep && !document.hasFocus()) beeper.play(); // if beep option enabled and tab no focused, beeps
// add message to list
$("#ulList").append(app.ITEM(line).trim().replace(/<a /g, "<a target=_blank "));
setTimeout(function(){
var elm=$("#msg_"+line.tx).removeClass("new");
}, 150);
$("#ulList li:last-child")[0].scrollIntoView(true);
// use this opportunity to add some timing to add a digit to STAMP
STAMP+=Math.floor(performance.now()*100).toString().slice(-1);
// enable the locked-out send button: (de-bounced)
clearTimeout(app.msgtimeout);
app.msgtimeout=setTimeout(function(){ $("#btnSend").prop("disabled", false);}, 200);
}, {
type:"aesdec",
key: getMessageKeyByIndex(line.data.i) ,
data: sjaes(line.data.iv, line.data.ct)
});
}); // end line forEach()
});// end api.fetch()
}());
///////////////////////////////////////////////
// utilities:
// generate worker, bind handlers, and post initial message:
function getWorker(fnResponder, objMessage, fnError){
var w=new Worker(app.workerURL);
w.onerror=function(e){
console.error(e);
if(fnError) fnError.call(w, e);
w.terminate();
};
w.onmessage=function(evt){
w.terminate();
fnResponder.call(w, evt);
};
w.postMessage(objMessage);
return w;
}
function expandNonce(nonce){ // stretches bits of nonce to get more otp data
return String(nonce).match(/.{12}/g).map(sha3_256).join("")
.match(/.{12}/g).map(sha3_256).join("").match(/../g).map(function(a){
return String.fromCharCode(parseInt( ("00"+a).slice(-2), 16 ));
}).join("");
}
var getMessageKeyByIndex =(function(){
var keys=[]; // make list of keys private
return function getKey(index){
index= +index || 0;
if(index>=keys.length){ // if no cached key, mint ones between last and needed:
for(var i=keys.length, mx=index+1;i<mx;i++){ // from the highest known key index to the needed index
var lastKey = keys.slice(-1)[0] || sha3_256(app.aes.iv + app.aes.key) ; // use last known or a new key to
keys.push( sha3_256( index + app.nonce.slice(index*8, (index*8)+8)+ lastKey ) ); // derive a key from the last, the index, and some otp data
} // next key
} // end if missing key?
return keys[index];
}
}()); // end getMessageKeyByIndex()
function sjaes(iv, ct){ // turns two pieces of an aes package into an sjcl-shaped packed string, for unpacking incoming messages into a format understood by sjcl
return JSON.stringify({
cipher: "aes",
mode: "gcm",
ks: 256,
iv: iv,
v: 1,
iter: 1000,
ts: 128,
adata: "",
ct: ct
});
}
function sanitize(ht){ // tested in ff, ch, ie9+, turns html into entities
return new Option(ht).innerHTML;
}
function get64RandomChars(){ // generates 64 very random base64 chars using accumulated entropy and fresh un-predictables
return sha3_512(
Math.random() + // a quick pad/iv
STAMP + // accumulated entropy
rndme._stamp() + // 10 digit based on time
rndme.crypto("base92", 124, Boolean).slice(124) // 124 based on CSPRNG+timer
).match(/\w{2}/g)
.map(function(a){
return "wp7aY_xzcFLfmoyQqu51KWsZEvOb9XJSP3tin06dR-ClUkGeMVIjgr2NDH4hBT8A"[Math.floor((parseInt(a, 16)/255)*64)]; // convert hex pairs to b64 chars
})
.filter(String)
.join("")
.slice(-64);
}
function stamp(){ // collects un-guessable information
var doms=[];
if(typeof document==="object"){
var s=URL.createObjectURL(new Blob([]));
URL.revokeObjectURL(s);
doms=[
parseInt(s.split("/").pop().replace(/\-/g,""),16).toString().replace(/\D/g,"").slice(1)/(setTimeout(Boolean,0)||3),
JSON.stringify(performance.timing).match(/\d,/g).join("").replace(/\D/g,"")*1,
((innerWidth*innerHeight)).toString().replace(/\D/g,"").slice(1)*1,
new Date(document.lastModified).getTime()/(setTimeout(Boolean,0)||3),
Object.keys(this||Math).length,
document.head.textContent.length,
document.referrer.length,
document.body.scrollHeight,
document.body.scrollWidth
];
}
return doms.concat([
Math.random(),
Math.floor(performance.now()*1000),
(Date.now()-147298194451)/(setTimeout(Boolean)+2),
crypto.getRandomValues(new Uint32Array(1))[0]
]).sort().join("").replace(/\D/g,"").replace(/^0+|0+$/g,"");
}
// a beep .wav file hard-coded into a url to avoid network activity just to beep on incoming messages if desired:
var beepStart=performance.now(); // used to capture media loading timings, which are un-predictable
var beeper=new Audio();
beeper.onload=beeper.oncanplay=function(){ STAMP+= Math.floor(( performance.now()-beepStart)*100); }; // add usable parts of load timing to entropy
beeper.src="data:audio/wav;base64,UklGRm4IAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgATElTVCwAAABJTkZPSUFSVAoAAABkYW5kYXZpcwAASUNNVA4AAABwdWJsaWMgZG9tYWluAGRhdGEWCAAAgICAgICAgICAgH+Af4B/gICAgIB/gH+Af4CAgICAgH+Af4B/gIB/gIB/gH+Af4B/gIB/gH+AgH+Af4B/gIB/gIB/gICAgIB/gH+AgICAgH+Af4B/gIB/gH+AgH+Af4CAf4B/gICAf4B/gH+Af4CAf4B/gH+Af4B/gH+AgH+Af4CAf4B/gICAf4B/gH+AgH+AgH+Af4B/gH+Af3+Af4CAgH9/f4CAf4CAgXKWgnKEgXWjZXaiaYmCeYiCZqNvaqJvgIV1iYlbo4Bdm3x+gHeFlFSXkVuTgH2Ed3qgWYWbYYuCfId4cKdheJ9oh4N5iIBmq2troHCBgnmIh12kfGKbd4CBeISTV5qJX5Z8gIN2fp1Xj5Vgj32AhXZ3plt/nWWJf36He2uoaHCfbYV+fImCYKd2Zp10g394iI1Zn4Vgl3mCgXaCmVeSkmCQfICEdnijWYObZIl/f4d6bahkdZ9qhn98iIFiqHBpn3KCgHiIi1mjg2GZeICDiIJ1eIGDjHl5cXWLi4+Kd2xwdoqUjY1xcnB6iYuLgnZ4e4CLgH92d4WHjI52bnNyipGSj3Btb3iHkY+LcXN5eoiFhX52f4eDh312cXeIj5GLdG1reImQlYp0cHB7hoqMhHd3fYCGgX52d4SLjYl3cG94iZCUiXVtbXmJjpCIc3N2fYiDhX12foaGiXp2dHaGj5CKdmtteIiQkYt0cHF8h4mLgXd6foGIgHx3eISKjYp3b294iY+UinRtbnqHjo+GdXR3f4eEg3aFapGBdXqZbn6Sg2eMhnB9jXV5mXx6h4NvgX58eJ13gYKCa4xwhX6WbomEe22UaoKFjXCOgXxwjnCCh4hwi4V4eYl4fIh8d4aOdIOHenWJcn+Ck3KLf31wjGuHgJFxkHx+co1qiYKKco98fXaLcIaEgHWLgnl/iHaAhnZ8hol3iH97eohthYCMdJB7f3eKaIt/jXKTeX51jGmMgYh1j3t9e4hviIR7eYx/eoODdYKGcoGFh3aNfXt7iWmIgYx0knh9dotmjYCNZZV2fZpuYr1qXah+cXaVhml3sl1rsGB9knB/ll+UhmqNdY19cI2UT52PVJqCdYh+dZpihJRmkXiAjXJvrVtzqGl9hoOCeW+nZHKjZoWIc4aNXJ9/ZZZ3hXx3i5BUoIlZmH19g3l8m1mNlGGPeYCIdXOoXHyfZoV/gIZ7a6hmcaJrg4J7hoVho3donXKCgHiGj1ifh2CWeoCBd4GZV5KUX5B9f4R4eKNbg51kiX6Ah3lsqGRzomuDgH6Hf2KocWmgcIGBeYeMWKCEYJd/c3Bzh5aJiHN2coCJhIV3d4OEgZB5b3Z0iYyTi3RubXaJko+JdnFuf4eIjIByfH9/jX97cXeHiI6PdmxwdIiQk49yb213iY2MinV0en+HgoR4doOHhol9c254iI+SjHZta3aIkJCLdnBwfYeIiIB2e4GCh4B6cniFjI+Ld25rd4mQlYp0cGx5iY2MhnV3eH6JgoF5d4GIiIp6c3F2iI+Si3ZsbHiIkJCJdHJyfYiIh352fYGCiX55cniGjI+Ld25teImQkot1bpJ3hISFXIp/enijcX2Gi12Nfn95mnCChoJsi396f4x0fY18d4p9eIZ7d4KUcIaHeHKRbYKBkm+KgX9vkm2DgpFtjX9+bY9whIOMb4uBe3eLdoGFfnaHiHiBhnh8h3J/g412i398dotrhoGPcpB9fnKMaYmCjXKRen12i2uKg4N0jXx8fIdyh4N5fIeDeIeBeYGHbYODiXSPfHx5imeJf4xzk3p+dotmjYGJdJJ4fniJbIyCgHiNe3yAhXKHhXZ9iIN4iIJ5fohthZtSj5BsfZB3i3CHhmagcm+gfVe1clumfHSAh4V3a6htZatrfIt0gJhUlJVcjoOCeniGlVSWlVeVhHWIgHCeZX2WaY96fY54ZrBlaqZveoSCg35ppm9pom2BhnaCk1iai2CUe4J/doOZVJaUW5J/e4R6dqJfgptkinx/h3psq2ZxoGyCfoCIf2SocmagcoB/fIaKW6CDYJl4gYB3gZZXlZJdkX5/gnd7oFiIm2CLf36Ge2+nYnigaIZ/fYh/YqtuaqBwgYF7h4dcpHxriImLjntqdHKKkpSId3BtfYeLi4hyeXp9i4CCdneChoiQeW9yd4WNl4t0bG11iJKPinR0cn6IiId9dH2EgYmAenB3iYmPj3ZtbnaIkJOLdXBteomLi4V3dnmCh4GBd3eCiYmLenFteIeQk4t2bWx3iZCPiHZycn2Hh4Z9dn2Chol8eXF3houQi3ZubHeJj5OLdHFveoiLi4R1ent/iIJ+d3eDiIqLeXFueIiPlIp2bW14iI+Qh3R1dH2IhoV8dn+Dhol8eHB4hod5fZF7cI+HZoKFeHGgeneIimKId314nXaEgodki3aBe5hyiYB+bo1yg4iLbYiGdHqPdIGHenaFjHWBiHh1jXJ+gZJxh4R9cI9tgoKScI1+gG6NbYeAjnCNfX51i3CHg4N0iYB6fol0gYZ3e4aIdoWFenuJbYGCjHSQfX51imiJf410kHiBdIprjICHdJB4fXiJb4qDgHeKf3qChHaEhnN/hYV3in97fohrhoCLdJF6f3eKZo1/i3SSd4B3immNgIV1knh8gHN3mIJthZJpeJlzapmGaYeQbHqOd3WSgHCIiHB/jXd4jIB0hoJ2g4d4hIZ5e4l6eImDdoaDeX2IenqIgXaEhnd8iXt5h4F3g4Z4f4h8eoeAeIKFeX+FfXuFf3uB";
beeper.volume=0.2; // file is somewhat loud, let's not blast anyone out of their chair!
// build up some un-predictable values with what's available in the browser:
var st=performance.now()+2.5; // helps make Math.random() safer and buys time for other number generation methods, just in case (legacy)
while(performance.now()<st) Math.random(); // runs the performance.now clock up for better rndme.time() seeds
// add entropy from dom to that from php:
STAMP+= stamp();
// add some entropy from window.crypto (munged with timing data)
STAMP+= rndme.crypto("int", 300, Boolean).slice(-256);
// add entropy from orientation and acceleration sensors on portable devices and some laptops
rndme.motion("int", 40, function(s){
STAMP+=s;
});
/////////////////////////////////////
//// worker builder and boot starter
setTimeout(function buildWorker(){ // load the worker code into a variable so that we can spawn fresh new workers without network IO:
rndme.time("int", 1000).then(function(s){
STAMP = s + STAMP + rndme.crypto("int", 200, Number);
$.get({ dataType: 'text', url: "/js/sjcl-core.js"}, function(coreCode){
$.get({ dataType: 'text', url: "/js/rsaworker.js"}, function(a,b,c){
// for older firefox and others w/o self.crypto IN WORKERS
// note: polyfilling with Math.random() is safe since other unpredictable and CSPRNG-sourced data is mixed into SJCL's fortuna, which is what really matters
var poly=';var crypto2={getRandomValues: function(r){r.forEach(function(a,b){r[b]=Math.floor(4294967296*Math.random());}); return r;}};'+
'if(typeof crypto==="undefined") crypto=crypto2;';
// add together the accumulated entropy, a sjcl shim for Workers (window=self), the self.crypto polyfill, and the fetched core and worker code and save to a blob url:
app.workerURL=URL.createObjectURL(new Blob(["var STAMP="+STAMP+";var window=self;"+ poly+ coreCode +";\n\n\n"+a],{type:"text/plain"}));
// throw away most of the accumulated entropy to protect the sealed worker thread code from XSS:
STAMP= rndme._stamp() +
sha3_512(STAMP+'').match(/\w{2}/g).map(function(a){return Math.floor((parseInt(a,16)/255)*100);}).join("") +
rndme._stamp() +
rndme.crypto("int", 200, Number);
rndme.motion("int", 40, function(s){ STAMP+=s;});
// crypto worker url ready, boot up the application:
setTimeout(app.BOOT.bind(app), 38);
}); // end worker code fetch;
});// end core fetch
});// end time() cb
}, 120 );
// remove server-passed entropy and privatize pool by clobbering global seed:
window.STAMP = null;
}(STAMP));