aikotoba 0.1.0

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 (57) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +334 -0
  4. data/Rakefile +18 -0
  5. data/app/controllers/aikotoba/accounts_controller.rb +61 -0
  6. data/app/controllers/aikotoba/application_controller.rb +13 -0
  7. data/app/controllers/aikotoba/confirms_controller.rb +103 -0
  8. data/app/controllers/aikotoba/recoveries_controller.rb +120 -0
  9. data/app/controllers/aikotoba/sessions_controller.rb +78 -0
  10. data/app/controllers/aikotoba/unlocks_controller.rb +103 -0
  11. data/app/controllers/concerns/aikotoba/authenticatable.rb +40 -0
  12. data/app/controllers/concerns/aikotoba/protection/session_fixation_attack.rb +21 -0
  13. data/app/controllers/concerns/aikotoba/protection/timing_atack.rb +23 -0
  14. data/app/mailers/aikotoba/account_mailer.rb +24 -0
  15. data/app/mailers/aikotoba/application_mailer.rb +5 -0
  16. data/app/models/aikotoba/account/confirmation_token.rb +22 -0
  17. data/app/models/aikotoba/account/recovery_token.rb +22 -0
  18. data/app/models/aikotoba/account/service/authentication.rb +65 -0
  19. data/app/models/aikotoba/account/service/confirmation.rb +31 -0
  20. data/app/models/aikotoba/account/service/lock.rb +42 -0
  21. data/app/models/aikotoba/account/service/recovery.rb +31 -0
  22. data/app/models/aikotoba/account/service/registration.rb +30 -0
  23. data/app/models/aikotoba/account/unlock_token.rb +22 -0
  24. data/app/models/aikotoba/account/value/password.rb +48 -0
  25. data/app/models/aikotoba/account/value/token.rb +18 -0
  26. data/app/models/aikotoba/account.rb +120 -0
  27. data/app/models/concerns/aikotoba/enabled_feature_checkable.rb +41 -0
  28. data/app/models/concerns/aikotoba/token_encryptable.rb +27 -0
  29. data/app/views/aikotoba/account_mailer/confirm.html.erb +3 -0
  30. data/app/views/aikotoba/account_mailer/recover.html.erb +3 -0
  31. data/app/views/aikotoba/account_mailer/unlock.html.erb +3 -0
  32. data/app/views/aikotoba/accounts/new.html.erb +19 -0
  33. data/app/views/aikotoba/common/_errors.html.erb +9 -0
  34. data/app/views/aikotoba/common/_message.html.erb +8 -0
  35. data/app/views/aikotoba/confirms/new.html.erb +14 -0
  36. data/app/views/aikotoba/recoveries/edit.html.erb +15 -0
  37. data/app/views/aikotoba/recoveries/new.html.erb +14 -0
  38. data/app/views/aikotoba/sessions/_links.html.erb +20 -0
  39. data/app/views/aikotoba/sessions/new.html.erb +16 -0
  40. data/app/views/aikotoba/unlocks/new.html.erb +21 -0
  41. data/app/views/layouts/aikotoba/application.html.erb +11 -0
  42. data/app/views/layouts/aikotoba/mailer.html.erb +13 -0
  43. data/app/views/layouts/aikotoba/mailer.text.erb +1 -0
  44. data/config/locales/en.yml +49 -0
  45. data/config/routes.rb +32 -0
  46. data/db/migrate/20211204121532_create_aikotoba_accounts.rb +50 -0
  47. data/lib/aikotoba/constraints/confirmable_constraint.rb +7 -0
  48. data/lib/aikotoba/constraints/lockable_constraint.rb +7 -0
  49. data/lib/aikotoba/constraints/recoverable_constraint.rb +7 -0
  50. data/lib/aikotoba/constraints/registerable_constraint.rb +7 -0
  51. data/lib/aikotoba/engine.rb +7 -0
  52. data/lib/aikotoba/errors.rb +7 -0
  53. data/lib/aikotoba/test/authentication_helper.rb +48 -0
  54. data/lib/aikotoba/version.rb +5 -0
  55. data/lib/aikotoba.rb +45 -0
  56. data/lib/tasks/aikotoba_tasks.rake +4 -0
  57. metadata +128 -0
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikotoba
4
+ class SessionsController < ApplicationController
5
+ include Authenticatable
6
+
7
+ def new
8
+ return redirect_to after_sign_in_path if aikotoba_current_account
9
+ @account = build_account({email: "", password: ""})
10
+ end
11
+
12
+ def create
13
+ @account = authenticate_account(session_params.to_h.symbolize_keys)
14
+ if @account
15
+ before_sign_in_process
16
+ aikotoba_sign_in(@account)
17
+ after_sign_in_process
18
+ redirect_to after_sign_in_path, notice: successed_message
19
+ else
20
+ failed_sign_in_process
21
+ @account = build_account({email: "", password: ""})
22
+ flash[:alert] = failed_message
23
+ render :new, status: :unprocessable_entity
24
+ end
25
+ end
26
+
27
+ def destroy
28
+ aikotoba_sign_out if aikotoba_current_account
29
+ redirect_to after_sign_out_path, notice: signed_out_message
30
+ end
31
+
32
+ private
33
+
34
+ def session_params
35
+ params.require(:account).permit(:email, :password)
36
+ end
37
+
38
+ def build_account(params)
39
+ Account.build_by(attributes: params)
40
+ end
41
+
42
+ def authenticate_account(params)
43
+ Account.authenticate_by(attributes: params)
44
+ end
45
+
46
+ def after_sign_in_path
47
+ Aikotoba.after_sign_in_path
48
+ end
49
+
50
+ def after_sign_out_path
51
+ Aikotoba.after_sign_out_path
52
+ end
53
+
54
+ def successed_message
55
+ I18n.t(".aikotoba.messages.authentication.success")
56
+ end
57
+
58
+ def failed_message
59
+ I18n.t(".aikotoba.messages.authentication.failed")
60
+ end
61
+
62
+ def signed_out_message
63
+ I18n.t(".aikotoba.messages.authentication.sign_out")
64
+ end
65
+
66
+ # NOTE: Methods to override if you want to do something before sign in.
67
+ def before_sign_in_process
68
+ end
69
+
70
+ # NOTE: Methods to override if you want to do something after sign in.
71
+ def after_sign_in_process
72
+ end
73
+
74
+ # NOTE: Methods to override if you want to do something failed sign in.
75
+ def failed_sign_in_process(e = nil)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikotoba
4
+ class UnlocksController < ApplicationController
5
+ include Protection::TimingAtack
6
+
7
+ before_action :prevent_timing_atack, only: [:update]
8
+
9
+ def new
10
+ @account = build_account({email: "", password: ""})
11
+ end
12
+
13
+ def create
14
+ account = find_by_send_token_account!(unlock_accounts_params)
15
+ before_send_unlock_token_process
16
+ send_token_account!(account)
17
+ after_send_unlock_token_process
18
+ redirect_to success_send_unlock_token_path, flash: {notice: success_send_unlock_token_message}
19
+ rescue ActiveRecord::RecordNotFound => e
20
+ failed_send_unlock_token_process(e)
21
+ @account = build_account({email: "", password: ""})
22
+ flash[:alert] = failed_send_unlock_token_message
23
+ render :new, status: :unprocessable_entity
24
+ end
25
+
26
+ def update
27
+ account = find_by_has_token_account!(params)
28
+ before_unlock_process
29
+ unlock_account!(account)
30
+ after_unlock_process
31
+ redirect_to after_unlocked_path, flash: {notice: unlocked_message}
32
+ end
33
+
34
+ private
35
+
36
+ def unlock_accounts_params
37
+ params.require(:account).permit(:email)
38
+ end
39
+
40
+ def build_account(params)
41
+ Account.build_by(attributes: params)
42
+ end
43
+
44
+ def find_by_send_token_account!(params)
45
+ Account.locked.find_by!(email: params[:email])
46
+ end
47
+
48
+ def send_token_account!(account)
49
+ Account::Service::Lock.create_unlock_token!(account: account, notify: true)
50
+ end
51
+
52
+ def find_by_has_token_account!(params)
53
+ Account::UnlockToken.active.find_by!(token: params[:token]).account
54
+ end
55
+
56
+ def unlock_account!(account)
57
+ # NOTE: Unlocking is done using URL tokens, so it is done in the writing role.
58
+ ActiveRecord::Base.connected_to(role: :writing) do
59
+ Account::Service::Lock.unlock!(account: account)
60
+ end
61
+ end
62
+
63
+ def after_unlocked_path
64
+ aikotoba.new_session_path
65
+ end
66
+
67
+ def success_send_unlock_token_path
68
+ aikotoba.new_session_path
69
+ end
70
+
71
+ def unlocked_message
72
+ I18n.t(".aikotoba.messages.unlocking.success")
73
+ end
74
+
75
+ def success_send_unlock_token_message
76
+ I18n.t(".aikotoba.messages.unlocking.sent")
77
+ end
78
+
79
+ def failed_send_unlock_token_message
80
+ I18n.t(".aikotoba.messages.unlocking.failed")
81
+ end
82
+
83
+ # NOTE: Methods to override if you want to do something before send unlock token.
84
+ def before_send_unlock_token_process
85
+ end
86
+
87
+ # NOTE: Methods to override if you want to do something after send unlock token.
88
+ def after_send_unlock_token_process
89
+ end
90
+
91
+ # NOTE: Methods to override if you want to do something failed send unlock token.
92
+ def failed_send_unlock_token_process(e)
93
+ end
94
+
95
+ # NOTE: Methods to override if you want to do something before unlock.
96
+ def before_unlock_process
97
+ end
98
+
99
+ # NOTE: Methods to override if you want to do something after unlock.
100
+ def after_unlock_process
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,40 @@
1
+ module Aikotoba
2
+ module Authenticatable
3
+ extend ActiveSupport::Concern
4
+ include Protection::SessionFixationAttack
5
+
6
+ def aikotoba_current_account
7
+ unless defined?(@aikotoba_current_account)
8
+ @aikotoba_current_account ||= aikotoba_authenticate_by_session
9
+ end
10
+ @aikotoba_current_account
11
+ end
12
+
13
+ def aikotoba_sign_in(account)
14
+ prevent_session_fixation_attack
15
+ session[aikotoba_session_key] = account.id
16
+ end
17
+
18
+ def aikotoba_sign_out
19
+ @aikotoba_current_account = nil
20
+ reset_session
21
+ end
22
+
23
+ # NOTE: Even if there is already a session, verify that it can be authenticated, and if not, reset the session,
24
+ # in case the session is created and then locked by another browser etc.
25
+ def aikotoba_authenticate_by_session
26
+ account = Account.authenticatable.find_by(id: session[aikotoba_session_key])
27
+ account.tap { |account| reset_aikotoba_session unless account }
28
+ end
29
+
30
+ private
31
+
32
+ def reset_aikotoba_session
33
+ session[aikotoba_session_key] = nil
34
+ end
35
+
36
+ def aikotoba_session_key
37
+ Aikotoba.session_key
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # NOTE: Provides the ability to refresh session before sign_in for session fixation attacks.
4
+ # https://owasp.org/www-community/attacks/Session_fixation
5
+ module Aikotoba
6
+ module Protection::SessionFixationAttack
7
+ extend ActiveSupport::Concern
8
+
9
+ def prevent_session_fixation_attack
10
+ reflesh_session
11
+ end
12
+
13
+ private
14
+
15
+ def reflesh_session
16
+ old_session = session.dup.to_hash
17
+ reset_session
18
+ old_session.each_pair { |k, v| session[k.to_sym] = v }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # NOTE: Add random delay for Timing attack.
4
+ # https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/10-Business_Logic_Testing/04-Test_for_Process_Timing
5
+ module Aikotoba
6
+ module Protection::TimingAtack
7
+ extend ActiveSupport::Concern
8
+
9
+ def prevent_timing_atack
10
+ random_delay if aikotoba_prevent_timing_atack
11
+ end
12
+
13
+ private
14
+
15
+ def aikotoba_prevent_timing_atack
16
+ Aikotoba.prevent_timing_atack
17
+ end
18
+
19
+ def random_delay
20
+ sleep (1..5).to_a.sample / 100.0
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ module Aikotoba
2
+ class AccountMailer < ApplicationMailer
3
+ def confirm
4
+ @account = params[:account]
5
+ @token = @account.confirmation_token
6
+ @confirm_url = aikotoba.confirm_account_url(token: @token.token)
7
+ mail(to: @account.email, subject: I18n.t(".aikotoba.mailers.confirm.subject"))
8
+ end
9
+
10
+ def unlock
11
+ @account = params[:account]
12
+ @token = @account.unlock_token
13
+ @unlock_url = aikotoba.unlock_account_url(token: @token.token)
14
+ mail(to: @account.email, subject: I18n.t(".aikotoba.mailers.unlock.subject"))
15
+ end
16
+
17
+ def recover
18
+ @account = params[:account]
19
+ @token = @account.recovery_token
20
+ @recover_url = aikotoba.edit_account_password_url(token: @token.token)
21
+ mail(to: @account.email, subject: I18n.t(".aikotoba.mailers.recover.subject"))
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ module Aikotoba
2
+ class ApplicationMailer < Aikotoba.parent_mailer.constantize
3
+ default from: Aikotoba.mailer_sender
4
+ end
5
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikotoba
4
+ class Account::ConfirmationToken < ApplicationRecord
5
+ include TokenEncryptable
6
+ belongs_to :account, class_name: "Aikotoba::Account", foreign_key: "aikotoba_account_id"
7
+ validates :token, presence: true
8
+ validates :expired_at, presence: true
9
+
10
+ scope :active, ->(now: Time.current) { where("expired_at >= ?", now) }
11
+
12
+ after_initialize do |record|
13
+ token = Account::Value::Token.new(extipry: Aikotoba.confirmation_token_expiry)
14
+ record.token ||= token.value
15
+ record.expired_at ||= token.expired_at
16
+ end
17
+
18
+ def notify
19
+ AccountMailer.with(account: account).confirm.deliver_now
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikotoba
4
+ class Account::RecoveryToken < ApplicationRecord
5
+ include TokenEncryptable
6
+ belongs_to :account, class_name: "Aikotoba::Account", foreign_key: "aikotoba_account_id"
7
+ validates :token, presence: true
8
+ validates :expired_at, presence: true
9
+
10
+ scope :active, ->(now: Time.current) { where("expired_at >= ?", now) }
11
+
12
+ after_initialize do |record|
13
+ token = Account::Value::Token.new(extipry: Aikotoba.recovery_token_expiry)
14
+ record.token ||= token.value
15
+ record.expired_at ||= token.expired_at
16
+ end
17
+
18
+ def notify
19
+ AccountMailer.with(account: account).recover.deliver_now
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikotoba
4
+ class Account::Service::Authentication
5
+ def self.call!(email:, password:)
6
+ new(email: email, password: password).call!
7
+ end
8
+
9
+ def initialize(email:, password:)
10
+ @account_class = Account
11
+ @lock_service = Account::Service::Lock
12
+ @lockable = @account_class.lockable?
13
+ @email = email
14
+ @password = password
15
+ end
16
+
17
+ def call!
18
+ account = find_by_identifier
19
+ return prevent_timing_atack && nil unless account
20
+
21
+ authenticate(account).tap do |result|
22
+ ActiveRecord::Base.transaction do
23
+ result ? success_callback(account) : failed_callback(account)
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ # NOTE: Verify passwords even when accounts are not found to prevent timing attacks.
31
+ def prevent_timing_atack
32
+ return true unless aikotoba_prevent_timing_atack
33
+ account = @account_class.build_by(attributes: {email: @email, password: @password})
34
+ account.password_match?(@password)
35
+ true
36
+ end
37
+
38
+ def aikotoba_prevent_timing_atack
39
+ Aikotoba.prevent_timing_atack
40
+ end
41
+
42
+ def success_callback(account)
43
+ account.authentication_success!
44
+ end
45
+
46
+ def failed_callback(account)
47
+ account.authentication_failed!
48
+ lock_when_should_lock!(account) if @lockable
49
+ end
50
+
51
+ def find_by_identifier
52
+ @account_class.authenticatable.find_by(email: @email)
53
+ end
54
+
55
+ def authenticate(account)
56
+ account.password_match?(@password) ? account : nil
57
+ end
58
+
59
+ concerning :Lockable do
60
+ def lock_when_should_lock!(account)
61
+ @lock_service.lock!(account: account, notify: true) if account.should_lock?
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikotoba
4
+ class Account::Service::Confirmation
5
+ def self.create_token!(account:, notify: false)
6
+ new(account: account).create_token!(notify: notify)
7
+ end
8
+
9
+ def self.confirm!(account:)
10
+ new(account: account).confirm!
11
+ end
12
+
13
+ def initialize(account:)
14
+ @account = account
15
+ end
16
+
17
+ def create_token!(notify:)
18
+ ActiveRecord::Base.transaction do
19
+ @account.build_confirmation_token.save!
20
+ @account.confirmation_token.notify if notify
21
+ end
22
+ end
23
+
24
+ def confirm!
25
+ ActiveRecord::Base.transaction do
26
+ @account.confirm!
27
+ @account.confirmation_token&.destroy!
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikotoba
4
+ class Account::Service::Lock
5
+ def self.lock!(account:, notify: false)
6
+ new(account: account).lock!(notify: notify)
7
+ end
8
+
9
+ def self.unlock!(account:)
10
+ new(account: account).unlock!
11
+ end
12
+
13
+ def self.create_unlock_token!(account:, notify: false)
14
+ new(account: account).create_unlock_token!(notify: notify)
15
+ end
16
+
17
+ def initialize(account:)
18
+ @account = account
19
+ end
20
+
21
+ def lock!(notify:)
22
+ ActiveRecord::Base.transaction do
23
+ @account.lock!
24
+ create_unlock_token!(notify: notify)
25
+ end
26
+ end
27
+
28
+ def unlock!
29
+ ActiveRecord::Base.transaction do
30
+ @account.unlock!
31
+ @account.unlock_token&.destroy!
32
+ end
33
+ end
34
+
35
+ def create_unlock_token!(notify:)
36
+ ActiveRecord::Base.transaction do
37
+ @account.build_unlock_token.save!
38
+ @account.unlock_token.notify if notify
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikotoba
4
+ class Account::Service::Recovery
5
+ def self.create_token!(account:, notify: false)
6
+ new(account: account).create_token!(notify: notify)
7
+ end
8
+
9
+ def self.recover!(account:, new_password:)
10
+ new(account: account).recover!(new_password: new_password)
11
+ end
12
+
13
+ def initialize(account:)
14
+ @account = account
15
+ end
16
+
17
+ def create_token!(notify:)
18
+ ActiveRecord::Base.transaction do
19
+ @account.build_recovery_token.save!
20
+ @account.recovery_token.notify if notify
21
+ end
22
+ end
23
+
24
+ def recover!(new_password:)
25
+ ActiveRecord::Base.transaction do
26
+ @account.recover!(new_password: new_password)
27
+ @account.recovery_token&.destroy!
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikotoba
4
+ class Account::Service::Registration
5
+ def self.call!(account:)
6
+ new.call!(account: account)
7
+ end
8
+
9
+ def initialize
10
+ @account_class = Account
11
+ @confirm_service = Account::Service::Confirmation
12
+ @confirmable = @account_class.confirmable?
13
+ end
14
+
15
+ def call!(account:)
16
+ ActiveRecord::Base.transaction do
17
+ account.save!
18
+ send_confirmation_token!(account) if @confirmable
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ concerning :Confirmable do
25
+ def send_confirmation_token!(account)
26
+ @confirm_service.create_token!(account: account, notify: true)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikotoba
4
+ class Account::UnlockToken < ApplicationRecord
5
+ include TokenEncryptable
6
+ belongs_to :account, class_name: "Aikotoba::Account", foreign_key: "aikotoba_account_id"
7
+ validates :token, presence: true
8
+ validates :expired_at, presence: true
9
+
10
+ scope :active, ->(now: Time.current) { where("expired_at >= ?", now) }
11
+
12
+ after_initialize do |record|
13
+ token = Account::Value::Token.new(extipry: Aikotoba.unlock_token_expiry)
14
+ record.token ||= token.value
15
+ record.expired_at ||= token.expired_at
16
+ end
17
+
18
+ def notify
19
+ AccountMailer.with(account: account).unlock.deliver_now
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "argon2"
4
+
5
+ module Aikotoba
6
+ class Account::Value::Password
7
+ LENGTH_RENGE = Aikotoba.password_length_range
8
+
9
+ def initialize(
10
+ value:,
11
+ pepper: Aikotoba.password_pepper
12
+ )
13
+ @value = value
14
+ @pepper = pepper
15
+ end
16
+
17
+ attr_reader :value
18
+
19
+ def match?(digest:)
20
+ verify_password?(password_with_pepper(value), digest)
21
+ end
22
+
23
+ def digest
24
+ return "" if value.blank?
25
+ generate_hash(password_with_pepper(value))
26
+ end
27
+
28
+ private
29
+
30
+ def verify_password?(password, digest)
31
+ Argon2::Password.verify_password(password, digest)
32
+ rescue Argon2::ArgonHashFail # NOTE: If an invalid digest is passed, consider it a mismatch.
33
+ false
34
+ end
35
+
36
+ def password_with_pepper(password)
37
+ "#{password}-#{@pepper}"
38
+ end
39
+
40
+ def generate_hash(password)
41
+ # NOTE: Adjusted to be OWASAP's recommended value by default.
42
+ # > Use Argon2id with a minimum configuration of 15 MiB of memory, an iteration count of 2, and 1 degree of parallelism.
43
+ # > https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#introduction
44
+ argon = Argon2::Password.new(t_cost: 2, m_cost: 14, p_cost: 1)
45
+ argon.create(password)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikotoba
4
+ class Account::Value::Token
5
+ def initialize(extipry:)
6
+ @value = build_token
7
+ @expired_at = extipry.since
8
+ end
9
+
10
+ attr_reader :value, :expired_at
11
+
12
+ private
13
+
14
+ def build_token
15
+ SecureRandom.urlsafe_base64(32)
16
+ end
17
+ end
18
+ end