77 lines
		
	
	
	
		
			3.3 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			77 lines
		
	
	
	
		
			3.3 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
| # 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
 |