From ba09e532ea044565ed29c2ebdf3fb5520d6a2441 Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Wed, 24 Jan 2024 02:44:24 +0100 Subject: [PATCH] feat: Support rage as an alternative age encryption command --- .github/workflows/main.yml | 26 +++- .../docs/developer/developing-locally.md | 4 +- .../docs/user-guide/encryption/rage.md | 13 ++ assets/chezmoi.io/mkdocs.yml | 1 + internal/chezmoi/ageencryption_test.go | 120 ++++++++++-------- internal/chezmoitest/chezmoitest.go | 4 +- internal/cmd/main_test.go | 2 +- 7 files changed, 109 insertions(+), 61 deletions(-) create mode 100644 assets/chezmoi.io/docs/user-guide/encryption/rage.md diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3e1acf74cbb0..7cc2a8f8b8bc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,12 +13,13 @@ env: AGE_VERSION: 1.1.1 CHOCOLATEY_VERSION: 2.2.2 EDITORCONFIG_CHECKER_VERSION: 2.7.2 + FIND_TYPOS_VERSION: 0.0.3 GO_VERSION: 1.21.6 GOFUMPT_VERSION: 0.5.0 GOLANGCI_LINT_VERSION: 1.55.2 GOLINES_VERSION: 0.11.0 GOVERSIONINFO_VERSION: 1.4.0 - FIND_TYPOS_VERSION: 0.0.3 + RAGE_VERSION: 0.9.2 jobs: changes: runs-on: ubuntu-20.04 @@ -102,10 +103,13 @@ jobs: go run . --version - name: install-age run: | - cd "$(mktemp -d)" - curl -fsSL "https://github.com/FiloSottile/age/releases/download/v${AGE_VERSION}/age-v${AGE_VERSION}-darwin-amd64.tar.gz" | tar xzf - - sudo install -m 755 age/age /usr/local/bin - sudo install -m 755 age/age-keygen /usr/local/bin + brew install age + age --version + - name: install-rage + run: | + brew tap str4d.xyz/rage https://str4d.xyz/rage + brew install rage + rage --version - name: install-keepassxc run: | brew install keepassxc @@ -140,6 +144,12 @@ jobs: curl -fsSL "https://github.com/FiloSottile/age/releases/download/v${AGE_VERSION}/age-v${AGE_VERSION}-linux-amd64.tar.gz" | tar xzf - sudo install -m 755 age/age /usr/local/bin sudo install -m 755 age/age-keygen /usr/local/bin + - name: install-rage + run: | + cd "$(mktemp -d)" + curl -fsSL "https://github.com/str4d/rage/releases/download/v${RAGE_VERSION}/rage-v${RAGE_VERSION}-x86_64-linux.tar.gz" | tar xzf - + sudo install -m 755 rage/rage /usr/local/bin + sudo install -m 755 rage/rage-keygen /usr/local/bin - name: test env: CHEZMOI_GITHUB_TOKEN: ${{ secrets.CHEZMOI_GITHUB_TOKEN }} @@ -225,6 +235,12 @@ jobs: curl -fsSL "https://github.com/FiloSottile/age/releases/download/v${AGE_VERSION}/age-v${AGE_VERSION}-linux-amd64.tar.gz" | tar xzf - sudo install -m 755 age/age /usr/local/bin sudo install -m 755 age/age-keygen /usr/local/bin + - name: install-rage + run: | + cd "$(mktemp -d)" + curl -fsSL "https://github.com/str4d/rage/releases/download/v${RAGE_VERSION}/rage-v${RAGE_VERSION}-x86_64-linux.tar.gz" | tar xzf - + sudo install -m 755 rage/rage /usr/local/bin + sudo install -m 755 rage/rage-keygen /usr/local/bin - name: build run: | go build ./... diff --git a/assets/chezmoi.io/docs/developer/developing-locally.md b/assets/chezmoi.io/docs/developer/developing-locally.md index 1ac0b9858ba3..bc6c550f02ed 100644 --- a/assets/chezmoi.io/docs/developer/developing-locally.md +++ b/assets/chezmoi.io/docs/developer/developing-locally.md @@ -33,8 +33,8 @@ $ go test ./... ``` chezmoi's tests include integration tests with other software. If the other -software is not found in `$PATH` the tests will be skipped. Running the full -set of tests requires `age`, `base64`, `bash`, `gpg`, `perl`, `python3`, +software is not found in `$PATH` the tests will be skipped. Running the full set +of tests requires `age`, `base64`, `bash`, `gpg`, `perl`, `python3`, `rage`, `ruby`, `sed`, `sha256sum`, `unzip`, `xz`, `zip`, and `zstd`. Run chezmoi: diff --git a/assets/chezmoi.io/docs/user-guide/encryption/rage.md b/assets/chezmoi.io/docs/user-guide/encryption/rage.md new file mode 100644 index 000000000000..19faefa9422b --- /dev/null +++ b/assets/chezmoi.io/docs/user-guide/encryption/rage.md @@ -0,0 +1,13 @@ +# rage + +chezmoi supports encrypting files with [rage](https://str4d.xyz/rage). + +To use rage, set `age.command` to `rage` in your configuration file, for example: + +```toml title="~/.config/chezmoi/chezmoi.toml" +encryption = "age" +[age] + command = "rage" +``` + +Then, configure chezmoi as you would for [age](age.md). \ No newline at end of file diff --git a/assets/chezmoi.io/mkdocs.yml b/assets/chezmoi.io/mkdocs.yml index 04449f8050a9..8c5ae2a64d1d 100644 --- a/assets/chezmoi.io/mkdocs.yml +++ b/assets/chezmoi.io/mkdocs.yml @@ -80,6 +80,7 @@ nav: - user-guide/encryption/index.md - age: user-guide/encryption/age.md - gpg: user-guide/encryption/gpg.md + - rage: user-guide/encryption/rage.md - Machines: - General: user-guide/machines/general.md - Linux: user-guide/machines/linux.md diff --git a/internal/chezmoi/ageencryption_test.go b/internal/chezmoi/ageencryption_test.go index 445dc278f98e..b26534449091 100644 --- a/internal/chezmoi/ageencryption_test.go +++ b/internal/chezmoi/ageencryption_test.go @@ -11,66 +11,75 @@ import ( "github.com/twpayne/chezmoi/v2/internal/chezmoitest" ) -func TestAgeEncryption(t *testing.T) { - command := lookPathOrSkip(t, "age") - - identityFile := filepath.Join(t.TempDir(), "chezmoi-test-age-key.txt") - recipient, err := chezmoitest.AgeGenerateKey(identityFile) - assert.NoError(t, err) +var ageCommands = []string{ + "age", + "rage", +} - testEncryption(t, &AgeEncryption{ - Command: command, - Identity: NewAbsPath(identityFile), - Recipient: recipient, +func TestAgeEncryption(t *testing.T) { + forEachAgeCommand(t, func(t *testing.T, command string) { + t.Helper() + identityFile := filepath.Join(t.TempDir(), "chezmoi-test-age-key.txt") + recipient, err := chezmoitest.AgeGenerateKey(command, identityFile) + assert.NoError(t, err) + + testEncryption(t, &AgeEncryption{ + Command: command, + Identity: NewAbsPath(identityFile), + Recipient: recipient, + }) }) } func TestAgeMultipleIdentitiesAndMultipleRecipients(t *testing.T) { - command := lookPathOrSkip(t, "age") - - tempDir := t.TempDir() - identityFile1 := filepath.Join(tempDir, "chezmoi-test-age-key1.txt") - recipient1, err := chezmoitest.AgeGenerateKey(identityFile1) - assert.NoError(t, err) - identityFile2 := filepath.Join(tempDir, "chezmoi-test-age-key2.txt") - recipient2, err := chezmoitest.AgeGenerateKey(identityFile2) - assert.NoError(t, err) - - testEncryption(t, &AgeEncryption{ - Command: command, - Identities: []AbsPath{ - NewAbsPath(identityFile1), - NewAbsPath(identityFile2), - }, - Recipients: []string{ - recipient1, - recipient2, - }, + forEachAgeCommand(t, func(t *testing.T, command string) { + t.Helper() + tempDir := t.TempDir() + identityFile1 := filepath.Join(tempDir, "chezmoi-test-age-key1.txt") + recipient1, err := chezmoitest.AgeGenerateKey(command, identityFile1) + assert.NoError(t, err) + identityFile2 := filepath.Join(tempDir, "chezmoi-test-age-key2.txt") + recipient2, err := chezmoitest.AgeGenerateKey(command, identityFile2) + assert.NoError(t, err) + + testEncryption(t, &AgeEncryption{ + Command: command, + Identities: []AbsPath{ + NewAbsPath(identityFile1), + NewAbsPath(identityFile2), + }, + Recipients: []string{ + recipient1, + recipient2, + }, + }) }) } func TestAgeRecipientsFile(t *testing.T) { - command := lookPathOrSkip(t, "age") - - tempDir := t.TempDir() - identityFile := filepath.Join(tempDir, "chezmoi-test-age-key.txt") - recipient, err := chezmoitest.AgeGenerateKey(identityFile) - assert.NoError(t, err) - recipientsFile := filepath.Join(t.TempDir(), "chezmoi-test-age-recipients.txt") - assert.NoError(t, os.WriteFile(recipientsFile, []byte(recipient), 0o666)) - - testEncryption(t, &AgeEncryption{ - Command: command, - Identity: NewAbsPath(identityFile), - RecipientsFile: NewAbsPath(recipientsFile), - }) - - testEncryption(t, &AgeEncryption{ - Command: command, - Identity: NewAbsPath(identityFile), - RecipientsFiles: []AbsPath{ - NewAbsPath(recipientsFile), - }, + t.Helper() + forEachAgeCommand(t, func(t *testing.T, command string) { + t.Helper() + tempDir := t.TempDir() + identityFile := filepath.Join(tempDir, "chezmoi-test-age-key.txt") + recipient, err := chezmoitest.AgeGenerateKey(command, identityFile) + assert.NoError(t, err) + recipientsFile := filepath.Join(t.TempDir(), "chezmoi-test-age-recipients.txt") + assert.NoError(t, os.WriteFile(recipientsFile, []byte(recipient), 0o666)) + + testEncryption(t, &AgeEncryption{ + Command: command, + Identity: NewAbsPath(identityFile), + RecipientsFile: NewAbsPath(recipientsFile), + }) + + testEncryption(t, &AgeEncryption{ + Command: command, + Identity: NewAbsPath(identityFile), + RecipientsFiles: []AbsPath{ + NewAbsPath(recipientsFile), + }, + }) }) } @@ -129,3 +138,12 @@ func builtinAgeGenerateKey(t *testing.T) (*age.X25519Recipient, AbsPath) { assert.NoError(t, os.WriteFile(identityFile, []byte(identity.String()), 0o600)) return identity.Recipient(), NewAbsPath(identityFile) } + +func forEachAgeCommand(t *testing.T, f func(*testing.T, string)) { + t.Helper() + for _, command := range ageCommands { + t.Run(command, func(t *testing.T) { + f(t, lookPathOrSkip(t, command)) + }) + } +} diff --git a/internal/chezmoitest/chezmoitest.go b/internal/chezmoitest/chezmoitest.go index c3071a51b1ff..e84fecce29bf 100644 --- a/internal/chezmoitest/chezmoitest.go +++ b/internal/chezmoitest/chezmoitest.go @@ -23,8 +23,8 @@ var ageRecipientRx = regexp.MustCompile(`(?m)^Public key: ([0-9a-z]+)\s*$`) // AgeGenerateKey generates an identity in identityFile and returns the // recipient. -func AgeGenerateKey(identityFile string) (string, error) { - cmd := exec.Command("age-keygen", "--output", identityFile) +func AgeGenerateKey(command, identityFile string) (string, error) { + cmd := exec.Command(command+"-keygen", "--output", identityFile) //nolint:gosec output, err := chezmoilog.LogCmdCombinedOutput(cmd) if err != nil { return "", err diff --git a/internal/cmd/main_test.go b/internal/cmd/main_test.go index cab3dbabd30b..08cbbd860c56 100644 --- a/internal/cmd/main_test.go +++ b/internal/cmd/main_test.go @@ -306,7 +306,7 @@ func cmdMkAgeConfig(ts *testscript.TestScript, neg bool, args []string) { homeDir := ts.Getenv("HOME") ts.Check(os.MkdirAll(homeDir, fs.ModePerm)) identityFile := filepath.Join(homeDir, "key.txt") - recipient, err := chezmoitest.AgeGenerateKey(ts.MkAbs(identityFile)) + recipient, err := chezmoitest.AgeGenerateKey("age", ts.MkAbs(identityFile)) ts.Check(err) configFile := filepath.Join(homeDir, ".config", "chezmoi", "chezmoi.toml") ts.Check(os.MkdirAll(filepath.Dir(configFile), fs.ModePerm))