Skip to content

Data Classification and Protection Levels

CASA Tier 2 self-attestation for ASVS 1.8.1, 1.8.2.

Classification Levels

CRITICAL -- Encryption + Access Control Required

DataStorageProtection
User passwordsusers.password_hashbcrypt (bcryptjs), 12 rounds. Timing-attack resistant: dummy hash compared when user not found.
Recovery keysusers.recovery_key_hashSHA-256 hash stored. Raw key (32 random bytes, base64-encoded) shown to user once at signup as mnemonic phrase. Never persisted in plaintext.
OAuth tokensaccounts.auth_contextAES-256-GCM encrypted with APP_SECRET-derived key. Wire format: base64(iv):base64(ciphertext):base64(tag).
Connector credentialsconnector_credentials.credentialsAES-256-GCM encrypted (same scheme as OAuth tokens).
JWT secretsEnvironment variables onlyJWT_ACCESS_SECRET, JWT_REFRESH_SECRET, OAUTH_JWT_SECRET -- never stored in database.
APP_SECRETEnvironment variable onlyServer-level master key. Fatal warning if default value detected. Derived via scrypt into 256-bit AES key.

SENSITIVE -- Per-User Encryption Required

DataStorageProtection
Memory textmemories.textAES-256-GCM with per-user DEK (recovery key). Stale DEK detection triggers recovery key prompt.
Memory entitiesmemories.entitiesAES-256-GCM with per-user DEK.
Memory claimsmemories.claimsAES-256-GCM with per-user DEK.
Memory metadatamemories.metadataAES-256-GCM with per-user DEK.
Memory factualitymemories.factualityAES-256-GCM encrypted text (migrated from jsonb). Plaintext factuality_label column kept for SQL aggregation only.
Contact display namespeople.display_nameEncrypted. display_name_hash (HMAC-SHA256) used as blind index for search.
Contact identifiersperson_identifiers.identifier_valueEncrypted. identifier_value_hash (HMAC-SHA256 blind index) for lookups without decryption.
Account identifiersaccounts.identifierEncrypted. identifier_hash (HMAC-SHA256 blind index) for search.
Raw event payloadsraw_events.payloadLarge JSON stored as text. Contains original connector data before normalization.
Refresh tokensrefresh_tokens.token_hashSHA-256 hashed (never stored plaintext). 7-day expiry. Token family tracking for rotation detection.

INTERNAL -- Access-Controlled Application Data

DataStorageProtection
Job metadatajobs tableStatus, progress, error fields. Scoped by account_id -> user_id via RLS.
Connector manifestsIn-code manifest objectsStatic metadata (connector type, auth type, config schema). No secrets.
Sync stateaccounts.last_cursor, last_sync_atPagination cursors for incremental sync. Scoped by user.
System configurationsettings tableKey-value pairs for system-level config.
Queue job payloadsRedis (BullMQ)Contains accountId, memoryBankId, job parameters. Redis on internal Docker network only.
Memory banksmemory_banks tableUser-scoped grouping. name_hash HMAC blind index for encrypted name search.
Memory weightsmemories.weights (jsonb)Scoring metadata (semantic, recency, importance, trust). Not encrypted (no PII).
Search tokensmemories.search_tokens (tsvector)Pre-computed from plaintext before encryption. Enables PostgreSQL full-text search without decryption.

PUBLIC -- No Protection Required

DataEndpoint
API version + build infoGET /api/version
Health checkGET /api/health
Connector type listReturned in connector manifests (public metadata)
OpenAPI specGET /api/docs (Swagger)

Key Management

APP_SECRET (Server-Level Key)

  • Set via APP_SECRET environment variable.
  • Derived into 256-bit AES key via scrypt(APP_SECRET, ENCRYPTION_SALT, 32).
  • Separate HMAC key derived via scrypt(APP_SECRET, 'botmem-hmac-v1', 32).
  • Used for: connector credentials, OAuth tokens, Redis DEK cache wrapping.
  • Default value triggers warning in dev, should be fatal in production.
  • Rotation requires re-encrypting all server-encrypted columns.

Per-User DEK (Data Encryption Key)

  • Generated at signup: crypto.randomBytes(32) -- 256-bit random key.
  • Recovery key = base64-encoded DEK, displayed to user once as mnemonic phrase.
  • users.recovery_key_hash stores SHA-256 hash for verification.
  • 2-tier cache (no database tier for raw DEK):
    • Tier 1 (Memory): In-process Map, 1-hour inactivity TTL. Buffer zeroed on eviction.
    • Tier 2 (Redis): Encrypted with APP_SECRET (AES-256-GCM), 30-day TTL. Survives process restarts.
  • If both caches are cold, user must re-enter recovery key via POST /user-auth/recovery-key.
  • On module shutdown, all in-memory key buffers are zeroed (Buffer.fill(0)) before deletion.

HMAC Blind Indexes

  • Deterministic HMAC-SHA256 computed from plaintext using HMAC key (derived from APP_SECRET).
  • Stored alongside encrypted values in *_hash columns.
  • Enables equality search on encrypted fields without decryption.
  • Used for: accounts.identifier_hash, people.display_name_hash, person_identifiers.identifier_value_hash, memory_banks.name_hash.

Data Retention

  • Memories are never deleted -- classified by factuality label (FACT / UNVERIFIED / FICTION) instead.
  • Raw events are immutable -- original connector payloads preserved for audit and re-processing.
  • Refresh tokens expire after 7 days. Revoked tokens remain in database for family rotation detection.
  • Password reset tokens expire (configurable). Marked as used after consumption.
  • BullMQ jobs retained in Redis with configurable TTL per queue.

Encryption in Transit

  • External traffic: TLS 1.2+ via Caddy with automatic Let's Encrypt certificates (ACME).
  • HSTS: Enforced by Caddy configuration.
  • Internal traffic: All data-zone services (PostgreSQL + pgvector, Redis, Qdrant) communicate over Docker internal network with no exposed ports. API-to-data connections are unencrypted loopback (acceptable per CASA for same-host Docker networking).
  • CORS: Origin whitelist from FRONTEND_URL environment variable. Credentials allowed. Methods restricted to standard REST verbs.
  • WebSocket (/events): Same origin policy, authenticated via JWT in connection handshake.

Cipher Configuration (CASA 6.2.3)

  • Algorithm: AES-256-GCM (authenticated encryption with associated data)
  • IV: 12 bytes (96 bits), cryptographically random per encryption operation
  • Auth tag: 16 bytes (128 bits), appended to ciphertext for integrity verification
  • Mode: GCM -- provides both confidentiality and authenticity. No ECB or CBC used.
  • Key derivation: scrypt(APP_SECRET, salt, keylen=32) -- 256-bit key
  • Wire format: base64(iv):base64(ciphertext):base64(tag)
  • Failure mode: If GCM authentication fails, decrypt() throws -- never returns partial or corrupt plaintext (CASA 6.2.1).
  • Plaintext passthrough: Data not matching the iv:data:tag format is returned as-is, supporting pre-encryption migration data gracefully.

Your memories. Your agents. Your control.