Class: ActiveCipherStorage::Adapters::ActiveStorageService

Inherits:
Object
  • Object
show all
Defined in:
lib/active_cipher_storage/adapters/active_storage_service.rb

Overview

Active Storage service that transparently encrypts uploads and decrypts downloads. Configure in config/storage.yml:

encrypted_s3:
  service: ActiveCipherStorage
  wrapped_service: s3

Backward compatibility ───────────────────────Blobs uploaded before encryption was enabled are detected via the “ACSx01” magic header. If the magic is absent the raw bytes are returned as-is, so the service is safe to enable on a bucket with existing plaintext objects.

Range requests (download_chunk) must decrypt the full blob first because GCM authentication requires the complete ciphertext before any plaintext can be safely released.

Defined Under Namespace

Classes: BlobRef

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(wrapped_service:, **_kwargs) ⇒ ActiveStorageService

Returns a new instance of ActiveStorageService.



28
29
30
31
32
# File 'lib/active_cipher_storage/adapters/active_storage_service.rb', line 28

def initialize(wrapped_service:, **_kwargs)
  @inner         = wrapped_service
  @cipher        = Cipher.new
  @stream_cipher = StreamCipher.new
end

Instance Attribute Details

#innerObject (readonly)

Returns the value of attribute inner.



22
23
24
# File 'lib/active_cipher_storage/adapters/active_storage_service.rb', line 22

def inner
  @inner
end

Class Method Details

.build(configurator:, wrapped_service:, **kwargs) ⇒ Object



24
25
26
# File 'lib/active_cipher_storage/adapters/active_storage_service.rb', line 24

def self.build(configurator:, wrapped_service:, **kwargs)
  new(wrapped_service: configurator.build(wrapped_service), **kwargs)
end

Instance Method Details

#delete(key) ⇒ Object



92
# File 'lib/active_cipher_storage/adapters/active_storage_service.rb', line 92

def delete(key)          = @inner.delete(key)

#delete_prefixed(pfx) ⇒ Object



93
# File 'lib/active_cipher_storage/adapters/active_storage_service.rb', line 93

def delete_prefixed(pfx) = @inner.delete_prefixed(pfx)

#download(key, &block) ⇒ Object



57
58
59
60
61
62
63
64
65
# File 'lib/active_cipher_storage/adapters/active_storage_service.rb', line 57

def download(key, &block)
  raw = collect_download(key)

  # Legacy plaintext blob — no magic header present.
  return (block ? yield(raw) : raw) unless cipher_payload?(raw)

  plaintext = decrypt_raw(raw)
  block ? yield(plaintext) : plaintext
end

#download_chunk(key, range) ⇒ Object



67
68
69
# File 'lib/active_cipher_storage/adapters/active_storage_service.rb', line 67

def download_chunk(key, range)
  download(key).b[range]
end

#download_raw(key) ⇒ Object

Used by KeyRotation to fetch raw ciphertext without decrypting.



72
73
74
# File 'lib/active_cipher_storage/adapters/active_storage_service.rb', line 72

def download_raw(key)
  collect_download(key)
end

#exist?(key) ⇒ Boolean

Returns:

  • (Boolean)


94
# File 'lib/active_cipher_storage/adapters/active_storage_service.rb', line 94

def exist?(key)          = @inner.exist?(key)

#headers_for_direct_uploadObject



106
# File 'lib/active_cipher_storage/adapters/active_storage_service.rb', line 106

def headers_for_direct_upload(*) = {}

#rekey(key, old_provider:, new_provider:) ⇒ Object

Re-wraps the DEK in a single blob’s header under new_provider without decrypting or re-encrypting the file body.



83
84
85
86
87
88
89
90
# File 'lib/active_cipher_storage/adapters/active_storage_service.rb', line 83

def rekey(key, old_provider:, new_provider:)
  KeyRotation.rotate_blob(
    BlobRef.new(key),
    old_provider: old_provider,
    new_provider: new_provider,
    service:      self
  )
end

#upload(key, io, checksum: nil, content_type: nil, filename: nil, disposition: nil, custom_metadata: {}) ⇒ Object



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/active_cipher_storage/adapters/active_storage_service.rb', line 34

def upload(key, io, checksum: nil, content_type: nil, filename: nil,
           disposition: nil, custom_metadata: {})
  unless ActiveCipherStorage.configuration.encrypt_uploads
    @inner.upload(key, io,
      checksum:        checksum,
      content_type:    content_type,
      filename:        filename,
      disposition:     disposition,
      custom_metadata: )
    BlobMetadata.write_plaintext(key)
    return
  end

  @inner.upload(key, encrypt_io(io),
    checksum:        nil,  # checksum is over plaintext; skip for ciphertext
    content_type:    "application/octet-stream",
    filename:        filename,
    disposition:     disposition,
    custom_metadata: )

  BlobMetadata.write(key, ActiveCipherStorage.configuration.provider)
end

#upload_raw(key, io) ⇒ Object

Used by KeyRotation to overwrite a blob’s bytes without re-encrypting.



77
78
79
# File 'lib/active_cipher_storage/adapters/active_storage_service.rb', line 77

def upload_raw(key, io)
  @inner.upload(key, io, content_type: "application/octet-stream")
end

#url(key, expires_in:, filename:, content_type:, disposition:) ⇒ Object



96
97
98
99
# File 'lib/active_cipher_storage/adapters/active_storage_service.rb', line 96

def url(key, expires_in:, filename:, content_type:, disposition:, **)
  @inner.url(key, expires_in: expires_in, filename: filename,
                  content_type: content_type, disposition: disposition)
end

#url_for_direct_uploadObject



101
102
103
104
# File 'lib/active_cipher_storage/adapters/active_storage_service.rb', line 101

def url_for_direct_upload(*)
  raise Errors::UnsupportedOperation,
        "Direct uploads bypass encryption — use server-side upload instead"
end