ksd 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,7 @@
1
+ == 0.0.1 2009-01-05
2
+
3
+ * 1 major enhancement:
4
+ * Made my library into a gem.
5
+ * 2 known bugs:
6
+ * Paths to analysis and password files must be set through constants before use. The default should use the current path of the requiring file.
7
+ * Needs specs!
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
@@ -0,0 +1,7 @@
1
+
2
+ For more information on keystroke_dynamics, see http://ksd.rubyforge.org
3
+
4
+ NOTE: Change this information in PostInstall.txt
5
+ You can also delete it if you don't want it.
6
+
7
+
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)
@@ -0,0 +1,11 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ # Time to add your specs!
4
+ # http://rspec.info/
5
+ describe "Place your specs here" do
6
+
7
+ it "find this spec in spec directory" do
8
+ violated "Be sure to write your specs"
9
+ end
10
+
11
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --colour
@@ -0,0 +1,10 @@
1
+ begin
2
+ require 'spec'
3
+ rescue LoadError
4
+ require 'rubygems'
5
+ gem 'rspec'
6
+ require 'spec'
7
+ end
8
+
9
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
10
+ require 'keystroke_dynamics'
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
+