Data Encryption Policy
Data Encryption Policy
Project / Organization:
ALAI Holding ASBilko —DropBalkanPaymentAccountingAppSaaS Policy Number: POL-SEC-ENC-001 Version: 1.0 Date: 2026-02-23 Author:ALAIComplianceSecurity TeamArchitect Status: Draft Reviewers:CISO,CTO,DPODPO, Engineering Lead Next Review:2027-02-2026-08-23(annual) or after Phase 2 infrastructure migrationClassification: Confidential
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | 2026-02- |
Initial draft — Bilko encryption standards | |
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 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.Bilko.
Scope: This policy applies to:
- All
Dropsystemssystems,operatedAPIs,byand infrastructureBilko (production,api.bilko.io,staging,bilko.io,development)PostgreSQL, Cloudflare R2) - All employees, contractors, and third parties with access to
DropBilko systems - All data classified
asInternal, Confidential, or Restricted (see compliance-framework.md §6) AllRailwaycloud environmentsPostgreSQL (AWSEU—West),EEAVercelregions)(frontend),andCloudflaredeveloperR2workstations(file All third-party integrations: BankID, Sumsub, Neonomics, Swan, Sentrystorage)
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,32DORA— Appropriate technical measures including encryption - ZZPL Art.
9(4)50 (d),Serbia)IKT-forskriften—§Security5,ofPSD2personal data processing - ZZLP Art.
95.14 (BiH) — Technical data protection measures - Zakon 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 | |
Asymmetric Encryption
| Use Case | Algorithm | Key Size | Notes |
|---|---|---|---|
| ECDHE | P-256 / P-384 | ||
| JWT signing |
HMAC-SHA-256 (HS256) | 256-bit | JWT_SECRET |
| 256-bit | |||
| 256-bit |
Hashing & Password Storage
| Use Case | Algorithm | Parameters | Notes |
|---|---|---|---|
| Password hashing | bcrypt | cost factor = 12 | |
| |||
| 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 USEUSE)
| Algorithm | Reason | |
|---|---|---|
| MD5 | Collision attacks | — |
| SHA-1 | Collision attacks | |
| DES / 3DES | Key size insufficient | |
| RC4 | Statistical biases | |
| ECB mode (any cipher) | Leaks data patterns | |
| RSA < 2048-bit | Insufficient key strength | |
| AES-128 for Restricted |
Insufficient for |
|
| bcrypt < 12 rounds | ||
| MD5 for password hashing | NEVER — use bcrypt |
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 | |
|---|---|---|---|
|
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)
| PostgreSQL |
All data classifications | ||
| PostgreSQL — |
AES-256-GCM |
L4 Restricted |
|
| PostgreSQL — |
AES-256-GCM |
||
| AES-256 ( |
All |
Field-level encryption patternfor (plannedL4 PhaseRestricted 2):data:
// EnvelopeTax IDs and bank account numbers encrypted at application layer
// In addition to Railway disk encryption
forimport fødselsnummercrypto andfrom high-sensitivity PII'crypto';
async function encryptField(encryptRestrictedField(plaintext: string): Promise<string> {
const dekkey = crypto.randomBytes(32)Buffer.from(process.env.FIELD_ENCRYPTION_KEY!, 'hex'); const// encryptedDek32 = await kmsClient.encrypt({ KeyId: KEK_ARN, Plaintext: dek });bytes
const iv = crypto.randomBytes(12); // 96-bit IV for GCM
const cipher = crypto.createCipheriv('aes-256-gcm', dek,key, iv);
const ciphertext = Buffer.concat([cipher.update(plaintext)plaintext, 'utf8'), 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 CardFile DataStorage Encryption
Policy:
| Storage | Method | Key |
Notes |
|---|---|---|---|
Cloudflare R2 (PDFs) |
AES-256 server-side ( |
Cloudflare-managed | EU |
3.3 Backup Encryption
PhaseRailway 2provides backupautomatic strategy:daily PostgreSQL WAL → Continuous replication (EEA)
Daily full snapshot → AWS RDSbackups, encrypted snapshotwith (AES-256, KMS)256. Backup key → Separate AWS KMS key (different region from primary)
Backup key escrow → Offline copy in secure location (physical)
Retention:retention: 30 days automated(Railway +default). quarterly offlineCustom backup
strategy Source:to implemented in Phase 2 with separate backup encryption key stored in Railway environment secrets.legal/ikt-sikkerhetspolicy.mdbe §12
4. Encryption in Transit
4.1 TLS Configuration Standards
Minimum TLS version:
- External-facing
services:(api.bilko.io, bilko.io): TLS 1.3 (TLSenforced1.2viaonly for legacy integration with explicit exception)Cloudflare) InternalRailwayservice-to-service:internal: 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)minimum)
ProhibitedHSTS TLSconfiguration:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
HTTP redirect: All HTTP traffic redirected to HTTPS (301). No HTTP allowed in production.
Prohibited:
- 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 TransportCookie Security (HSTS)
Strict-Transport-Security:res.cookie('refreshToken', max-age=63072000;token, includeSubDomains;{
preloadhttpOnly: true, // Not accessible to JavaScript
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
});
Source: src/drop-app/next.config.ts
4.3 CertificateAPI ManagementSecurity Headers (Helmet.js)
Certificate monitoring: Alert 30 days before expiry. Channel: #security-alerts (Slack).
4.4 Third-Party API Communications
5. Application-Level Encryption
5.1 JWT Token Security
Current implementation (src/drop-app/src/lib/auth.ts):
Algorithm: HS256 (HMAC-SHA-256) — symmetricLibrary:jose ^6.1.3Secret:JWT_SECRETenv var (fatal error if missing in production)setIssuedAt(): Yes — prevents token reusesetProtectedHeader(): Yes — explicit algorithmExpiry: 24 hoursStorage: 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 FieldsRequirements
Current MVP:
Passwords: bcrypt(cost=12) — implementedSession tokens: SHA-256 hash stored — implementedCard data: Onlylast_four+token_ref— implemented (fix C1)Bank account numbers: Masked to last 4 digits in API responses — implemented
Phase 2 — Required for compliance:all L4 Restricted fields:
| Field | Table | |||
|---|---|---|---|---|
( |
organizations |
National business identifier | ||
taxId (contact tax ID: PIB/JMBG/OIB/JIB) |
contacts |
National person/business identifier | ||
iban |
organizations, contacts, bankAccounts |
Bank account number | ||
totpSecret |
users |
must | ||
| | |||
| readable |
5.32 APIJWT ResponseToken Data MaskingEncryption
CurrentlyJWTs implemented:signed with HMAC-SHA-256 (HS256) using JWT_SECRET:
CardJWT_SECRET:numbers:32+Maskedcharacter random string (CSPRNG), stored in Railway secretsJWT_REFRESH_SECRET: Separate 32+ character key — never same as JWT_SECRET- Access token payload:
****{****sub:****userId,XXXXorg: organizationId, role, iat, exp } - NO PII in
allJWTAPIpayloadresponses— CVV:noAlwaysemail,name,***intaxAPI responsesBank account numbers: Only last 4 digits visibleSession tokens: Never returned in API responses (httpOnly cookie only)ID
Source:Refresh tokens stored as HMAC-SHA-256 hash in database — raw token never stored.
5.3 Password Hashing
src/drop-app/src/app/api/cards/[id]import bcrypt from 'bcrypt'; /route.ts:25-35/ 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_SECRET |
Quarterly | Security |
|
JWT_REFRESH_SECRET |
Quarterly | Security | |
FIELD_ENCRYPTION_KEY ( |
Railway environment secret | Annual | Security |
| Cloudflare R2 encryption | Cloudflare-managed | Cloudflare-managed | Cloudflare |
| TLS certificates (Cloudflare) | 90 days (automatic) | ||
CurrentSecrets MVPnever keycommitted storage:to git. files in JWT_SECRET.env.gitignore. All secrets managed via Railway environment variable.variables Loaded(production) at startup, fatal error if missing in production. Dev fallback usesor process.cwd().env.localhash.(development, git-ignored).
7. Cryptographic Inventory
| System | Algorithm | Key Size | Mode | Key Location | |
|---|---|---|---|---|---|
| PostgreSQL disk (Railway) | AES-256 | 256-bit | XTS | Railway-managed | Railway-managed |
| Tax ID field encryption | AES-256-GCM | 256-bit | GCM | Railway env secret | Annual |
| IBAN field encryption | AES-256-GCM | 256-bit | GCM | Railway env secret | Annual |
| Cloudflare R2 | AES-256 | 256-bit | — | Cloudflare-managed | Cloudflare-managed |
| External TLS (Cloudflare) | ECDSA P-256 | 256-bit | — | Cloudflare | 90 days (auto) |
| JWT signing | HMAC-SHA-256 | 256-bit | — | Railway env secret | Quarterly |
| Refresh token signing | HMAC-SHA-256 | 256-bit | — | Railway env secret | Quarterly |
| Password hashing | bcrypt | cost=12 | — | N/A | |
| |||||
| N/A | |||||
8. AlgorithmFinancial DeprecationData ScheduleIntegrity
9. Exception Process
Exceptions are not permitted for: L4 Restricted (L4) data (fødselsnummer,tax AMLIDs, records)IBAN, TOTP secrets, password hashes) — no exceptions.
Exception request process:
- Submit
exception requestto:CISO ([email protected])[email protected] RequiredRequired:information:systemSystem, data classification,affected, algorithmexcepted,excepted from, business justification, risk assessment, compensating controls, proposed duration (max 12 months)ApprovalApproval:required from: CISOCTOExceptions logged in:Log:legal/internkontroll./docs/security/exceptions.md- Review: Quarterly
Active exceptions: None at this time.
| |||
|
Approval
| Role | Name | Date | Signature |
|---|---|---|---|
| Author | 2026-02-23 | ||
| CTO | |||
| DPO |
|||