Skip to main content

Data Encryption Policy

Data Encryption Policy

Project / Organization: ALAI Holding ASBilkoDropBalkan PaymentAccounting AppSaaS Policy Number: POL-SEC-ENC-001 Version: 1.0 Date: 2026-02-23 Author: ALAICompliance Security TeamArchitect Status: Draft Reviewers: CISO, CTO, DPODPO, Engineering Lead Next Review: 2027-02-2026-08-23 (annual) or after Phase 2 infrastructure migration Classification: Confidential

Document History

Version Date Author Changes
0.1 2026-02-1223 SecurityCompliance Agent (ALAI)Architect Initial draft — Bilko encryption standards from security audit
1.02026-02-23Security 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 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 Dropsystems systems,operated APIs,by and 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 as Internal, Confidential, or Restricted (see compliance-framework.md §6)
  • AllRailway cloud environmentsPostgreSQL (AWSEU West), EEAVercel regions)(frontend), andCloudflare developerR2 workstations
  • (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,32 DORA— Appropriate technical measures including encryption
  • ZZPL Art. 9(4)50 (d),Serbia) IKT-forskriften §Security 5,of PSD2personal 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 — general(general) AES 256-bit GCM Authenticated encryption — required for PIIdefault
DataDatabase atdisk rest — legacy/interopencryption AES 256-bit CBCXTS OnlyRailway withPostgreSQL HMAC-SHA-256 for integritydefault
FødselsnummerFile (national ID)storage AES 256-bit GCM HSM-backedCloudflare keyR2 — separate key from general PIIserver-side
Backup encryption AES 256-bit GCM OfflineRailway keyautomatic copy in safe
Logbackup encryptionAES256-bitGCMKey rotation every 90 days

Asymmetric Encryption

Use Case Algorithm Key Size Notes
KeyTLS key exchange / TLS ECDHE P-256 / P-384 MinimumCloudflare for+ all externalRailway TLS
Digital signaturesEd25519256-bitPreferred for JWT signing (future)1.3
JWT signing (current) HMAC-SHA-256 (HS256) 256-bit joseJWT_SECRET library(32+ chars, symmetric, see auth.tsCSPRNG)
CodeJWT signingrefresh Ed25519HMAC-SHA-256 (HS256) 256-bit AllJWT_REFRESH_SECRET release(separate artifactskey)
TLSFuture: certificatesJWT asymmetric ECDSAEd25519 P-256(EdDSA) 256-bit ExternalPlanned Phase managed2 by cert-manager/Let's Encryptmigration

Hashing & Password Storage

refresh
Use Case Algorithm Parameters Notes
Password hashing bcrypt cost factor = 12 bcryptjsbcrypt.hash(password, ^3.0.312) — implemented
PasswordToken hashing (future) Argon2idm=65536, t=3, p=4Upgrade path for Phase 2
Session token storageSHA-256Token hash stored in sessions.token_hash, not plaintext
HMAC (webhook signatures)tokens) HMAC-SHA-256 For Sumsub webhooks, future APIRefresh tokens hashed before DB storage
FileData integrity (general) SHA-256 ForFile artifactchecksums, verificationnon-security hashing

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 Status
SHA-256 for passwordsNo salt, no key stretching — crackable in seconds with GPURemoved (fix C4, 2026-02-13)
MD5 Collision attacks since(2004+) 2004 Prohibitedcompletely broken
SHA-1 Collision attacks since 2017Prohibited(2017+)
DES / 3DES Key size insufficient Prohibited
RC4 Statistical biasesProhibited
ECB mode (any cipher) Leaks data patterns Prohibited— deterministic
RSA < 2048-bit Insufficient key strength Prohibited
AES-128 for Restricted (L4) data Insufficient for Restrictedfinancial/tax classificationID data
bcrypt < 12 rounds ProhibitedInsufficient work factor
MD5 for password hashingNEVER — 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 Classification
SQLite (drop.db)OS-level filesystem encryption (developer machine)Platform keyDevelopment 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)

DatabaseMethodKey ManagementClassificationCoverage
PostgreSQL primary(Railway EU West) TDEAES-256 viadisk AWS RDSencryption (AES-256)Railway TDE) AWS KMSRailway-managed All data classifications
PostgreSQL — fødselsnummertax fieldIDs (PIB/JMBG/OIB/JIB) AES-256-GCM (applicationapplication-layer layer)field encryption SeparateEnvironment AWSvariable KMS(Railway keysecrets) L4 Restricted (L4)
PostgreSQL — PII fieldsIBAN AES-256-GCM (applicationapplication-layer layer)field encryption AWSEnvironment KMSvariable (Railway secrets) ConfidentialL4 (L3)Restricted
BackupsPostgreSQL (RDS automated)backups AES-256 (AWSRailway RDS snapshot encryption)automatic) AWS KMSRailway-managed All classificationsdata

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:

DropNEVERstoresfullcardCVVs,orexpirydates.

  • Only
  • token)storedbehindfeatureflags (all default false in 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

    Storage Method Key PANs,Management Notes
    Cloudflare last_fourR2 (display)receipts, andinvoice token_refPDFs)AES-256 server-side (futureCloudflare partnerdefault) Cloudflare-managed EU
  • Cardregion featurebucket gatedrequired
  • 3.3 Backup Encryption

    Phase

    Railway 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 legal/ikt-sikkerhetspolicy.mdbe §12implemented in Phase 2 with separate backup encryption key stored in Railway environment secrets.


    4. Encryption in Transit

    4.1 TLS Configuration Standards

    Minimum TLS version:

    • External-facing services:(api.bilko.io, bilko.io): TLS 1.3 (TLSenforced 1.2via only for legacy integration with explicit exception)Cloudflare)
    • InternalRailway service-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:

    configurations:
    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

    ImplementedAll (fixsession M2,cookies 2026-02-13):set with:

    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)

    app.use(helmet({
      
    hsts: { 63072000, includeSubDomains: true, preload: true }, contentSecurityPolicy: { defaultSrc: scriptSrc: styleSrc: "'unsafe-inline'"], imgSrc: ["'self'", }));
    CertificatemaxAge: TypeValidityAuthorityRotation
    Externaldirectives: TLS{ (*.getdrop.no)90["'self'"], daysLet's["'self'", Encrypt"'unsafe-inline'"], (planned)Automated["'self'", (cert-manager)
    Internal"data:", service"https:"], TLS}, (Phase}, 3)30 daysInternal CAAutomated
    Code signing1 yearTBDManual

    Certificate monitoring: Alert 30 days before expiry. Channel: #security-alerts (Slack).

    4.4 Third-Party API Communications

    IntegrationProtocolAuthNotes
    BankID (OIDC)HTTPS / TLS 1.3OIDC + client credentialsPhase 2
    SumsubHTTPS / TLS 1.3API key + HMAC-SHA-256 webhook signaturePhase 2
    Neonomics (PSD2)HTTPS / TLS 1.3OAuth 2.0Phase 2
    SwanHTTPS / TLS 1.3OAuth 2.0Phase 2
    SentryHTTPS / TLS 1.3DSN tokenCurrent

    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_SECRET env var (fatal error if missing in production)
    • setIssuedAt(): Yes — prevents token reuse
    • setProtectedHeader(): 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 FieldsRequirements

    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:all L4 Restricted fields:

    seed
    Field Table ClassificationEncryptionKeyReason
    fodselsnummertaxId (nationalorganization ID)tax ID: PIB/JIB/OIB)organizationsNational business identifier
    taxId (contact tax ID: PIB/JMBG/OIB/JIB)contactsNational person/business identifier
    ibanorganizations, contacts, bankAccountsBank account number
    totpSecret users Restricted2FA (L4) AES-256-GCM FODSELSNUMMER_KEYmust (separatenot HSMbe key)
    KYC document hasheskyc_recordsRestricted (L4)AES-256-GCMKYC_KEY
    AML investigation notesaml_casesRestricted (L4)AES-256-GCMAML_KEYreadable

    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 secrets
    • JWT_REFRESH_SECRET: Separate 32+ character key — never same as JWT_SECRET
    • Access token payload: ****{ ****sub: ****userId, XXXXorg: organizationId, role, iat, exp }
    • NO PII in allJWT APIpayload responses
    • CVV:no Alwaysemail, ***name, intax API responses
    • Bank account numbers: Only last 4 digits visible
    • Session 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-26

    bcrypt parameters: cost factor 12. Never downgrade below 12.


    6. Key Management Summary

    Full policy: key-management-policy.md

    Summary

    HSM-backed
    Key Type KMSStorage Rotation Owner
    JWT signing key (JWT_SECRET) EnvironmentRailway variableenvironment (MVP) → AWS Secrets Manager (Phase 2)secret Quarterly Security team
    Database KEK (fødselsnummer)JWT_REFRESH_SECRET AWSRailway KMSenvironment secretQuarterlySecurity
    FIELD_ENCRYPTION_KEY (Phasetax 2)IDs, IBAN) Railway environment secret Annual Security team
    DatabasePostgreSQL DEKsdisk (PII fields)encryption AWS KMSRailway-managed (Phase 2)TDE) QuarterlyRailway-managed PlatformRailway
    Cloudflare R2 encryptionCloudflare-managedCloudflare-managedCloudflare
    TLS certificates (Cloudflare) cert-managerCloudflare +Certificate Let's Encrypt (Phase 2)Manager 90 days (automatic) Platform
    Sumsub webhook HMAC keyAWS Secrets Manager (Phase 2)On rotationEngineering
    Backup encryption keyAWS KMS (separate region)AnnualSecurity teamCloudflare

    CurrentSecrets MVPnever keycommitted storage:to git. JWT_SECRET.env files in .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.local hash.(development, git-ignored).


    7. Cryptographic Inventory

    System Algorithm Key Size Mode Key Location StatusNext Rotation
    PostgreSQL disk (Railway)AES-256256-bitXTSRailway-managedRailway-managed
    Tax ID field encryptionAES-256-GCM256-bitGCMRailway env secretAnnual
    IBAN field encryptionAES-256-GCM256-bitGCMRailway env secretAnnual
    Cloudflare R2AES-256256-bitCloudflare-managedCloudflare-managed
    External TLS (Cloudflare)ECDSA P-256256-bitCloudflare90 days (auto)
    JWT signingHMAC-SHA-256256-bitRailway env secretQuarterly
    Refresh token signingHMAC-SHA-256256-bitRailway env secretQuarterly
    Password hashing bcrypt cost=12 N/A — one-wayImplemented
    JWT signingHS256 (HMAC-SHA-256)256-bitJWT_SECRET env varImplemented
    Session token storageSHA-256256-bit N/A — hash onlyImplemented
    External TLSECDSA P-256 / TLS 1.3256-bitGCMHosting providerPlanned Phase 2
    PostgreSQL TDEAES-256256-bitXTSAWS KMSPlanned Phase 2
    Fødselsnummer field encryptionAES-256-GCM256-bitGCMAWS KMS (HSM-backed)Planned Phase 2
    Backup encryptionAES-256-GCM256-bitGCMAWS KMS (separate region)Planned Phase 2
    Sumsub webhookHMAC-SHA-256256-bitAWS Secrets ManagerPlanned Phase 2

    8. AlgorithmFinancial DeprecationData ScheduleIntegrity

    Financial

    datarequiresnotconfidentialityalsoBilkoenforces:

    allnever JavaScriptroundingerrorsin
  • Immutable
  • tableallmutationsappend-onlywithold/newfinancial
  • Double-entry
  • debit at backend. Prevents imbalanced
    Algorithm Currentjust Usage Deprecationbut Date Migrationintegrity. Target Status
    SHA-256
  • NUMERIC(19,4) for passwords
  • REMOVEDmonetary (fixamounts C4) 2026-02-13 bcrypt cost 12Completed
    HS256 JWT (symmetric)Current auth2026 H2RS256float or EdDSA Plannednumber. PhasePrevents 2
    bcryptVAT costcalculations. 12CurrentLoggedAction MVP 2027 (review) Argon2id Long-term
    SQLitevalues. (noEnables TDE) Currentaudit. MVPPhaseenforcement 2 migrationPostgreSQL= +credit AWSvalidated KMSPlanned
    entries.
  • Exchange rate locking — rates stored at transaction date. Historical accuracy preserved.

  • 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:

    1. Submit exception request to: CISO ([email protected])[email protected]
    2. RequiredRequired: information:system System, data classification,affected, algorithm excepted,excepted from, business justification, risk assessment, compensating controls, proposed duration (max 12 months)
    3. ApprovalApproval: required from: CISOCTO
    4. Exceptions logged in:Log: legal/internkontroll./docs/security/exceptions.md
    5. Review: Quarterly

    Active exceptions: None at this time.

    SystemExceptionExpiryCompensating Controls
    Drop MVP databaseSQLite without TDE (not PostgreSQL)Phase 2 migration (est. 2026 Q2)File permissions restricted, not committed to git, drop.db in .gitignore
    JWT signingHS256 (symmetric) instead of RS256Phase 2 migration (est. 2026 Q2)JWT_SECRET required env var, session revocation table active, 24h expiry

    Approval

    Role Name Date Signature
    Author ALAICompliance Security TeamArchitect 2026-02-23
    CISOAlem Bašić
    CTO Alem Bašić
    DPO (GDPR relevance) TBD — appointment required
    ManagementEngineering Lead Alem Bašić