Data Encryption Policy
Data Encryption Policy
Project / Organization: ALAI Holding AS — Drop Payment App Policy Number: POL-SEC-ENC-001 Version: 1.0 Date: 2026-02-23 Author: ALAI Security Team Status: Draft Reviewers: CISO, CTO, DPO Next Review: 2027-02-23 (annual) or after Phase 2 infrastructure migration Classification: Confidential
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | 2026-02-12 | Security Agent (ALAI) | Initial encryption standards from security audit |
| 1.0 | 2026-02-23 | Security Architect (ALAI) | Policy document |
Sources: security/drop-security-rapport.md, security/security-hardening-implementation.md, legal/ikt-sikkerhetspolicy.md §6, docs/security/SECURITY-ARCHITECTURE.md §4
1. Purpose & Scope
Purpose: This policy defines the minimum encryption standards for data at rest, in transit, and at the application level across all systems operated by ALAI Holding AS for the Drop payment application. Drop processes sensitive financial and personal data regulated by GDPR, PSD2, AML/hvitvaskingsloven, and DORA — all requiring strong encryption protections.
Scope: This policy applies to:
- All Drop systems, APIs, and infrastructure (production, staging, development)
- All employees, contractors, and third parties with access to Drop systems
- All data classified as Internal, Confidential, or Restricted (see compliance-framework.md §6)
- All cloud environments (AWS — EEA regions) and developer workstations
- All third-party integrations: BankID, Sumsub, Neonomics, Swan, Sentry
Exceptions: Public-facing static content (HTML, CSS, JS bundles, public images) does not require encryption at rest beyond TLS in transit.
Regulatory basis: GDPR Art. 32, DORA Art. 9(4)(d), IKT-forskriften § 5, PSD2 Art. 95.
2. Encryption Standards & Approved Algorithms
2.1 Approved Algorithms
Symmetric Encryption
| Use Case | Algorithm | Key Size | Mode | Notes |
|---|---|---|---|---|
| Data at rest — general | AES | 256-bit | GCM | Authenticated encryption — required for PII |
| Data at rest — legacy/interop | AES | 256-bit | CBC | Only with HMAC-SHA-256 for integrity |
| Fødselsnummer (national ID) | AES | 256-bit | GCM | HSM-backed key — separate key from general PII |
| Backup encryption | AES | 256-bit | GCM | Offline key copy in safe |
| Log encryption | AES | 256-bit | GCM | Key rotation every 90 days |
Asymmetric Encryption
| Use Case | Algorithm | Key Size | Notes |
|---|---|---|---|
| Key exchange / TLS | ECDHE | P-256 | Minimum for all external TLS |
| Digital signatures | Ed25519 | 256-bit | Preferred for JWT signing (future) |
| JWT signing (current) | HMAC-SHA-256 (HS256) | 256-bit | jose library — symmetric, see auth.ts |
| Code signing | Ed25519 | 256-bit | All release artifacts |
| TLS certificates | ECDSA P-256 | 256-bit | External — managed by cert-manager/Let's Encrypt |
Hashing & Password Storage
| Use Case | Algorithm | Parameters | Notes |
|---|---|---|---|
| Password hashing | bcrypt | cost factor 12 | bcryptjs ^3.0.3 — implemented |
| Password hashing (future) | Argon2id | m=65536, t=3, p=4 | Upgrade path for Phase 2 |
| Session token storage | SHA-256 | — | Token hash stored in sessions.token_hash, not plaintext |
| HMAC (webhook signatures) | HMAC-SHA-256 | — | For Sumsub webhooks, future API tokens |
| File integrity | SHA-256 | — | For artifact verification |
Current implementation: src/drop-app/src/lib/auth.ts (JWT/sessions), src/drop-app/src/lib/utils-server.ts (bcrypt)
2.2 Prohibited Algorithms — NEVER USE
| Algorithm | Reason | Status |
|---|---|---|
| SHA-256 for passwords | No salt, no key stretching — crackable in seconds with GPU | Removed (fix C4, 2026-02-13) |
| MD5 | Collision attacks since 2004 | Prohibited |
| SHA-1 | Collision attacks since 2017 | Prohibited |
| DES / 3DES | Key size insufficient | Prohibited |
| RC4 | Statistical biases | Prohibited |
| ECB mode (any cipher) | Leaks data patterns | Prohibited |
| RSA < 2048-bit | Insufficient key strength | Prohibited |
| AES-128 for Restricted (L4) data | Insufficient for Restricted classification | Prohibited |
Critical fix applied (2026-02-13): SHA-256 legacy password support completely removed from verifyPassword(). All accounts now require bcrypt.
3. Encryption at Rest
3.1 Database Encryption
MVP (Current — SQLite)
| Database | Method | Key Management | Classification |
|---|---|---|---|
SQLite (drop.db) |
OS-level filesystem encryption (developer machine) | Platform key | Development only |
Known limitation: SQLite stored in app working directory (process.cwd()/drop.db). Risk of accidental exposure. Mitigation: add to .gitignore, restrict permissions.
Phase 2 Target (PostgreSQL on AWS RDS)
| Database | Method | Key Management | Classification |
|---|---|---|---|
| PostgreSQL primary | TDE via AWS RDS (AES-256) | AWS KMS | All classifications |
| PostgreSQL — fødselsnummer field | AES-256-GCM (application layer) | Separate AWS KMS key | Restricted (L4) |
| PostgreSQL — PII fields | AES-256-GCM (application layer) | AWS KMS | Confidential (L3) |
| Backups (RDS automated) | AES-256 (AWS RDS snapshot encryption) | AWS KMS | All classifications |
Field-level encryption pattern (planned Phase 2):
// Envelope encryption for fødselsnummer and high-sensitivity PII
async function encryptField(plaintext: string): Promise<string> {
const dek = crypto.randomBytes(32);
const encryptedDek = await kmsClient.encrypt({ KeyId: KEK_ARN, Plaintext: dek });
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', dek, iv);
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
const tag = cipher.getAuthTag();
return Buffer.concat([encryptedDek, iv, tag, ciphertext]).toString('base64');
}
3.2 Card Data
Policy: Drop NEVER stores full card PANs, CVVs, or expiry dates.
- Only
last_four(display) andtoken_ref(future partner token) stored - Card feature gated behind feature flags (all default
falsein production) - Future card issuance will use PCI-compliant partner (Marqeta, Lithic, or similar)
Source: Fix C1 (2026-02-13) — src/drop-app/src/lib/db.ts cards schema
3.3 Backup Encryption
Phase 2 backup strategy:
PostgreSQL WAL → Continuous replication (EEA)
Daily full snapshot → AWS RDS encrypted snapshot (AES-256, KMS)
Backup key → Separate AWS KMS key (different region from primary)
Backup key escrow → Offline copy in secure location (physical)
Retention: 30 days automated + quarterly offline backup
Source: legal/ikt-sikkerhetspolicy.md §12
4. Encryption in Transit
4.1 TLS Configuration Standards
Minimum TLS version:
- External-facing services: TLS 1.3 (TLS 1.2 only for legacy integration with explicit exception)
- Internal service-to-service: TLS 1.3 (Phase 3 mTLS when microservices introduced)
- Third-party API calls: TLS 1.3 (BankID, Sumsub, Neonomics, Swan all require TLS 1.3)
Approved cipher suites (TLS 1.3):
TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256
TLS_AES_128_GCM_SHA256 (minimum — prefer 256)
Prohibited TLS configurations:
- TLS 1.0 and TLS 1.1 — prohibited
- SSL 2.0 / SSL 3.0 — prohibited
- RC4 cipher suites — prohibited
- NULL cipher suites — prohibited
Source: legal/ikt-sikkerhetspolicy.md §6.1
4.2 HTTP Strict Transport Security (HSTS)
Implemented (fix M2, 2026-02-13):
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Source: src/drop-app/next.config.ts
4.3 Certificate Management
| Certificate Type | Validity | Authority | Rotation |
|---|---|---|---|
| External TLS (*.getdrop.no) | 90 days | Let's Encrypt (planned) | Automated (cert-manager) |
| Internal service TLS (Phase 3) | 30 days | Internal CA | Automated |
| Code signing | 1 year | TBD | Manual |
Certificate monitoring: Alert 30 days before expiry. Channel: #security-alerts (Slack).
4.4 Third-Party API Communications
| Integration | Protocol | Auth | Notes |
|---|---|---|---|
| BankID (OIDC) | HTTPS / TLS 1.3 | OIDC + client credentials | Phase 2 |
| Sumsub | HTTPS / TLS 1.3 | API key + HMAC-SHA-256 webhook signature | Phase 2 |
| Neonomics (PSD2) | HTTPS / TLS 1.3 | OAuth 2.0 | Phase 2 |
| Swan | HTTPS / TLS 1.3 | OAuth 2.0 | Phase 2 |
| Sentry | HTTPS / TLS 1.3 | DSN token | Current |
5. Application-Level Encryption
5.1 JWT Token Security
Current implementation (src/drop-app/src/lib/auth.ts):
- Algorithm: HS256 (HMAC-SHA-256) — symmetric
- Library:
jose ^6.1.3 - Secret:
JWT_SECRETenv var (fatal error if missing in production) setIssuedAt(): Yes — prevents token reusesetProtectedHeader(): Yes — explicit algorithm- Expiry: 24 hours
- Storage: httpOnly cookie (never in localStorage)
Phase 2 upgrade path: RS256 (RSA-4096) or EdDSA (Ed25519) for asymmetric JWT signing, enabling token verification by multiple services without sharing the secret.
5.2 Field-Level Encryption — Sensitive Fields
Current MVP:
- Passwords: bcrypt(cost=12) — implemented
- Session tokens: SHA-256 hash stored — implemented
- Card data: Only
last_four+token_ref— implemented (fix C1) - Bank account numbers: Masked to last 4 digits in API responses — implemented
Phase 2 — Required for compliance:
| Field | Table | Classification | Encryption | Key |
|---|---|---|---|---|
fodselsnummer (national ID) |
users |
Restricted (L4) | AES-256-GCM | FODSELSNUMMER_KEY (separate HSM key) |
| KYC document hashes | kyc_records |
Restricted (L4) | AES-256-GCM | KYC_KEY |
| AML investigation notes | aml_cases |
Restricted (L4) | AES-256-GCM | AML_KEY |
5.3 API Response Data Masking
Currently implemented:
- Card numbers: Masked as
**** **** **** XXXXin all API responses - CVV: Always
***in API responses - Bank account numbers: Only last 4 digits visible
- Session tokens: Never returned in API responses (httpOnly cookie only)
Source: src/drop-app/src/app/api/cards/[id]/route.ts:25-35, src/drop-app/src/lib/utils-server.ts:23-26
6. Key Management
Full policy: key-management-policy.md
Summary
| Key Type | KMS | Rotation | Owner |
|---|---|---|---|
JWT signing key (JWT_SECRET) |
Environment variable (MVP) → AWS Secrets Manager (Phase 2) | Quarterly | Security team |
| Database KEK (fødselsnummer) | AWS KMS (Phase 2) — HSM-backed | Annual | Security team |
| Database DEKs (PII fields) | AWS KMS (Phase 2) | Quarterly | Platform |
| TLS certificates | cert-manager + Let's Encrypt (Phase 2) | 90 days | Platform |
| Sumsub webhook HMAC key | AWS Secrets Manager (Phase 2) | On rotation | Engineering |
| Backup encryption key | AWS KMS (separate region) | Annual | Security team |
Current MVP key storage: JWT_SECRET in environment variable. Loaded at startup, fatal error if missing in production. Dev fallback uses process.cwd() hash.
7. Cryptographic Inventory
| System | Algorithm | Key Size | Mode | Key Location | Status |
|---|---|---|---|---|---|
| Password hashing | bcrypt | cost=12 | — | N/A — one-way | Implemented |
| JWT signing | HS256 (HMAC-SHA-256) | 256-bit | — | JWT_SECRET env var |
Implemented |
| Session token storage | SHA-256 | 256-bit | — | N/A — hash only | Implemented |
| External TLS | ECDSA P-256 / TLS 1.3 | 256-bit | GCM | Hosting provider | Planned Phase 2 |
| PostgreSQL TDE | AES-256 | 256-bit | XTS | AWS KMS | Planned Phase 2 |
| Fødselsnummer field encryption | AES-256-GCM | 256-bit | GCM | AWS KMS (HSM-backed) | Planned Phase 2 |
| Backup encryption | AES-256-GCM | 256-bit | GCM | AWS KMS (separate region) | Planned Phase 2 |
| Sumsub webhook | HMAC-SHA-256 | 256-bit | — | AWS Secrets Manager | Planned Phase 2 |
8. Algorithm Deprecation Schedule
| Algorithm | Current Usage | Deprecation Date | Migration Target | Status |
|---|---|---|---|---|
| SHA-256 for passwords | REMOVED (fix C4) | 2026-02-13 | bcrypt cost 12 | Completed |
| HS256 JWT (symmetric) | Current auth | 2026 H2 | RS256 or EdDSA | Planned Phase 2 |
| bcrypt cost 12 | Current MVP | 2027 (review) | Argon2id | Long-term |
| SQLite (no TDE) | Current MVP | Phase 2 migration | PostgreSQL + AWS KMS | Planned |
9. Exception Process
Exceptions are not permitted for: Restricted (L4) data (fødselsnummer, AML records) — no exceptions.
Exception request process:
- Submit exception request to: CISO ([email protected])
- Required information: System, data classification, algorithm excepted, business justification, risk assessment, compensating controls, duration (max 12 months)
- Approval required from: CISO
- Exceptions logged in:
legal/internkontroll.md - Review: Quarterly
Active exceptions:
| System | Exception | Expiry | Compensating Controls |
|---|---|---|---|
| Drop MVP database | SQLite without TDE (not PostgreSQL) | Phase 2 migration (est. 2026 Q2) | File permissions restricted, not committed to git, drop.db in .gitignore |
| JWT signing | HS256 (symmetric) instead of RS256 | Phase 2 migration (est. 2026 Q2) | JWT_SECRET required env var, session revocation table active, 24h expiry |
Approval
| Role | Name | Date | Signature |
|---|---|---|---|
| Author | ALAI Security Team | 2026-02-23 | |
| CISO | Alem Bašić | ||
| CTO | Alem Bašić | ||
| DPO (GDPR relevance) | TBD — appointment required | ||
| Management | Alem Bašić |