nadachat
Version:
simple secure chat with end to end encyption
642 lines (453 loc) • 15.3 kB
JavaScript
// rndme.js - unpredictable number generation from sound, sight, movement, and time
(function(a,b){"function"==typeof define&&define.amd?define([],a):"object"==typeof exports?module.exports=a():b.rndme=a()}(function(){
/* example uses:
// source ( format size callback progess )
rndme.sound("bytes", 12345, function(s){alert(s)});
rndme.motion("hex", 1024,function(s){alert(s)}, console.info.bind(console));
rndme.time("float", 256,function(s){alert(s)}, console.info.bind(console));
rndme.video("base92", 1024).then(alert).catch(confirm);
*/
var rndme=Object.create(null);
// video - capture unpredictable data from user camera
rndme.video=getRandomFromVideo;
function getRandomFromVideo(format, chars, callback, progress, err) { // returns a 64 char base64url string
"use strict";
chars=Math.floor(chars * 0.55);
var canvas = document.createElement("canvas"),
ctx = canvas.getContext("2d"),
webkit = false,
moz = false,
v = null,
W = 320,
H = 240,
n = navigator,
dataBuffer=[],
v = document.createElement("video");
if(n.getUserMedia) {
n.getUserMedia({
video: {
width: W,
height: H,
framerate: 60,
facingMode: "environment",
}
}, success, err);
} else if(n.webkitGetUserMedia) {
webkit = true;
n.webkitGetUserMedia({
video: {
facingMode: "environment",
width: W,
height: H,
framerate: 60,
optional: []
}
}, success, err);
} else if(n.mozGetUserMedia) {
moz = true;
n.mozGetUserMedia({
video: {
facingMode: "environment",
width: W,
height: H,
framerate: 60,
},
audio: false
}, success, err);
}
function dumpCanvas() {
var data = ctx.getImageData(0, 0, W, H).data,
pSum=data.slice(0, 512).reduce(function(a,b){return a+b;}, 10),
sigLength = Math.floor(Math.max(64,Math.min(2048, (pSum-32768)/9))),
sig = crypto.getRandomValues(new Uint32Array(sigLength)),
r = [],
taken = 0;
if( pSum < 100 ) return setTimeout(updateCanvas, 20);
for(var i = 1, mx = data.length; i < mx; i++) {
var v = data[i];
if( v < 255) {
sig[(sigLength-1)-(taken++ % sigLength)]+=v;
}
}
//keygen and dump:
sig = [].slice.call(sig).map(function(a) {
return ("00"+a).slice(-2);
//parseInt("0" + a.toString(2).slice(-10), 2);
}).join("");
if(/^0+$/.test(sig)) return setTimeout(updateCanvas, 20);
dataBuffer.push(sig);
var used=(""+dataBuffer).length*0.53;
if(used > chars){
var collect=[];
formatData(dataBuffer.join(""), format, collect);
updateCanvas.stop();
if(callback) callback(collect.join(""));
}else{
if(progress) progress({value: used, max: chars});
setTimeout(updateCanvas, 0);
}
}
function updateCanvas() {
ctx.drawImage(v, 0, 0, W, H);
setTimeout(dumpCanvas, 25);
}
function success(stream) {
updateCanvas.stop = function() {
stream.getTracks()[0].stop();
};
if(webkit) v.src = window.webkitURL.createObjectURL(stream);
else if(moz) {
v.mozSrcObject = stream;
v.play();
} else v.src = stream;
setTimeout(updateCanvas, 9);
}
} //end getRandomFromVideo()
// time - capture unpredictable data from user device performance
rndme.time = getRandomFromTime;
function getRandomFromTime(format, chars, callback, progress) {
var ua = new Uint32Array(1),
counts=Math.ceil(chars/2),
ds=(Date.now()+performance.now()).toString().replace(/\D/g,""),
dsl=ds.length,
out =random(counts),// Array.from(Array(counts)).map(function(a,b){return rndme._stamp().slice(b%10, (b%10)+1)*b}),
//.map((a,b)=>b*ds[b%dsl]),//random(counts),
rxd = /[523403467]/,
limit = 0,
round = 0,
roundLimit = chars * 0.333, //0.7,
seeds = random(roundLimit + 3),
loads = random(roundLimit + 3),
r = [],
t = (Date.now().toString().slice(-3).slice(1)*1),
st = performance.now();
function random(n) {return [].slice.call(crypto.getRandomValues(new Uint32Array(n)));}
function work() {return Math.random().toString(16).split("").filter(rxd.test, rxd).length;}
function snap() {return +String(("" + performance.now()).match(/\.\d\d/) || "0").replace(/\D/g, "").slice(-2).split("").reverse().join("") || 0;}
while(performance.now() < st + 1) t += work(limit++) ;
limit = Math.max(limit*1.75, 2);
function next(){
round++;
for(var i = ((loads[round] / 4294967296) * (limit / 9)) + 1; i > 0; i--) t = work();
var slot = counts-(round % counts)//Math.floor((seeds[round] / 4294967296) * counts)// round % counts; //;
out[slot] = +out[slot] + +t + snap();
if(progress) progress({value: round, max: roundLimit});
if(round < roundLimit) return setTimeout(next,0);
var collect=[];
formatData(out.map(function(a, b, c) {
return ("000" + a).slice(-2);
}).filter(String).join("").replace(/\D+/g,""), format, collect);
callback(collect.slice(-chars).join(""));
}//end next()
next();
} //end time()
// sound - capture unpredictable data from user microphone
rndme.sound=sound;
function sound(mode, length, callback, progress, err) {
"use strict";
var n = navigator,
format = mode,
limit = length,
count = 0,
ALLS = [],
makeRandom = sound,
isRecording = false;
var AudioContext = window.AudioContext || window.webkitAudioContext;
var audioContext = makeRandom.ac || (makeRandom.ac = new AudioContext()),
audioInput = null,
inputPoint = null,
buffers = [],
TIMER = 0,
xstream,
gum = n.getUserMedia
if(!gum) gum = (n.mediaDevices && n.mediaDevices.getUserMedia && n.mediaDevices.getUserMedia.bind(n.mediaDevices)) || n.webkitGetUserMedia || n.mozGetUserMedia;
if(!gum) return err("sound source needs getUserMedia()");
function makeIt() {
var resp = gum.call(n, {
"audio": {
volume: 0.7,
channelCount: 1,
echoCancellation: false,
"mandatory": {
"googEchoCancellation": "false",
"googAutoGainControl": "false",
"googNoiseSuppression": "false",
"googHighpassFilter": "false",
},
"optional": []
},
}, gotStream, err)
if(resp && resp.then) resp.then(gotStream);
} //end makeIt()
function gotStream(stream) {
xstream = stream;
inputPoint = audioContext.createGain();
audioInput = audioContext.createMediaStreamSource(stream);
audioInput.connect(inputPoint);
gotStream.ip = inputPoint;
gotStream.ai = audioInput;
var node = (inputPoint.context.createScriptProcessor || inputPoint.context.createJavaScriptNode).call(inputPoint.context, 2048, 1, 1);
audioInput.connect(node);
node.connect(audioInput.context.destination); //this should not be necessary
node.onaudioprocess = function(e) {
if(isRecording) buffers.push(e.inputBuffer.getChannelData(0));
};
gotStream.node = node;
var zg = gotStream.zg || (gotStream.zg = audioContext.createGain());
zg.gain.value = 0.0;
inputPoint.connect(zg);
zg.connect(audioContext.destination);
function rec() {
isRecording = true;
setTimeout(function() {
getMyBuffers(buffers);
isRecording = false;
}, 100 );
}
TIMER = setInterval(rec, 160 );
setTimeout(rec,0);
}
function getMyBuffers(buffers) {
var rxd = /\D/g,
rxd3 = /\d{3}/g,
x=[],
s="";
buffers.some(function(r, ind) {
if(progress) progress({
value: count,
max: limit
});
if(!r || !r.map) return err("getMyBuffers passed non buffer");
for(var i = 0, mx = r.length; i < mx; i++) {
var a = r[i],
u = ("" + a).replace(rxd, "").slice(-11, - 3);
if(+u) x.push(u);
}
s = x.join("");
count+=formatData(s, format, ALLS);
if(count > limit) {
if(format === "raw" && buffers[ind + 1]) return false;
if(format === "raw") limit = 9e9;
clearInterval(TIMER);
setTimeout(function() {
xstream.getTracks()[0].stop();
}, 50);
buffers.length = 0;
isRecording = false;
if(progress)progress({
value: count,
max:limit
});
gotStream.ai.disconnect();
gotStream.ip.disconnect();
callback(ALLS.join("").slice(0, limit));
ALLS.length = 0;
return true;
}
}); //end some();
}
makeIt();
} //end sound();
// motion - capture unpredictable data from user movement and sensor noise
rndme.motion = getRandomMotion;
function getRandomMotion(format, chars, callback, progress, err) { // returns a long string of digits to callback
if(!window.ondevicemotion && !window.ondeviceorientation) err("motion source needs device motion API");
chars = +chars || 1024;
var samples = {},
rounds=0,
pad=random(~~(chars/19)).map(function(a){return 1/a;}),
lastPos = [0,0,0];
function random(n) {return [].slice.call(crypto.getRandomValues(new Uint16Array(n)));}
function accelChange(e) {
var acc = e.accelerationIncludingGravity || "",
off=pad[rounds] || 0,
buff,
pos = [acc.x || e.alpha||0, acc.y||e.beta||0, acc.z||e.gamma||0],
dif = [pos[0] - lastPos[0]-off, pos[1] - lastPos[1]-off, pos[2] - lastPos[2]-off];
samples[dif[0].toString().slice(-9,-1)]=1;
samples[dif[1].toString().slice(-9,-1)]=1;
samples[dif[2].toString().slice(-9,-1)]=1;
samples[(dif[0]+dif[1]+dif[2]).toString().slice(-9,-1)]=1;
samples[(dif[1]-dif[2]-off).toString().slice(-9,-1)]=1;
samples[((dif[2]-dif[0])/(off/7)).toString().slice(-9,-1)]=1;
rounds++;
lastPos = pos;
buff=Object.keys(samples);
if(progress) progress({value:buff.length, max: buff.length*8})
if ( (buff.length*7.95) > chars) done( buff );
}
function done(data) {
samples=null;
window.removeEventListener('devicemotion', accelChange, false);
window.removeEventListener('deviceorientation', accelChange, false);
var out=String(data).replace(/\-?0\.00*/g, "").replace(/\D+/g,""),
collect=[];
formatData(out, format, collect);
callback(collect.slice(-chars).join());
if(progress) progress({value:0, max: 1});
}
window.addEventListener('devicemotion', accelChange, false);
window.addEventListener('deviceorientation', accelChange, false);
}//end getRandomMotion()
rndme.crypto= getRandomFromCrypto;
// crypto - OS-provided CSPRNG with timing data mixin - sync and fast
function getRandomFromCrypto(format, chars, callback, progress) {
chars=Math.floor(chars/1.5);
var pad=(""+Array(Math.floor(chars/9.75))).split("").map(rndme._stamp),
out = [].slice.call(crypto.getRandomValues(new Int8Array(chars)))
.map(Math.abs)
.filter(function(a,b,c){ return a<100; });
out = out.map(function(a,b,c){
var padSlot=Math.floor(b/10), padCol=b%10;
return ("00"+(a+(+pad[padSlot][padCol]||1)).toString().slice(-2)).slice(-2);
}).join("");
var collect=[], buff;
formatData(out, format, collect);
buff =collect.slice(-chars).join("");
if(callback) callback(buff);
return buff;
} //end getRandomFromCrypto()
rndme.unsafe= getRandomFromMath;
// crypto - Browser-provided PRNG - sync, fast, uniform, and not secure
function getRandomFromMath(format, chars, callback, progress) {
chars=Math.floor(chars/1.5);
var out=Array(Math.floor(chars/15)).join(",").split(",").map(Math.random).join("").replace(/,?0\./g,"");
var collect=[], buff;
formatData(out, format, collect);
buff =collect.slice(-chars).join("");
if(callback) callback(buff);
return buff;
} //end getRandomFromMath()
rndme.combo = function combo (sources, format, samples, cb) {
if(!Array.isArray(sources)) sources = [sources];
sources = sources.map(function(a) {
return rndme[a]("int", samples);
});
return Promise.all(sources).then(function(out) {
var collect = [];
formatData(out.reduce(rndme._combine), format, collect);
collect = collect.slice(-samples).join("")
if(cb) cb(collect);
return collect;
});
};//end combo()
function formatData(strData, format, dest){
dest=dest || [];
var rxd = /\D/g,
rxd3 = /\d{3}/g,
count= 0,
s=strData;
switch(format) {
case 'hex':
dest.push((s.match(rxd3) || []).map(function(a, b, c) {
a = ~~((a/999) * 256);
count++;
return("00" + a.toString(16)).slice(-2);
}).join(""));
break;
case 'bytes':
var tr=(s.match(rxd3) || []), i=0, mx=tr.length, rr=Array(mx);
for(;i<mx;i++){
count++;
rr[i]= ~~ ((tr[i] / 999) * 255);
}
dest.push(rr+'');
break;
case 'int':
dest.push(s.replace(rxd, "") || "");
count = dest.join("").length;
break;
case 'raw':
count += String(s).length;
dest.push(s);
break;
case 'float':
dest.push((s.match(/\d{16}/g) || []).map(function(a, b, c) {
count++;
return +("0." + a);
}).join(","));
break;
case 'base64':
var chars = "wp7aY_xzcFLfmoyQqu51KWsZEvOb9XJSP3tin06dR-ClUkGeMVIjgr2NDH4hBT8A".repeat(26).split("").sort(munge);
dest.push((s.match(rxd3) || []).map(function(a, b, c) {
count++;
return chars[+a];
}).join(""));
break;
case 'base92':
default:
var chars="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&()*+,-./:;<=> ?@[]^_{|}~`\t".split("").sort(munge);
var tr=(s.match(/\d{2}/g) || []), i=0, mx=tr.length, rr=Array(mx);
for(;i<mx;i++){
count++;
rr[i]= chars[+tr[i]] || "";
}
dest.push(rr.join(""));
break;
}
return count;
}//end formatData()
//utils
function spin(r, max) { // to unweave linearity of gathered unique samples
for(var slot1 = 0, slot2 = 0, temp = 0, i = 0, mx = r.length, limit = +max || mx; i < limit; i++) {
slot1 = Math.floor(Math.random() * mx);
slot2 = Math.floor(Math.random() * mx);
temp = r[slot1];
r[slot1] = r[slot2];
r[slot2] = temp;
}
return r;
}
function munge(a, b) {
return Math.random() > .5 ? 1 : -1;
}
// a sync timestamp method, returns a new 10-digit string each time
function stamp() {
return(Date.now() / (performance.now() * 100)).toString().split("").filter(/./.test, /\d/).slice(-10).join("");
};
function combine(r1, r2){
var i=0, mx=Math.min(r1.length, r2.length), out=Array(mx);
for(i;i<mx;i++) out[i] = String(+r1[i]+ +r2[i]).slice(-1);
return out.join("");
}
//publish utils:
rndme._munge=munge;
rndme._spin= spin;
rndme._stamp= stamp;
rndme._combine=combine;
function make(method) {
var func = rndme[method];
rndme[method] = function _rnd(format, size, callback, progress, err) {
var one5 = size * 1.5,
three = size * 3,
osize = size;
size = {
'float': size * 16,
hex: one5,
base64: three,
bytes: three,
base92: size * 2.2
}[format] || size;
var cb2 = function rndme_cb(x) {
var u, delim = {
float: ',',
bytes: ',',
base92: '',
int: '',
hex: '',
base64: '',
}[format];
if (delim !== u) x = x.split(delim).slice(-osize).join(delim.exec?"":delim);
if (callback) callback(x);
return x;
};
if (callback) return func(format, size, cb2, progress, err || console.error.bind(console));
return new Promise(function(resolve, reject) {
func(format, size, resolve, null, reject);
}).then(cb2); //end promise
} //end _rnd()
} //end make
["sound","motion","time","video","crypto","unsafe"].forEach(make);
// return static class:
return rndme;
}, this));