Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 29 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
- Signed envelopes detect tampering and sender spoofing.
- Plaintext output to stdout is blocked by default unless `--out -` is explicitly passed.

## Terminology
- `peer`: the person or device you are sending a secret to
- `share code`: the `ENDE-PUB-1:...` string used for peer onboarding
- `send` / `receive`: task-oriented names for `encrypt` / `decrypt`
- `recipient` / `sender`: lower-level trust model terms still used in advanced commands

## Install/build
```bash
go build ./cmd/ende
Expand Down Expand Up @@ -110,18 +116,18 @@ The tutorial guides you through:
./ende key keygen --name bob --export-public --export-dir .
```

2. Alice shares `share:` token from keygen output to Bob.
2. Alice shares the `share:` code from keygen output to Bob.

You can re-print a share token later:
You can re-print a share code later:
```bash
./ende key share --name alice
```

3. Bob registers interactively in one command (recipient + sender):
3. Bob adds Alice as a peer in one command:
```bash
./ende register
# share token (ENDE-PUB-1:...): ENDE-PUB-1:...
# alias override (optional, Enter to use token id):
./ende add-peer
# share code (ENDE-PUB-1:...): ENDE-PUB-1:...
# peer name override (optional, Enter to use the shared name):
```

4. Run a local safety check before first real use:
Expand All @@ -132,77 +138,77 @@ You can re-print a share token later:
- keyring file presence and permissions
- default signer configuration
- private key file paths and `0600` permissions
- recipient/trusted-sender registration consistency
- peer / trusted-signing-key registration consistency

To remove a registered alias later:
```bash
./ende unregister alice
```

5. Encrypt + sign (default: text to stdout):
5. Send a secret securely (default: text to stdout):
```bash
echo 'TOKEN=abc123' | ./ende encrypt -t bob
echo 'TOKEN=abc123' | ./ende send -t bob
```

5-0. Encrypt from file input:
```bash
./ende encrypt -t bob -f secrets.env -o secret.txt
./ende send -t bob -f secrets.env -o secret.txt
```

5-1. Save text output to file (optional):
```bash
echo 'TOKEN=abc123' | ./ende encrypt -t bob --text -o secret.txt
echo 'TOKEN=abc123' | ./ende send -t bob --text -o secret.txt
```

5-2. Raw binary output (optional):
```bash
echo 'TOKEN=abc123' | ./ende encrypt -t bob --binary -o secret.ende
echo 'TOKEN=abc123' | ./ende send -t bob --binary -o secret.ende
```

5-3. Prompt for a secret interactively without echoing it to the terminal:
```bash
./ende encrypt -t bob --prompt -o secret.txt
./ende send -t bob --prompt -o secret.txt
```
Interactive prompt notes:
- TTY input is masked so the secret is not echoed while typing.
- Empty prompt input is rejected.
- Non-interactive stdin/file workflows continue to work as before.

5-4. Review recipients and output details before encrypting:
5-4. Review peer and output details before sending:
```bash
echo 'TOKEN=abc123' | ./ende encrypt -t bob --confirm -o secret.txt
echo 'TOKEN=abc123' | ./ende send -t bob --confirm -o secret.txt
```
`--confirm` shows:
- recipient alias and short fingerprint
- peer alias and short fingerprint
- signer key id
- output target
- output format

For automation, you can keep the summary behavior in scripts and skip the prompt explicitly:
```bash
echo 'TOKEN=abc123' | ./ende encrypt -t bob --confirm --yes -o secret.txt
echo 'TOKEN=abc123' | ./ende send -t bob --confirm --yes -o secret.txt
```

6. Verify and decrypt:
6. Receive and decrypt:
```bash
./ende verify -i secret.ende
./ende decrypt -i secret.ende -o decrypted.txt
./ende receive -i secret.ende -o decrypted.txt
```

Text envelope input is also supported:
```bash
./ende verify -i secret.txt
./ende decrypt -i secret.txt -o decrypted.txt
./ende decrypt -i secret.txt --text-out
./ende receive -i secret.txt -o decrypted.txt
./ende receive -i secret.txt --text-out
```

Safer plaintext output options:
```bash
# Refuse to overwrite an existing plaintext file
./ende decrypt -i secret.ende -o decrypted.txt --no-clobber
./ende receive -i secret.ende -o decrypted.txt --no-clobber

# Write plaintext to a temporary 0600 file and print the path
./ende decrypt -i secret.ende --out-temp
./ende receive -i secret.ende --out-temp
```

`--out-temp` is useful when you want Ende to choose a short-lived secure file path for you.
Expand Down
16 changes: 8 additions & 8 deletions cmd/ende/crypto_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func newEncryptCommand() *cobra.Command {
var yes bool
cmd := &cobra.Command{
Use: "encrypt",
Short: "Encrypt and sign secret payload",
Short: "Send a secret securely",
Aliases: []string{
"enc",
},
Expand Down Expand Up @@ -151,15 +151,15 @@ func newEncryptCommand() *cobra.Command {
return nil
},
}
cmd.Flags().StringSliceVarP(&tos, "to", "t", nil, "recipient alias, github:user, or age1... public key")
cmd.Flags().StringSliceVarP(&tos, "to", "t", nil, "peer alias, github:user, or age1... public key")
cmd.Flags().StringVarP(&signAs, "sign-as", "s", "", "local signing key id (optional if default signer is set)")
cmd.Flags().StringVarP(&in, "in", "i", "-", "input path or -")
cmd.Flags().StringVarP(&fileInput, "file", "f", "", "input file path (alias of --in)")
cmd.Flags().StringVarP(&out, "out", "o", "-", "output path or -")
cmd.Flags().BoolVar(&textOut, "text", true, "output ASCII-armored envelope for copy/paste transport (default true)")
cmd.Flags().BoolVar(&binaryOut, "binary", false, "output raw binary envelope")
cmd.Flags().BoolVar(&prompt, "prompt", false, "prompt for secret value interactively")
cmd.Flags().BoolVar(&confirm, "confirm", false, "show recipient summary and ask for confirmation before encrypting")
cmd.Flags().BoolVar(&confirm, "confirm", false, "show peer summary and ask for confirmation before encrypting")
cmd.Flags().BoolVar(&yes, "yes", false, "skip confirmation prompt (useful with --confirm in automation)")
return cmd
}
Expand All @@ -172,7 +172,7 @@ func newDecryptCommand() *cobra.Command {
var outTemp bool
cmd := &cobra.Command{
Use: "decrypt",
Short: "Verify and decrypt envelope",
Short: "Receive and decrypt a secret",
Aliases: []string{
"dec",
},
Expand Down Expand Up @@ -258,7 +258,7 @@ func newVerifyCommand() *cobra.Command {
var in string
cmd := &cobra.Command{
Use: "verify",
Short: "Verify signature without decrypting",
Short: "Verify who sent a secret without decrypting",
Aliases: []string{
"v",
},
Expand Down Expand Up @@ -315,7 +315,7 @@ func resolveRecipient(store *keyring.Store, target string) (age.Recipient, strin
}
r, ok := store.Recipient(target)
if !ok {
return nil, "", encryptRecipientSummary{}, fmt.Errorf("recipient alias not found: %s", target)
return nil, "", encryptRecipientSummary{}, fmt.Errorf("peer alias not found: %s", target)
}
rec, err := age.ParseX25519Recipient(r.AgePublic)
if err != nil {
Expand Down Expand Up @@ -344,9 +344,9 @@ func openConfirmationReader(in io.Reader) (io.Reader, io.Closer, error) {
}

func confirmEncrypt(in io.Reader, errw io.Writer, summary encryptSummary) error {
fmt.Fprintln(errw, "Encrypt summary:")
fmt.Fprintln(errw, "Send summary:")
for _, recipient := range summary.Recipients {
fmt.Fprintf(errw, "- recipient: %s (fp=%s source=%s)\n", recipient.Label, recipient.Fingerprint, recipient.Source)
fmt.Fprintf(errw, "- peer: %s (fp=%s source=%s)\n", recipient.Label, recipient.Fingerprint, recipient.Source)
}
fmt.Fprintf(errw, "- signer: %s\n", summary.SignerID)
fmt.Fprintf(errw, "- output: %s\n", summary.OutputPath)
Expand Down
4 changes: 2 additions & 2 deletions cmd/ende/crypto_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ func TestConfirmEncryptAcceptsYes(t *testing.T) {
t.Fatalf("confirmEncrypt: %v", err)
}
got := errBuf.String()
if !strings.Contains(got, "recipient: bob") {
t.Fatalf("expected recipient summary, got:\n%s", got)
if !strings.Contains(got, "peer: bob") {
t.Fatalf("expected peer summary, got:\n%s", got)
}
if !strings.Contains(got, "Continue? [y/N]: ") {
t.Fatalf("expected confirmation prompt, got:\n%s", got)
Expand Down
2 changes: 1 addition & 1 deletion cmd/ende/key_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ func newKeyShareCommand() *cobra.Command {
var name string
cmd := &cobra.Command{
Use: "share",
Short: "Print share token for an existing local key",
Short: "Print a share code for an existing local key",
RunE: func(cmd *cobra.Command, args []string) error {
if name == "" && len(args) == 1 {
name = args[0]
Expand Down
26 changes: 13 additions & 13 deletions cmd/ende/parties_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
)

func newSenderCommand() *cobra.Command {
cmd := &cobra.Command{Use: "sender", Short: "Manage trusted sender signing keys", Aliases: []string{"snd"}}
cmd := &cobra.Command{Use: "sender", Short: "Manage trusted peer signing keys", Aliases: []string{"snd"}}
cmd.AddCommand(newSenderAddCommand(), newSenderShowCommand(), newSenderRotateCommand(), newSenderListCommand())
return cmd
}
Expand All @@ -22,7 +22,7 @@ func newSenderAddCommand() *cobra.Command {
var force bool
cmd := &cobra.Command{
Use: "add",
Short: "Add trusted sender signing public key",
Short: "Add a trusted peer signing public key",
RunE: func(cmd *cobra.Command, args []string) error {
if id == "" || signingPublic == "" {
return fmt.Errorf("--id and --signing-public are required")
Expand Down Expand Up @@ -56,7 +56,7 @@ func newSenderAddCommand() *cobra.Command {
}

func newRecipientCommand() *cobra.Command {
cmd := &cobra.Command{Use: "recipient", Short: "Manage recipient aliases", Aliases: []string{"rcpt"}}
cmd := &cobra.Command{Use: "recipient", Short: "Manage peer aliases", Aliases: []string{"rcpt"}}
cmd.AddCommand(newRecipientAddCommand(), newRecipientShowCommand(), newRecipientRotateCommand())
return cmd
}
Expand All @@ -66,7 +66,7 @@ func newRegisterCommand() *cobra.Command {
var force bool
cmd := &cobra.Command{
Use: "register",
Short: "Register recipient and trusted sender in one step",
Short: "Add a peer from a share code or public keys",
Aliases: []string{"reg"},
RunE: func(cmd *cobra.Command, args []string) error {
store, err := keyring.Load()
Expand Down Expand Up @@ -151,7 +151,7 @@ func newUnregisterCommand() *cobra.Command {
var force bool
cmd := &cobra.Command{
Use: "unregister <alias>",
Short: "Remove recipient and trusted sender registration for an alias",
Short: "Remove a peer alias and its trusted signing key",
Aliases: []string{"unreg"},
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -185,7 +185,7 @@ func newRecipientAddCommand() *cobra.Command {
var force bool
cmd := &cobra.Command{
Use: "add",
Short: "Add recipient by alias or GitHub username",
Short: "Add a peer by alias, share code, or GitHub username",
RunE: func(cmd *cobra.Command, args []string) error {
store, err := keyring.Load()
if err != nil {
Expand Down Expand Up @@ -288,9 +288,9 @@ func newRecipientAddCommand() *cobra.Command {
return nil
},
}
cmd.Flags().StringVar(&alias, "alias", "", "recipient alias")
cmd.Flags().StringVar(&alias, "alias", "", "peer alias")
cmd.Flags().StringVar(&key, "key", "", "age recipient public key")
cmd.Flags().StringVar(&share, "share", "", "share token from keygen output")
cmd.Flags().StringVar(&share, "share", "", "share code from keygen output")
cmd.Flags().StringVar(&githubUser, "github", "", "github username (optional resolver)")
cmd.Flags().IntVar(&keyIndex, "key-index", 0, "github ssh key index for pinning")
cmd.Flags().BoolVar(&force, "force", false, "overwrite existing recipient alias")
Expand All @@ -300,7 +300,7 @@ func newRecipientAddCommand() *cobra.Command {
func newSenderListCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List trusted senders",
Short: "List trusted peer signing keys",
Aliases: []string{
"ls",
},
Expand All @@ -322,7 +322,7 @@ func newSenderListCommand() *cobra.Command {
func newSenderShowCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "show <id>",
Short: "Show trusted sender details",
Short: "Show trusted peer signing key details",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
store, err := keyring.Load()
Expand All @@ -344,7 +344,7 @@ func newSenderRotateCommand() *cobra.Command {
var signingPublic string
cmd := &cobra.Command{
Use: "rotate <id>",
Short: "Rotate trusted sender signing public key",
Short: "Rotate a trusted peer signing public key",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if signingPublic == "" {
Expand Down Expand Up @@ -378,7 +378,7 @@ func newSenderRotateCommand() *cobra.Command {
func newRecipientShowCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "show <alias>",
Short: "Show recipient details",
Short: "Show peer details",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
store, err := keyring.Load()
Expand All @@ -400,7 +400,7 @@ func newRecipientRotateCommand() *cobra.Command {
var key string
cmd := &cobra.Command{
Use: "rotate <alias>",
Short: "Rotate recipient public key",
Short: "Rotate a peer public key",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if key == "" {
Expand Down
14 changes: 7 additions & 7 deletions cmd/ende/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ type fdReader interface {

func promptRecipientInput(in io.Reader, errw io.Writer) (alias string, keyOrShare string, err error) {
r := bufio.NewReader(in)
fmt.Fprint(errw, "alias: ")
fmt.Fprint(errw, "peer alias: ")
alias, err = r.ReadString('\n')
if err != nil && err != io.EOF {
return "", "", fmt.Errorf("read alias: %w", err)
}
fmt.Fprint(errw, "key/share: ")
fmt.Fprint(errw, "peer public key or share code: ")
keyOrShare, err = r.ReadString('\n')
if err != nil && err != io.EOF {
return "", "", fmt.Errorf("read key/share: %w", err)
Expand Down Expand Up @@ -67,7 +67,7 @@ func promptRegisterInput(in io.Reader, errw io.Writer) (alias string, recipientO
if err != nil && err != io.EOF {
return "", "", "", fmt.Errorf("read alias: %w", err)
}
fmt.Fprint(errw, "recipient key or share token: ")
fmt.Fprint(errw, "peer public key or share code: ")
recipientOrShare, err = r.ReadString('\n')
if err != nil && err != io.EOF {
return "", "", "", fmt.Errorf("read recipient/share: %w", err)
Expand Down Expand Up @@ -108,16 +108,16 @@ func readEnvelopeInteractive(in io.Reader, errw io.Writer) ([]byte, error) {

func promptShareRegisterInput(in io.Reader, errw io.Writer) (share string, aliasOverride string, err error) {
r := bufio.NewReader(in)
fmt.Fprint(errw, "share token (ENDE-PUB-1:...): ")
fmt.Fprint(errw, "share code (ENDE-PUB-1:...): ")
share, err = r.ReadString('\n')
if err != nil && err != io.EOF {
return "", "", fmt.Errorf("read share token: %w", err)
return "", "", fmt.Errorf("read share code: %w", err)
}
share = strings.TrimSpace(share)
if share == "" {
return "", "", fmt.Errorf("share token is required")
return "", "", fmt.Errorf("share code is required")
}
fmt.Fprint(errw, "alias override (optional, Enter to use token id): ")
fmt.Fprint(errw, "peer name override (optional, Enter to use the shared name): ")
aliasOverride, err = r.ReadString('\n')
if err != nil && err != io.EOF {
return "", "", fmt.Errorf("read alias override: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion cmd/ende/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func main() {
var debug bool
root := &cobra.Command{
Use: "ende",
Short: "Ende securely encrypts secrets between developers",
Short: "Send secrets securely between developers and peers",
Version: version,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if debug {
Expand Down
Loading
Loading