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
