Skip to content

Commit

Permalink
fix: Improve robustness of keepassxc-cli integration in open mode
Browse files Browse the repository at this point in the history
  • Loading branch information
twpayne committed Jan 13, 2024
1 parent 49791f9 commit 3436563
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 27 deletions.
35 changes: 26 additions & 9 deletions internal/cmd/keepassxctemplatefuncs.go
Expand Up @@ -8,7 +8,9 @@ import (
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"time"

"github.com/Netflix/go-expect"
"github.com/coreos/go-semver/semver"
Expand Down Expand Up @@ -72,7 +74,7 @@ func (c *Config) keepassxcAttachmentTemplateFunc(entry, name string) string {
if err != nil {
panic(err)
}
tempFilename := tempDir.JoinString(name).String()
tempFilename := tempDir.JoinString("attachment-" + strconv.FormatInt(time.Now().UnixNano(), 10)).String()
if _, err := c.keepassxcOutputOpen("attachment-export", "--quiet", entry, name, tempFilename); err != nil {
panic(err)
}
Expand Down Expand Up @@ -246,8 +248,15 @@ func (c *Config) keepassxcOutputOpen(command string, args ...string) ([]byte, er
c.Keepassxc.prompt = keepassxcPromptRx.FindString(output)
}

// Send the command.
line := strings.Join(append([]string{command}, args...), " ")
// Build the command line. Strings with spaces and other non-word characters
// need to be quoted.
quotedArgs := make([]string, 0, len(args))
for _, arg := range args {
quotedArgs = append(quotedArgs, maybeQuote(arg))
}
line := strings.Join(append([]string{command}, quotedArgs...), " ")

// Send the line.
if _, err := c.Keepassxc.console.SendLine(line); err != nil {
return nil, err
}
Expand All @@ -257,14 +266,22 @@ func (c *Config) keepassxcOutputOpen(command string, args ...string) ([]byte, er
if err != nil {
return nil, err
}
outputLines := strings.Split(output, "\r\n")

// Trim the echoed command from the output, which is the first line.
// keepassxc-cli version 2.7.6 on macOS inserts " \b" somewhere in the
// echoed command. Rather than trying to match this, remove the first line
// entirely.
if len(outputLines) > 0 {
outputLines = outputLines[1:]
}

// Trim the command from the output.
output = strings.TrimPrefix(output, line+"\r\n")

// Trim the prompt from the output.
output = strings.TrimSuffix(output, c.Keepassxc.prompt)
// Trim the prompt from the output, which is the last line.
if len(outputLines) > 0 {
outputLines = outputLines[:len(outputLines)-1]
}

return []byte(output), nil
return []byte(strings.Join(outputLines, "\r\n")), nil
}

// keepassxcParseOutput parses a list of key-value pairs.
Expand Down
30 changes: 21 additions & 9 deletions internal/cmd/keepassxctemplatefuncs_test.go
Expand Up @@ -63,15 +63,16 @@ func TestKeepassxcTemplateFuncs(t *testing.T) {
assert.NoError(t, err)

tempDir := t.TempDir()
database := filepath.Join(tempDir, "Passwords.kdbx")
databasePassword := "test-database-password"
entryName := "test-entry"
entryUsername := "test-username"
entryPassword := "test-password"
attachmentName := "test-attachment-name"
attachmentData := "test-attachment-data"
importFile := filepath.Join(tempDir, "import-file")
assert.NoError(t, os.WriteFile(importFile, []byte(attachmentData), 0o666))

// The following test data includes spaces and slashes to test quoting.
database := filepath.Join(tempDir, "KeePassXC Passwords.kdbx")
databasePassword := "test / database / password"
groupName := "test group"
entryName := groupName + "/test entry"
entryUsername := "test / username"
entryPassword := "test / password"
attachmentName := "test / attachment name"
attachmentData := "test / attachment data"

// Create a KeePassXC database.
dbCreateCmd := exec.Command(command, "db-create", "--set-password", database)
Expand All @@ -83,6 +84,15 @@ func TestKeepassxcTemplateFuncs(t *testing.T) {
dbCreateCmd.Stderr = os.Stderr
assert.NoError(t, dbCreateCmd.Run())

// Create a group in the database.
mkdirCmd := exec.Command(command, "mkdir", database, groupName)
mkdirCmd.Stdin = strings.NewReader(chezmoitest.JoinLines(
databasePassword,
))
mkdirCmd.Stdout = os.Stdout
mkdirCmd.Stderr = os.Stderr
assert.NoError(t, mkdirCmd.Run())

// Create an entry in the database.
addCmd := exec.Command(command, "add", database, entryName, "--username", entryUsername, "--password-prompt")
addCmd.Stdin = strings.NewReader(chezmoitest.JoinLines(
Expand All @@ -94,6 +104,8 @@ func TestKeepassxcTemplateFuncs(t *testing.T) {
assert.NoError(t, addCmd.Run())

// Import an attachment to the entry in the database.
importFile := filepath.Join(tempDir, "import file")
assert.NoError(t, os.WriteFile(importFile, []byte(attachmentData), 0o666))
attachmentImportCmd := exec.Command(command, "attachment-import", database, entryName, attachmentName, importFile)
attachmentImportCmd.Stdin = strings.NewReader(chezmoitest.JoinLines(
databasePassword,
Expand Down
13 changes: 8 additions & 5 deletions internal/cmd/templatefuncs.go
Expand Up @@ -675,11 +675,7 @@ func writeIniMap(w io.Writer, data map[string]any, sectionPrefix string) error {
}
subsections = append(subsections, subsection)
case string:
if needsQuote(value) {
fmt.Fprintf(w, "%s = %q\n", key, value)
} else {
fmt.Fprintf(w, "%s = %s\n", key, value)
}
fmt.Fprintf(w, "%s = %s\n", key, maybeQuote(value))
default:
return fmt.Errorf("%s%s: %T: unsupported type", sectionPrefix, key, value)
}
Expand All @@ -698,6 +694,13 @@ func writeIniMap(w io.Writer, data map[string]any, sectionPrefix string) error {
return nil
}

func maybeQuote(s string) string {
if needsQuote(s) {
return strconv.Quote(s)
}
return s
}

func needsQuote(s string) bool {
if s == "" {
return true
Expand Down
10 changes: 6 additions & 4 deletions internal/cmd/templatefuncs_test.go
Expand Up @@ -640,15 +640,17 @@ func TestToIniTemplateFunc(t *testing.T) {
}{
{
data: map[string]any{
"bool": true,
"float": 1.0,
"int": 1,
"string": "string",
"bool": true,
"float": 1.0,
"int": 1,
"quotedString": "\"",
"string": "string",
},
expected: chezmoitest.JoinLines(
`bool = true`,
`float = 1.000000`,
`int = 1`,
`quotedString = "\""`,
`string = string`,
),
},
Expand Down

0 comments on commit 3436563

Please sign in to comment.