summaryrefslogtreecommitdiffstats
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/concerns/legacy_otp_secret.rb77
-rw-r--r--app/models/user.rb8
2 files changed, 80 insertions, 5 deletions
diff --git a/app/models/concerns/legacy_otp_secret.rb b/app/models/concerns/legacy_otp_secret.rb
new file mode 100644
index 00000000000..466c4ec9bb3
--- /dev/null
+++ b/app/models/concerns/legacy_otp_secret.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+# TODO: This file is here for legacy support during devise-two-factor upgrade.
+# It should be removed after all records have been migrated.
+
+module LegacyOtpSecret
+ extend ActiveSupport::Concern
+
+ private
+
+ # Decrypt and return the `encrypted_otp_secret` attribute which was used in
+ # prior versions of devise-two-factor
+ # @return [String] The decrypted OTP secret
+ def legacy_otp_secret
+ return nil unless self[:encrypted_otp_secret]
+ return nil unless self.class.otp_secret_encryption_key
+
+ hmac_iterations = 2000 # a default set by the Encryptor gem
+ key = self.class.otp_secret_encryption_key
+ salt = Base64.decode64(encrypted_otp_secret_salt)
+ iv = Base64.decode64(encrypted_otp_secret_iv)
+
+ raw_cipher_text = Base64.decode64(encrypted_otp_secret)
+ # The last 16 bytes of the ciphertext are the authentication tag - we use
+ # Galois Counter Mode which is an authenticated encryption mode
+ cipher_text = raw_cipher_text[0..-17]
+ auth_tag = raw_cipher_text[-16..-1] # rubocop:disable Style/SlicingWithRange
+
+ # this alrorithm lifted from
+ # https://github.com/attr-encrypted/encryptor/blob/master/lib/encryptor.rb#L54
+
+ # create an OpenSSL object which will decrypt the AES cipher with 256 bit
+ # keys in Galois Counter Mode (GCM). See
+ # https://ruby.github.io/openssl/OpenSSL/Cipher.html
+ cipher = OpenSSL::Cipher.new('aes-256-gcm')
+
+ # tell the cipher we want to decrypt. Symmetric algorithms use a very
+ # similar process for encryption and decryption, hence the same object can
+ # do both.
+ cipher.decrypt
+
+ # Use a Password-Based Key Derivation Function to generate the key actually
+ # used for encryptoin from the key we got as input.
+ cipher.key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(key, salt, hmac_iterations, cipher.key_len)
+
+ # set the Initialization Vector (IV)
+ cipher.iv = iv
+
+ # The tag must be set after calling Cipher#decrypt, Cipher#key= and
+ # Cipher#iv=, but before calling Cipher#final. After all decryption is
+ # performed, the tag is verified automatically in the call to Cipher#final.
+ #
+ # If the auth_tag does not verify, then #final will raise OpenSSL::Cipher::CipherError
+ cipher.auth_tag = auth_tag
+
+ # auth_data must be set after auth_tag has been set when decrypting See
+ # http://ruby-doc.org/stdlib-2.0.0/libdoc/openssl/rdoc/OpenSSL/Cipher.html#method-i-auth_data-3D
+ # we are not adding any authenticated data but OpenSSL docs say this should
+ # still be called.
+ cipher.auth_data = ''
+
+ # #update is (somewhat confusingly named) the method which actually
+ # performs the decryption on the given chunk of data. Our OTP secret is
+ # short so we only need to call it once.
+ #
+ # It is very important that we call #final because:
+ #
+ # 1. The authentication tag is checked during the call to #final
+ # 2. Block based cipher modes (e.g. CBC) work on fixed size chunks. We need
+ # to call #final to get it to process the last chunk properly. The output
+ # of #final should be appended to the decrypted value. This isn't
+ # required for streaming cipher modes but including it is a best practice
+ # so that your code will continue to function correctly even if you later
+ # change to a block cipher mode.
+ cipher.update(cipher_text) + cipher.final
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 584120cf2eb..8bc0b23ce84 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -39,6 +39,7 @@
# role_id :bigint(8)
# settings :text
# time_zone :string
+# otp_secret :string
#
class User < ApplicationRecord
@@ -72,6 +73,8 @@ class User < ApplicationRecord
devise :two_factor_authenticatable,
otp_secret_encryption_key: Rails.configuration.x.otp_secret
+ include LegacyOtpSecret # Must be after the above `devise` line in order to override the legacy method
+
devise :two_factor_backupable,
otp_number_of_backup_codes: 10
@@ -131,11 +134,6 @@ class User < ApplicationRecord
normalizes :time_zone, with: ->(time_zone) { ActiveSupport::TimeZone[time_zone].nil? ? nil : time_zone }
normalizes :chosen_languages, with: ->(chosen_languages) { chosen_languages.compact_blank.presence }
- # This avoids a deprecation warning from Rails 5.1
- # It seems possible that a future release of devise-two-factor will
- # handle this itself, and this can be removed from our User class.
- attribute :otp_secret
-
has_many :session_activations, dependent: :destroy
delegate :can?, to: :role