active_cipher_storage 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -1
- data/README.md +97 -77
- data/lib/active_cipher_storage/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d9e4e6edf34d0d997fcc28765c599526e07eba891aabf24dfa6b739663002b51
|
|
4
|
+
data.tar.gz: 3de2f77f7bf9dd3f70049ffb2d4f988b49008ae294d1b4b9af493247fe557cdf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 90621f0a221f638a47587884a3fbdce8c303edb3e64c56b7aba96a1721408118aa9fb6c66cae3491aa30e7e33484d779615e088c48e5778590d3bdc8ba1e34bf
|
|
7
|
+
data.tar.gz: 7989b6cb3630975d9a03c48f067f7a8985b89c6fb287f4f43717d21d79c002b046281cbcd2028ff0aea2d0afde29baad6241c584c08d0f9f618b6e92c5552127
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.3] - 2026-04-25
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Update the README with clearer usage guidance and improved readability.
|
|
15
|
+
|
|
10
16
|
## [1.0.2] - 2026-04-25
|
|
11
17
|
|
|
12
18
|
### Changed
|
|
@@ -39,7 +45,8 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
|
|
|
39
45
|
- Header-only key rotation for re-wrapping encrypted DEKs.
|
|
40
46
|
- Unit and integration coverage for crypto, providers, Active Storage, S3, multipart upload, streaming, metadata, and key rotation.
|
|
41
47
|
|
|
42
|
-
[Unreleased]: https://github.com/codebyjass/active-cipher-storage/compare/v1.0.
|
|
48
|
+
[Unreleased]: https://github.com/codebyjass/active-cipher-storage/compare/v1.0.3...HEAD
|
|
49
|
+
[1.0.3]: https://github.com/codebyjass/active-cipher-storage/compare/v1.0.2...v1.0.3
|
|
43
50
|
[1.0.2]: https://github.com/codebyjass/active-cipher-storage/compare/v1.0.1...v1.0.2
|
|
44
51
|
[1.0.1]: https://github.com/codebyjass/active-cipher-storage/compare/v1.0.0...v1.0.1
|
|
45
52
|
[1.0.0]: https://github.com/codebyjass/active-cipher-storage/releases/tag/v1.0.0
|
data/README.md
CHANGED
|
@@ -1,83 +1,88 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Active Cipher Storage
|
|
2
2
|
|
|
3
3
|
[](https://github.com/codebyjass/active-cipher-storage/actions/workflows/ruby.yml)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Active Cipher Storage is published as the `active_cipher_storage` Ruby gem.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
It adds Rails Active Storage encryption and decryption without changing the way your Rails app attaches files. Files are encrypted before they are stored in AWS S3 or another storage service, and decrypted when your app reads them back.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
This solves a common Rails security problem: sensitive files should be protected before they leave your application.
|
|
10
|
+
|
|
11
|
+
It works with normal Rails Active Storage attachments, direct S3 uploads from Ruby service objects, streaming downloads, and backend-managed multipart uploads for large files.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- Encrypt files before uploading them to S3 or Active Storage.
|
|
16
|
+
- Decrypt files automatically when downloading.
|
|
17
|
+
- Works with Rails Active Storage.
|
|
18
|
+
- Supports direct AWS S3 client usage.
|
|
19
|
+
- Handles large files with streaming AES-256-GCM encryption.
|
|
20
|
+
- Supports backend-managed multipart uploads for frontend chunk upload flows.
|
|
21
|
+
- Uses pluggable key providers: environment variables, AWS KMS, or custom KMS providers.
|
|
22
|
+
- Supports header-only key rotation without rewriting the full file body.
|
|
23
|
+
|
|
24
|
+
## Use Cases
|
|
25
|
+
|
|
26
|
+
- Encrypt user documents before storing them in S3.
|
|
27
|
+
- Secure financial records, contracts, medical files, invoices, and other sensitive uploads.
|
|
28
|
+
- Add application-level encryption on top of AWS S3 server-side encryption.
|
|
29
|
+
- Keep Rails Active Storage APIs while storing encrypted files.
|
|
30
|
+
- Stream large encrypted files from S3 without loading the whole file into memory.
|
|
31
|
+
- Meet compliance and privacy requirements around PII, GDPR, HIPAA-style data, or internal security policies.
|
|
12
32
|
|
|
13
33
|
## Contents
|
|
14
34
|
|
|
15
|
-
1. [
|
|
16
|
-
2. [
|
|
17
|
-
3. [
|
|
18
|
-
4. [
|
|
19
|
-
5. [
|
|
20
|
-
6. [
|
|
21
|
-
7. [
|
|
22
|
-
8. [
|
|
23
|
-
9. [
|
|
35
|
+
1. [Features](#features)
|
|
36
|
+
2. [Use Cases](#use-cases)
|
|
37
|
+
3. [How it works](#how-it-works)
|
|
38
|
+
4. [Installation](#installation)
|
|
39
|
+
5. [Rails / Active Storage setup](#rails--active-storage-setup)
|
|
40
|
+
6. [Standalone S3 usage](#standalone-s3-usage)
|
|
41
|
+
7. [Chunked multipart upload](#chunked-multipart-upload)
|
|
42
|
+
8. [Streaming download](#streaming-download)
|
|
43
|
+
9. [Manual encrypt / decrypt](#manual-encrypt--decrypt)
|
|
44
|
+
10. [Blob metadata](#blob-metadata)
|
|
45
|
+
11. [KMS providers](#kms-providers)
|
|
24
46
|
- [Environment-variable provider](#environment-variable-provider)
|
|
25
47
|
- [AWS KMS provider](#aws-kms-provider)
|
|
26
48
|
- [Custom provider](#custom-provider)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
49
|
+
12. [Key rotation](#key-rotation)
|
|
50
|
+
13. [Configuration reference](#configuration-reference)
|
|
51
|
+
14. [Encryption format](#encryption-format)
|
|
52
|
+
15. [Security notes](#security-notes)
|
|
53
|
+
16. [Testing](#testing)
|
|
54
|
+
17. [Contributing](#contributing)
|
|
55
|
+
18. [Security reports](#security-reports)
|
|
56
|
+
19. [License](#license)
|
|
57
|
+
20. [Ruby and Rails compatibility](#ruby-and-rails-compatibility)
|
|
36
58
|
|
|
37
59
|
## How it works
|
|
38
60
|
|
|
39
|
-
Every
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
┌──────────────▼──────────────┐
|
|
52
|
-
│ 2. Encrypt file with DEK │ AES-256-GCM
|
|
53
|
-
│ unique IV per operation │ + auth tag
|
|
54
|
-
└──────────────┬──────────────┘
|
|
55
|
-
│
|
|
56
|
-
┌──────────────▼──────────────┐
|
|
57
|
-
│ 3. Wrap DEK with KMS │ ENV, AWS KMS,
|
|
58
|
-
│ master key │ or custom
|
|
59
|
-
└──────────────┬──────────────┘
|
|
60
|
-
│
|
|
61
|
-
┌──────────────▼──────────────┐
|
|
62
|
-
│ 4. Binary payload │ Header + IV +
|
|
63
|
-
│ (stored in S3) │ Ciphertext + Auth tag
|
|
64
|
-
└─────────────────────────────┘
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
Decryption reverses the flow: the KMS provider unwraps the DEK from the header, then AES-GCM verifies the auth tag and decrypts the ciphertext.
|
|
68
|
-
|
|
69
|
-
Every encrypted payload uses the same self-describing format, whether it came from Active Storage, the direct S3 adapter, or the backend chunk upload API.
|
|
61
|
+
Every file gets its own random data encryption key. The file is encrypted with AES-256-GCM, and that data key is wrapped by your configured key provider.
|
|
62
|
+
|
|
63
|
+
The encrypted file is self-contained. It stores:
|
|
64
|
+
|
|
65
|
+
- a small Active Cipher Storage header,
|
|
66
|
+
- the encrypted data key,
|
|
67
|
+
- the ciphertext,
|
|
68
|
+
- authentication tags used to detect tampering.
|
|
69
|
+
|
|
70
|
+
When the file is downloaded, the gem reads the header, asks the key provider to unwrap the data key, verifies the AES-GCM authentication tag, and returns plaintext to your app.
|
|
71
|
+
|
|
72
|
+
The same format is used for Rails Active Storage uploads, direct S3 uploads, streaming downloads, and multipart upload flows.
|
|
70
73
|
|
|
71
74
|
## Installation
|
|
72
75
|
|
|
76
|
+
Add the gem to your Gemfile:
|
|
77
|
+
|
|
73
78
|
```ruby
|
|
74
|
-
# Gemfile
|
|
75
79
|
gem "active_cipher_storage"
|
|
80
|
+
```
|
|
76
81
|
|
|
77
|
-
|
|
78
|
-
gem "aws-sdk-kms"
|
|
82
|
+
If you use AWS KMS or the direct S3 adapter, add the AWS SDK gems you need:
|
|
79
83
|
|
|
80
|
-
|
|
84
|
+
```ruby
|
|
85
|
+
gem "aws-sdk-kms"
|
|
81
86
|
gem "aws-sdk-s3"
|
|
82
87
|
```
|
|
83
88
|
|
|
@@ -87,7 +92,11 @@ bundle install
|
|
|
87
92
|
|
|
88
93
|
## Rails / Active Storage setup
|
|
89
94
|
|
|
90
|
-
|
|
95
|
+
Use this path when you want Rails Active Storage to encrypt attachments automatically.
|
|
96
|
+
|
|
97
|
+
Your model, controller, and view code can keep using normal Active Storage APIs. The only change is the storage service configuration.
|
|
98
|
+
|
|
99
|
+
### 1. Configure a key provider
|
|
91
100
|
|
|
92
101
|
```ruby
|
|
93
102
|
# config/initializers/active_cipher_storage.rb
|
|
@@ -154,7 +163,9 @@ user.document.attach(io: file, filename: "report.pdf")
|
|
|
154
163
|
url = rails_blob_url(user.document)
|
|
155
164
|
```
|
|
156
165
|
|
|
157
|
-
Active Storage
|
|
166
|
+
Active Storage now encrypts on upload and decrypts on download.
|
|
167
|
+
|
|
168
|
+
Existing plaintext objects are still readable. If a blob does not start with the `ACS\x01` magic header, the service returns it unchanged.
|
|
158
169
|
|
|
159
170
|
`config.encrypt_uploads` controls new Active Storage writes only. When disabled, new uploads are stored as plaintext and marked with `"encrypted": false` metadata. Reads continue to auto-detect by payload header, so existing encrypted blobs still decrypt correctly and existing plaintext blobs still download unchanged.
|
|
160
171
|
|
|
@@ -162,7 +173,9 @@ Direct Active Storage browser uploads are intentionally disabled because they by
|
|
|
162
173
|
|
|
163
174
|
## Standalone S3 usage
|
|
164
175
|
|
|
165
|
-
|
|
176
|
+
You can also use Active Cipher Storage without Rails.
|
|
177
|
+
|
|
178
|
+
This is useful for background jobs, service objects, scripts, or non-Rails Ruby apps that upload encrypted files directly to S3.
|
|
166
179
|
|
|
167
180
|
```ruby
|
|
168
181
|
require "active_cipher_storage"
|
|
@@ -176,12 +189,12 @@ s3 = ActiveCipherStorage::Adapters::S3Adapter.new(
|
|
|
176
189
|
region: "us-east-1"
|
|
177
190
|
)
|
|
178
191
|
|
|
179
|
-
# Encrypt
|
|
192
|
+
# Encrypt before upload
|
|
180
193
|
File.open("contract.pdf", "rb") do |f|
|
|
181
194
|
s3.put_encrypted("legal/contract-2026.pdf", f)
|
|
182
195
|
end
|
|
183
196
|
|
|
184
|
-
# Download and decrypt
|
|
197
|
+
# Download and decrypt
|
|
185
198
|
io = s3.get_decrypted("legal/contract-2026.pdf")
|
|
186
199
|
File.binwrite("decrypted_contract.pdf", io.read)
|
|
187
200
|
```
|
|
@@ -197,9 +210,11 @@ s3 = ActiveCipherStorage::Adapters::S3Adapter.new(
|
|
|
197
210
|
|
|
198
211
|
## Chunked multipart upload
|
|
199
212
|
|
|
200
|
-
For large files
|
|
213
|
+
For large files, many apps upload from the browser in chunks.
|
|
214
|
+
|
|
215
|
+
Active Cipher Storage supports that flow, but the browser still does not get encryption keys. The frontend sends plaintext chunks to your Rails app, and your backend encrypts those chunks before uploading encrypted multipart parts to S3.
|
|
201
216
|
|
|
202
|
-
|
|
217
|
+
Use `EncryptedMultipartUpload` for this backend-managed upload flow.
|
|
203
218
|
|
|
204
219
|
```ruby
|
|
205
220
|
uploader = ActiveCipherStorage::EncryptedMultipartUpload.new(
|
|
@@ -207,14 +222,14 @@ uploader = ActiveCipherStorage::EncryptedMultipartUpload.new(
|
|
|
207
222
|
bucket: "my-bucket"
|
|
208
223
|
)
|
|
209
224
|
|
|
210
|
-
#
|
|
225
|
+
# Request 1: start the upload
|
|
211
226
|
session_id = uploader.initiate(key: "uploads/video.mp4")
|
|
212
227
|
# Keep session_id for this active upload lifecycle.
|
|
213
228
|
|
|
214
|
-
#
|
|
229
|
+
# Requests 2..N: send chunks
|
|
215
230
|
uploader.upload_part(session_id: session_id, chunk_io: request.body)
|
|
216
231
|
|
|
217
|
-
#
|
|
232
|
+
# Final request: seal and complete
|
|
218
233
|
result = uploader.complete(session_id: session_id)
|
|
219
234
|
# => { status: :completed, key: "uploads/video.mp4", parts_count: 12 }
|
|
220
235
|
```
|
|
@@ -255,13 +270,14 @@ class UploadsController < ApplicationController
|
|
|
255
270
|
end
|
|
256
271
|
```
|
|
257
272
|
|
|
258
|
-
**Session storage
|
|
273
|
+
**Session storage**
|
|
274
|
+
|
|
259
275
|
By default, session state is held in process memory (`MemorySessionStore`). This is intended for one active backend-managed upload lifecycle and is not durable across process restarts or deploys.
|
|
260
276
|
|
|
261
277
|
For multi-process deployments where chunks for the same active upload may land on different workers or hosts, pass a shared store:
|
|
262
278
|
|
|
263
279
|
```ruby
|
|
264
|
-
# Rails.cache backed by Redis
|
|
280
|
+
# Rails.cache backed by Redis allows cross-worker active upload sessions.
|
|
265
281
|
uploader = ActiveCipherStorage::EncryptedMultipartUpload.new(
|
|
266
282
|
s3_client: s3_client,
|
|
267
283
|
bucket: "my-bucket",
|
|
@@ -269,11 +285,13 @@ uploader = ActiveCipherStorage::EncryptedMultipartUpload.new(
|
|
|
269
285
|
)
|
|
270
286
|
```
|
|
271
287
|
|
|
272
|
-
**Security:** The plaintext
|
|
288
|
+
**Security:** The plaintext data key is never stored in the session. Only the KMS-wrapped encrypted data key is persisted; it is decrypted fresh for each chunk and zeroed immediately after use.
|
|
273
289
|
|
|
274
290
|
## Streaming download
|
|
275
291
|
|
|
276
|
-
`stream_decrypted`
|
|
292
|
+
Use `stream_decrypted` when you need to send a large encrypted file to a client without loading the whole file into memory.
|
|
293
|
+
|
|
294
|
+
The adapter reads encrypted bytes from S3, decrypts authenticated chunks as they arrive, and yields plaintext chunks to your block. Memory usage stays bounded by one Active Cipher Storage chunk, which is 5 MiB by default.
|
|
277
295
|
|
|
278
296
|
```ruby
|
|
279
297
|
s3 = ActiveCipherStorage::Adapters::S3Adapter.new(
|
|
@@ -302,13 +320,15 @@ File.open("output.bin", "wb") do |f|
|
|
|
302
320
|
end
|
|
303
321
|
```
|
|
304
322
|
|
|
305
|
-
`stream_decrypted` handles S3 delivering data in any chunk size
|
|
323
|
+
`stream_decrypted` handles S3 delivering data in any chunk size. The internal decryptor buffers incoming bytes and emits plaintext only when a complete, authenticated frame is available.
|
|
306
324
|
|
|
307
325
|
Use `stream_decrypted` for chunked ACS objects. If the object is non-chunked, call `get_decrypted`; streaming a non-chunked or non-ACS/plaintext object raises `InvalidFormat` with a clear error.
|
|
308
326
|
|
|
309
327
|
## Manual encrypt / decrypt
|
|
310
328
|
|
|
311
|
-
|
|
329
|
+
If you do not need Rails or S3 integration, you can use the lower-level cipher classes directly.
|
|
330
|
+
|
|
331
|
+
Use `Cipher` for small files and `StreamCipher` for large files:
|
|
312
332
|
|
|
313
333
|
```ruby
|
|
314
334
|
require "active_cipher_storage"
|
|
@@ -317,7 +337,7 @@ ActiveCipherStorage.configure do |c|
|
|
|
317
337
|
c.provider = ActiveCipherStorage::Providers::EnvProvider.new
|
|
318
338
|
end
|
|
319
339
|
|
|
320
|
-
#
|
|
340
|
+
# Small files
|
|
321
341
|
cipher = ActiveCipherStorage::Cipher.new
|
|
322
342
|
encrypted = cipher.encrypt(File.open("secret.txt", "rb"))
|
|
323
343
|
# => Binary String with embedded header, IV, ciphertext, auth tag
|
|
@@ -325,7 +345,7 @@ encrypted = cipher.encrypt(File.open("secret.txt", "rb"))
|
|
|
325
345
|
plaintext = cipher.decrypt(encrypted)
|
|
326
346
|
# => Original plaintext String
|
|
327
347
|
|
|
328
|
-
#
|
|
348
|
+
# Large files
|
|
329
349
|
stream = ActiveCipherStorage::StreamCipher.new
|
|
330
350
|
|
|
331
351
|
File.open("large.bin", "rb") do |input|
|