mirror of
https://github.com/FiloSottile/age.git
synced 2026-11-08 12:26:58 +00:00
Compare commits
42
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e609359651 | ||
|
|
33355dcc1c | ||
|
|
9a08b7e66f | ||
|
|
9b83d948f5 | ||
|
|
c9a35c0727 | ||
|
|
7d608d1219 | ||
|
|
6782356e45 | ||
|
|
085466567a | ||
|
|
292c3aaeea | ||
|
|
b32ea4c1f6 | ||
|
|
c7c7f1870f | ||
|
|
a7c4274d23 | ||
|
|
7088a73234 | ||
|
|
f0f8092d60 | ||
|
|
f28f85d87b | ||
|
|
f54bb8daab | ||
|
|
35e582514d | ||
|
|
111d0fe1f4 | ||
|
|
9fdb125641 | ||
|
|
a798d4ef31 | ||
|
|
68da6c5f50 | ||
|
|
2419b5da58 | ||
|
|
4e84199130 | ||
|
|
f9f6d7ca50 | ||
|
|
e43cf8b4a2 | ||
|
|
7935150f35 | ||
|
|
1110c3b6e1 | ||
|
|
1223fbe6da | ||
|
|
b194267aaa | ||
|
|
3807646596 | ||
|
|
dd887fdc87 | ||
|
|
0da94651f3 | ||
|
|
80c6b4edd9 | ||
|
|
b3fc5d19ea | ||
|
|
c02443e4d0 | ||
|
|
a5773f28a3 | ||
|
|
63301d18b4 | ||
|
|
d6fe1cf5c8 | ||
|
|
18edf29a75 | ||
|
|
1bf22e2163 | ||
|
|
3d73da544d | ||
|
|
c185781433 |
No files matched your search
@@ -0,0 +1,9 @@
|
||||
env:
|
||||
CIRRUS_CLONE_DEPTH: 1
|
||||
|
||||
freebsd_12_task:
|
||||
freebsd_instance:
|
||||
image: freebsd-12-1-release-amd64
|
||||
install_script: pkg install -y go
|
||||
build_script: go build -v ./...
|
||||
test_script: go test -v ./...
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
name: Spec feedback
|
||||
about: Have a comment about the age spec as it's implemented by this and other tools?
|
||||
title: 'spec: '
|
||||
labels: 'spec'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- This is the issue tracker of a specific implementation of
|
||||
the age format, which is specified at https://age-encryption.org/v1
|
||||
|
||||
Please consider using the mailing list to discuss the specification:
|
||||
|
||||
https://age-encryption.org/ml -->
|
||||
@@ -0,0 +1,14 @@
|
||||
name: Interoperability tests
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
trigger:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger interoperability tests in str4d/rage
|
||||
run: |
|
||||
curl -X POST https://api.github.com/repos/str4d/rage/dispatches \
|
||||
-H 'Accept: application/vnd.github.v3+json' \
|
||||
-H 'Authorization: token ${{ secrets.RAGE_INTEROP_ACCESS_TOKEN }}' \
|
||||
--data '{"event_type": "age-interop-request", "client_payload": { "sha": "'"$GITHUB_SHA"'" }}'
|
||||
@@ -0,0 +1,20 @@
|
||||
# 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
|
||||
|
||||
class Age < Formula
|
||||
desc "Simple, modern, secure file encryption"
|
||||
homepage "https://filippo.io/age"
|
||||
url "https://github.com/FiloSottile/age/archive/v1.0.0-beta2.zip"
|
||||
sha256 "b7417e94c32c7e9356e441815f814073009c4a6455da96bde1536fae8cb0edbf"
|
||||
|
||||
depends_on "go" => :build
|
||||
|
||||
def install
|
||||
mkdir bin
|
||||
system "go", "build", "-trimpath", "-o", bin, "filippo.io/age/cmd/..."
|
||||
prefix.install_metafiles
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,115 @@
|
||||
age is meant to be a simple, secure and modern encryption tool with small explicit keys, no config options, and UNIX-style composability. The spec is at [age-encryption.org/v1](https://age-encryption.org/v1).
|
||||
# age
|
||||
|
||||
This implementation is in progress, and things will keep moving around, so it's not yet a good time to contribute, and it's definitely not a good time to rely on it.
|
||||
age is a simple, modern and secure file encryption tool.
|
||||
|
||||
To discuss the spec or other age related topics, please email the mailing list at age-dev@googlegroups.com. Subscribe at [groups.google.com/d/forum/age-dev](https://groups.google.com/d/forum/age-dev) or by emailing age-dev+subscribe@googlegroups.com.
|
||||
It features small explicit keys, no config options, and UNIX-style composability.
|
||||
|
||||
Development is sometimes livestreamed at [twitch.tv/filosottile](https://www.twitch.tv/filosottile).
|
||||
```
|
||||
$ age-keygen -o key.txt
|
||||
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
|
||||
$ tar cvz ~/data | age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p > data.tar.gz.age
|
||||
$ age -d -i key.txt data.tar.gz.age > data.tar.gz
|
||||
```
|
||||
|
||||
The format specification is at [age-encryption.org/v1](https://age-encryption.org/v1). To discuss the spec or other age related topics, please email [the mailing list](https://groups.google.com/d/forum/age-dev) at age-dev@googlegroups.com. age was designed by [@Benjojo12](https://twitter.com/Benjojo12) and [@FiloSottile](https://twitter.com/FiloSottile).
|
||||
|
||||
An alternative interoperable Rust implementation is available at [github.com/str4d/rage](https://github.com/str4d/rage).
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
Usage:
|
||||
age -r RECIPIENT [-a] [-o OUTPUT] [INPUT]
|
||||
age --decrypt [-i KEY] [-o OUTPUT] [INPUT]
|
||||
|
||||
Options:
|
||||
-o, --output OUTPUT Write the result to the file at path OUTPUT.
|
||||
-a, --armor Encrypt to a PEM encoded format.
|
||||
-p, --passphrase Encrypt with a passphrase.
|
||||
-r, --recipient RECIPIENT Encrypt to the specified RECIPIENT. Can be repeated.
|
||||
-d, --decrypt Decrypt the input to the output.
|
||||
-i, --identity KEY Use the private key file at path KEY. Can be repeated.
|
||||
|
||||
INPUT defaults to standard input, and OUTPUT defaults to standard output.
|
||||
|
||||
RECIPIENT can be an age public key, as generated by age-keygen, ("age1...")
|
||||
or an SSH public key ("ssh-ed25519 AAAA...", "ssh-rsa AAAA...").
|
||||
|
||||
KEY is a path to a file with age secret keys, one per line
|
||||
(ignoring "#" prefixed comments and empty lines), or to an SSH key file.
|
||||
Multiple keys can be provided, and any unused ones will be ignored.
|
||||
```
|
||||
|
||||
### Multiple recipients
|
||||
|
||||
Files can be encrypted to multiple recipients by repeating `-r/--recipient`. Every recipient will be able to decrypt the file.
|
||||
|
||||
```
|
||||
$ age -o example.jpg.age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p \
|
||||
-r age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg example.jpg
|
||||
```
|
||||
|
||||
### Passphrases
|
||||
|
||||
Files can be encrypted with a passphrase by using `-p/--passphrase`. By default age will automatically generate a secure passphrase. Passphrase protected files are automatically detected at decrypt time.
|
||||
|
||||
```
|
||||
$ age -p secrets.txt > secrets.txt.age
|
||||
Enter passphrase (leave empty to autogenerate a secure one):
|
||||
Using the autogenerated passphrase "release-response-step-brand-wrap-ankle-pair-unusual-sword-train".
|
||||
$ age -d secrets.txt.age > secrets.txt
|
||||
Enter passphrase:
|
||||
```
|
||||
|
||||
### SSH keys
|
||||
|
||||
As a convenience feature, age also supports encrypting to `ssh-rsa` and `ssh-ed25519` SSH public keys, and decrypting with the respective private key file. (`ssh-agent` is not supported.)
|
||||
|
||||
```
|
||||
$ cat ~/.ssh/id_ed25519.pub
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIZDRcvS8PnhXr30WKSKmf7WKKi92ACUa5nW589WukJz filippo@Bistromath.local
|
||||
$ age -r "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIZDRcvS8PnhXr30WKSKmf7WKKi92ACUa5nW589WukJz" example.jpg > example.jpg.age
|
||||
$ age -d -i ~/.ssh/id_ed25519 example.jpg.age > example.jpg
|
||||
```
|
||||
|
||||
Note that SSH key support employs more complex cryptography, and embeds a public key tag in the encrypted file, making it possible to track files that are encrypted to a specific public key.
|
||||
|
||||
## Installation
|
||||
|
||||
On macOS or Linux, you can use Homebrew:
|
||||
|
||||
```
|
||||
brew tap filippo.io/age https://filippo.io/age
|
||||
brew install age
|
||||
```
|
||||
|
||||
On Windows, Linux, and macOS, you can use [the pre-built binaries](https://github.com/FiloSottile/age/releases).
|
||||
|
||||
If your system has [Go 1.13+](https://golang.org/dl/), you can build from source:
|
||||
|
||||
```
|
||||
git clone https://filippo.io/age && cd age
|
||||
go build -o . filippo.io/age/cmd/...
|
||||
```
|
||||
|
||||
On Arch Linux, age is available from AUR as [`age`](https://aur.archlinux.org/packages/age/) or [`age-git`](https://aur.archlinux.org/packages/age-git/):
|
||||
|
||||
```bash
|
||||
git clone https://aur.archlinux.org/age.git
|
||||
cd age
|
||||
makepkg -si
|
||||
```
|
||||
|
||||
On OpenBSD -current and 6.7+, you can use the port:
|
||||
|
||||
```
|
||||
pkg_add age
|
||||
```
|
||||
|
||||
On all supported versions of FreeBSD, you can build the security/age port or use pkg:
|
||||
|
||||
```
|
||||
pkg install age
|
||||
```
|
||||
|
||||
Help from new packagers is very welcome.
|
||||
@@ -4,7 +4,16 @@
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
// Package age implements file encryption according to age-encryption.org/v1.
|
||||
// Package age implements file encryption according to the age-encryption.org/v1
|
||||
// specification.
|
||||
//
|
||||
// For most use cases, use the Encrypt and Decrypt functions with
|
||||
// X25519Recipient and X25519Identity. If passphrase encryption is required, use
|
||||
// ScryptRecipient and ScryptIdentity. For compatibility with existing SSH keys
|
||||
// use the filippo.io/age/agessh package.
|
||||
//
|
||||
// Age encrypted files are binary and not malleable, for encoding them as text,
|
||||
// use the filippo.io/age/armor package.
|
||||
package age
|
||||
|
||||
import (
|
||||
@@ -18,37 +27,51 @@ import (
|
||||
"filippo.io/age/internal/stream"
|
||||
)
|
||||
|
||||
// An Identity is a private key or other value that can decrypt an opaque file
|
||||
// key from a recipient stanza.
|
||||
//
|
||||
// Unwrap must return ErrIncorrectIdentity for recipient blocks that don't match
|
||||
// the identity, any other error might be considered fatal.
|
||||
type Identity interface {
|
||||
Type() string
|
||||
Unwrap(block *format.Recipient) (fileKey []byte, err error)
|
||||
Unwrap(block *Stanza) (fileKey []byte, err error)
|
||||
}
|
||||
|
||||
// IdentityMatcher can be optionally implemented by an Identity that can
|
||||
// communicate whether it can decrypt a recipient stanza without decrypting it.
|
||||
//
|
||||
// If an Identity implements IdentityMatcher, its Unwrap method will only be
|
||||
// invoked on blocks for which Match returned nil. Match must return
|
||||
// ErrIncorrectIdentity for recipient blocks that don't match the identity, any
|
||||
// other error might be considered fatal.
|
||||
type IdentityMatcher interface {
|
||||
Identity
|
||||
Matches(block *format.Recipient) error
|
||||
Match(block *Stanza) error
|
||||
}
|
||||
|
||||
var ErrIncorrectIdentity = errors.New("incorrect identity for recipient block")
|
||||
|
||||
// A Recipient is a public key or other value that can encrypt an opaque file
|
||||
// key to a recipient stanza.
|
||||
type Recipient interface {
|
||||
Type() string
|
||||
Wrap(fileKey []byte) (*format.Recipient, error)
|
||||
Wrap(fileKey []byte) (*Stanza, error)
|
||||
}
|
||||
|
||||
// A Stanza is a section of the age header that encapsulates the file key as
|
||||
// encrypted to a specific recipient.
|
||||
type Stanza struct {
|
||||
Type string
|
||||
Args []string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
// Encrypt returns a WriteCloser. Writes to the returned value are encrypted and
|
||||
// written to dst as an age file. Every recipient will be able to decrypt the file.
|
||||
//
|
||||
// The caller must call Close on the returned value when done for the last chunk
|
||||
// to be encrypted and flushed to dst.
|
||||
func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
|
||||
// stream.Writer takes a WriteCloser, and will propagate Close calls (so
|
||||
// that the ArmoredWriter will get closed), but we don't want to expose
|
||||
// that behavior to our caller.
|
||||
dstCloser := format.NopCloser(dst)
|
||||
return encrypt(dstCloser, recipients...)
|
||||
}
|
||||
|
||||
func EncryptWithArmor(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
|
||||
dstCloser := format.ArmoredWriter(dst)
|
||||
return encrypt(dstCloser, recipients...)
|
||||
}
|
||||
|
||||
func encrypt(dst io.WriteCloser, recipients ...Recipient) (io.WriteCloser, error) {
|
||||
if len(recipients) == 0 {
|
||||
return nil, errors.New("no recipients specified")
|
||||
}
|
||||
@@ -68,7 +91,7 @@ func encrypt(dst io.WriteCloser, recipients ...Recipient) (io.WriteCloser, error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to wrap key for recipient #%d: %v", i, err)
|
||||
}
|
||||
hdr.Recipients = append(hdr.Recipients, block)
|
||||
hdr.Recipients = append(hdr.Recipients, (*format.Stanza)(block))
|
||||
}
|
||||
if mac, err := headerMAC(fileKey, hdr); err != nil {
|
||||
return nil, fmt.Errorf("failed to compute header MAC: %v", err)
|
||||
@@ -90,6 +113,8 @@ func encrypt(dst io.WriteCloser, recipients ...Recipient) (io.WriteCloser, error
|
||||
return stream.NewWriter(streamKey(fileKey, nonce), dst)
|
||||
}
|
||||
|
||||
// Decrypt returns a Reader reading the decrypted plaintext of the age file read
|
||||
// from src. All identities will be tried until one successfully decrypts the file.
|
||||
func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {
|
||||
if len(identities) == 0 {
|
||||
return nil, errors.New("no identities specified")
|
||||
@@ -115,7 +140,7 @@ RecipientsLoop:
|
||||
}
|
||||
|
||||
if i, ok := i.(IdentityMatcher); ok {
|
||||
err := i.Matches(r)
|
||||
err := i.Match((*Stanza)(r))
|
||||
if err != nil {
|
||||
if err == ErrIncorrectIdentity {
|
||||
continue
|
||||
@@ -124,9 +149,12 @@ RecipientsLoop:
|
||||
}
|
||||
}
|
||||
|
||||
fileKey, err = i.Unwrap(r)
|
||||
fileKey, err = i.Unwrap((*Stanza)(r))
|
||||
if err != nil {
|
||||
if err == ErrIncorrectIdentity {
|
||||
// TODO: we should collect these errors and return them as an
|
||||
// []error type with an Error method. That will require turning
|
||||
// ErrIncorrectIdentity into an interface or wrapper error.
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age/age"
|
||||
)
|
||||
|
||||
func ExampleEncrypt() {
|
||||
publicKey := "age1cy0su9fwf3gf9mw868g5yut09p6nytfmmnktexz2ya5uqg9vl9sss4euqm"
|
||||
recipient, err := age.ParseX25519Recipient(publicKey)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse public key %q: %v", publicKey, err)
|
||||
}
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
|
||||
w, err := age.Encrypt(out, recipient)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create encrypted file: %v", err)
|
||||
}
|
||||
if _, err := io.WriteString(w, "Black lives matter."); err != nil {
|
||||
log.Fatalf("Failed to write to encrypted file: %v", err)
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
log.Fatalf("Failed to close encrypted file: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Encrypted file size: %d\n", out.Len())
|
||||
// Output:
|
||||
// Encrypted file size: 219
|
||||
}
|
||||
|
||||
var fileContents, _ = hex.DecodeString("6167652d656e6372797074696f6e2e6f72" +
|
||||
"672f76310a2d3e20583235353139203868726c4d2b5a4247334464346646322b61353" +
|
||||
"8337a64544957446b382f5234316b43595a7376775457340a794f345059646c4d5744" +
|
||||
"4a2b437867554e527159355a30542f6d2b6733464368356a4978474c62435658630a2" +
|
||||
"d2d2d20492f696d65765a7a79383132304a537a6d4a6e6d6e2f4b4d6b337035413131" +
|
||||
"5638334e6b34316d394e50450a70c5e53624a1520753f92c5ad10ecab273ba4d61178" +
|
||||
"07713e83820417a1df2ca08182272c8f85c857734a1311a3b75e98d0eaf")
|
||||
|
||||
var privateKey = "AGE-SECRET-KEY-184JMZMVQH3E6U0PSL869004Y3U2NYV7R30EU99CSEDNPH02YUVFSZW44VU"
|
||||
|
||||
func ExampleDecrypt() {
|
||||
// DO NOT hardcode the private key. Store it in a secret storage solution,
|
||||
// on disk if the local machine is trusted, or have the user provide it.
|
||||
identity, err := age.ParseX25519Identity(privateKey)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse private key %q: %v", privateKey, err)
|
||||
}
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
f := bytes.NewReader(fileContents)
|
||||
|
||||
r, err := age.Decrypt(f, identity)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open encrypted file: %v", err)
|
||||
}
|
||||
if _, err := io.Copy(out, r); err != nil {
|
||||
log.Fatalf("Failed to read encrypted file: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("File contents: %q\n", out.Bytes())
|
||||
// Output:
|
||||
// File contents: "Black lives matter."
|
||||
}
|
||||
|
||||
func ExampleGenerateX25519Identity() {
|
||||
identity, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to generate key pair: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Public key: %s...\n", identity.Recipient().String()[:4])
|
||||
fmt.Printf("Private key: %s...\n", identity.String()[:16])
|
||||
// Output:
|
||||
// Public key: age1...
|
||||
// Private key: AGE-SECRET-KEY-1...
|
||||
}
|
||||
|
||||
const helloWorld = "Hello, Twitch!"
|
||||
|
||||
func TestEncryptDecryptX25519(t *testing.T) {
|
||||
a, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
w, err := age.Encrypt(buf, a.Recipient(), b.Recipient())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := io.WriteString(w, helloWorld); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
out, err := age.Decrypt(buf, b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
outBytes, err := ioutil.ReadAll(out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(outBytes) != helloWorld {
|
||||
t.Errorf("wrong data: %q, excepted %q", outBytes, helloWorld)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptDecryptScrypt(t *testing.T) {
|
||||
password := "twitch.tv/filosottile"
|
||||
|
||||
r, err := age.NewScryptRecipient(password)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.SetWorkFactor(15)
|
||||
buf := &bytes.Buffer{}
|
||||
w, err := age.Encrypt(buf, r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := io.WriteString(w, helloWorld); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
i, err := age.NewScryptIdentity(password)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := age.Decrypt(buf, i)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
outBytes, err := ioutil.ReadAll(out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(outBytes) != helloWorld {
|
||||
t.Errorf("wrong data: %q, excepted %q", outBytes, helloWorld)
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,11 @@ func aeadEncrypt(key, plaintext []byte) ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The nonce is fixed because this function is only used in places where the
|
||||
// spec guarantees each key is only used once (by deriving it from values
|
||||
// that include fresh randomness), allowing us to save the overhead.
|
||||
// For the code that encrypts the actual payload, look at the
|
||||
// filippo.io/age/internal/stream package.
|
||||
nonce := make([]byte, chacha20poly1305.NonceSize)
|
||||
return aead.Seal(nil, nonce, plaintext, nil), nil
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age/age"
|
||||
"filippo.io/age/internal/format"
|
||||
)
|
||||
|
||||
func TestX25519RoundTrip(t *testing.T) {
|
||||
i, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := i.Recipient()
|
||||
|
||||
if r.Type() != i.Type() || r.Type() != "X25519" {
|
||||
t.Errorf("invalid Type values: %v, %v", r.Type(), i.Type())
|
||||
}
|
||||
|
||||
if r1, err := age.ParseX25519Recipient(r.String()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if r1.String() != r.String() {
|
||||
t.Errorf("recipient did not round-trip through parsing: got %q, want %q", r1, r)
|
||||
}
|
||||
if i1, err := age.ParseX25519Identity(i.String()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if i1.String() != i.String() {
|
||||
t.Errorf("identity did not round-trip through parsing: got %q, want %q", i1, i)
|
||||
}
|
||||
|
||||
fileKey := make([]byte, 16)
|
||||
if _, err := rand.Read(fileKey); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
block, err := r.Wrap(fileKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := &bytes.Buffer{}
|
||||
(*format.Stanza)(block).Marshal(b)
|
||||
t.Logf("%s", b.Bytes())
|
||||
|
||||
out, err := i.Unwrap(block)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(fileKey, out) {
|
||||
t.Errorf("invalid output: %x, expected %x", out, fileKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScryptRoundTrip(t *testing.T) {
|
||||
password := "twitch.tv/filosottile"
|
||||
|
||||
r, err := age.NewScryptRecipient(password)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.SetWorkFactor(15)
|
||||
i, err := age.NewScryptIdentity(password)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if r.Type() != i.Type() || r.Type() != "scrypt" {
|
||||
t.Errorf("invalid Type values: %v, %v", r.Type(), i.Type())
|
||||
}
|
||||
|
||||
fileKey := make([]byte, 16)
|
||||
if _, err := rand.Read(fileKey); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
block, err := r.Wrap(fileKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := &bytes.Buffer{}
|
||||
(*format.Stanza)(block).Marshal(b)
|
||||
t.Logf("%s", b.Bytes())
|
||||
|
||||
out, err := i.Unwrap(block)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(fileKey, out) {
|
||||
t.Errorf("invalid output: %x, expected %x", out, fileKey)
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,14 @@ import (
|
||||
|
||||
const scryptLabel = "age-encryption.org/v1/scrypt"
|
||||
|
||||
// ScryptRecipient is a password-based recipient.
|
||||
//
|
||||
// If a ScryptRecipient is used, it must be the only recipient for the file: it
|
||||
// can't be mixed with other recipient types and can't be used multiple times
|
||||
// for the same file.
|
||||
//
|
||||
// Its use is not recommended for automated systems, which should prefer
|
||||
// X25519Recipient.
|
||||
type ScryptRecipient struct {
|
||||
password []byte
|
||||
workFactor int
|
||||
@@ -28,9 +36,10 @@ var _ Recipient = &ScryptRecipient{}
|
||||
|
||||
func (*ScryptRecipient) Type() string { return "scrypt" }
|
||||
|
||||
// NewScryptRecipient returns a new ScryptRecipient with the provided password.
|
||||
func NewScryptRecipient(password string) (*ScryptRecipient, error) {
|
||||
if len(password) == 0 {
|
||||
return nil, errors.New("empty scrypt password")
|
||||
return nil, errors.New("passphrase can't be empty")
|
||||
}
|
||||
r := &ScryptRecipient{
|
||||
password: []byte(password),
|
||||
@@ -42,6 +51,8 @@ func NewScryptRecipient(password string) (*ScryptRecipient, error) {
|
||||
|
||||
// SetWorkFactor sets the scrypt work factor to 2^logN.
|
||||
// It must be called before Wrap.
|
||||
//
|
||||
// If SetWorkFactor is not called, a reasonable default is used.
|
||||
func (r *ScryptRecipient) SetWorkFactor(logN int) {
|
||||
if logN > 30 || logN < 1 {
|
||||
panic("age: SetWorkFactor called with illegal value")
|
||||
@@ -49,14 +60,14 @@ func (r *ScryptRecipient) SetWorkFactor(logN int) {
|
||||
r.workFactor = logN
|
||||
}
|
||||
|
||||
func (r *ScryptRecipient) Wrap(fileKey []byte) (*format.Recipient, error) {
|
||||
func (r *ScryptRecipient) Wrap(fileKey []byte) (*Stanza, error) {
|
||||
salt := make([]byte, 16)
|
||||
if _, err := rand.Read(salt[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logN := r.workFactor
|
||||
l := &format.Recipient{
|
||||
l := &Stanza{
|
||||
Type: "scrypt",
|
||||
Args: []string{format.EncodeToString(salt), strconv.Itoa(logN)},
|
||||
}
|
||||
@@ -76,6 +87,7 @@ func (r *ScryptRecipient) Wrap(fileKey []byte) (*format.Recipient, error) {
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// ScryptIdentity is a password-based identity.
|
||||
type ScryptIdentity struct {
|
||||
password []byte
|
||||
maxWorkFactor int
|
||||
@@ -85,9 +97,10 @@ var _ Identity = &ScryptIdentity{}
|
||||
|
||||
func (*ScryptIdentity) Type() string { return "scrypt" }
|
||||
|
||||
// NewScryptIdentity returns a new ScryptIdentity with the provided password.
|
||||
func NewScryptIdentity(password string) (*ScryptIdentity, error) {
|
||||
if len(password) == 0 {
|
||||
return nil, errors.New("empty scrypt password")
|
||||
return nil, errors.New("passphrase can't be empty")
|
||||
}
|
||||
i := &ScryptIdentity{
|
||||
password: []byte(password),
|
||||
@@ -96,8 +109,12 @@ func NewScryptIdentity(password string) (*ScryptIdentity, error) {
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// SetWorkFactor sets the maximum accepted scrypt work factor to 2^logN.
|
||||
// SetMaxWorkFactor sets the maximum accepted scrypt work factor to 2^logN.
|
||||
// It must be called before Unwrap.
|
||||
//
|
||||
// This caps the amount of work that Decrypt might have to do to process
|
||||
// received files. If SetMaxWorkFactor is not called, a fairly high default is
|
||||
// used, which might not be suitable for systems processing untrusted files.
|
||||
func (i *ScryptIdentity) SetMaxWorkFactor(logN int) {
|
||||
if logN > 30 || logN < 1 {
|
||||
panic("age: SetMaxWorkFactor called with illegal value")
|
||||
@@ -105,7 +122,7 @@ func (i *ScryptIdentity) SetMaxWorkFactor(logN int) {
|
||||
i.maxWorkFactor = logN
|
||||
}
|
||||
|
||||
func (i *ScryptIdentity) Unwrap(block *format.Recipient) ([]byte, error) {
|
||||
func (i *ScryptIdentity) Unwrap(block *Stanza) ([]byte, error) {
|
||||
if block.Type != "scrypt" {
|
||||
return nil, ErrIncorrectIdentity
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
|
||||
const x25519Label = "age-encryption.org/v1/X25519"
|
||||
|
||||
// X25519Recipient is the standard age public key, based on a Curve25519 point.
|
||||
type X25519Recipient struct {
|
||||
theirPublicKey []byte
|
||||
}
|
||||
@@ -31,7 +32,8 @@ var _ Recipient = &X25519Recipient{}
|
||||
|
||||
func (*X25519Recipient) Type() string { return "X25519" }
|
||||
|
||||
func NewX25519Recipient(publicKey []byte) (*X25519Recipient, error) {
|
||||
// newX25519RecipientFromPoint returns a new X25519Recipient from a raw Curve25519 point.
|
||||
func newX25519RecipientFromPoint(publicKey []byte) (*X25519Recipient, error) {
|
||||
if len(publicKey) != curve25519.PointSize {
|
||||
return nil, errors.New("invalid X25519 public key")
|
||||
}
|
||||
@@ -42,6 +44,8 @@ func NewX25519Recipient(publicKey []byte) (*X25519Recipient, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// ParseX25519Recipient returns a new X25519Recipient from a Bech32 public key
|
||||
// encoding with the "age1" prefix.
|
||||
func ParseX25519Recipient(s string) (*X25519Recipient, error) {
|
||||
t, k, err := bech32.Decode(s)
|
||||
if err != nil {
|
||||
@@ -50,14 +54,14 @@ func ParseX25519Recipient(s string) (*X25519Recipient, error) {
|
||||
if t != "age" {
|
||||
return nil, fmt.Errorf("malformed recipient %q: invalid type %q", s, t)
|
||||
}
|
||||
r, err := NewX25519Recipient(k)
|
||||
r, err := newX25519RecipientFromPoint(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed recipient %q: %v", s, err)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *X25519Recipient) Wrap(fileKey []byte) (*format.Recipient, error) {
|
||||
func (r *X25519Recipient) Wrap(fileKey []byte) (*Stanza, error) {
|
||||
ephemeral := make([]byte, curve25519.ScalarSize)
|
||||
if _, err := rand.Read(ephemeral); err != nil {
|
||||
return nil, err
|
||||
@@ -72,7 +76,7 @@ func (r *X25519Recipient) Wrap(fileKey []byte) (*format.Recipient, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l := &format.Recipient{
|
||||
l := &Stanza{
|
||||
Type: "X25519",
|
||||
Args: []string{format.EncodeToString(ourPublicKey)},
|
||||
}
|
||||
@@ -95,11 +99,13 @@ func (r *X25519Recipient) Wrap(fileKey []byte) (*format.Recipient, error) {
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// String returns the Bech32 public key encoding of r.
|
||||
func (r *X25519Recipient) String() string {
|
||||
s, _ := bech32.Encode("age", r.theirPublicKey)
|
||||
return s
|
||||
}
|
||||
|
||||
// X25519Identity is the standard age private key, based on a Curve25519 scalar.
|
||||
type X25519Identity struct {
|
||||
secretKey, ourPublicKey []byte
|
||||
}
|
||||
@@ -108,7 +114,8 @@ var _ Identity = &X25519Identity{}
|
||||
|
||||
func (*X25519Identity) Type() string { return "X25519" }
|
||||
|
||||
func NewX25519Identity(secretKey []byte) (*X25519Identity, error) {
|
||||
// newX25519IdentityFromScalar returns a new X25519Identity from a raw Curve25519 scalar.
|
||||
func newX25519IdentityFromScalar(secretKey []byte) (*X25519Identity, error) {
|
||||
if len(secretKey) != curve25519.ScalarSize {
|
||||
return nil, errors.New("invalid X25519 secret key")
|
||||
}
|
||||
@@ -120,14 +127,17 @@ func NewX25519Identity(secretKey []byte) (*X25519Identity, error) {
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// GenerateX25519Identity randomly generates a new X25519Identity.
|
||||
func GenerateX25519Identity() (*X25519Identity, error) {
|
||||
secretKey := make([]byte, curve25519.ScalarSize)
|
||||
if _, err := rand.Read(secretKey); err != nil {
|
||||
return nil, fmt.Errorf("internal error: %v", err)
|
||||
}
|
||||
return NewX25519Identity(secretKey)
|
||||
return newX25519IdentityFromScalar(secretKey)
|
||||
}
|
||||
|
||||
// ParseX25519Identity returns a new X25519Recipient from a Bech32 private key
|
||||
// encoding with the "AGE-SECRET-KEY-1" prefix.
|
||||
func ParseX25519Identity(s string) (*X25519Identity, error) {
|
||||
t, k, err := bech32.Decode(s)
|
||||
if err != nil {
|
||||
@@ -136,14 +146,14 @@ func ParseX25519Identity(s string) (*X25519Identity, error) {
|
||||
if t != "AGE-SECRET-KEY-" {
|
||||
return nil, fmt.Errorf("malformed secret key %q: invalid type %q", s, t)
|
||||
}
|
||||
r, err := NewX25519Identity(k)
|
||||
r, err := newX25519IdentityFromScalar(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed secret key %q: %v", s, err)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (i *X25519Identity) Unwrap(block *format.Recipient) ([]byte, error) {
|
||||
func (i *X25519Identity) Unwrap(block *Stanza) ([]byte, error) {
|
||||
if block.Type != "X25519" {
|
||||
return nil, ErrIncorrectIdentity
|
||||
}
|
||||
@@ -179,12 +189,14 @@ func (i *X25519Identity) Unwrap(block *format.Recipient) ([]byte, error) {
|
||||
return fileKey, nil
|
||||
}
|
||||
|
||||
// Recipient returns the public X25519Recipient value corresponding to i.
|
||||
func (i *X25519Identity) Recipient() *X25519Recipient {
|
||||
r := &X25519Recipient{}
|
||||
r.theirPublicKey = i.ourPublicKey
|
||||
return r
|
||||
}
|
||||
|
||||
// String returns the Bech32 private key encoding of i.
|
||||
func (i *X25519Identity) String() string {
|
||||
s, _ := bech32.Encode("AGE-SECRET-KEY-", i.secretKey)
|
||||
return strings.ToUpper(s)
|
||||
@@ -4,7 +4,13 @@
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
package age
|
||||
// Package agessh provides age.Identity and age.Recipient implementations of
|
||||
// types "ssh-rsa" and "ssh-ed25519", which allow reusing existing SSH key files
|
||||
// for encryption with age-encryption.org/v1.
|
||||
//
|
||||
// These should only be used for compatibility with existing keys, and native
|
||||
// X25519 keys should be preferred otherwise.
|
||||
package agessh
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
@@ -17,6 +23,7 @@ import (
|
||||
"io"
|
||||
"math/big"
|
||||
|
||||
"filippo.io/age/age"
|
||||
"filippo.io/age/internal/format"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
@@ -24,29 +31,27 @@ import (
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func SSHFingerprint(pk ssh.PublicKey) string {
|
||||
h := sha256.New()
|
||||
h.Write(pk.Marshal())
|
||||
hh := h.Sum(nil)
|
||||
return format.EncodeToString(hh[:4])
|
||||
func sshFingerprint(pk ssh.PublicKey) string {
|
||||
h := sha256.Sum256(pk.Marshal())
|
||||
return format.EncodeToString(h[:4])
|
||||
}
|
||||
|
||||
const oaepLabel = "age-encryption.org/v1/ssh-rsa"
|
||||
|
||||
type SSHRSARecipient struct {
|
||||
type RSARecipient struct {
|
||||
sshKey ssh.PublicKey
|
||||
pubKey *rsa.PublicKey
|
||||
}
|
||||
|
||||
var _ Recipient = &SSHRSARecipient{}
|
||||
var _ age.Recipient = &RSARecipient{}
|
||||
|
||||
func (*SSHRSARecipient) Type() string { return "ssh-rsa" }
|
||||
func (*RSARecipient) Type() string { return "ssh-rsa" }
|
||||
|
||||
func NewSSHRSARecipient(pk ssh.PublicKey) (*SSHRSARecipient, error) {
|
||||
func NewRSARecipient(pk ssh.PublicKey) (*RSARecipient, error) {
|
||||
if pk.Type() != "ssh-rsa" {
|
||||
return nil, errors.New("SSH public key is not an RSA key")
|
||||
}
|
||||
r := &SSHRSARecipient{
|
||||
r := &RSARecipient{
|
||||
sshKey: pk,
|
||||
}
|
||||
|
||||
@@ -62,10 +67,10 @@ func NewSSHRSARecipient(pk ssh.PublicKey) (*SSHRSARecipient, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *SSHRSARecipient) Wrap(fileKey []byte) (*format.Recipient, error) {
|
||||
l := &format.Recipient{
|
||||
func (r *RSARecipient) Wrap(fileKey []byte) (*age.Stanza, error) {
|
||||
l := &age.Stanza{
|
||||
Type: "ssh-rsa",
|
||||
Args: []string{SSHFingerprint(r.sshKey)},
|
||||
Args: []string{sshFingerprint(r.sshKey)},
|
||||
}
|
||||
|
||||
wrappedKey, err := rsa.EncryptOAEP(sha256.New(), rand.Reader,
|
||||
@@ -78,36 +83,36 @@ func (r *SSHRSARecipient) Wrap(fileKey []byte) (*format.Recipient, error) {
|
||||
return l, nil
|
||||
}
|
||||
|
||||
type SSHRSAIdentity struct {
|
||||
type RSAIdentity struct {
|
||||
k *rsa.PrivateKey
|
||||
sshKey ssh.PublicKey
|
||||
}
|
||||
|
||||
var _ Identity = &SSHRSAIdentity{}
|
||||
var _ age.Identity = &RSAIdentity{}
|
||||
|
||||
func (*SSHRSAIdentity) Type() string { return "ssh-rsa" }
|
||||
func (*RSAIdentity) Type() string { return "ssh-rsa" }
|
||||
|
||||
func NewSSHRSAIdentity(key *rsa.PrivateKey) (*SSHRSAIdentity, error) {
|
||||
func NewRSAIdentity(key *rsa.PrivateKey) (*RSAIdentity, error) {
|
||||
s, err := ssh.NewSignerFromKey(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i := &SSHRSAIdentity{
|
||||
i := &RSAIdentity{
|
||||
k: key, sshKey: s.PublicKey(),
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (i *SSHRSAIdentity) Unwrap(block *format.Recipient) ([]byte, error) {
|
||||
func (i *RSAIdentity) Unwrap(block *age.Stanza) ([]byte, error) {
|
||||
if block.Type != "ssh-rsa" {
|
||||
return nil, ErrIncorrectIdentity
|
||||
return nil, age.ErrIncorrectIdentity
|
||||
}
|
||||
if len(block.Args) != 1 {
|
||||
return nil, errors.New("invalid ssh-rsa recipient block")
|
||||
}
|
||||
|
||||
if block.Args[0] != SSHFingerprint(i.sshKey) {
|
||||
return nil, ErrIncorrectIdentity
|
||||
if block.Args[0] != sshFingerprint(i.sshKey) {
|
||||
return nil, age.ErrIncorrectIdentity
|
||||
}
|
||||
|
||||
fileKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, i.k,
|
||||
@@ -118,20 +123,20 @@ func (i *SSHRSAIdentity) Unwrap(block *format.Recipient) ([]byte, error) {
|
||||
return fileKey, nil
|
||||
}
|
||||
|
||||
type SSHEd25519Recipient struct {
|
||||
type Ed25519Recipient struct {
|
||||
sshKey ssh.PublicKey
|
||||
theirPublicKey []byte
|
||||
}
|
||||
|
||||
var _ Recipient = &SSHEd25519Recipient{}
|
||||
var _ age.Recipient = &Ed25519Recipient{}
|
||||
|
||||
func (*SSHEd25519Recipient) Type() string { return "ssh-ed25519" }
|
||||
func (*Ed25519Recipient) Type() string { return "ssh-ed25519" }
|
||||
|
||||
func NewSSHEd25519Recipient(pk ssh.PublicKey) (*SSHEd25519Recipient, error) {
|
||||
func NewEd25519Recipient(pk ssh.PublicKey) (*Ed25519Recipient, error) {
|
||||
if pk.Type() != "ssh-ed25519" {
|
||||
return nil, errors.New("SSH public key is not an Ed25519 key")
|
||||
}
|
||||
r := &SSHEd25519Recipient{
|
||||
r := &Ed25519Recipient{
|
||||
sshKey: pk,
|
||||
}
|
||||
|
||||
@@ -147,18 +152,18 @@ func NewSSHEd25519Recipient(pk ssh.PublicKey) (*SSHEd25519Recipient, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func ParseSSHRecipient(s string) (Recipient, error) {
|
||||
func ParseRecipient(s string) (age.Recipient, error) {
|
||||
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(s))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed SSH recipient: %q: %v", s, err)
|
||||
}
|
||||
|
||||
var r Recipient
|
||||
var r age.Recipient
|
||||
switch t := pubKey.Type(); t {
|
||||
case "ssh-rsa":
|
||||
r, err = NewSSHRSARecipient(pubKey)
|
||||
r, err = NewRSARecipient(pubKey)
|
||||
case "ssh-ed25519":
|
||||
r, err = NewSSHEd25519Recipient(pubKey)
|
||||
r, err = NewEd25519Recipient(pubKey)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown SSH recipient type: %q", t)
|
||||
}
|
||||
@@ -202,7 +207,7 @@ func ed25519PublicKeyToCurve25519(pk ed25519.PublicKey) []byte {
|
||||
|
||||
const ed25519Label = "age-encryption.org/v1/ssh-ed25519"
|
||||
|
||||
func (r *SSHEd25519Recipient) Wrap(fileKey []byte) (*format.Recipient, error) {
|
||||
func (r *Ed25519Recipient) Wrap(fileKey []byte) (*age.Stanza, error) {
|
||||
ephemeral := make([]byte, curve25519.ScalarSize)
|
||||
if _, err := rand.Read(ephemeral); err != nil {
|
||||
return nil, err
|
||||
@@ -224,9 +229,9 @@ func (r *SSHEd25519Recipient) Wrap(fileKey []byte) (*format.Recipient, error) {
|
||||
}
|
||||
sharedSecret, _ = curve25519.X25519(tweak, sharedSecret)
|
||||
|
||||
l := &format.Recipient{
|
||||
l := &age.Stanza{
|
||||
Type: "ssh-ed25519",
|
||||
Args: []string{SSHFingerprint(r.sshKey),
|
||||
Args: []string{sshFingerprint(r.sshKey),
|
||||
format.EncodeToString(ourPublicKey[:])},
|
||||
}
|
||||
|
||||
@@ -248,21 +253,21 @@ func (r *SSHEd25519Recipient) Wrap(fileKey []byte) (*format.Recipient, error) {
|
||||
return l, nil
|
||||
}
|
||||
|
||||
type SSHEd25519Identity struct {
|
||||
type Ed25519Identity struct {
|
||||
secretKey, ourPublicKey []byte
|
||||
sshKey ssh.PublicKey
|
||||
}
|
||||
|
||||
var _ Identity = &SSHEd25519Identity{}
|
||||
var _ age.Identity = &Ed25519Identity{}
|
||||
|
||||
func (*SSHEd25519Identity) Type() string { return "ssh-ed25519" }
|
||||
func (*Ed25519Identity) Type() string { return "ssh-ed25519" }
|
||||
|
||||
func NewSSHEd25519Identity(key ed25519.PrivateKey) (*SSHEd25519Identity, error) {
|
||||
func NewEd25519Identity(key ed25519.PrivateKey) (*Ed25519Identity, error) {
|
||||
s, err := ssh.NewSignerFromKey(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i := &SSHEd25519Identity{
|
||||
i := &Ed25519Identity{
|
||||
sshKey: s.PublicKey(),
|
||||
secretKey: ed25519PrivateKeyToCurve25519(key),
|
||||
}
|
||||
@@ -270,7 +275,7 @@ func NewSSHEd25519Identity(key ed25519.PrivateKey) (*SSHEd25519Identity, error)
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func ParseSSHIdentity(pemBytes []byte) (Identity, error) {
|
||||
func ParseIdentity(pemBytes []byte) (age.Identity, error) {
|
||||
k, err := ssh.ParseRawPrivateKey(pemBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -278,9 +283,9 @@ func ParseSSHIdentity(pemBytes []byte) (Identity, error) {
|
||||
|
||||
switch k := k.(type) {
|
||||
case *ed25519.PrivateKey:
|
||||
return NewSSHEd25519Identity(*k)
|
||||
return NewEd25519Identity(*k)
|
||||
case *rsa.PrivateKey:
|
||||
return NewSSHRSAIdentity(k)
|
||||
return NewRSAIdentity(k)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported SSH identity type: %T", k)
|
||||
@@ -293,9 +298,9 @@ func ed25519PrivateKeyToCurve25519(pk ed25519.PrivateKey) []byte {
|
||||
return out[:curve25519.ScalarSize]
|
||||
}
|
||||
|
||||
func (i *SSHEd25519Identity) Unwrap(block *format.Recipient) ([]byte, error) {
|
||||
func (i *Ed25519Identity) Unwrap(block *age.Stanza) ([]byte, error) {
|
||||
if block.Type != "ssh-ed25519" {
|
||||
return nil, ErrIncorrectIdentity
|
||||
return nil, age.ErrIncorrectIdentity
|
||||
}
|
||||
if len(block.Args) != 2 {
|
||||
return nil, errors.New("invalid ssh-ed25519 recipient block")
|
||||
@@ -308,8 +313,8 @@ func (i *SSHEd25519Identity) Unwrap(block *format.Recipient) ([]byte, error) {
|
||||
return nil, errors.New("invalid ssh-ed25519 recipient block")
|
||||
}
|
||||
|
||||
if block.Args[0] != SSHFingerprint(i.sshKey) {
|
||||
return nil, ErrIncorrectIdentity
|
||||
if block.Args[0] != sshFingerprint(i.sshKey) {
|
||||
return nil, age.ErrIncorrectIdentity
|
||||
}
|
||||
|
||||
sharedSecret, err := curve25519.X25519(i.secretKey, publicKey)
|
||||
@@ -339,3 +344,23 @@ func (i *SSHEd25519Identity) Unwrap(block *format.Recipient) ([]byte, error) {
|
||||
}
|
||||
return fileKey, nil
|
||||
}
|
||||
|
||||
// aeadEncrypt and aeadDecrypt are copied from package age.
|
||||
|
||||
func aeadEncrypt(key, plaintext []byte) ([]byte, error) {
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nonce := make([]byte, chacha20poly1305.NonceSize)
|
||||
return aead.Seal(nil, nonce, plaintext, nil), nil
|
||||
}
|
||||
|
||||
func aeadDecrypt(key, ciphertext []byte) ([]byte, error) {
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nonce := make([]byte, chacha20poly1305.NonceSize)
|
||||
return aead.Open(nil, nonce, ciphertext, nil)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// 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 agessh_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age/agessh"
|
||||
"filippo.io/age/internal/format"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func TestSSHRSARoundTrip(t *testing.T) {
|
||||
pk, err := rsa.GenerateKey(rand.Reader, 768)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pub, err := ssh.NewPublicKey(&pk.PublicKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r, err := agessh.NewRSARecipient(pub)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
i, err := agessh.NewRSAIdentity(pk)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if r.Type() != i.Type() || r.Type() != "ssh-rsa" {
|
||||
t.Errorf("invalid Type values: %v, %v", r.Type(), i.Type())
|
||||
}
|
||||
|
||||
fileKey := make([]byte, 16)
|
||||
if _, err := rand.Read(fileKey); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
block, err := r.Wrap(fileKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := &bytes.Buffer{}
|
||||
(*format.Stanza)(block).Marshal(b)
|
||||
t.Logf("%s", b.Bytes())
|
||||
|
||||
out, err := i.Unwrap(block)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(fileKey, out) {
|
||||
t.Errorf("invalid output: %x, expected %x", out, fileKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSHEd25519RoundTrip(t *testing.T) {
|
||||
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sshPubKey, err := ssh.NewPublicKey(pub)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r, err := agessh.NewEd25519Recipient(sshPubKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
i, err := agessh.NewEd25519Identity(priv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if r.Type() != i.Type() || r.Type() != "ssh-ed25519" {
|
||||
t.Errorf("invalid Type values: %v, %v", r.Type(), i.Type())
|
||||
}
|
||||
|
||||
fileKey := make([]byte, 16)
|
||||
if _, err := rand.Read(fileKey); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
block, err := r.Wrap(fileKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := &bytes.Buffer{}
|
||||
(*format.Stanza)(block).Marshal(b)
|
||||
t.Logf("%s", b.Bytes())
|
||||
|
||||
out, err := i.Unwrap(block)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(fileKey, out) {
|
||||
t.Errorf("invalid output: %x, expected %x", out, fileKey)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// 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 agessh
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
|
||||
"filippo.io/age/age"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// EncryptedSSHIdentity is an age.IdentityMatcher implementation based on a
|
||||
// passphrase encrypted SSH private key.
|
||||
//
|
||||
// It provides public key based matching and deferred decryption so the
|
||||
// passphrase is only requested if necessary. If the application knows it will
|
||||
// unconditionally have to decrypt the private key, it would be simpler to use
|
||||
// ssh.ParseRawPrivateKeyWithPassphrase directly and pass the result to
|
||||
// NewEd25519Identity or NewRSAIdentity.
|
||||
type EncryptedSSHIdentity struct {
|
||||
pubKey ssh.PublicKey
|
||||
pemBytes []byte
|
||||
passphrase func() ([]byte, error)
|
||||
|
||||
decrypted age.Identity
|
||||
}
|
||||
|
||||
// NewEncryptedSSHIdentity returns a new EncryptedSSHIdentity.
|
||||
//
|
||||
// pubKey must be the public key associated with the encrypted private key, and
|
||||
// it must have type "ssh-ed25519" or "ssh-rsa". For OpenSSH encrypted files it
|
||||
// can be extracted from an ssh.PassphraseMissingError, otherwise in can often
|
||||
// be found in ".pub" files.
|
||||
//
|
||||
// pemBytes must be a valid input to ssh.ParseRawPrivateKeyWithPassphrase.
|
||||
// passphrase is a callback that will be invoked by Unwrap when the passphrase
|
||||
// is necessary.
|
||||
func NewEncryptedSSHIdentity(pubKey ssh.PublicKey, pemBytes []byte, passphrase func() ([]byte, error)) (*EncryptedSSHIdentity, error) {
|
||||
switch t := pubKey.Type(); t {
|
||||
case "ssh-ed25519", "ssh-rsa":
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported SSH key type: %v", t)
|
||||
}
|
||||
return &EncryptedSSHIdentity{
|
||||
pubKey: pubKey,
|
||||
pemBytes: pemBytes,
|
||||
passphrase: passphrase,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ age.IdentityMatcher = &EncryptedSSHIdentity{}
|
||||
|
||||
// Type returns the type of the underlying private key, "ssh-ed25519" or "ssh-rsa".
|
||||
func (i *EncryptedSSHIdentity) Type() string {
|
||||
return i.pubKey.Type()
|
||||
}
|
||||
|
||||
// Unwrap implements age.Identity. If the private key is still encrypted, it
|
||||
// will request the passphrase. The decrypted private key will be cached after
|
||||
// the first successful invocation.
|
||||
func (i *EncryptedSSHIdentity) Unwrap(block *age.Stanza) (fileKey []byte, err error) {
|
||||
if i.decrypted != nil {
|
||||
return i.decrypted.Unwrap(block)
|
||||
}
|
||||
|
||||
passphrase, err := i.passphrase()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to obtain passphrase: %v", err)
|
||||
}
|
||||
k, err := ssh.ParseRawPrivateKeyWithPassphrase(i.pemBytes, passphrase)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt SSH key file: %v", err)
|
||||
}
|
||||
|
||||
switch k := k.(type) {
|
||||
case *ed25519.PrivateKey:
|
||||
i.decrypted, err = NewEd25519Identity(*k)
|
||||
case *rsa.PrivateKey:
|
||||
i.decrypted, err = NewRSAIdentity(k)
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected SSH key type: %T", k)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid SSH key: %v", err)
|
||||
}
|
||||
if i.decrypted.Type() != i.pubKey.Type() {
|
||||
return nil, fmt.Errorf("mismatched SSH key type: got %q, expected %q", i.decrypted.Type(), i.pubKey.Type())
|
||||
}
|
||||
|
||||
return i.decrypted.Unwrap(block)
|
||||
}
|
||||
|
||||
// Match implements age.IdentityMatcher without decrypting the private key, to
|
||||
// ensure the passphrase is only obtained if necessary.
|
||||
func (i *EncryptedSSHIdentity) Match(block *age.Stanza) error {
|
||||
if block.Type != i.Type() {
|
||||
return age.ErrIncorrectIdentity
|
||||
}
|
||||
if len(block.Args) < 1 {
|
||||
return fmt.Errorf("invalid %v recipient block", i.Type())
|
||||
}
|
||||
|
||||
if block.Args[0] != sshFingerprint(i.pubKey) {
|
||||
return age.ErrIncorrectIdentity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -4,7 +4,12 @@
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
package format
|
||||
// Package armor provides a strict, streaming implementation of the ASCII
|
||||
// armoring format for age files.
|
||||
//
|
||||
// It's PEM with type "AGE ENCRYPTED FILE", 64 character columns, no headers,
|
||||
// and strict base64 decoding.
|
||||
package armor
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -12,50 +17,14 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"filippo.io/age/internal/format"
|
||||
)
|
||||
|
||||
type newlineWriter struct {
|
||||
dst io.Writer
|
||||
written int
|
||||
}
|
||||
|
||||
func (w *newlineWriter) Write(p []byte) (n int, err error) {
|
||||
for len(p) > 0 {
|
||||
remainingInLine := columnsPerLine - (w.written % columnsPerLine)
|
||||
if remainingInLine == columnsPerLine && w.written != 0 {
|
||||
if _, err := w.dst.Write([]byte("\n")); err != nil {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
toWrite := remainingInLine
|
||||
if toWrite > len(p) {
|
||||
toWrite = len(p)
|
||||
}
|
||||
nn, err := w.dst.Write(p[:toWrite])
|
||||
n += nn
|
||||
w.written += nn
|
||||
p = p[nn:]
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
type CloserFunc func() error
|
||||
|
||||
func (f CloserFunc) Close() error { return f() }
|
||||
|
||||
type nopCloser struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (nopCloser) Close() error { return nil }
|
||||
|
||||
func NopCloser(w io.Writer) io.WriteCloser { return nopCloser{w} }
|
||||
|
||||
const armorPreamble = "-----BEGIN AGE ENCRYPTED FILE-----"
|
||||
const armorEnd = "-----END AGE ENCRYPTED FILE-----"
|
||||
const (
|
||||
Header = "-----BEGIN AGE ENCRYPTED FILE-----"
|
||||
Footer = "-----END AGE ENCRYPTED FILE-----"
|
||||
)
|
||||
|
||||
type armoredWriter struct {
|
||||
started, closed bool
|
||||
@@ -65,7 +34,7 @@ type armoredWriter struct {
|
||||
|
||||
func (a *armoredWriter) Write(p []byte) (int, error) {
|
||||
if !a.started {
|
||||
if _, err := io.WriteString(a.dst, armorPreamble+"\n"); err != nil {
|
||||
if _, err := io.WriteString(a.dst, Header+"\n"); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
@@ -81,26 +50,26 @@ func (a *armoredWriter) Close() error {
|
||||
if err := a.encoder.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := io.WriteString(a.dst, "\n"+armorEnd+"\n")
|
||||
_, err := io.WriteString(a.dst, "\n"+Footer+"\n")
|
||||
return err
|
||||
}
|
||||
|
||||
func ArmoredWriter(dst io.Writer) io.WriteCloser {
|
||||
func NewWriter(dst io.Writer) io.WriteCloser {
|
||||
// TODO: write a test with aligned and misaligned sizes, and 8 and 10 steps.
|
||||
return &armoredWriter{dst: dst,
|
||||
encoder: base64.NewEncoder(base64.StdEncoding.Strict(),
|
||||
&newlineWriter{dst: dst})}
|
||||
format.NewlineWriter(dst))}
|
||||
}
|
||||
|
||||
type armoredReader struct {
|
||||
r *bufio.Reader
|
||||
started bool
|
||||
unread []byte // backed by buf
|
||||
buf [bytesPerLine]byte
|
||||
buf [format.BytesPerLine]byte
|
||||
err error
|
||||
}
|
||||
|
||||
func ArmoredReader(r io.Reader) io.Reader {
|
||||
func NewReader(r io.Reader) io.Reader {
|
||||
return &armoredReader{r: bufio.NewReader(r)}
|
||||
}
|
||||
|
||||
@@ -130,7 +99,7 @@ func (r *armoredReader) Read(p []byte) (int, error) {
|
||||
if err != nil {
|
||||
return 0, r.setErr(err)
|
||||
}
|
||||
if string(line) != armorPreamble {
|
||||
if string(line) != Header {
|
||||
return 0, r.setErr(errors.New("invalid armor first line: " + string(line)))
|
||||
}
|
||||
r.started = true
|
||||
@@ -139,25 +108,25 @@ func (r *armoredReader) Read(p []byte) (int, error) {
|
||||
if err != nil {
|
||||
return 0, r.setErr(err)
|
||||
}
|
||||
if string(line) == armorEnd {
|
||||
if string(line) == Footer {
|
||||
return 0, r.setErr(io.EOF)
|
||||
}
|
||||
if len(line) > columnsPerLine {
|
||||
if len(line) > format.ColumnsPerLine {
|
||||
return 0, r.setErr(errors.New("invalid armor: column limit exceeded"))
|
||||
}
|
||||
r.unread = r.buf[:]
|
||||
n, err := base64.StdEncoding.Strict().Decode(r.unread, line)
|
||||
if err != nil {
|
||||
return 0, r.setErr(err)
|
||||
return 0, r.setErr(errors.New("invalid armor: " + err.Error()))
|
||||
}
|
||||
r.unread = r.unread[:n]
|
||||
|
||||
if n < bytesPerLine {
|
||||
if n < format.BytesPerLine {
|
||||
line, err := getLine()
|
||||
if err != nil {
|
||||
return 0, r.setErr(err)
|
||||
}
|
||||
if string(line) != armorEnd {
|
||||
if string(line) != Footer {
|
||||
return 0, r.setErr(errors.New("invalid armor closing line: " + string(line)))
|
||||
}
|
||||
r.err = io.EOF
|
||||
@@ -0,0 +1,116 @@
|
||||
// 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 armor_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age/age"
|
||||
"filippo.io/age/armor"
|
||||
)
|
||||
|
||||
func ExampleNewWriter() {
|
||||
publicKey := "age1cy0su9fwf3gf9mw868g5yut09p6nytfmmnktexz2ya5uqg9vl9sss4euqm"
|
||||
recipient, err := age.ParseX25519Recipient(publicKey)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse public key %q: %v", publicKey, err)
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
armorWriter := armor.NewWriter(buf)
|
||||
|
||||
w, err := age.Encrypt(armorWriter, recipient)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create encrypted file: %v", err)
|
||||
}
|
||||
if _, err := io.WriteString(w, "Black lives matter."); err != nil {
|
||||
log.Fatalf("Failed to write to encrypted file: %v", err)
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
log.Fatalf("Failed to close encrypted file: %v", err)
|
||||
}
|
||||
|
||||
if err := armorWriter.Close(); err != nil {
|
||||
log.Fatalf("Failed to close armor: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s[...]", buf.Bytes()[:35])
|
||||
// Output:
|
||||
// -----BEGIN AGE ENCRYPTED FILE-----
|
||||
// [...]
|
||||
}
|
||||
|
||||
var privateKey = "AGE-SECRET-KEY-184JMZMVQH3E6U0PSL869004Y3U2NYV7R30EU99CSEDNPH02YUVFSZW44VU"
|
||||
|
||||
func ExampleNewReader() {
|
||||
fileContents := `-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4YWdhZHZ0WG1PZldDT1hD
|
||||
K3RPRzFkUlJnWlFBQlUwemtjeXFRMFp6V1VFCnRzZFV3a3Vkd1dSUWw2eEtrRkVv
|
||||
SHcvZnp6Q3lqLy9HMkM4ZjUyUGdDZjQKLS0tIDlpVUpuVUQ5YUJyUENFZ0lNSTB2
|
||||
ekUvS3E5WjVUN0F5ZWR1ejhpeU5rZUUKsvPGYt7vf0o1kyJ1eVFMz1e4JnYYk1y1
|
||||
kB/RRusYjn+KVJ+KTioxj0THtzZPXcjFKuQ1
|
||||
-----END AGE ENCRYPTED FILE-----`
|
||||
|
||||
// DO NOT hardcode the private key. Store it in a secret storage solution,
|
||||
// on disk if the local machine is trusted, or have the user provide it.
|
||||
identity, err := age.ParseX25519Identity(privateKey)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse private key %q: %v", privateKey, err)
|
||||
}
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
f := strings.NewReader(fileContents)
|
||||
armorReader := armor.NewReader(f)
|
||||
|
||||
r, err := age.Decrypt(armorReader, identity)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open encrypted file: %v", err)
|
||||
}
|
||||
if _, err := io.Copy(out, r); err != nil {
|
||||
log.Fatalf("Failed to read encrypted file: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("File contents: %q\n", out.Bytes())
|
||||
// Output:
|
||||
// File contents: "Black lives matter."
|
||||
}
|
||||
|
||||
func TestArmor(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := armor.NewWriter(buf)
|
||||
plain := make([]byte, 611)
|
||||
if _, err := w.Write(plain); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(buf.Bytes())
|
||||
if block == nil {
|
||||
t.Fatal("PEM decoding failed")
|
||||
}
|
||||
if !bytes.Equal(block.Bytes, plain) {
|
||||
t.Error("PEM decoded value doesn't match")
|
||||
}
|
||||
|
||||
r := armor.NewReader(buf)
|
||||
out, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(out, plain) {
|
||||
t.Error("decoded value doesn't match")
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,12 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"filippo.io/age/internal/age"
|
||||
"filippo.io/age/age"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -38,20 +38,24 @@ func main() {
|
||||
|
||||
if fi, err := out.Stat(); err == nil {
|
||||
if fi.Mode().IsRegular() && fi.Mode().Perm()&0004 != 0 {
|
||||
fmt.Fprintf(os.Stderr, "Warning: writing to a world-readable file.\n")
|
||||
fmt.Fprintf(os.Stderr, "Consider setting the umask to 066 and trying again.\n")
|
||||
fmt.Fprintf(os.Stderr, "Warning: writing to a world-readable file.\n"+
|
||||
"Consider setting the umask to 066 and trying again.\n")
|
||||
}
|
||||
}
|
||||
|
||||
generate(out)
|
||||
}
|
||||
|
||||
func generate(out io.Writer) {
|
||||
func generate(out *os.File) {
|
||||
k, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
log.Fatalf("Internal error: %v", err)
|
||||
}
|
||||
|
||||
if !terminal.IsTerminal(int(out.Fd())) {
|
||||
fmt.Fprintf(os.Stderr, "Public key: %s\n", k.Recipient())
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "# created: %s\n", time.Now().Format(time.RFC3339))
|
||||
fmt.Fprintf(out, "# public key: %s\n", k.Recipient())
|
||||
fmt.Fprintf(out, "%s\n", k)
|
||||
|
||||
+83
-9
@@ -7,14 +7,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
_log "log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"filippo.io/age/internal/age"
|
||||
"filippo.io/age/age"
|
||||
"filippo.io/age/armor"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
@@ -27,8 +30,36 @@ func (f *multiFlag) Set(value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const usage = `Usage:
|
||||
age -r RECIPIENT [-a] [-o OUTPUT] [INPUT]
|
||||
age --decrypt [-i KEY] [-o OUTPUT] [INPUT]
|
||||
|
||||
Options:
|
||||
-o, --output OUTPUT Write the result to the file at path OUTPUT.
|
||||
-a, --armor Encrypt to a PEM encoded format.
|
||||
-p, --passphrase Encrypt with a passphrase.
|
||||
-r, --recipient RECIPIENT Encrypt to the specified RECIPIENT. Can be repeated.
|
||||
-d, --decrypt Decrypt the input to the output.
|
||||
-i, --identity KEY Use the private key file at path KEY. Can be repeated.
|
||||
|
||||
INPUT defaults to standard input, and OUTPUT defaults to standard output.
|
||||
|
||||
RECIPIENT can be an age public key, as generated by age-keygen, ("age1...")
|
||||
or an SSH public key ("ssh-ed25519 AAAA...", "ssh-rsa AAAA...").
|
||||
|
||||
KEY is a path to a file with age secret keys, one per line
|
||||
(ignoring "#" prefixed comments and empty lines), or to an SSH key file.
|
||||
Multiple keys can be provided, and any unused ones will be ignored.
|
||||
|
||||
Example:
|
||||
$ age-keygen -o key.txt
|
||||
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
|
||||
$ tar cvz ~/data | age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p > data.tar.gz.age
|
||||
$ age -d -i key.txt -o data.tar.gz data.tar.gz.age`
|
||||
|
||||
func main() {
|
||||
_log.SetFlags(0)
|
||||
flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", usage) }
|
||||
|
||||
var (
|
||||
outFlag string
|
||||
@@ -41,6 +72,7 @@ func main() {
|
||||
flag.BoolVar(&passFlag, "p", false, "use a passphrase")
|
||||
flag.BoolVar(&passFlag, "passphrase", false, "use a passphrase")
|
||||
flag.StringVar(&outFlag, "o", "", "output to `FILE` (default stdout)")
|
||||
flag.StringVar(&outFlag, "output", "", "output to `FILE` (default stdout)")
|
||||
flag.BoolVar(&armorFlag, "a", false, "generate an armored file")
|
||||
flag.BoolVar(&armorFlag, "armor", false, "generate an armored file")
|
||||
flag.Var(&recipientFlags, "r", "recipient (can be repeated)")
|
||||
@@ -99,13 +131,15 @@ func main() {
|
||||
}
|
||||
defer f.Close()
|
||||
out = f
|
||||
} else if terminal.IsTerminal(int(os.Stdout.Fd())) && !decryptFlag {
|
||||
} else if terminal.IsTerminal(int(os.Stdout.Fd())) {
|
||||
if armorFlag {
|
||||
// If the output will go to a TTY, and it will be armored, buffer it
|
||||
// up so it doesn't get in the way of typing the input.
|
||||
buf := &bytes.Buffer{}
|
||||
defer func() { io.Copy(os.Stdout, buf) }()
|
||||
out = buf
|
||||
} else if decryptFlag && name != "-" {
|
||||
// TODO: buffer the output and check it's printable.
|
||||
} else if name != "-" {
|
||||
// If the output wouldn't be armored, refuse to send binary to the
|
||||
// terminal unless explicitly requested with "-o -".
|
||||
@@ -118,7 +152,7 @@ func main() {
|
||||
case decryptFlag:
|
||||
decrypt(identityFlags, in, out)
|
||||
case passFlag:
|
||||
pass, err := passphrasePrompt()
|
||||
pass, err := passphrasePromptForEncryption()
|
||||
if err != nil {
|
||||
logFatalf("Error: %v", err)
|
||||
}
|
||||
@@ -128,13 +162,31 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func passphrasePrompt() (string, error) {
|
||||
fmt.Fprintf(os.Stderr, "Enter passphrase: ")
|
||||
func passphrasePromptForEncryption() (string, error) {
|
||||
fmt.Fprintf(os.Stderr, "Enter passphrase (leave empty to autogenerate a secure one): ")
|
||||
pass, err := readPassphrase()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read passphrase: %v", err)
|
||||
}
|
||||
return string(pass), nil
|
||||
p := string(pass)
|
||||
if p == "" {
|
||||
var words []string
|
||||
for i := 0; i < 10; i++ {
|
||||
words = append(words, randomWord())
|
||||
}
|
||||
p = strings.Join(words, "-")
|
||||
fmt.Fprintf(os.Stderr, "Using the autogenerated passphrase %q.\n", p)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Confirm passphrase: ")
|
||||
confirm, err := readPassphrase()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read passphrase: %v", err)
|
||||
}
|
||||
if string(confirm) != p {
|
||||
return "", fmt.Errorf("passphrases didn't match")
|
||||
}
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func encryptKeys(keys []string, in io.Reader, out io.Writer, armor bool) {
|
||||
@@ -157,10 +209,16 @@ func encryptPass(pass string, in io.Reader, out io.Writer, armor bool) {
|
||||
encrypt([]age.Recipient{r}, in, out, armor)
|
||||
}
|
||||
|
||||
func encrypt(recipients []age.Recipient, in io.Reader, out io.Writer, armor bool) {
|
||||
func encrypt(recipients []age.Recipient, in io.Reader, out io.Writer, withArmor bool) {
|
||||
ageEncrypt := age.Encrypt
|
||||
if armor {
|
||||
ageEncrypt = age.EncryptWithArmor
|
||||
if withArmor {
|
||||
a := armor.NewWriter(out)
|
||||
defer func() {
|
||||
if err := a.Close(); err != nil {
|
||||
logFatalf("Error: %v", err)
|
||||
}
|
||||
}()
|
||||
out = a
|
||||
}
|
||||
w, err := ageEncrypt(out, recipients...)
|
||||
if err != nil {
|
||||
@@ -191,6 +249,13 @@ func decrypt(keys []string, in io.Reader, out io.Writer) {
|
||||
identities = append(identities, ids...)
|
||||
}
|
||||
|
||||
rr := bufio.NewReader(in)
|
||||
if start, _ := rr.Peek(len(armor.Header)); string(start) == armor.Header {
|
||||
in = armor.NewReader(rr)
|
||||
} else {
|
||||
in = rr
|
||||
}
|
||||
|
||||
r, err := age.Decrypt(in, identities...)
|
||||
if err != nil {
|
||||
logFatalf("Error: %v", err)
|
||||
@@ -200,6 +265,15 @@ func decrypt(keys []string, in io.Reader, out io.Writer) {
|
||||
}
|
||||
}
|
||||
|
||||
func passphrasePrompt() (string, error) {
|
||||
fmt.Fprintf(os.Stderr, "Enter passphrase: ")
|
||||
pass, err := readPassphrase()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read passphrase: %v", err)
|
||||
}
|
||||
return string(pass), nil
|
||||
}
|
||||
|
||||
func logFatalf(format string, v ...interface{}) {
|
||||
_log.Printf(format, v...)
|
||||
_log.Fatalf("[ Did age not do what you expected? Could an error be more useful?" +
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// 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 (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age/age"
|
||||
)
|
||||
|
||||
func TestVectors(t *testing.T) {
|
||||
files, _ := filepath.Glob("testdata/*.age")
|
||||
for _, f := range files {
|
||||
name := strings.TrimSuffix(strings.TrimPrefix(f, "testdata/"), ".age")
|
||||
t.Run(name, func(t *testing.T) {
|
||||
identities, err := parseIdentitiesFile("testdata/" + name + "_key.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, i := range identities {
|
||||
t.Logf("%s", i.Type())
|
||||
}
|
||||
|
||||
in, err := os.Open("testdata/" + name + ".age")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, err := age.Decrypt(in, identities...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("%s", out)
|
||||
})
|
||||
}
|
||||
}
|
||||
+11
-80
@@ -7,90 +7,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"filippo.io/age/internal/age"
|
||||
"filippo.io/age/internal/format"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"filippo.io/age/age"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
type EncryptedSSHIdentity struct {
|
||||
pubKey ssh.PublicKey
|
||||
pemBytes []byte
|
||||
passphrase func() ([]byte, error)
|
||||
|
||||
decrypted age.Identity
|
||||
}
|
||||
|
||||
func NewEncryptedSSHIdentity(pubKey ssh.PublicKey, pemBytes []byte, passphrase func() ([]byte, error)) (*EncryptedSSHIdentity, error) {
|
||||
switch t := pubKey.Type(); t {
|
||||
case "ssh-ed25519", "ssh-rsa":
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported SSH key type: %v", t)
|
||||
}
|
||||
return &EncryptedSSHIdentity{
|
||||
pubKey: pubKey,
|
||||
pemBytes: pemBytes,
|
||||
passphrase: passphrase,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ age.IdentityMatcher = &EncryptedSSHIdentity{}
|
||||
|
||||
func (i *EncryptedSSHIdentity) Type() string {
|
||||
return i.pubKey.Type()
|
||||
}
|
||||
|
||||
func (i *EncryptedSSHIdentity) Unwrap(block *format.Recipient) (fileKey []byte, err error) {
|
||||
if i.decrypted != nil {
|
||||
return i.decrypted.Unwrap(block)
|
||||
}
|
||||
|
||||
passphrase, err := i.passphrase()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to obtain passphrase: %v", err)
|
||||
}
|
||||
k, err := ssh.ParseRawPrivateKeyWithPassphrase(i.pemBytes, passphrase)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt SSH key file: %v", err)
|
||||
}
|
||||
|
||||
switch k := k.(type) {
|
||||
case *ed25519.PrivateKey:
|
||||
i.decrypted, err = age.NewSSHEd25519Identity(*k)
|
||||
case *rsa.PrivateKey:
|
||||
i.decrypted, err = age.NewSSHRSAIdentity(k)
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected SSH key type: %T", k)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid SSH key: %v", err)
|
||||
}
|
||||
if i.decrypted.Type() != i.pubKey.Type() {
|
||||
return nil, fmt.Errorf("mismatched SSH key type: got %q, expected %q", i.decrypted.Type(), i.pubKey.Type())
|
||||
}
|
||||
|
||||
return i.decrypted.Unwrap(block)
|
||||
}
|
||||
|
||||
func (i *EncryptedSSHIdentity) Matches(block *format.Recipient) error {
|
||||
if block.Type != i.Type() {
|
||||
return age.ErrIncorrectIdentity
|
||||
}
|
||||
if len(block.Args) < 1 {
|
||||
return fmt.Errorf("invalid %v recipient block", i.Type())
|
||||
}
|
||||
|
||||
if block.Args[0] != age.SSHFingerprint(i.pubKey) {
|
||||
return age.ErrIncorrectIdentity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type LazyScryptIdentity struct {
|
||||
Passphrase func() (string, error)
|
||||
}
|
||||
@@ -101,7 +24,7 @@ func (i *LazyScryptIdentity) Type() string {
|
||||
return "scrypt"
|
||||
}
|
||||
|
||||
func (i *LazyScryptIdentity) Unwrap(block *format.Recipient) (fileKey []byte, err error) {
|
||||
func (i *LazyScryptIdentity) Unwrap(block *age.Stanza) (fileKey []byte, err error) {
|
||||
pass, err := i.Passphrase()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read passphrase: %v", err)
|
||||
@@ -110,7 +33,15 @@ func (i *LazyScryptIdentity) Unwrap(block *format.Recipient) (fileKey []byte, er
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ii.Unwrap(block)
|
||||
fileKey, err = ii.Unwrap(block)
|
||||
if err == age.ErrIncorrectIdentity {
|
||||
// The API will just ignore the identity if the passphrase is wrong, and
|
||||
// move on, eventually returning "no identity matched a recipient".
|
||||
// Since we only supply one identity from the CLI, make it a fatal
|
||||
// error with a better message.
|
||||
return nil, fmt.Errorf("incorrect passphrase")
|
||||
}
|
||||
return fileKey, err
|
||||
}
|
||||
|
||||
// stdinInUse is set in main. It's a singleton like os.Stdin.
|
||||
|
||||
+6
-5
@@ -15,7 +15,8 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"filippo.io/age/internal/age"
|
||||
"filippo.io/age/age"
|
||||
"filippo.io/age/agessh"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
@@ -24,7 +25,7 @@ func parseRecipient(arg string) (age.Recipient, error) {
|
||||
case strings.HasPrefix(arg, "age1"):
|
||||
return age.ParseX25519Recipient(arg)
|
||||
case strings.HasPrefix(arg, "ssh-"):
|
||||
return age.ParseSSHRecipient(arg)
|
||||
return agessh.ParseRecipient(arg)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unknown recipient type: %q", arg)
|
||||
@@ -82,8 +83,8 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
|
||||
}
|
||||
|
||||
func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {
|
||||
id, err := age.ParseSSHIdentity(pemBytes)
|
||||
if sshErr, ok := err.(*ssh.PassphraseNeededError); ok {
|
||||
id, err := agessh.ParseIdentity(pemBytes)
|
||||
if sshErr, ok := err.(*ssh.PassphraseMissingError); ok {
|
||||
pubKey := sshErr.PublicKey
|
||||
if pubKey == nil {
|
||||
pubKey, err = readPubFile(name)
|
||||
@@ -99,7 +100,7 @@ func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {
|
||||
}
|
||||
return pass, nil
|
||||
}
|
||||
i, err := NewEncryptedSSHIdentity(pubKey, pemBytes, passphrasePrompt)
|
||||
i, err := agessh.NewEncryptedSSHIdentity(pubKey, pemBytes, passphrasePrompt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
age-encryption.org/v1
|
||||
-> ssh-ed25519 o1Hudg SZISkI5Qn8YgUBmTKG/Zp/QpFjXWvAivzvB+hOcN5W8
|
||||
dYfwGWYvCwpSU5EXIC1XqfXdsBvCi3kMypdqCVShrpk
|
||||
-> joint-oil-hw
|
||||
--- gC/27VAgqOEzAQMKHvBjih7sJ1oDKht+HNdguTIbjt8
|
||||
fëtAeµÖ¨&8{Ëéðνcat—íΘœ¯šË·}«=šC†Ÿu
|
||||
@@ -0,0 +1,7 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACAwKgrb/LkvtI887QylSoUh5xUlKr1fb37euR6et5jHowAAAJgxqUx+MalM
|
||||
fgAAAAtzc2gtZWQyNTUxOQAAACAwKgrb/LkvtI887QylSoUh5xUlKr1fb37euR6et5jHow
|
||||
AAAEC7gKj74YIwaM1BT2tnODjfeZJvo8lcazvL6Uljv3+nIDAqCtv8uS+0jzztDKVKhSHn
|
||||
FSUqvV9vft65Hp63mMejAAAADnJ1bm5lckBmdi1hejMyAQIDBAUGBw==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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"
|
||||
"encoding/binary"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func randomWord() string {
|
||||
buf := make([]byte, 2)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
n := binary.BigEndian.Uint16(buf)
|
||||
return wordlist[int(n)%2048]
|
||||
}
|
||||
|
||||
// wordlist is the BIP39 list of 2048 english words, and it's used to generate
|
||||
// the suggested passphrases.
|
||||
var wordlist = strings.Split(`abandon ability able about above absent absorb abstract absurd abuse access accident account accuse achieve acid acoustic acquire across act action actor actress actual adapt add addict address adjust admit adult advance advice aerobic affair afford afraid again age agent agree ahead aim air airport aisle alarm album alcohol alert alien all alley allow almost alone alpha already also alter always amateur amazing among amount amused analyst anchor ancient anger angle angry animal ankle announce annual another answer antenna antique anxiety any apart apology appear apple approve april arch arctic area arena argue arm armed armor army around arrange arrest arrive arrow art artefact artist artwork ask aspect assault asset assist assume asthma athlete atom attack attend attitude attract auction audit august aunt author auto autumn average avocado avoid awake aware away awesome awful awkward axis baby bachelor bacon badge bag balance balcony ball bamboo banana banner bar barely bargain barrel base basic basket battle beach bean beauty because become beef before begin behave behind believe below belt bench benefit best betray better between beyond bicycle bid bike bind biology bird birth bitter black blade blame blanket blast bleak bless blind blood blossom blouse blue blur blush board boat body boil bomb bone bonus book boost border boring borrow boss bottom bounce box boy bracket brain brand brass brave bread breeze brick bridge brief bright bring brisk broccoli broken bronze broom brother brown brush bubble buddy budget buffalo build bulb bulk bullet bundle bunker burden burger burst bus business busy butter buyer buzz cabbage cabin cable cactus cage cake call calm camera camp can canal cancel candy cannon canoe canvas canyon capable capital captain car carbon card cargo carpet carry cart case cash casino castle casual cat catalog catch category cattle caught cause caution cave ceiling celery cement census century cereal certain chair chalk champion change chaos chapter charge chase chat cheap check cheese chef cherry chest chicken chief child chimney choice choose chronic chuckle chunk churn cigar cinnamon circle citizen city civil claim clap clarify claw clay clean clerk clever click client cliff climb clinic clip clock clog close cloth cloud clown club clump cluster clutch coach coast coconut code coffee coil coin collect color column combine come comfort comic common company concert conduct confirm congress connect consider control convince cook cool copper copy coral core corn correct cost cotton couch country couple course cousin cover coyote crack cradle craft cram crane crash crater crawl crazy cream credit creek crew cricket crime crisp critic crop cross crouch crowd crucial cruel cruise crumble crunch crush cry crystal cube culture cup cupboard curious current curtain curve cushion custom cute cycle dad damage damp dance danger daring dash daughter dawn day deal debate debris decade december decide decline decorate decrease deer defense define defy degree delay deliver demand demise denial dentist deny depart depend deposit depth deputy derive describe desert design desk despair destroy detail detect develop device devote diagram dial diamond diary dice diesel diet differ digital dignity dilemma dinner dinosaur direct dirt disagree discover disease dish dismiss disorder display distance divert divide divorce dizzy doctor document dog doll dolphin domain donate donkey donor door dose double dove draft dragon drama drastic draw dream dress drift drill drink drip drive drop drum dry duck dumb dune during dust dutch duty dwarf dynamic eager eagle early earn earth easily east easy echo ecology economy edge edit educate effort egg eight either elbow elder electric elegant element elephant elevator elite else embark embody embrace emerge emotion employ empower empty enable enact end endless endorse enemy energy enforce engage engine enhance enjoy enlist enough enrich enroll ensure enter entire entry envelope episode equal equip era erase erode erosion error erupt escape essay essence estate eternal ethics evidence evil evoke evolve exact example excess exchange excite exclude excuse execute exercise exhaust exhibit exile exist exit exotic expand expect expire explain expose express extend extra eye eyebrow fabric face faculty fade faint faith fall false fame family famous fan fancy fantasy farm fashion fat fatal father fatigue fault favorite feature february federal fee feed feel female fence festival fetch fever few fiber fiction field figure file film filter final find fine finger finish fire firm first fiscal fish fit fitness fix flag flame flash flat flavor flee flight flip float flock floor flower fluid flush fly foam focus fog foil fold follow food foot force forest forget fork fortune forum forward fossil foster found fox fragile frame frequent fresh friend fringe frog front frost frown frozen fruit fuel fun funny furnace fury future gadget gain galaxy gallery game gap gLine truncated
|
||||
@@ -2,6 +2,7 @@ module filippo.io/age
|
||||
|
||||
go 1.13
|
||||
|
||||
require golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc
|
||||
|
||||
replace golang.org/x/crypto => github.com/Filosottile/go v0.0.0-20191122011136-9090b284250b
|
||||
require (
|
||||
github.com/sergi/go-diff v1.1.0
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59
|
||||
)
|
||||
@@ -1,6 +1,29 @@
|
||||
github.com/Filosottile/go v0.0.0-20191122011136-9090b284250b h1:4AVIiSN9FRvfh7Oq7NhMHoU4oDhNkpfq4q9prQNlq7k=
|
||||
github.com/Filosottile/go v0.0.0-20191122011136-9090b284250b/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -1,110 +0,0 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age/internal/age"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
const helloWorld = "Hello, Twitch!"
|
||||
|
||||
func TestEncryptDecryptX25519(t *testing.T) {
|
||||
secretKeyA := make([]byte, curve25519.ScalarSize)
|
||||
secretKeyB := make([]byte, curve25519.ScalarSize)
|
||||
if _, err := rand.Read(secretKeyA); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := rand.Read(secretKeyB); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
publicKeyA, _ := curve25519.X25519(secretKeyA, curve25519.Basepoint)
|
||||
publicKeyB, _ := curve25519.X25519(secretKeyB, curve25519.Basepoint)
|
||||
|
||||
rA, err := age.NewX25519Recipient(publicKeyA)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rB, err := age.NewX25519Recipient(publicKeyB)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
w, err := age.Encrypt(buf, rA, rB)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := io.WriteString(w, helloWorld); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("%s", buf.Bytes())
|
||||
|
||||
i, err := age.NewX25519Identity(secretKeyB)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := age.Decrypt(buf, i)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
outBytes, err := ioutil.ReadAll(out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(outBytes) != helloWorld {
|
||||
t.Errorf("wrong data: %q, excepted %q", outBytes, helloWorld)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptDecryptScrypt(t *testing.T) {
|
||||
password := "twitch.tv/filosottile"
|
||||
|
||||
r, err := age.NewScryptRecipient(password)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.SetWorkFactor(15)
|
||||
buf := &bytes.Buffer{}
|
||||
w, err := age.Encrypt(buf, r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := io.WriteString(w, helloWorld); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("%s", buf.Bytes())
|
||||
|
||||
i, err := age.NewScryptIdentity(password)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := age.Decrypt(buf, i)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
outBytes, err := ioutil.ReadAll(out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(outBytes) != helloWorld {
|
||||
t.Errorf("wrong data: %q, excepted %q", outBytes, helloWorld)
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age/internal/age"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func TestX25519RoundTrip(t *testing.T) {
|
||||
secretKey := make([]byte, curve25519.ScalarSize)
|
||||
if _, err := rand.Read(secretKey); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
publicKey, _ := curve25519.X25519(secretKey, curve25519.Basepoint)
|
||||
|
||||
r, err := age.NewX25519Recipient(publicKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
i, err := age.NewX25519Identity(secretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if r.Type() != i.Type() || r.Type() != "X25519" {
|
||||
t.Errorf("invalid Type values: %v, %v", r.Type(), i.Type())
|
||||
}
|
||||
|
||||
if r1, err := age.ParseX25519Recipient(r.String()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if r1.String() != r.String() {
|
||||
t.Errorf("recipient did not round-trip through parsing: got %q, want %q", r1, r)
|
||||
}
|
||||
if i1, err := age.ParseX25519Identity(i.String()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if i1.String() != i.String() {
|
||||
t.Errorf("identity did not round-trip through parsing: got %q, want %q", i1, i)
|
||||
}
|
||||
|
||||
fileKey := make([]byte, 16)
|
||||
if _, err := rand.Read(fileKey); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
block, err := r.Wrap(fileKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := &bytes.Buffer{}
|
||||
block.Marshal(b)
|
||||
t.Logf("%s", b.Bytes())
|
||||
|
||||
out, err := i.Unwrap(block)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(fileKey, out) {
|
||||
t.Errorf("invalid output: %x, expected %x", out, fileKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScryptRoundTrip(t *testing.T) {
|
||||
password := "twitch.tv/filosottile"
|
||||
|
||||
r, err := age.NewScryptRecipient(password)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.SetWorkFactor(15)
|
||||
i, err := age.NewScryptIdentity(password)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if r.Type() != i.Type() || r.Type() != "scrypt" {
|
||||
t.Errorf("invalid Type values: %v, %v", r.Type(), i.Type())
|
||||
}
|
||||
|
||||
fileKey := make([]byte, 16)
|
||||
if _, err := rand.Read(fileKey); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
block, err := r.Wrap(fileKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := &bytes.Buffer{}
|
||||
block.Marshal(b)
|
||||
t.Logf("%s", b.Bytes())
|
||||
|
||||
out, err := i.Unwrap(block)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(fileKey, out) {
|
||||
t.Errorf("invalid output: %x, expected %x", out, fileKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSHRSARoundTrip(t *testing.T) {
|
||||
pk, err := rsa.GenerateKey(rand.Reader, 768)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pub, err := ssh.NewPublicKey(&pk.PublicKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r, err := age.NewSSHRSARecipient(pub)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
i, err := age.NewSSHRSAIdentity(pk)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if r.Type() != i.Type() || r.Type() != "ssh-rsa" {
|
||||
t.Errorf("invalid Type values: %v, %v", r.Type(), i.Type())
|
||||
}
|
||||
|
||||
fileKey := make([]byte, 16)
|
||||
if _, err := rand.Read(fileKey); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
block, err := r.Wrap(fileKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := &bytes.Buffer{}
|
||||
block.Marshal(b)
|
||||
t.Logf("%s", b.Bytes())
|
||||
|
||||
out, err := i.Unwrap(block)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(fileKey, out) {
|
||||
t.Errorf("invalid output: %x, expected %x", out, fileKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSHEd25519RoundTrip(t *testing.T) {
|
||||
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sshPubKey, err := ssh.NewPublicKey(pub)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r, err := age.NewSSHEd25519Recipient(sshPubKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
i, err := age.NewSSHEd25519Identity(priv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if r.Type() != i.Type() || r.Type() != "ssh-ed25519" {
|
||||
t.Errorf("invalid Type values: %v, %v", r.Type(), i.Type())
|
||||
}
|
||||
|
||||
fileKey := make([]byte, 16)
|
||||
if _, err := rand.Read(fileKey); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
block, err := r.Wrap(fileKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := &bytes.Buffer{}
|
||||
block.Marshal(b)
|
||||
t.Logf("%s", b.Bytes())
|
||||
|
||||
out, err := i.Unwrap(block)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(fileKey, out) {
|
||||
t.Errorf("invalid output: %x, expected %x", out, fileKey)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# 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
|
||||
|
||||
# docker run --rm -v $PWD/workdir:/workdir $(docker build -q -f internal/format/Dockerfile.go-fuzz .)
|
||||
|
||||
FROM golang:1.14-alpine3.11
|
||||
|
||||
RUN apk add --no-cache git
|
||||
RUN go get github.com/dvyukov/go-fuzz/...
|
||||
|
||||
ADD . $GOPATH/src/filippo.io/age/
|
||||
WORKDIR $GOPATH/src/filippo.io/age
|
||||
|
||||
RUN go-fuzz-build ./internal/format
|
||||
|
||||
VOLUME /workdir
|
||||
|
||||
ENTRYPOINT ["go-fuzz", "-workdir", "/workdir", "-bin", "format-fuzz.zip"]
|
||||
@@ -1,45 +0,0 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age/internal/format"
|
||||
)
|
||||
|
||||
func TestArmor(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := format.ArmoredWriter(buf)
|
||||
plain := make([]byte, 611)
|
||||
if _, err := w.Write(plain); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(buf.Bytes())
|
||||
if block == nil {
|
||||
t.Fatal("PEM decoding failed")
|
||||
}
|
||||
if !bytes.Equal(block.Bytes, plain) {
|
||||
t.Error("PEM decoded value doesn't match")
|
||||
}
|
||||
|
||||
r := format.ArmoredReader(buf)
|
||||
out, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(out, plain) {
|
||||
t.Error("decoded value doesn't match")
|
||||
}
|
||||
}
|
||||
+77
-19
@@ -18,11 +18,13 @@ import (
|
||||
)
|
||||
|
||||
type Header struct {
|
||||
Recipients []*Recipient
|
||||
Recipients []*Stanza
|
||||
MAC []byte
|
||||
}
|
||||
|
||||
type Recipient struct {
|
||||
// Stanza is assignable to age.Stanza, and if this package is made public,
|
||||
// age.Stanza can be made a type alias of this type.
|
||||
type Stanza struct {
|
||||
Type string
|
||||
Args []string
|
||||
Body []byte
|
||||
@@ -40,15 +42,50 @@ func DecodeString(s string) ([]byte, error) {
|
||||
|
||||
var EncodeToString = b64.EncodeToString
|
||||
|
||||
const columnsPerLine = 64
|
||||
const bytesPerLine = columnsPerLine / 4 * 3
|
||||
const ColumnsPerLine = 64
|
||||
const BytesPerLine = ColumnsPerLine / 4 * 3
|
||||
|
||||
// NewlineWriter returns a Writer that writes to dst, inserting an LF character
|
||||
// every ColumnsPerLine bytes. It does not insert a newline neither at the
|
||||
// beginning nor at the end of the stream.
|
||||
func NewlineWriter(dst io.Writer) io.Writer {
|
||||
return &newlineWriter{dst: dst}
|
||||
}
|
||||
|
||||
type newlineWriter struct {
|
||||
dst io.Writer
|
||||
written int
|
||||
}
|
||||
|
||||
func (w *newlineWriter) Write(p []byte) (n int, err error) {
|
||||
for len(p) > 0 {
|
||||
remainingInLine := ColumnsPerLine - (w.written % ColumnsPerLine)
|
||||
if remainingInLine == ColumnsPerLine && w.written != 0 {
|
||||
if _, err := w.dst.Write([]byte("\n")); err != nil {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
toWrite := remainingInLine
|
||||
if toWrite > len(p) {
|
||||
toWrite = len(p)
|
||||
}
|
||||
nn, err := w.dst.Write(p[:toWrite])
|
||||
n += nn
|
||||
w.written += nn
|
||||
p = p[nn:]
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
const intro = "age-encryption.org/v1\n"
|
||||
|
||||
var recipientPrefix = []byte("->")
|
||||
var footerPrefix = []byte("---")
|
||||
|
||||
func (r *Recipient) Marshal(w io.Writer) error {
|
||||
func (r *Stanza) Marshal(w io.Writer) error {
|
||||
if _, err := w.Write(recipientPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -60,7 +97,10 @@ func (r *Recipient) Marshal(w io.Writer) error {
|
||||
if _, err := io.WriteString(w, "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
ww := base64.NewEncoder(b64, &newlineWriter{dst: w})
|
||||
if len(r.Body) == 0 {
|
||||
return nil
|
||||
}
|
||||
ww := base64.NewEncoder(b64, NewlineWriter(w))
|
||||
if _, err := ww.Write(r.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -109,13 +149,6 @@ func Parse(input io.Reader) (*Header, io.Reader, error) {
|
||||
h := &Header{}
|
||||
rr := bufio.NewReader(input)
|
||||
|
||||
// TODO: find a way to communicate to the caller that the file was armored,
|
||||
// as they might not appreciate the malleability.
|
||||
if start, _ := rr.Peek(len(armorPreamble)); string(start) == armorPreamble {
|
||||
input = ArmoredReader(rr)
|
||||
rr = bufio.NewReader(input)
|
||||
}
|
||||
|
||||
line, err := rr.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, nil, errorf("failed to read intro: %v", err)
|
||||
@@ -124,7 +157,7 @@ func Parse(input io.Reader) (*Header, io.Reader, error) {
|
||||
return nil, nil, errorf("unexpected intro: %q", line)
|
||||
}
|
||||
|
||||
var r *Recipient
|
||||
var r *Stanza
|
||||
for {
|
||||
line, err := rr.ReadBytes('\n')
|
||||
if err != nil {
|
||||
@@ -143,11 +176,16 @@ func Parse(input io.Reader) (*Header, io.Reader, error) {
|
||||
break
|
||||
|
||||
} else if bytes.HasPrefix(line, recipientPrefix) {
|
||||
r = &Recipient{}
|
||||
r = &Stanza{}
|
||||
prefix, args := splitArgs(line)
|
||||
if prefix != string(recipientPrefix) || len(args) < 1 {
|
||||
return nil, nil, errorf("malformed recipient: %q", line)
|
||||
}
|
||||
for _, a := range args {
|
||||
if !isValidString(a) {
|
||||
return nil, nil, errorf("malformed recipient: %q", line)
|
||||
}
|
||||
}
|
||||
r.Type = args[0]
|
||||
r.Args = args[1:]
|
||||
h.Recipients = append(h.Recipients, r)
|
||||
@@ -157,11 +195,14 @@ func Parse(input io.Reader) (*Header, io.Reader, error) {
|
||||
if err != nil {
|
||||
return nil, nil, errorf("malformed body line %q: %v", line, err)
|
||||
}
|
||||
if len(b) > bytesPerLine {
|
||||
if len(b) > BytesPerLine {
|
||||
return nil, nil, errorf("malformed body line %q: too long", line)
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil, nil, errorf("malformed body line %q: line is empty", line)
|
||||
}
|
||||
r.Body = append(r.Body, b...)
|
||||
if len(b) < bytesPerLine {
|
||||
if len(b) < BytesPerLine {
|
||||
// Only the last line of a body can be short.
|
||||
r = nil
|
||||
}
|
||||
@@ -171,13 +212,18 @@ func Parse(input io.Reader) (*Header, io.Reader, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Unwind the bufio overread and return the unbuffered input.
|
||||
// If input is a bufio.Reader, rr might be equal to input because
|
||||
// bufio.NewReader short-circuits. In this case we can just return it (and
|
||||
// we would end up reading the buffer twice if we prepended the peek below).
|
||||
if rr == input {
|
||||
return h, rr, nil
|
||||
}
|
||||
// Otherwise, unwind the bufio overread and return the unbuffered input.
|
||||
buf, err := rr.Peek(rr.Buffered())
|
||||
if err != nil {
|
||||
return nil, nil, errorf("internal error: %v", err)
|
||||
}
|
||||
payload := io.MultiReader(bytes.NewReader(buf), input)
|
||||
|
||||
return h, payload, nil
|
||||
}
|
||||
|
||||
@@ -186,3 +232,15 @@ func splitArgs(line []byte) (string, []string) {
|
||||
parts := strings.Split(l, " ")
|
||||
return parts[0], parts[1:]
|
||||
}
|
||||
|
||||
func isValidString(s string) bool {
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, c := range s {
|
||||
if c < 33 || c > 126 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -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
|
||||
|
||||
// +build gofuzz
|
||||
|
||||
package format
|
||||
@@ -6,10 +12,13 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
func Fuzz(data []byte) int {
|
||||
isArmored := bytes.HasPrefix(data, []byte("-----BEGIN AGE ENCRYPTED FILE-----"))
|
||||
h, payload, err := Parse(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
if h != nil {
|
||||
@@ -21,14 +30,30 @@ func Fuzz(data []byte) int {
|
||||
return 0
|
||||
}
|
||||
w := &bytes.Buffer{}
|
||||
if err := h.Marshal(w); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if _, err := io.Copy(w, payload); err != nil {
|
||||
panic(err)
|
||||
if isArmored {
|
||||
w := ArmoredWriter(w)
|
||||
if err := h.Marshal(w); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if _, err := io.Copy(w, payload); err != nil {
|
||||
if strings.Contains(err.Error(), "invalid armor") {
|
||||
return 0
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
w.Close()
|
||||
} else {
|
||||
if err := h.Marshal(w); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if _, err := io.Copy(w, payload); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
if !bytes.Equal(w.Bytes(), data) {
|
||||
fmt.Fprintf(os.Stderr, "%s\n%q\n%q\n\n", w, data, w)
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(data), string(w.Bytes()), false)
|
||||
fmt.Println(dmp.DiffToDelta(diffs))
|
||||
panic("Marshal output different from input")
|
||||
}
|
||||
return 1
|
||||
|
||||
+10
-12
@@ -74,7 +74,7 @@ func (r *Reader) Read(p []byte) (int, error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// readChunk reads the next chunk of ciphertext from r.c and makes in available
|
||||
// readChunk reads the next chunk of ciphertext from r.c and makes it available
|
||||
// in r.unread. last is true if the chunk was marked as the end of the message.
|
||||
// readChunk must not be called again after returning a last chunk or an error.
|
||||
func (r *Reader) readChunk() (last bool, err error) {
|
||||
@@ -106,7 +106,7 @@ func (r *Reader) readChunk() (last bool, err error) {
|
||||
out, err = r.a.Open(outBuf, r.nonce[:], in, nil)
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, errors.New("failed to decrypt and authenticate payload chunk")
|
||||
}
|
||||
|
||||
incNonce(&r.nonce)
|
||||
@@ -132,14 +132,14 @@ func setLastChunkFlag(nonce *[chacha20poly1305.NonceSize]byte) {
|
||||
|
||||
type Writer struct {
|
||||
a cipher.AEAD
|
||||
dst io.WriteCloser
|
||||
dst io.Writer
|
||||
unwritten []byte // backed by buf
|
||||
buf [encChunkSize]byte
|
||||
nonce [chacha20poly1305.NonceSize]byte
|
||||
err error
|
||||
}
|
||||
|
||||
func NewWriter(key []byte, dst io.WriteCloser) (*Writer, error) {
|
||||
func NewWriter(key []byte, dst io.Writer) (*Writer, error) {
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -178,21 +178,19 @@ func (w *Writer) Write(p []byte) (n int, err error) {
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// Close will flush the last chunk and call the underlying
|
||||
// WriteCloser's Close method.
|
||||
// Close flushes the last chunk. It does not close the underlying Writer.
|
||||
func (w *Writer) Close() error {
|
||||
if w.err != nil {
|
||||
return w.err
|
||||
}
|
||||
|
||||
err := w.flushChunk(lastChunk)
|
||||
if err != nil {
|
||||
w.err = err
|
||||
return err
|
||||
w.err = w.flushChunk(lastChunk)
|
||||
if w.err != nil {
|
||||
return w.err
|
||||
}
|
||||
w.err = errors.New("stream.Writer is already closed")
|
||||
|
||||
return w.dst.Close()
|
||||
w.err = errors.New("stream.Writer is already closed")
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -10,10 +10,8 @@ import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age/internal/format"
|
||||
"filippo.io/age/internal/stream"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
@@ -40,19 +38,7 @@ func testRoundTrip(t *testing.T, stepSize, length int) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var closed bool
|
||||
bufCloser := struct {
|
||||
io.Writer
|
||||
format.CloserFunc
|
||||
}{
|
||||
Writer: buf,
|
||||
CloserFunc: func() error {
|
||||
closed = true
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
w, err := stream.NewWriter(key, bufCloser)
|
||||
w, err := stream.NewWriter(key, buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -84,9 +70,6 @@ func testRoundTrip(t *testing.T, stepSize, length int) {
|
||||
if err := w.Close(); err != nil {
|
||||
t.Error("Close returned an error:", err)
|
||||
}
|
||||
if !closed {
|
||||
t.Error("(*stream.Writer).Close didn't close the underlying WriteCloser")
|
||||
}
|
||||
|
||||
t.Logf("buffer size: %d", buf.Len())
|
||||
|
||||
|
||||
Reference in new issue
Block a user