Skip to content

Commit

Permalink
Merge pull request #23 from FiloSottile/master
Browse files Browse the repository at this point in the history
Sync Fork from Upstream Repo
  • Loading branch information
sthagen committed Jul 9, 2021
2 parents 2c7c0ab + 7cb6b84 commit 09cb968
Show file tree
Hide file tree
Showing 36 changed files with 1,670 additions and 166 deletions.
33 changes: 33 additions & 0 deletions .github/workflows/ronn.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
on:
push:
branches:
- '**'
paths:
- '**.ronn'
name: Generate man pages
jobs:
ronn:
runs-on: ubuntu-latest
name: Ronn
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Run ronn
uses: ./.github/workflows/ronn
id: ronn
- name: Undo email mangling
# rdiscount randomizes the output for no good reason, which causes
# changes to always get committed. Sigh.
# https://github.com/davidfstr/rdiscount/blob/6b1471ec3/ext/generate.c#L781-L795
run: |-
for f in doc/*.html; do
awk '/Filippo Valsorda/ { $0 = "<p>Filippo Valsorda <a href=\"mailto:age@filippo.io\" data-bare-link=\"true\">age@filippo.io</a></p>" } { print }' "$f" > "$f.tmp"
mv "$f.tmp" "$f"
done
- name: Commit and push if changed
run: |-
git config user.name "GitHub Actions"
git config user.email "actions@users.noreply.github.com"
git add -A
git commit -m "doc: regenerate groff and html man pages" || exit 0
git push
8 changes: 8 additions & 0 deletions .github/workflows/ronn/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM ruby:3.0.1-buster

RUN apt-get update && apt-get install -y groff
RUN bundle config --global frozen 1
COPY Gemfile Gemfile.lock ./
RUN bundle install
ENTRYPOINT ["bash", "-O", "globstar", "-c", \
"/usr/local/bundle/bin/ronn **/*.ronn"]
5 changes: 5 additions & 0 deletions .github/workflows/ronn/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

source "https://rubygems.org"

gem "ronn", "~> 0.7.3"
20 changes: 20 additions & 0 deletions .github/workflows/ronn/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
GEM
remote: https://rubygems.org/
specs:
hpricot (0.8.6)
mustache (1.1.1)
rdiscount (2.2.0.2)
ronn (0.7.3)
hpricot (>= 0.8.2)
mustache (>= 0.7.0)
rdiscount (>= 1.5.8)

PLATFORMS
aarch64-linux
x86_64-linux

DEPENDENCIES
ronn (~> 0.7.3)

BUNDLED WITH
2.2.15
4 changes: 4 additions & 0 deletions .github/workflows/ronn/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name: Ronn
runs:
using: docker
image: Dockerfile
7 changes: 5 additions & 2 deletions HomebrewFormula/age.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@
class Age < Formula
desc "Simple, modern, secure file encryption"
homepage "https://filippo.io/age"
url "https://github.com/FiloSottile/age/archive/v1.0.0-rc.1.zip"
sha256 "b9269bc3533fefb1dbbc90cb3683be4d4fa3ea41c1a8e7a3265415b2de26f007"
url "https://github.com/FiloSottile/age/archive/v1.0.0-rc.3.zip"
sha256 "0e7d94f17e610d5ad9ce8e88e3c157b073dcc41984b1d07793aef44b9e3b67d8"
head "https://github.com/FiloSottile/age.git"

depends_on "go" => :build

def install
mkdir bin
system "go", "build", "-trimpath", "-o", bin, "-ldflags", "-X main.Version=v#{version}", "filippo.io/age/cmd/..."
prefix.install_metafiles
man1.install "doc/age.1"
man1.install "doc/age-keygen.1"
end
end
51 changes: 45 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# age

age is a simple, modern and secure file encryption tool, format, and [Go library](https://pkg.go.dev/filippo.io/age).
[![Go Reference](https://pkg.go.dev/badge/filippo.io/age.svg)](https://pkg.go.dev/filippo.io/age)
[![man page](https://img.shields.io/badge/man-page-lightgrey)](https://htmlpreview.github.io/?https://github.com/FiloSottile/age/blob/master/doc/age.1.html)

age is a simple, modern and secure file encryption tool, format, and Go library.

It features small explicit keys, no config options, and UNIX-style composability.

Expand All @@ -19,22 +22,26 @@ The author pronounces it `[aɡe̞]`, like the Italian [“aghe”](https://trans

## Usage

For the full documentation, read [the age(1) man page](https://htmlpreview.github.io/?https://github.com/FiloSottile/age/blob/master/doc/age.1.html).

```
Usage:
age (-r RECIPIENT | -R PATH)... [--armor] [-o OUTPUT] [INPUT]
age --passphrase [--armor] [-o OUTPUT] [INPUT]
age [--encrypt] (-r RECIPIENT | -R PATH)... [--armor] [-o OUTPUT] [INPUT]
age [--encrypt] --passphrase [--armor] [-o OUTPUT] [INPUT]
age --decrypt [-i PATH]... [-o OUTPUT] [INPUT]
Options:
-e, --encrypt Encrypt the input to the output. Default if omitted.
-d, --decrypt Decrypt the input to the output.
-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.
-R, --recipients-file PATH Encrypt to recipients listed at PATH. Can be repeated.
-d, --decrypt Decrypt the input to the output.
-i, --identity PATH Use the identity file at PATH. Can be repeated.
INPUT defaults to standard input, and OUTPUT defaults to standard output.
If OUTPUT exists, it will be overwritten.
RECIPIENT can be an age public key generated by age-keygen ("age1...")
or an SSH public key ("ssh-ed25519 AAAA...", "ssh-rsa AAAA...").
Expand All @@ -45,8 +52,12 @@ read recipients from standard input.
Identity files contain one or more secret keys ("AGE-SECRET-KEY-1..."),
one per line, or an SSH key. Empty lines and lines starting with "#" are
ignored as comments. Multiple key files can be provided, and any unused ones
ignored as comments. Passphrase encrypted age files can be used as
identity files. Multiple key files can be provided, and any unused ones
will be ignored. "-" may be used to read identities from standard input.
When --encrypt is specified explicitly, -i can also be used to encrypt to an
identity file symmetrically, instead or in addition to normal recipients.
```

### Multiple recipients
Expand Down Expand Up @@ -85,6 +96,22 @@ $ age -d secrets.txt.age > secrets.txt
Enter passphrase:
```

### Passphrase-protected key files

If an identity file passed to `-i` is a passphrase encrypted age file, it will be automatically decrypted.

```
$ age-keygen | age -p > key.age
Public key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5
Enter passphrase (leave empty to autogenerate a secure one):
Using the autogenerated passphrase "hip-roast-boring-snake-mention-east-wasp-honey-input-actress".
$ age -r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets.txt > secrets.txt.age
$ age -d -i key.age secrets.txt.age > secrets.txt
Enter passphrase for identity file "key.age":
```

Passphrase-protected identity files are not necessary for most use cases, where access to the encrypted identity file implies access to the whole system. However, they can be useful if the identity file is stored remotely.

### 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.)
Expand Down Expand Up @@ -137,7 +164,7 @@ Keep in mind that people might not protect SSH keys long-term, since they are re
<tr>
<td>Arch Linux</td>
<td>
<code>pacman -Syu age</code>
<code>pacman -S age</code>
</td>
</tr>
<tr>
Expand All @@ -164,6 +191,18 @@ Keep in mind that people might not protect SSH keys long-term, since they are re
<code>nix-env -i age</code>
</td>
</tr>
<tr>
<td>Gentoo Linux</td>
<td>
<code>emerge app-crypt/age</code>
</td>
</tr>
<tr>
<td>Void Linux</td>
<td>
<code>xbps-install age</code>
</td>
</tr>
</table>

On Windows, Linux, macOS, and FreeBSD you can use [the pre-built binaries](https://github.com/FiloSottile/age/releases).
Expand Down
21 changes: 10 additions & 11 deletions age.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@ func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
return nil, errors.New("no recipients specified")
}

// As a best effort, prevent an API user from generating a file that the
// ScryptIdentity will refuse to decrypt. This check can't unfortunately be
// implemented as part of the Recipient interface, so it lives as a special
// case in Encrypt.
for _, r := range recipients {
if _, ok := r.(*ScryptRecipient); ok && len(recipients) != 1 {
return nil, errors.New("an ScryptRecipient must be the only one for the file")
}
}

fileKey := make([]byte, fileKeySize)
if _, err := rand.Read(fileKey); err != nil {
return nil, err
Expand All @@ -118,11 +128,6 @@ func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
hdr.Recipients = append(hdr.Recipients, (*format.Stanza)(s))
}
}
for _, s := range hdr.Recipients {
if s.Type == "scrypt" && len(hdr.Recipients) != 1 {
return nil, errors.New("an scrypt recipient must be the only one")
}
}
if mac, err := headerMAC(fileKey, hdr); err != nil {
return nil, fmt.Errorf("failed to compute header MAC: %v", err)
} else {
Expand Down Expand Up @@ -169,12 +174,6 @@ func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {
return nil, fmt.Errorf("failed to read header: %v", err)
}

for _, r := range hdr.Recipients {
if r.Type == "scrypt" && len(hdr.Recipients) != 1 {
return nil, errors.New("an scrypt recipient must be the only one")
}
}

stanzas := make([]*Stanza, 0, len(hdr.Recipients))
for _, s := range hdr.Recipients {
stanzas = append(stanzas, (*Stanza)(s))
Expand Down
5 changes: 4 additions & 1 deletion agessh/agessh.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ func NewRSARecipient(pk ssh.PublicKey) (*RSARecipient, error) {
} else {
return nil, errors.New("pk does not implement ssh.CryptoPublicKey")
}
if r.pubKey.Size() < 2048/8 {
return nil, errors.New("RSA key size is too small")
}
return r, nil
}

Expand Down Expand Up @@ -189,7 +192,7 @@ func ParseRecipient(s string) (age.Recipient, error) {
func ed25519PublicKeyToCurve25519(pk ed25519.PublicKey) ([]byte, error) {
// See https://blog.filippo.io/using-ed25519-keys-for-encryption and
// https://pkg.go.dev/filippo.io/edwards25519#Point.BytesMontgomery.
p, err := (&edwards25519.Point{}).SetBytes(pk)
p, err := new(edwards25519.Point).SetBytes(pk)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion agessh/agessh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
)

func TestSSHRSARoundTrip(t *testing.T) {
pk, err := rsa.GenerateKey(rand.Reader, 768)
pk, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
Expand Down
29 changes: 21 additions & 8 deletions agessh/encrypted_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
// pass the result to NewEd25519Identity or NewRSAIdentity.
type EncryptedSSHIdentity struct {
pubKey ssh.PublicKey
recipient age.Recipient
pemBytes []byte
passphrase func() ([]byte, error)

Expand All @@ -41,22 +42,34 @@ type EncryptedSSHIdentity struct {
// 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) {
i := &EncryptedSSHIdentity{
pubKey: pubKey,
pemBytes: pemBytes,
passphrase: passphrase,
}
switch t := pubKey.Type(); t {
case "ssh-ed25519", "ssh-rsa":
case "ssh-ed25519":
r, err := NewEd25519Recipient(pubKey)
if err != nil {
return nil, err
}
i.recipient = r
case "ssh-rsa":
r, err := NewRSARecipient(pubKey)
if err != nil {
return nil, err
}
i.recipient = r
default:
return nil, fmt.Errorf("unsupported SSH key type: %v", t)
}
return &EncryptedSSHIdentity{
pubKey: pubKey,
pemBytes: pemBytes,
passphrase: passphrase,
}, nil
return i, nil
}

var _ age.Identity = &EncryptedSSHIdentity{}

func (i *EncryptedSSHIdentity) Recipient() (age.Recipient, error) {
return ParseRecipient(string(ssh.MarshalAuthorizedKey(i.pubKey)))
func (i *EncryptedSSHIdentity) Recipient() age.Recipient {
return i.recipient
}

// Unwrap implements age.Identity. If the private key is still encrypted, and
Expand Down
15 changes: 10 additions & 5 deletions armor/armor.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const (

type armoredWriter struct {
started, closed bool
encoder io.WriteCloser
encoder *format.WrappedBase64Encoder
dst io.Writer
}

Expand All @@ -50,15 +50,20 @@ func (a *armoredWriter) Close() error {
if err := a.encoder.Close(); err != nil {
return err
}
_, err := io.WriteString(a.dst, "\n"+Footer+"\n")
footer := Footer + "\n"
if !a.encoder.LastLineIsEmpty() {
footer = "\n" + footer
}
_, err := io.WriteString(a.dst, footer)
return err
}

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(),
format.NewlineWriter(dst))}
return &armoredWriter{
dst: dst,
encoder: format.NewWrappedBase64Encoder(base64.StdEncoding, dst),
}
}

type armoredReader struct {
Expand Down

0 comments on commit 09cb968

Please sign in to comment.