HIPAA-by-default for Spring Boot. Annotate a field with @Phi and get encryption at rest, an immutable, tamper-evident audit trail, and minimum-necessary access control — for free, by adding one dependency.
This starter turns three of the HIPAA §164.312 technical safeguards into inherited platform behavior, so your application code stays focused on the domain instead of re-implementing crypto, audit logging, and RBAC on every project.
Honest scope. This is a building block, not a HIPAA compliance program. It ships with illustrative key management (a key read from config) — in production you must supply a KMS/Vault-backed
KeyProviderand a durableAuditSink. Compliance also requires administrative and physical safeguards, BAAs, risk analysis, and more that no library can provide.
Companion write-up: HIPAA by Default: Field-Level Encryption, Immutable Audit, and Minimum-Necessary Access
| Feature | How | HIPAA §164.312 control |
|---|---|---|
| PHI field encryption at rest | @Phi field + PhiEncryptionListener (AES-256-GCM) |
(a)(2)(iv) Encryption and decryption; (a)(1) Access control |
| Tamper-evident audit trail | @AuditPhiAccess + hash-chained AuditEvents in an AuditSink |
(b) Audit controls; (c) Integrity |
| Minimum-necessary access control | @MinimumNecessary(roles...) + PhiAccessContext |
(a)(1) Access control |
// 1. Add the dependency (see below) and set the key:
// hipaa.phi.key=<Base64-encoded 32-byte AES key>
// 2. Annotate your entity — one listener, one @Phi per protected field:
@Entity
@EntityListeners(PhiEncryptionListener::class)
class Patient(
@field:Phi @Column(length = 512) var name: String = "",
@field:Phi @Column(length = 512) var ssn: String = "",
var mrn: String = "", // non-PHI, stored in clear text
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null,
)
// 3. Annotate the service method that reads PHI:
@Service
class PatientService(private val repo: PatientRepository) {
@AuditPhiAccess(action = "read", resourceType = "Patient") // → audit trail
@MinimumNecessary("clinician", "admin") // → access control
fun getById(id: Long): Patient = repo.findById(id).orElseThrow()
}
// Done. ssn/name are encrypted at rest, every read is audited, and callers
// without the clinician/admin role get PhiAccessDeniedException.This is a multi-module Gradle build; publish the starter module to your repository, then depend on it:
dependencies {
implementation("io.nirmitee.hipaa:starter:0.1.0")
}# Generate the Gradle wrapper (pinned to 8.10) and build + test both modules:
gradle wrapper --gradle-version 8.10 --distribution-type bin
JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home ./gradlew build
# Run the demo:
JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home ./gradlew :demo:bootRunThen, in another shell:
# Create a patient
curl -s -X POST localhost:8080/patients -H 'Content-Type: application/json' \
-d '{"name":"Jane Doe","ssn":"123-45-6789","dateOfBirth":"1990-02-15","phone":"555-0100","mrn":"MRN-1"}'
# → {"id":1,"mrn":"MRN-1"}
# Prove encryption at rest: raw JDBC read shows ciphertext for PHI, clear text for mrn
curl -s localhost:8080/patients/1/raw
# → {"ID":1,"MRN":"MRN-1","NAME":"<base64 ciphertext>","SSN":"<base64 ciphertext>",...}
# Read as a clinician (allowed + audited); PHI comes back decrypted
curl -s localhost:8080/patients/1 -H 'X-Actor: dr-house' -H 'X-Roles: clinician'
# → {"name":"Jane Doe","ssn":"123-45-6789",...}
# Read without the role → 403 access denied
curl -s -o /dev/null -w '%{http_code}\n' localhost:8080/patients/1 -H 'X-Actor: intruder' -H 'X-Roles: billing'
# → 403
# Inspect the tamper-evident audit chain
curl -s localhost:8080/audit
# → {"chainValid":true,"events":[{"seq":0,"actor":"dr-house","action":"read","resourceType":"Patient","resourceId":"1",...}]}hipaa-spring-boot-starter/
├── starter/ # io.nirmitee.hipaa — the auto-configuration library
│ └── src/main/kotlin/io/nirmitee/hipaa/
│ ├── annotations/ # @Phi, @PhiInside
│ ├── crypto/ # PhiCipher (AES-256-GCM), KeyProvider, PhiEncryptionListener
│ ├── audit/ # @AuditPhiAccess, AuditEvent (hash chain), AuditSink, PhiAuditAspect
│ ├── access/ # @MinimumNecessary, PhiAccessContext, MinimumNecessaryAspect
│ └── config/ # HipaaAutoConfiguration, HipaaProperties
└── demo/ # io.nirmitee.hipaa.demo — Spring Boot web app + H2 showing it end to end
- Key management: define your own
KeyProviderbean backed by AWS KMS, GCP KMS, HashiCorp Vault, or an HSM. The starter's bean is@ConditionalOnMissingBean, so yours wins. - Durable audit: define your own
AuditSinkbean that appends to an append-only table or WORM store while preserving the hash chain. - Identity: back
PhiAccessContextwith Spring Security'sSecurityContextinstead of the request-header filter used by the demo.
See docs/architecture.md for the mechanics.
Apache-2.0. See LICENSE.