Music Store Management Platform Vault — On-Premise Password Manager Version 1.0 | Draft # 1. Overview LunarFront includes a built-in password vault for on-premise deployments. Music stores deal with dozens of credentials — supplier portals, distributor accounts, payment processor dashboards, shipping services, insurance logins, ASCAP/BMI licensing portals — and most store owners are not technical enough to adopt a separate password manager. The vault is a simple, encrypted credential store baked into LunarFront. It encrypts all secrets at rest using AES-256-GCM with a key derived from a master passphrase. The key exists only in server memory and is discarded on restart. **On-premise only.** Cloud/hosted deployments would require a more sophisticated key management approach (HSM, envelope encryption) and are out of scope for this design. # 2. Security Model ## 2.1 Encryption - Algorithm: AES-256-GCM (authenticated encryption) - Key derivation: PBKDF2 or Argon2id from master passphrase + stored salt - Each entry encrypted individually (not a single encrypted blob) - Each entry gets its own random IV/nonce - Encrypted fields: username, password, notes - Plaintext fields: entry name, URL, category, tags (searchable) ## 2.2 Key Lifecycle 1. **First-time setup:** Admin sets master passphrase via the admin UI. System generates a random salt, derives the encryption key, encrypts a known verification string, and stores the salt + verification ciphertext in the database. 2. **Server start:** Vault is locked. Encryption key is not in memory. All vault API endpoints return `423 Locked`. 3. **Unlock:** Admin enters master passphrase via the UI. System re-derives the key using the stored salt, verifies it against the verification ciphertext. If correct, key is held in memory. Vault endpoints become functional. 4. **Server restart:** Key is lost. Vault returns to locked state. 5. **Lock (manual):** Admin can lock the vault without restarting. Key is wiped from memory. ## 2.3 What's Encrypted vs. Plaintext | Field | Storage | Reason | |-------|---------|--------| | `name` | Plaintext | Searchable, displayed in list | | `url` | Plaintext | Searchable, displayed in list | | `category` | Plaintext | Filterable | | `username` | **Encrypted** | Credential | | `password` | **Encrypted** | Credential | | `notes` | **Encrypted** | May contain secrets | | `created_at` | Plaintext | Metadata | | `updated_at` | Plaintext | Metadata | | `created_by` | Plaintext | Audit | ## 2.4 Development Mode In development, support a `VAULT_PASSPHRASE` environment variable to auto-unlock on startup. This avoids blocking the dev workflow. Never use this in production. # 3. Database Schema ## 3.1 vault_config Stores the salt and verification data. One row per company. ``` vault_config id uuid PK company_id uuid FK → company salt bytea -- random salt for key derivation verification bytea -- encrypted known string for passphrase verification kdf_algorithm varchar(20) -- 'argon2id' or 'pbkdf2' kdf_iterations integer -- iteration count / cost parameter created_at timestamptz updated_at timestamptz ``` ## 3.2 vault_entry Individual credential entries. ``` vault_entry id uuid PK company_id uuid FK → company name varchar(255) -- plaintext, searchable url varchar(500) -- plaintext, searchable category varchar(100) -- plaintext, filterable username_enc bytea -- AES-256-GCM encrypted username_iv bytea -- nonce for username password_enc bytea -- AES-256-GCM encrypted password_iv bytea -- nonce for password notes_enc bytea -- AES-256-GCM encrypted (nullable) notes_iv bytea -- nonce for notes (nullable) created_by uuid FK → user updated_by uuid FK → user created_at timestamptz updated_at timestamptz ``` ## 3.3 vault_access_log Append-only audit trail for credential access. ``` vault_access_log id uuid PK company_id uuid FK → company entry_id uuid FK → vault_entry user_id uuid FK → user action varchar(20) -- 'view', 'copy', 'create', 'update', 'delete' field varchar(20) -- 'password', 'username', 'notes' (for view/copy) ip_address varchar(45) created_at timestamptz ``` # 4. API Endpoints All vault endpoints are prefixed with `/v1/vault`. ## 4.1 Vault Status & Management | Method | Path | Permission | Description | |--------|------|------------|-------------| | GET | `/vault/status` | Any authenticated | Returns `{ locked: bool, initialized: bool }` | | POST | `/vault/initialize` | `vault.admin` | Set master passphrase (first time only) | | POST | `/vault/unlock` | `vault.admin` | Unlock vault with passphrase | | POST | `/vault/lock` | `vault.admin` | Lock vault (wipe key from memory) | | POST | `/vault/change-passphrase` | `vault.admin` | Change master passphrase (re-encrypts all entries) | ## 4.2 Entries All return `423 Locked` when vault is locked. | Method | Path | Permission | Description | |--------|------|------------|-------------| | GET | `/vault/entries` | `vault.view` | List entries (paginated, name/url/category only) | | POST | `/vault/entries` | `vault.edit` | Create entry | | GET | `/vault/entries/:id` | `vault.view` | Get entry (decrypts username, password, notes) | | PATCH | `/vault/entries/:id` | `vault.edit` | Update entry | | DELETE | `/vault/entries/:id` | `vault.edit` | Delete entry | | POST | `/vault/entries/:id/copy` | `vault.view` | Log that user copied a credential | ## 4.3 Audit | Method | Path | Permission | Description | |--------|------|------------|-------------| | GET | `/vault/audit` | `vault.admin` | Access log (paginated, filterable by user/entry) | ## 4.4 Locked Response When vault is locked, all entry/audit endpoints return: ```json { "error": { "message": "Vault is locked", "statusCode": 423 } } ``` # 5. Permissions New permission domain: `vault` | Slug | Description | |------|-------------| | `vault.view` | View entry list, decrypt and view credentials | | `vault.edit` | Create, update, delete entries | | `vault.admin` | Initialize, unlock, lock, change passphrase, view audit log | Permission inheritance: `vault.admin` → `vault.edit` → `vault.view` Default role assignments: - **Admin:** `vault.admin` - **Manager:** `vault.edit` - **All others:** No vault access by default # 6. Frontend ## 6.1 Sidebar New "Vault" item in the Admin section (visible only with `vault.view`). Shows a lock icon when locked. ## 6.2 Pages ### Vault List Page - Search by name/URL - Filter by category - Table columns: Name, URL, Category, Updated - Click row to view details - "New Entry" button (requires `vault.edit`) ### Vault Entry Detail - Shows name, URL, category - Username field with copy button (masked by default, click to reveal) - Password field with copy button (masked by default, click to reveal) - Notes (hidden by default, click to reveal) - Edit/Delete buttons (requires `vault.edit`) - Every reveal/copy action is logged ### Vault Locked Page - Shown when navigating to vault and it's locked - Passphrase input + "Unlock" button (requires `vault.admin`) - If not initialized: "Set Master Passphrase" flow with confirmation input ### Vault Setup (First Time) - Enter master passphrase (min 16 characters) - Confirm passphrase - Warning: "This passphrase cannot be recovered. If lost, all vault entries are permanently inaccessible." ## 6.3 UI Behaviors - Password fields use `type="password"` with a toggle eye icon - Copy-to-clipboard auto-clears after 30 seconds - Toast notification on copy: "Password copied — clipboard will clear in 30s" - Reveal auto-hides after 30 seconds - No browser autocomplete on vault forms (`autocomplete="off"`) # 7. Operational Considerations ## 7.1 Passphrase Recovery There is no recovery mechanism by design. If the master passphrase is lost, all encrypted entries are permanently inaccessible. The vault_config and vault_entry rows can be deleted to start fresh, but the old data is gone. Document this clearly in the UI and in the setup flow. ## 7.2 Backup Database backups include encrypted vault data. Without the master passphrase, the encrypted fields are useless. This is the correct behavior — a stolen backup should not expose credentials. ## 7.3 Passphrase Change Changing the master passphrase requires: 1. Verify old passphrase 2. Derive new key from new passphrase + new salt 3. Decrypt every entry with old key, re-encrypt with new key (transaction) 4. Update vault_config with new salt + verification 5. Replace in-memory key This is an expensive operation (scales with entry count) but should be rare. ## 7.4 Server Restart Behavior After restart: - Vault status: locked - All vault API calls: 423 - Everything else in LunarFront: fully functional - Admin unlocks when ready — no urgency, no data loss ## 7.5 Multiple Servers If running multiple backend instances behind a load balancer, each instance needs to be unlocked independently (since the key is in-process memory). For multi-instance deployments, consider a shared unlock via Redis (store the derived key in Valkey with a TTL matching the server session). This is an optional enhancement — most on-prem music stores run a single instance. # 8. Categories (Defaults) Pre-populated categories that stores can customize: - Suppliers - Distributors - Payment Processing - Shipping & Freight - Insurance - Licensing (ASCAP, BMI, SESAC) - Banking - Software & Services - Utilities - Social Media - Website & Hosting - Other # 9. Implementation Order 1. Database schema + migration 2. Encryption service (encrypt, decrypt, key derivation, verification) 3. Vault status/initialize/unlock/lock endpoints 4. Entry CRUD endpoints 5. Audit logging 6. Add `vault.*` permissions to RBAC seed 7. Frontend: locked page + unlock flow 8. Frontend: entry list + detail + create/edit 9. Frontend: audit log page 10. Tests: encryption unit tests, API integration tests