Skip to main content

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

  1. 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.
  2. 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.
  3. 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

PropertyValue
AlgorithmAES-256-GCM (Galois/Counter Mode)
Key length256 bits
IV/Nonce12 bytes, randomly generated per file
Authentication tag128 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

ModelFieldEncryption ModeWhy
SubmitteremailDeterministicEnables find_by(email: ...) queries while keeping data encrypted at rest.
SubmitternameNon-deterministicMaximum privacy. No database-level queries needed on this field.
SubmitterphoneNon-deterministicMaximum privacy. No database-level queries needed on this field.
Submitterip_addressNon-deterministicStored for audit purposes only. No query matching needed.
Submitteruser_agentNon-deterministicStored for audit purposes only. No query matching needed.
SubmittervaluesNon-deterministicContains all submitted field values (text entries, signature data, dates, checkboxes).
SubmittermetadataNon-deterministicSubmitter 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:
KeyEnvironment VariablePurpose
Document encryption keyDOCUMENT_ENCRYPTION_KEYEncrypts and decrypts document files in storage.
Active Record primary keyAR_ENCRYPTION_PRIMARY_KEYEncrypts and decrypts PII fields in the database.
Config encryption keyCONFIG_ENCRYPTION_KEYEncrypts 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

  1. Set the new key: Update the environment variable (e.g., DOCUMENT_ENCRYPTION_KEY) with the new key value.
  2. Preserve the old key: Add the previous key to the corresponding _PREVIOUS environment variable (e.g., DOCUMENT_ENCRYPTION_KEY_PREVIOUS).
  3. 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.
  4. 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 KeyPrevious Key
DOCUMENT_ENCRYPTION_KEYDOCUMENT_ENCRYPTION_KEY_PREVIOUS
AR_ENCRYPTION_PRIMARY_KEYAR_ENCRYPTION_PRIMARY_KEY_PREVIOUS
CONFIG_ENCRYPTION_KEYCONFIG_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:
  1. S3 — Durable object storage with server-side encryption (SSE-S3 or SSE-KMS)
  2. 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.