Data Encryption Policy
Data Encryption Policy
Project / Organization:
BilkoALAI Holding AS —BalkanDropAccountingPaymentSaaSApp Policy Number: POL-SEC-ENC-001 Version: 1.0 Date: 2026-02-23 Author:ComplianceALAIArchitectSecurity Team Status: Draft Reviewers: CISO, CTO,DPO, Engineering LeadDPO Next Review:2026-08-2027-02-23 (annual) or after Phase 2 infrastructure migration Classification: Confidential
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | 2026-02- |
Initial |
|
| 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 all data at rest, in transit, and at the application level across all systems operated by Bilko.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
systemsDropoperatedsystems,byAPIs,Bilkoand infrastructure (api.bilko.io,production,bilko.io,staging,PostgreSQL, Cloudflare R2)development) - All employees, contractors, and third parties with access to
BilkoDrop systems - All data classified as Internal, Confidential, or Restricted (see compliance-framework.md §6)
RailwayAllPostgreSQLcloud environments (EUAWSWest),—VercelEEA(frontend),regions)CloudflareandR2developer(fileworkstations- 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.
3232,— Appropriate technical measures including encryption ZZPLDORA Art.509(4)(d),(Serbia)IKT-forskriften—§Security5,of personal data processingZZLPPSD2 Art.14 (BiH) — Technical data protection measuresZakon o računovodstvu (RS/HR/BA) — Integrity of financial records
2. Encryption Standards & Approved Algorithms
2.1 Approved Algorithms
Symmetric Encryption
| Use Case | Algorithm | Key Size | Mode | Notes |
|---|---|---|---|---|
| Data at rest |
AES | 256-bit | GCM | Authenticated encryption — |
| AES | 256-bit | |||
| AES | 256-bit | GCM | ||
| Backup encryption | AES | 256-bit | GCM | |
| Log encryption | AES | 256-bit | GCM | Key rotation every 90 days |
Asymmetric Encryption
| Use Case | Algorithm | Key Size | Notes |
|---|---|---|---|
| ECDHE | P-256 |
||
| Digital signatures | Ed25519 | 256-bit | Preferred for JWT signing (future) |
| JWT signing (current) | HMAC-SHA-256 (HS256) | 256-bit | jose |
| 256-bit | |||
| 256-bit |
Hashing & Password Storage
| Use Case | Algorithm | Parameters | Notes |
|---|---|---|---|
| Password hashing | bcrypt | cost factor |
— implemented |
| 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 | — | |
| SHA-256 | — |
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)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 |
Prohibited |
| SHA-1 | Collision attacks |
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 | |
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 | |
|---|---|---|---|
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 |
All |
||
| PostgreSQL — |
AES-256-GCM |
||
| PostgreSQL — |
AES-256-GCM |
||
| AES-256 ( |
All |
Field-level encryption forpattern L4(planned RestrictedPhase data:2):
// TaxEnvelope IDsencryption for fødselsnummer and bankhigh-sensitivity account numbers encrypted at application layer
// In addition to Railway disk encryption
import crypto from 'crypto';PII
async function encryptRestrictedField(encryptField(plaintext: string): Promise<string> {
const keydek = Buffer.from(process.env.FIELD_ENCRYPTION_KEY!,crypto.randomBytes(32);
'hex'const encryptedDek = await kmsClient.encrypt({ KeyId: KEK_ARN, Plaintext: dek }); // 32 bytes
const iv = crypto.randomBytes(12); // 96-bit IV for GCM
const cipher = crypto.createCipheriv('aes-256-gcm', key,dek, iv);
const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8')plaintext), cipher.final()]);
const tag = cipher.getAuthTag();
// Store: base64(iv || tag || ciphertext)
return Buffer.concat([encryptedDek, iv, tag, ciphertext]).toString('base64');
}
async function decryptRestrictedField(stored: string): Promise<string> {
const key = Buffer.from(process.env.FIELD_ENCRYPTION_KEY!, 'hex');
const buf = Buffer.from(stored, 'base64');
const iv = buf.subarray(0, 12);
const tag = buf.subarray(12, 28);
const ciphertext = buf.subarray(28);
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(tag);
return decipher.update(ciphertext) + decipher.final('utf8');
}
3.2 FileCard Storage EncryptionData
last_four ( | token_ref ( | partner
false in production)
Source: Fix C1 (2026-02-13) — src/drop-app/src/lib/db.ts cards schema
3.3 Backup Encryption
Railway
Phase provides2 automaticbackup dailystrategy:
PostgreSQL backups,WAL → Continuous replication (EEA)
Daily full snapshot → AWS RDS encrypted withsnapshot (AES-256.256, KMS)
Backup retention:key → Separate AWS KMS key (different region from primary)
Backup key escrow → Offline copy in secure location (physical)
Retention: 30 days (Railwayautomated default).+ Customquarterly offline backup
strategy
Source: belegal/ikt-sikkerhetspolicy.md implemented in Phase 2 with separate backup encryption key stored in Railway environment secrets.§12
4. Encryption in Transit
4.1 TLS Configuration Standards
Minimum TLS version:
- External-facing
(api.bilko.io, bilko.io):services: TLS 1.3 (enforcedTLSvia1.2Cloudflare)only for legacy integration with explicit exception) RailwayInternalinternal: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)minimum — prefer 256)
HSTSProhibited configuration:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
HTTP redirect: All HTTP traffic redirected to HTTPS (301). No HTTP allowed in production.
Prohibited: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 CookieHTTP Strict Transport Security (HSTS)
AllImplemented session(fix cookiesM2, set with:2026-02-13):
res.cookie('refreshToken',Strict-Transport-Security: token,max-age=63072000; {includeSubDomains; httpOnly: true, // Not accessible to JavaScript
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
});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 SecurityCommunications
| Integration | Protocol | Auth | Notes |
|---|---|---|---|
| BankID ( |
HTTPS |
OIDC |
Phase |
| Sumsub | HTTPS |
API |
Phase |
| Neonomics |
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 Requirements— 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 all L4 Restricted fields:compliance:
| Field | Table | Encryption | Key | |
|---|---|---|---|---|
( | | |||
| | |||
| | |||
ID) |
users |
AES-256-GCM | FODSELSNUMMER_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.23 JWTAPI TokenResponse EncryptionData Masking
JWTsCurrently signed with HMAC-SHA-256 (HS256) using JWT_SECRET:implemented:
- Card numbers: Masked as
JWT_SECRET**** **** **** XXXX: 32+ character random string (CSPRNG), storedinRailwayallsecretsAPI responses - CVV: Always
JWT_REFRESH_SECRET***:Separatein32+APIcharacter key — never same as JWT_SECRETresponses AccessBanktokenaccountpayload:numbers:visible{Onlysub:lastuserId,4org:digitsorganizationId, role, iat, exp }NOSessionPIItokens: Never returned inJWTAPIpayloadresponses—(httpOnlynocookieemail, name, tax IDonly)
RefreshSource: tokenssrc/drop-app/src/app/api/cards/[id]/route.ts:25-35, stored as HMAC-SHA-256 hash in database — raw token never stored.
5.3 Password Hashing
import bcrypt from 'bcrypt';
// Registration
const hash = await bcrypt.hash(plainPassword, 12);
// Login verification
const isValid = await bcrypt.compare(plainPassword, storedHash);
src/drop-app/src/lib/utils-server.ts:23-26bcrypt parameters: cost factor 12. Never downgrade below 12.
6. Key Management Summary
Full policy: key-management-policy.md
Summary
| Key Type | Rotation | Owner | |
|---|---|---|---|
JWT signing key (JWT_SECRET) |
Quarterly | Security team | |
Database KEK (fødselsnummer) |
|||
KMS ( | — Annual | Security team | |
| TLS certificates |
90 days |
||
| Sumsub webhook HMAC key | AWS Secrets Manager (Phase 2) | On rotation | Engineering |
| Backup encryption key | AWS KMS (separate region) | Annual | Security team |
SecretsCurrent neverMVP committedkey to git.storage: .envJWT_SECRET files in .gitignore. All secrets managed via Railway environment variablesvariable. (production)Loaded orat startup, fatal error if missing in production. Dev fallback uses .env.localprocess.cwd()(development, git-ignored).hash.
7. Cryptographic Inventory
| System | Algorithm | Key Size | Mode | Key Location | |
|---|---|---|---|---|---|
| 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. FinancialAlgorithm DataDeprecation IntegritySchedule
Financial
| Algorithm | Current |
Deprecation |
Migration |
Status |
|---|---|---|---|---|
| SHA-256 for |
REMOVED |
2026-02-13 | bcrypt cost 12 | Completed |
| HS256 JWT (symmetric) | Current auth | 2026 H2 | RS256 or |
Planned |
| bcrypt |
Current |
2027 |
Argon2id | Long-term |
| SQLite |
Current |
Phase |
PostgreSQL |
Planned |
9. Exception Process
Exceptions are not permitted for: L4Restricted Restricted(L4) data (taxfødselsnummer, IDs,AML IBAN, TOTP secrets, password hashes)records) — no exceptions.
Exception request process:
- Submit exception request to:
[email protected]CISO ([email protected]) Required:Requiredsysteminformation:affected,System, data classification, algorithmexcepted from,excepted, business justification, risk assessment, compensating controls,proposedduration (max 12 months)Approval:ApprovalCTOrequired from: CISOLog:Exceptions logged in:/docs/security/exceptions.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 | 2026-02-23 | ||
| CISO | Alem Bašić | ||
| CTO | Alem Bašić | ||
| DPO (GDPR relevance) | TBD — appointment required | ||
| Alem Bašić |