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.
- checksums.yaml +7 -0
- data/.rspec +2 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.md +88 -0
- data/Rakefile +32 -0
- data/app/assets/javascripts/door_mat/application.js +13 -0
- data/app/assets/stylesheets/door_mat/application.css +15 -0
- data/app/assets/stylesheets/scaffold.css +56 -0
- data/app/controllers/door_mat/activities_controller.rb +106 -0
- data/app/controllers/door_mat/application_controller.rb +14 -0
- data/app/controllers/door_mat/change_password_controller.rb +32 -0
- data/app/controllers/door_mat/forgot_passwords_controller.rb +57 -0
- data/app/controllers/door_mat/manage_email_controller.rb +61 -0
- data/app/controllers/door_mat/password_less_session_controller.rb +121 -0
- data/app/controllers/door_mat/reconfirm_password_controller.rb +27 -0
- data/app/controllers/door_mat/sessions_controller.rb +17 -0
- data/app/controllers/door_mat/sign_in_controller.rb +60 -0
- data/app/controllers/door_mat/sign_up_controller.rb +59 -0
- data/app/controllers/door_mat/static_controller.rb +5 -0
- data/app/mailers/door_mat/activity_mailer.rb +18 -0
- data/app/mailers/door_mat/password_less_session_mailer.rb +12 -0
- data/app/models/door_mat/access_token.rb +315 -0
- data/app/models/door_mat/activity.rb +14 -0
- data/app/models/door_mat/activity_confirm_email.rb +45 -0
- data/app/models/door_mat/activity_download_recovery_key.rb +30 -0
- data/app/models/door_mat/activity_reset_password.rb +47 -0
- data/app/models/door_mat/actor.rb +149 -0
- data/app/models/door_mat/change_password.rb +12 -0
- data/app/models/door_mat/email.rb +58 -0
- data/app/models/door_mat/forgot_password.rb +12 -0
- data/app/models/door_mat/membership.rb +42 -0
- data/app/models/door_mat/session.rb +315 -0
- data/app/models/door_mat/sign_in.rb +31 -0
- data/app/models/door_mat/sign_up.rb +17 -0
- data/app/views/door_mat/activity_mailer/confirm_email.html.erb +11 -0
- data/app/views/door_mat/activity_mailer/confirm_email.text.erb +7 -0
- data/app/views/door_mat/activity_mailer/reset_password.html.erb +11 -0
- data/app/views/door_mat/activity_mailer/reset_password.text.erb +7 -0
- data/app/views/door_mat/change_password/new.html.erb +22 -0
- data/app/views/door_mat/forgot_passwords/choose_new_password.html.erb +34 -0
- data/app/views/door_mat/forgot_passwords/new.html.erb +14 -0
- data/app/views/door_mat/helpers/_errors_if_any.html.erb +10 -0
- data/app/views/door_mat/manage_email/new.html.erb +14 -0
- data/app/views/door_mat/password_less_session/access_token.html.erb +16 -0
- data/app/views/door_mat/password_less_session/new.html.erb +34 -0
- data/app/views/door_mat/password_less_session_mailer/send_token.html.erb +11 -0
- data/app/views/door_mat/password_less_session_mailer/send_token.text.erb +7 -0
- data/app/views/door_mat/reconfirm_password/new.html.erb +12 -0
- data/app/views/door_mat/sign_in/new.html.erb +30 -0
- data/app/views/door_mat/sign_up/new.html.erb +24 -0
- data/app/views/door_mat/static/add_email_success.html.erb +5 -0
- data/app/views/door_mat/static/change_password_success.html.erb +2 -0
- data/app/views/door_mat/static/confirm_email_success.html.erb +2 -0
- data/app/views/door_mat/static/email_confirmation_required.html.erb +17 -0
- data/app/views/door_mat/static/forgot_password_verification_mail_sent.html.erb +2 -0
- data/app/views/door_mat/static/reconfirm_password_success.html.erb +4 -0
- data/app/views/door_mat/static/sign_in_success.html.erb +5 -0
- data/app/views/door_mat/static/sign_out_success.html.erb +5 -0
- data/app/views/door_mat/static/sign_up_success.html.erb +4 -0
- data/bin/rails +12 -0
- data/config/locales/en.yml +73 -0
- data/config/routes.rb +48 -0
- data/db/migrate/20140616234935_create_door_mat_actors.rb +23 -0
- data/db/migrate/20140617233357_create_door_mat_sessions.rb +17 -0
- data/db/migrate/20140630043202_create_door_mat_emails.rb +12 -0
- data/db/migrate/20140702045729_create_door_mat_activities.rb +14 -0
- data/db/migrate/20141115183045_create_door_mat_access_tokens.rb +17 -0
- data/db/migrate/20141121191824_create_door_mat_memberships.rb +14 -0
- data/db/migrate/20150910182126_rename_session_guid_column.rb +5 -0
- data/db/migrate/20150918210831_add_access_token_rating_column.rb +5 -0
- data/door_mat.gemspec +37 -0
- data/lib/door_mat.rb +20 -0
- data/lib/door_mat/attr_asymmetric_store.rb +82 -0
- data/lib/door_mat/attr_symmetric_store.rb +82 -0
- data/lib/door_mat/configuration.rb +193 -0
- data/lib/door_mat/controller.rb +117 -0
- data/lib/door_mat/crypto.rb +49 -0
- data/lib/door_mat/crypto/asymmetric_store.rb +77 -0
- data/lib/door_mat/crypto/fast_hash.rb +17 -0
- data/lib/door_mat/crypto/password_hash.rb +39 -0
- data/lib/door_mat/crypto/secure_compare.rb +23 -0
- data/lib/door_mat/crypto/symmetric_store.rb +68 -0
- data/lib/door_mat/engine.rb +23 -0
- data/lib/door_mat/process/actor_password_change.rb +65 -0
- data/lib/door_mat/process/actor_sign_in.rb +38 -0
- data/lib/door_mat/process/actor_sign_up.rb +39 -0
- data/lib/door_mat/process/create_new_anonymous_actor.rb +36 -0
- data/lib/door_mat/process/manage_email.rb +42 -0
- data/lib/door_mat/process/reset_password.rb +50 -0
- data/lib/door_mat/regex.rb +17 -0
- data/lib/door_mat/test_helper.rb +58 -0
- data/lib/door_mat/url_protocol.rb +9 -0
- data/lib/door_mat/version.rb +3 -0
- data/lib/tasks/door_mat_tasks.rake +31 -0
- data/spec/controllers/door_mat/activities_controller_spec.rb +70 -0
- data/spec/controllers/door_mat/forgot_passwords_controller_spec.rb +57 -0
- data/spec/controllers/door_mat/manage_email_spec.rb +181 -0
- data/spec/controllers/door_mat/password_less_session_controller_spec.rb +344 -0
- data/spec/controllers/door_mat/sign_in_controller_spec.rb +211 -0
- data/spec/controllers/door_mat/sign_up_controller_spec.rb +90 -0
- data/spec/factories/door_mat_access_tokens.rb +6 -0
- data/spec/factories/door_mat_activitiess.rb +6 -0
- data/spec/factories/door_mat_actors.rb +23 -0
- data/spec/factories/door_mat_emails.rb +14 -0
- data/spec/factories/door_mat_memberships.rb +6 -0
- data/spec/factories/door_mat_sessions.rb +24 -0
- data/spec/features/password_less_session_spec.rb +165 -0
- data/spec/features/remember_me_spec.rb +672 -0
- data/spec/features/session_spec.rb +336 -0
- data/spec/lib/attr_store_spec.rb +237 -0
- data/spec/lib/crypto_spec.rb +130 -0
- data/spec/lib/process_spec.rb +159 -0
- data/spec/models/door_mat/access_token_spec.rb +134 -0
- data/spec/models/door_mat/activity_spec.rb +38 -0
- data/spec/models/door_mat/actor_spec.rb +56 -0
- data/spec/models/door_mat/email_spec.rb +25 -0
- data/spec/models/door_mat/session_spec.rb +69 -0
- data/spec/spec_helper.rb +223 -0
- data/spec/support/timecop/timecop_helper.rb +52 -0
- data/spec/test_app/README.rdoc +28 -0
- data/spec/test_app/Rakefile +6 -0
- data/spec/test_app/app/assets/javascripts/application.js +13 -0
- data/spec/test_app/app/assets/stylesheets/application.css +15 -0
- data/spec/test_app/app/controllers/account_controller.rb +28 -0
- data/spec/test_app/app/controllers/application_controller.rb +10 -0
- data/spec/test_app/app/controllers/password_less_sample_controller.rb +56 -0
- data/spec/test_app/app/controllers/static_controller.rb +7 -0
- data/spec/test_app/app/helpers/account_helper.rb +2 -0
- data/spec/test_app/app/helpers/application_helper.rb +2 -0
- data/spec/test_app/app/models/game.rb +62 -0
- data/spec/test_app/app/models/shared_data.rb +4 -0
- data/spec/test_app/app/models/shared_key.rb +8 -0
- data/spec/test_app/app/models/user_detail.rb +7 -0
- data/spec/test_app/app/views/account/show.html.erb +133 -0
- data/spec/test_app/app/views/door_mat/static/sign_out_success.html.erb +7 -0
- data/spec/test_app/app/views/layouts/application.html.erb +20 -0
- data/spec/test_app/app/views/password_less_sample/draw_results.html.erb +6 -0
- data/spec/test_app/app/views/password_less_sample/final_result.html.erb +7 -0
- data/spec/test_app/app/views/password_less_sample/play_game.html.erb +5 -0
- data/spec/test_app/app/views/password_less_sample/show_loosing_door.html.erb +10 -0
- data/spec/test_app/app/views/static/index.html.erb +12 -0
- data/spec/test_app/app/views/static/only_confirmed_email_allowed.html.erb +10 -0
- data/spec/test_app/app/views/static/page_that_require_password_reconfirmation.html.erb +16 -0
- data/spec/test_app/app/views/static/session_protected_page.html.erb +32 -0
- data/spec/test_app/bin/bundle +3 -0
- data/spec/test_app/bin/rails +4 -0
- data/spec/test_app/bin/rake +4 -0
- data/spec/test_app/config.ru +4 -0
- data/spec/test_app/config/application.rb +29 -0
- data/spec/test_app/config/boot.rb +5 -0
- data/spec/test_app/config/database.yml +25 -0
- data/spec/test_app/config/environment.rb +19 -0
- data/spec/test_app/config/environments/development.rb +50 -0
- data/spec/test_app/config/environments/production.rb +83 -0
- data/spec/test_app/config/environments/test.rb +48 -0
- data/spec/test_app/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/test_app/config/initializers/cookies_serializer.rb +3 -0
- data/spec/test_app/config/initializers/door_mat.rb +72 -0
- data/spec/test_app/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/test_app/config/initializers/inflections.rb +16 -0
- data/spec/test_app/config/initializers/mime_types.rb +4 -0
- data/spec/test_app/config/initializers/session_store.rb +3 -0
- data/spec/test_app/config/initializers/wrap_parameters.rb +14 -0
- data/spec/test_app/config/locales/en.yml +23 -0
- data/spec/test_app/config/routes.rb +42 -0
- data/spec/test_app/config/secrets.yml +31 -0
- data/spec/test_app/db/migrate/20140717182813_create_user_details.rb +10 -0
- data/spec/test_app/db/migrate/20140908225256_create_shared_data.rb +10 -0
- data/spec/test_app/db/migrate/20140908225604_create_shared_keys.rb +11 -0
- data/spec/test_app/db/migrate/20141121190714_create_games.rb +10 -0
- data/spec/test_app/public/404.html +67 -0
- data/spec/test_app/public/422.html +67 -0
- data/spec/test_app/public/500.html +66 -0
- data/spec/test_app/public/favicon.ico +0 -0
- 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
|