Module: ActiveCipherStorage::BlobMetadata

Defined in:
lib/active_cipher_storage/blob_metadata.rb

Overview

Reads and writes encryption metadata on ActiveStorage::Blob records.

Written fields (all stored under the blob’s existing ‘metadata` JSON column):

encrypted      => true
cipher_version => Integer  (Format::VERSION)
provider_id    => String   (e.g. "aws_kms", "env")
kms_key_id     => String   (CMK ARN, env-var name, or nil)

These are for operational visibility — rotation queries, auditing, backward-compat detection. The encrypted file header is always the authoritative source for decryption.

Class Method Summary collapse

Class Method Details

.blobs_for(provider) ⇒ Object

Finds all blobs whose metadata matches the given provider. Iterates in batches to avoid loading all blobs into memory. Yields each matching blob.

For large tables, add a DB-level index on ‘metadata->>’kms_key_id’‘ and narrow the scope before passing to this method.



64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/active_cipher_storage/blob_metadata.rb', line 64

def self.blobs_for(provider)
  return enum_for(:blobs_for, provider) unless block_given?
  return unless active_storage_available?

  ActiveStorage::Blob.find_each do |blob|
    meta = blob.
    next unless meta["encrypted"] == true
    next unless meta["provider_id"] == provider.provider_id
    next if provider.key_id && meta["kms_key_id"] != provider.key_id

    yield blob
  end
end

.for(storage_key) ⇒ Object

Returns the metadata hash for a blob, or nil if AR is unavailable.



53
54
55
56
# File 'lib/active_cipher_storage/blob_metadata.rb', line 53

def self.for(storage_key)
  return nil unless active_storage_available?
  ActiveStorage::Blob.find_by(key: storage_key)&.
end

.update_after_rotation(storage_key, new_provider) ⇒ Object



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/active_cipher_storage/blob_metadata.rb', line 34

def self.update_after_rotation(storage_key, new_provider)
  return unless active_storage_available?

  blob = ActiveStorage::Blob.find_by(key: storage_key)
  return unless blob

  blob.update_columns(
    metadata: blob..merge(
      "provider_id" => new_provider.provider_id,
      "kms_key_id"  => new_provider.key_id
    ).compact
  )
rescue => e
  ActiveCipherStorage.configuration.logger.warn(
    "[ActiveCipherStorage] Could not update rotation metadata for #{storage_key}: #{e.message}"
  )
end

.write(storage_key, provider) ⇒ Object



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/active_cipher_storage/blob_metadata.rb', line 14

def self.write(storage_key, provider)
  return unless active_storage_available?

  blob = ActiveStorage::Blob.find_by(key: storage_key)
  return unless blob

  blob.update_columns(
    metadata: blob..merge(
      "encrypted"      => true,
      "cipher_version" => Format::VERSION,
      "provider_id"    => provider.provider_id,
      "kms_key_id"     => provider.key_id
    ).compact
  )
rescue => e
  ActiveCipherStorage.configuration.logger.warn(
    "[ActiveCipherStorage] Could not write blob metadata for #{storage_key}: #{e.message}"
  )
end