Documentation Index
Fetch the complete documentation index at: https://docs.spitshake.io/llms.txt
Use this file to discover all available pages before exploring further.
Encryption
DocuTrust encrypts data at multiple layers: documents at the storage level, PII fields at the database level, and all communication via TLS. This page covers the technical details of each encryption layer and how keys are managed.
Document Encryption
All uploaded documents (PDFs, DOCX files, HTML templates) and generated output files (flattened signed PDFs) are encrypted at rest using AES-256-GCM.
How It Works
-
On upload: When a document is uploaded through any path (API, web UI, template creation, or signing submission), the
DocumentEncryptor service encrypts the file before writing it to storage.
-
On download: When an authorized user requests a document, the file is decrypted in memory and streamed to the client via the
send_decrypted_file method. The plaintext file never touches disk.
-
During processing: Operations that require access to document contents (PDF flattening, thumbnail generation) decrypt to a secure temporary file, perform the operation, encrypt the output, and securely delete the temporary file.
Encryption Details
| Property | Value |
|---|
| Algorithm | AES-256-GCM (Galois/Counter Mode) |
| Key length | 256 bits |
| IV/Nonce | 12 bytes, randomly generated per file |
| Authentication tag | 128 bits, appended to ciphertext |
| File format | [12-byte IV][ciphertext][16-byte auth tag] |
GCM mode provides both confidentiality and integrity. Any tampering with the ciphertext or IV is detected during decryption via the authentication tag.
PII Field Encryption
Submitter PII stored in the PostgreSQL database is encrypted using Active Record Encryption, a built-in Rails framework feature.
Encrypted Fields
| Model | Field | Encryption Mode | Why |
|---|
| Submitter | email | Deterministic | Enables find_by(email: ...) queries while keeping data encrypted at rest. |
| Submitter | name | Non-deterministic | Maximum privacy. No database-level queries needed on this field. |
| Submitter | phone | Non-deterministic | Maximum privacy. No database-level queries needed on this field. |
| Submitter | ip_address | Non-deterministic | Stored for audit purposes only. No query matching needed. |
| Submitter | user_agent | Non-deterministic | Stored for audit purposes only. No query matching needed. |
| Submitter | values | Non-deterministic | Contains all submitted field values (text entries, signature data, dates, checkboxes). |
| Submitter | metadata | Non-deterministic | Submitter preferences and processing metadata. |
Deterministic vs. Non-Deterministic
-
Deterministic encryption produces the same ciphertext for the same plaintext input. This allows the database to perform equality comparisons (e.g.,
WHERE email = ?), but it is slightly less secure because identical plaintext values produce identical ciphertext, making frequency analysis theoretically possible.
-
Non-deterministic encryption produces unique ciphertext every time, even for the same plaintext. This provides the strongest protection but means the field cannot be used in database queries.
Compatibility
Active Record Encryption is configured with support_unencrypted_data = true, which allows the application to read both encrypted and legacy unencrypted data. This is essential during the migration period when existing records are being encrypted in place.
Key Management
DocuTrust uses three separate encryption keys, each serving a different purpose:
| Key | Environment Variable | Purpose |
|---|
| Document encryption key | DOCUMENT_ENCRYPTION_KEY | Encrypts and decrypts document files in storage. |
| Active Record primary key | AR_ENCRYPTION_PRIMARY_KEY | Encrypts and decrypts PII fields in the database. |
| Config encryption key | CONFIG_ENCRYPTION_KEY | Encrypts sensitive configuration values (e.g., SMTP passwords, webhook secrets). |
Key Derivation Fallback
If the environment variables are not set, DocuTrust derives keys using HKDF (HMAC-based Key Derivation Function) from the application’s SECRET_KEY_BASE. Each key is derived with a unique context string to ensure key separation:
DOCUMENT_ENCRYPTION_KEY = HKDF(SECRET_KEY_BASE, info: "document-encryption")
AR_ENCRYPTION_PRIMARY_KEY = HKDF(SECRET_KEY_BASE, info: "active-record-encryption")
CONFIG_ENCRYPTION_KEY = HKDF(SECRET_KEY_BASE, info: "config-encryption")
The HKDF fallback is provided for convenience in development and initial deployment. In production, you should set explicit encryption keys via environment variables. This allows you to rotate keys independently without changing SECRET_KEY_BASE.
Key Loading Order
Encryption keys are loaded in the initializer config/initializers/00_encryption_keys.rb. The 00_ prefix ensures this file loads before active_record_encryption.rb (Rails initializers load alphabetically). This ordering is critical because Active Record Encryption must have access to the keys at initialization time.
Key Rotation
The KeyRotationService supports seamless key rotation with zero downtime:
How Rotation Works
-
Set the new key: Update the environment variable (e.g.,
DOCUMENT_ENCRYPTION_KEY) with the new key value.
-
Preserve the old key: Add the previous key to the corresponding
_PREVIOUS environment variable (e.g., DOCUMENT_ENCRYPTION_KEY_PREVIOUS).
-
Deploy: On the next deployment, DocuTrust will:
- Use the new key for all new encryption operations.
- Attempt decryption with the new key first, then fall back to the previous key if decryption fails.
-
Re-encrypt: Run the key rotation service to re-encrypt all existing data with the new key. Once complete, the previous key can be removed.
Environment Variables for Rotation
| Current Key | Previous Key |
|---|
DOCUMENT_ENCRYPTION_KEY | DOCUMENT_ENCRYPTION_KEY_PREVIOUS |
AR_ENCRYPTION_PRIMARY_KEY | AR_ENCRYPTION_PRIMARY_KEY_PREVIOUS |
CONFIG_ENCRYPTION_KEY | CONFIG_ENCRYPTION_KEY_PREVIOUS |
Re-encryption Process
After deploying with new keys, trigger re-encryption:
# Re-encrypt all documents
rails runner "KeyRotationService.rotate_documents!"
# Re-encrypt all PII fields
rails runner "KeyRotationService.rotate_pii_fields!"
# Re-encrypt configuration values
rails runner "KeyRotationService.rotate_config!"
Re-encryption is an idempotent operation. If it is interrupted, you can safely run it again. Records already encrypted with the new key will be skipped.
Security Considerations
Column Width
Encrypted values are significantly larger than their plaintext equivalents. All encrypted database columns use the text type (unlimited length) rather than fixed-width varchar columns to accommodate the ciphertext, IV, and authentication tag.
JSONB to Text Migration
PostgreSQL jsonb columns cannot store encrypted values (which are opaque strings). Fields like values and metadata that were originally jsonb have been migrated to text columns. The application layer handles JSON parsing and serialization in model accessors.
Object Storage (S3)
Signed documents support dual-write to S3 via ActiveStorage. When configured (AWS_BUCKET environment variable), documents are stored in both the database content blob and S3:
- S3 — Durable object storage with server-side encryption (SSE-S3 or SSE-KMS)
- Database blob — Fallback for Railway’s ephemeral filesystem
The system prefers S3 when available, falls back to the database blob, ensuring documents survive deploys regardless of storage backend. Contact support to initiate a migration of existing documents to S3.
Temporary Files
When documents must be decrypted for processing (e.g., PDF flattening, thumbnail generation), the plaintext is written to a temporary file with restricted permissions (0600). The temporary file is securely deleted immediately after the operation completes, regardless of whether the operation succeeded or failed.