Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Pass-CLI is a fast, secure password and API key manager that stores credentials
- **Usage Tracking**: Automatic tracking of where credentials are used across projects
- **Offline First**: No cloud dependencies, works completely offline
- **Interactive TUI**: Terminal UI for visual credential management
- **TOTP / 2FA Support**: Store TOTP secrets and generate 6-digit codes - no separate authenticator app needed

## Quick Start

Expand Down Expand Up @@ -247,10 +248,6 @@ For more questions and troubleshooting, see [docs/04-troubleshooting/faq.md](doc

## Roadmap

Planned features for future releases:

- **TOTP / 2FA Support**: Store TOTP secrets with credentials and generate 6-digit codes on demand - no separate authenticator app needed

Have a feature request? Open an issue on [GitHub](https://github.com/arimxyer/pass-cli/issues).

## Contributing
Expand Down
73 changes: 66 additions & 7 deletions cmd/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ import (
)

var (
addUsername string
addPassword string
addCategory string
addURL string
addNotes string
addUsername string
addPassword string
addCategory string
addURL string
addNotes string
addGeneratePassword bool
addGenLength int
addGenLength int
addTOTPURI string // TOTP otpauth:// URI
addTOTP bool // Prompt for TOTP secret interactively
)

var addCmd = &cobra.Command{
Expand All @@ -38,6 +40,8 @@ hidden for security. If you want to provide these values via flags, use:
--category (-c) for organizing credentials (e.g., 'Cloud', 'Databases')
--url for the service URL (e.g., login page URL)
--notes for additional information
--totp-uri to add TOTP/2FA support with an otpauth:// URI
--totp to be prompted for TOTP secret interactively

The service name should be descriptive and unique (e.g., "github", "aws-prod", "db-staging").`,
Example: ` # Add a credential with prompts
Expand All @@ -59,7 +63,13 @@ The service name should be descriptive and unique (e.g., "github", "aws-prod", "
pass-cli add github -u user@example.com -g --gen-length 32

# Add with all metadata fields
pass-cli add github -u user@example.com -c "Version Control" --url "https://github.com" --notes "Work account"`,
pass-cli add github -u user@example.com -c "Version Control" --url "https://github.com" --notes "Work account"

# Add with TOTP/2FA support
pass-cli add github -u user@example.com --totp-uri "otpauth://totp/GitHub:user?secret=JBSWY3DPEHPK3PXP&issuer=GitHub"

# Add with interactive TOTP prompt
pass-cli add github -u user@example.com --totp`,
Args: cobra.ExactArgs(1),
RunE: runAdd,
}
Expand All @@ -73,9 +83,13 @@ func init() {
addCmd.Flags().StringVarP(&addCategory, "category", "c", "", "category for organizing credentials (e.g., 'Cloud', 'Databases')")
addCmd.Flags().StringVar(&addURL, "url", "", "URL associated with the credential (e.g., login page)")
addCmd.Flags().StringVar(&addNotes, "notes", "", "optional notes about the credential")
addCmd.Flags().StringVar(&addTOTPURI, "totp-uri", "", "TOTP/2FA otpauth:// URI (from QR code or authenticator app)")
addCmd.Flags().BoolVar(&addTOTP, "totp", false, "prompt for TOTP secret interactively")

// Mark --password and --generate as mutually exclusive
addCmd.MarkFlagsMutuallyExclusive("password", "generate")
// Mark --totp-uri and --totp as mutually exclusive
addCmd.MarkFlagsMutuallyExclusive("totp-uri", "totp")
}

func runAdd(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -156,6 +170,48 @@ func runAdd(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to add credential: %w", err)
}

// Handle TOTP if provided
var totpConfigured bool
if addTOTPURI != "" || addTOTP {
totpURI := addTOTPURI

// If --totp flag is set, prompt for TOTP secret
if addTOTP {
fmt.Print("TOTP Secret (base32) or otpauth:// URI: ")
var totpInput string
if _, err := fmt.Scanln(&totpInput); err != nil {
fmt.Fprintf(os.Stderr, "⚠️ Warning: failed to read TOTP input, skipping TOTP setup: %v\n", err)
} else {
totpURI = strings.TrimSpace(totpInput)
}
}

if totpURI != "" {
// Parse and validate TOTP
totpConfig, err := vault.ParseTOTPURI(totpURI)
if err != nil {
fmt.Fprintf(os.Stderr, "⚠️ Warning: invalid TOTP configuration: %v\n", err)
} else {
// Update credential with TOTP fields
opts := vault.UpdateOpts{
TOTPSecret: &totpConfig.Secret,
TOTPAlgorithm: &totpConfig.Algorithm,
TOTPDigits: &totpConfig.Digits,
TOTPPeriod: &totpConfig.Period,
}
if totpConfig.Issuer != "" {
opts.TOTPIssuer = &totpConfig.Issuer
}

if err := vaultService.UpdateCredential(service, opts); err != nil {
fmt.Fprintf(os.Stderr, "⚠️ Warning: failed to save TOTP configuration: %v\n", err)
} else {
totpConfigured = true
}
}
}
}

// Success message
fmt.Printf("✅ Credential added successfully!\n")
fmt.Printf("📝 Service: %s\n", service)
Expand All @@ -171,6 +227,9 @@ func runAdd(cmd *cobra.Command, args []string) error {
if addNotes != "" {
fmt.Printf("📋 Notes: %s\n", addNotes)
}
if totpConfigured {
fmt.Printf("🔐 TOTP: configured\n")
}

return nil
}
Expand Down
170 changes: 169 additions & 1 deletion cmd/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ var (
getField string
getNoClipboard bool
getMasked bool
getTOTP bool // Output TOTP code instead of password
getTOTPQR bool // Display TOTP QR code in terminal
getTOTPQRFile string // Export TOTP QR code to file
)

var getCmd = &cobra.Command{
Expand All @@ -32,6 +35,9 @@ are displayed. Use flags to customize the output:
--field Extract a specific field (username, password, category, url, notes, service)
--no-clipboard Skip copying to clipboard
--masked Display password as asterisks (default shows full password)
--totp Output TOTP code instead of password (requires TOTP to be configured)
--totp-qr Display TOTP QR code in terminal (for adding to another device)
--totp-qr-file Export TOTP QR code to a PNG file

Automatic usage tracking records where credentials are accessed based on
your current working directory.`,
Expand All @@ -48,7 +54,19 @@ your current working directory.`,
pass-cli get github --no-clipboard

# Get with masked password display
pass-cli get github --masked`,
pass-cli get github --masked

# Get TOTP code
pass-cli get github --totp

# Get TOTP code for scripts
pass-cli get github --totp --quiet

# Display TOTP QR code in terminal (to add to another device)
pass-cli get github --totp-qr

# Export TOTP QR code to a PNG file
pass-cli get github --totp-qr-file totp-github.png`,
Args: cobra.ExactArgs(1),
RunE: runGet,
}
Expand All @@ -59,6 +77,9 @@ func init() {
getCmd.Flags().StringVarP(&getField, "field", "f", "password", "field to extract (username, password, category, url, notes, service)")
getCmd.Flags().BoolVar(&getNoClipboard, "no-clipboard", false, "do not copy to clipboard")
getCmd.Flags().BoolVar(&getMasked, "masked", false, "display password as asterisks")
getCmd.Flags().BoolVar(&getTOTP, "totp", false, "output TOTP code instead of password")
getCmd.Flags().BoolVar(&getTOTPQR, "totp-qr", false, "display TOTP QR code in terminal")
getCmd.Flags().StringVar(&getTOTPQRFile, "totp-qr-file", "", "export TOTP QR code to PNG file")
}

func runGet(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -92,6 +113,21 @@ func runGet(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to get credential: %w", err)
}

// TOTP QR code display mode
if getTOTPQR {
return outputTOTPQRMode(cred, service)
}

// TOTP QR code file export mode
if getTOTPQRFile != "" {
return exportTOTPQRFile(cred, service, getTOTPQRFile)
}

// TOTP mode - output TOTP code
if getTOTP {
return outputTOTPMode(cred, vaultService, service)
}

// Quiet mode - output only requested field
if getQuiet {
return outputQuietMode(cred, vaultService, service)
Expand Down Expand Up @@ -139,6 +175,129 @@ func outputQuietMode(cred *vault.Credential, vaultService *vault.VaultService, s
return nil
}

// outputTOTPMode generates and displays the TOTP code
func outputTOTPMode(cred *vault.Credential, vaultService *vault.VaultService, service string) error {
// Check time sync in background (don't block code generation)
timeSyncChan := make(chan vault.TimeSyncResult, 1)
go func() {
timeSyncChan <- vault.CheckTimeSync()
}()

// Generate TOTP code with audit logging
code, remaining, err := vaultService.GetTOTPCode(service)
if err != nil {
return fmt.Errorf("failed to generate TOTP code: %w", err)
}

// Track TOTP access for usage statistics
if err := vaultService.RecordFieldAccess(service, "totp"); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to track TOTP access: %v\n", err)
}

// Quiet mode - just output the code
if getQuiet {
fmt.Println(code)
return nil
}

// Check for time sync warning (non-blocking, with short timeout)
select {
case result := <-timeSyncChan:
if warning := vault.FormatTimeSyncWarning(result); warning != "" {
fmt.Fprintln(os.Stderr, warning)
fmt.Fprintln(os.Stderr)
}
case <-time.After(100 * time.Millisecond):
// Don't wait too long - time check is best-effort
}

// Normal mode - show code with countdown
fmt.Printf("🔐 TOTP Code: %s\n", code)
fmt.Printf("⏱ Valid for: %ds\n", remaining)

// Show progress bar
period := 30
if cred.TOTPPeriod > 0 {
period = cred.TOTPPeriod
}
progress := float64(remaining) / float64(period)
barWidth := 20
filled := int(progress * float64(barWidth))
empty := barWidth - filled
fmt.Printf(" [%s%s]\n", strings.Repeat("█", filled), strings.Repeat("░", empty))

// Copy to clipboard unless disabled
if !getNoClipboard {
if err := clipboard.WriteAll(code); err != nil {
fmt.Fprintf(os.Stderr, "\n⚠️ Warning: failed to copy to clipboard: %v\n", err)
} else {
fmt.Println("\n✅ TOTP code copied to clipboard!")

// Schedule clipboard clear in background (based on remaining validity)
go func() {
time.Sleep(time.Duration(remaining) * time.Second)
// Only clear if the clipboard still contains our code
if current, err := clipboard.ReadAll(); err == nil && current == code {
_ = clipboard.WriteAll("")
if IsVerbose() {
fmt.Fprintln(os.Stderr, "🧹 Clipboard cleared")
}
}
}()
}
}

return nil
}

// outputTOTPQRMode displays the TOTP QR code in the terminal
func outputTOTPQRMode(cred *vault.Credential, service string) error {
if !cred.HasTOTP() {
return fmt.Errorf("no TOTP configured for credential: %s", service)
}

fmt.Printf("🔐 TOTP QR Code for: %s\n", service)
if cred.TOTPIssuer != "" {
fmt.Printf(" Issuer: %s\n", cred.TOTPIssuer)
}
fmt.Println()
fmt.Println("Scan this QR code with your authenticator app:")
fmt.Println()

if err := cred.DisplayQRCode(os.Stdout); err != nil {
return fmt.Errorf("failed to display QR code: %w", err)
}

fmt.Println()
fmt.Println("⚠️ Keep this QR code private - it contains your TOTP secret!")

return nil
}

// exportTOTPQRFile exports the TOTP QR code to a PNG file
func exportTOTPQRFile(cred *vault.Credential, service string, filename string) error {
if !cred.HasTOTP() {
return fmt.Errorf("no TOTP configured for credential: %s", service)
}

// Default size of 256x256 pixels
size := 256

if err := cred.ExportQRCode(filename, size); err != nil {
return fmt.Errorf("failed to export QR code: %w", err)
}

fmt.Printf("✅ TOTP QR code exported to: %s\n", filename)
fmt.Printf(" Service: %s\n", service)
if cred.TOTPIssuer != "" {
fmt.Printf(" Issuer: %s\n", cred.TOTPIssuer)
}
fmt.Println()
fmt.Println("⚠️ Keep this file private - it contains your TOTP secret!")

return nil
}

func outputNormalMode(cred *vault.Credential, vaultService *vault.VaultService, service string) error {
// Display credential details
fmt.Printf("📝 Service: %s\n", cred.Service)
Expand Down Expand Up @@ -167,6 +326,15 @@ func outputNormalMode(cred *vault.Credential, vaultService *vault.VaultService,
fmt.Printf("📋 Notes: %s\n", cred.Notes)
}

// Display TOTP status if configured
if cred.HasTOTP() {
issuer := cred.TOTPIssuer
if issuer == "" {
issuer = "configured"
}
fmt.Printf("🔐 TOTP: %s (use --totp to get code)\n", issuer)
}

// Display timestamps
fmt.Printf("📅 Created: %s\n", cred.CreatedAt.Format("2006-01-02 15:04:05"))
if !cred.UpdatedAt.Equal(cred.CreatedAt) {
Expand Down
Loading
Loading