Smart Vault is a Spring Boot file vault that combines envelope encryption, S3-compatible object storage, and optional AWS KMS-backed key wrapping. The system accepts arbitrary files, encrypts them before persistence, and reconstructs the plaintext only for an authorized download request.
The repository is organized around three concerns:
backend/contains the HTTP API, encryption pipeline, configuration, and static browser UI.infra/local/contains a Docker Compose environment for local S3-compatible storage through MinIO.infra/k8s/andinfra/terraform/aws/contain deployment scaffolding for Kubernetes and AWS infrastructure.
Smart Vault uses a two-layer crypto model:
- A random per-file data key encrypts the file contents with AES-256-GCM.
- That data key is wrapped by either AWS KMS or a locally configured AES-256 master key.
The file ciphertext is streamed directly into S3-compatible storage, while the encryption metadata is stored as S3 object metadata on the same object. That makes each object self-describing and keeps plaintext key material out of durable storage.
flowchart LR
U[Browser / API client] --> API[Spring Boot API]
API --> V[VaultService]
V --> KE[KeyEncryptionService]
KE -->|local| LK[LocalKeyEncryptionService]
KE -->|aws-kms| KMS[AWS KMS]
V --> C[AesGcmStreamingCipher]
C --> S3[S3 / MinIO]
V --> S3
API --> UI[Static UI under /]
sequenceDiagram
autonumber
participant Client as Client
participant API as FileController
participant Vault as VaultService
participant Key as KeyEncryptionService
participant Cipher as AesGcmStreamingCipher
participant Store as ObjectStorage / S3
Client->>API: POST /api/files (multipart file)
API->>Vault: upload(file)
Vault->>Key: generateDataKey()
Key-->>Vault: plaintext data key + wrapped key
Vault->>Cipher: encrypt(file stream, plaintext data key)
Vault->>Store: put(objectKey, ciphertext stream, metadata)
Store-->>Vault: object persisted
Vault-->>API: UploadResult{id, filename, size, keyProvider}
API-->>Client: JSON response
sequenceDiagram
autonumber
participant Client as Client
participant API as FileController
participant Vault as VaultService
participant Key as KeyEncryptionService
participant Cipher as AesGcmStreamingCipher
participant Store as ObjectStorage / S3
Client->>API: GET /api/files/{id}
API->>Vault: download(id)
Vault->>Store: get(objectKey)
Store-->>Vault: ciphertext stream + metadata
Vault->>Key: decryptDataKey(encryptedKey, iv?)
Key-->>Vault: plaintext data key
Vault->>Cipher: decrypt(ciphertext stream, data key, file IV)
Vault-->>API: DownloadResult{filename, contentType, size, plaintext stream}
API-->>Client: attachment response
The backend is a single Spring Boot application. The key classes are:
- SmartVaultApplication boots the application.
- FileController exposes the HTTP API.
- VaultService orchestrates key generation, encryption, metadata handling, and download reconstruction.
- AesGcmStreamingCipher performs streaming AES/GCM encryption and decryption.
- KeyEncryptionService abstracts key wrapping so the application can switch between local and AWS KMS modes.
- S3ObjectStorage is the storage adapter for S3-compatible backends.
- AppConfig wires AWS clients and credentials.
- ServiceWiringConfig selects the active key wrapping implementation from configuration.
classDiagram
class FileController {
+upload(file)
+download(id)
}
class VaultService {
+upload(file)
+download(objectKey)
}
class KeyEncryptionService {
<<interface>>
+generateDataKey()
+decryptDataKey(encryptedKey, encryptedKeyIv)
+providerName()
}
class LocalKeyEncryptionService
class AwsKmsKeyEncryptionService
class AesGcmStreamingCipher {
+encrypt(plaintext, dataKey)
+decrypt(ciphertext, dataKey, iv)
}
class ObjectStorage {
<<interface>>
+put(objectKey, content, contentLength, metadata)
+get(objectKey)
}
class S3ObjectStorage
FileController --> VaultService
VaultService --> KeyEncryptionService
VaultService --> AesGcmStreamingCipher
VaultService --> ObjectStorage
LocalKeyEncryptionService ..|> KeyEncryptionService
AwsKmsKeyEncryptionService ..|> KeyEncryptionService
S3ObjectStorage ..|> ObjectStorage
Smart Vault separates three different secrets:
- The file plaintext, which exists only while a file is being uploaded or downloaded.
- The per-file data key, which is generated for each upload and used only for one object.
- The master wrapping key, which protects the data key but never encrypts file contents directly.
The cryptographic sequence for a single file is:
- Generate a fresh 256-bit data key.
- Generate a 96-bit AES-GCM IV for the file content.
- Encrypt the file stream with AES/GCM/NoPadding.
- Wrap the data key using either KMS or the local master key.
- Persist the ciphertext stream to S3.
- Persist metadata needed for reconstruction on download.
The file cipher uses AES-GCM because it provides confidentiality and integrity in a single construction. The implementation uses:
- AES-256 keys.
- A 12-byte IV for GCM, which matches the standard nonce size.
- A 128-bit authentication tag.
The code streams data through CipherInputStream, so file contents do not need to be fully buffered in memory. The on-disk ciphertext is roughly the plaintext size plus the GCM authentication tag overhead.
The following metadata keys are stored with the S3 object:
- file-iv: Base64-encoded AES-GCM IV for the file ciphertext.
- encrypted-key: Base64-encoded wrapped data key.
- encrypted-key-iv: Base64-encoded IV used only by the local key provider.
- original-filename: original client filename, preserved for download responses.
- original-content-type: original MIME type, used to reconstruct the response content type.
- original-size: original plaintext size.
- key-provider: the active wrapping provider used at upload time.
The application never stores plaintext data keys durably.
Local mode is intended for development and offline testing. In this mode, the wrapped data key is produced by encrypting the per-file data key with a 32-byte master key using AES-GCM. The encrypted key IV is stored alongside the ciphertext metadata so the data key can be unwrapped later.
Use local mode when you want to run the system without AWS KMS access.
AWS KMS mode is the production-oriented option. The application calls GenerateDataKey with AES_256 so KMS returns both the plaintext data key and the encrypted data key blob. On download, the encrypted blob is passed back to Decrypt to recover the same plaintext key.
This means the application never needs to persist the plaintext data key itself. KMS becomes the authority for wrapping and unwrapping file keys.
flowchart TB
P[KeyEncryptionService] --> L[LocalKeyEncryptionService\nAES-GCM master key]
P --> K[AwsKmsKeyEncryptionService\nGenerateDataKey / Decrypt]
In practice, provider selection is controlled by ServiceWiringConfig:
- local or blank -> LocalKeyEncryptionService
- aws-kms or kms -> AwsKmsKeyEncryptionService
- any other value -> startup failure
Uploads a multipart file, encrypts it, and stores the encrypted object in the configured bucket.
Request:
POST /api/files
Content-Type: multipart/form-data
file=<binary file>Successful response:
{
"id": "8df0f8b8-5e60-4a1f-8b7a-1aa3b8c9d7b8",
"filename": "report.pdf",
"size": 1048576,
"keyProvider": "aws-kms"
}Downloads the encrypted object, decrypts it on the fly, and returns the plaintext file as an attachment.
Response behavior:
- Content-Disposition is set to attachment; filename="...".
- Content-Type is restored from stored metadata when available.
- Content-Length is emitted when the original size is known.
All runtime settings are bound from the smartvault.* namespace in backend/src/main/resources/application.yml.
| Property | Env var | Default | Purpose |
|---|---|---|---|
| smartvault.key-provider | SMARTVAULT_KEY_PROVIDER | local | Selects local or aws-kms key wrapping |
| smartvault.s3.bucket | SMARTVAULT_S3_BUCKET | smartvault | Target object bucket |
| smartvault.s3.region | SMARTVAULT_S3_REGION | us-east-1 | S3 region |
| smartvault.s3.endpoint | SMARTVAULT_S3_ENDPOINT | empty | Optional custom S3 endpoint for MinIO or other S3-compatible stores |
| smartvault.s3.access-key | SMARTVAULT_S3_ACCESS_KEY | empty | Static S3 credentials for local use |
| smartvault.s3.secret-key | SMARTVAULT_S3_SECRET_KEY | empty | Static S3 credentials for local use |
| smartvault.s3.force-path-style | SMARTVAULT_S3_FORCE_PATH_STYLE | true | Enables path-style addressing for local S3-compatible targets |
| smartvault.kms.region | SMARTVAULT_KMS_REGION | mirrors S3 region | KMS region, if different from S3 |
| smartvault.kms.key-id | SMARTVAULT_KMS_KEY_ID | empty | KMS key ARN or alias |
| smartvault.kms.endpoint | SMARTVAULT_KMS_ENDPOINT | empty | Optional custom KMS endpoint |
| smartvault.local.master-key-base64 | SMARTVAULT_LOCAL_MASTER_KEY_BASE64 | empty | 32-byte Base64 master key for local wrapping |
- HTTP port: 8080
- Multipart max file size: 200MB
- Multipart max request size: 200MB
- Actuator exposure: health, info, prometheus
- Java 17
- Maven 3.9+ or the Maven wrapper if you add one later
- Docker Desktop
The local stack is defined in infra/local/docker-compose.yml.
cd infra/local
docker compose up -dThis starts:
- MinIO on http://localhost:9000
- MinIO console on http://localhost:9001
- a bootstrap container that creates the smartvault bucket
For local development, configure the application to use the local key provider and point S3 at MinIO:
$env:SMARTVAULT_S3_ENDPOINT="http://localhost:9000"
$env:SMARTVAULT_S3_REGION="us-east-1"
$env:SMARTVAULT_S3_BUCKET="smartvault"
$env:SMARTVAULT_S3_ACCESS_KEY="minioadmin"
$env:SMARTVAULT_S3_SECRET_KEY="minioadmin"
$env:SMARTVAULT_S3_FORCE_PATH_STYLE="true"
$env:SMARTVAULT_KEY_PROVIDER="local"
# 32 random bytes, Base64-encoded. Example only.
$env:SMARTVAULT_LOCAL_MASTER_KEY_BASE64="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="Then start the backend:
cd backend
mvn spring-boot:runOpen the built-in static UI at http://localhost:8080/.
curl -F "[email protected]" http://localhost:8080/api/files
curl -L -o out.bin http://localhost:8080/api/files/<id>The JSON response contains the object identifier you use for download.
AWS KMS mode is the intended production configuration. The application calls AWS APIs through the AWS SDK v2 BOM declared in backend/pom.xml.
- An S3 bucket for encrypted object storage.
- A symmetric KMS key for envelope encryption.
- An IAM principal with permissions to write the bucket and use the KMS key.
flowchart LR
User[End user] --> LB[Ingress / Load Balancer]
LB --> App[Smart Vault backend]
App --> S3[(S3 bucket)]
App --> KMS[(AWS KMS key)]
App --> CW[(Prometheus / metrics scrape)]
The examples below assume the Mumbai region ap-south-1, but the architecture works in any AWS region as long as S3 and KMS are configured consistently.
Create a symmetric customer-managed key with encrypt/decrypt usage.
Key policy and IAM policy both matter. If decrypt or data-key generation fails, check both layers:
- IAM identity policy on the user or role.
- KMS key policy on the CMK itself.
Recommended bucket settings:
- Block all public access: enabled
- Versioning: enabled when you need recovery or auditability
- Default encryption: SSE-KMS
- KMS key: the same CMK used by the application
Use an IAM role for runtime workloads when possible:
- EC2 instance profile
- ECS task role
- EKS IRSA role
For local AWS development, an IAM user with an access key is the simplest option.
Minimum IAM policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3ListBucket",
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::YOUR_BUCKET_NAME"
},
{
"Sid": "S3ReadWriteObjects",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:HeadObject"],
"Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
},
{
"Sid": "KmsEnvelope",
"Effect": "Allow",
"Action": ["kms:GenerateDataKey", "kms:Decrypt"],
"Resource": "YOUR_KMS_KEY_ARN"
}
]
}$env:SMARTVAULT_KEY_PROVIDER="aws-kms"
$env:SMARTVAULT_S3_REGION="ap-south-1"
$env:SMARTVAULT_S3_BUCKET="YOUR_BUCKET_NAME"
$env:SMARTVAULT_KMS_KEY_ID="YOUR_KMS_KEY_ARN"
Remove-Item Env:SMARTVAULT_S3_ENDPOINT -ErrorAction SilentlyContinueCredentials can be supplied either through environment variables or via an AWS profile:
$env:AWS_ACCESS_KEY_ID="..."
$env:AWS_SECRET_ACCESS_KEY="..."
# $env:AWS_SESSION_TOKEN="..."or
$env:AWS_PROFILE="smartvault"cd backend
mvn spring-boot:runThen open http://localhost:8080/.
- HTTP 400 ... smartvault.kms.key-id is required: SMARTVAULT_KMS_KEY_ID is missing while key-provider=aws-kms.
- AccessDeniedException from KMS: the IAM policy or key policy does not allow kms:GenerateDataKey or kms:Decrypt.
- PermanentRedirect or region mismatch: the bucket region and SMARTVAULT_S3_REGION do not match.
- AccessDenied from S3: the bucket policy or identity policy does not allow the required S3 actions.
The Kubernetes implementation lives in infra/k8s/, and it has been wired as a small but complete runtime layer for Smart Vault. The manifests package the backend as a two-replica Deployment, expose it internally through a ClusterIP Service, and inject configuration through a ConfigMap plus a Secret. The result is a standard Kubernetes pattern that keeps non-sensitive defaults in Git while leaving credentials and master-key material out of the repository.
Apply the stack with:
kubectl apply -k infra/k8sThe kustomization currently includes:
- configmap.yaml for the baseline runtime settings.
- deployment.yaml for the backend pods.
- service.yaml for in-cluster traffic routing.
- secret.example.yaml as the template for the required secret.
flowchart TB
User[Client / Ingress] --> SVC[ClusterIP Service\nsmartvault-backend]
SVC --> POD1[Pod 1\nSmart Vault backend]
SVC --> POD2[Pod 2\nSmart Vault backend]
POD1 --> CM[ConfigMap\nsmartvault-config]
POD2 --> CM
POD1 --> SEC[Secret\nsmartvault-secrets]
POD2 --> SEC
POD1 --> AWS[(AWS S3 / AWS KMS)]
POD2 --> AWS
The implementation is intentionally conventional so it behaves predictably in cluster environments:
- The container image is built from backend/Dockerfile using a multi-stage build.
- The final runtime image exposes port
8080, which is the same port the Spring Boot application listens on inside the pod. - The Deployment runs two replicas by default, so the service stays available during restarts and supports basic horizontal availability.
- The Service maps cluster port
80to container port8080, which keeps in-cluster access simple while preserving the application port unchanged. - The ConfigMap provides non-secret runtime values such as the key provider, S3 bucket, region, endpoint, and path-style flag.
- The Secret provides S3 credentials, the local wrapping master key, and the KMS key identifier when AWS KMS mode is enabled.
- The readiness and liveness probes call
/actuator/health, which means Kubernetes only routes traffic to pods that have started successfully and can also restart pods that become unhealthy.
The pod environment is assembled from both config and secret sources:
SMARTVAULT_KEY_PROVIDERcomes from the ConfigMap.SMARTVAULT_S3_BUCKETcomes from the ConfigMap.SMARTVAULT_S3_REGIONcomes from the ConfigMap.SMARTVAULT_S3_ENDPOINTcomes from the ConfigMap.SMARTVAULT_S3_FORCE_PATH_STYLEcomes from the ConfigMap.SMARTVAULT_S3_ACCESS_KEYcomes from the Secret.SMARTVAULT_S3_SECRET_KEYcomes from the Secret.SMARTVAULT_LOCAL_MASTER_KEY_BASE64comes from the Secret.SMARTVAULT_KMS_KEY_IDcomes from the Secret.
That split is important because it lets the same manifest set support both local S3-compatible targets and AWS KMS-backed production deployments without changing the Deployment object itself.
When the pod starts, the container runs the Spring Boot jar built by the Dockerfile. Spring Boot then reads the smartvault.* values from environment variables and wires the storage and key-encryption services accordingly:
localmode uses the Base64-encoded master key from the Secret.aws-kmsmode uses the KMS key ARN or alias from the Secret.- the S3 client reads the endpoint and path-style settings from the ConfigMap.
That means Kubernetes is not acting as a custom controller or operator here. It is simply the durable runtime envelope for the existing application, with standard primitives for rollout, health checking, configuration injection, and internal service discovery.
The repository keeps infra/k8s/secret.example.yaml as a placeholder so a real Secret can be created per environment. In a production cluster, that Secret should be supplied by your secret delivery mechanism, for example:
- manual
kubectl applyfrom a secured workstation, - External Secrets Operator,
- CSI Secret Store,
- or a GitOps workflow that materializes secrets from a vault backend.
The ConfigMap is safe to version because it contains only non-sensitive defaults. The Secret is not safe to populate with real values in Git, which is why the repository ships only the example shape.
The Kubernetes implementation assumes the application is stateless:
- file bytes are persisted to S3 or S3-compatible storage,
- encryption metadata is stored on the object itself,
- and no pod-local disk state is required for recovery.
This makes the Deployment horizontally scalable and restart-friendly. If a pod is rescheduled, the next pod can continue serving reads and writes as long as the backing bucket and KMS configuration remain available.
This implementation matches the application architecture instead of forcing the application to adapt to Kubernetes-specific abstractions:
- the backend already exposes a health endpoint, so probes are easy and meaningful,
- the application already reads configuration from environment variables, so ConfigMap and Secret injection is natural,
- the file state already lives in object storage, so there is no database migration or attached volume requirement,
- and the encryption model already separates metadata from ciphertext, which keeps pod restarts simple.
In practice, the Kubernetes layer is the deployment envelope, not the source of business logic. The actual file-vault behavior remains in the Spring Boot application.
Terraform scaffolding is available under infra/terraform/aws/.
The current layout suggests an AWS EKS-oriented deployment path. The Terraform layer is useful when you want to provision:
- the container orchestration platform,
- networking primitives,
- IAM roles and policies,
- and supporting AWS resources such as S3 and KMS.
Treat the Terraform directory as infrastructure scaffolding rather than a fully opinionated production module set.
The root Jenkinsfile provides CI/CD integration for automated build and deployment workflows. At a minimum, a mature pipeline for this project should validate:
- Maven build and tests for the backend.
- Container image build, if you package the backend into a runtime image.
- Kubernetes or AWS deployment steps, depending on the target environment.
The application serves a small browser UI from the Spring Boot static resources in backend/src/main/resources/static/index.html and backend/src/main/resources/static/app.js.
That UI is intentionally simple:
- upload a file,
- capture the returned object ID,
- download the encrypted object through the backend,
- and verify the round trip without a separate frontend build.
- The upload path is streaming, so the application does not need to load the full file body into memory.
- The download path restores the original filename and content type from metadata.
- The stored object key is a random UUID, not the original filename.
- S3 object metadata acts as the reconstruction contract for decryption.
- The application exposes Prometheus metrics at /actuator/prometheus when the actuator endpoint is enabled.
What Smart Vault does well:
- Encrypts file contents before they reach object storage.
- Uses a fresh data key per object, limiting key reuse blast radius.
- Keeps plaintext data keys out of persistent storage.
- Supports AWS KMS for centralized key policy enforcement and auditability.
What it does not do by itself:
- It does not provide user authentication or authorization.
- It does not implement bucket-level access governance beyond the underlying AWS or MinIO permissions.
- It does not provide document-level search or indexing.
- It does not remove the need to protect the runtime environment, logs, and credentials.
Before exposing this service outside a trusted development environment:
- Put the API behind an authentication and authorization layer. Object IDs are opaque identifiers, not access controls.
- Use
aws-kmsin production and provide an IAM role with only the required S3 and KMS permissions. - Configure a real bucket, region, and KMS key ID; do not deploy the Kubernetes example values unchanged.
- Create
smartvault-secretswith the required values before applying the Kubernetes manifests. Local mode requires a Base64-encoded 32-byte master key. - Enable S3 public-access blocking, TLS-only bucket access, lifecycle retention rules, and audit logging.
- Set CPU and memory limits, run containers as non-root, and restrict network access with Kubernetes policies.
- Test upload/download and a failed download against the target storage provider before releasing.
- Missing objects return
404 Not Found. - Invalid client input, such as an empty upload, returns
400 Bad Request. - Processing failures detected before the download response starts return
500 Internal Server Errorwithout exposing implementation details. - Download filenames are emitted as UTF-8 attachment headers and control characters are removed before the response is built.
The service accepts files up to 200 MB by default. Change spring.servlet.multipart.max-file-size and spring.servlet.multipart.max-request-size together if a different limit is required.
Project/
├── backend/ Spring Boot application and static UI
│ ├── src/main/java API, crypto, storage, and configuration code
│ ├── src/main/resources application.yml plus static assets
│ └── src/test/java crypto-focused tests
├── infra/local MinIO development stack
├── infra/k8s Kubernetes manifests and examples
├── infra/terraform/aws AWS infrastructure scaffolding
└── Jenkinsfile CI/CD pipeline definition
If upload or download fails, check the following in order:
- The application is pointing at the correct S3 endpoint and region.
- The selected key provider matches the available configuration.
- The local master key is 32 bytes after Base64 decoding when using local mode.
- The KMS key ID is present when using AWS KMS mode.
- IAM and KMS key policies allow the principal to generate data keys and decrypt them.
- The target object exists in the bucket and the bucket name matches the configured value.
The repository already contains crypto-focused tests under backend/src/test/java/com/smartvault/crypto.
Recommended testing layers for the backend:
- unit tests for the cipher and key wrapping implementations,
- integration tests against MinIO,
- and end-to-end validation against real AWS S3 and KMS when production credentials are available.
This design keeps the system compact while preserving a strong security boundary:
- one service owns request handling, encryption, and metadata wiring,
- S3 stores only encrypted blobs,
- KMS or the local master key wraps only the per-file key,
- and the download path can be fully reconstructed from object metadata and the wrapping backend.
That combination makes the implementation easy to reason about, easy to run locally, and straightforward to move to AWS.