age

Simple, secure encryption with UNIX-style composability.
git clone git://git.sgregoratto.me/age
Log | Files | Refs | README | LICENSE

commit 0940f184fb8fdf8f729361a79443ddd4e62f6bb9
parent 37d95cc84a986bf2582b3daf9a5aa5db009ecfe1
Author: Filippo Valsorda <hi@filippo.io>
Date:   Sun,  6 Oct 2019 23:16:20 -0400

cmd/age: add a prototype of the command line tool

Diffstat:
DDockerfile.gofuzz | 9---------
Acmd/age/age.go | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/age/parse.go | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/age/age.go | 6++++++
Minternal/age/age_test.go | 6++++++
Minternal/age/primitives.go | 6++++++
Minternal/age/recipients_test.go | 6++++++
Minternal/age/scrypt.go | 6++++++
Minternal/age/ssh.go | 6++++++
Minternal/age/x25519.go | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/format/format.go | 6++++++
Minternal/stream/stream.go | 9+++++++--
Minternal/stream/stream_test.go | 6++++++
13 files changed, 266 insertions(+), 11 deletions(-)

diff --git a/Dockerfile.gofuzz b/Dockerfile.gofuzz @@ -1,9 +0,0 @@ -FROM golang:1.12-alpine3.10 -RUN apk add --no-cache git -RUN go get github.com/dvyukov/go-fuzz/... -ADD . $GOPATH/src/github.com/FiloSottile/age/ -WORKDIR $GOPATH/src/github.com/FiloSottile/age -RUN GO111MODULE=on go mod vendor -RUN go-fuzz-build ./internal/format -VOLUME /workdir -ENTRYPOINT ["go-fuzz", "-workdir", "/workdir", "-bin", "format-fuzz.zip"] diff --git a/cmd/age/age.go b/cmd/age/age.go @@ -0,0 +1,106 @@ +// Copyright 2019 Google LLC +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +package main + +import ( + "crypto/rand" + "flag" + "fmt" + "io" + "log" + "os" + "time" + + "github.com/FiloSottile/age/internal/age" +) + +func main() { + log.SetFlags(0) + + generateFlag := flag.Bool("generate", false, "generate a new age key pair") + decryptFlag := flag.Bool("d", false, "decrypt the input") + flag.Parse() + + switch { + case *generateFlag: + if *decryptFlag { + log.Fatalf("Invalid flag combination") + } + generate() + case *decryptFlag: + if *generateFlag { + log.Fatalf("Invalid flag combination") + } + decrypt() + default: + encrypt() + } +} + +func generate() { + if len(flag.Args()) != 0 { + log.Fatalf("-generate takes no arguments") + } + + key := make([]byte, 32) + if _, err := rand.Read(key); err != nil { + log.Fatalf("Internal error: %v", err) + } + k, err := age.NewX25519Identity(key) + if err != nil { + log.Fatalf("Internal error: %v", err) + } + + fmt.Printf("# created: %s\n", time.Now().Format(time.RFC3339)) + fmt.Printf("# %s\n", k.Recipient()) + fmt.Printf("%s\n", k) +} + +func encrypt() { + var recipients []age.Recipient + for _, arg := range flag.Args() { + r, err := parseRecipient(arg) + if err != nil { + log.Fatalf("Error: %v", err) + } + recipients = append(recipients, r) + } + if len(recipients) == 0 { + log.Fatalf("Missing recipients!") + } + + w, err := age.Encrypt(os.Stdout, recipients...) + if err != nil { + log.Fatalf("Error initializing encryption: %v", err) + } + if _, err := io.Copy(w, os.Stdin); err != nil { + log.Fatalf("Error encrypting the input: %v", err) + } + if err := w.Close(); err != nil { + log.Fatalf("Error finalizing encryption: %v", err) + } +} + +func decrypt() { + var identities []age.Identity + // TODO: use the default location if no arguments are provided. + for _, name := range flag.Args() { + ids, err := parseIdentitiesFile(name) + if err != nil { + log.Fatalf("Error: %v", err) + } + identities = append(identities, ids...) + } + + r, err := age.Decrypt(os.Stdin, identities...) + if err != nil { + log.Fatalf("Error initializing decryption: %v", err) + } + if _, err := io.Copy(os.Stdout, r); err != nil { + log.Fatalf("Error decrypting the input: %v", err) + } +} diff --git a/cmd/age/parse.go b/cmd/age/parse.go @@ -0,0 +1,52 @@ +// Copyright 2019 Google LLC +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +package main + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/FiloSottile/age/internal/age" +) + +func parseRecipient(arg string) (age.Recipient, error) { + if strings.HasPrefix(arg, "pubkey:") { + return age.ParseX25519Recipient(arg) + } + return nil, fmt.Errorf("unknown recipient type: %s", arg) +} + +func parseIdentitiesFile(name string) ([]age.Identity, error) { + f, err := os.Open(name) + if err != nil { + return nil, fmt.Errorf("failed to open file: %v", err) + } + + var ids []age.Identity + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "#") || line == "" { + continue + } + i, err := age.ParseX25519Identity(line) + if err != nil { + return nil, fmt.Errorf("malformed secret keys file %q: %v", name, err) + } + ids = append(ids, i) + } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("failed to read %q: %v", name, err) + } + + if len(ids) == 0 { + return nil, fmt.Errorf("no secret keys found in %q", name) + } + return ids, nil +} diff --git a/internal/age/age.go b/internal/age/age.go @@ -1,3 +1,9 @@ +// Copyright 2019 Google LLC +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + package age import ( diff --git a/internal/age/age_test.go b/internal/age/age_test.go @@ -1,3 +1,9 @@ +// Copyright 2019 Google LLC +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + package age_test import ( diff --git a/internal/age/primitives.go b/internal/age/primitives.go @@ -1,3 +1,9 @@ +// Copyright 2019 Google LLC +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + package age import ( diff --git a/internal/age/recipients_test.go b/internal/age/recipients_test.go @@ -1,3 +1,9 @@ +// Copyright 2019 Google LLC +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + package age_test import ( diff --git a/internal/age/scrypt.go b/internal/age/scrypt.go @@ -1,3 +1,9 @@ +// Copyright 2019 Google LLC +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + package age import ( diff --git a/internal/age/ssh.go b/internal/age/ssh.go @@ -1,3 +1,9 @@ +// Copyright 2019 Google LLC +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + package age import ( diff --git a/internal/age/x25519.go b/internal/age/x25519.go @@ -1,3 +1,9 @@ +// Copyright 2019 Google LLC +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + package age import ( @@ -6,6 +12,7 @@ import ( "errors" "fmt" "io" + "strings" "github.com/FiloSottile/age/internal/format" "golang.org/x/crypto/chacha20poly1305" @@ -32,6 +39,22 @@ func NewX25519Recipient(publicKey []byte) (*X25519Recipient, error) { return r, nil } +func ParseX25519Recipient(s string) (*X25519Recipient, error) { + if !strings.HasPrefix(s, "pubkey:") { + return nil, fmt.Errorf("malformed recipient: %s", s) + } + pubKey := strings.TrimPrefix(s, "pubkey:") + k, err := format.DecodeString(pubKey) + if err != nil { + return nil, fmt.Errorf("malformed recipient: %s", s) + } + r, err := NewX25519Recipient(k) + if err != nil { + return nil, fmt.Errorf("malformed recipient: %s", s) + } + return r, nil +} + func (r *X25519Recipient) Wrap(fileKey []byte) (*format.Recipient, error) { var ephemeral, ourPublicKey [32]byte if _, err := rand.Read(ephemeral[:]); err != nil { @@ -65,6 +88,10 @@ func (r *X25519Recipient) Wrap(fileKey []byte) (*format.Recipient, error) { return l, nil } +func (r *X25519Recipient) String() string { + return "pubkey:" + format.EncodeToString(r.theirPublicKey[:]) +} + type X25519Identity struct { secretKey, ourPublicKey [32]byte } @@ -83,6 +110,22 @@ func NewX25519Identity(secretKey []byte) (*X25519Identity, error) { return i, nil } +func ParseX25519Identity(s string) (*X25519Identity, error) { + if !strings.HasPrefix(s, "AGE_SECRET_KEY_") { + return nil, fmt.Errorf("malformed secret key: %s", s) + } + privKey := strings.TrimPrefix(s, "AGE_SECRET_KEY_") + k, err := format.DecodeString(privKey) + if err != nil { + return nil, fmt.Errorf("malformed secret key: %s", s) + } + r, err := NewX25519Identity(k) + if err != nil { + return nil, fmt.Errorf("malformed secret key: %s", s) + } + return r, nil +} + func (i *X25519Identity) Unwrap(block *format.Recipient) ([]byte, error) { if block.Type != "X25519" { return nil, errors.New("wrong recipient block type") @@ -121,3 +164,13 @@ func (i *X25519Identity) Unwrap(block *format.Recipient) ([]byte, error) { } return fileKey, nil } + +func (i *X25519Identity) Recipient() *X25519Recipient { + r := &X25519Recipient{} + r.theirPublicKey = i.ourPublicKey + return r +} + +func (i *X25519Identity) String() string { + return "AGE_SECRET_KEY_" + format.EncodeToString(i.secretKey[:]) +} diff --git a/internal/format/format.go b/internal/format/format.go @@ -1,3 +1,9 @@ +// Copyright 2019 Google LLC +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + package format import ( diff --git a/internal/stream/stream.go b/internal/stream/stream.go @@ -1,3 +1,9 @@ +// Copyright 2019 Google LLC +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + package stream import ( @@ -156,8 +162,7 @@ func (w *Writer) Write(p []byte) (n int, err error) { total := len(p) for len(p) > 0 { - free := ChunkSize - len(w.unwritten) - freeBuf := w.buf[len(w.unwritten) : len(w.unwritten)+free] + freeBuf := w.buf[len(w.unwritten):ChunkSize] n := copy(freeBuf, p) p = p[n:] w.unwritten = w.unwritten[:len(w.unwritten)+n] diff --git a/internal/stream/stream_test.go b/internal/stream/stream_test.go @@ -1,3 +1,9 @@ +// Copyright 2019 Google LLC +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + package stream_test import (