Skip to content

Commit

Permalink
Add initial --use-builtin-age flag
Browse files Browse the repository at this point in the history
  • Loading branch information
twpayne committed Sep 9, 2021
1 parent 34ecd67 commit 98b92c2
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 12 deletions.
11 changes: 11 additions & 0 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Manage your dotfiles across multiple machines, securely.
* [`-o`, `--output` *filename*](#-o---output-filename)
* [`-R`, `--refresh-externals`](#-r---refresh-externals)
* [`-S`, `--source` *directory*](#-s---source-directory)
* [`--use-builtin-age` *value*](#--use-builtin-age-value)
* [`--use-builtin-git` *value*](#--use-builtin-git-value)
* [`-v`, `--verbose`](#-v---verbose)
* [`--version`](#--version)
Expand Down Expand Up @@ -219,6 +220,16 @@ Refresh externals cache. See `.chezmoiexternal.<format>`.

Use *directory* as the source directory.

### `--use-builtin-age` *value*

Use chezmoi's builtin [age encryption](https://age-encryption.org) instead of an
external `age` command. *value* can be `on`, `off`, `auto`, or any boolean-like
value recognized by `parseBool`. The default is `auto` which will only use the
builtin age if `age.command` cannot be found in `$PATH`.

The builtin `age` command does not support passphrases, symmetric encryption, or
the use of SSH keys.

### `--use-builtin-git` *value*

Use chezmoi's builtin git instead of `git.command` for the `init` and `update`
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/twpayne/chezmoi/v2
go 1.17

require (
filippo.io/age v1.0.0
github.com/Masterminds/sprig/v3 v3.2.2
github.com/ProtonMail/go-crypto v0.0.0-20210707164159-52430bf6b52c // indirect
github.com/alecthomas/chroma v0.9.2 // indirect
Expand Down Expand Up @@ -49,7 +50,7 @@ require (
go.uber.org/multierr v1.7.0
golang.org/x/net v0.0.0-20210825183410-e898025ed96a // indirect
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf
golang.org/x/sys v0.0.0-20210903071746-97244b99971b
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/yaml.v2 v2.4.0
Expand Down
7 changes: 5 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/age v1.0.0 h1:V6q14n0mqYU3qKFkZ6oOaF9oXneOviS3ubXsSVBRSzc=
filippo.io/age v1.0.0/go.mod h1:PaX+Si/Sd5G8LgfCwldsSba3H1DDQZhIhFGkhbHaBq8=
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
Expand Down Expand Up @@ -614,8 +617,8 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf h1:2ucpDCmfkl8Bd/FsLtiD653Wf96cW37s+iGx93zsu4k=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210903071746-97244b99971b h1:3Dq0eVHn0uaQJmPO+/aYPI/fRMqdrVDbu7MQcku54gg=
golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
Expand Down
143 changes: 139 additions & 4 deletions internal/chezmoi/ageencryption.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package chezmoi

// FIXME add builtin support for --passphrase
// FIXME add builtin support for --symmetric
// FIXME add builtin support for SSH keys if recommended

import (
"bytes"
"io"
"os"
"os/exec"

"filippo.io/age"
"filippo.io/age/armor"
"github.com/rs/zerolog/log"

"github.com/twpayne/chezmoi/v2/internal/chezmoilog"
Expand All @@ -13,10 +20,12 @@ import (
// An AgeEncryption uses age for encryption and decryption. See
// https://age-encryption.org.
type AgeEncryption struct {
UseBuiltin bool
BaseSystem System
Command string
Args []string
Identity string
Identities []string
Identity AbsPath
Identities []AbsPath
Passphrase bool
Recipient string
Recipients []string
Expand All @@ -28,6 +37,10 @@ type AgeEncryption struct {

// Decrypt implements Encyrption.Decrypt.
func (e *AgeEncryption) Decrypt(ciphertext []byte) ([]byte, error) {
if e.UseBuiltin {
return e.builtinDecrypt(ciphertext)
}

//nolint:gosec
cmd := exec.Command(e.Command, append(e.decryptArgs(), e.Args...)...)
cmd.Stdin = bytes.NewReader(ciphertext)
Expand All @@ -41,6 +54,14 @@ func (e *AgeEncryption) Decrypt(ciphertext []byte) ([]byte, error) {

// DecryptToFile implements Encryption.DecryptToFile.
func (e *AgeEncryption) DecryptToFile(plaintextAbsPath AbsPath, ciphertext []byte) error {
if e.UseBuiltin {
plaintext, err := e.builtinDecrypt(ciphertext)
if err != nil {
return err
}
return e.BaseSystem.WriteFile(plaintextAbsPath, plaintext, 0o644) // FIXME encrypted executables
}

//nolint:gosec
cmd := exec.Command(e.Command, append(append(e.decryptArgs(), "--output", string(plaintextAbsPath)), e.Args...)...)
cmd.Stdin = bytes.NewReader(ciphertext)
Expand All @@ -50,6 +71,10 @@ func (e *AgeEncryption) DecryptToFile(plaintextAbsPath AbsPath, ciphertext []byt

// Encrypt implements Encryption.Encrypt.
func (e *AgeEncryption) Encrypt(plaintext []byte) ([]byte, error) {
if e.UseBuiltin {
return e.builtinEncrypt(plaintext)
}

//nolint:gosec
cmd := exec.Command(e.Command, append(e.encryptArgs(), e.Args...)...)
cmd.Stdin = bytes.NewReader(plaintext)
Expand All @@ -63,6 +88,14 @@ func (e *AgeEncryption) Encrypt(plaintext []byte) ([]byte, error) {

// EncryptFile implements Encryption.EncryptFile.
func (e *AgeEncryption) EncryptFile(plaintextAbsPath AbsPath) ([]byte, error) {
if e.UseBuiltin {
plaintext, err := e.BaseSystem.ReadFile(plaintextAbsPath)
if err != nil {
return nil, err
}
return e.builtinEncrypt(plaintext)
}

//nolint:gosec
cmd := exec.Command(e.Command, append(append(e.encryptArgs(), e.Args...), string(plaintextAbsPath))...)
cmd.Stderr = os.Stderr
Expand All @@ -74,6 +107,90 @@ func (e *AgeEncryption) EncryptedSuffix() string {
return e.Suffix
}

func (e *AgeEncryption) builtinDecrypt(ciphertext []byte) ([]byte, error) {
identities, err := e.builtinIdentities()
if err != nil {
return nil, err
}
r, err := age.Decrypt(armor.NewReader(bytes.NewReader(ciphertext)), identities...)
if err != nil {
return nil, err
}
w := &bytes.Buffer{}
if _, err = io.Copy(w, r); err != nil {
return nil, err
}
return w.Bytes(), err
}

func (e *AgeEncryption) builtinEncrypt(plaintext []byte) ([]byte, error) {
recipients, err := e.builtinRecipients()
if err != nil {
return nil, err
}
output := &bytes.Buffer{}
armorWriter := armor.NewWriter(output)
writer, err := age.Encrypt(armorWriter, recipients...)
if err != nil {
return nil, err
}
if _, err := io.Copy(writer, bytes.NewReader(plaintext)); err != nil {
return nil, err
}
if err := writer.Close(); err != nil {
return nil, err
}
if err := armorWriter.Close(); err != nil {
return nil, err
}
return output.Bytes(), nil
}

func (e *AgeEncryption) builtinIdentities() ([]age.Identity, error) {
var identities []age.Identity
if e.Identity != "" {
parsedIdentities, err := parseIdentityFile(e.Identity)
if err != nil {
return nil, err
}
identities = append(identities, parsedIdentities...)
}
for _, identityAbsPath := range e.Identities {
parsedIdentities, err := parseIdentityFile(identityAbsPath)
if err != nil {
return nil, err
}
identities = append(identities, parsedIdentities...)
}
return identities, nil
}

func (e *AgeEncryption) builtinRecipients() ([]age.Recipient, error) {
recipients := make([]age.Recipient, 0, 1+len(e.Recipients))
if e.Recipient != "" {
parsedRecipient, err := age.ParseX25519Recipient(e.Recipient)
if err != nil {
return nil, err
}
recipients = append(recipients, parsedRecipient)
}
for _, recipient := range e.Recipients {
parsedRecipient, err := age.ParseX25519Recipient(recipient)
if err != nil {
return nil, err
}
recipients = append(recipients, parsedRecipient)
}
for _, recipientsFile := range e.RecipientsFiles {
parsedRecipients, err := parseRecipientsFile(recipientsFile)
if err != nil {
return nil, err
}
recipients = append(recipients, parsedRecipients...)
}
return recipients, nil
}

// decryptArgs returns the arguments for decryption.
func (e *AgeEncryption) decryptArgs() []string {
var args []string
Expand Down Expand Up @@ -116,10 +233,28 @@ func (e *AgeEncryption) encryptArgs() []string {
func (e *AgeEncryption) identityArgs() []string {
args := make([]string, 0, 2+2*len(e.Identities))
if e.Identity != "" {
args = append(args, "--identity", e.Identity)
args = append(args, "--identity", string(e.Identity))
}
for _, identity := range e.Identities {
args = append(args, "--identity", identity)
args = append(args, "--identity", string(identity))
}
return args
}

func parseIdentityFile(identityFile AbsPath) ([]age.Identity, error) {
file, err := os.Open(string(identityFile))
if err != nil {
return nil, err
}
defer file.Close()
return age.ParseIdentities(file)
}

func parseRecipientsFile(recipientsFile AbsPath) ([]age.Recipient, error) {
file, err := os.Open(string(recipientsFile))
if err != nil {
return nil, err
}
defer file.Close()
return age.ParseRecipients(file)
}
2 changes: 1 addition & 1 deletion internal/chezmoi/ageencryption_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestAgeEncryption(t *testing.T) {

ageEncryption := &AgeEncryption{
Command: command,
Identity: privateKeyFile,
Identity: AbsPath(privateKeyFile),
Recipient: publicKey,
}

Expand Down
21 changes: 21 additions & 0 deletions internal/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ type Config struct {
SourceDirAbsPath chezmoi.AbsPath `mapstructure:"sourceDir"`
Template templateConfig `mapstructure:"template"`
Umask fs.FileMode `mapstructure:"umask"`
UseBuiltinAge autoBool `mapstructure:"useBuiltinAge"`
UseBuiltinGit autoBool `mapstructure:"useBuiltinGit"`

// Global configuration, not settable in the config file.
Expand Down Expand Up @@ -221,6 +222,9 @@ func newConfig(options ...configOption) (*Config, error) {
Options: chezmoi.DefaultTemplateOptions,
},
Umask: chezmoi.Umask,
UseBuiltinAge: autoBool{
auto: true,
},
UseBuiltinGit: autoBool{
auto: true,
},
Expand Down Expand Up @@ -1062,6 +1066,7 @@ func (c *Config) newRootCmd() (*cobra.Command, error) {
persistentFlags.BoolVar(&c.Safe, "safe", c.Safe, "Safely replace files and symlinks")
persistentFlags.VarP(&c.SourceDirAbsPath, "source", "S", "Set source directory")
persistentFlags.Var(&c.Mode, "mode", "Mode")
persistentFlags.Var(&c.UseBuiltinAge, "use-builtin-age", "Use builtin age")
persistentFlags.Var(&c.UseBuiltinGit, "use-builtin-git", "Use builtin git")
for _, key := range []string{
"color",
Expand Down Expand Up @@ -1377,6 +1382,15 @@ func (c *Config) persistentPreRunRootE(cmd *cobra.Command, args []string) error

switch c.Encryption {
case "age":
// Only use builtin age encryption if age encryption is explicitly
// specified. Otherwise, chezmoi would fall back to using age encryption
// (rather than no encryption) if age is not in $PATH, which leads to
// error messages from the builtin age instead of error messages about
// encryption not being configured.
c.Age.UseBuiltin, err = c.UseBuiltinAge.Value(c.useBuiltinAgeAutoFunc)
if err != nil {
return err
}
c.encryption = &c.Age
case "gpg":
c.encryption = &c.GPG
Expand Down Expand Up @@ -1610,6 +1624,13 @@ func (c *Config) targetRelPathsBySourcePath(sourceState *chezmoi.SourceState, ar
return targetRelPaths, nil
}

func (c *Config) useBuiltinAgeAutoFunc() (bool, error) {
if _, err := exec.LookPath(c.Age.Command); err == nil {
return false, nil
}
return true, nil
}

func (c *Config) useBuiltinGitAutoFunc() (bool, error) {
if _, err := exec.LookPath(c.Git.Command); err == nil {
return false, nil
Expand Down
16 changes: 16 additions & 0 deletions internal/cmd/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func TestScript(t *testing.T) {
"mkgpgconfig": cmdMkGPGConfig,
"mkhomedir": cmdMkHomeDir,
"mksourcedir": cmdMkSourceDir,
"prependline": cmdPrependLine,
"readlink": cmdReadLink,
"removeline": cmdRemoveLine,
"rmfinalnewline": cmdRmFinalNewline,
Expand Down Expand Up @@ -415,6 +416,21 @@ func cmdMkSourceDir(ts *testscript.TestScript, neg bool, args []string) {
}
}

// cmdPrependLine prepends lines to a file.
func cmdPrependLine(ts *testscript.TestScript, neg bool, args []string) {
if neg {
ts.Fatalf("unsupported: ! prependline")
}
if len(args) != 2 {
ts.Fatalf("usage: prependline file line")
}
filename := ts.MkAbs(args[0])
data, err := os.ReadFile(filename)
ts.Check(err)
data = append(append([]byte(args[1]), '\n'), data...)
ts.Check(os.WriteFile(filename, data, 0o666))
}

// cmdReadLink reads a symlink and verifies that its target is as expected.
func cmdReadLink(ts *testscript.TestScript, neg bool, args []string) {
if len(args) != 2 {
Expand Down
Loading

0 comments on commit 98b92c2

Please sign in to comment.