FEAT: Multi-profile vaults with isolated credentials and per-profile master passwords #19

Merged
kashish merged 29 commits from feat/vault/profiles into main 2026-08-07 19:15:57 +00:00
Owner

Summary

Introduces profiles: each profile is a separate vault file with its own
Curve25519 keypair, its own Argon2id salt and its own master password. Every
command operates on the active profile only, so unrelated secrets - work and
personal, say - stay cryptographically isolated on the same machine.

Existing vaults are migrated automatically and losslessly on first run.

Functional Changes

New commands

  • kosh use [profile] - switch the active profile. With no arguments, opens an
    interactive picker (type to filter, enter to select, esc to cancel). Fails if
    the named profile does not exist; it does not create profiles. The choice is
    persisted to config.
  • kosh profile list [filter] - table of profiles and which one is active.
    Reads filenames only, so it opens no vault and requires no master password.
  • kosh profile create <name> - creates the profile, switches to it, and
    initializes its vault with a master password of its own. kosh init is not
    needed afterwards.
  • kosh profile delete <name> - deletes a profile and every credential in it.
    Guarded four ways: the active profile cannot be deleted, the target profile's
    own master password must be verified, a confirmation phrase must be typed out
    in full, and the vault file is overwritten with random bytes and synced to
    disk before it is unlinked.
  • kosh copy <id> <profile> - copies a credential into another profile's vault.
    The secret is decrypted with the active profile's password and re-encrypted
    for the target under a fresh ephemeral keypair and nonce. The original is left
    untouched.

On-disk layout and migration

~/.kosh/kosh.db becomes ~/.kosh/profiles/<name>.db, with the active profile
recorded in ~/.kosh/config.json (0600; ~/.kosh stays 0700).

Migration runs on every startup and acts once: if profiles/default.db already
exists it exits early, otherwise a legacy kosh.db is renamed into place as
the default profile. Nothing is re-encrypted and the master password is
unchanged. The early exit also means a stray kosh.db can never clobber an
existing default profile.

Existing commands

  • kosh init - now scoped to the active profile, idempotent, and reports
    vault already initialized rather than touching existing data.
  • kosh list- auto-sized table instead of fixed-width truncation; adds
    Access Count, and renders last-used/last-updated as relative times.
  • kosh add / kosh delete / kosh update - destructive paths now show a
    caution block and require a typed confirmation phrase.
  • kosh generate - flag state moved off package globals onto a per-command
    struct.
  • Every command gained a full Long description and worked Example block.

Internal Changes

  • Dependency injection. Commands are NewCmdX(ctx *app.Context) constructors
    registered in cmd/root.go, replacing package-level globals and init() side
    effects. The store is opened in PersistentPreRun, closed in
    PersistentPostRun.
  • Service interfaces. core.VaultService and core.ProfileService are now
    interfaces (KoshVault, KoshProfile), so commands can be tested with fakes.
  • Schema migrations. New schema_migrations table and an ordered migration
    list applied transactionally on store init. DDL moved out of
    InitializeVault, which now only inserts the vault row.
  • Logging. Custom logger replaced with log/slog. Lower layers return
    errors without logging them; Execute() logs once and prints once.
  • Error boundaries. Storage returns constants.ErrCredentialNotFound
    instead of leaking sql.ErrNoRows; vault/store errors are wrapped with %w.
  • New internal/ui surface: output.go (profile prefix, glyphs, Caution),
    table.go (zero-dependency auto-sized table), time.go (RelativeTime).
  • New packages: internal/config, internal/app, internal/model/profile.go,
    internal/crypto/file.go, cmd/profile/.

Security

  • Profiles are fully isolated - unlocking one grants no access to another.
  • DecryptCredential returns []byte rather than string, so plaintext is not
    copied into an immutable value.
  • Credential, CredentialData and CredentialSummary implement
    slog.LogValue and redact secrets, nonces and ephemeral keys, so debug output
    cannot print ciphertext by accident.
  • Master-password and secret confirmation use crypto/subtle constant-time
    comparison.
  • Profile deletion overwrites the vault file with random bytes before unlinking,
    on top of SQLite's secure_delete=ON.

Bug Fixes

  • Ctrl+C during a password prompt no longer leaves the terminal with echo
    disabled - terminal state is restored and the process exits 130.
  • kosh generate --lower was documented as "include uppercase letters".

⚠ Breaking Change

Debug logging no longer requires a rebuild. The
logger.BuildMode ldflag is removed from .goreleaser.yaml; set KOSH_DEBUG
to a truthy value instead. Logs are structured and go to stderr.

KOSH_DEBUG=1 kosh list          # bash / zsh
$env:KOSH_DEBUG=1; kosh list    # PowerShell
## Summary Introduces **profiles**: each profile is a separate vault file with its own Curve25519 keypair, its own Argon2id salt and its own master password. Every command operates on the active profile only, so unrelated secrets - work and personal, say - stay cryptographically isolated on the same machine. Existing vaults are migrated automatically and losslessly on first run. ## Functional Changes ### New commands * `kosh use [profile]` - switch the active profile. With no arguments, opens an interactive picker (type to filter, enter to select, esc to cancel). Fails if the named profile does not exist; it does not create profiles. The choice is persisted to config. * `kosh profile list [filter]` - table of profiles and which one is active. Reads filenames only, so it opens no vault and requires no master password. * `kosh profile create <name>` - creates the profile, switches to it, and initializes its vault with a master password of its own. `kosh init` is not needed afterwards. * `kosh profile delete <name>` - deletes a profile and every credential in it. Guarded four ways: the active profile cannot be deleted, the target profile's own master password must be verified, a confirmation phrase must be typed out in full, and the vault file is overwritten with random bytes and synced to disk before it is unlinked. * `kosh copy <id> <profile>` - copies a credential into another profile's vault. The secret is decrypted with the active profile's password and re-encrypted for the target under a fresh ephemeral keypair and nonce. The original is left untouched. ### On-disk layout and migration `~/.kosh/kosh.db` becomes `~/.kosh/profiles/<name>.db`, with the active profile recorded in `~/.kosh/config.json` (`0600`; `~/.kosh` stays `0700`). Migration runs on every startup and acts once: if `profiles/default.db` already exists it exits early, otherwise a legacy `kosh.db` is **renamed** into place as the `default` profile. Nothing is re-encrypted and the master password is unchanged. The early exit also means a stray `kosh.db` can never clobber an existing default profile. ### Existing commands * `kosh init` - now scoped to the active profile, idempotent, and reports `vault already initialized` rather than touching existing data. * `kosh list`- auto-sized table instead of fixed-width truncation; adds `Access Count`, and renders last-used/last-updated as relative times. * `kosh add` / `kosh delete` / `kosh update` - destructive paths now show a caution block and require a typed confirmation phrase. * `kosh generate` - flag state moved off package globals onto a per-command struct. * Every command gained a full `Long` description and worked `Example` block. ## Internal Changes * **Dependency injection.** Commands are `NewCmdX(ctx *app.Context)` constructors registered in `cmd/root.go`, replacing package-level globals and `init()` side effects. The store is opened in `PersistentPreRun`, closed in `PersistentPostRun`. * **Service interfaces.** `core.VaultService` and `core.ProfileService` are now interfaces (`KoshVault`, `KoshProfile`), so commands can be tested with fakes. * **Schema migrations.** New `schema_migrations` table and an ordered migration list applied transactionally on store init. DDL moved out of `InitializeVault`, which now only inserts the vault row. * **Logging.** Custom logger replaced with `log/slog`. Lower layers return errors without logging them; `Execute()` logs once and prints once. * **Error boundaries.** Storage returns `constants.ErrCredentialNotFound` instead of leaking `sql.ErrNoRows`; vault/store errors are wrapped with `%w`. * New `internal/ui` surface: `output.go` (profile prefix, glyphs, `Caution`), `table.go` (zero-dependency auto-sized table), `time.go` (`RelativeTime`). * New packages: `internal/config`, `internal/app`, `internal/model/profile.go`, `internal/crypto/file.go`, `cmd/profile/`. ## Security * Profiles are fully isolated - unlocking one grants no access to another. * `DecryptCredential` returns `[]byte` rather than `string`, so plaintext is not copied into an immutable value. * `Credential`, `CredentialData` and `CredentialSummary` implement `slog.LogValue` and redact secrets, nonces and ephemeral keys, so debug output cannot print ciphertext by accident. * Master-password and secret confirmation use `crypto/subtle` constant-time comparison. * Profile deletion overwrites the vault file with random bytes before unlinking, on top of SQLite's `secure_delete=ON`. ## Bug Fixes * `Ctrl+C` during a password prompt no longer leaves the terminal with echo disabled - terminal state is restored and the process exits `130`. * `kosh generate --lower` was documented as "include uppercase letters". ## ⚠ Breaking Change Debug logging no longer requires a rebuild. The `logger.BuildMode` ldflag is removed from `.goreleaser.yaml`; set `KOSH_DEBUG` to a truthy value instead. Logs are structured and go to stderr. ```sh KOSH_DEBUG=1 kosh list # bash / zsh $env:KOSH_DEBUG=1; kosh list # PowerShell
- Profile functions as part of service for consistency
- All services now implement as interface for mocking
- App Context moved to separate package to avoid cycle imports
- Misc. code cleanup
- Added profile list subcommand
- Improved Table UI implemented as UI entity
kashish self-assigned this 2026-08-02 16:58:24 +00:00
Author
Owner
  • Update README.md
- [ ] Update README.md
Author
Owner

Added

  • Profile Name Sanitization
  • Partial Profile creation rollback
  • e2e tests for profile creation, name sanitization and access

Changed

  • kosh profile create now first initializes vault and then switches to it. Switch profile only when persisted.
  • Suppress cobra Error & Help command on application error
Added - Profile Name Sanitization - Partial Profile creation rollback - e2e tests for profile creation, name sanitization and access Changed - `kosh profile create` now first initializes vault and then switches to it. Switch profile only when persisted. - Suppress cobra Error & Help command on application error
kashish changed title from WIP: FEAT: Multi-profile vaults with isolated credentials and per-profile master passwords to FEAT: Multi-profile vaults with isolated credentials and per-profile master passwords 2026-08-07 19:12:08 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
plutolab/kosh!19
No description provided.