diff --git a/app/models/super_admin.rb b/app/models/super_admin.rb index 9fe9b4590..c7a1f9926 100644 --- a/app/models/super_admin.rb +++ b/app/models/super_admin.rb @@ -16,6 +16,7 @@ # last_sign_in_ip :string # locked_at :datetime # otp_required_for_login :boolean +# otp_secret :string # remember_created_at :datetime # reset_password_sent_at :datetime # reset_password_token :string @@ -43,6 +44,7 @@ class SuperAdmin < ApplicationRecord def disable_otp! self.assign_attributes( { + otp_secret: nil, encrypted_otp_secret: nil, encrypted_otp_secret_iv: nil, encrypted_otp_secret_salt: nil, @@ -66,4 +68,75 @@ class SuperAdmin < ApplicationRecord def send_devise_notification(notification, *args) devise_mailer.send(notification, self, *args).deliver_later end + + private + + # From https://github.com/tinfoil/devise-two-factor/blob/main/UPGRADING.md + # Remove me after super admin have been migrated to the new OTP system. + # 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] + + # 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/db/migrate/20230502160046_add_otp_secret_to_super_admin.rb b/db/migrate/20230502160046_add_otp_secret_to_super_admin.rb new file mode 100644 index 000000000..9a591434a --- /dev/null +++ b/db/migrate/20230502160046_add_otp_secret_to_super_admin.rb @@ -0,0 +1,5 @@ +class AddOtpSecretToSuperAdmin < ActiveRecord::Migration[7.0] + def change + add_column :super_admins, :otp_secret, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 7dbd65dd7..d3fc165be 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_03_31_125931) do +ActiveRecord::Schema[7.0].define(version: 2023_05_02_160046) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -843,6 +843,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_03_31_125931) do t.boolean "otp_required_for_login" t.datetime "remember_created_at", precision: 6 t.datetime "reset_password_sent_at", precision: 6 + t.string "otp_secret" t.string "reset_password_token" t.integer "sign_in_count", default: 0, null: false t.string "unlock_token"