Rails 7 Active Record记录级加密密钥删除后仍可解密问题咨询
问题排查
你的代码存在4个核心逻辑错误,导致自定义的单记录密钥完全没生效,删除密钥后自然还能解密:
encrypts配置的key参数是类加载阶段求值的,你传入的self.instance_encryption_key是类方法,只会在Rails加载User类时执行一次,后续读写单条记录的加密属性时,根本不会读取你给每个实例赋值的p_encryption_key,全程用的是Rails全局默认加密密钥,你自己生成的单条密钥从头到尾没参与加解密流程。- 方法名拼写错误:
get_user_encryption_key里调用的是不存在的user_key_name方法,你实际定义的缓存key方法叫profile_key_name,就算密钥逻辑走到这一步也会报错。 - 开启了
deterministic: true(确定性加密),该模式为了支持等值查询,强制使用全局固定的确定性加密密钥,本身就不支持单记录自定义密钥。 after_initialize回调在每次实例化User对象时都会执行,一旦缓存没命中就会自动生成新密钥,哪怕你删了老密钥,下次加载记录也会生成新key,完全无法匹配历史密文。
正确实现方案
Rails 7原生提供了单记录动态密钥的接口,不需要自己写零散的回调实现,完全满足「删密钥即永久无法解密」的crypto shredding需求:
- 不要对需要擦除的加密字段开
deterministic: true,如果需要按该字段查询,单独存一个确定性加密的哈希字段做索引即可。 - 不要把密钥只存在缓存里,必须持久化到独立的密钥表,缓存仅做读取加速,避免缓存丢失导致全量数据无法解密。
- 重写模型实例的
encryption_key_for方法,这是Rails加解密单条记录属性时会自动调用的动态密钥入口,直接返回对应记录的专属密钥即可。
实现代码
首先生成密钥表迁移:
class CreateUserEncryptionKeys < ActiveRecord::Migration[7.0] def change create_table :user_encryption_keys do |t| t.references :user, null: false, foreign_key: true, index: { unique: true } t.string :key, null: false t.timestamps end end end
密钥表关联模型:
class UserEncryptionKey < ActiveRecord::Base belongs_to :user end
修正后的User模型:
class User < ActiveRecord::Base has_one :encryption_key_record, class_name: 'UserEncryptionKey', dependent: :destroy # 需要擦除的字段关闭确定性加密 encrypts :last_name, deterministic: false # 单独存哈希字段用于查询,开启确定性加密 encrypts :last_name_query_hash, deterministic: true before_save :sync_last_name_query_hash, if: :last_name_changed? # Rails原生动态密钥入口,每次加解密指定属性时自动调用 def encryption_key_for(attribute) case attribute when :last_name fetch_or_generate_per_record_key else super # 其他属性沿用全局加密配置 end end # 数据擦除方法:删除密钥后该记录的last_name永久无法解密 def crypto_shred_last_name encryption_key_record&.destroy Rails.cache.delete(per_record_key_cache_key) end private def sync_last_name_query_hash self.last_name_query_hash = last_name end def per_record_key_cache_key "user_encryption_key/#{id}" end def fetch_or_generate_per_record_key return nil if new_record? # 优先读缓存,缓存miss读数据库,数据库无记录则生成新密钥持久化 Rails.cache.fetch(per_record_key_cache_key) do (encryption_key_record || create_encryption_key_record!).key end end def create_encryption_key_record random_key = ActiveRecord::Encryption.key_generator.generate_random_hex_key(length: 16) create_encryption_key_record!(key: random_key) end end
验证逻辑
创建一条带last_name的User记录后,调用user.crypto_shred_last_name删除密钥,重新从数据库加载该记录再读取last_name时,会直接抛出ActiveRecord::Encryption::Errors::Decryption解密错误,符合预期。
内容的提问来源于stack exchange,提问作者Louie
相关产品推荐
相关产品推荐

