ksd 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +7 -0
- data/Manifest.txt +18 -0
- data/PostInstall.txt +7 -0
- data/README.rdoc +59 -0
- data/Rakefile +28 -0
- data/examples/enroll_login.rb +93 -0
- data/examples/enroll_scentences.rb +94 -0
- data/examples/login.rb +63 -0
- data/lib/keystroke_dynamics/analysis.rb +226 -0
- data/lib/keystroke_dynamics/validation.rb +167 -0
- data/lib/keystroke_dynamics.rb +28 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/spec/keystroke_dynamics_spec.rb +11 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- data/tasks/rspec.rake +21 -0
- metadata +94 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
PostInstall.txt
|
4
|
+
README.rdoc
|
5
|
+
Rakefile
|
6
|
+
examples/enroll_login.rb
|
7
|
+
examples/enroll_scentences.rb
|
8
|
+
examples/login.rb
|
9
|
+
lib/keystroke_dynamics.rb
|
10
|
+
lib/keystroke_dynamics/analysis.rb
|
11
|
+
lib/keystroke_dynamics/validation.rb
|
12
|
+
script/console
|
13
|
+
script/destroy
|
14
|
+
script/generate
|
15
|
+
spec/keystroke_dynamics_spec.rb
|
16
|
+
spec/spec.opts
|
17
|
+
spec/spec_helper.rb
|
18
|
+
tasks/rspec.rake
|
data/PostInstall.txt
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
= keystroke_dynamics
|
2
|
+
|
3
|
+
* http://ksd.rubyforge.org/
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
Simple keystroke dynamics analyzer/validator written in Ruby-GTK
|
7
|
+
|
8
|
+
|
9
|
+
== FEATURES:
|
10
|
+
|
11
|
+
* Analysis holds logic to extract simple biometric information from GTK widgets.
|
12
|
+
* Validation holds logic to manage user enrollment, validation and cryptographic functions.
|
13
|
+
|
14
|
+
== EXAMPLES:
|
15
|
+
The three included Ruby-GTK examples demonstrate the principle of biometric authentication based on keystroke dynamics. For experimentational purposes, I have created two different examples which establish a metric from a users typing. The first, enroll_scentences.rb, lets you type in 5 pangrams (scentences that hold every letter of the alphabet) to establish your metric. The other, enroll_login.rb, lets you type your login details 10 times. I leave it as an excercise to the user to see which method works best for him or her.
|
16
|
+
The login.rb example lets you try out your newly created username, password and keystroke metric on a login screen.
|
17
|
+
|
18
|
+
== REQUIREMENTS:
|
19
|
+
|
20
|
+
|
21
|
+
Depends on
|
22
|
+
* GTK
|
23
|
+
* Ruby GTK bindings
|
24
|
+
* Ruby OpenSSL bindings
|
25
|
+
|
26
|
+
== INSTALL:
|
27
|
+
|
28
|
+
* sudo gem install keystroke_dynamics
|
29
|
+
|
30
|
+
If you don't have the Ruby GTK or OpenSSL bindings you should install them through your package manager.
|
31
|
+
|
32
|
+
On Debian/Ubuntu:
|
33
|
+
* sudo apt-get install libgtk2-ruby
|
34
|
+
* sudo apt-get install libopenssl-ruby
|
35
|
+
|
36
|
+
== LICENSE:
|
37
|
+
|
38
|
+
(The MIT License)
|
39
|
+
|
40
|
+
Copyright (c) 2009 Aram Verstegen
|
41
|
+
|
42
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
43
|
+
a copy of this software and associated documentation files (the
|
44
|
+
'Software'), to deal in the Software without restriction, including
|
45
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
46
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
47
|
+
permit persons to whom the Software is furnished to do so, subject to
|
48
|
+
the following conditions:
|
49
|
+
|
50
|
+
The above copyright notice and this permission notice shall be
|
51
|
+
included in all copies or substantial portions of the Software.
|
52
|
+
|
53
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
54
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
55
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
56
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
57
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
58
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
59
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
%w[rubygems rake rake/clean fileutils newgem rubigen].each { |f| require f }
|
2
|
+
require File.dirname(__FILE__) + '/lib/keystroke_dynamics'
|
3
|
+
|
4
|
+
# Generate all the Rake tasks
|
5
|
+
# Run 'rake -T' to see list of generated tasks (from gem root directory)
|
6
|
+
$hoe = Hoe.new('ksd', KeystrokeDynamics::VERSION) do |p|
|
7
|
+
p.developer('Aram Verstegen', 'aram@aczid.nl')
|
8
|
+
p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
|
9
|
+
p.post_install_message = 'PostInstall.txt' # TODO remove if post-install message not required
|
10
|
+
p.rubyforge_name = p.name # TODO this is default value
|
11
|
+
# p.extra_deps = [
|
12
|
+
# ['activesupport','>= 2.0.2'],
|
13
|
+
# ]
|
14
|
+
p.extra_dev_deps = [
|
15
|
+
['newgem', ">= #{::Newgem::VERSION}"]
|
16
|
+
]
|
17
|
+
|
18
|
+
p.clean_globs |= %w[**/.DS_Store tmp *.log]
|
19
|
+
path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
|
20
|
+
p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
|
21
|
+
p.rsync_args = '-av --delete --ignore-errors'
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'newgem/tasks' # load /tasks/*.rake
|
25
|
+
Dir['tasks/**/*.rake'].each { |t| load t }
|
26
|
+
|
27
|
+
# TODO - want other tests/tasks run by default? Add them to the list
|
28
|
+
# task :default => [:spec, :features]
|
@@ -0,0 +1,93 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# == Summary
|
3
|
+
# This is an example enrollment application set up to collect keystroke dynamics data by letting the user type his username and password several times.
|
4
|
+
|
5
|
+
require 'gtk2'
|
6
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
7
|
+
require 'rubygems'
|
8
|
+
require 'keystroke_dynamics'
|
9
|
+
KeystrokeDynamics::Validation::KSD_DIR = './keystroke_dynamics/'
|
10
|
+
KeystrokeDynamics::Validation::PH_FILE = './passwd'
|
11
|
+
|
12
|
+
window = Gtk::Window.new
|
13
|
+
window.title = "Keystroke dynamics enrollment test application"
|
14
|
+
window.signal_connect("destroy") { Gtk.main_quit }
|
15
|
+
window.signal_connect("delete_event") { Gtk.main_quit }
|
16
|
+
|
17
|
+
mainbox = Gtk::VBox.new(false,0)
|
18
|
+
window.add(mainbox)
|
19
|
+
mainbox.show
|
20
|
+
|
21
|
+
user = Gtk::Entry.new
|
22
|
+
user.set_text("username")
|
23
|
+
user.set_editable(true)
|
24
|
+
pass = Gtk::Entry.new
|
25
|
+
pass.set_text("pass")
|
26
|
+
pass.set_editable(true)
|
27
|
+
pass.set_invisible_char(42) # 42 is "*" in unicode
|
28
|
+
pass.set_visibility(false)
|
29
|
+
|
30
|
+
loginbox = Gtk::VBox.new
|
31
|
+
loginbox.pack_start(Gtk::Label.new("Choose you login details"), true, true, 0)
|
32
|
+
loginbox.pack_start(user, true, true, 0)
|
33
|
+
loginbox.pack_start(pass, true, true, 0)
|
34
|
+
|
35
|
+
mainbox.pack_start(loginbox, true, true,0)
|
36
|
+
mainbox.show
|
37
|
+
|
38
|
+
mainbox.pack_start(Gtk::Label.new("Please type your login details 10 times so I can learn they way you type them."), true, true, 20)
|
39
|
+
|
40
|
+
statsbox = Gtk::HBox.new
|
41
|
+
mean_hold_label = Gtk::Label.new
|
42
|
+
mean_seek_label = Gtk::Label.new
|
43
|
+
mean_kps_label = Gtk::Label.new
|
44
|
+
statsbox.pack_start(mean_hold_label, true, true, 0)
|
45
|
+
statsbox.pack_start(mean_seek_label, true, true, 0)
|
46
|
+
statsbox.pack_start(mean_kps_label, true, true, 0)
|
47
|
+
|
48
|
+
ksds = []
|
49
|
+
for i in (0..9)
|
50
|
+
loginbox = Gtk::HBox.new
|
51
|
+
login = Gtk::Entry.new
|
52
|
+
login.select_region(0,-1)
|
53
|
+
login.set_editable(true)
|
54
|
+
pass = Gtk::Entry.new
|
55
|
+
pass.set_editable(true)
|
56
|
+
pass.set_invisible_char(42) # 42 is "*" in unicode
|
57
|
+
pass.set_visibility(false)
|
58
|
+
loginbox.pack_start(login, true, true, 0)
|
59
|
+
loginbox.pack_start(pass, true, true, 0)
|
60
|
+
ksds[i] = KeystrokeDynamics::Analysis.new
|
61
|
+
ksds[i].analyze_keys([login, pass])
|
62
|
+
loginbox.signal_connect_after("key-release-event", ksds[i]) do |w, e, analyzer|
|
63
|
+
mean_hold_label.text = "Mean hold: #{analyzer.mean_hold}"
|
64
|
+
mean_seek_label.text = "Mean seek: #{analyzer.mean_seek}"
|
65
|
+
mean_kps_label.text = "Mean KPS: #{analyzer.mean_kps}"
|
66
|
+
false
|
67
|
+
end
|
68
|
+
loginbox.show
|
69
|
+
mainbox.pack_start(loginbox,true,true,0)
|
70
|
+
end
|
71
|
+
|
72
|
+
mainbox.pack_start(statsbox,true,true,20)
|
73
|
+
statsbox.show
|
74
|
+
|
75
|
+
button = Gtk::Button.new("Enroll")
|
76
|
+
button.signal_connect("clicked") do |w|
|
77
|
+
keystroke_array_array = []
|
78
|
+
ksds.each do |ksd|
|
79
|
+
keystroke_array_array << ksd.keystrokes
|
80
|
+
end
|
81
|
+
if KeystrokeDynamics::Validation.enroll(user.text, pass.text, keystroke_array_array)
|
82
|
+
Gtk.main_quit
|
83
|
+
end
|
84
|
+
end
|
85
|
+
button.show
|
86
|
+
mainbox.pack_start(button, true, true,0)
|
87
|
+
|
88
|
+
window.set_default_size(100, 100).show_all
|
89
|
+
|
90
|
+
window.show
|
91
|
+
|
92
|
+
Gtk.main
|
93
|
+
exit
|
@@ -0,0 +1,94 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# == Summary
|
3
|
+
# This is an example enrollment application set up to collect keystroke dynamics data by letting the user type several scentences.
|
4
|
+
require 'gtk2'
|
5
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
6
|
+
require 'rubygems'
|
7
|
+
require 'keystroke_dynamics'
|
8
|
+
KeystrokeDynamics::Validation::KSD_DIR = './keystroke_dynamics/'
|
9
|
+
KeystrokeDynamics::Validation::PH_FILE = './passwd'
|
10
|
+
|
11
|
+
window = Gtk::Window.new
|
12
|
+
window.title = "Keystroke dynamics enrollment test application"
|
13
|
+
window.signal_connect("destroy") { Gtk.main_quit }
|
14
|
+
window.signal_connect("delete_event") { Gtk.main_quit }
|
15
|
+
|
16
|
+
mainbox = Gtk::VBox.new(false,0)
|
17
|
+
window.add(mainbox)
|
18
|
+
mainbox.show
|
19
|
+
|
20
|
+
user = Gtk::Entry.new
|
21
|
+
user.set_text("username")
|
22
|
+
user.set_editable(true)
|
23
|
+
pass = Gtk::Entry.new
|
24
|
+
pass.set_text("pass")
|
25
|
+
pass.set_editable(true)
|
26
|
+
pass.set_invisible_char(42) # 42 is "*" in unicode
|
27
|
+
pass.set_visibility(false)
|
28
|
+
|
29
|
+
loginbox = Gtk::VBox.new
|
30
|
+
loginbox.pack_start(Gtk::Label.new("Choose you login details"), true, true, 0)
|
31
|
+
loginbox.pack_start(user, true, true, 0)
|
32
|
+
loginbox.pack_start(pass, true, true, 0)
|
33
|
+
|
34
|
+
mainbox.pack_start(loginbox, true, true,0)
|
35
|
+
mainbox.show
|
36
|
+
|
37
|
+
mainbox.pack_start(Gtk::Label.new("Please type these scentences so I can learn your keyboard dynamics."), true, true, 20)
|
38
|
+
|
39
|
+
scentences = [
|
40
|
+
Gtk::Label.new("Two driven jocks help fax my big quiz."),
|
41
|
+
Gtk::Label.new("How quickly daft jumping zebras vex."),
|
42
|
+
Gtk::Label.new("The five boxing wizards jump quickly."),
|
43
|
+
Gtk::Label.new("Jackdaws love my big sphinx of quartz."),
|
44
|
+
Gtk::Label.new("The quick brown fox jumps over the lazy dog.")
|
45
|
+
]
|
46
|
+
|
47
|
+
statsbox = Gtk::HBox.new
|
48
|
+
mean_hold_label = Gtk::Label.new
|
49
|
+
mean_seek_label = Gtk::Label.new
|
50
|
+
mean_kps_label = Gtk::Label.new
|
51
|
+
statsbox.pack_start(mean_hold_label, true, true, 0)
|
52
|
+
statsbox.pack_start(mean_seek_label, true, true, 0)
|
53
|
+
statsbox.pack_start(mean_kps_label, true, true, 0)
|
54
|
+
|
55
|
+
ksds = []
|
56
|
+
for i in (0..4)
|
57
|
+
entry = Gtk::Entry.new
|
58
|
+
entry.set_text("")
|
59
|
+
entry.set_editable(true)
|
60
|
+
ksds[i] = KeystrokeDynamics::Analysis.new
|
61
|
+
ksds[i].analyze_keys(entry)
|
62
|
+
mainbox.pack_start(scentences[i],true,true,0)
|
63
|
+
entry.signal_connect_after("key-release-event", ksds[i]) do |w, e, analyzer|
|
64
|
+
mean_hold_label.text = "Mean hold: #{analyzer.mean_hold}"
|
65
|
+
mean_seek_label.text = "Mean seek: #{analyzer.mean_seek}"
|
66
|
+
mean_kps_label.text = "Mean KPS: #{analyzer.mean_kps}"
|
67
|
+
false
|
68
|
+
end
|
69
|
+
entry.show
|
70
|
+
mainbox.pack_start(entry,true,true,0)
|
71
|
+
end
|
72
|
+
|
73
|
+
mainbox.pack_start(statsbox,true,true,20)
|
74
|
+
statsbox.show
|
75
|
+
|
76
|
+
button = Gtk::Button.new("Enroll")
|
77
|
+
button.signal_connect("clicked") do |w|
|
78
|
+
keystroke_array_array = []
|
79
|
+
ksds.each do |ksd|
|
80
|
+
keystroke_array_array << ksd.keystrokes
|
81
|
+
end
|
82
|
+
if KeystrokeDynamics::Validation.enroll(user.text, pass.text, keystroke_array_array)
|
83
|
+
Gtk.main_quit
|
84
|
+
end
|
85
|
+
end
|
86
|
+
button.show
|
87
|
+
mainbox.pack_start(button, true, true,0)
|
88
|
+
|
89
|
+
window.set_default_size(100, 100).show_all
|
90
|
+
|
91
|
+
window.show
|
92
|
+
|
93
|
+
Gtk.main
|
94
|
+
exit
|
data/examples/login.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# == Summary
|
3
|
+
# This is an example login application set up to validate keystroke dynamics data.
|
4
|
+
|
5
|
+
require 'gtk2'
|
6
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
7
|
+
require 'rubygems'
|
8
|
+
require 'keystroke_dynamics'
|
9
|
+
KeystrokeDynamics::Validation::KSD_DIR = './keystroke_dynamics/'
|
10
|
+
KeystrokeDynamics::Validation::PH_FILE = './passwd'
|
11
|
+
KeystrokeDynamics::Validation::ACCURACY_THRESHOLD = 0.5
|
12
|
+
|
13
|
+
|
14
|
+
window = Gtk::Window.new
|
15
|
+
window.title = "Keystroke dynamics login test application"
|
16
|
+
window.signal_connect("destroy") { Gtk.main_quit }
|
17
|
+
window.signal_connect("delete_event") { Gtk.main_quit }
|
18
|
+
|
19
|
+
mainbox = Gtk::VBox.new(false,0)
|
20
|
+
window.add(mainbox)
|
21
|
+
mainbox.show
|
22
|
+
|
23
|
+
mainbox.pack_start(Gtk::Label.new("Try to log in!"), true, true, 20)
|
24
|
+
|
25
|
+
login = Gtk::Entry.new
|
26
|
+
login.select_region(0,-1)
|
27
|
+
login.set_editable(true)
|
28
|
+
pass = Gtk::Entry.new
|
29
|
+
pass.set_editable(true)
|
30
|
+
pass.set_invisible_char(42) # 42 is "*" in unicode
|
31
|
+
pass.set_visibility(false)
|
32
|
+
|
33
|
+
ksd = KeystrokeDynamics::Analysis.new
|
34
|
+
ksd.analyze_keys([login, pass])
|
35
|
+
|
36
|
+
button = Gtk::Button.new("Log in")
|
37
|
+
button.signal_connect("clicked") do |w|
|
38
|
+
if(KeystrokeDynamics::Validation.validate(login.text, pass.text, [ksd.keystrokes]))
|
39
|
+
puts "Logged in successfully!"
|
40
|
+
Gtk.main_quit
|
41
|
+
end
|
42
|
+
# reset entry fields and identified keys to allow several login attempts, avoid the risk of comparing empty keys
|
43
|
+
login.text=""
|
44
|
+
pass.text=""
|
45
|
+
ksd.keystrokes = []
|
46
|
+
end
|
47
|
+
|
48
|
+
button.show
|
49
|
+
|
50
|
+
mainbox.pack_start(login, true, true,0)
|
51
|
+
mainbox.pack_start(pass, true, true,0)
|
52
|
+
mainbox.pack_start(button, true, true,20)
|
53
|
+
|
54
|
+
login.show
|
55
|
+
pass.show
|
56
|
+
|
57
|
+
window.set_default_size(100, 100).show_all
|
58
|
+
|
59
|
+
window.show
|
60
|
+
|
61
|
+
Gtk.main
|
62
|
+
exit
|
63
|
+
|
@@ -0,0 +1,226 @@
|
|
1
|
+
# == Summary
|
2
|
+
# The Analysis class is used to:
|
3
|
+
# * gather keystroke data from attached GTK widgets
|
4
|
+
# * calculate character-specific statistics from the gathered keystroke data to base a metric on
|
5
|
+
# * calculate an average metric, consisting of min/max/mean seek/hold times per character, over several instances of keystroke data using these statistics
|
6
|
+
# * calculate the deviiation between these average metrics
|
7
|
+
# * calculate general statistics from the gathered keystroke data, to provide real time feedback to users
|
8
|
+
|
9
|
+
# Module housing Analysis class, Validation class and associated constants
|
10
|
+
module KeystrokeDynamics
|
11
|
+
|
12
|
+
# Number of milliseconds allowed to be slower or faster from measured profile.
|
13
|
+
# For example, setting this to 1000 means that there can be a total of 1 second deviation in keystroke min/max/mean seeks and holds combined. Having a 1 second deviation would then make the compare_metrics function return 0.
|
14
|
+
# This number needs to be increased as the number of compared metrics increases to allow for standard deviation.
|
15
|
+
MAX_ALLOWED_DEVIATION = 800
|
16
|
+
|
17
|
+
# == Summary
|
18
|
+
# The Analysis class is used to:
|
19
|
+
# * gather keystroke data from attached GTK widgets
|
20
|
+
# * calculate character-specific statistics from the gathered keystroke data to base a metric on
|
21
|
+
# * calculate an average metric, consisting of min/max/mean seek/hold times per character, over several instances of keystroke data using these statistics
|
22
|
+
# * calculate the deviiation between these average metrics
|
23
|
+
# * calculate general statistics from the gathered keystroke data, to provide real time feedback to users
|
24
|
+
#
|
25
|
+
# === Data structures
|
26
|
+
# First, a signal handler is attached to log key data. These are keystrokes with metadata such as time_pressed, time_released. These are elaborated on by calculating the hold time and the seek time since the last character interactively.
|
27
|
+
# These keystrokes are stored in an array which ensures they stay in an ordered position.
|
28
|
+
# The statistics and metric functions operate on an array of these keystroke arrays, which allows for test data from several widgets to be analyzed simultaneously. This metric is a hash of keystrokes with more explicit biometric information, namely min/max/mean seek/hold times.
|
29
|
+
# This metric can be tested for simmilarity to a refrence metric by counting the number of deviations in ms from the reference metric. The reference metric is actually calculated using the same metric function, because it is able to condense many instances of widgets (arrays of arrays) to a metric. Averaging over many widgets ensures any deviation gets more flattened out, and thus makes the metric more accurate.
|
30
|
+
#
|
31
|
+
# == Notes
|
32
|
+
# This class is instantiable as an Analysis, to capture input and do simple statistics on it.
|
33
|
+
# However, it also houses all the logic to calculate and compare the metric as class methods, which can be called from everywhere. (So it is partly like a static class would be in other languages.)
|
34
|
+
class Analysis
|
35
|
+
# This array houses the analyzed keystrokes, with hashes like <tt>{:time_pressed, :time_released, :character, :seek_time, :hold_time}</tt>.
|
36
|
+
# If the concept of hashes is unfamilliar to you, it might help to think of them like simple structs.
|
37
|
+
attr_accessor :keystrokes
|
38
|
+
|
39
|
+
def initialize
|
40
|
+
# Using an array ensures the keystrokes are stored in the order they were inserted.
|
41
|
+
# This is important because the release event handler needs to look through the list to find the last key which was pressed, but possibly not yet released.
|
42
|
+
@keystrokes = []
|
43
|
+
end
|
44
|
+
|
45
|
+
# Attaches signal handlers to (an array of) widgets so that keystroke dynamics data can be collected from them.
|
46
|
+
def analyze_keys(widget_array)
|
47
|
+
widget_array = [widget_array] if !widget_array.is_a?(Array)
|
48
|
+
widget_array.each do |widget|
|
49
|
+
widget.add_events(Gdk::Event::KEY_PRESS)
|
50
|
+
widget.add_events(Gdk::Event::KEY_RELEASE)
|
51
|
+
widget.signal_connect("key-press-event") do |w, e|
|
52
|
+
if(e.keyval)
|
53
|
+
keystroke = {:time_pressed => e.time, :character => Gdk::Keyval.to_name(e.keyval).to_s}
|
54
|
+
last = last_keystroke || {}
|
55
|
+
# Calculates seek time.
|
56
|
+
if(last[:time_pressed] != nil)
|
57
|
+
keystroke[:seek_time] = (keystroke[:time_pressed] - last[:time_pressed]) if last[:time_pressed] != nil
|
58
|
+
end
|
59
|
+
@keystrokes << keystroke
|
60
|
+
end
|
61
|
+
# Lets the event propagate up to the original signal handler.
|
62
|
+
false
|
63
|
+
end
|
64
|
+
|
65
|
+
widget.signal_connect("key-release-event") do |w, e|
|
66
|
+
if(e.keyval)
|
67
|
+
# Calculates hold time.
|
68
|
+
# Iterates through the array in reverse to find the last pressed, but not yet released key.
|
69
|
+
@keystrokes.reverse_each do |keystroke|
|
70
|
+
if(keystroke[:time_released] == nil)
|
71
|
+
keystroke[:time_released] = e.time
|
72
|
+
keystroke[:hold_time] = (e.time - keystroke[:time_pressed]) if keystroke[:time_pressed] != nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
# Lets the event propagate up to the original signal handler.
|
77
|
+
false
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns last recorded keystroke hash.
|
83
|
+
def last_keystroke
|
84
|
+
# Looks if we are still in the middle of our last keystroke, and returns it if necessary.
|
85
|
+
@keystrokes.reverse_each do |keystroke|
|
86
|
+
if(keystroke[:time_released] == nil)
|
87
|
+
return keystroke
|
88
|
+
end
|
89
|
+
end
|
90
|
+
# If all the logged keystrokes were released, return the last one in the array.
|
91
|
+
@keystrokes.last
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns mean seek time for all analyzed keystrokes.
|
95
|
+
# Used to display realtime statistics in the application.
|
96
|
+
def mean_seek
|
97
|
+
mean = Analysis.metric([@keystrokes])
|
98
|
+
mean_seek = 0
|
99
|
+
mean.each_pair do |idx, keystroke|
|
100
|
+
mean_seek += keystroke[:mean_seek].to_i
|
101
|
+
end
|
102
|
+
if mean.size.to_i != 0
|
103
|
+
return (mean_seek.to_i / mean.size.to_i).to_i
|
104
|
+
else
|
105
|
+
return 0
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns mean hold time for all analyzed keystrokes.
|
110
|
+
# Used to display realtime statistics in the application.
|
111
|
+
def mean_hold
|
112
|
+
mean = Analysis.metric([@keystrokes])
|
113
|
+
mean_hold = 0
|
114
|
+
mean.each_pair do |idx, keystroke|
|
115
|
+
mean_hold += keystroke[:mean_hold].to_i
|
116
|
+
end
|
117
|
+
if mean.size.to_i != 0
|
118
|
+
return (mean_hold.to_i / mean.size.to_i).to_i
|
119
|
+
else
|
120
|
+
return 0
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns mean number of keystrokes per second.
|
125
|
+
# Used to display realtime statistics in the application.
|
126
|
+
def mean_kps
|
127
|
+
last = last_keystroke || {}
|
128
|
+
first = @keystrokes.first || {}
|
129
|
+
time_in_ms = (last[:time_pressed].to_i - first[:time_pressed].to_i).to_f
|
130
|
+
if time_in_ms != 0
|
131
|
+
return (@keystrokes.size.to_f / ((time_in_ms / 1000).to_f)).to_i
|
132
|
+
else
|
133
|
+
return 0
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Returns deviation as an int betwoon 0 and 1.
|
138
|
+
# Returns 0 if the max deviation is reached or exceeded.
|
139
|
+
# Returns 0.5 if half of the max deviation is reached.
|
140
|
+
# Returns 1 if the deviation is 0, ie the hashes of keystrokes match perfectly.
|
141
|
+
def self.compare_metrics(metric_test, metric_ref)
|
142
|
+
deviation = 0
|
143
|
+
metric_test.each_pair do |idx,keystroke|
|
144
|
+
if metric_test[idx.to_sym].is_a?(Hash) && metric_ref[idx.to_sym].is_a?(Hash)
|
145
|
+
# Deviation will increase by the amount of ms seeks and holds differ from mean.
|
146
|
+
mean_seek_diff = (metric_test[idx.to_sym][:mean_seek].to_i - metric_ref[idx.to_sym][:mean_seek].to_i)
|
147
|
+
mean_hold_diff = (metric_test[idx.to_sym][:mean_hold].to_i - metric_ref[idx.to_sym][:mean_hold].to_i)
|
148
|
+
if mean_seek_diff > 0
|
149
|
+
deviation += mean_seek_diff
|
150
|
+
else
|
151
|
+
deviation -= mean_seek_diff
|
152
|
+
end
|
153
|
+
|
154
|
+
if mean_hold_diff > 0
|
155
|
+
deviation += mean_hold_diff
|
156
|
+
else
|
157
|
+
deviation -= mean_hold_diff
|
158
|
+
end
|
159
|
+
|
160
|
+
# Deviation will increase by the amount of ms seeks and holds exceed min/max holds and seeks.
|
161
|
+
if metric_test[idx.to_sym][:mean_hold] < metric_ref[idx.to_sym][:min_hold]
|
162
|
+
deviation += (metric_ref[idx.to_sym][:min_hold] - metric_test[idx.to_sym][:mean_hold]).to_i
|
163
|
+
end
|
164
|
+
if metric_test[idx.to_sym][:mean_hold] > metric_ref[idx.to_sym][:max_hold]
|
165
|
+
deviation += (metric_test[idx.to_sym][:mean_hold] - metric_ref[idx.to_sym][:max_hold]).to_i
|
166
|
+
end
|
167
|
+
if metric_test[idx.to_sym][:mean_seek] < metric_ref[idx.to_sym][:min_seek]
|
168
|
+
deviation += (metric_ref[idx.to_sym][:min_seek] - metric_test[idx.to_sym][:mean_seek]).to_i
|
169
|
+
end
|
170
|
+
if metric_test[idx.to_sym][:mean_seek] > metric_ref[idx.to_sym][:max_seek]
|
171
|
+
deviation += (metric_test[idx.to_sym][:mean_seek] - metric_ref[idx.to_sym][:max_seek]).to_i
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
if(deviation > MAX_ALLOWED_DEVIATION)
|
176
|
+
return 0
|
177
|
+
elsif((0 < deviation) && (deviation < MAX_ALLOWED_DEVIATION))
|
178
|
+
return 1-(deviation.to_f/MAX_ALLOWED_DEVIATION)
|
179
|
+
else
|
180
|
+
return 1
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Calculates mean, min and max seek/hold times per character from an array of keystroke arrays.
|
185
|
+
# Returns a hash with <tt>{character.to_sym => {:mean_seek, :mean_hold, :min_seek, :max_seek, :min_hold, :max_hold}}</tt>.
|
186
|
+
def self.metric(keystroke_array_array)
|
187
|
+
stats = self.statistics(keystroke_array_array)
|
188
|
+
metric = {}
|
189
|
+
stats.each_pair do |idx, key|
|
190
|
+
metric[idx] = {
|
191
|
+
:mean_hold => (key[:hold_total] / key[:holds]),
|
192
|
+
:mean_seek => (key[:seek_total] / key[:seeks]),
|
193
|
+
:min_seek => key[:min_seek].to_i,
|
194
|
+
:max_seek => key[:max_seek].to_i,
|
195
|
+
:min_hold => key[:min_hold].to_i,
|
196
|
+
:max_hold => key[:max_hold].to_i
|
197
|
+
}
|
198
|
+
end
|
199
|
+
metric
|
200
|
+
end
|
201
|
+
|
202
|
+
# Calculates total seeks/holds, total seeks/holds time and min/max hold/seek values for each character analyzed.
|
203
|
+
# Returns a hash with <tt>{character.to_sym => {:seek_total, :hold_total, :seeks, :holds, :character, :min_seek, :max_seek, :min_hold, :max_hold}}</tt>.
|
204
|
+
def self.statistics(keystroke_array_array)
|
205
|
+
stats = {}
|
206
|
+
keystroke_array_array.each do |keystroke_array|
|
207
|
+
keystroke_array.each do |keystroke|
|
208
|
+
key = keystroke[:character].to_sym
|
209
|
+
stats[key] = {:seek_total => 0, :hold_total => 0, :seeks => 0, :holds => 0, :character => keystroke[:character]} unless stats[key].is_a?(Hash)
|
210
|
+
stats[key][:seek_total] += keystroke[:seek_time].to_i
|
211
|
+
stats[key][:hold_total] += keystroke[:hold_time].to_i
|
212
|
+
stats[key][:seeks] += 1
|
213
|
+
stats[key][:holds] += 1
|
214
|
+
stats[key][:min_seek] = keystroke[:seek_time].to_i if stats[key][:min_seek].to_i > keystroke[:seek_time].to_i
|
215
|
+
stats[key][:max_seek] = keystroke[:seek_time].to_i if stats[key][:max_seek].to_i < keystroke[:seek_time].to_i
|
216
|
+
stats[key][:min_hold] = keystroke[:hold_time].to_i if stats[key][:min_hold].to_i > keystroke[:hold_time].to_i
|
217
|
+
stats[key][:max_hold] = keystroke[:hold_time].to_i if stats[key][:max_hold].to_i < keystroke[:hold_time].to_i
|
218
|
+
end
|
219
|
+
end
|
220
|
+
stats
|
221
|
+
end
|
222
|
+
|
223
|
+
|
224
|
+
end
|
225
|
+
|
226
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# == Summary
|
2
|
+
# The Validation class is used to:
|
3
|
+
# * validate username, password and keyboard metric
|
4
|
+
# * enroll new users with username, password and averaged keyboard metric
|
5
|
+
# * house the cryptographic functions needed for these operations
|
6
|
+
|
7
|
+
require 'fileutils'
|
8
|
+
require 'digest/sha1'
|
9
|
+
require 'openssl'
|
10
|
+
require 'base64'
|
11
|
+
|
12
|
+
# Module housing Analysis class, Validation class and associated constants
|
13
|
+
module KeystrokeDynamics
|
14
|
+
|
15
|
+
# Password hashes file.
|
16
|
+
PH_FILE = File.join(File.dirname(__FILE__), "../passwd")
|
17
|
+
|
18
|
+
# Keystrokes dynamics dir.
|
19
|
+
KSD_DIR = File.join(File.dirname(__FILE__), "../keystroke_dynamics")
|
20
|
+
|
21
|
+
# An accuracy threshold of 0 allows for maximum deviation from measured mean/min/max seek and hold times.
|
22
|
+
# An accuracy threshold of 0.5 allows for half of total deviation from measured mean/min/max seek and hold times.
|
23
|
+
# An accuracy threshold of 1 allows for 0 milliseconds of total deviation from measured mean/min/max seek and hold times.
|
24
|
+
ACCURACY_THRESHOLD = 0.5
|
25
|
+
|
26
|
+
# == Summary
|
27
|
+
# The Validation class is used to:
|
28
|
+
# * validate username, password and keyboard metric
|
29
|
+
# * enroll new users with username, password and averaged keyboard metric
|
30
|
+
# * house the cryptographic functions needed for these operations
|
31
|
+
#
|
32
|
+
# === Keying scheme
|
33
|
+
# The password file houses a salted password hash and the salt generated for each user that has enrolled. The unsalted password hash, plus the salt (which is actually an AES IV) is used for encrypting and decrypting the keyboard metrics analyzed during enrollment and validation.
|
34
|
+
#
|
35
|
+
# == Notes
|
36
|
+
# All these methods are class methods. In other languages you could call this a static class.
|
37
|
+
class Validation
|
38
|
+
|
39
|
+
# Validates login details accompanied by an array of keystroke arrays
|
40
|
+
# (Usually this would be an array with just one keystroke array, but I made it like this to be able to support more elaborate authentication mechanisms with longer texts and more input fields. it might be beneficial to have more data validated at a higher threshold to even out deviation.)
|
41
|
+
# When a user tries to validate, the user's salted password hash and iv (the salt) are loaded from the reference metric file created at enrollment. These are used to compare the user's login credentials as would normally happen. After that the metric acquired during login is compared to the reference metric for the user, which is decrypted from the disk using the unsalted password hash as a key.
|
42
|
+
# Returns a boolean:
|
43
|
+
# Returns false if a login error occurs.
|
44
|
+
# Returns true only if login information is correct and keystroke dynamics match the profile saved at enrollment within limit defined by MAX_ALLOWED_DEVIATION.
|
45
|
+
def self.validate(username, password, keystroke_array_array)
|
46
|
+
pass_hashes = self.load_pass_hashes
|
47
|
+
unless pass_hashes[username.to_sym].nil?
|
48
|
+
iv = pass_hashes[username.to_sym][:iv]
|
49
|
+
else
|
50
|
+
iv = ""
|
51
|
+
end
|
52
|
+
# Match login details in password file
|
53
|
+
if (pass_hashes[username.to_sym] || {})[:hash] == self.pass_hash(password,iv)
|
54
|
+
# Open user's reference metric
|
55
|
+
begin
|
56
|
+
mean_metric = File.open(File.join(KSD_DIR,"#{username}.met"), 'rb') do |f|
|
57
|
+
Marshal.load(self.decrypt(f.read, password, iv))
|
58
|
+
end
|
59
|
+
rescue
|
60
|
+
mean_metric = []
|
61
|
+
puts "Keystroke dynamics not registered for user #{username}"
|
62
|
+
return false
|
63
|
+
end
|
64
|
+
# Compare metrics for known characters
|
65
|
+
mean_accuracy = Analysis.compare_metrics(Analysis.metric(keystroke_array_array), mean_metric)
|
66
|
+
# ACCURACY_THRESHOLD allows weighting of the allowed deviation.
|
67
|
+
# For example, if MAX_ALLOWED_DEVIATION is 1000 ms, setting ACCURACY_THRESHOLD to 0.5 would allow deviations of no more than 500 ms.
|
68
|
+
if mean_accuracy < ACCURACY_THRESHOLD
|
69
|
+
puts "Keystroke dynamics didn't match user #{username} (measuered mean accuracy: #{mean_accuracy}, required mean accuracy: > #{ACCURACY_THRESHOLD})"
|
70
|
+
return false
|
71
|
+
else
|
72
|
+
puts "Identified user #{username} with mean accuracy of #{mean_accuracy}"
|
73
|
+
return true
|
74
|
+
end
|
75
|
+
elsif pass_hashes[username.to_sym].is_a?(Hash)
|
76
|
+
puts "Incorrect password for user #{username}"
|
77
|
+
return false
|
78
|
+
else
|
79
|
+
puts "User \"#{username}\" not enrolled"
|
80
|
+
return false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Enrolls a user.
|
85
|
+
# When a user tries to enroll and no such user exists in the password file, a new entry in the password file is created. The entry contains a salted password hash and the salt which double respectively as they key and intialization vector (IV) for cryptographic operations on the user's keystroke metric.
|
86
|
+
# An average of the supplied 2 dimensional array is calculated, which is encrypted and written to a file to be used as a reference metric.
|
87
|
+
# Returns a boolean
|
88
|
+
# Returns false if a user with the supplied username is already enrolled.
|
89
|
+
# Returns true if everything went as planned, returns false if username or password are empty or a user by the same name exists.
|
90
|
+
def self.enroll(username, password, keystroke_array_array)
|
91
|
+
pass_hashes = self.load_pass_hashes
|
92
|
+
if username == ""
|
93
|
+
puts "Username can't be blank"
|
94
|
+
return false
|
95
|
+
end
|
96
|
+
if password == ""
|
97
|
+
puts "Password can't be blank"
|
98
|
+
return false
|
99
|
+
end
|
100
|
+
if pass_hashes[username.to_sym] != nil
|
101
|
+
puts "User exists"
|
102
|
+
return false
|
103
|
+
end
|
104
|
+
iv = OpenSSL::Cipher::Cipher.new("aes-256-cbc").random_iv
|
105
|
+
pass_hashes[username.to_sym] = {:hash => self.pass_hash(password,iv), :iv => iv}
|
106
|
+
self.create_met_dir
|
107
|
+
# Saves encrypted metric to file
|
108
|
+
File.open(File.join(KSD_DIR,"#{username}.met"), 'wb') do |f|
|
109
|
+
marshal = Marshal.dump(Analysis.metric(keystroke_array_array))
|
110
|
+
f.write(self.encrypt(marshal, password, pass_hashes[username.to_sym][:iv]))
|
111
|
+
end
|
112
|
+
self.save_pass_hashes(pass_hashes)
|
113
|
+
return true
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns SHA1 hash of optionally salted string.
|
117
|
+
def self.pass_hash(pass, salt = "")
|
118
|
+
Digest::SHA1.hexdigest(pass+salt)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Returns Base64 encoded, AES 256 encrypted string using hashed key.
|
122
|
+
def self.encrypt(string, key, iv)
|
123
|
+
c = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
|
124
|
+
c.encrypt
|
125
|
+
# it is very important to not use a salted hash here, because that is written to the passwords file
|
126
|
+
c.key = self.pass_hash(key)
|
127
|
+
c.iv = iv
|
128
|
+
e = c.update(Base64.encode64(string))
|
129
|
+
e << c.final
|
130
|
+
e
|
131
|
+
end
|
132
|
+
|
133
|
+
# Returns AES 256 decrypted, Base64 decoded string using hashed key.
|
134
|
+
def self.decrypt(string, key, iv)
|
135
|
+
c = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
|
136
|
+
c.decrypt
|
137
|
+
c.key = self.pass_hash(key)
|
138
|
+
c.iv = iv
|
139
|
+
d = c.update(string)
|
140
|
+
d << c.final
|
141
|
+
Base64.decode64(d)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Loads usernames, salted password hashes and ivs.
|
145
|
+
# Returns a hash of login information loaded from disk.
|
146
|
+
# Returned hash takes the form of <tt>{username.to_sym => {:hash, :iv }</tt>.
|
147
|
+
def self.load_pass_hashes
|
148
|
+
unless File.exists?(PH_FILE)
|
149
|
+
FileUtils.touch(PH_FILE)
|
150
|
+
File.open(PH_FILE,'wb') {|f| Marshal.dump({}, f)}
|
151
|
+
end
|
152
|
+
File.open(PH_FILE, 'rb') { |f| Marshal.load(f)} || {}
|
153
|
+
end
|
154
|
+
|
155
|
+
# Writes the login information information to disk.
|
156
|
+
def self.save_pass_hashes(pass_hashes)
|
157
|
+
File.open(PH_FILE, 'wb') { |f| Marshal.dump(pass_hashes, f)}
|
158
|
+
end
|
159
|
+
|
160
|
+
# Creates a directory for encrypted metric files if it doenst exist yet.
|
161
|
+
def self.create_met_dir
|
162
|
+
FileUtils.mkdir(KSD_DIR) unless File.exists?(KSD_DIR)
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
require "keystroke_dynamics/analysis"
|
5
|
+
require "keystroke_dynamics/validation"
|
6
|
+
|
7
|
+
# == Simple keystroke dynamics analyzer/validator written in Ruby-GTK
|
8
|
+
# Copyright (c) 2008 Aram Verstegen <aram@aczid.nl>
|
9
|
+
#
|
10
|
+
# == Summary
|
11
|
+
# The three included Ruby-GTK examples demonstrate the principle of biometric authentication based on keystroke dynamics. For experimentational purposes, I have created two different examples which establish a metric from a users typing. The first, enroll_scentences.rb, lets you type in 5 pangrams (scentences that hold every letter of the alphabet) to establish your metric. The other, enroll_login.rb, lets you type your login details 10 times. I leave it as an excercise to the user to see which method works best for him or her.
|
12
|
+
# The login.rb example lets you try out your newly created username, password and keystroke metric on a login screen.
|
13
|
+
#
|
14
|
+
# === Libraries
|
15
|
+
# The logic in analyzer.rb and validation.rb can be used by other Ruby-GTK applications, as Analysis instances have methods to attach signal handlers for any Instantiable GTK widget.
|
16
|
+
#
|
17
|
+
# ==== Analysis
|
18
|
+
# This class holds logic to extract simple biometric information from GTK widgets.
|
19
|
+
#
|
20
|
+
# ==== Validation
|
21
|
+
# This class holds logic to manage user enrollment, validation and cryptographic functions.
|
22
|
+
#
|
23
|
+
# === Dependencies
|
24
|
+
# To run the Ruby-GTK examples you will need libgtk2-ruby.
|
25
|
+
# The validation functions require libopenssl-ruby.
|
26
|
+
module KeystrokeDynamics
|
27
|
+
VERSION = '0.0.1'
|
28
|
+
end
|
data/script/console
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# File: script/console
|
3
|
+
irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
|
4
|
+
|
5
|
+
libs = " -r irb/completion"
|
6
|
+
# Perhaps use a console_lib to store any extra methods I may want available in the cosole
|
7
|
+
# libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
|
8
|
+
libs << " -r #{File.dirname(__FILE__) + '/../lib/keystroke_dynamics.rb'}"
|
9
|
+
puts "Loading keystroke_dynamics gem"
|
10
|
+
exec "#{irb} #{libs} --simple-prompt"
|
data/script/destroy
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rubigen'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rubigen'
|
9
|
+
end
|
10
|
+
require 'rubigen/scripts/destroy'
|
11
|
+
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
13
|
+
RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
|
14
|
+
RubiGen::Scripts::Destroy.new.run(ARGV)
|
data/script/generate
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rubigen'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rubigen'
|
9
|
+
end
|
10
|
+
require 'rubigen/scripts/generate'
|
11
|
+
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
13
|
+
RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
|
14
|
+
RubiGen::Scripts::Generate.new.run(ARGV)
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/spec/spec_helper.rb
ADDED
data/tasks/rspec.rake
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
begin
|
2
|
+
require 'spec'
|
3
|
+
rescue LoadError
|
4
|
+
require 'rubygems'
|
5
|
+
require 'spec'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'spec/rake/spectask'
|
9
|
+
rescue LoadError
|
10
|
+
puts <<-EOS
|
11
|
+
To use rspec for testing you must install rspec gem:
|
12
|
+
gem install rspec
|
13
|
+
EOS
|
14
|
+
exit(0)
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Run the specs under spec/models"
|
18
|
+
Spec::Rake::SpecTask.new do |t|
|
19
|
+
t.spec_opts = ['--options', "spec/spec.opts"]
|
20
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ksd
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Aram Verstegen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-01-21 00:00:00 +01:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: newgem
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.2.3
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: hoe
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.8.0
|
34
|
+
version:
|
35
|
+
description: Simple keystroke dynamics analyzer/validator written in Ruby-GTK
|
36
|
+
email:
|
37
|
+
- aram@aczid.nl
|
38
|
+
executables: []
|
39
|
+
|
40
|
+
extensions: []
|
41
|
+
|
42
|
+
extra_rdoc_files:
|
43
|
+
- History.txt
|
44
|
+
- Manifest.txt
|
45
|
+
- PostInstall.txt
|
46
|
+
- README.rdoc
|
47
|
+
files:
|
48
|
+
- History.txt
|
49
|
+
- Manifest.txt
|
50
|
+
- PostInstall.txt
|
51
|
+
- README.rdoc
|
52
|
+
- Rakefile
|
53
|
+
- examples/enroll_login.rb
|
54
|
+
- examples/enroll_scentences.rb
|
55
|
+
- examples/login.rb
|
56
|
+
- lib/keystroke_dynamics.rb
|
57
|
+
- lib/keystroke_dynamics/analysis.rb
|
58
|
+
- lib/keystroke_dynamics/validation.rb
|
59
|
+
- script/console
|
60
|
+
- script/destroy
|
61
|
+
- script/generate
|
62
|
+
- spec/keystroke_dynamics_spec.rb
|
63
|
+
- spec/spec.opts
|
64
|
+
- spec/spec_helper.rb
|
65
|
+
- tasks/rspec.rake
|
66
|
+
has_rdoc: true
|
67
|
+
homepage: http://ksd.rubyforge.org/
|
68
|
+
post_install_message: PostInstall.txt
|
69
|
+
rdoc_options:
|
70
|
+
- --main
|
71
|
+
- README.rdoc
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: "0"
|
79
|
+
version:
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: "0"
|
85
|
+
version:
|
86
|
+
requirements: []
|
87
|
+
|
88
|
+
rubyforge_project: ksd
|
89
|
+
rubygems_version: 1.3.1
|
90
|
+
signing_key:
|
91
|
+
specification_version: 2
|
92
|
+
summary: Simple keystroke dynamics analyzer/validator written in Ruby-GTK
|
93
|
+
test_files: []
|
94
|
+
|