door_mat 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (176) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +3 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +88 -0
  6. data/Rakefile +32 -0
  7. data/app/assets/javascripts/door_mat/application.js +13 -0
  8. data/app/assets/stylesheets/door_mat/application.css +15 -0
  9. data/app/assets/stylesheets/scaffold.css +56 -0
  10. data/app/controllers/door_mat/activities_controller.rb +106 -0
  11. data/app/controllers/door_mat/application_controller.rb +14 -0
  12. data/app/controllers/door_mat/change_password_controller.rb +32 -0
  13. data/app/controllers/door_mat/forgot_passwords_controller.rb +57 -0
  14. data/app/controllers/door_mat/manage_email_controller.rb +61 -0
  15. data/app/controllers/door_mat/password_less_session_controller.rb +121 -0
  16. data/app/controllers/door_mat/reconfirm_password_controller.rb +27 -0
  17. data/app/controllers/door_mat/sessions_controller.rb +17 -0
  18. data/app/controllers/door_mat/sign_in_controller.rb +60 -0
  19. data/app/controllers/door_mat/sign_up_controller.rb +59 -0
  20. data/app/controllers/door_mat/static_controller.rb +5 -0
  21. data/app/mailers/door_mat/activity_mailer.rb +18 -0
  22. data/app/mailers/door_mat/password_less_session_mailer.rb +12 -0
  23. data/app/models/door_mat/access_token.rb +315 -0
  24. data/app/models/door_mat/activity.rb +14 -0
  25. data/app/models/door_mat/activity_confirm_email.rb +45 -0
  26. data/app/models/door_mat/activity_download_recovery_key.rb +30 -0
  27. data/app/models/door_mat/activity_reset_password.rb +47 -0
  28. data/app/models/door_mat/actor.rb +149 -0
  29. data/app/models/door_mat/change_password.rb +12 -0
  30. data/app/models/door_mat/email.rb +58 -0
  31. data/app/models/door_mat/forgot_password.rb +12 -0
  32. data/app/models/door_mat/membership.rb +42 -0
  33. data/app/models/door_mat/session.rb +315 -0
  34. data/app/models/door_mat/sign_in.rb +31 -0
  35. data/app/models/door_mat/sign_up.rb +17 -0
  36. data/app/views/door_mat/activity_mailer/confirm_email.html.erb +11 -0
  37. data/app/views/door_mat/activity_mailer/confirm_email.text.erb +7 -0
  38. data/app/views/door_mat/activity_mailer/reset_password.html.erb +11 -0
  39. data/app/views/door_mat/activity_mailer/reset_password.text.erb +7 -0
  40. data/app/views/door_mat/change_password/new.html.erb +22 -0
  41. data/app/views/door_mat/forgot_passwords/choose_new_password.html.erb +34 -0
  42. data/app/views/door_mat/forgot_passwords/new.html.erb +14 -0
  43. data/app/views/door_mat/helpers/_errors_if_any.html.erb +10 -0
  44. data/app/views/door_mat/manage_email/new.html.erb +14 -0
  45. data/app/views/door_mat/password_less_session/access_token.html.erb +16 -0
  46. data/app/views/door_mat/password_less_session/new.html.erb +34 -0
  47. data/app/views/door_mat/password_less_session_mailer/send_token.html.erb +11 -0
  48. data/app/views/door_mat/password_less_session_mailer/send_token.text.erb +7 -0
  49. data/app/views/door_mat/reconfirm_password/new.html.erb +12 -0
  50. data/app/views/door_mat/sign_in/new.html.erb +30 -0
  51. data/app/views/door_mat/sign_up/new.html.erb +24 -0
  52. data/app/views/door_mat/static/add_email_success.html.erb +5 -0
  53. data/app/views/door_mat/static/change_password_success.html.erb +2 -0
  54. data/app/views/door_mat/static/confirm_email_success.html.erb +2 -0
  55. data/app/views/door_mat/static/email_confirmation_required.html.erb +17 -0
  56. data/app/views/door_mat/static/forgot_password_verification_mail_sent.html.erb +2 -0
  57. data/app/views/door_mat/static/reconfirm_password_success.html.erb +4 -0
  58. data/app/views/door_mat/static/sign_in_success.html.erb +5 -0
  59. data/app/views/door_mat/static/sign_out_success.html.erb +5 -0
  60. data/app/views/door_mat/static/sign_up_success.html.erb +4 -0
  61. data/bin/rails +12 -0
  62. data/config/locales/en.yml +73 -0
  63. data/config/routes.rb +48 -0
  64. data/db/migrate/20140616234935_create_door_mat_actors.rb +23 -0
  65. data/db/migrate/20140617233357_create_door_mat_sessions.rb +17 -0
  66. data/db/migrate/20140630043202_create_door_mat_emails.rb +12 -0
  67. data/db/migrate/20140702045729_create_door_mat_activities.rb +14 -0
  68. data/db/migrate/20141115183045_create_door_mat_access_tokens.rb +17 -0
  69. data/db/migrate/20141121191824_create_door_mat_memberships.rb +14 -0
  70. data/db/migrate/20150910182126_rename_session_guid_column.rb +5 -0
  71. data/db/migrate/20150918210831_add_access_token_rating_column.rb +5 -0
  72. data/door_mat.gemspec +37 -0
  73. data/lib/door_mat.rb +20 -0
  74. data/lib/door_mat/attr_asymmetric_store.rb +82 -0
  75. data/lib/door_mat/attr_symmetric_store.rb +82 -0
  76. data/lib/door_mat/configuration.rb +193 -0
  77. data/lib/door_mat/controller.rb +117 -0
  78. data/lib/door_mat/crypto.rb +49 -0
  79. data/lib/door_mat/crypto/asymmetric_store.rb +77 -0
  80. data/lib/door_mat/crypto/fast_hash.rb +17 -0
  81. data/lib/door_mat/crypto/password_hash.rb +39 -0
  82. data/lib/door_mat/crypto/secure_compare.rb +23 -0
  83. data/lib/door_mat/crypto/symmetric_store.rb +68 -0
  84. data/lib/door_mat/engine.rb +23 -0
  85. data/lib/door_mat/process/actor_password_change.rb +65 -0
  86. data/lib/door_mat/process/actor_sign_in.rb +38 -0
  87. data/lib/door_mat/process/actor_sign_up.rb +39 -0
  88. data/lib/door_mat/process/create_new_anonymous_actor.rb +36 -0
  89. data/lib/door_mat/process/manage_email.rb +42 -0
  90. data/lib/door_mat/process/reset_password.rb +50 -0
  91. data/lib/door_mat/regex.rb +17 -0
  92. data/lib/door_mat/test_helper.rb +58 -0
  93. data/lib/door_mat/url_protocol.rb +9 -0
  94. data/lib/door_mat/version.rb +3 -0
  95. data/lib/tasks/door_mat_tasks.rake +31 -0
  96. data/spec/controllers/door_mat/activities_controller_spec.rb +70 -0
  97. data/spec/controllers/door_mat/forgot_passwords_controller_spec.rb +57 -0
  98. data/spec/controllers/door_mat/manage_email_spec.rb +181 -0
  99. data/spec/controllers/door_mat/password_less_session_controller_spec.rb +344 -0
  100. data/spec/controllers/door_mat/sign_in_controller_spec.rb +211 -0
  101. data/spec/controllers/door_mat/sign_up_controller_spec.rb +90 -0
  102. data/spec/factories/door_mat_access_tokens.rb +6 -0
  103. data/spec/factories/door_mat_activitiess.rb +6 -0
  104. data/spec/factories/door_mat_actors.rb +23 -0
  105. data/spec/factories/door_mat_emails.rb +14 -0
  106. data/spec/factories/door_mat_memberships.rb +6 -0
  107. data/spec/factories/door_mat_sessions.rb +24 -0
  108. data/spec/features/password_less_session_spec.rb +165 -0
  109. data/spec/features/remember_me_spec.rb +672 -0
  110. data/spec/features/session_spec.rb +336 -0
  111. data/spec/lib/attr_store_spec.rb +237 -0
  112. data/spec/lib/crypto_spec.rb +130 -0
  113. data/spec/lib/process_spec.rb +159 -0
  114. data/spec/models/door_mat/access_token_spec.rb +134 -0
  115. data/spec/models/door_mat/activity_spec.rb +38 -0
  116. data/spec/models/door_mat/actor_spec.rb +56 -0
  117. data/spec/models/door_mat/email_spec.rb +25 -0
  118. data/spec/models/door_mat/session_spec.rb +69 -0
  119. data/spec/spec_helper.rb +223 -0
  120. data/spec/support/timecop/timecop_helper.rb +52 -0
  121. data/spec/test_app/README.rdoc +28 -0
  122. data/spec/test_app/Rakefile +6 -0
  123. data/spec/test_app/app/assets/javascripts/application.js +13 -0
  124. data/spec/test_app/app/assets/stylesheets/application.css +15 -0
  125. data/spec/test_app/app/controllers/account_controller.rb +28 -0
  126. data/spec/test_app/app/controllers/application_controller.rb +10 -0
  127. data/spec/test_app/app/controllers/password_less_sample_controller.rb +56 -0
  128. data/spec/test_app/app/controllers/static_controller.rb +7 -0
  129. data/spec/test_app/app/helpers/account_helper.rb +2 -0
  130. data/spec/test_app/app/helpers/application_helper.rb +2 -0
  131. data/spec/test_app/app/models/game.rb +62 -0
  132. data/spec/test_app/app/models/shared_data.rb +4 -0
  133. data/spec/test_app/app/models/shared_key.rb +8 -0
  134. data/spec/test_app/app/models/user_detail.rb +7 -0
  135. data/spec/test_app/app/views/account/show.html.erb +133 -0
  136. data/spec/test_app/app/views/door_mat/static/sign_out_success.html.erb +7 -0
  137. data/spec/test_app/app/views/layouts/application.html.erb +20 -0
  138. data/spec/test_app/app/views/password_less_sample/draw_results.html.erb +6 -0
  139. data/spec/test_app/app/views/password_less_sample/final_result.html.erb +7 -0
  140. data/spec/test_app/app/views/password_less_sample/play_game.html.erb +5 -0
  141. data/spec/test_app/app/views/password_less_sample/show_loosing_door.html.erb +10 -0
  142. data/spec/test_app/app/views/static/index.html.erb +12 -0
  143. data/spec/test_app/app/views/static/only_confirmed_email_allowed.html.erb +10 -0
  144. data/spec/test_app/app/views/static/page_that_require_password_reconfirmation.html.erb +16 -0
  145. data/spec/test_app/app/views/static/session_protected_page.html.erb +32 -0
  146. data/spec/test_app/bin/bundle +3 -0
  147. data/spec/test_app/bin/rails +4 -0
  148. data/spec/test_app/bin/rake +4 -0
  149. data/spec/test_app/config.ru +4 -0
  150. data/spec/test_app/config/application.rb +29 -0
  151. data/spec/test_app/config/boot.rb +5 -0
  152. data/spec/test_app/config/database.yml +25 -0
  153. data/spec/test_app/config/environment.rb +19 -0
  154. data/spec/test_app/config/environments/development.rb +50 -0
  155. data/spec/test_app/config/environments/production.rb +83 -0
  156. data/spec/test_app/config/environments/test.rb +48 -0
  157. data/spec/test_app/config/initializers/backtrace_silencers.rb +7 -0
  158. data/spec/test_app/config/initializers/cookies_serializer.rb +3 -0
  159. data/spec/test_app/config/initializers/door_mat.rb +72 -0
  160. data/spec/test_app/config/initializers/filter_parameter_logging.rb +4 -0
  161. data/spec/test_app/config/initializers/inflections.rb +16 -0
  162. data/spec/test_app/config/initializers/mime_types.rb +4 -0
  163. data/spec/test_app/config/initializers/session_store.rb +3 -0
  164. data/spec/test_app/config/initializers/wrap_parameters.rb +14 -0
  165. data/spec/test_app/config/locales/en.yml +23 -0
  166. data/spec/test_app/config/routes.rb +42 -0
  167. data/spec/test_app/config/secrets.yml +31 -0
  168. data/spec/test_app/db/migrate/20140717182813_create_user_details.rb +10 -0
  169. data/spec/test_app/db/migrate/20140908225256_create_shared_data.rb +10 -0
  170. data/spec/test_app/db/migrate/20140908225604_create_shared_keys.rb +11 -0
  171. data/spec/test_app/db/migrate/20141121190714_create_games.rb +10 -0
  172. data/spec/test_app/public/404.html +67 -0
  173. data/spec/test_app/public/422.html +67 -0
  174. data/spec/test_app/public/500.html +66 -0
  175. data/spec/test_app/public/favicon.ico +0 -0
  176. metadata +552 -0
@@ -0,0 +1,14 @@
1
+ module DoorMat
2
+ class Activity < ActiveRecord::Base
3
+
4
+ belongs_to :actor
5
+ belongs_to :notifier, :polymorphic => true
6
+
7
+ enum status: [:pending, :started, :done, :failed]
8
+
9
+ def self.hash_token(token)
10
+ DoorMat::Crypto::FastHash.sha256(token)
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,45 @@
1
+ module DoorMat
2
+ class ActivityConfirmEmail < Activity
3
+
4
+ belongs_to :email, :class_name => "DoorMat::Email", :foreign_key => :notifier_id
5
+
6
+ def self.for(email, controller)
7
+ return unless email.not_confirmed?
8
+
9
+ actor = email.actor
10
+
11
+ # Fail any existing email confirmation activities for the current email
12
+ actor.confirm_email_activities.each do |a|
13
+ a.failed! if email == a.email
14
+ end
15
+ token = SecureRandom.uuid
16
+
17
+ activity = self.new
18
+ activity.actor = actor
19
+ activity.email = email
20
+ activity.link_hash = self.hash_token(token)
21
+ activity.started!
22
+
23
+ activity.send_email_with(token, controller)
24
+ end
25
+
26
+ def input_valid?(token, encoded_address)
27
+ DoorMat::Crypto.secure_compare(
28
+ [DoorMat::Activity.hash_token(token.to_s), DoorMat::Email.address_hash_from_encoded_address(encoded_address)].join('_'),
29
+ [self.link_hash, self.email.address_hash].join('_')
30
+ ) && self.email.not_confirmed?
31
+ end
32
+
33
+ def send_email_with(token, controller)
34
+ parameters = {
35
+ url: controller.send(:confirm_email_url, token: token, email: self.email.to_urlsafe_encoded, protocol: DoorMat::UrlProtocol.url_protocol),
36
+ address: self.email.address
37
+ }
38
+ DoorMat::ActivityMailer.confirm_email(parameters).deliver_now
39
+ rescue Exception => e
40
+ DoorMat.configuration.logger.error "ERROR: Failed to deliver confirmation email for actor #{self.actor.id} to #{self.email.address} w #{parameters[:url]} - #{e}"
41
+ raise e
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,30 @@
1
+ module DoorMat
2
+ class ActivityDownloadRecoveryKey < Activity
3
+
4
+ def self.for(actor)
5
+ # Fail any existing activities
6
+ actor.download_recovery_key_activities.each do |a|
7
+ a.failed!
8
+ end
9
+
10
+ activity = self.new
11
+ activity.actor = actor
12
+ activity.notifier = actor
13
+ activity.link_hash = self.hash_token(SecureRandom.uuid)
14
+ activity.started!
15
+ end
16
+
17
+ def input_valid?(token)
18
+ DoorMat::Crypto.secure_compare(DoorMat::Activity.hash_token(token.to_s), self.link_hash)
19
+ end
20
+
21
+ def get_new_token
22
+ token = SecureRandom.uuid
23
+ self.link_hash = DoorMat::Activity.hash_token(token)
24
+ self.save!
25
+
26
+ token
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,47 @@
1
+ module DoorMat
2
+ class ActivityResetPassword < Activity
3
+
4
+ belongs_to :email, :class_name => "DoorMat::Email", :foreign_key => :notifier_id
5
+
6
+ def self.for(email, controller)
7
+ # BFP: prevent email flooding
8
+ return nil unless self.where( actor: email.actor ).where(["created_at > ?", DoorMat.configuration.forgot_password_link_request_delay_minutes.minutes.ago]).blank?
9
+
10
+ token = SecureRandom.uuid
11
+
12
+ activity = self.new
13
+ activity.actor = email.actor
14
+ activity.email = email
15
+ activity.link_hash = self.hash_token(token)
16
+ activity.started!
17
+
18
+ activity.send_email_with(token, controller)
19
+ activity
20
+ end
21
+
22
+ def self.with(token, emails)
23
+
24
+ self.where(type: "DoorMat::ActivityResetPassword", link_hash: self.hash_token(token)).started.each do |activity|
25
+ if activity.created_at < DoorMat.configuration.forgot_password_link_expiration_delay_minutes.minutes.ago
26
+ activity.failed!
27
+ else
28
+ return activity if emails.include? activity.email
29
+ end
30
+ end
31
+
32
+ nil
33
+ end
34
+
35
+ def send_email_with(token, controller)
36
+ parameters = {
37
+ url: controller.send(:choose_new_password_url, token: token, email: self.email.to_urlsafe_encoded, protocol: DoorMat::UrlProtocol.url_protocol),
38
+ address: self.email.address
39
+ }
40
+ DoorMat::ActivityMailer.reset_password(parameters).deliver_now
41
+ rescue Exception => e
42
+ DoorMat.configuration.logger.error "ERROR: Failed to deliver reset password email for actor #{self.actor.id} to #{self.email.address} w #{parameters[:url]} - #{e}"
43
+ raise e
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,149 @@
1
+ module DoorMat
2
+ class Actor < ActiveRecord::Base
3
+
4
+ has_many :emails, :dependent => :destroy
5
+ has_many :sessions, :dependent => :destroy
6
+ has_many :activities, :dependent => :destroy
7
+ has_many :access_tokens, :dependent => :destroy
8
+
9
+ has_many :memberships,
10
+ :inverse_of => :member,
11
+ :foreign_key => :member_id,
12
+ :class_name => 'DoorMat::Membership',
13
+ :dependent => :destroy
14
+ has_many :anonymous_actors,
15
+ :through => :memberships,
16
+ :source => :member_of
17
+
18
+ has_many :members,
19
+ :inverse_of => :member_of,
20
+ :foreign_key => :member_of_id,
21
+ :class_name => 'DoorMat::Membership',
22
+ :dependent => :destroy
23
+ has_many :member_actors,
24
+ :through => :members,
25
+ :source => :member
26
+
27
+ attr_accessor :current_email
28
+
29
+ validates_presence_of :key_salt, :password_salt, :password_hash, :system_key
30
+
31
+ def self.create_with(password)
32
+ actor = self.new
33
+
34
+ actor.re_key_with(password)
35
+
36
+ actor.system_key = DoorMat::Crypto::SymmetricStore.random_key
37
+
38
+ actor
39
+ end
40
+
41
+ def re_key_with(password)
42
+ self.key_salt = DoorMat::Crypto::PasswordHash.pbkdf2_salt
43
+
44
+ self.password_salt = DoorMat::Crypto::PasswordHash.bcrypt_salt
45
+ self.password_hash = DoorMat::Crypto::PasswordHash.bcrypt_hash(password, self.password_salt)
46
+ end
47
+
48
+ def self.authenticate_with(address, password)
49
+ actor = nil
50
+ matching_emails = DoorMat::Email.matching(address)
51
+
52
+ # As with a secure_compare, spend constant time
53
+ # testing the password against all the accounts, all the time, even if
54
+ # the first account is matching
55
+ matching_emails.each do |e|
56
+ if e.actor.authenticate(password)
57
+ actor = e.actor
58
+ actor.current_email = e
59
+ end
60
+ end
61
+
62
+ actor
63
+ end
64
+
65
+ def confirm_email_activities
66
+ activities.started.where(type: "DoorMat::ActivityConfirmEmail")
67
+ end
68
+
69
+ def download_recovery_key_activities
70
+ activities.started.where(type: "DoorMat::ActivityDownloadRecoveryKey")
71
+ end
72
+
73
+ def can_add_email?(email)
74
+ if emails.count >= DoorMat::configuration.max_email_count_per_actor
75
+ email.errors[:base] << I18n.t("door_mat.actor.max_email_count_per_actor_reached")
76
+ return false
77
+ end
78
+
79
+ if emails.where(:address_hash => email.address_hash).count > 0
80
+ email.errors[:base] << I18n.t("door_mat.actor.email_already_associated")
81
+ return false
82
+ end
83
+
84
+ true
85
+ end
86
+
87
+ def has_primary_email?
88
+ emails.map {|e| e.primary?}.inject(false, :|)
89
+ end
90
+
91
+ def email_from_urlsafe_encoded(encoded_address)
92
+ emails.where(:address_hash => DoorMat::Email.address_hash_from_encoded_address(encoded_address)).first
93
+ end
94
+
95
+ def system_encrypt(message)
96
+ DoorMat::Crypto::SymmetricStore.encrypt(message, self.system_key)[:ciphertext]
97
+ end
98
+
99
+ def system_decrypt(ciphertext)
100
+ DoorMat::Crypto::SymmetricStore.decrypt(ciphertext, self.system_key)
101
+ end
102
+
103
+ def authenticate(password)
104
+ DoorMat::Crypto::secure_compare(
105
+ self.password_hash,
106
+ DoorMat::Crypto::PasswordHash.bcrypt_hash(password, self.password_salt)
107
+ )
108
+ end
109
+
110
+ def setup_public_key_pairs(session)
111
+ pem_encrypted_pkey_pair_and_key = DoorMat::Crypto::AsymmetricStore.generate_pem_encrypted_pkey_pair_and_key
112
+ self.encrypted_pem_key = session.encrypt(pem_encrypted_pkey_pair_and_key[:key])
113
+ self.pem_encrypted_pkey = pem_encrypted_pkey_pair_and_key[:pem_encrypted_pkey]
114
+ self.pem_public_key = DoorMat::Crypto::AsymmetricStore.pem_public_key_from_pem_encrypted_pkey_pair(pem_encrypted_pkey_pair_and_key[:pem_encrypted_pkey], pem_encrypted_pkey_pair_and_key[:key])
115
+ end
116
+
117
+ def encrypt_shared_key(key)
118
+ self_pub_key = DoorMat::Crypto::AsymmetricStore.public_key_from_pem_public_key(self.pem_public_key)
119
+ DoorMat::Crypto::AsymmetricStore.encrypt(key, self_pub_key)
120
+ end
121
+
122
+ def decrypt_shared_key(key, session)
123
+ pem_key = session.decrypt(self.encrypted_pem_key)
124
+ private_key = DoorMat::Crypto::AsymmetricStore.private_key_from_pem_encrypted_pkey_pair(self.pem_encrypted_pkey, pem_key)
125
+ DoorMat::Crypto::AsymmetricStore.decrypt(key, private_key)
126
+ end
127
+
128
+ def share_with(other, secrets, with_key=nil)
129
+ secrets = Array(secrets)
130
+ with_key ||= DoorMat::Crypto::SymmetricStore.random_key
131
+ self_shared_key = self.encrypt_shared_key(with_key)
132
+ other_shared_key = other.encrypt_shared_key(with_key)
133
+ encrypted_secrets = DoorMat::Crypto.encrypt_shared(secrets, with_key)
134
+
135
+ {
136
+ key: self_shared_key,
137
+ other_key: other_shared_key,
138
+ secrets: encrypted_secrets
139
+ }
140
+ end
141
+
142
+ def keep_only_this_session!(session)
143
+ sessions.each do |s|
144
+ s.destroy! unless s == session
145
+ end
146
+ end
147
+
148
+ end
149
+ end
@@ -0,0 +1,12 @@
1
+ module DoorMat
2
+ class ChangePassword
3
+
4
+ include ActiveModel::Model
5
+
6
+ attr_accessor :old_password, :new_password, :new_password_confirmation
7
+
8
+ validates :old_password, length: { minimum: 1 }
9
+ validates :new_password, length: { minimum: 1 }
10
+ validates :new_password, confirmation: true
11
+ end
12
+ end
@@ -0,0 +1,58 @@
1
+ module DoorMat
2
+ class Email < ActiveRecord::Base
3
+
4
+ include DoorMat::AttrSymmetricStore
5
+
6
+ belongs_to :actor
7
+ has_many :sessions, :dependent => :destroy
8
+
9
+ validates_presence_of :address_hash, :address
10
+ validates_format_of :address, with: DoorMat::Regex.simple_email
11
+
12
+ attr_symmetric_store :address
13
+
14
+ enum status: [:not_confirmed, :confirmed, :primary, :not_available]
15
+
16
+ def self.address_hash(address)
17
+ DoorMat::Crypto::FastHash.sha256(address.to_str)
18
+ end
19
+
20
+ def self.decode_urlsafe(encoded_address)
21
+ Base64.urlsafe_decode64(encoded_address.to_str)
22
+ rescue ArgumentError
23
+ return encoded_address.to_str
24
+ end
25
+
26
+ def self.address_hash_from_encoded_address(encoded_address)
27
+ self.address_hash(self.decode_urlsafe(encoded_address))
28
+ end
29
+
30
+ def self.matching(address)
31
+ address_hash = self.address_hash(address.to_str)
32
+ self.where(address_hash: address_hash)
33
+ end
34
+
35
+ def self.confirmed_matching(address)
36
+ address_hash = self.address_hash(address.to_str)
37
+ self.where(address_hash: address_hash).where('status = :confirmed or status = :primary', self.statuses)
38
+ end
39
+
40
+ def self.count_matching(address)
41
+ address_hash = self.address_hash(address.to_str)
42
+ self.where(address_hash: address_hash).count
43
+ end
44
+
45
+ def self.for(address)
46
+ e = self.new
47
+ e.address_hash = self.address_hash(address.to_str)
48
+ e.address = address.to_str
49
+ e.status = :not_confirmed
50
+ e
51
+ end
52
+
53
+ def to_urlsafe_encoded
54
+ Base64.urlsafe_encode64(self.address)
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,12 @@
1
+ module DoorMat
2
+ class ForgotPassword
3
+
4
+ include ActiveModel::Model
5
+
6
+ attr_accessor :email, :password, :password_confirmation, :token, :recovery_key
7
+
8
+ validates_format_of :email, with: DoorMat::Regex.simple_email
9
+ validates :password, length: { minimum: 1 }
10
+ validates :password, confirmation: true
11
+ end
12
+ end
@@ -0,0 +1,42 @@
1
+ module DoorMat
2
+ class Membership < ActiveRecord::Base
3
+
4
+ include DoorMat::AttrAsymmetricStore
5
+
6
+ belongs_to :member,
7
+ :inverse_of => :memberships,
8
+ :foreign_key => :member_id,
9
+ :class_name => 'DoorMat::Actor'
10
+ belongs_to :member_of,
11
+ :inverse_of => :members,
12
+ :foreign_key => :member_of_id,
13
+ :class_name => 'DoorMat::Actor'
14
+
15
+ attr_asymmetric_store :key, actor_column: :member
16
+
17
+ enum sponsor: [:sponsor_false, :sponsor_true]
18
+ enum owner: [:owner_false, :owner_true]
19
+ enum permission: [:no_permission, :list_permission, :read_permission, :write_permission]
20
+
21
+ def share_with!(actor, ownership = :owner_true, permission = :no_permission )
22
+ false unless self.owner_true?
23
+
24
+ membership = DoorMat::Membership.new
25
+ membership.member = actor
26
+ membership.member_of = self.member_of
27
+ membership.sponsor = DoorMat::Membership.sponsors[:sponsor_false]
28
+ membership.owner = DoorMat::Membership.owners[ownership]
29
+ membership.permission = DoorMat::Membership.permissions[permission]
30
+ membership.key = self.key
31
+ membership.save!
32
+ true
33
+ end
34
+
35
+ def load_sub_session
36
+ sub_session = DoorMat::Session.new_sub_session_for_actor(self.member_of, self.key)
37
+
38
+ self.member.setup_public_key_pairs(sub_session)
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,315 @@
1
+ module DoorMat
2
+ class Session < ActiveRecord::Base
3
+
4
+ belongs_to :actor
5
+ belongs_to :email
6
+
7
+ attr_accessor :token
8
+
9
+ validate :initialization_performed?
10
+
11
+ enum rating: [:public_computer, :private_computer, :remember_me]
12
+
13
+ def initialization_performed?
14
+ if @symmetric_actor_key.blank? || @symmetric_actor_key.first.blank? || @session_key.blank? || @token.blank? || self.hashed_token.blank?
15
+ errors.add(:base, I18n.t("door_mat.session.initialization_failure"))
16
+ end
17
+ end
18
+
19
+ # The current_session is never nil but it might not be valid
20
+ # Check with DoorMat::Session.current_session.valid?
21
+ def self.current_session
22
+ RequestStore.store[:current_session] ||= self.new
23
+ end
24
+
25
+ # destroy existing valid session if any at sign_in and sign_up time
26
+ # or when expired
27
+ # to prevent unreferenced sessions from accumulating in the store
28
+ def self.clear_current_session
29
+ session = current_session
30
+ RequestStore.store[:current_session] = nil
31
+
32
+ session.destroy if session.persisted?
33
+
34
+ nil
35
+ end
36
+
37
+ def self.for(actor, password, request)
38
+ clear_current_session
39
+
40
+ return nil if password.blank? || actor.key_salt.blank?
41
+
42
+ session = self.new
43
+ session.ip = request.remote_ip
44
+ session.agent = request.user_agent
45
+
46
+ RequestStore.store[:current_session] = session.initialize_with(actor, password)
47
+ end
48
+
49
+ def initialize_with(actor, password)
50
+ reset
51
+ self.email = actor.current_email
52
+
53
+ re_key_with(actor, password)
54
+
55
+ self
56
+ rescue Exception => e
57
+ reset
58
+ DoorMat.configuration.logger.error "ERROR: Failed to initialize session with password for actor #{actor.id} - #{e}"
59
+
60
+ nil
61
+ end
62
+
63
+ def re_key_with(actor, password)
64
+ @symmetric_actor_key << DoorMat::Crypto::PasswordHash.pbkdf2_hash(password, actor.key_salt)
65
+
66
+ encrypt_actor_key(actor, @symmetric_actor_key.last)
67
+ set_new_token
68
+
69
+ self.password_authenticated_at = DateTime.current
70
+
71
+ self
72
+ end
73
+
74
+ def session_for_actor_loaded?(actor)
75
+ actor.nil? || (self.actor == actor) || (sub_sessions.has_key? actor.id)
76
+ end
77
+
78
+ def self.new_sub_session_for_actor(actor, password)
79
+ session = self.new
80
+ session.initialize_with(actor, password)
81
+ session.actor = actor
82
+
83
+ session
84
+ end
85
+
86
+ def append_sub_session(session)
87
+ unless DoorMat::Session.current_session == self
88
+ DoorMat.configuration.logger.error "ERROR: append_sub_session must only be called on DoorMat::Session.current_session"
89
+ return false
90
+ end
91
+
92
+ unless session.valid?
93
+ DoorMat.configuration.logger.error "ERROR: append_sub_session was given an invalid session"
94
+ return false
95
+ end
96
+
97
+ actor_id = session.actor.id
98
+ if sub_sessions.has_key? actor_id
99
+ DoorMat.configuration.logger.warn "WARN: sub_session #{actor_id} already present; updating."
100
+ end
101
+ sub_sessions[actor_id] = session
102
+
103
+ return true
104
+ end
105
+
106
+ def memberships_for(actor)
107
+ current_session_ids = sub_sessions.keys
108
+ current_session_ids.push(self.actor_id) unless self.actor_id.nil?
109
+
110
+ DoorMat::Membership.where("member_of_id = :member_of and member_id in (:ids)",
111
+ :member_of => actor.id,
112
+ :ids => current_session_ids).select {|m| !m.readonly?}
113
+ end
114
+
115
+ def autoload_sesion_for(actor)
116
+ return if session_for_actor_loaded?(actor)
117
+
118
+ memberships = memberships_for(actor)
119
+ unless memberships.empty?
120
+ membership = memberships.first
121
+ session = DoorMat::Session.new_sub_session_for_actor(membership.member_of, membership.key)
122
+ append_sub_session(session)
123
+ end
124
+ end
125
+
126
+ def self.from(cookies, request)
127
+ clear_current_session
128
+ session_guid = cookies.encrypted[:session_guid].to_s.strip
129
+ session_key = cookies.encrypted[:session_key].to_s.strip
130
+
131
+ return nil if session_guid.blank? || session_key.blank?
132
+ return nil if DoorMat::Regex.session_guid.match(session_guid).blank?
133
+ session = self.find_by(hashed_token: DoorMat::Crypto::FastHash.sha256(session_guid) ) || self.new
134
+ session.token = session_guid
135
+ if session.actor.nil?
136
+ session.destroy!
137
+ return nil
138
+ end
139
+
140
+ case
141
+ when session.remember_me?
142
+ if session.created_at < DoorMat.configuration.remember_me_max_day_count.days.ago
143
+ session.destroy!
144
+ return nil
145
+ end
146
+ if session.updated_at < DoorMat.configuration.private_computer_access_session_timeout.minutes.ago
147
+ session_key = session.renew_session_key_and_token(session_key, cookies)
148
+ end
149
+ when session.private_computer?
150
+ if session.updated_at < DoorMat.configuration.private_computer_access_session_timeout.minutes.ago
151
+ session.destroy!
152
+ return nil
153
+ end
154
+ else
155
+ if session.updated_at < DoorMat.configuration.public_computer_access_session_timeout.minutes.ago
156
+ session.destroy!
157
+ return nil
158
+ end
159
+ end
160
+
161
+ session.ip = request.remote_ip
162
+ session.agent = request.user_agent # Check for major change in user_agent in case of strict session policy
163
+ session.updated_at = DateTime.current
164
+
165
+ RequestStore.store[:current_session] = session
166
+ if session.unlock_with(session_key)
167
+ session.save
168
+ else
169
+ clear_current_session
170
+ end
171
+ end
172
+
173
+ def renew_session_key_and_token(old_session_key, cookies)
174
+
175
+ if unlock_with(old_session_key)
176
+ encrypt_actor_key(self.actor, @symmetric_actor_key.last)
177
+ set_new_token
178
+
179
+ set_up(cookies)
180
+ end
181
+
182
+ @session_key
183
+ end
184
+
185
+ def unlock_with(session_key)
186
+ @symmetric_actor_key = [DoorMat::Crypto::SymmetricStore.decrypt(
187
+ self.encrypted_symmetric_actor_key,
188
+ self.actor.system_decrypt(session_key)
189
+ )]
190
+ @session_key = session_key
191
+
192
+ true
193
+ rescue Exception => e
194
+ reset
195
+ DoorMat.configuration.logger.error "ERROR: Failed to unlock session with session_key for actor #{self.actor.id} - #{e}"
196
+ false
197
+ end
198
+
199
+ def self.destroy_if_linked_to(cookies)
200
+ session_guid = cookies.encrypted[:session_guid].to_s.strip
201
+ clean_up(cookies)
202
+
203
+ return false if DoorMat::Regex.session_guid.match(session_guid).blank?
204
+ session = self.find_by(hashed_token: DoorMat::Crypto::FastHash.sha256(session_guid) ) || self.new
205
+ return false unless session.persisted?
206
+ session.destroy!
207
+
208
+ true
209
+ end
210
+
211
+ def set_up(cookies)
212
+ options = {
213
+ secure: DoorMat.configuration.transmit_cookies_only_over_https,
214
+ httponly: true
215
+ }
216
+ options.merge!({ expires: DoorMat.configuration.remember_me_max_day_count.days.since(self.created_at) }) unless self.public_computer?
217
+
218
+ cookies.encrypted[:session_guid] = options.merge({value: self.token})
219
+ cookies.encrypted[:session_key] = options.merge({value: @session_key})
220
+
221
+ nil
222
+ end
223
+
224
+ def self.clean_up(cookies)
225
+ cookies.delete(:session_guid)
226
+ cookies.delete(:session_key)
227
+
228
+ nil
229
+ end
230
+
231
+ def is_older_than(minutes_old)
232
+ self.password_authenticated_at < minutes_old.minutes.ago
233
+ end
234
+
235
+ def with_session_for_actor(actor)
236
+ if self.valid? && (actor.nil? || self.actor_id.nil? || self.actor == actor)
237
+ yield(self)
238
+ elsif !actor.nil? && (sub_sessions.has_key? actor.id)
239
+ yield(sub_sessions[actor.id])
240
+ end
241
+ end
242
+
243
+ def encrypt(message)
244
+ DoorMat::Crypto::SymmetricStore.encrypt(message, @symmetric_actor_key.last)[:ciphertext]
245
+ end
246
+
247
+ def decrypt(ciphertext)
248
+ DoorMat::Crypto::SymmetricStore.decrypt(ciphertext, @symmetric_actor_key.first)
249
+ rescue OpenSSL::Cipher::CipherError => e
250
+ DoorMat.configuration.logger.warn "WARN: Failed to decrypt for actor #{self.actor.id} - #{e}"
251
+ nil
252
+ end
253
+
254
+ def package_recovery_key
255
+ h = DoorMat::Crypto::SymmetricStore.encrypt(@symmetric_actor_key.last)
256
+ self.actor.recovery_key = h[:ciphertext]
257
+ self.actor.save!
258
+ self.actor.system_encrypt(h[:key])
259
+ end
260
+
261
+ def recovery_key_restore(actor, recovery_key)
262
+ self.actor = actor
263
+ key = self.actor.system_decrypt(recovery_key)
264
+ @symmetric_actor_key = [DoorMat::Crypto::SymmetricStore.decrypt(self.actor.recovery_key, key)]
265
+ true
266
+ rescue OpenSSL::Cipher::CipherError => e
267
+ DoorMat.configuration.logger.warn "WARN: Failed recovery_key_restore for actor #{self.actor.id} - #{e}"
268
+ false
269
+ end
270
+
271
+ def reconfirm_password(password)
272
+ if self.actor.authenticate(password)
273
+ self.password_authenticated_at = DateTime.current
274
+ self.save!
275
+ true
276
+ else
277
+ false
278
+ end
279
+ end
280
+
281
+ private
282
+
283
+ def reset
284
+ # This key must never be stored or shared in clear outside this object
285
+ @symmetric_actor_key = []
286
+ # This token must never be stored in clear on the system side
287
+ @token = ''
288
+ # This random key is encrypted with the actor system_key and used to decrypt the session symmetric_actor key; see unlock_with(session_key)
289
+ @session_key = ''
290
+ # Sub sessions grant access to data associated with additional actors
291
+ @sub_sessions = {}
292
+
293
+ nil
294
+ end
295
+
296
+ def sub_sessions
297
+ @sub_sessions ||= {}
298
+ end
299
+
300
+ def encrypt_actor_key(actor, actor_key)
301
+ symmetric_store = DoorMat::Crypto::SymmetricStore.encrypt(actor_key)
302
+ self.encrypted_symmetric_actor_key = symmetric_store[:ciphertext]
303
+ @session_key = actor.system_encrypt(symmetric_store[:key])
304
+
305
+ nil
306
+ end
307
+
308
+ def set_new_token
309
+ @token = SecureRandom.uuid
310
+
311
+ self.hashed_token = DoorMat::Crypto::FastHash.sha256(@token)
312
+ end
313
+
314
+ end
315
+ end