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:SecurityCompliance Architect Status: Draft Reviewers:CISO,CTO,DPODPO, Engineering Lead Next Review:2027-02-2026-08-23 Classification: Confidential
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | 2026-02-23 | Initial draft — |
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 app.Bilko.
Regulatory basis:
GDPR Art. 32 — appropriate technical measuresPersonopplysningsloven (LOV-2018-06-15-38) § 28IKT-forskriften (FOR-2003-05-21-630) §§ 5-6DORA (EU) 2022/2554 Art. 9(4)(d)Hvitvaskingsloven (LOV-2018-06-01-23) § 30 — AES-256 for KYC data at rest
Scope: This policy applies to:
- All
systems, applications, and infrastructuresystems operated byALAIBilkoHolding(api.bilko.io,ASbilko.io,forPostgreSQL,DropCloudflare R2) - All employees, contractors, and third parties with access to
DropBilko systems - All data classified
asInternal, Confidential, or Restricted (see compliance-framework.md §7)6) AllRailwaycloud environmentsPostgreSQL (AWSEUAppWest),Runner,VercelS3)(frontend),andCloudflaredeveloperR2workstations(file storage)
Exceptions:Regulatory basis:
- GDPR
contentArt. 32 — Appropriate technical measures including encryption - ZZPL Art. 50 (
HTML,Serbia)CSS,—JS,Securitypublicofexchangepersonalratedatadata)processing - ZZLP
notArt.require14encryption(BiH)at—restTechnicalbeyonddatatransportprotectionlayermeasures - 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 (general) | AES | 256-bit | GCM | Authenticated encryption — |
| AES | 256-bit | XTS | Railway PostgreSQL default | |
| File storage | AES | 256-bit | GCM | Cloudflare R2 server-side |
| Backup encryption | AES | 256-bit | GCM | |
Asymmetric Encryption
| Use Case | Algorithm | Key Size | Notes |
|---|---|---|---|
| TLS key exchange | ECDHE | P-256 / P-384 | Cloudflare + |
| 256-bit | |||
| Future: JWT asymmetric | Ed25519 (EdDSA) | 256-bit | Planned Phase 2 migration |
Hashing & Password Storage
| Use Case | Algorithm | Parameters | Notes |
|---|---|---|---|
| Password hashing | bcrypt | cost factor = 12 | |
| HMAC-SHA-256 | — | ||
| Data integrity (general) | SHA-256 | — | File checksums, non-security hashing |
Source: src/drop-app/src/lib/utils-server.ts:8-16 (bcrypt); src/drop-app/src/lib/auth.ts (JWT)
2.2 Prohibited Algorithms (NEVER USE)
| Algorithm | Reason | |
|---|---|---|
| MD5 | Collision attacks (2004+) | |
| SHA-1 | Collision attacks (2017+) | |
| DES / 3DES | Key size insufficient | |
| RC4 | Statistical biases | |
| ECB mode (any cipher) | Leaks data patterns | |
| RSA < 2048-bit | Insufficient key strength | |
| Insufficient for financial/tax ID data | ||
| Insufficient work factor | ||
| MD5 for password hashing |
Critical fix (C4): SHA-256 legacy password support was completely removed in the 2026-02-13 hardening. verifyPassword() now only accepts bcrypt hashes. Source: src/drop-app/src/lib/utils-server.ts:14-19.
3. Encryption at Rest
3.1 Database Encryption
| Database | Method | Key Management | |
|---|---|---|---|
| All data classifications | |||
| PostgreSQL — |
AES- |
secrets) |
|
| PostgreSQL — |
AES-256-GCM |
variable ( |
L4 Restricted |
| PostgreSQL |
AES- |
Railway-managed |
ImplementationField-level patternencryption —for fødselsnummerL4 fieldRestricted encryption:data:
// EnvelopeTax encryptionIDs forand fødselsnummerbank account numbers encrypted at application layer
// Key:In drop-national-id-keyaddition (AWSto KMSRailway —disk separateencryption
import crypto from db master)
// Stored: Only AES-256-GCM ciphertext — never plaintext in DB
// Access: Compliance function only, via role-based KMS key policy'crypto';
async function encryptNationalId(fodselsnummer:encryptRestrictedField(plaintext: string): Promise<string> {
const key = Buffer.from(process.env.FIELD_ENCRYPTION_KEY!, 'hex'); // 32 bytes
const iv = crypto.randomBytes(12); // 96-bit random IV constfor dek = await kmsClient.generateDataKey({ KeyId: NATIONAL_ID_KEY_ARN, KeySpec: 'AES_256' });GCM
const cipher = crypto.createCipheriv('aes-256-gcm', dek.Plaintext,key, iv);
const ciphertext = Buffer.concat([cipher.update(fodselsnummer,plaintext, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
// Return:Store: base64(encryptedDEK || iv || tag || ciphertext)
return Buffer.concat([dek.CiphertextBlob, 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');
}
Card data note (critical fix C1): Cards feature is gated behind feature flags (all default false). No full PAN or CVV is ever stored. Only last_four and token_ref are stored. Card issuance will use a PCI-compliant partner (Marqeta/Lithic). Source: src/drop-app/src/lib/feature-flags.ts.
3.2 File / Object Storage Encryption
| Storage | Method | Key Management | Notes |
|---|---|---|---|
Cloudflare-managed |
|||
| |||
3.3 Backup Encryption
AllRailway databaseprovides backupsautomatic daily PostgreSQL backups, encrypted beforewith transferAES-256. offBackup primary system.
Key: drop-backup-key (AWS KMS — eu-west-1 region, separate from primary eu-north-1)
Rotation: Annual
Retention:retention: 30 days rolling(Railway Backupdefault). verification:Custom Monthlybackup restorestrategy testto —be seeimplemented beredskapsplan.mdin
Phase 2 with separate backup encryption key stored in Railway environment secrets.
4. Encryption in Transit
4.1 TLS Configuration Standards
External-facingMinimum servicesTLS (Cloudflare + AWS App Runner):version:
MinimumExternal-facingTLS(api.bilko.io,version:bilko.io): TLS 1.3 (enforcedatviaCloudflare edge)Cloudflare)- Railway internal: TLS 1.
2: Not supported (Cloudflare configuration) HTTP → HTTPS: Automatic redirect (HTTP 301)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)
HSTS configurationconfiguration: (source: next.config.ts):
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
ProhibitedHTTP TLSredirect: configurations:All HTTP traffic redirected to HTTPS (301). No HTTP allowed in production.
Prohibited:
- TLS 1.0 and TLS 1.1 — prohibited
(Cloudflare blocks) - SSL 2.0 / SSL 3.0 — prohibited
- RC4 cipher suites — prohibited
NULL cipher suites — prohibited
4.2 CertificateCookie ManagementSecurity
4.3 API CommunicationSecurity Headers (Helmet.js)
Allapp.use(helmet({
Drophsts: API{ traffic:maxAge: Client63072000, →includeSubDomains: Cloudflare:true, TLS 1.3 (edge termination)
Cloudflare → AWS App Runner: TLS 1.3 (re-encrypted)
Open Banking API calls (AISP/PISP — Phase 2):
Drop API → Bank API: TLS 1.3 + OAuth 2.0 / eIDAS certificate
httpOnly cookie security:
secure:preload: true (HTTPS-only},
transport)contentSecurityPolicy: sameSite:{
directives: {
defaultSrc: ["'strict'self'"],
(CSRFscriptSrc: protection)["'self'", Source:"'unsafe-inline'"],
src/drop-app/src/lib/auth.ts:48-54styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
}));
5. Application-Level Encryption
5.1 Field-Level Encryption Requirements
WhenRequired required:for Allall fields classified asL4 Restricted (L4) must be encrypted at the application layer, in addition to database-level encryption.
Fields requiring field-level encryption:fields:
| Field | Table | ||
|---|---|---|---|
(organization tax ID: PIB/JIB/OIB) |
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 |
| |
| | | |
| | ||
| |
5.2 JWT Token SecurityEncryption
Algorithm:JWTs HS256signed with HMAC-SHA-256 (HS256) using JWT_SECRET:
JWT_SECRET: 32+ character random string (CSPRNG), stored in Railway secretsJWT_REFRESH_SECRET: Separate 32+ character key — never same as JWT_SECRET- Access token payload:
{ sub: userId, org: organizationId, role, iat, exp } - NO PII in JWT payload — no email, name, tax ID
Refresh tokens stored as HMAC-SHA-256)256 Library:hash in jose^6.1.database — raw token never stored.
5.3 Configuration:Password Hashing
import bcrypt from 'bcrypt';
// Source: src/drop-app/src/lib/auth.tsRegistration
const tokenhash = await newbcrypt.hash(plainPassword, SignJWT({12);
userId,// roleLogin })verification
.setProtectedHeader({const alg:isValid 'HS256'= })await .setIssuedAt()bcrypt.compare(plainPassword, .setExpirationTime('24h')
.sign(new TextEncoder().encode(process.env.JWT_SECRET))storedHash);
Productionbcrypt requirement:parameters: cost JWT_SECRETenvfactor var12. isNever requireddowngrade —below throws fatal error if missing (dev fallback uses process.cwd() hash).
Phase 2 upgrade path: Migrate to RS256 (RSA) or EdDSA (Ed25519) for better key rotation support and multi-service verification.
5.3 Tokenization
Payment data: Drop uses a pass-through PSD2 model — no card numbers stored. Cards feature gated behind feature flags (all default false). When cards feature is activated in future: PCI-compliant tokenization via partner (Marqeta/Lithic) — Drop stores only last_four + token_ref.12.
6. Key Management Summary
Full policy: key-management-policy.md
Summary
| Key Type | Rotation | Owner | |
|---|---|---|---|
JWT_SECRET |
Quarterly | Security | |
JWT_REFRESH_SECRET |
Railway environment secret | Quarterly | Security |
FIELD_ENCRYPTION_KEY ( |
Railway environment secret | Annual | Security |
|
|||
|
|||
| |||
| TLS certificates ( |
Cloudflare |
90 days ( |
Secrets never committed to git. .env files in .gitignore. All secrets managed via Railway environment variables (production) or .env.local (development, git-ignored).
7. Cryptographic Inventory
| System | Algorithm | Key Size | Mode | Key Location | |
|---|---|---|---|---|---|
| AES-256 | 256-bit | XTS | Railway-managed |
||
| AES-256-GCM | 256-bit | GCM | secret |
||
| 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 |
|
| JWT signing | HMAC-SHA-256 | 256-bit | — | ||
| 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 KYCIDs, documents)IBAN, TOTP secrets, password hashes) — no exceptions.
Exception request process:
- Submit
exception requestto:[email protected][email protected] RequiredRequired:information:systemsystem, data classification,affected, algorithmbeingexceptedexcepted,from, business justification, risk assessment, compensating controls, proposedexceptionduration (max 12 months)ApprovalApproval:required from: CISOCTOExceptionsLog:logged in: compliance register (internal)/docs/security/exceptions.md- Review: Quarterly
— exceptions exceeding 12 months require board-level approval
Active exceptions: None at this time.
Approval
| Role | Name | Date | Signature |
|---|---|---|---|
| Author | 2026-02-23 | ||
| CTO | |||
| DPO |
|||