cookie_crypt 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 03a95f39e7dfef045da3c962fd52a2df5ace9e11
4
- data.tar.gz: af04600f35989fb07666ec55558406210f16612f
3
+ metadata.gz: 79b8a44522cb81c5ce3f8f162a2b5e85f2f9066b
4
+ data.tar.gz: d23f608c9c30e1fa0c4bcbcdcfef8341bd219425
5
5
  SHA512:
6
- metadata.gz: 645fa5fe1fe957cd9d4157a0ecf1978ddf0cba504a720029a332239aa9b70b454058f7a64693978d4b12a419e6e167631fb83cee8bdceb5f6ec6cc87c25626cc
7
- data.tar.gz: 938a738da2b1f352aeb1309fb6b1bb0036814dd48aa93c8999a98743aee687ac897d8b6288ef0a88a82b4d5d757cfb8573848e6e1da6fbee520de827227eb699
6
+ metadata.gz: 8b0f355e9348f6422fcf4e38699d61d5a68d99bcc75d1df2f7655745e42c6bbdcd05df941451d72064a0f8ce00473e7dafea8348a578c5e5ecc468f30c9fd590
7
+ data.tar.gz: e0c5418dc790bc69108996cce3435338e78e719c8f51e128042d02f133601a6c61f41e17423c627f1c99cdf41c8ea20039c674b717fa9269e73874a909b453c8
data/README.md CHANGED
@@ -5,9 +5,9 @@
5
5
  ## Features
6
6
 
7
7
  * User customizable security questions and answers
8
- * Configurable max login attempts
8
+ * Configurable max login attempts & cookie expiration time
9
+ * Configurable authentication styles
9
10
  * Per user level of control (Allow certain ips to bypass two-factor)
10
- * Customizable views
11
11
 
12
12
  ## Configuration
13
13
 
@@ -31,32 +31,47 @@ In order to add encrypted cookie two factor authorization to a model, run the co
31
31
  Where MODEL is your model name (e.g. User or Admin). This generator will add `:cookie_cryptable` to your model
32
32
  and create a migration in `db/migrate/`, which will add the required columns to your table.
33
33
 
34
- NOTE: This will create a field called "username" on the table it is creating if that field does not already exist.
35
- The fields are security_question_one, security_question_two, security_answer_one, security_answer_two,
36
- agent_list, and cookie_crypt_attempts_count
34
+ ### NOTE!
37
35
 
38
- Finally, run the migration with:
36
+ This will create a field called "username" on the table it is creating if that field does not already exist.
37
+ The fields are security_hash, security_cycle, agent_list, and cookie_crypt_attempts_count.
38
+
39
+ Having rails generate the files will also create views in the app/views/devise/cookie_crypt directory so you can
40
+ style your two-factor pages. If you like the default look of the views, feel free to delete these files and the gem
41
+ will serve the default ones.
42
+
43
+ Run the migration with:
39
44
 
40
45
  bundle exec rake db:migrate
41
46
 
47
+ With the 1.1 update, more steps are required. After following the above steps or upgrading from a previous version, run the command:
42
48
 
43
- ### Manual installation
49
+ bundle exec rails g cookie_crypt MODEL
44
50
 
45
- To manually enable encrypted cookie two factor authentication for the User model, you should add cookie_crypt to your devise line, like:
51
+ On your model again to generate the 1.1 cleanup and migration files. After doing this run
46
52
 
47
- ```ruby
48
- devise :database_authenticatable, :registerable,
49
- :recoverable, :rememberable, :trackable, :validatable, :cookie_cryptable
50
- ```
53
+ bundle exec rake db:migrate
51
54
 
52
- Config setup
55
+ This process will move your data (in a dev environment) from the old system to the new system.
53
56
 
54
- ```ruby
55
- config.max_login_attempts = 3
56
- config.cookie_deletion_time_frame = '30.days.from_now'
57
- ```
57
+ ### Production Updating from 1.0 to 1.1
58
+
59
+ Assuming all files are already on the production box, run
60
+
61
+ bundle exec rake db:migrate:up
58
62
 
59
- ### Customisation
63
+ To go forward only to the next migration, then run
64
+
65
+ bundle exec rails g cookie_crypt MODEL
66
+
67
+ On your model to export the security question and answer data to security_hash (nothing else will be added though it may notify you of conflicts).
68
+ Do not overwrite the conflicting migration file. Then run
69
+
70
+ bundle exec rake db:migrate:up
71
+
72
+ Again to remove the old fields.
73
+
74
+ ### Customization
60
75
 
61
76
  By default encrypted cookie two factor authentication is enabled for each user, you can change it with this method in your User model:
62
77
 
@@ -66,7 +81,9 @@ By default encrypted cookie two factor authentication is enabled for each user,
66
81
  end
67
82
  ```
68
83
 
69
- This will disable two factor authentication for local users and just puts the code in the logs.
84
+ This will disable two factor authentication for local users and just put the code in the logs.
85
+
86
+ It is recommended to take a look at the source for the views, they are not complex but the default ones may not suit your design.
70
87
 
71
88
  ### Rationalle
72
89
 
@@ -79,23 +96,23 @@ After inputting this information they are authenticated and redirected to the ro
79
96
  It is important to note that the security answers are not saved as plaintext in the database. They are encrypted and that output is matched against
80
97
  whatever the user inputs for their answers in the future.
81
98
 
82
- When the user attempts to login again, they will be displayed their two security questions and asked to answer them with their two security answers.
99
+ When the user attempts to login again, they will be shown their two security questions and asked to answer them with their two security answers.
83
100
  If successful, an auth cookie is generated on the user's machine based on the user's username and encrypted password. The cookie is username - application
84
- specific. No two cookies should ever be the same if the username field is unique. After receiving their auth cookie, the user's user agent is logged and
85
- they are sent to the root of the system as fully authenticated. If the user was unsuccessful in authenticating 3 (or more) times, they will be locked out
86
- until their cookie_crypt_attempts_count is reset to 0.
101
+ specific. No two cookies from different users should ever be the same if the username field is unique. After receiving their auth cookie, the user's user
102
+ agent is logged and they are sent to the root of the system as fully authenticated. If the user was unsuccessful in authenticating 3 (or more) times, they
103
+ will be locked out until their cookie_crypt_attempts_count is reset to 0.
87
104
 
88
- ### Two factor Defense
105
+ ### Two Factor Defense
89
106
 
90
107
  So a user now has an auth cookie and the server knows it gave an auth cookie to this user that possessed this user agent, what now? If that same user with
91
108
  that same agent tries to login again, they will authenticate through cookie crypt auth without any work on their part. The server simply matches the value
92
109
  of their cookie with what it expects it should be. If they match AND the user agent the user is using is in the list of agents allocated to that user,
93
- everything is square and they are authenticated. Using the UserAgent gem, incremental updates in a user's user agent will not be treated as differing agents.
110
+ everything is square and they are authenticated. Using the [UserAgent](https://github.com/josh/useragent) gem, incremental updates in a user's user agent will not be treated as differing agents.
94
111
  The system will log the attempt as successful and update the user's agent_list with the updated agent value.
95
112
 
96
113
  But what if they're logging in through a different machine / browser? Then they input their security answers and are given a cookie for that agent.
97
114
 
98
- But what if an attacker knows the user's username and password? The attacker must also know the user's security question answers to auth as the user.
115
+ But what if an attacker knows the user's username and password? The attacker must also know the user's security answers to auth as the user.
99
116
 
100
117
  But what if an attacker knows the user's username and password AND has a copy of the user's cookie in their browser? Cookie crypt detects this case and
101
118
  locks out the attacker by referencing the agent_list. A user that has a cookie but not a validated agent is obviously an attacker. This case also creates a
@@ -104,10 +121,41 @@ decent enough amount of data for fingerprinting. More could be done in this rega
104
121
  of this gem and would make it more difficult for the gem to be only a minor inconvienence to the users.
105
122
 
106
123
  What cookie crypt doesnt prevent:
107
- An attacker that knows a user's username and password thats logging in from the user's machine / browser
108
- An attacker that knows a user's username and password thats also spoofing the user's agent and also has the user's same auth-cookie
109
- An attacker that knows a user's username, password, security questions and answers to said questions
124
+
125
+ * An attacker that knows a user's username and password thats logging in from the user's machine / browser.
126
+ * An attacker that knows a user's username and password thats also spoofing the user's agent and also has the user's same auth-cookie.
127
+ * An attacker that knows a user's username, password, security questions and answers to said questions.
110
128
 
111
129
  Afterword: Spoofing a user agent is not that difficult, any modern browser with dev tools can change its user agent rather easily. The catch is that the values
112
130
  need to match with what the user already has which requires additional work on the attacker's part. Also, The system recognizes updates to both the user's OS AND
113
131
  browser.
132
+
133
+
134
+ ### Whats new with the 1.1 Update
135
+ * Reworked security questions and answers to allow for more customization options
136
+ * cookie_crypt_auth_through
137
+ ** :one_question_cyclical
138
+ *** The default
139
+ *** Each user must answer only one of their questions at the end of a cookie cycle to authenticate.
140
+ *** The questions are chosen cyclically, the user will not answer the same question the next time they have to auth through two-factor
141
+ *** This prevents users logging in on a new machine from always being shown the same questions and is more secure
142
+ ** :one_question_random
143
+ *** The user is shown a random question that was not their previous question every time they auth through two-factor, otherwise exactly like cyclical
144
+ ** :two_questions_cyclical
145
+ *** Exactly like one_question_cyclical except two questions must be answered every auth
146
+ ** :two_questions_random
147
+ *** Exactly like one_question_random except two questions must be answered every auth
148
+ ** :all_questions
149
+ *** This option is not advised, but is available. It is the old functionality the system had.
150
+ *** The user must answer all authentication questions every auth session
151
+ * cookie_crypt_minimum_questions
152
+ ** Default is 3
153
+ ** Minimum number of questions and answers the user must enter into the system on their initial attempt
154
+ ** Systems upgrading from 1.0 will prompt the user to add the difference in numbers of questions and answers
155
+ * cycle_question_on_fail_count
156
+ ** Default is 2
157
+ ** Minimum number of failed attempts before the question(s) is(are) cycled to the next question(s)
158
+ ** Works in conjunction with max_cookie_crypt_login_attempts
159
+ * enable_custom_question_counts
160
+ ** Default is false
161
+ ** Allows users to have more than the minimum number of security question / answer pairs.
@@ -4,6 +4,7 @@ require "logger"
4
4
  class Devise::CookieCryptController < DeviseController
5
5
  prepend_before_filter :authenticate_scope!
6
6
  before_filter :prepare_and_validate, :handle_cookie_crypt
7
+ before_filter :set_questions, :if => :show_request
7
8
 
8
9
  def show
9
10
  if has_matching_encrypted_cookie?
@@ -30,19 +31,36 @@ class Devise::CookieCryptController < DeviseController
30
31
  end
31
32
 
32
33
  def update
33
- if resource.security_question_one.blank? # initial case (first login)
34
+ h = Hash.class_eval(resource.security_hash)
35
+ if h.empty? # initial case (first login)
36
+
37
+ (1..(params[:security].keys.count/2)).each do |n|
38
+ h["security_question_#{n}"] = sanitize(params[:security]["security_question_#{n}".to_sym])
39
+ h["security_answer_#{n}"] = Digest::SHA512.hexdigest(sanitize(params[:security]["security_answer_#{n}".to_sym]))
40
+ end
41
+
42
+ resource.security_hash = h.to_s
43
+
44
+ resource.save
45
+
46
+ authentication_success
47
+ elsif (h.keys.count/2) < resource.class.cookie_crypt_minimum_questions # Need to update hash from an old install
48
+
49
+ ((h.keys.count/2)+1..(params[:security].keys.count/2)+((h.keys.count/2))).each do |n|
50
+ h["security_question_#{n}"] = sanitize(params[:security]["security_question_#{n}".to_sym])
51
+ h["security_answer_#{n}"] = Digest::SHA512.hexdigest(sanitize(params[:security]["security_answer_#{n}".to_sym]))
52
+ end
53
+
54
+ resource.security_hash = h.to_s
34
55
 
35
- resource.security_question_one = sanitize(params[:security_question_one])
36
- resource.security_question_two = sanitize(params[:security_question_two])
37
- resource.security_answer_one = Digest::SHA512.hexdigest(sanitize(params[:security_answer_one]))
38
- resource.security_answer_two = Digest::SHA512.hexdigest(sanitize(params[:security_answer_two]))
39
56
  resource.save
40
57
 
41
58
  authentication_success
42
59
  else
43
60
 
44
- if matching_answers?
61
+ if matching_answers?(h)
45
62
  generate_cookie
63
+ update_resource_cycle(h)
46
64
  log_agent_to_resource
47
65
  authentication_success
48
66
  else
@@ -50,6 +68,7 @@ class Devise::CookieCryptController < DeviseController
50
68
  resource.save
51
69
  set_flash_message :error, :attempt_failed
52
70
  if resource.max_cookie_crypt_login_attempts?
71
+ update_resource_cycle(h)
53
72
  sign_out(resource)
54
73
  render template: 'devise/cookie_crypt/max_login_attempts_reached' and return
55
74
  else
@@ -65,38 +84,6 @@ class Devise::CookieCryptController < DeviseController
65
84
  self.resource = send("current_#{resource_name}")
66
85
  end
67
86
 
68
- def encrypted_username_and_pass
69
- Digest::SHA512.hexdigest("#{resource.username}_#{resource.encrypted_password}")
70
- end
71
-
72
- def generate_cookie
73
- cookies["#{resource.username}_#{Rails.application.class.to_s.split("::").first}".to_sym] = {
74
- value: "#{encrypted_username_and_pass}",
75
- expires: Date.class_eval("#{resource.class.cookie_deletion_time_frame}")
76
- }
77
- end
78
-
79
- def has_matching_encrypted_cookie?
80
- cookies["#{resource.username}_#{Rails.application.class.to_s.split("::").first}"] == encrypted_username_and_pass
81
- end
82
-
83
- def log_hack_attempt
84
- logger = Logger.new("#{Rails.root.join('log','hack_attempts.log')}")
85
- logger.warn "Attempt to bypass two factor authentication and devise detected from ip #{request.remote_ip} using #{resource_name}: #{resource.inspect}!"
86
- end
87
-
88
- def log_agent_to_resource
89
- unless using_an_agent_that_is_already_being_used?
90
- resource.agent_list = "#{resource.agent_list}#{'|' unless resource.agent_list.blank?}#{request.user_agent}"
91
- resource.save
92
- end
93
- end
94
-
95
- def matching_answers?
96
- resource.security_answer_one == Digest::SHA512.hexdigest(sanitize(params[:security_answer_one])) &&
97
- resource.security_answer_two == Digest::SHA512.hexdigest(sanitize(params[:security_answer_two]))
98
- end
99
-
100
87
  def prepare_and_validate
101
88
  redirect_to :root and return if resource.nil?
102
89
  @limit = resource.class.max_cookie_crypt_login_attempts
@@ -106,37 +93,66 @@ class Devise::CookieCryptController < DeviseController
106
93
  end
107
94
  end
108
95
 
109
- def authentication_success
110
- flash[:notice] = 'Signed in through two-factor authentication successfully.'
111
- warden.session(resource_name)[:need_cookie_crypt_auth] = false
112
- sign_in resource_name, resource, :bypass => true
113
- resource.update_attribute(:cookie_crypt_attempts_count, 0)
114
- redirect_to stored_location_for(resource_name) || :root
115
- end
96
+ def set_questions # Options are: :one_question_cyclical, :one_question_random, :two_questions_cyclical, :two_questions_random, :all_questions
97
+ h = Hash.class_eval(resource.security_hash)
98
+ @questions = []
99
+ unless h.empty?
100
+ if resource.class.cookie_crypt_auth_through == :one_question_cyclical
101
+ set_cyclicial_cyclemod(h)
102
+ elsif resource.class.cookie_crypt_auth_through == :one_question_random
103
+ set_random_cyclemod
104
+ elsif resource.class.cookie_crypt_auth_through == :two_questions_cyclical
105
+ set_cyclicial_cyclemod(h)
106
+
107
+ if session[:cyclemod]+resource.security_cycle+1 <= h.keys.count/2
108
+ next_question_mod = session[:cyclemod]+1
109
+ else
110
+ next_question_mod = 0
111
+ end
116
112
 
117
- def using_an_agent_that_is_already_being_used?
118
- unless resource.agent_list.blank?
119
- request_agent = UserAgent.parse("#{request.user_agent}")
120
- resource.agent_list.split('|').each do |agent_string|
121
- if agent_string.include?("#{request_agent.application}")
122
- agent = UserAgent.parse("#{agent_string}")
123
- if agent.application == request_agent.application && agent.browser == request_agent.browser
124
- if request_agent >= agent #version number is higher for example
125
- #update user agent string and return true
126
- resource.agent_list = resource.agent_list.gsub("#{agent.browser}/#{agent.version}","#{request_agent.browser}/#{request_agent.version}")
127
- resource.save
128
- return true
129
- elsif request_agent.version == agent.version
130
- return true
131
- end
113
+ @questions << h["security_question_#{next_question_mod}"]
114
+ elsif resource.class.cookie_crypt_auth_through == :two_questions_random
115
+ if resource.cookie_crypt_attempts_count == 0
116
+ session[:cyclemod] = Random.rand(0..(h.keys.count/2))
117
+ r = Random.rand(0..(h.keys.count/2))
118
+ while session[:cyclemod] != r && resource.security_cycle != r
119
+ r = Random.rand(0..(h.keys.count/2))
120
+ end
121
+ session[:cyclemod2] = r
122
+ elsif resource.cookie_crypt_attempts_count != 0 && resource.cookie_crypt_attempts_count%resource.class.cycle_question_on_fail_count == 0
123
+ r = Random.rand(0..(h.keys.count/2))
124
+ while session[:cyclemod] != r && resource.security_cycle != r
125
+ r = Random.rand(0..(h.keys.count/2))
132
126
  end
127
+ session[:cyclemod] = r
128
+
129
+ r = Random.rand(0..(h.keys.count/2))
130
+ while session[:cyclemod] != r && resource.security_cycle != r
131
+ r = Random.rand(0..(h.keys.count/2))
132
+ end
133
+ session[:cyclemod2] = r
134
+ end
135
+
136
+ @questions << h["security_question_#{session[:cyclemod2]}"]
137
+ else #:all_questions case
138
+ h.keys.delete_if{|x| x.include?("answer")}.each do |key|
139
+ @questions << h[key]
140
+ end
141
+ end
142
+
143
+
144
+ unless resource.class.cookie_crypt_auth_through == :all_questions
145
+ if resource.class.cookie_crypt_auth_through == :one_question_cyclical ||
146
+ resource.class.cookie_crypt_auth_through == :two_questions_cyclical
147
+ @questions << h["security_question_#{resource.security_cycle+session[:cyclemod]}"]
148
+ else #random cyclemod case
149
+ @questions << h["security_question_#{session[:cyclemod]}"]
133
150
  end
134
151
  end
135
152
  end
136
- false
137
153
  end
138
154
 
139
- def unrecognized_agent?
140
- resource.agent_list.include?("#{request.user_agent}")
155
+ def show_request
156
+ action_name == "show"
141
157
  end
142
158
  end
@@ -0,0 +1,14 @@
1
+ <div id="security_binder_<%= session[:cookie_crypt_questions_count]%>">
2
+ <h3>Please input a security question</h3>
3
+
4
+ <%=text_field_tag "security_question_#{session[:cookie_crypt_questions_count]}", nil, size: 50, name: "security[security_question_#{session[:cookie_crypt_questions_count]}]" %>
5
+ <br></br>
6
+
7
+ <h3>Please input a security answer</h3>
8
+
9
+ <%=text_field_tag "security_answer_#{session[:cookie_crypt_questions_count]}", nil, size: 50, name: "security[security_answer_#{session[:cookie_crypt_questions_count]}]" %>
10
+ <br></br>
11
+
12
+ <%= link_to "Remove this question / answer pair?", "#{request.fullpath}?remove=#{session[:cookie_crypt_questions_count]}", remote: true %>
13
+ <br></br>
14
+ </div>
@@ -2,46 +2,84 @@
2
2
  <h2>Two Factor Login Page</h2>
3
3
 
4
4
  <%=form_tag([resource_name, :cookie_crypt], method: :put) do %>
5
- <% if @user.security_question_one.blank? %>
5
+ <% security_hash = Hash.class_eval(@user.security_hash) %>
6
+ <% if security_hash.empty? %>
6
7
  <h2>You have not yet setup two-factor questions and answers. Please follow the instructions below.</h2>
7
8
 
8
9
  <h2>Note: It is a generally a good idea to have your questions be about people/events/objects that do not change over time.</h2>
9
10
 
10
- <h3>Please input your first security question</h3>
11
+ <% (1..@user.class.cookie_crypt_minimum_questions).each do |n| %>
12
+ <h3>Please input a security question</h3>
11
13
 
12
- <%=text_field_tag :security_question_one, nil, size: 50 %>
13
- <br></br>
14
+ <%=text_field_tag "security_question_#{n}", nil, size: 50, name: "security[security_question_#{n}]" %>
15
+ <br></br>
14
16
 
15
- <h3>Please input your first question's answer</h3>
17
+ <h3>Please input a security answer</h3>
16
18
 
17
- <%=text_field_tag :security_answer_one, nil, size: 50 %>
18
- <br></br>
19
+ <%=text_field_tag "security_answer_#{n}", nil, size: 50, name: "security[security_answer_#{n}]" %>
20
+ <br></br>
21
+
22
+ <% end %>
23
+
24
+ <% if @user.class.enable_custom_question_counts %>
25
+ <div id="cookie_crypt_additions_binder"></div>
26
+ <%= link_to "Add more security questions and answers?", "#{request.fullpath}", remote: true %>
27
+ <br></br>
28
+ <% end %>
19
29
 
20
- <h3>Please input your second security question </h3>
30
+ <h2>Please note that you will not be given a security token to login by skipping two factor until you login next time.</h2>
21
31
 
22
- <%=text_field_tag :security_question_two, nil, size: 50 %>
23
- <br></br>
32
+ <% elsif (security_hash.keys.count/2) < @user.class.cookie_crypt_minimum_questions %>
33
+ <h2>There has been a change with the system that requires you to input more security questions and answers. Please follow the instructions below.</h2>
24
34
 
25
- <h3>Please input your second question's answer</h3>
35
+ <h3 class="centered">Your current questions are
26
36
 
27
- <%=text_field_tag :security_answer_two, nil, size: 50 %>
37
+ <% h = Hash.class_eval(@user.security_hash) %>
38
+ <% h.keys.delete_if{|x| x.include?("answer")}.each do |key| %>
39
+
40
+ <h3><%="#{h[key]}"%></h2>
41
+
42
+ <% end %>
28
43
  <br></br>
29
-
30
- <h2>Please note that you will not be given a security token to login by skipping two factor until you login next time.</h2>
31
44
 
32
- <% else %>
45
+ <% (1..@user.class.cookie_crypt_minimum_questions).each do |n| %>
33
46
 
34
- <h2><%="#{@user.security_question_one}"%></h2>
47
+ <% next if security_hash["security_question_#{n}"] %>
35
48
 
36
- <%=text_field_tag :security_answer_one, nil, size: 50 %>
49
+ <h3>Please input a security question</h3>
37
50
 
38
- <br></br>
51
+ <%=text_field_tag "security_question_#{n}", nil, size: 50, name: "security[security_question_#{n}]" %>
52
+ <br></br>
53
+
54
+ <h3>Please input a security answer</h3>
55
+
56
+ <%=text_field_tag "security_answer_#{n}", nil, size: 50, name: "security[security_answer_#{n}]" %>
57
+ <br></br>
39
58
 
40
- <h2><%="#{@user.security_question_two}" %></h2>
59
+ <% end %>
41
60
 
42
- <%=text_field_tag :security_answer_two, nil, size: 50 %>
61
+ <% if @user.class.enable_custom_question_counts %>
62
+ <div id="cookie_crypt_additions_binder"></div>
63
+ <%= link_to "Add more security questions and answers?", "#{request.fullpath}", remote: true %>
64
+ <br></br>
65
+ <% end %>
43
66
 
67
+ <h2>You will be authenticated for this session but not for your next login. You will need to enter your security answers next time.</h2>
44
68
  <br></br>
69
+ <% else %>
70
+
71
+ <% h = Hash.class_eval(@user.security_hash) %>
72
+
73
+ <% @questions.each do |q| %>
74
+
75
+ <h2><%="#{q}"%></h2>
76
+
77
+ <%=text_field_tag h.key(q).gsub('question','answer'), nil, size: 50, name: "security_answers[#{h.key(q).gsub('question','answer')}]" %>
78
+
79
+ <br></br>
80
+
81
+ <% end %>
82
+
45
83
  <% end %>
46
84
 
47
85
  <%= submit_tag "Submit" %>
@@ -0,0 +1,8 @@
1
+ <% if params[:remove] %>
2
+ $('#security_binder_<%= params[:remove] %>').remove();
3
+ <% else %>
4
+ <% session[:cookie_crypt_questions_count] ||= @user.class.cookie_crypt_minimum_questions %>
5
+ <% session[:cookie_crypt_questions_count] += 1 %>
6
+
7
+ $('#cookie_crypt_additions_binder').append('<%= escape_javascript(render partial: "extra_fields")%>');
8
+ <% end %>
data/cookie_crypt.gemspec CHANGED
@@ -12,8 +12,8 @@ Gem::Specification.new do |s|
12
12
  s.description = <<-EOF
13
13
  ### Features ###
14
14
  * User customizable security questions and answers
15
- * Configurable max login attempts
16
- * per user level control if he really need two factor authentication
15
+ * Configurable max login attempts & cookie expiration time
16
+ * Per user level of control (Allow certain ips to bypass two-factor)
17
17
  EOF
18
18
 
19
19
  s.rubyforge_project = "cookie_crypt"
@@ -29,4 +29,19 @@ Gem::Specification.new do |s|
29
29
 
30
30
  s.add_development_dependency 'bundler'
31
31
  s.license = 'MIT'
32
+ s.post_install_message = <<END_DESC
33
+
34
+ ********************************************
35
+
36
+ A major revision was made with the 1.1 update for CookieCrypt.
37
+
38
+ You will need to run 'bundle exec rails g cookie_crypt MODEL' again
39
+
40
+ to start the upgrade process from 1.0 to 1.1.
41
+
42
+ For more information check the homepage at 'https://github.com/loualrid/CookieCrypt'
43
+
44
+ ********************************************
45
+
46
+ END_DESC
32
47
  end
data/lib/cookie_crypt.rb CHANGED
@@ -4,9 +4,13 @@ require 'digest'
4
4
  require 'active_support/concern'
5
5
 
6
6
  module Devise
7
- mattr_accessor :max_cookie_crypt_login_attempts, :cookie_deletion_time_frame
7
+ mattr_accessor :max_cookie_crypt_login_attempts, :cookie_deletion_time_frame, :cookie_crypt_auth_through, :cookie_crypt_minimum_questions, :cycle_question_on_fail_count, :enable_custom_question_counts
8
8
  @@max_cookie_crypt_login_attempts = 3
9
- @@cookie_deletion_time_frame = 30.days.from_now
9
+ @@cookie_deletion_time_frame = '30.days.from_now'
10
+ @@cookie_crypt_auth_through = :one_question_cyclical
11
+ @@cookie_crypt_minimum_questions = 3
12
+ @@cycle_question_on_fail_count = 2
13
+ @@enable_custom_question_counts = false
10
14
  end
11
15
 
12
16
  module CookieCrypt
@@ -9,6 +9,31 @@ module CookieCrypt
9
9
 
10
10
  private
11
11
 
12
+ def authentication_success
13
+ flash[:notice] = 'Signed in through two-factor authentication successfully.'
14
+ warden.session(resource_name)[:need_cookie_crypt_auth] = false
15
+ sign_in resource_name, resource, :bypass => true
16
+ resource.update_attribute(:cookie_crypt_attempts_count, 0)
17
+ redirect_to stored_location_for(resource_name) || :root
18
+ end
19
+
20
+ def cookie_crypt_auth_path_for(resource_or_scope = nil)
21
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
22
+ change_path = "#{scope}_cookie_crypt_path"
23
+ send(change_path)
24
+ end
25
+
26
+ def encrypted_username_and_pass
27
+ Digest::SHA512.hexdigest("#{resource.username}_#{resource.encrypted_password}")
28
+ end
29
+
30
+ def generate_cookie
31
+ cookies["#{resource.username}_#{Rails.application.class.to_s.split("::").first}".to_sym] = {
32
+ value: "#{encrypted_username_and_pass}",
33
+ expires: Date.class_eval("#{resource.class.cookie_deletion_time_frame}")
34
+ }
35
+ end
36
+
12
37
  def handle_cookie_crypt
13
38
  unless devise_controller?
14
39
  Devise.mappings.keys.flatten.any? do |scope|
@@ -28,12 +53,121 @@ module CookieCrypt
28
53
  end
29
54
  end
30
55
 
31
- def cookie_crypt_auth_path_for(resource_or_scope = nil)
32
- scope = Devise::Mapping.find_scope!(resource_or_scope)
33
- change_path = "#{scope}_cookie_crypt_path"
34
- send(change_path)
56
+ def has_matching_encrypted_cookie?
57
+ cookies["#{resource.username}_#{Rails.application.class.to_s.split("::").first}"] == encrypted_username_and_pass
58
+ end
59
+
60
+ def log_hack_attempt
61
+ logger = Logger.new("#{Rails.root.join('log','hack_attempts.log')}")
62
+ logger.warn "Attempt to bypass two factor authentication and devise detected from ip #{request.remote_ip} using #{resource_name}: #{resource.inspect}!"
63
+ end
64
+
65
+ def log_agent_to_resource
66
+ unless using_an_agent_that_is_already_being_used?
67
+ resource.agent_list = "#{resource.agent_list}#{'|' unless resource.agent_list.blank?}#{request.user_agent}"
68
+ resource.save
69
+ end
70
+ end
71
+
72
+ def matching_answers? hash
73
+ answers = []
74
+ unless resource.class.cookie_crypt_auth_through == :all_questions
75
+ if resource.class.cookie_crypt_auth_through == :one_question_cyclical ||
76
+ resource.class.cookie_crypt_auth_through == :two_questions_cyclical
77
+ answers << h["security_answer_#{resource.security_cycle+session[:cyclemod]}"]
78
+ else #random cyclemod case
79
+ answers << h["security_answer_#{session[:cyclemod]}"]
80
+ end
81
+ end
82
+
83
+ if resource.class.cookie_crypt_auth_through == :two_questions_cyclical
84
+ if session[:cyclemod]+resource.security_cycle+1 <= hash.keys.count/2
85
+ next_question_mod = session[:cyclemod]+1
86
+ else
87
+ next_question_mod = 0
88
+ end
89
+
90
+ answers << "security_answer_#{next_question_mod}"
91
+ elsif resource.class.cookie_crypt_auth_through == :two_questions_random
92
+ answers << "security_answer_#{session[:cyclemod2]}"
93
+ elsif resource.class.cookie_crypt_auth_through == :all_questions
94
+ hash.keys.delete_if{|x| x.include?("question")}.each do |key|
95
+ answers << key
96
+ end
97
+ end
98
+
99
+ authed = false
100
+ a_arr = []
101
+ answers.sort.each do |key|
102
+ if hash[key] == Digest::SHA512.hexdigest(sanitize(params[:security_answers][key]))
103
+ a_arr[answers.index(key)] = true
104
+ else
105
+ a_arr[answers.index(key)] = false
106
+ end
107
+ end
108
+
109
+ authed = true unless q_arr.include?(false)
110
+ authed
35
111
  end
36
112
 
113
+ def set_cyclicial_cyclemod hash
114
+ if resource.cookie_crypt_attempts_count == 0
115
+ session[:cyclemod] = 0
116
+ elsif resource.cookie_crypt_attempts_count != 0 && resource.cookie_crypt_attempts_count%resource.class.cycle_question_on_fail_count == 0
117
+ session[:cyclemod] += 1
118
+ end
119
+
120
+ session[:cyclemod] = 0 if session[:cyclemod]+resource.security_cycle > hash.keys.count/2
121
+ end
122
+
123
+ def set_random_cyclemod hash
124
+ if resource.cookie_crypt_attempts_count == 0
125
+ session[:cyclemod] = Random.rand(0..(hash.keys.count/2))
126
+ elsif resource.cookie_crypt_attempts_count != 0 && resource.cookie_crypt_attempts_count%resource.class.cycle_question_on_fail_count == 0
127
+ r = Random.rand(0..(hash.keys.count/2))
128
+ while session[:cyclemod] != r && resource.security_cycle != r
129
+ r = Random.rand(0..(hash.keys.count/2))
130
+ end
131
+ session[:cyclemod] = r
132
+
133
+ end
134
+ end
135
+
136
+ def update_resource_cycle hash
137
+ if resource.security_cycle+1 > hash.keys.count/2
138
+ resource.security_cycle = 1
139
+ else
140
+ resource.security_cycle += 1
141
+ end
142
+
143
+ resource.save
144
+ end
145
+
146
+ def using_an_agent_that_is_already_being_used?
147
+ unless resource.agent_list.blank?
148
+ request_agent = UserAgent.parse("#{request.user_agent}")
149
+ resource.agent_list.split('|').each do |agent_string|
150
+ if agent_string.include?("#{request_agent.application}")
151
+ agent = UserAgent.parse("#{agent_string}")
152
+ if agent.application == request_agent.application && agent.browser == request_agent.browser
153
+ if request_agent >= agent #version number is higher for example
154
+ #update user agent string and return true
155
+ resource.agent_list = resource.agent_list.gsub("#{agent.browser}/#{agent.version}","#{request_agent.browser}/#{request_agent.version}")
156
+ resource.save
157
+ return true
158
+ elsif request_agent.version == agent.version
159
+ return true
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
165
+ false
166
+ end
167
+
168
+ def unrecognized_agent?
169
+ resource.agent_list.include?("#{request.user_agent}")
170
+ end
37
171
  end
38
172
  end
39
173
  end
@@ -5,7 +5,7 @@ module Devise
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  module ClassMethods
8
- ::Devise::Models.config(self, :max_cookie_crypt_login_attempts, :cookie_deletion_time_frame)
8
+ ::Devise::Models.config(self, :max_cookie_crypt_login_attempts, :cookie_deletion_time_frame, :cookie_crypt_auth_through, :cookie_crypt_minimum_questions, :cycle_question_on_fail_count, :enable_custom_question_counts)
9
9
  end
10
10
 
11
11
  def need_cookie_crypt_auth?(request)
@@ -1,3 +1,3 @@
1
1
  module CookieCrypt
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -5,10 +5,23 @@ module ActiveRecord
5
5
  class CookieCryptGenerator < ActiveRecord::Generators::Base
6
6
  source_root File.expand_path("../templates", __FILE__)
7
7
 
8
- def copy_cookie_crypt_migration
9
- migration_template "migration.rb", "db/migrate/cookie_crypt_add_to_#{table_name}"
8
+ def copy_cookie_crypt_migration_1_0
9
+ if ActiveRecord::Base.class_eval("#{table_name.camelize.singularize}.inspect['security_question_one: string'].blank?")
10
+ migration_template "migration.rb", "db/migrate/cookie_crypt_add_to_#{table_name}"
11
+ end
10
12
  end
11
13
 
14
+ def copy_cookie_crypt_migration_1_1
15
+ if ActiveRecord::Base.class_eval("#{table_name.camelize.singularize}.inspect['security_hash: text'].blank?")
16
+ migration_template "migration_1_1.rb", "db/migrate/cookie_crypt_1_1_update_to_#{table_name}"
17
+ end
18
+ end
19
+
20
+ def copy_cookie_crypt_migration_1_1_cleanup
21
+ if $generate_1_1_cleanup_migration
22
+ migration_template "migration_1_1_cleanup.rb", "db/migrate/cookie_crypt_1_1_cleanup_to_#{table_name}"
23
+ end
24
+ end
12
25
  end
13
26
  end
14
27
  end
@@ -0,0 +1,8 @@
1
+ class CookieCrypt11UpdateTo<%= table_name.camelize %> < ActiveRecord::Migration
2
+ def change
3
+ change_table :<%= table_name %> do |t|
4
+ t.text :security_hash, default: '{}'
5
+ t.integer :security_cycle, default: 1
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ class CookieCrypt11CleanupTo<%= table_name.camelize %> < ActiveRecord::Migration
2
+ def change
3
+ remove_column :<%= table_name %>, :security_question_one
4
+ remove_column :<%= table_name %>, :security_question_two
5
+ remove_column :<%= table_name %>, :security_answer_one
6
+ remove_column :<%= table_name %>, :security_answer_two
7
+ end
8
+ end
@@ -2,30 +2,98 @@ module CookieCryptable
2
2
  module Generators
3
3
  class CookieCryptGenerator < Rails::Generators::NamedBase
4
4
  namespace "cookie_crypt"
5
- desc "Adds :cookie_cryptable directive in the given model.
6
- It also generates an active record migration."
7
-
8
- def inject_cookie_crypt_content
9
- paths = [File.join("app", "models", "#{file_path}.rb"),File.join("config", "initializers", "devise.rb")]
10
- inject_into_file(paths[0], "cookie_cryptable, :", :after => "devise :") if File.exists?(paths[0])
11
- if File.exists?(paths[1])
12
- inject_into_file(paths[1], "\n # ==> Cookie Crypt Configuration Parameters\n config.max_cookie_crypt_login_attempts = 3
13
- \n # For cookie_deletion_time_frame field, make sure your timeframe parses into an actual date and is a string
14
- \n config.cookie_deletion_time_frame = '30.days.from_now'", after: "Devise.setup do |config|")
5
+ desc "Automates setup process, will also update between versions."
6
+
7
+ #BEGIN 1.0 generator
8
+ def inject_1_0_cookie_crypt_content
9
+ if ActiveRecord::Base.class_eval("#{table_name.camelize.singularize}.inspect['security_question_one: string'].blank?")
10
+ puts "Beginning 1.0 content injection..."
11
+ paths = [File.join("app", "models", "#{file_path}.rb"),File.join("config", "initializers", "devise.rb")]
12
+ inject_into_file(paths[0], "cookie_cryptable, :", :after => "devise :") if File.exists?(paths[0])
13
+ if File.exists?(paths[1])
14
+ inject_into_file(paths[1], "\n # ==> Cookie Crypt Configuration Parameters\n config.max_cookie_crypt_login_attempts = 3
15
+ \n # For cookie_deletion_time_frame field, make sure your timeframe parses into an actual date and is a string
16
+ \n config.cookie_deletion_time_frame = '30.days.from_now'", after: "Devise.setup do |config|")
17
+ end
15
18
  end
16
19
  end
17
20
 
18
21
  source_root File.expand_path('../../../../app/views/devise/cookie_crypt', __FILE__)
19
22
 
20
- def generate_files
23
+ def generate_1_0_files
21
24
  Dir.mkdir("app/views/devise") unless Dir.exists?("app/views/devise")
22
25
  unless Dir.exists?("app/views/devise/cookie_crypt")
26
+ puts "Beginning 1.0 views creation..."
23
27
  Dir.mkdir("app/views/devise/cookie_crypt")
24
28
  copy_file "max_login_attempts_reached.html.erb", "app/views/devise/cookie_crypt/max_login_attempts_reached.html.erb"
25
29
  copy_file "show.html.erb", "app/views/devise/cookie_crypt/show.html.erb"
26
30
  end
27
31
  end
28
32
 
33
+ source_root File.expand_path(__FILE__)
34
+
35
+ #BEGIN 1.1 generator
36
+ def inject_1_1_cookie_crypt_content
37
+ if ActiveRecord::Base.class_eval("#{table_name.camelize.singularize}.inspect['security_hash: text'].blank?")
38
+ puts "Beginning 1.1 content injection..."
39
+ paths = [File.join("app", "models", "#{file_path}.rb"),File.join("config", "initializers", "devise.rb")]
40
+ if File.exists?(paths[1])
41
+ inject_into_file(paths[1], "\n # cookie_crypt_auth_through manages the various styles of authenticating through two factor when the need arises.
42
+ \n # Valid options are: :one_question_cyclical, :one_question_random, :two_questions_cyclical, :two_questions_random, :all_questions
43
+ \n config.cookie_crypt_auth_through = :one_question_cyclical
44
+ \n # cookie_crypt_minimum_questions determines how many questions and answers the user must create the first time they are auth'ing through CC
45
+ \n # This option must be greater than or equal to 2.
46
+ \n config.cookie_crypt_minimum_questions = 3
47
+ \n # cycle_question_on_fail_count determines how many tries the user gets per question(s) before the system changes the questions shown
48
+ \n # It is recommended to set this to at least 2, but 1 is allowed. This value is ignored if the system is set to :all_questions
49
+ \n config.cycle_question_on_fail_count = 2
50
+ \n # enable_custom_question_counts allows users to have *more* than the minimum number of questions. This works via ajax and javascript.
51
+ \n config.enable_custom_question_counts = false", after: " # ==> Cookie Crypt Configuration Parameters")
52
+ end
53
+ end
54
+ end
55
+
56
+ source_root File.expand_path('../../../../app/views/devise/cookie_crypt', __FILE__)
57
+
58
+ def generate_1_1_files
59
+ unless File.exist?("app/views/devise/cookie_crypt/show.js.erb")
60
+ puts "Beginning 1.1 views creation..."
61
+ copy_file "show.js.erb", "app/views/devise/cookie_crypt/show.js.erb"
62
+ copy_file "_extra_fields.html.erb", "app/views/devise/cookie_crypt/_extra_fields.html.erb"
63
+ File.delete("app/views/devise/cookie_crypt/show.html.erb")
64
+ copy_file "show.html.erb", "app/views/devise/cookie_crypt/show.html.erb"
65
+
66
+ puts "Please run rake db:migrate then run this generator again to cleanup unused fields."
67
+ end
68
+ end
69
+
70
+ def generate_1_1_update
71
+ unless ActiveRecord::Base.class_eval("#{table_name.camelize.singularize}.inspect['security_hash: text'].blank?")
72
+ unless ActiveRecord::Base.class_eval("#{table_name.camelize.singularize}.inspect['security_question_one: string'].blank?")
73
+ puts "Beginning data cleanup, moving 1.0 database data to 1.1 database style..."
74
+ objs = ActiveRecord::Base.class_eval("#{table_name.camelize.singularize}.all")
75
+ objs.each do |obj|
76
+ next if obj.security_question_one.blank?
77
+ h = {}
78
+ h["security_question_1"] = obj.security_question_one
79
+ h["security_answer_1"] = obj.security_answer_one
80
+ h["security_question_2"] = obj.security_question_two
81
+ h["security_answer_2"] = obj.security_answer_two
82
+ obj.security_hash = h.to_s
83
+
84
+ obj.save
85
+
86
+ puts "#{obj.security_hash}"
87
+ end
88
+
89
+ puts "Completed data cleanup, database is now 1.1 ready."
90
+ puts "Generating cleanup migration that will remove now unneeded security_question_one, security_answer_one, security_question_two, security_answer_two fields."
91
+
92
+ $generate_1_1_cleanup_migration = true
93
+ end
94
+ end
95
+ end
96
+
29
97
  hook_for :orm
30
98
  end
31
99
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cookie_crypt
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitrii Golub
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-10-22 00:00:00.000000000 Z
12
+ date: 2013-11-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -70,8 +70,8 @@ dependencies:
70
70
  description: |2
71
71
  ### Features ###
72
72
  * User customizable security questions and answers
73
- * Configurable max login attempts
74
- * per user level control if he really need two factor authentication
73
+ * Configurable max login attempts & cookie expiration time
74
+ * Per user level of control (Allow certain ips to bypass two-factor)
75
75
  email:
76
76
  - loualrid@gmail.com
77
77
  executables: []
@@ -84,8 +84,10 @@ files:
84
84
  - README.md
85
85
  - Rakefile
86
86
  - app/controllers/devise/cookie_crypt_controller.rb
87
+ - app/views/devise/cookie_crypt/_extra_fields.html.erb
87
88
  - app/views/devise/cookie_crypt/max_login_attempts_reached.html.erb
88
89
  - app/views/devise/cookie_crypt/show.html.erb
90
+ - app/views/devise/cookie_crypt/show.js.erb
89
91
  - config/locales/en.yml
90
92
  - cookie_crypt.gemspec
91
93
  - lib/cookie_crypt.rb
@@ -99,12 +101,27 @@ files:
99
101
  - lib/cookie_crypt/version.rb
100
102
  - lib/generators/active_record/cookie_crypt_generator.rb
101
103
  - lib/generators/active_record/templates/migration.rb
104
+ - lib/generators/active_record/templates/migration_1_1.rb
105
+ - lib/generators/active_record/templates/migration_1_1_cleanup.rb
102
106
  - lib/generators/cookie_crypt/cookie_crypt_generator.rb
103
107
  homepage: https://github.com/loualrid/CookieCrypt
104
108
  licenses:
105
109
  - MIT
106
110
  metadata: {}
107
- post_install_message:
111
+ post_install_message: |2+
112
+
113
+ ********************************************
114
+
115
+ A major revision was made with the 1.1 update for CookieCrypt.
116
+
117
+ You will need to run 'bundle exec rails g cookie_crypt MODEL' again
118
+
119
+ to start the upgrade process from 1.0 to 1.1.
120
+
121
+ For more information check the homepage at 'https://github.com/loualrid/CookieCrypt'
122
+
123
+ ********************************************
124
+
108
125
  rdoc_options: []
109
126
  require_paths:
110
127
  - lib