Skip to content

Commit b1341d6

Browse files
committed
typingdna.js class, version 1.2 created on Apr 8, 2016
1 parent 66b1cab commit b1341d6

1 file changed

Lines changed: 394 additions & 0 deletions

File tree

typingdna.js

Lines changed: 394 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,394 @@
1+
/**
2+
* TypingDNA - Typing Biometrics JavaScript API v1.2
3+
* http://typingdna.com/scripts/typingdna.js
4+
* Use your own copy. (hotlinks not recommended)
5+
*
6+
* @version 1.2
7+
* @author Raul Popa
8+
* @copyright Typingdna.com
9+
* @license http://www.apache.org/licenses/LICENSE-2.0
10+
* Licensed under the Apache License, Version 2.0 (the "License");
11+
* you may not use this file except in compliance with the License.
12+
* You may obtain a copy of the License at
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
*
21+
* Typical usage:
22+
* var tdna = new TypingDNA(); // creates a new TypingDNA object and starts recording
23+
* var typingPattern = tdna.get(); // returns a typing pattern (and continues recording afterwards),
24+
* optionally you can pass a length, tdna.get(200) will return the pattern based on the last 200 key events.
25+
*
26+
* Optional:
27+
* tdna.stop(); // ends recording and clears history stack (returns recording flag: false)
28+
* tdna.start(); // restarts the recording after a stop (returns recording flag: true)
29+
* tdna.reset(); // restarts the recording anytime, clears history stack and starts from scratch (returns nothing)
30+
* var typingPatternQuality = TypingDNA.getQuality(typingPattern); //returns the quality/strength of any typing pattern
31+
* (there is no need to initialize the class to do pattern quality checking)
32+
*/
33+
34+
/**
35+
* Creates a single instance (or a reference) of the TypingDNA class
36+
* @param {Number} maxHistoryLength Optional: The maximum length of the
37+
* history stack, default:2000; optimal lengths needed for good quality
38+
* typing patterns are between 500 and 5000, you can also pass 0 for default.
39+
* @return {Object} Returns the single instance of the TypingDNA class.
40+
* @example var tdna = new TypingDNA();
41+
* @example var tdna = new TypingDNA(0);
42+
* @example var tdna = new TypingDNA(300);
43+
*/
44+
function TypingDNA(maxHistoryLength) {
45+
if (TypingDNA.initialized != true) {
46+
TypingDNA.prototype.start = function() {
47+
return TypingDNA.start.apply(this, arguments);
48+
}
49+
TypingDNA.prototype.stop = function() {
50+
return TypingDNA.stop.apply(this, arguments);
51+
}
52+
TypingDNA.prototype.reset = function() {
53+
return TypingDNA.reset.apply(this, arguments);
54+
}
55+
TypingDNA.prototype.get = function(args) {
56+
return TypingDNA.get.apply(this, arguments);
57+
}
58+
TypingDNA.prototype.getQuality = function(args) {
59+
return TypingDNA.getQuality.apply(this, arguments);
60+
}
61+
TypingDNA.prototype.maxHistoryLength = TypingDNA.maxHistoryLength;
62+
TypingDNA.prototype.defaultHistoryLength = TypingDNA.defaultHistoryLength;
63+
TypingDNA.prototype.maxSeekTime = TypingDNA.maxSeekTime;
64+
TypingDNA.prototype.maxPressTime = TypingDNA.maxPressTime;
65+
TypingDNA.instance = this;
66+
TypingDNA.element = document;
67+
TypingDNA.maxHistoryLength = 2000;
68+
TypingDNA.maxSeekTime = 1500;
69+
TypingDNA.maxPressTime = 300;
70+
TypingDNA.defaultHistoryLength = 500;
71+
// 44 chars
72+
TypingDNA.chars = [97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 32, 39, 44, 46, 59, 63, 45, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57];
73+
TypingDNA.pt1 = TypingDNA.ut1 = (new Date).getTime();
74+
TypingDNA.wfk = [];
75+
TypingDNA.sti = [];
76+
TypingDNA.skt = [];
77+
TypingDNA.rkc = [];
78+
TypingDNA.lastDownKey;
79+
TypingDNA.prevKeyCode = 0;
80+
TypingDNA.zl = 0.000000000001;
81+
TypingDNA.keydown = function(e) {
82+
TypingDNA.lastDownKey = e.which;
83+
}
84+
TypingDNA.keypress = function(e) {
85+
var t0 = TypingDNA.pt1;
86+
TypingDNA.pt1 = (new Date).getTime();
87+
var rkco = e.which;
88+
var seekTotal = TypingDNA.pt1 - t0;
89+
var startTime = TypingDNA.pt1;
90+
var downKey = TypingDNA.lastDownKey;
91+
TypingDNA.wfk[downKey] = 1;
92+
TypingDNA.skt[downKey] = seekTotal;
93+
TypingDNA.sti[downKey] = startTime;
94+
TypingDNA.rkc[downKey] = rkco;
95+
}
96+
TypingDNA.keyup = function(e) {
97+
var ut = (new Date).getTime();
98+
var upKey = e.which;
99+
if (TypingDNA.wfk[upKey] == 1) {
100+
var keyCode = TypingDNA.rkc[upKey];
101+
var pressTime = ut - TypingDNA.sti[upKey];
102+
var seekTime = TypingDNA.skt[upKey];
103+
var arr = [keyCode, seekTime, pressTime, TypingDNA.prevKeyCode, ut];
104+
TypingDNA.history.add(arr);
105+
TypingDNA.prevKeyCode = keyCode;
106+
TypingDNA.wfk[upKey] = 0;
107+
}
108+
}
109+
TypingDNA.element.addEventListener("keydown", TypingDNA.keydown);
110+
TypingDNA.element.addEventListener("keypress", TypingDNA.keypress);
111+
TypingDNA.element.addEventListener("keyup", TypingDNA.keyup);
112+
113+
/**
114+
* Resets the history stack
115+
*/
116+
TypingDNA.reset = function() {
117+
TypingDNA.history.stack = [];
118+
}
119+
120+
/**
121+
* Automatically called at initilization. It starts the recording of keystrokes.
122+
*/
123+
TypingDNA.start = function() {
124+
if (TypingDNA.recording == true) {
125+
TypingDNA.stop();
126+
}
127+
TypingDNA.reset();
128+
return TypingDNA.recording = true;
129+
}
130+
131+
/**
132+
* Ends the recording of further keystrokes. To restart recording afterwards you can
133+
* either call TypingDNA.start() or create a new TypingDNA object again, not recommended.
134+
*/
135+
TypingDNA.stop = function() {
136+
TypingDNA.reset();
137+
return TypingDNA.recording = false;
138+
}
139+
140+
/**
141+
* This function outputs the typing pattern as a String, in a new basic structure for
142+
* easy storage and usage in any kind of keystroke dynamics applications (e.g. typing
143+
* pattern matching, user recognition)
144+
* @param {Number} length Optional: The amount of history keystrokes to use for the
145+
* typing pattern. By default it will use the last 500 recorded keystrokes (or as many
146+
* available if less than 500).
147+
* @return {String} The TypingDNA typing pattern, comma separated.
148+
* A fixed vector of only numeric values separated by commas.
149+
* @example var typingPattern = tdna.get();
150+
* @example var typingPattern = tdna.get(200);
151+
*/
152+
TypingDNA.get = function(length) {
153+
var historyTotalLength = TypingDNA.history.stack.length;
154+
if (length == undefined) {
155+
length = TypingDNA.defaultHistoryLength;
156+
}
157+
if (length > historyTotalLength) {
158+
length = historyTotalLength;
159+
}
160+
var obj = {};
161+
obj.arr = TypingDNA.history.get(length);
162+
var zl = TypingDNA.zl;
163+
var histRev = length;
164+
var histSktF = TypingDNA.math.fo(TypingDNA.history.get(length, "seek"));
165+
var histPrtF = TypingDNA.math.fo(TypingDNA.history.get(length, "press"));
166+
var histRevPF = histPrtF.length;
167+
var histRevSF = histSktF.length;
168+
var pressHistMean = Math.round(TypingDNA.math.avg(histPrtF));
169+
var seekHistMean = Math.round(TypingDNA.math.avg(histSktF));
170+
var pressHistSD = Math.round(TypingDNA.math.sd(histPrtF));
171+
var seekHistSD = Math.round(TypingDNA.math.sd(histSktF));
172+
var charMeanTime = seekHistMean + pressHistMean;
173+
var pressRatio = TypingDNA.math.rd((pressHistMean + zl) / (charMeanTime + zl), 3);
174+
var seekToPressRatio = TypingDNA.math.rd((1 - pressRatio)/pressRatio, 3);
175+
var pressSDToPressRatio = TypingDNA.math.rd((pressHistSD + zl) / (pressHistMean + zl), 3);
176+
var seekSDToPressRatio = TypingDNA.math.rd((seekHistSD + zl) / (pressHistMean + zl), 3);
177+
var cpm = Math.round(6E4 / (charMeanTime + zl));
178+
for (var i in obj.arr) {
179+
var rev = obj.arr[i][1].length;
180+
var seekMean = 0;
181+
var pressMean = 0;
182+
var postMean = 0;
183+
var seekSD = 0;
184+
var pressSD = 0;
185+
var postSD = 0;
186+
switch (obj.arr[i][0].length) {
187+
case 0:
188+
break;
189+
case 1:
190+
var seekMean = TypingDNA.math.rd((obj.arr[i][0][0] + zl) / (seekHistMean + zl), 3);
191+
break;
192+
default:
193+
var arr = TypingDNA.math.fo(obj.arr[i][0]);
194+
seekMean = TypingDNA.math.rd((TypingDNA.math.avg(arr) + zl) / (seekHistMean + zl), 3);
195+
seekSD = TypingDNA.math.rd((TypingDNA.math.sd(arr) + zl) / (seekHistSD + zl), 3);
196+
}
197+
switch (obj.arr[i][1].length) {
198+
case 0:
199+
break;
200+
case 1:
201+
var pressMean = TypingDNA.math.rd((obj.arr[i][1][0] + zl) / (pressHistMean + zl), 3);
202+
break;
203+
default:
204+
var arr = TypingDNA.math.fo(obj.arr[i][1]);
205+
pressMean = TypingDNA.math.rd((TypingDNA.math.avg(arr) + zl) / (pressHistMean + zl), 3);
206+
pressSD = TypingDNA.math.rd((TypingDNA.math.sd(arr) + zl) / (pressHistSD + zl), 3);
207+
}
208+
switch (obj.arr[i][2].length) {
209+
case 0:
210+
break;
211+
case 1:
212+
var postMean = TypingDNA.math.rd((obj.arr[i][2][0] + zl) / (seekHistMean + zl), 3);
213+
break;
214+
default:
215+
var arr = TypingDNA.math.fo(obj.arr[i][2]);
216+
postMean = TypingDNA.math.rd((TypingDNA.math.avg(arr) + zl) / (seekHistMean + zl), 3);
217+
postSD = TypingDNA.math.rd((TypingDNA.math.sd(arr) + zl) / (seekHistSD + zl), 3);
218+
}
219+
delete obj.arr[i][2];
220+
delete obj.arr[i][1];
221+
delete obj.arr[i][0];
222+
obj.arr[i][0] = rev;
223+
obj.arr[i][1] = seekMean;
224+
obj.arr[i][2] = pressMean;
225+
obj.arr[i][3] = postMean;
226+
obj.arr[i][4] = seekSD;
227+
obj.arr[i][5] = pressSD;
228+
obj.arr[i][6] = postSD;
229+
}
230+
var arr = [];
231+
TypingDNA.apu(arr, histRev);
232+
TypingDNA.apu(arr, cpm);
233+
TypingDNA.apu(arr, charMeanTime);
234+
TypingDNA.apu(arr, pressRatio);
235+
TypingDNA.apu(arr, seekToPressRatio);
236+
TypingDNA.apu(arr, pressSDToPressRatio);
237+
TypingDNA.apu(arr, seekSDToPressRatio);
238+
TypingDNA.apu(arr, pressHistMean);
239+
TypingDNA.apu(arr, seekHistMean);
240+
TypingDNA.apu(arr, pressHistSD);
241+
TypingDNA.apu(arr, seekHistSD);
242+
for (var c = 0; c <= 6; c++) {
243+
for (var i = 0; i < 44; i++) {
244+
var keyCode = TypingDNA.chars[i];
245+
var val = obj.arr[keyCode][c];
246+
if (val == 0 && c > 0) {
247+
val = 1;
248+
}
249+
TypingDNA.apu(arr, val);
250+
}
251+
}
252+
return arr.join(",");
253+
}
254+
TypingDNA.apu = function(arr, val) {
255+
"NaN" == String(val) && (val = 0);
256+
arr.push(val);
257+
}
258+
TypingDNA.math = {};
259+
TypingDNA.math.rd = function(val, dec) {
260+
return Number(val.toFixed(dec));
261+
}
262+
TypingDNA.math.avg = function(arr) {
263+
var len = arr.length;
264+
var sum = 0;
265+
for (var i = 0; i < len; i++) {
266+
sum += arr[i];
267+
}
268+
return this.rd(sum / len, 3);
269+
}
270+
TypingDNA.math.sd = function(arr) {
271+
var len = arr.length;
272+
if (len < 2) {
273+
return 0;
274+
} else {
275+
var sumVS = 0;
276+
var mean = this.avg(arr);
277+
for (var i = 0; i < len; i++) {
278+
sumVS += (arr[i] - mean) * (arr[i] - mean);
279+
}
280+
var sd = Math.sqrt(sumVS / len);
281+
return sd;
282+
}
283+
}
284+
TypingDNA.math.fo = function(arr) {
285+
var values = arr.concat();
286+
values.sort(function(a, b) {
287+
return a - b;
288+
});
289+
var asd = this.sd(values);
290+
var aMean = values[Math.ceil(arr.length / 2)];
291+
var multiplier = 2;
292+
var maxVal = aMean + multiplier * asd;
293+
var minVal = aMean - multiplier * asd;
294+
if (arr.length < 20) {
295+
minVal = 0;
296+
}
297+
var fVal = values.filter(function(x) {
298+
return x < maxVal && x > minVal;
299+
});
300+
return fVal;
301+
}
302+
TypingDNA.history = {};
303+
TypingDNA.history.stack = [];
304+
TypingDNA.history.add = function(arr) {
305+
this.stack.push(arr);
306+
if (this.stack.length > TypingDNA.maxHistoryLength) {
307+
this.stack.shift();
308+
}
309+
}
310+
TypingDNA.history.get = function(length, type) {
311+
var historyTotalLength = this.stack.length;
312+
if (length == 0 || length == undefined) {
313+
length = TypingDNA.defaultHistoryLength;
314+
}
315+
if (length > historyTotalLength) {
316+
length = historyTotalLength;
317+
}
318+
switch (type) {
319+
case "seek":
320+
var seekArr = [];
321+
for (i = 1; i <= length; i++) {
322+
var seekTime = this.stack[historyTotalLength - i][1];
323+
if (seekTime <= TypingDNA.maxSeekTime) {
324+
seekArr.push(seekTime);
325+
}
326+
};
327+
return seekArr;
328+
break;
329+
case "press":
330+
var pressArr = [];
331+
for (i = 1; i <= length; i++) {
332+
var pressTime = this.stack[historyTotalLength - i][2];
333+
if (pressTime <= TypingDNA.maxPressTime) {
334+
pressArr.push(pressTime);
335+
}
336+
};
337+
return pressArr;
338+
break;
339+
default:
340+
var historyStackObj = {};
341+
for (var i in TypingDNA.chars) {
342+
historyStackObj[TypingDNA.chars[i]] = [
343+
[],
344+
[],
345+
[]
346+
];
347+
}
348+
for (i = 1; i <= length; i++) {
349+
var arr = this.stack[historyTotalLength - i];
350+
var keyCode = arr[0];
351+
var seekTime = arr[1];
352+
var pressTime = arr[2];
353+
var prevKeyCode = arr[3];
354+
if (TypingDNA.chars.indexOf(keyCode) != -1) {
355+
if (seekTime <= TypingDNA.maxSeekTime) {
356+
historyStackObj[keyCode][0].push(seekTime);
357+
if (prevKeyCode != 0 && TypingDNA.chars.indexOf(prevKeyCode) != -1) {
358+
historyStackObj[prevKeyCode][2].push(seekTime);
359+
}
360+
}
361+
if (pressTime <= TypingDNA.maxPressTime) {
362+
historyStackObj[keyCode][1].push(pressTime);
363+
}
364+
}
365+
};
366+
return historyStackObj;
367+
}
368+
}
369+
}
370+
371+
/**
372+
* Checks the quality of a typing pattern, how well it is revelated, how useful the
373+
* information will be for matching applications. It returns a value between 0 and 1.
374+
* Values over 0.3 are acceptable, however a value over 0.7 shows good pattern strength.
375+
* @param {String} typingPattern The typing pattern string returned by the get() function.
376+
* @return {Number} A real number between 0 and 1. A close to 1 value means a stronger pattern.
377+
* @example var quality = tdna.getQuality(typingPattern);
378+
*/
379+
TypingDNA.getQuality = function(typingPattern) {
380+
var obj = typingPattern.split(",").map(Number);
381+
var totalEvents = obj[0];
382+
var acc = rec = avgAcc = 0;
383+
var avg = TypingDNA.math.avg(obj);
384+
var revs = obj.slice(11, 55);
385+
for (var i in revs) {
386+
rec += Number(revs[i] > 0);
387+
acc += Number(revs[i] > 4);
388+
avgAcc += Number(revs[i] > avg);
389+
}
390+
var tReturn = Math.sqrt(rec * acc * avgAcc) / 80;
391+
return tReturn > 1 ? 1 : tReturn;
392+
}
393+
394+
}

0 commit comments

Comments
 (0)