Add OS keychain integration #57

Open
opened 2026-01-03 23:47:04 +00:00 by KofTwentyTwo · 0 comments
KofTwentyTwo commented 2026-01-03 23:47:04 +00:00 (Migrated from github.com)

User Story

As a developer, I want qctl to store my credentials in the OS keychain so I don't have tokens in plain text files and they're protected by my system's security.

Design

Command Interface

# Credentials are stored automatically on login
qctl auth login
# -> Stores token in macOS Keychain / Windows Credential Store / Linux secret-tool

# View credential storage location
qctl auth status
# -> Shows: "Credentials stored in: macOS Keychain"

# Configure storage backend
qctl config set auth.credential_store keychain  # keychain | file | none

Keychain Entries

# macOS Keychain
Service: qctl
Account: voyage:access_token
Password: <token>

Service: qctl
Account: voyage:refresh_token
Password: <token>

# Windows Credential Store
Target: qctl:voyage:access_token
User: qctl
Password: <token>

# Linux secret-tool (libsecret)
Label: qctl voyage access_token
Attributes: application=qctl, provider=voyage, type=access_token
Secret: <token>

Fallback Strategy

1. Try OS keychain
2. If unavailable, fall back to encrypted file (~/.qctl/credentials.enc)
3. If encryption unavailable, warn and use plain file with strict permissions

Storage Configuration

# ~/.qctl/qctl.yaml
auth:
  credential_store: keychain  # keychain | file | memory
  file_encryption: true       # encrypt file-based storage
  encryption_key_source: keychain  # where to get encryption key

Files to Create/Modify

File Action Purpose
qctl-core/src/main/java/io/qrun/qctl/core/auth/CredentialStore.java Create Store interface
qctl-core/src/main/java/io/qrun/qctl/core/auth/KeychainStore.java Create OS keychain implementation
qctl-core/src/main/java/io/qrun/qctl/core/auth/MacOSKeychain.java Create macOS Keychain via Security.framework
qctl-core/src/main/java/io/qrun/qctl/core/auth/WindowsCredStore.java Create Windows Credential Store
qctl-core/src/main/java/io/qrun/qctl/core/auth/LinuxSecretService.java Create Linux libsecret/secret-tool
qctl-core/src/main/java/io/qrun/qctl/core/auth/FileCredentialStore.java Create Encrypted file fallback
qctl-core/src/main/java/io/qrun/qctl/core/auth/CredentialStoreFactory.java Create Platform detection

Implementation Tasks

  • Create CredentialStore interface
  • Implement MacOSKeychain using JNA/native calls
  • Implement WindowsCredStore using JNA/Credential Manager API
  • Implement LinuxSecretService using secret-tool CLI
  • Implement FileCredentialStore with AES encryption
  • Create CredentialStoreFactory with platform detection
  • Integrate with TokenManager
  • Handle keychain access prompts gracefully
  • Add fallback chain with warnings
  • Support credential migration between stores
  • Set strict file permissions (600) for fallback
  • Write unit tests for each store implementation
  • Write integration tests for credential lifecycle

Acceptance Criteria

  • macOS: tokens stored in Keychain.app
  • Windows: tokens in Credential Manager
  • Linux: tokens via secret-tool (libsecret)
  • Fallback to encrypted file if keychain unavailable
  • qctl auth status shows storage location
  • Tokens survive qctl upgrades
  • No plaintext tokens in ~/.qctl/
  • Graceful handling of keychain access denied
  • Works in headless/SSH environments with fallback
## User Story As a developer, I want qctl to store my credentials in the OS keychain so I don't have tokens in plain text files and they're protected by my system's security. ## Design ### Command Interface ```bash # Credentials are stored automatically on login qctl auth login # -> Stores token in macOS Keychain / Windows Credential Store / Linux secret-tool # View credential storage location qctl auth status # -> Shows: "Credentials stored in: macOS Keychain" # Configure storage backend qctl config set auth.credential_store keychain # keychain | file | none ``` ### Keychain Entries ``` # macOS Keychain Service: qctl Account: voyage:access_token Password: <token> Service: qctl Account: voyage:refresh_token Password: <token> # Windows Credential Store Target: qctl:voyage:access_token User: qctl Password: <token> # Linux secret-tool (libsecret) Label: qctl voyage access_token Attributes: application=qctl, provider=voyage, type=access_token Secret: <token> ``` ### Fallback Strategy ``` 1. Try OS keychain 2. If unavailable, fall back to encrypted file (~/.qctl/credentials.enc) 3. If encryption unavailable, warn and use plain file with strict permissions ``` ### Storage Configuration ```yaml # ~/.qctl/qctl.yaml auth: credential_store: keychain # keychain | file | memory file_encryption: true # encrypt file-based storage encryption_key_source: keychain # where to get encryption key ``` ## Files to Create/Modify | File | Action | Purpose | |------|--------|---------| | `qctl-core/src/main/java/io/qrun/qctl/core/auth/CredentialStore.java` | Create | Store interface | | `qctl-core/src/main/java/io/qrun/qctl/core/auth/KeychainStore.java` | Create | OS keychain implementation | | `qctl-core/src/main/java/io/qrun/qctl/core/auth/MacOSKeychain.java` | Create | macOS Keychain via Security.framework | | `qctl-core/src/main/java/io/qrun/qctl/core/auth/WindowsCredStore.java` | Create | Windows Credential Store | | `qctl-core/src/main/java/io/qrun/qctl/core/auth/LinuxSecretService.java` | Create | Linux libsecret/secret-tool | | `qctl-core/src/main/java/io/qrun/qctl/core/auth/FileCredentialStore.java` | Create | Encrypted file fallback | | `qctl-core/src/main/java/io/qrun/qctl/core/auth/CredentialStoreFactory.java` | Create | Platform detection | ## Implementation Tasks - [ ] Create CredentialStore interface - [ ] Implement MacOSKeychain using JNA/native calls - [ ] Implement WindowsCredStore using JNA/Credential Manager API - [ ] Implement LinuxSecretService using secret-tool CLI - [ ] Implement FileCredentialStore with AES encryption - [ ] Create CredentialStoreFactory with platform detection - [ ] Integrate with TokenManager - [ ] Handle keychain access prompts gracefully - [ ] Add fallback chain with warnings - [ ] Support credential migration between stores - [ ] Set strict file permissions (600) for fallback - [ ] Write unit tests for each store implementation - [ ] Write integration tests for credential lifecycle ## Acceptance Criteria - [ ] macOS: tokens stored in Keychain.app - [ ] Windows: tokens in Credential Manager - [ ] Linux: tokens via secret-tool (libsecret) - [ ] Fallback to encrypted file if keychain unavailable - [ ] `qctl auth status` shows storage location - [ ] Tokens survive qctl upgrades - [ ] No plaintext tokens in ~/.qctl/ - [ ] Graceful handling of keychain access denied - [ ] Works in headless/SSH environments with fallback
Sign in to join this conversation.