Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 [email protected] -g --gen-length 32

# Add with all metadata fields
pass-cli add github -u [email protected] -c "Version Control" --url "https://github.com" --notes "Work account"`,
pass-cli add github -u [email protected] -c "Version Control" --url "https://github.com" --notes "Work account"

# Add with TOTP/2FA support
pass-cli add github -u [email protected] --totp-uri "otpauth://totp/GitHub:user?secret=JBSWY3DPEHPK3PXP&issuer=GitHub"

# Add with interactive TOTP prompt
pass-cli add github -u [email protected] --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