Security & Compliance
Disclaimer. This page is provided for informational purposes only and does not constitute legal, compliance, or security advice. Organizations must conduct their own assessments with qualified professionals to determine obligations under HIPAA, state privacy laws, and other applicable regulations. FHIRfly makes no representations or warranties regarding the suitability of any configuration for specific compliance requirements.
Zero-Knowledge Architecture
The SMART Health Links SDK uses a zero-knowledge encryption architecture: the FHIRfly server never possesses the decryption key. All PHI is encrypted client-side before upload, and the key exists only in the shlink:/ URL held by the patient or sharing party.
| Data | Server sees | Server does NOT see |
|---|---|---|
| Encrypted JWE blob | Yes (opaque ciphertext) | Plaintext health data |
| Manifest structure | File count, content types | File contents |
| Metadata | Expiration, access count, passcode hash | Passcode plaintext, decryption key |
| Decryption key | Never | Embedded in shlink:/ URL only |
| Patient identity | Never | Name, DOB, identifiers (all encrypted) |
How It Works
- Client generates a 256-bit AES key using
crypto.randomBytes(32)(Node.js CSPRNG) - Client encrypts the FHIR Bundle using AES-256-GCM with a fresh 12-byte IV per encryption
- Encrypted JWE is uploaded to storage (FhirflyStorage or your own S3/Azure/GCS bucket)
- Server stores only the opaque ciphertext — it cannot decrypt without the key
- The decryption key is encoded into the
shlink:/URL — it never touches the server - Recipient scans the QR code, which contains the full URL including the key
- Decryption happens in the recipient's browser or app — the key is never transmitted to the server
This means a breach of the storage layer (S3, database, or FHIRfly's infrastructure) would expose only encrypted blobs that are computationally infeasible to decrypt without the key.
Encryption Details
All encryption uses Node.js built-in crypto module only — no third-party cryptography dependencies.
| Property | Value |
|---|---|
| Algorithm | AES-256-GCM (authenticated encryption with associated data) |
| Key length | 256 bits (32 bytes) |
| Key generation | crypto.randomBytes(32) — OS-level CSPRNG |
| IV length | 96 bits (12 bytes), fresh per encryption |
| IV generation | crypto.randomBytes(12) — never reused |
| Authentication | GCM tag (128-bit), AAD set to JWE protected header |
| Compression | DEFLATE before encryption (zip: "DEF" per JWE spec) |
| Format | JWE Compact Serialization (RFC 7516) |
| JWE algorithm | alg: "dir" (direct key agreement — no key wrapping) |
| JWE encryption | enc: "A256GCM" |
| Key in URL | Base64url-encoded in the shlink:/ URL fragment |
| Storage encryption | JWE + S3 SSE-AES256 (double encryption for FhirflyStorage) |
Why AES-256-GCM?
- Authenticated encryption: GCM provides both confidentiality and integrity. Tampering with the ciphertext is detected via the authentication tag.
- No padding oracle: GCM is a stream cipher mode, eliminating padding oracle attacks that affect CBC.
- NIST approved: AES-256-GCM is approved for use with sensitive government data (FIPS 140-2/3 compliant when using a validated module).
- Performance: Hardware-accelerated AES-NI on modern processors.
Why Not Key Wrapping?
The alg: "dir" (direct key agreement) approach means the encryption key is used directly — no intermediate key encryption key (KEK). This is correct for this use case because:
- The key is generated per-SHL and never shared between SHLs
- There is no need for key rotation (each SHL has its own ephemeral key)
- Fewer cryptographic operations means fewer potential failure modes
Access Controls
Passcode Protection
SHLs can optionally require a passcode before revealing the manifest. Passcodes are:
- Hashed with SHA-256 before storage on the FHIRfly platform (plaintext is never persisted)
- Compared using
crypto.timingSafeEqual()to prevent timing side-channel attacks - Rate-limited: 5 attempts per 5 minutes per IP per SHL, plus 50 failed attempts per hour globally per SHL
Passcode strength is the implementer's responsibility. Recommendations:
| Risk Level | Minimum Passcode | Use Case |
|---|---|---|
| Low | 4-digit numeric PIN | ER triage, short-lived links (hours) |
| Medium | 6+ alphanumeric | Patient portal sharing (days/weeks) |
| High | 8+ mixed characters | Long-lived records, sensitive diagnoses |
For high-risk use cases (mental health, substance abuse, HIV status), consider short expiration windows and single-use access counts in addition to strong passcodes.
Expiration
SHLs support time-based expiration (expiresAt). After expiration:
- The manifest endpoint returns
410 Gone - Content download is blocked
- A nightly cleanup job deletes expired encrypted content from S3
Important: Expiration prevents new access to the encrypted content. Anyone who previously decrypted the content retains their local copy. Expiration does not "un-share" data that has already been decrypted.
Access Count Limits
SHLs support count-based limits (maxAccessCount). Each successful manifest retrieval increments an atomic counter. When the limit is reached, further access returns 410 Gone.
Access count increments use MongoDB's atomic $inc operator on the FHIRfly platform, preventing race conditions from concurrent requests.
Revocation
SHLs can be revoked immediately via SHL.revoke() or the API. Revocation:
- Sets a
revokedflag on the SHL record - Causes all subsequent requests to return
404 Not Found - Is immediate — no propagation delay
Rate Limiting
Public SHL endpoints enforce three tiers of rate limiting to prevent abuse:
| Tier | Scope | Limit | Purpose |
|---|---|---|---|
| 1. IP rate limit | All public SHL endpoints | 60 req/min/IP | Prevent scraping and DoS |
| 2. Passcode per-IP | Manifest endpoint (passcode-protected SHLs) | 5 attempts/5 min/IP/SHL | Slow brute-force from single source |
| 3. Passcode global | Manifest endpoint (passcode-protected SHLs) | 50 failed attempts/hour/SHL | Defend against distributed attacks |
Rate limit headers are included on all responses:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
Retry-After: 42 (only on 429 responses)
Brute-Force Analysis
With rate limiting in place, brute-force attacks on passcode-protected SHLs are computationally impractical:
| Passcode Type | Combinations | Single IP (5/5min) | Distributed (50/hr) |
|---|---|---|---|
| 4-digit numeric | 10,000 | ~7 days | ~8 days |
| 6-digit numeric | 1,000,000 | ~1.9 years | ~2.3 years |
| 4-char alphanumeric | 1,679,616 | ~3.2 years | ~3.8 years |
HIPAA Considerations
Is FHIRfly a Business Associate?
Under the zero-knowledge architecture, FHIRfly does not access, maintain, or transmit PHI when using FhirflyStorage. The encrypted JWE blobs stored in FHIRfly's infrastructure are opaque ciphertext that FHIRfly cannot decrypt.
Whether this constitutes "maintenance" of PHI under HIPAA is a legal determination that depends on your specific facts and circumstances. Organizations should consult qualified HIPAA counsel.
Key factors for your assessment:
- FHIRfly never possesses the decryption key
- Encrypted blobs cannot be decrypted without the key in the
shlink:/URL - FHIRfly cannot identify patients from the encrypted data
- S3 storage is additionally encrypted with SSE-AES256
BYOS (Bring Your Own Storage) Users
If you use S3Storage, AzureStorage, or GCSStorage with your own infrastructure, FHIRfly's SDK runs entirely in your environment. The SDK is a client-side library — no data flows to FHIRfly's servers. Your existing BAA with your cloud provider (AWS, Azure, GCP) covers the storage layer.
Business Associate Agreements
If your organization requires a BAA for HIPAA-covered use of FhirflyStorage,
contact us at support@fhirfly.io. Self-hosted
deployments using S3Storage, AzureStorage, or GCSStorage do not require
a BAA with FHIRfly since no PHI passes through our infrastructure.
De-Identification
SHLs containing de-identified data (per 45 CFR 164.514) are not PHI. If your IPS bundles contain only de-identified data, HIPAA's security requirements may not apply to the SHL content. Verify with your privacy officer.
Implementer Compliance Checklist
Use this checklist when preparing for a security review or compliance assessment.
Encryption & Key Management
- Encryption keys are generated using
crypto.randomBytes(32)(CSPRNG) - Each SHL has a unique encryption key (no key reuse)
- Decryption keys are distributed only via the
shlink:/URL - QR codes containing
shlink:/URLs are treated as sensitive (not stored in logs, databases, or analytics) - Debug mode (
debug: true) is disabled in production — it stores unencrypted bundles
Access Controls
- Passcode-protected SHLs use passcodes appropriate for the data sensitivity
- Expiration is set on all SHLs (no indefinite links for production use)
- Access count limits are set when appropriate
- Revocation capability is integrated into your application
Infrastructure (BYOS)
- S3 bucket has
ServerSideEncryptionenabled (SSE-S3 or SSE-KMS) - S3 bucket has Block Public Access enabled
- Azure Blob Storage uses service-level encryption
- GCS bucket uses Google-managed or customer-managed encryption keys
- Storage access credentials use least-privilege IAM roles
Application Security
- SHL URLs are transmitted over HTTPS only
- Server endpoints have TLS 1.2+ (handled by CloudFront/ALB for FhirflyStorage)
- Application logs do not contain
shlink:/URLs or decryption keys - Error responses do not leak internal state or stack traces
Organizational
- Data classification for SHL content is documented
- Incident response plan covers potential SHL key exposure
- Staff with access to infrastructure are trained on zero-knowledge architecture
- Retention and deletion policies are defined for expired SHLs
Shared Responsibility Model
| Responsibility | FhirflyStorage | BYOS (S3/Azure/GCS) |
|---|---|---|
| Encryption algorithm (AES-256-GCM) | SDK | SDK |
| Key generation (CSPRNG) | SDK | SDK |
| Client-side encryption | SDK | SDK |
| Server-side storage encryption | FHIRfly (SSE-AES256) | You (configure SSE) |
| Rate limiting | FHIRfly (3-tier) | You (implement) |
| TLS termination | FHIRfly (CloudFront) | You (your infra) |
| Access logging | FHIRfly (access counts) | You (S3/CloudTrail) |
| Passcode hashing | FHIRfly (SHA-256) | SDK (depends on handler) |
| Data residency | US (us-east-1) | Your choice |
| Backup/recovery | FHIRfly | You |
| Key management (shlink:/ URLs) | You (always) | You (always) |
| Passcode strength policy | You (always) | You (always) |
| Expiration/access count policy | You (always) | You (always) |
Specifications & Standards
| Standard | Reference |
|---|---|
| SMART Health Links | HL7 SMART Health Links IG |
| International Patient Summary | IPS Implementation Guide |
| JWE (JSON Web Encryption) | RFC 7516 |
| AES-GCM | NIST SP 800-38D |
| AES Key Sizes | NIST FIPS 197 |
| HIPAA Security Rule | 45 CFR Part 164, Subpart C |
| HIPAA De-Identification | 45 CFR 164.514 |