Class: KeystrokeDynamics::Validation
- Inherits:
-
Object
- Object
- KeystrokeDynamics::Validation
- Defined in:
- lib/keystroke_dynamics/validation.rb
Overview
Summary
The Validation class is used to:
-
validate username, password and keyboard metric
-
enroll new users with username, password and averaged keyboard metric
-
house the cryptographic functions needed for these operations
Keying scheme
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.
Notes
All these methods are class methods. In other languages you could call this a static class.
Class Method Summary collapse
-
.create_met_dir ⇒ Object
Creates a directory for encrypted metric files if it doenst exist yet.
-
.decrypt(string, key, iv) ⇒ Object
Returns AES 256 decrypted, Base64 decoded string using hashed key.
-
.encrypt(string, key, iv) ⇒ Object
Returns Base64 encoded, AES 256 encrypted string using hashed key.
-
.enroll(username, password, keystroke_array_array) ⇒ Object
Enrolls a user.
-
.load_pass_hashes ⇒ Object
Loads usernames, salted password hashes and ivs.
-
.pass_hash(pass, salt = "") ⇒ Object
Returns SHA1 hash of optionally salted string.
-
.save_pass_hashes(pass_hashes) ⇒ Object
Writes the login information information to disk.
-
.validate(username, password, keystroke_array_array) ⇒ Object
Validates login details accompanied by an array of keystroke arrays (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.) 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.
Class Method Details
.create_met_dir ⇒ Object
Creates a directory for encrypted metric files if it doenst exist yet.
161 162 163 |
# File 'lib/keystroke_dynamics/validation.rb', line 161 def self.create_met_dir FileUtils.mkdir(KSD_DIR) unless File.exists?(KSD_DIR) end |
.decrypt(string, key, iv) ⇒ Object
Returns AES 256 decrypted, Base64 decoded string using hashed key.
134 135 136 137 138 139 140 141 142 |
# File 'lib/keystroke_dynamics/validation.rb', line 134 def self.decrypt(string, key, iv) c = OpenSSL::Cipher::Cipher.new("aes-256-cbc") c.decrypt c.key = self.pass_hash(key) c.iv = iv d = c.update(string) d << c.final Base64.decode64(d) end |
.encrypt(string, key, iv) ⇒ Object
Returns Base64 encoded, AES 256 encrypted string using hashed key.
122 123 124 125 126 127 128 129 130 131 |
# File 'lib/keystroke_dynamics/validation.rb', line 122 def self.encrypt(string, key, iv) c = OpenSSL::Cipher::Cipher.new("aes-256-cbc") c.encrypt # it is very important to not use a salted hash here, because that is written to the passwords file c.key = self.pass_hash(key) c.iv = iv e = c.update(Base64.encode64(string)) e << c.final e end |
.enroll(username, password, keystroke_array_array) ⇒ Object
Enrolls a user. 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. 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. Returns a boolean Returns false if a user with the supplied username is already enrolled. Returns true if everything went as planned, returns false if username or password are empty or a user by the same name exists.
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/keystroke_dynamics/validation.rb', line 90 def self.enroll(username, password, keystroke_array_array) pass_hashes = self.load_pass_hashes if username == "" puts "Username can't be blank" return false end if password == "" puts "Password can't be blank" return false end if pass_hashes[username.to_sym] != nil puts "User exists" return false end iv = OpenSSL::Cipher::Cipher.new("aes-256-cbc").random_iv pass_hashes[username.to_sym] = {:hash => self.pass_hash(password,iv), :iv => iv} self.create_met_dir # Saves encrypted metric to file File.open(File.join(KSD_DIR,"#{username}.met"), 'wb') do |f| marshal = Marshal.dump(Analysis.metric(keystroke_array_array)) f.write(self.encrypt(marshal, password, pass_hashes[username.to_sym][:iv])) end self.save_pass_hashes(pass_hashes) return true end |
.load_pass_hashes ⇒ Object
Loads usernames, salted password hashes and ivs. Returns a hash of login information loaded from disk. Returned hash takes the form of {username.to_sym => {:hash, :iv }
.
147 148 149 150 151 152 153 |
# File 'lib/keystroke_dynamics/validation.rb', line 147 def self.load_pass_hashes unless File.exists?(PH_FILE) FileUtils.touch(PH_FILE) File.open(PH_FILE,'wb') {|f| Marshal.dump({}, f)} end File.open(PH_FILE, 'rb') { |f| Marshal.load(f)} || {} end |
.pass_hash(pass, salt = "") ⇒ Object
Returns SHA1 hash of optionally salted string.
117 118 119 |
# File 'lib/keystroke_dynamics/validation.rb', line 117 def self.pass_hash(pass, salt = "") Digest::SHA1.hexdigest(pass+salt) end |
.save_pass_hashes(pass_hashes) ⇒ Object
Writes the login information information to disk.
156 157 158 |
# File 'lib/keystroke_dynamics/validation.rb', line 156 def self.save_pass_hashes(pass_hashes) File.open(PH_FILE, 'wb') { |f| Marshal.dump(pass_hashes, f)} end |
.validate(username, password, keystroke_array_array) ⇒ Object
Validates login details accompanied by an array of keystroke arrays (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.) 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. Returns a boolean: Returns false if a login error occurs. 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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/keystroke_dynamics/validation.rb', line 45 def self.validate(username, password, keystroke_array_array) pass_hashes = self.load_pass_hashes unless pass_hashes[username.to_sym].nil? iv = pass_hashes[username.to_sym][:iv] else iv = "" end # Match login details in password file if (pass_hashes[username.to_sym] || {})[:hash] == self.pass_hash(password,iv) # Open user's reference metric begin mean_metric = File.open(File.join(KSD_DIR,"#{username}.met"), 'rb') do |f| Marshal.load(self.decrypt(f.read, password, iv)) end rescue mean_metric = [] puts "Keystroke dynamics not registered for user #{username}" return false end # Compare metrics for known characters mean_accuracy = Analysis.compare_metrics(Analysis.metric(keystroke_array_array), mean_metric) # ACCURACY_THRESHOLD allows weighting of the allowed deviation. # For example, if MAX_ALLOWED_DEVIATION is 1000 ms, setting ACCURACY_THRESHOLD to 0.5 would allow deviations of no more than 500 ms. if mean_accuracy < ACCURACY_THRESHOLD puts "Keystroke dynamics didn't match user #{username} (measuered mean accuracy: #{mean_accuracy}, required mean accuracy: > #{ACCURACY_THRESHOLD})" return false else puts "Identified user #{username} with mean accuracy of #{mean_accuracy}" return true end elsif pass_hashes[username.to_sym].is_a?(Hash) puts "Incorrect password for user #{username}" return false else puts "User \"#{username}\" not enrolled" return false end end |