door_mat 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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