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: 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 SecurityCompliance Architect Initial draft — DropBilko encryption standards

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 measures
  • Personopplysningsloven (LOV-2018-06-15-38) § 28
  • IKT-forskriften (FOR-2003-05-21-630) §§ 5-6
  • DORA (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 by ALAIBilko Holding(api.bilko.io, ASbilko.io, forPostgreSQL, DropCloudflare R2)
  • All employees, contractors, and third parties with access to DropBilko systems
  • All data classified as Internal, Confidential, or Restricted (see compliance-framework.md §7)6)
  • AllRailway cloud environmentsPostgreSQL (AWSEU AppWest), Runner,Vercel S3)(frontend), andCloudflare developerR2 workstations(file storage)

Exceptions:Regulatory basis:

Public-facing
    static
  • GDPR contentArt. 32 — Appropriate technical measures including encryption
  • ZZPL Art. 50 (HTML,Serbia) CSS, JS,Security publicof exchangepersonal ratedata data)processing
  • does
  • ZZLP notArt. require14 encryption(BiH) at restTechnical beyonddata transportprotection layermeasures
  • security.

  • Zakon o računovodstvu (RS/HR/BA) — Integrity of financial records

2. Encryption Standards & Approved Algorithms

2.1 Approved Algorithms

Symmetric Encryption

field
Use Case Algorithm Key Size Mode Notes
Data at rest (general) AES 256-bit GCM Authenticated encryption — preferred; provides confidentiality + integritydefault
FødselsnummerDatabase (nationaldisk ID)encryption AES256-bitXTSRailway PostgreSQL default
File storageAES256-bitGCMCloudflare R2 server-side
Backup encryption AES 256-bit GCM SeparateRailway key from database master; application layer
KYC document encryptionAES256-bitGCMStored in S3 with SSE-KMS
Database backupsAES256-bitGCMSeparateautomatic backup KMS key in separate AWS region
Log encryptionAES256-bitGCMRotation every 90 days

Asymmetric Encryption

Use Case Algorithm Key Size Notes
TLS key exchange ECDHE P-256 / P-384 Cloudflare + AWSRailway AppTLS Runner1.3
BankID JWT verificationRSA2048-bit (BankID certificate)BankID Norway certificate authority
Code signing Ed25519HMAC-SHA-256 (HS256) 256-bit CI/CDJWT_SECRET artifact(32+ signingchars, CSPRNG)
AWSJWT KMS key wrappingrefresh RSA-OAEPHMAC-SHA-256 (HS256) 4096-256-bit AWS-managedJWT_REFRESH_SECRET (separate key)
Future: JWT asymmetricEd25519 (EdDSA)256-bitPlanned Phase 2 migration

Hashing & Password Storage

Use Case Algorithm Parameters Notes
Password hashing bcrypt cost factor = 12 bcryptjsbcrypt.hash(password, 12) ^3.0.3 — pure JS; SHA-256 legacy support REMOVED (fix C4)
JWT tokenToken hashing (sessionrefresh lookup)SHA-256Session table stores SHA-256(JWT) for revocation check
HMAC (webhook signatures)tokens) HMAC-SHA-256 ForRefresh APItokens integrationshashed before DB storage
Data integrity (general)SHA-256File 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 Status
MD5 Collision attacks (2004+) Prohibited— completely broken
SHA-1 Collision attacks (2017+)Prohibited
DES / 3DES Key size insufficient Prohibited
RC4 Statistical biasesProhibited
ECB mode (any cipher) Leaks data patterns Prohibited— deterministic
RSA < 2048-bit Insufficient key strength
ProhibitedAES-128 for Restricted dataInsufficient for financial/tax ID data
SHA-256bcrypt < 12 roundsInsufficient work factor
MD5 for password hashing Not a password hashNEVERnouse salt/stretchingREMOVED (security fix C4, 2026-02-13)bcrypt

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 Classification Coverage
SQLitePostgreSQL (currentRailway MVP)EU West) OS-levelAES-256 full-disk encryption (Railway TDE) Platform (AWS)Railway-managed All data classifications
PostgreSQL — Phasetax 2IDs (primary)PIB/JMBG/OIB/JIB) AES-256256-GCM TDEapplication-layer viafield AWS RDSencryption AWSEnvironment KMSvariable (Railway drop-db-master-keysecrets) AllL4 classificationsRestricted
PostgreSQL — fødselsnummer fieldIBAN AES-256-GCM applicationapplication-layer layerfield encryption AWSEnvironment KMS — drop-national-id-keyvariable (SEPARATE)Railway secrets) L4 Restricted (L4)
PostgreSQL — KYC documentsbackups AES-256-GCM256 application(Railway layerautomatic) AWS KMS — drop-kyc-keyRailway-managed RestrictedAll (L4)data

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

bucket
Storage Method Key Management Notes
AWSCloudflare S3R2 (receipts, KYCinvoice documentsPDFs) SSE-KMSAES-256 server-side (AES-256)Cloudflare default) AWS KMS — drop-kyc-keyCloudflare-managed Server-sideEU encryption,region per-object
AWS S3 — backupsSSE-KMS (AES-256)AWS KMS — drop-backup-key (separate region)Separate key from primary
Developer workstationsOS-level full disk encryptionPlatform key (FileVault on macOS)Required for all development machinesrequired

3.3 Backup Encryption

All

Railway 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-facing TLS(api.bilko.io, version:bilko.io): TLS 1.3 (enforced atvia Cloudflare 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

All

sessioncookieswith:

res.cookie('refreshToken', 
token, { httpOnly: true, // Not accessible secure: // 'strict', //
Certificateset Type ValidityAuthorityRotationMonitoring
Externalto TLSJavaScript (getdrop.no)90true, daysLet'sHTTPS Encryptonly (viasameSite: Cloudflare)AutomatedAlertCSRF 14protection maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days before}); expiry
BankID integration certificatePer BankID scheduleBankID Norge AS CAManual — BankID renewal processCalendar reminder
AWS internal certificatesAWS-managedAWS Certificate ManagerAWS-managedAWS CloudWatch alerts

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 ClassificationKeyReason
national_id_encryptedtaxId (organization 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)drop-national-id-key (AWS KMS)
kyc_document_refusers / kyc_recordsRestricted (L4)drop-kyc-key (AWS KMS)
bankid_subusersConfidential (L3)Database TDE
token_hashsessionsConfidential (L3)Database TDE (SHA-256 hashseedmust not rawbe token)readable

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 secrets
  • JWT_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 josein ^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: JWT_SECRETcost envfactor 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 KMSStorage Rotation Owner
Fødselsnummer encryption keyJWT_SECRET AWSRailway KMSenvironment secretQuarterlySecurity
JWT_REFRESH_SECRETRailway environment secretQuarterlySecurity
FIELD_ENCRYPTION_KEY (drop-national-id-key)tax IDs, IBAN)Railway environment secret Annual Security team
DatabasePostgreSQL masterdisk keyencryption AWS KMSRailway-managed (drop-db-master-key)TDE) AnnualRailway-managed Security teamRailway
KYCCloudflare documentR2 keyencryption AWS KMS (drop-kyc-key)Cloudflare-managed AnnualCloudflare-managed Security team
Backup encryption keyAWS KMS — separate regionAnnualSecurity team
JWT signing secretAWS Secrets Manager (JWT_SECRET)QuarterlySecurity team
BankID certificatesBankID Norge AS CAPer BankID scheduleBankID Norge ASCloudflare
TLS certificates (external)Cloudflare) Cloudflare /Certificate Let's EncryptManager 90 days (automated)automatic) PlatformCloudflare

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

2 separatekeyquarterly
System Algorithm Key Size Mode Key Location NotesNext Rotation
SQLite (current)OSPostgreSQL disk encryptionPlatformXTSPlatformMVP only
PostgreSQL TDE (Phase 2)Railway) AES-256 256-bit XTS AWS KMS drop-db-master-keyRailway-managed Phase 2Railway-managed
FødselsnummerTax ID field encryption AES-256-GCM 256-bit GCM AWSRailway KMSenv drop-national-id-keysecret PhaseAnnual
IBAN field encryptionAES-256-GCM256-bitGCMRailway env secretAnnual
Cloudflare R2AES-256256-bit Cloudflare-managed Cloudflare-managed
External TLS (Cloudflare) ECDSA P-256 256-bit Cloudflare / Let's Encrypt 90-day90 certdays (auto)
JWT signing HMAC-SHA-256 256-bit AWSRailway Secretsenv Managersecret Rotation:Quarterly
Refresh token signingHMAC-SHA-256256-bitRailway env secretQuarterly
Password hashing bcrypt cost=12 N/A bcryptjs library
Session token lookupSHA-256256-bitN/AHash stored in sessions table
S3 backup encryptionAES-256 SSE-KMS256-bitAWS KMS drop-backup-keySeparate region

8. AlgorithmFinancial DeprecationData ScheduleIntegrity

Financial

datarequiresnotconfidentialityalsoBilkoenforces:

allfloatorJavaScriptnumber.PreventsroundingVATLoggedActiontableallmutationsappend-onlywithfinancial
  • Double-entry
  • = credit validated at
  • Exchange
  • rate transaction date. Historical accuracy
    Algorithm Currentjust Usage Deprecationbut Date Migrationintegrity. Target Status
    SHA-256
  • NUMERIC(19,4) for passwords
  • REMOVEDmonetary amounts (fix C4,never 2026-02-13) Removed bcrypt(12) DONE
    bcrypterrors in Argon2id Allcalculations. user
  • Immutable passwords
  • 2027-Q1 Argon2id (m=65536,t=3,p=4) Planned
    HS256old/new JWTvalues. Enables RS256/EdDSA JWTaudit. signingPhaseenforcement 2Ed25519debit (EdDSA)Planned
    SQLitebackend. TDEPrevents imbalanced PostgreSQLentries. TDEDatabasePhaselocking 2PostgreSQLrates +stored AWSat KMSPlanned
    preserved.

    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:

    1. Submit exception request to: [email protected][email protected]
    2. RequiredRequired: information:system system, data classification,affected, algorithm beingexcepted excepted,from, business justification, risk assessment, compensating controls, proposed exception duration (max 12 months)
    3. ApprovalApproval: required from: CISOCTO
    4. ExceptionsLog: logged in: compliance register (internal)/docs/security/exceptions.md
    5. Review: Quarterly — exceptions exceeding 12 months require board-level approval

    Active exceptions: None at this time.

    SystemExceptionExpiryCompensating Controls
    JWT signing (HS256)Shared-secret JWT instead of asymmetric keysPhase 2JWT_SECRET in AWS Secrets Manager; short expiry (24h); session revocation table
    SQLite (MVP)Filesystem encryption only, no column-level TDEPhase 2 migrationFull disk encryption; private network; no public DB access

    Approval

    Role Name Date Signature
    Author SecurityCompliance Architect 2026-02-23
    CISO
    CTO
    DPO (GDPR relevance)
    ManagementEngineering Lead