From 700164fc770fe3f8236ad699ee7eb9a761583ee0 Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Sun, 25 Aug 2024 20:08:59 +0000 Subject: [PATCH] feat: Handle KeePassXC prompts for YubiKeys --- .../user-guide/password-managers/keepassxc.md | 8 +- internal/cmd/keepassxctemplatefuncs.go | 73 +++++++++++-------- 2 files changed, 48 insertions(+), 33 deletions(-) diff --git a/assets/chezmoi.io/docs/user-guide/password-managers/keepassxc.md b/assets/chezmoi.io/docs/user-guide/password-managers/keepassxc.md index 2dc0ecb5932..b6d84769038 100644 --- a/assets/chezmoi.io/docs/user-guide/password-managers/keepassxc.md +++ b/assets/chezmoi.io/docs/user-guide/password-managers/keepassxc.md @@ -33,6 +33,7 @@ If your database is not password protected, add `--no-password` to ```toml title="~/.config/chezmoi/chezmoi.toml" [keepassxc] + database = "/home/user/Passwords.kdbx" args = ["--no-password"] prompt = false ``` @@ -40,11 +41,12 @@ If your database is not password protected, add `--no-password` to ## YubiKey support chezmoi includes an experimental mode to support using KeePassXC with YubiKeys. -Set `keepassxc.mode` to `open` and `keepassxc.args` to the arguments required to -set your YubiKey, for example: +Set `keepassxc.mode` to `open` and `keepassxc.openArgs` to the arguments +required to set your YubiKey, for example: ```toml title="~/.config/chezmoi/chezmoi.toml" [keepassxc] - args = ["--yubikey", "1:7370001"] + database = "/home/user/Passwords.kdbx" + openArgs = ["--no-password", "--yubikey", "2:7370001"] mode = "open" ``` diff --git a/internal/cmd/keepassxctemplatefuncs.go b/internal/cmd/keepassxctemplatefuncs.go index 067eac68fda..2d300a2116b 100644 --- a/internal/cmd/keepassxctemplatefuncs.go +++ b/internal/cmd/keepassxctemplatefuncs.go @@ -51,10 +51,13 @@ type keepassxcConfig struct { var ( keepassxcMinVersion = semver.Version{Major: 2, Minor: 7, Patch: 0} - keepassxcEnterPasswordToUnlockDatabaseRx = regexp.MustCompile(`^Enter password to unlock .*: `) - keepassxcAnyResponseRx = regexp.MustCompile(`(?m)\A.*\r\n`) - keepassxcPairRx = regexp.MustCompile(`^([A-Z]\w*):\s*(.*)$`) - keepassxcPromptRx = regexp.MustCompile(`^.*> `) + keepassxcEnterPasswordToUnlockDatabaseRx = regexp.MustCompile(`^Enter password to unlock .*: `) + keepassxcPleasePresentOrTouchYourYubiKeyToContinueRx = regexp.MustCompile( + "^Please present or touch your \\S+ to continue\\.\r\n", + ) + keepassxcAnyResponseRx = regexp.MustCompile(`(?m)\A.*\r\n`) + keepassxcPairRx = regexp.MustCompile(`^([A-Z]\w*):\s*(.*)$`) + keepassxcPromptRx = regexp.MustCompile(`^.*> `) ) func (c *Config) keepassxcAttachmentTemplateFunc(entry, name string) string { @@ -220,55 +223,65 @@ func (c *Config) keepassxcOutputOpen(command string, args ...string) ([]byte, er } if c.Keepassxc.Prompt { - // Expect the password prompt, e.g. "Enter password to unlock $HOME/Passwords.kdbx: ". - enterPasswordToUnlockPrompt, err := console.Expect( + // Expect the password or YubiKey response. + response, err := console.Expect( expect.Regexp(keepassxcEnterPasswordToUnlockDatabaseRx), + expect.Regexp(keepassxcPleasePresentOrTouchYourYubiKeyToContinueRx), expect.Regexp(keepassxcAnyResponseRx), ) if err != nil { return nil, err } - if !keepassxcEnterPasswordToUnlockDatabaseRx.MatchString(enterPasswordToUnlockPrompt) { - return nil, errors.New(strings.TrimSpace(enterPasswordToUnlockPrompt)) - } - // Read the password from the user, if necessary. - var password string - if c.Keepassxc.password != "" { - password = c.Keepassxc.password - } else { - password, err = c.readPassword(enterPasswordToUnlockPrompt) - if err != nil { - return nil, err + switch { + case keepassxcEnterPasswordToUnlockDatabaseRx.MatchString(response): + // Read the password from the user, if necessary. + var password string + if c.Keepassxc.password != "" { + password = c.Keepassxc.password + } else { + password, err = c.readPassword(response) + if err != nil { + return nil, err + } } - } - // Send the password. - if _, err := console.SendLine(password); err != nil { - return nil, err - } + // Send the password. + if _, err := console.SendLine(password); err != nil { + return nil, err + } - // Wait for the end of the password prompt. - if _, err := console.ExpectString("\r\n"); err != nil { - return nil, err + // Wait for the end of the password prompt. + if _, err := console.ExpectString("\r\n"); err != nil { + return nil, err + } + case keepassxcPleasePresentOrTouchYourYubiKeyToContinueRx.MatchString(response): + if _, err := console.ExpectString("\r\n"); err != nil { + return nil, err + } + if _, err := c.stderr.Write([]byte(strings.TrimSpace(response) + "\n")); err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("%q: unexpected response", response) } } - // Read the prompt, e.g "Passwords> ", so we can expect it later. - output, err := console.Expect( + // Read the response, e.g "Passwords> ", so we can expect it later. + response, err := console.Expect( expect.Regexp(keepassxcPromptRx), expect.Regexp(keepassxcAnyResponseRx), ) if err != nil { return nil, err } - if !keepassxcPromptRx.MatchString(output) { - return nil, errors.New(strings.TrimSpace(output)) + if !keepassxcPromptRx.MatchString(response) { + return nil, fmt.Errorf("%q: unexpected response", response) } c.Keepassxc.cmd = cmd c.Keepassxc.console = console - c.Keepassxc.prompt = keepassxcPromptRx.FindString(output) + c.Keepassxc.prompt = keepassxcPromptRx.FindString(response) } // Build the command line. Strings with spaces and other non-word characters