-
Notifications
You must be signed in to change notification settings - Fork 470
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
217 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# `age` | ||
|
||
Interact with age's passphrase-based encryption. | ||
|
||
!!! hint | ||
|
||
To get a full list of subcommands run: | ||
|
||
```console | ||
$ chezmoi age help | ||
``` | ||
|
||
!!! example | ||
|
||
```console | ||
$ chezmoi age encrypt --passphrase plaintext.txt > ciphertext.txt | ||
$ chezmoi age decrypt --passphrase ciphertext.txt > decrypted-ciphertext.txt | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
package cmd | ||
|
||
import ( | ||
"bytes" | ||
"errors" | ||
"io" | ||
|
||
"filippo.io/age" | ||
"filippo.io/age/armor" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
type ageDecryptCmdConfig struct { | ||
passphrase bool | ||
} | ||
|
||
type ageEncryptCmdConfig struct { | ||
passphrase bool | ||
} | ||
|
||
type ageCmdConfig struct { | ||
decrypt ageDecryptCmdConfig | ||
encrypt ageEncryptCmdConfig | ||
} | ||
|
||
func (c *Config) newAgeCmd() *cobra.Command { | ||
ageCmd := &cobra.Command{ | ||
Use: "age", | ||
Args: cobra.NoArgs, | ||
Short: "Interact with age", | ||
} | ||
|
||
ageDecryptCmd := &cobra.Command{ | ||
Use: "decrypt [file...]", | ||
Short: "Decrypt file or standard input", | ||
RunE: c.runAgeDecryptCmd, | ||
} | ||
ageDecryptFlags := ageDecryptCmd.Flags() | ||
ageDecryptFlags.BoolVarP( | ||
&c.age.decrypt.passphrase, | ||
"passphrase", | ||
"p", | ||
c.age.decrypt.passphrase, | ||
"Decrypt with a passphrase", | ||
) | ||
ageCmd.AddCommand(ageDecryptCmd) | ||
|
||
ageEncryptCmd := &cobra.Command{ | ||
Use: "encrypt [file...]", | ||
Short: "Encrypt file or standard input", | ||
RunE: c.runAgeEncryptCmd, | ||
} | ||
ageEncryptFlags := ageEncryptCmd.Flags() | ||
ageEncryptFlags.BoolVarP( | ||
&c.age.encrypt.passphrase, | ||
"passphrase", | ||
"p", | ||
c.age.encrypt.passphrase, | ||
"Encrypt with a passphrase", | ||
) | ||
ageCmd.AddCommand(ageEncryptCmd) | ||
|
||
return ageCmd | ||
} | ||
|
||
func (c *Config) runAgeDecryptCmd(cmd *cobra.Command, args []string) error { | ||
if !c.age.decrypt.passphrase { | ||
return errors.New("only passphrase encryption is supported") | ||
} | ||
decrypt := func(ciphertext []byte) ([]byte, error) { | ||
ciphertextReader := bytes.NewReader(ciphertext) | ||
armoredCiphertextReader := armor.NewReader(ciphertextReader) | ||
identity := &LazyScryptIdentity{ | ||
Passphrase: func() (string, error) { | ||
return c.readPassword("Enter passphrase: ") | ||
}, | ||
} | ||
plaintextReader, err := age.Decrypt(armoredCiphertextReader, identity) | ||
if err != nil { | ||
return nil, err | ||
} | ||
plaintextBuffer := &bytes.Buffer{} | ||
if _, err := io.Copy(plaintextBuffer, plaintextReader); err != nil { | ||
return nil, err | ||
} | ||
return plaintextBuffer.Bytes(), nil | ||
} | ||
return c.filterInput(args, decrypt) | ||
} | ||
|
||
func (c *Config) runAgeEncryptCmd(cmd *cobra.Command, args []string) error { | ||
if !c.age.encrypt.passphrase { | ||
return errors.New("only passphrase encryption is supported") | ||
} | ||
passphrase, err := c.readPassword("Enter passphrase: ") | ||
if err != nil { | ||
return err | ||
} | ||
confirmPassphrase, err := c.readPassword("Confirm passphrase: ") | ||
if err != nil { | ||
return err | ||
} | ||
if passphrase != confirmPassphrase { | ||
return errors.New("passphrases didn't match") | ||
} | ||
recipient, err := age.NewScryptRecipient(passphrase) | ||
if err != nil { | ||
return err | ||
} | ||
encrypt := func(plaintext []byte) ([]byte, error) { | ||
ciphertextBuffer := &bytes.Buffer{} | ||
armoredCiphertextWriter := armor.NewWriter(ciphertextBuffer) | ||
ciphertextWriteCloser, err := age.Encrypt(armoredCiphertextWriter, recipient) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if _, err := io.Copy(ciphertextWriteCloser, bytes.NewReader(plaintext)); err != nil { | ||
return nil, err | ||
} | ||
if err := ciphertextWriteCloser.Close(); err != nil { | ||
return nil, err | ||
} | ||
if err := armoredCiphertextWriter.Close(); err != nil { | ||
return nil, err | ||
} | ||
return ciphertextBuffer.Bytes(), nil | ||
} | ||
return c.filterInput(args, encrypt) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package cmd | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
|
||
"filippo.io/age" | ||
) | ||
|
||
// This is copied from https://github.com/FiloSottile/age/blob/6ad4560f4afc3fe46b6cda0bc568e50b89a22e4c/cmd/age/encrypted_keys.go#L15-L51 | ||
|
||
// LazyScryptIdentity is an age.Identity that requests a passphrase only if it | ||
// encounters an scrypt stanza. After obtaining a passphrase, it delegates to | ||
// ScryptIdentity. | ||
type LazyScryptIdentity struct { | ||
Passphrase func() (string, error) | ||
} | ||
|
||
var _ age.Identity = &LazyScryptIdentity{} | ||
|
||
func (i *LazyScryptIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) { | ||
for _, s := range stanzas { | ||
if s.Type == "scrypt" && len(stanzas) != 1 { | ||
return nil, errors.New("an scrypt recipient must be the only one") | ||
} | ||
} | ||
if len(stanzas) != 1 || stanzas[0].Type != "scrypt" { | ||
return nil, age.ErrIncorrectIdentity | ||
} | ||
pass, err := i.Passphrase() | ||
if err != nil { | ||
return nil, fmt.Errorf("could not read passphrase: %w", err) | ||
} | ||
ii, err := age.NewScryptIdentity(pass) | ||
if err != nil { | ||
return nil, err | ||
} | ||
fileKey, err = ii.Unwrap(stanzas) | ||
if errors.Is(err, age.ErrIncorrectIdentity) { | ||
// ScryptIdentity returns ErrIncorrectIdentity for an incorrect | ||
// passphrase, which would lead Decrypt to returning "no identity | ||
// matched any recipient". That makes sense in the API, where there | ||
// might be multiple configured ScryptIdentity. Since in cmd/age there | ||
// can be only one, return a better error message. | ||
return nil, errors.New("incorrect passphrase") | ||
} | ||
return fileKey, err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# test that chezmoi age encrypt encrypts a file with a passphrase | ||
stdin $HOME/passphrases | ||
exec chezmoi age encrypt --output $HOME${/}secret.txt.age --passphrase --no-tty $HOME${/}secret.txt | ||
grep '-----BEGIN AGE ENCRYPTED FILE----' $HOME/secret.txt.age | ||
|
||
# test that chezmoi decrypt decrypts a file with a passphrase | ||
stdin $HOME/passphrase | ||
exec chezmoi age decrypt --output $HOME${/}secret.txt.decrypted --passphrase --no-tty $HOME${/}secret.txt.age | ||
cmp $HOME/secret.txt.decrypted $HOME/secret.txt | ||
|
||
-- home/user/passphrase -- | ||
passphrase | ||
-- home/user/passphrases -- | ||
passphrase | ||
passphrase | ||
-- home/user/plaintext.txt -- | ||
plaintext | ||
-- home/user/secret.txt -- | ||
secret |