必须使用 attr_encrypted 指定 iv 错误

must specify an iv error using attr_encrypted

我正在尝试将加密的 jsonb 字段添加到 rails 中的用户模型。我在尝试读取或设置值时遇到错误。

错误

irb(main):002:0> User.last.q2_email_address = "bob@bob.com"
  User Load (0.4ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT   [["LIMIT", 1]]
  Encrypt Data Key (190.1ms)  Context: {:model_name=>"User", :model_id=>11}
/Users/antarr/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/encryptor-3.0.0/lib/encryptor.rb:61:in `crypt': must specify an iv (ArgumentError)

user.rb

    has_kms_key eager_encrypt: :fetch_id
    attr_encrypted :settings, key: :kms_key, mashall: true
  
    store :settings,
          accessors: [:address1, :address2, :city, :customer_id, :customer_name, :customer_primary_cif, :social_security_number,
                      :email_address, :first_name, :group_desc, :group_id, :home_phone, :aba, :hq_session_id, :language, :last_name,
                      :login_name, :middle_name, :mobile_phone, :postal_code, :ssn, :state, :user_logon_id, :user_id, :user_primary_cif,
                      :work_phone, :ip_address, :token], coder: JSON, prefix: :q2

迁移

  def up
    add_column :users, :encrypted_kms_key, :text
    add_column :users, :encrypted_settings, :jsonb, null: false, default: '{}'
    add_column :users, :encrypted_settings_iv, :string
    add_index :users, :encrypted_settings, using: :gin
  end

config/initializer/aws.rb

Aws.config[:credentials] = Aws::Credentials.new(
  ENV['AMAZON_ACCESS_KEY'],
  ENV['AMAZON_SECRET_KEY']
)

宝石文件

gem 'attr_encrypted' # 3.1.0
gem 'aws-sdk-kms'
gem 'kms_encrypted'

gemattr_encrypted中有2个方法encrypt,一个是class方法,一个是实例方法,实例方法会自动生成random iv,同时class方法不会,它使用你设置的iv

调用User.last.q2_email_address = "bob@bob.com"时,this method会调用class方法encrypt,不会调用实例方法,所以如果不设置iv,会报错must specify an iv (ArgumentError)

有两种方法可以修复,第一种是设置 iv

attr_encrypted :settings, key: :kms_key, mashall: true, iv: SecureRandom.random_bytes(12)

不幸的是,将引发另一个错误(与 iv_len 相关),我仍然没有找出根本原因并修复它。

第二种方式:将this method复制到模型中,这样就会调用实例方法encrypt,随机生成一个iv,就可以了。

但是,在store attributes的情况下,它不保存encrypted attribute,例如

user = User.last
user.settings = {....}
user.save! # OK

user.q2_email_address = "bob@bob.com" 
user.settings # changed, but not re-encrypt `encrypted_settings`
user.save! # changed settings will not be saved

所以我想出了一个想法,我们可以创建一个模块支持 store attributes,如下所示

# model/concern/store_encrypted.rb
module StoreEncrypted
  def self.extended(base)
    base.class_eval do
     # initialize store attributes values with default `{}`
     # so that a new object or existed object that miss iv
     # will generate random iv
     def initialize(*args)
      super(*args)
      @@store_attr_encryptes.each do |attribute|
        instance_variable_get("@#{attribute}") || send("#{attribute}=", {})
      end
     end

      alias old_save! save!
      def save!(**options, &block)
        # re-set attribute (encrypt again)
        @@store_attr_encryptes&.each do |attribute|
          send("#{attribute}=", send("#{attribute}"))
        end
        old_save!(**options, &block)
      end
    end
  end

  def store_encrypted(attribute, options={})
    @@store_attr_encryptes ||= []
    @@store_attr_encryptes << attribute
    attr_encrypted attribute, key: options[:key], marshal: options[:marshal]
    store attribute, accessors: options[:accessors], coder: options[:coder], 
                      prefix: options[:prefix], suffix: options[:suffix]
    # copy
    define_method("#{attribute}=") do |value|
      send("encrypted_#{attribute}=", encrypt(attribute, value))
      instance_variable_set("@#{attribute}", value)
    end
  end
end

# user.rb
class User < ActiveRecord::Base
  extend StoreEncrypted
  has_kms_key eager_encrypt: :fetch_id
  store_encrypted :settings, key: :kms_key, mashall: true,
                  accessors: [...], coder: JSON, prefix: :q2
end

#
user = User.last
user.q2_email_address = "bob@bob.com" 
user.save! # it'll re-encrypt settings