42 Commits
Author SHA1 Message Date
Filippo Valsorda 1bf22e2163 cmd/age-keygen: print the public key to standard error 2019-12-27 23:49:09 +01:00
Filippo Valsorda 3d73da544d cmd/age: add -help and README 2019-12-27 23:48:29 +01:00
Filippo Valsorda c185781433 HomebrewFormula: add Homebrew formula 2019-12-27 18:37:48 +01:00
Filippo Valsorda bbab440e19 internal/age: add a label to the scrypt salt
This way every recipient is labeled with the version.

-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNjcnlwdCBSbmw2ellyQ1VFK25rVkwx
TkF0SklnIDE4CjlZL2RKb2FOcjFrM0MwSVZqS1BzMUFLeVF5Y1RaMEwvQlRLMWwv
Q0xJbEEKLS0tIEJ1STZCbFh5Vjdsam5nSEFGTVZUY1BxcVVIek04ZUVrOGR4L3ph
NkYzS28KucY25ejFefMDMtKvsAEofDQLsYF41NPrSPITpoxuVWMMZ1ldm+lDh09q
RzCIZAhLN8jaqdeVdCEutqiniJ/9qv4=
-----END AGE ENCRYPTED FILE-----

Password: lies
2019-12-27 17:13:20 +01:00
Filippo Valsorda 158b2ce899 all: switch labels and intro to age-encryption.org/v1 2019-12-27 17:13:20 +01:00
Filippo Valsorda c434eee6e8 internal/format: switch armor to PEM
It's with a heavy heart that I admit using the ASCII header as part of
the armor was clever, and you know what we think about being clever
around here.

Still, PEM is so lax, we target a subset without headers, and without
garbage before and after the markers.

-----BEGIN AGE ENCRYPTED FILE-----
VGhpcyBpcyBhIGZpbGUgZW5jcnlwdGVkIHdpdGggYWdlLXRvb2wuY29tLCB2ZXJz
aW9uIDEKLT4gWDI1NTE5IGozWWtNTWtaVGNDc0tKVGtMN29aam9NT2FUaGpBTVdU
Y1k5ZHVNdWJhUlkKb0F5d2N4ZW1lSTM1SkZiWHIxcHRFWW0rMjNzK3RuOTg1OHpN
L0ZkVzNCTQotLS0gQWZqdXFFaXNhbmYxbGpPRVZsSS9QM0wyM0RrTHRWWElsQnFu
ejFmRW4zdwq1FMc+yjVJBDuBUZSPMi0nCAtELIObQOHHQlQnvhk6BCITceOD5DbN
S7b6oumB8i/hEJvTtsOLgTBofzqzB90iAQ==
-----END AGE ENCRYPTED FILE-----

AGE-SECRET-KEY-1Y77J4M9R7GEKMZHR6YFDLDWV74VK2YQV4C7SR2H7SSVVJ05HQS4Q7NNMS3
2019-12-27 17:13:20 +01:00
Filippo Valsorda b142e0fd01 internal/format: switch to 64 columns of standard Base64
Now that we don't use Base64 in keys, let's just go back to the standard
alphabet. Still in the spirit of reducing weirdness, use the PEM column
count, so we can also reuse the lineWriter for the PEM armor.

See https://groups.google.com/d/msg/age-dev/UAjkvLoCr9I/l4Q1h3OPAgAJ.
2019-12-27 17:13:20 +01:00
Filippo Valsorda 9a84e437b1 all: switch key format to Bech32
Use the BIP173 format, which is whole-word selectable, markup safe, and
case insensitive.

AGE-SECRET-KEY-1FPSHVEFQXYSX5MMFDE6ZCGRTV4JHQGRFWSS8WETVDSSX76TVV4JQU272CR

See https://groups.google.com/d/msg/age-dev/UAjkvLoCr9I/l4Q1h3OPAgAJ.

All bech32 Go packages have funky APIs, internal types, or case
handling, so include a heavily refactored version of the reference
implementation, and the tests from github.com/btcsuite/btcutil/bech32.
2019-12-27 17:13:20 +01:00
Filippo Valsorda 8c600131ec cmd/age: print age-tool.com/report link on errors 2019-12-07 01:19:02 -05:00
Filippo Valsorda 0369d9b7fb cmd/age-keygen: detect world-readable output file 2019-12-07 00:59:41 -05:00
Filippo Valsorda 93ea79628d all: change module name to filippo.io/age
No need to tie ourselves to GitHub.

The redirect is not set up yet, but as long as there is a replace in the
go.mod the tool can't be installed with "go get" anyway.

Not using age-tool.com because A) I don't actually like the domain and
B) it should be about the spec not the specific implementation.
2019-12-07 00:59:41 -05:00
Filippo Valsorda aefae7ca0f cmd/age: autodetect --passphrase in --decrypt mode 2019-12-07 00:59:41 -05:00
Filippo Valsorda 470186da64 cmd/age: implement -p/--passphrase 2019-12-07 00:59:41 -05:00
Filippo Valsorda 1a5bcfeab9 .github: add issue templates (#23) 2019-12-07 00:58:07 -05:00
Filippo Valsorda 7ef2aa8a4e README: mention mailing list and Twitch 2019-11-28 18:44:00 -04:00
Filippo Valsorda a070570595 cmd/age: don't worry about sending decryption output to the terminal 2019-11-27 22:55:29 -04:00
Filippo Valsorda 025d8d5c0b cmd/age: fix support for password-protected ssh-ed25519 keys 2019-11-27 22:54:24 -04:00
Filippo Valsorda 03f7237541 internal/age: refactor to use the new golang.org/x/crypto/curve25519 API 2019-11-27 22:49:40 -04:00
Filippo Valsorda 9821fcefc9 internal/stream: fix TestRoundTrip 2019-11-27 22:00:48 -04:00
Filippo Valsorda f90681e0df cmd/age: detect output to the terminal and require opt-in or armor
Also, buffer the armor, so it doesn't get in the way of typing.
2019-11-27 19:40:59 -04:00
Filippo Valsorda 5ea3ee2f63 cmd/age: revamp CLI with -r/-i, long options, and input arg 2019-11-27 19:40:41 -04:00
Filippo Valsorda 779b033c8b cmd/age-keygen: move to a standalone binary 2019-11-27 18:56:08 -04:00
Filippo Valsorda 884b6f365d internal/age: support parsing armored files 2019-11-24 22:28:57 -05:00
Filippo Valsorda 4c4e446f72 internal/age,internal/format: implement armored file generation 2019-11-24 21:10:57 -05:00
Filippo Valsorda c624abc0ad cmd/age: add support for encrypted SSH key files 2019-11-24 19:15:53 -05:00
Filippo Valsorda 2cc62919a6 cmd/age: refactor parseIdentitiesFile 2019-10-17 10:31:44 -04:00
Filippo Valsorda 8f7e6dcd45 cmd/age: detect age and SSH private keys from file contents 2019-10-14 21:43:48 -04:00
Filippo Valsorda a5bf40786d cmd/age: add -i and -o flags 2019-10-14 21:09:11 -04:00
Filippo Valsorda 2a0aef5803 internal/age: use a prototype of X25519 from golang/go#32670 2019-10-13 18:14:54 -04:00
Filippo Valsorda 11fc3e293a internal/format: wrap body at 56 columns 2019-10-13 17:24:21 -04:00
Artyom Pervukhin 2d009c8eaf cmd/age: add missing close in parseIdentitiesFile (#14) 2019-10-13 16:34:09 -04:00
Dimitris Apostolou fe362d8f0a internal/age: fix typo in an ed25519PublicKeyToCurve25519 comment (#13) 2019-10-09 17:57:02 -04:00
Filippo Valsorda a99e57c53c internal/age: make the scrypt recipient work parameter log2(N)
Fixes #10
2019-10-08 23:24:13 -04:00
Filippo Valsorda c4269b6185 .travis.yml: enable Travis-CI (#5) 2019-10-07 22:39:10 -04:00
Matt Layher dd0939ffaa cmd/age: initial support for SSH identities and recipients
Signed-off-by: Matt Layher <mdlayher@gmail.com>
2019-10-07 22:14:24 -04:00
Filippo Valsorda 7f61cf23bf age: add README about the state of the implementation 2019-10-07 16:59:42 -04:00
Filippo Valsorda 022002171c internal/age: add ssh-ed25519 recipients 2019-10-07 00:45:59 -04:00
Filippo Valsorda 0940f184fb cmd/age: add a prototype of the command line tool 2019-10-06 23:16:20 -04:00
Filippo Valsorda 37d95cc84a all: remove AEAD marker
As Thomas convinced me, we can always add it back by bumping the
version, but the fewest knobs and joints we start with, the better.
2019-10-06 21:57:26 -04:00
Filippo Valsorda e9c118cea0 internal: implement STREAM, key exchange, encryption and decryption
Developed live over 6 hours of streaming on Twitch.

https://twitter.com/FiloSottile/status/1180875486911766528
2019-10-06 21:19:04 -04:00
Filippo Valsorda 52dbe9eecf internal/format: implement outer layer parsing and marshaling 2019-10-05 22:45:51 -04:00
Filippo Valsorda 06cbe4f91e Initial commit 2019-10-05 22:45:51 -04:00
29 changed files with 2974 additions and 5 deletions

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
---
name: Bug report
about: Create a report about a bug in this implementation.
title: ''
labels: ''
assignees: ''
---
## Environment
* OS:
* age version:
## What were you trying to do
## What happened
```
<insert terminal transcript here>
```
+21
View File
@@ -0,0 +1,21 @@
---
name: UX report
about: Was age hard to use? It's not you, it's us. We want to hear about it.
title: 'UX: '
labels: 'UX report'
assignees: ''
---
<!-- Did age not do what you expected?
Was it hard to figure out how to do something?
Could an error message be more helpful?
It's not you, it's us. We want to hear about it. -->
## What were you trying to do
## What happened
```
<insert terminal transcript here>
```
+5
View File
@@ -0,0 +1,5 @@
os: linux
arch: arm64
dist: bionic
language: go
go: 1.x
+21
View File
@@ -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
class Age < Formula
desc "Simple, modern, secure file encryption"
homepage "https://filippo.io/age"
url "https://github.com/FiloSottile/age/archive/v1.0.0-beta1.zip"
sha256 "6c7b1de0f312bc6e17e6b26ec27598672d1064e0287f202da5ab7efa9a1bb9d8"
depends_on "go" => :build
def install
ENV["GOPATH"] = HOMEBREW_CACHE/"go_cache"
system "mkdir", bin
system "go", "build", "-o", bin, "filippo.io/age/cmd/..."
prefix.install_metafiles
end
end
+27
View File
@@ -0,0 +1,27 @@
Copyright 2019 Google LLC
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-2
View File
@@ -1,2 +0,0 @@
This orphan master branch exists because it used to be checked out as
a Homebrew Tap, and "brew update" doesn't update the default branch.
+57
View File
@@ -0,0 +1,57 @@
# age
age is a simple, modern and secure file encryption tool.
I features small explicit keys, no config options, and UNIX-style composability.
```
$ 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
```
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 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.
## Usage
```
Usage:
age -r RECIPIENT [-a] [-o OUTPUT] [INPUT]
age --decrypt [-i KEY] [-o OUTPUT] [INPUT]
Options:
-o 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.
```
## Installation
On macOS, use Homebrew.
```
brew tap filippo.io/age https://filippo.io/age
brew install age
```
On Linux and Windows, use [the pre-built binaries](https://github.com/FiloSottile/age/releases) or build from source with Go 1.13+.
```
git clone https://filippo.io/age && cd age
go build filippo.io/cmd/age/...
```
Help from new packagers is very welcome.
+62
View File
@@ -0,0 +1,62 @@
// 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 (
"flag"
"fmt"
"log"
"os"
"time"
"filippo.io/age/internal/age"
"golang.org/x/crypto/ssh/terminal"
)
func main() {
log.SetFlags(0)
outFlag := flag.String("o", "", "output to `FILE` (default stdout)")
flag.Parse()
if len(flag.Args()) != 0 {
log.Fatalf("age-keygen takes no arguments")
}
out := os.Stdout
if name := *outFlag; name != "" {
f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
log.Fatalf("Failed to open output file %q: %v", name, err)
}
defer f.Close()
out = f
}
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")
}
}
generate(out)
}
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)
}
+235
View File
@@ -0,0 +1,235 @@
// 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 (
"bytes"
"flag"
"fmt"
"io"
_log "log"
"os"
"filippo.io/age/internal/age"
"golang.org/x/crypto/ssh/terminal"
)
type multiFlag []string
func (f *multiFlag) String() string { return fmt.Sprint(*f) }
func (f *multiFlag) Set(value string) error {
*f = append(*f, value)
return nil
}
const usage = `Usage:
age -r RECIPIENT [-a] [-o OUTPUT] [INPUT]
age --decrypt [-i KEY] [-o OUTPUT] [INPUT]
Options:
-o 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
decryptFlag, armorFlag, passFlag bool
recipientFlags, identityFlags multiFlag
)
flag.BoolVar(&decryptFlag, "d", false, "decrypt the input")
flag.BoolVar(&decryptFlag, "decrypt", false, "decrypt the input")
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.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)")
flag.Var(&recipientFlags, "recipient", "recipient (can be repeated)")
flag.Var(&identityFlags, "i", "identity (can be repeated)")
flag.Var(&identityFlags, "identity", "identity (can be repeated)")
flag.Parse()
if flag.NArg() > 1 {
logFatalf("Error: too many arguments.\n" +
"age accepts a single optional argument for the input file.")
}
switch {
case decryptFlag:
if armorFlag {
logFatalf("Error: -a/--armor can't be used with -d/--decrypt.\n" +
"Note that armored files are detected automatically.")
}
if passFlag {
logFatalf("Error: -p/--passphrase can't be used with -d/--decrypt.\n" +
"Note that password protected files are detected automatically.")
}
if len(recipientFlags) > 0 {
logFatalf("Error: -r/--recipient can't be used with -d/--decrypt.\n" +
"Did you mean to use -i/--identity to specify a private key?")
}
default: // encrypt
if len(identityFlags) > 0 {
logFatalf("Error: -i/--identity can't be used in encryption mode.\n" +
"Did you forget to specify -d/--decrypt?")
}
if len(recipientFlags) == 0 && !passFlag {
logFatalf("Error: missing recipients.\n" +
"Did you forget to specify -r/--recipient or -p/--passphrase?")
}
if len(recipientFlags) > 0 && passFlag {
logFatalf("Error: -p/--passphrase can't be combined with -r/--recipient.")
}
}
var in, out io.ReadWriter = os.Stdin, os.Stdout
if name := flag.Arg(0); name != "" && name != "-" {
f, err := os.Open(name)
if err != nil {
logFatalf("Error: failed to open input file %q: %v", name, err)
}
defer f.Close()
in = f
} else {
stdinInUse = true
}
if name := outFlag; name != "" && name != "-" {
f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
logFatalf("Error: failed to open output file %q: %v", name, err)
}
defer f.Close()
out = f
} else if terminal.IsTerminal(int(os.Stdout.Fd())) && !decryptFlag {
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 name != "-" {
// If the output wouldn't be armored, refuse to send binary to the
// terminal unless explicitly requested with "-o -".
logFatalf("Error: refusing to output binary to the terminal.\n" +
`Did you mean to use -a/--armor? Force with "-o -".`)
}
}
switch {
case decryptFlag:
decrypt(identityFlags, in, out)
case passFlag:
pass, err := passphrasePrompt()
if err != nil {
logFatalf("Error: %v", err)
}
encryptPass(pass, in, out, armorFlag)
default:
encryptKeys(recipientFlags, in, out, armorFlag)
}
}
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 encryptKeys(keys []string, in io.Reader, out io.Writer, armor bool) {
var recipients []age.Recipient
for _, arg := range keys {
r, err := parseRecipient(arg)
if err != nil {
logFatalf("Error: %v", err)
}
recipients = append(recipients, r)
}
encrypt(recipients, in, out, armor)
}
func encryptPass(pass string, in io.Reader, out io.Writer, armor bool) {
r, err := age.NewScryptRecipient(pass)
if err != nil {
logFatalf("Error: %v", err)
}
encrypt([]age.Recipient{r}, in, out, armor)
}
func encrypt(recipients []age.Recipient, in io.Reader, out io.Writer, armor bool) {
ageEncrypt := age.Encrypt
if armor {
ageEncrypt = age.EncryptWithArmor
}
w, err := ageEncrypt(out, recipients...)
if err != nil {
logFatalf("Error: %v", err)
}
if _, err := io.Copy(w, in); err != nil {
logFatalf("Error: %v", err)
}
if err := w.Close(); err != nil {
logFatalf("Error: %v", err)
}
}
func decrypt(keys []string, in io.Reader, out io.Writer) {
identities := []age.Identity{
// If there is an scrypt recipient (it will have to be the only one and)
// this identity will be invoked.
&LazyScryptIdentity{passphrasePrompt},
}
// TODO: use the default location if no arguments are provided:
// os.UserConfigDir()/age/keys.txt, ~/.ssh/id_rsa, ~/.ssh/id_ed25519
for _, name := range keys {
ids, err := parseIdentitiesFile(name)
if err != nil {
logFatalf("Error: %v", err)
}
identities = append(identities, ids...)
}
r, err := age.Decrypt(in, identities...)
if err != nil {
logFatalf("Error: %v", err)
}
if _, err := io.Copy(out, r); err != nil {
logFatalf("Error: %v", err)
}
}
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?" +
" Tell us: https://filippo.io/age/report ]")
}
+135
View File
@@ -0,0 +1,135 @@
// 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/ed25519"
"crypto/rsa"
"fmt"
"os"
"filippo.io/age/internal/age"
"filippo.io/age/internal/format"
"golang.org/x/crypto/ssh"
"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)
}
var _ age.Identity = &LazyScryptIdentity{}
func (i *LazyScryptIdentity) Type() string {
return "scrypt"
}
func (i *LazyScryptIdentity) Unwrap(block *format.Recipient) (fileKey []byte, err error) {
pass, err := i.Passphrase()
if err != nil {
return nil, fmt.Errorf("could not read passphrase: %v", err)
}
ii, err := age.NewScryptIdentity(pass)
if err != nil {
return nil, err
}
return ii.Unwrap(block)
}
// stdinInUse is set in main. It's a singleton like os.Stdin.
var stdinInUse bool
func readPassphrase() ([]byte, error) {
fd := int(os.Stdin.Fd())
if !terminal.IsTerminal(fd) || stdinInUse {
tty, err := os.Open("/dev/tty")
if err != nil {
return nil, fmt.Errorf("standard input is not available or not a terminal, and opening /dev/tty failed: %v", err)
}
defer tty.Close()
fd = int(tty.Fd())
}
defer fmt.Fprintf(os.Stderr, "\n")
p, err := terminal.ReadPassword(fd)
if err != nil {
return nil, err
}
return p, nil
}
+132
View File
@@ -0,0 +1,132 @@
// Copyright 2019 Google LLC
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"filippo.io/age/internal/age"
"golang.org/x/crypto/ssh"
)
func parseRecipient(arg string) (age.Recipient, error) {
switch {
case strings.HasPrefix(arg, "age1"):
return age.ParseX25519Recipient(arg)
case strings.HasPrefix(arg, "ssh-"):
return age.ParseSSHRecipient(arg)
}
return nil, fmt.Errorf("unknown recipient type: %q", arg)
}
const privateKeySizeLimit = 1 << 24 // 16 MiB
func parseIdentitiesFile(name string) ([]age.Identity, error) {
f, err := os.Open(name)
if err != nil {
return nil, fmt.Errorf("failed to open file: %v", err)
}
defer f.Close()
contents, err := ioutil.ReadAll(io.LimitReader(f, privateKeySizeLimit))
if err != nil {
return nil, fmt.Errorf("failed to read %q: %v", name, err)
}
if len(contents) == privateKeySizeLimit {
return nil, fmt.Errorf("failed to read %q: file too long", name)
}
var ids []age.Identity
var ageParsingError error
scanner := bufio.NewScanner(bytes.NewReader(contents))
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "#") || line == "" {
continue
}
if strings.HasPrefix(line, "-----BEGIN") {
return parseSSHIdentity(name, contents)
}
if ageParsingError != nil {
continue
}
i, err := age.ParseX25519Identity(line)
if err != nil {
ageParsingError = fmt.Errorf("malformed secret keys file %q: %v", name, err)
continue
}
ids = append(ids, i)
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("failed to read %q: %v", name, err)
}
if ageParsingError != nil {
return nil, ageParsingError
}
if len(ids) == 0 {
return nil, fmt.Errorf("no secret keys found in %q", name)
}
return ids, nil
}
func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {
id, err := age.ParseSSHIdentity(pemBytes)
if sshErr, ok := err.(*ssh.PassphraseNeededError); ok {
pubKey := sshErr.PublicKey
if pubKey == nil {
pubKey, err = readPubFile(name)
if err != nil {
return nil, err
}
}
passphrasePrompt := func() ([]byte, error) {
fmt.Fprintf(os.Stderr, "Enter passphrase for %q: ", name)
pass, err := readPassphrase()
if err != nil {
return nil, fmt.Errorf("could not read passphrase for %q: %v", name, err)
}
return pass, nil
}
i, err := NewEncryptedSSHIdentity(pubKey, pemBytes, passphrasePrompt)
if err != nil {
return nil, err
}
return []age.Identity{i}, nil
}
if err != nil {
return nil, fmt.Errorf("malformed SSH identity in %q: %v", name, err)
}
return []age.Identity{id}, nil
}
func readPubFile(name string) (ssh.PublicKey, error) {
f, err := os.Open(name + ".pub")
if err != nil {
return nil, fmt.Errorf(`failed to obtain public key for %q SSH key: %v
Ensure %q exists, or convert the private key %q to a modern format with "ssh-keygen -p -m RFC4716"`, name, err, name+".pub", name)
}
defer f.Close()
contents, err := ioutil.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("failed to read %q: %v", name+".pub", err)
}
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(contents)
if err != nil {
return nil, fmt.Errorf("failed to parse %q: %v", name+".pub", err)
}
return pubKey, nil
}
+7
View File
@@ -0,0 +1,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
+6
View File
@@ -0,0 +1,6 @@
github.com/Filosottile/go v0.0.0-20191122011136-9090b284250b h1:4AVIiSN9FRvfh7Oq7NhMHoU4oDhNkpfq4q9prQNlq7k=
github.com/Filosottile/go v0.0.0-20191122011136-9090b284250b/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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=
+154
View File
@@ -0,0 +1,154 @@
// 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 implements file encryption according to age-encryption.org/v1.
package age
import (
"crypto/hmac"
"crypto/rand"
"errors"
"fmt"
"io"
"filippo.io/age/internal/format"
"filippo.io/age/internal/stream"
)
type Identity interface {
Type() string
Unwrap(block *format.Recipient) (fileKey []byte, err error)
}
type IdentityMatcher interface {
Identity
Matches(block *format.Recipient) error
}
var ErrIncorrectIdentity = errors.New("incorrect identity for recipient block")
type Recipient interface {
Type() string
Wrap(fileKey []byte) (*format.Recipient, error)
}
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")
}
fileKey := make([]byte, 16)
if _, err := rand.Read(fileKey); err != nil {
return nil, err
}
hdr := &format.Header{}
for i, r := range recipients {
if r.Type() == "scrypt" && len(recipients) != 1 {
return nil, errors.New("an scrypt recipient must be the only one")
}
block, err := r.Wrap(fileKey)
if err != nil {
return nil, fmt.Errorf("failed to wrap key for recipient #%d: %v", i, err)
}
hdr.Recipients = append(hdr.Recipients, block)
}
if mac, err := headerMAC(fileKey, hdr); err != nil {
return nil, fmt.Errorf("failed to compute header MAC: %v", err)
} else {
hdr.MAC = mac
}
if err := hdr.Marshal(dst); err != nil {
return nil, fmt.Errorf("failed to write header: %v", err)
}
nonce := make([]byte, 16)
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
if _, err := dst.Write(nonce); err != nil {
return nil, fmt.Errorf("failed to write nonce: %v", err)
}
return stream.NewWriter(streamKey(fileKey, nonce), dst)
}
func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {
if len(identities) == 0 {
return nil, errors.New("no identities specified")
}
hdr, payload, err := format.Parse(src)
if err != nil {
return nil, fmt.Errorf("failed to read header: %v", err)
}
if len(hdr.Recipients) > 20 {
return nil, errors.New("too many recipients")
}
var fileKey []byte
RecipientsLoop:
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")
}
for _, i := range identities {
if i.Type() != r.Type {
continue
}
if i, ok := i.(IdentityMatcher); ok {
err := i.Matches(r)
if err != nil {
if err == ErrIncorrectIdentity {
continue
}
return nil, err
}
}
fileKey, err = i.Unwrap(r)
if err != nil {
if err == ErrIncorrectIdentity {
continue
}
return nil, err
}
break RecipientsLoop
}
}
if fileKey == nil {
return nil, errors.New("no identity matched a recipient")
}
if mac, err := headerMAC(fileKey, hdr); err != nil {
return nil, fmt.Errorf("failed to compute header MAC: %v", err)
} else if !hmac.Equal(mac, hdr.MAC) {
return nil, errors.New("bad header MAC")
}
nonce := make([]byte, 16)
if _, err := io.ReadFull(payload, nonce); err != nil {
return nil, fmt.Errorf("failed to read nonce: %v", err)
}
return stream.NewReader(streamKey(fileKey, nonce), payload)
}
+110
View File
@@ -0,0 +1,110 @@
// 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)
}
}
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2019 Google LLC
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
package age
import (
"crypto/hmac"
"crypto/sha256"
"io"
"filippo.io/age/internal/format"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/hkdf"
)
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)
}
func headerMAC(fileKey []byte, hdr *format.Header) ([]byte, error) {
h := hkdf.New(sha256.New, fileKey, nil, []byte("header"))
hmacKey := make([]byte, 32)
if _, err := io.ReadFull(h, hmacKey); err != nil {
return nil, err
}
hh := hmac.New(sha256.New, hmacKey)
if err := hdr.MarshalWithoutMAC(hh); err != nil {
return nil, err
}
return hh.Sum(nil), nil
}
func streamKey(fileKey, nonce []byte) []byte {
h := hkdf.New(sha256.New, fileKey, nonce, []byte("payload"))
streamKey := make([]byte, chacha20poly1305.KeySize)
if _, err := io.ReadFull(h, streamKey); err != nil {
panic("age: internal error: failed to read from HKDF: " + err.Error())
}
return streamKey
}
+201
View File
@@ -0,0 +1,201 @@
// 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)
}
}
+144
View File
@@ -0,0 +1,144 @@
// Copyright 2019 Google LLC
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
package age
import (
"crypto/rand"
"errors"
"fmt"
"strconv"
"filippo.io/age/internal/format"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/scrypt"
)
const scryptLabel = "age-encryption.org/v1/scrypt"
type ScryptRecipient struct {
password []byte
workFactor int
}
var _ Recipient = &ScryptRecipient{}
func (*ScryptRecipient) Type() string { return "scrypt" }
func NewScryptRecipient(password string) (*ScryptRecipient, error) {
if len(password) == 0 {
return nil, errors.New("empty scrypt password")
}
r := &ScryptRecipient{
password: []byte(password),
// TODO: automatically scale this to 1s (with a min) in the CLI.
workFactor: 18, // 1s on a modern machine
}
return r, nil
}
// SetWorkFactor sets the scrypt work factor to 2^logN.
// It must be called before Wrap.
func (r *ScryptRecipient) SetWorkFactor(logN int) {
if logN > 30 || logN < 1 {
panic("age: SetWorkFactor called with illegal value")
}
r.workFactor = logN
}
func (r *ScryptRecipient) Wrap(fileKey []byte) (*format.Recipient, error) {
salt := make([]byte, 16)
if _, err := rand.Read(salt[:]); err != nil {
return nil, err
}
logN := r.workFactor
l := &format.Recipient{
Type: "scrypt",
Args: []string{format.EncodeToString(salt), strconv.Itoa(logN)},
}
salt = append([]byte(scryptLabel), salt...)
k, err := scrypt.Key(r.password, salt, 1<<logN, 8, 1, chacha20poly1305.KeySize)
if err != nil {
return nil, fmt.Errorf("failed to generate scrypt hash: %v", err)
}
wrappedKey, err := aeadEncrypt(k, fileKey)
if err != nil {
return nil, err
}
l.Body = wrappedKey
return l, nil
}
type ScryptIdentity struct {
password []byte
maxWorkFactor int
}
var _ Identity = &ScryptIdentity{}
func (*ScryptIdentity) Type() string { return "scrypt" }
func NewScryptIdentity(password string) (*ScryptIdentity, error) {
if len(password) == 0 {
return nil, errors.New("empty scrypt password")
}
i := &ScryptIdentity{
password: []byte(password),
maxWorkFactor: 22, // 15s on a modern machine
}
return i, nil
}
// SetWorkFactor sets the maximum accepted scrypt work factor to 2^logN.
// It must be called before Unwrap.
func (i *ScryptIdentity) SetMaxWorkFactor(logN int) {
if logN > 30 || logN < 1 {
panic("age: SetMaxWorkFactor called with illegal value")
}
i.maxWorkFactor = logN
}
func (i *ScryptIdentity) Unwrap(block *format.Recipient) ([]byte, error) {
if block.Type != "scrypt" {
return nil, ErrIncorrectIdentity
}
if len(block.Args) != 2 {
return nil, errors.New("invalid scrypt recipient block")
}
salt, err := format.DecodeString(block.Args[0])
if err != nil {
return nil, fmt.Errorf("failed to parse scrypt salt: %v", err)
}
if len(salt) != 16 {
return nil, errors.New("invalid scrypt recipient block")
}
logN, err := strconv.Atoi(block.Args[1])
if err != nil {
return nil, fmt.Errorf("failed to parse scrypt work factor: %v", err)
}
if logN > i.maxWorkFactor {
return nil, fmt.Errorf("scrypt work factor too large: %v", logN)
}
if logN <= 0 {
return nil, fmt.Errorf("invalid scrypt work factor: %v", logN)
}
salt = append([]byte(scryptLabel), salt...)
k, err := scrypt.Key(i.password, salt, 1<<logN, 8, 1, chacha20poly1305.KeySize)
if err != nil {
return nil, fmt.Errorf("failed to generate scrypt hash: %v", err)
}
fileKey, err := aeadDecrypt(k, block.Body)
if err != nil {
return nil, ErrIncorrectIdentity
}
return fileKey, nil
}
+341
View File
@@ -0,0 +1,341 @@
// Copyright 2019 Google LLC
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
package age
import (
"crypto/ed25519"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/sha512"
"errors"
"fmt"
"io"
"math/big"
"filippo.io/age/internal/format"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/hkdf"
"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])
}
const oaepLabel = "age-encryption.org/v1/ssh-rsa"
type SSHRSARecipient struct {
sshKey ssh.PublicKey
pubKey *rsa.PublicKey
}
var _ Recipient = &SSHRSARecipient{}
func (*SSHRSARecipient) Type() string { return "ssh-rsa" }
func NewSSHRSARecipient(pk ssh.PublicKey) (*SSHRSARecipient, error) {
if pk.Type() != "ssh-rsa" {
return nil, errors.New("SSH public key is not an RSA key")
}
r := &SSHRSARecipient{
sshKey: pk,
}
if pk, ok := pk.(ssh.CryptoPublicKey); ok {
if pk, ok := pk.CryptoPublicKey().(*rsa.PublicKey); ok {
r.pubKey = pk
} else {
return nil, errors.New("unexpected public key type")
}
} else {
return nil, errors.New("pk does not implement ssh.CryptoPublicKey")
}
return r, nil
}
func (r *SSHRSARecipient) Wrap(fileKey []byte) (*format.Recipient, error) {
l := &format.Recipient{
Type: "ssh-rsa",
Args: []string{SSHFingerprint(r.sshKey)},
}
wrappedKey, err := rsa.EncryptOAEP(sha256.New(), rand.Reader,
r.pubKey, fileKey, []byte(oaepLabel))
if err != nil {
return nil, err
}
l.Body = wrappedKey
return l, nil
}
type SSHRSAIdentity struct {
k *rsa.PrivateKey
sshKey ssh.PublicKey
}
var _ Identity = &SSHRSAIdentity{}
func (*SSHRSAIdentity) Type() string { return "ssh-rsa" }
func NewSSHRSAIdentity(key *rsa.PrivateKey) (*SSHRSAIdentity, error) {
s, err := ssh.NewSignerFromKey(key)
if err != nil {
return nil, err
}
i := &SSHRSAIdentity{
k: key, sshKey: s.PublicKey(),
}
return i, nil
}
func (i *SSHRSAIdentity) Unwrap(block *format.Recipient) ([]byte, error) {
if block.Type != "ssh-rsa" {
return nil, 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
}
fileKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, i.k,
block.Body, []byte(oaepLabel))
if err != nil {
return nil, fmt.Errorf("failed to decrypt file key: %v", err)
}
return fileKey, nil
}
type SSHEd25519Recipient struct {
sshKey ssh.PublicKey
theirPublicKey []byte
}
var _ Recipient = &SSHEd25519Recipient{}
func (*SSHEd25519Recipient) Type() string { return "ssh-ed25519" }
func NewSSHEd25519Recipient(pk ssh.PublicKey) (*SSHEd25519Recipient, error) {
if pk.Type() != "ssh-ed25519" {
return nil, errors.New("SSH public key is not an Ed25519 key")
}
r := &SSHEd25519Recipient{
sshKey: pk,
}
if pk, ok := pk.(ssh.CryptoPublicKey); ok {
if pk, ok := pk.CryptoPublicKey().(ed25519.PublicKey); ok {
r.theirPublicKey = ed25519PublicKeyToCurve25519(pk)
} else {
return nil, errors.New("unexpected public key type")
}
} else {
return nil, errors.New("pk does not implement ssh.CryptoPublicKey")
}
return r, nil
}
func ParseSSHRecipient(s string) (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
switch t := pubKey.Type(); t {
case "ssh-rsa":
r, err = NewSSHRSARecipient(pubKey)
case "ssh-ed25519":
r, err = NewSSHEd25519Recipient(pubKey)
default:
return nil, fmt.Errorf("unknown SSH recipient type: %q", t)
}
if err != nil {
return nil, fmt.Errorf("malformed SSH recipient: %q: %v", s, err)
}
return r, nil
}
var curve25519P, _ = new(big.Int).SetString("57896044618658097711785492504343953926634992332820282019728792003956564819949", 10)
func ed25519PublicKeyToCurve25519(pk ed25519.PublicKey) []byte {
// ed25519.PublicKey is a little endian representation of the y-coordinate,
// with the most significant bit set based on the sign of the x-coordinate.
bigEndianY := make([]byte, ed25519.PublicKeySize)
for i, b := range pk {
bigEndianY[ed25519.PublicKeySize-i-1] = b
}
bigEndianY[0] &= 0b0111_1111
// The Montgomery u-coordinate is derived through the bilinear map
//
// u = (1 + y) / (1 - y)
//
// See https://blog.filippo.io/using-ed25519-keys-for-encryption.
y := new(big.Int).SetBytes(bigEndianY)
denom := big.NewInt(1)
denom.ModInverse(denom.Sub(denom, y), curve25519P) // 1 / (1 - y)
u := y.Mul(y.Add(y, big.NewInt(1)), denom)
u.Mod(u, curve25519P)
out := make([]byte, curve25519.PointSize)
uBytes := u.Bytes()
for i, b := range uBytes {
out[len(uBytes)-i-1] = b
}
return out
}
const ed25519Label = "age-encryption.org/v1/ssh-ed25519"
func (r *SSHEd25519Recipient) Wrap(fileKey []byte) (*format.Recipient, error) {
ephemeral := make([]byte, curve25519.ScalarSize)
if _, err := rand.Read(ephemeral); err != nil {
return nil, err
}
ourPublicKey, err := curve25519.X25519(ephemeral, curve25519.Basepoint)
if err != nil {
return nil, err
}
sharedSecret, err := curve25519.X25519(ephemeral, r.theirPublicKey)
if err != nil {
return nil, err
}
tweak := make([]byte, curve25519.ScalarSize)
tH := hkdf.New(sha256.New, nil, r.sshKey.Marshal(), []byte(ed25519Label))
if _, err := io.ReadFull(tH, tweak); err != nil {
return nil, err
}
sharedSecret, _ = curve25519.X25519(tweak, sharedSecret)
l := &format.Recipient{
Type: "ssh-ed25519",
Args: []string{SSHFingerprint(r.sshKey),
format.EncodeToString(ourPublicKey[:])},
}
salt := make([]byte, 0, len(ourPublicKey)+len(r.theirPublicKey))
salt = append(salt, ourPublicKey...)
salt = append(salt, r.theirPublicKey...)
h := hkdf.New(sha256.New, sharedSecret, salt, []byte(ed25519Label))
wrappingKey := make([]byte, chacha20poly1305.KeySize)
if _, err := io.ReadFull(h, wrappingKey); err != nil {
return nil, err
}
wrappedKey, err := aeadEncrypt(wrappingKey, fileKey)
if err != nil {
return nil, err
}
l.Body = wrappedKey
return l, nil
}
type SSHEd25519Identity struct {
secretKey, ourPublicKey []byte
sshKey ssh.PublicKey
}
var _ Identity = &SSHEd25519Identity{}
func (*SSHEd25519Identity) Type() string { return "ssh-ed25519" }
func NewSSHEd25519Identity(key ed25519.PrivateKey) (*SSHEd25519Identity, error) {
s, err := ssh.NewSignerFromKey(key)
if err != nil {
return nil, err
}
i := &SSHEd25519Identity{
sshKey: s.PublicKey(),
secretKey: ed25519PrivateKeyToCurve25519(key),
}
i.ourPublicKey, _ = curve25519.X25519(i.secretKey, curve25519.Basepoint)
return i, nil
}
func ParseSSHIdentity(pemBytes []byte) (Identity, error) {
k, err := ssh.ParseRawPrivateKey(pemBytes)
if err != nil {
return nil, err
}
switch k := k.(type) {
case *ed25519.PrivateKey:
return NewSSHEd25519Identity(*k)
case *rsa.PrivateKey:
return NewSSHRSAIdentity(k)
}
return nil, fmt.Errorf("unsupported SSH identity type: %T", k)
}
func ed25519PrivateKeyToCurve25519(pk ed25519.PrivateKey) []byte {
h := sha512.New()
h.Write(pk.Seed())
out := h.Sum(nil)
return out[:curve25519.ScalarSize]
}
func (i *SSHEd25519Identity) Unwrap(block *format.Recipient) ([]byte, error) {
if block.Type != "ssh-ed25519" {
return nil, ErrIncorrectIdentity
}
if len(block.Args) != 2 {
return nil, errors.New("invalid ssh-ed25519 recipient block")
}
publicKey, err := format.DecodeString(block.Args[1])
if err != nil {
return nil, fmt.Errorf("failed to parse ssh-ed25519 recipient: %v", err)
}
if len(publicKey) != curve25519.PointSize {
return nil, errors.New("invalid ssh-ed25519 recipient block")
}
if block.Args[0] != SSHFingerprint(i.sshKey) {
return nil, ErrIncorrectIdentity
}
sharedSecret, err := curve25519.X25519(i.secretKey, publicKey)
if err != nil {
return nil, fmt.Errorf("invalid X25519 recipient: %v", err)
}
tweak := make([]byte, curve25519.ScalarSize)
tH := hkdf.New(sha256.New, nil, i.sshKey.Marshal(), []byte(ed25519Label))
if _, err := io.ReadFull(tH, tweak); err != nil {
return nil, err
}
sharedSecret, _ = curve25519.X25519(tweak, sharedSecret)
salt := make([]byte, 0, len(publicKey)+len(i.ourPublicKey))
salt = append(salt, publicKey...)
salt = append(salt, i.ourPublicKey...)
h := hkdf.New(sha256.New, sharedSecret, salt, []byte(ed25519Label))
wrappingKey := make([]byte, chacha20poly1305.KeySize)
if _, err := io.ReadFull(h, wrappingKey); err != nil {
return nil, err
}
fileKey, err := aeadDecrypt(wrappingKey, block.Body)
if err != nil {
return nil, fmt.Errorf("failed to decrypt file key: %v", err)
}
return fileKey, nil
}
+191
View File
@@ -0,0 +1,191 @@
// Copyright 2019 Google LLC
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
package age
import (
"crypto/rand"
"crypto/sha256"
"errors"
"fmt"
"io"
"strings"
"filippo.io/age/internal/bech32"
"filippo.io/age/internal/format"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/hkdf"
)
const x25519Label = "age-encryption.org/v1/X25519"
type X25519Recipient struct {
theirPublicKey []byte
}
var _ Recipient = &X25519Recipient{}
func (*X25519Recipient) Type() string { return "X25519" }
func NewX25519Recipient(publicKey []byte) (*X25519Recipient, error) {
if len(publicKey) != curve25519.PointSize {
return nil, errors.New("invalid X25519 public key")
}
r := &X25519Recipient{
theirPublicKey: make([]byte, curve25519.PointSize),
}
copy(r.theirPublicKey, publicKey)
return r, nil
}
func ParseX25519Recipient(s string) (*X25519Recipient, error) {
t, k, err := bech32.Decode(s)
if err != nil {
return nil, fmt.Errorf("malformed recipient %q: %v", s, err)
}
if t != "age" {
return nil, fmt.Errorf("malformed recipient %q: invalid type %q", s, t)
}
r, err := NewX25519Recipient(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) {
ephemeral := make([]byte, curve25519.ScalarSize)
if _, err := rand.Read(ephemeral); err != nil {
return nil, err
}
ourPublicKey, err := curve25519.X25519(ephemeral, curve25519.Basepoint)
if err != nil {
return nil, err
}
sharedSecret, err := curve25519.X25519(ephemeral, r.theirPublicKey)
if err != nil {
return nil, err
}
l := &format.Recipient{
Type: "X25519",
Args: []string{format.EncodeToString(ourPublicKey)},
}
salt := make([]byte, 0, len(ourPublicKey)+len(r.theirPublicKey))
salt = append(salt, ourPublicKey...)
salt = append(salt, r.theirPublicKey...)
h := hkdf.New(sha256.New, sharedSecret, salt, []byte(x25519Label))
wrappingKey := make([]byte, chacha20poly1305.KeySize)
if _, err := io.ReadFull(h, wrappingKey); err != nil {
return nil, err
}
wrappedKey, err := aeadEncrypt(wrappingKey, fileKey)
if err != nil {
return nil, err
}
l.Body = wrappedKey
return l, nil
}
func (r *X25519Recipient) String() string {
s, _ := bech32.Encode("age", r.theirPublicKey)
return s
}
type X25519Identity struct {
secretKey, ourPublicKey []byte
}
var _ Identity = &X25519Identity{}
func (*X25519Identity) Type() string { return "X25519" }
func NewX25519Identity(secretKey []byte) (*X25519Identity, error) {
if len(secretKey) != curve25519.ScalarSize {
return nil, errors.New("invalid X25519 secret key")
}
i := &X25519Identity{
secretKey: make([]byte, curve25519.ScalarSize),
}
copy(i.secretKey, secretKey)
i.ourPublicKey, _ = curve25519.X25519(i.secretKey, curve25519.Basepoint)
return i, nil
}
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)
}
func ParseX25519Identity(s string) (*X25519Identity, error) {
t, k, err := bech32.Decode(s)
if err != nil {
return nil, fmt.Errorf("malformed secret key %q: %v", s, err)
}
if t != "AGE-SECRET-KEY-" {
return nil, fmt.Errorf("malformed secret key %q: invalid type %q", s, t)
}
r, err := NewX25519Identity(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) {
if block.Type != "X25519" {
return nil, ErrIncorrectIdentity
}
if len(block.Args) != 1 {
return nil, errors.New("invalid X25519 recipient block")
}
publicKey, err := format.DecodeString(block.Args[0])
if err != nil {
return nil, fmt.Errorf("failed to parse X25519 recipient: %v", err)
}
if len(publicKey) != curve25519.PointSize {
return nil, errors.New("invalid X25519 recipient block")
}
sharedSecret, err := curve25519.X25519(i.secretKey, publicKey)
if err != nil {
return nil, fmt.Errorf("invalid X25519 recipient: %v", err)
}
salt := make([]byte, 0, len(publicKey)+len(i.ourPublicKey))
salt = append(salt, publicKey...)
salt = append(salt, i.ourPublicKey...)
h := hkdf.New(sha256.New, sharedSecret, salt, []byte(x25519Label))
wrappingKey := make([]byte, chacha20poly1305.KeySize)
if _, err := io.ReadFull(h, wrappingKey); err != nil {
return nil, err
}
fileKey, err := aeadDecrypt(wrappingKey, block.Body)
if err != nil {
return nil, ErrIncorrectIdentity
}
return fileKey, nil
}
func (i *X25519Identity) Recipient() *X25519Recipient {
r := &X25519Recipient{}
r.theirPublicKey = i.ourPublicKey
return r
}
func (i *X25519Identity) String() string {
s, _ := bech32.Encode("AGE-SECRET-KEY-", i.secretKey)
return strings.ToUpper(s)
}
+179
View File
@@ -0,0 +1,179 @@
// Copyright (c) 2017 Takatoshi Nakagawa
// Copyright (c) 2019 Google LLC
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// Package bech32 is a modified version of the reference implementation of BIP173.
package bech32
import (
"fmt"
"strings"
)
var charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
var generator = []uint32{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}
func polymod(values []byte) uint32 {
chk := uint32(1)
for _, v := range values {
top := chk >> 25
chk = (chk & 0x1ffffff) << 5
chk = chk ^ uint32(v)
for i := 0; i < 5; i++ {
bit := top >> i & 1
if bit == 1 {
chk ^= generator[i]
}
}
}
return chk
}
func hrpExpand(hrp string) []byte {
h := []byte(strings.ToLower(hrp))
var ret []byte
for _, c := range h {
ret = append(ret, c>>5)
}
ret = append(ret, 0)
for _, c := range h {
ret = append(ret, c&31)
}
return ret
}
func verifyChecksum(hrp string, data []byte) bool {
return polymod(append(hrpExpand(hrp), data...)) == 1
}
func createChecksum(hrp string, data []byte) []byte {
values := append(hrpExpand(hrp), data...)
values = append(values, []byte{0, 0, 0, 0, 0, 0}...)
mod := polymod(values) ^ 1
ret := make([]byte, 6)
for p := range ret {
shift := 5 * (5 - p)
ret[p] = byte(mod>>shift) & 31
}
return ret
}
func convertBits(data []byte, frombits, tobits byte, pad bool) ([]byte, error) {
var ret []byte
acc := uint32(0)
bits := byte(0)
maxv := byte(1<<tobits - 1)
for idx, value := range data {
if value>>frombits != 0 {
return nil, fmt.Errorf("invalid data range: data[%d]=%d (frombits=%d)", idx, value, frombits)
}
acc = acc<<frombits | uint32(value)
bits += frombits
for bits >= tobits {
bits -= tobits
ret = append(ret, byte(acc>>bits)&maxv)
}
}
if pad {
if bits > 0 {
ret = append(ret, byte(acc<<(tobits-bits))&maxv)
}
} else if bits >= frombits {
return nil, fmt.Errorf("illegal zero padding")
} else if byte(acc<<(tobits-bits))&maxv != 0 {
return nil, fmt.Errorf("non-zero padding")
}
return ret, nil
}
// Encode encodes the HRP and a bytes slice to Bech32. If the HRP is uppercase,
// the output will be uppercase.
func Encode(hrp string, data []byte) (string, error) {
values, err := convertBits(data, 8, 5, true)
if err != nil {
return "", err
}
if len(hrp)+len(values)+7 > 90 {
return "", fmt.Errorf("too long: hrp length=%d, data length=%d", len(hrp), len(values))
}
if len(hrp) < 1 {
return "", fmt.Errorf("invalid HRP: %q", hrp)
}
for p, c := range hrp {
if c < 33 || c > 126 {
return "", fmt.Errorf("invalid HRP character: hrp[%d]=%d", p, c)
}
}
if strings.ToUpper(hrp) != hrp && strings.ToLower(hrp) != hrp {
return "", fmt.Errorf("mixed case HRP: %q", hrp)
}
lower := strings.ToLower(hrp) == hrp
hrp = strings.ToLower(hrp)
var ret strings.Builder
ret.WriteString(hrp)
ret.WriteString("1")
for _, p := range values {
ret.WriteByte(charset[p])
}
for _, p := range createChecksum(hrp, values) {
ret.WriteByte(charset[p])
}
if lower {
return ret.String(), nil
}
return strings.ToUpper(ret.String()), nil
}
// Decode decodes a Bech32 string. If the string is uppercase, the HRP will be uppercase.
func Decode(s string) (hrp string, data []byte, err error) {
if len(s) > 90 {
return "", nil, fmt.Errorf("too long: len=%d", len(s))
}
if strings.ToLower(s) != s && strings.ToUpper(s) != s {
return "", nil, fmt.Errorf("mixed case")
}
pos := strings.LastIndex(s, "1")
if pos < 1 || pos+7 > len(s) {
return "", nil, fmt.Errorf("separator '1' at invalid position: pos=%d, len=%d", pos, len(s))
}
hrp = s[:pos]
for p, c := range hrp {
if c < 33 || c > 126 {
return "", nil, fmt.Errorf("invalid character human-readable part: s[%d]=%d", p, c)
}
}
s = strings.ToLower(s)
for p, c := range s[pos+1:] {
d := strings.IndexRune(charset, c)
if d == -1 {
return "", nil, fmt.Errorf("invalid character data part: s[%d]=%v", p, c)
}
data = append(data, byte(d))
}
if !verifyChecksum(hrp, data) {
return "", nil, fmt.Errorf("invalid checksum")
}
data, err = convertBits(data[:len(data)-6], 5, 8, false)
if err != nil {
return "", nil, err
}
return hrp, data, nil
}
+94
View File
@@ -0,0 +1,94 @@
// Copyright (c) 2013-2017 The btcsuite developers
// Copyright (c) 2016-2017 The Lightning Network Developers
// Copyright (c) 2019 Google LLC
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package bech32_test
import (
"strings"
"testing"
"filippo.io/age/internal/bech32"
)
func TestBech32(t *testing.T) {
tests := []struct {
str string
valid bool
}{
{"A12UEL5L", true},
{"a12uel5l", true},
{"an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", true},
{"abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", true},
{"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", true},
{"split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", true},
// invalid checksum
{"split1checkupstagehandshakeupstreamerranterredcaperred2y9e2w", false},
// invalid character (space) in hrp
{"s lit1checkupstagehandshakeupstreamerranterredcaperredp8hs2p", false},
{"split1cheo2y9e2w", false}, // invalid character (o) in data part
{"split1a2y9w", false}, // too short data part
{"1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", false}, // empty hrp
// invalid character (DEL) in hrp
{"spl" + string(127) + "t1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", false},
// too long
{"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", false},
// BIP 173 invalid vectors.
{"an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", false},
{"pzry9x0s0muk", false},
{"1pzry9x0s0muk", false},
{"x1b4n0q5v", false},
{"li1dgmt3", false},
{"de1lg7wt\xff", false},
{"A1G7SGD8", false},
{"10a06t8", false},
{"1qzzfhee", false},
}
for _, test := range tests {
str := test.str
hrp, decoded, err := bech32.Decode(str)
if !test.valid {
// Invalid string decoding should result in error.
if err == nil {
t.Errorf("expected decoding to fail for invalid string %v", test.str)
}
continue
}
// Valid string decoding should result in no error.
if err != nil {
t.Errorf("expected string to be valid bech32: %v", err)
}
// Check that it encodes to the same string.
encoded, err := bech32.Encode(hrp, decoded)
if err != nil {
t.Errorf("encoding failed: %v", err)
}
if encoded != str {
t.Errorf("expected data to encode to %v, but got %v", str, encoded)
}
// Flip a bit in the string an make sure it is caught.
pos := strings.LastIndexAny(str, "1")
flipped := str[:pos+1] + string((str[pos+1] ^ 1)) + str[pos+2:]
if _, _, err = bech32.Decode(flipped); err == nil {
t.Error("expected decoding to fail")
}
}
}
+174
View File
@@ -0,0 +1,174 @@
// Copyright 2019 Google LLC
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
package format
import (
"bufio"
"bytes"
"encoding/base64"
"errors"
"io"
)
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-----"
type armoredWriter struct {
started, closed bool
encoder io.WriteCloser
dst io.Writer
}
func (a *armoredWriter) Write(p []byte) (int, error) {
if !a.started {
if _, err := io.WriteString(a.dst, armorPreamble+"\n"); err != nil {
return 0, err
}
}
a.started = true
return a.encoder.Write(p)
}
func (a *armoredWriter) Close() error {
if a.closed {
return errors.New("ArmoredWriter already closed")
}
a.closed = true
if err := a.encoder.Close(); err != nil {
return err
}
_, err := io.WriteString(a.dst, "\n"+armorEnd+"\n")
return err
}
func ArmoredWriter(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})}
}
type armoredReader struct {
r *bufio.Reader
started bool
unread []byte // backed by buf
buf [bytesPerLine]byte
err error
}
func ArmoredReader(r io.Reader) io.Reader {
return &armoredReader{r: bufio.NewReader(r)}
}
func (r *armoredReader) Read(p []byte) (int, error) {
if len(r.unread) > 0 {
n := copy(p, r.unread)
r.unread = r.unread[n:]
return n, nil
}
if r.err != nil {
return 0, r.err
}
getLine := func() ([]byte, error) {
line, err := r.r.ReadBytes('\n')
if err != nil && len(line) == 0 {
if err == io.EOF {
err = errors.New("invalid armor: unexpected EOF")
}
return nil, err
}
return bytes.TrimSpace(line), nil
}
if !r.started {
line, err := getLine()
if err != nil {
return 0, r.setErr(err)
}
if string(line) != armorPreamble {
return 0, r.setErr(errors.New("invalid armor first line: " + string(line)))
}
r.started = true
}
line, err := getLine()
if err != nil {
return 0, r.setErr(err)
}
if string(line) == armorEnd {
return 0, r.setErr(io.EOF)
}
if len(line) > 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)
}
r.unread = r.unread[:n]
if n < bytesPerLine {
line, err := getLine()
if err != nil {
return 0, r.setErr(err)
}
if string(line) != armorEnd {
return 0, r.setErr(errors.New("invalid armor closing line: " + string(line)))
}
r.err = io.EOF
}
nn := copy(p, r.unread)
r.unread = r.unread[nn:]
return nn, nil
}
func (r *armoredReader) setErr(err error) error {
r.err = err
return err
}
+45
View File
@@ -0,0 +1,45 @@
// 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")
}
}
+188
View File
@@ -0,0 +1,188 @@
// 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 implements the age file format.
package format
import (
"bufio"
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"strings"
)
type Header struct {
Recipients []*Recipient
MAC []byte
}
type Recipient struct {
Type string
Args []string
Body []byte
}
var b64 = base64.RawStdEncoding.Strict()
func DecodeString(s string) ([]byte, error) {
// CR and LF are ignored by DecodeString, but we don't want any malleability.
if strings.ContainsAny(s, "\n\r") {
return nil, errors.New(`unexpected newline character`)
}
return b64.DecodeString(s)
}
var EncodeToString = b64.EncodeToString
const columnsPerLine = 64
const bytesPerLine = columnsPerLine / 4 * 3
const intro = "age-encryption.org/v1\n"
var recipientPrefix = []byte("->")
var footerPrefix = []byte("---")
func (r *Recipient) Marshal(w io.Writer) error {
if _, err := w.Write(recipientPrefix); err != nil {
return err
}
for _, a := range append([]string{r.Type}, r.Args...) {
if _, err := io.WriteString(w, " "+a); err != nil {
return err
}
}
if _, err := io.WriteString(w, "\n"); err != nil {
return err
}
ww := base64.NewEncoder(b64, &newlineWriter{dst: w})
if _, err := ww.Write(r.Body); err != nil {
return err
}
if err := ww.Close(); err != nil {
return err
}
_, err := io.WriteString(w, "\n")
return err
}
func (h *Header) MarshalWithoutMAC(w io.Writer) error {
if _, err := io.WriteString(w, intro); err != nil {
return err
}
for _, r := range h.Recipients {
if err := r.Marshal(w); err != nil {
return err
}
}
_, err := fmt.Fprintf(w, "%s", footerPrefix)
return err
}
func (h *Header) Marshal(w io.Writer) error {
if err := h.MarshalWithoutMAC(w); err != nil {
return err
}
mac := b64.EncodeToString(h.MAC)
_, err := fmt.Fprintf(w, " %s\n", mac)
return err
}
type ParseError string
func (e ParseError) Error() string {
return "parsing age header: " + string(e)
}
func errorf(format string, a ...interface{}) error {
return ParseError(fmt.Sprintf(format, a...))
}
// Parse returns the header and a Reader that begins at the start of the
// payload.
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)
}
if line != intro {
return nil, nil, errorf("unexpected intro: %q", line)
}
var r *Recipient
for {
line, err := rr.ReadBytes('\n')
if err != nil {
return nil, nil, errorf("failed to read header: %v", err)
}
if bytes.HasPrefix(line, footerPrefix) {
prefix, args := splitArgs(line)
if prefix != string(footerPrefix) || len(args) != 1 {
return nil, nil, errorf("malformed closing line: %q", line)
}
h.MAC, err = DecodeString(args[0])
if err != nil {
return nil, nil, errorf("malformed closing line %q: %v", line, err)
}
break
} else if bytes.HasPrefix(line, recipientPrefix) {
r = &Recipient{}
prefix, args := splitArgs(line)
if prefix != string(recipientPrefix) || len(args) < 1 {
return nil, nil, errorf("malformed recipient: %q", line)
}
r.Type = args[0]
r.Args = args[1:]
h.Recipients = append(h.Recipients, r)
} else if r != nil {
b, err := DecodeString(strings.TrimSuffix(string(line), "\n"))
if err != nil {
return nil, nil, errorf("malformed body line %q: %v", line, err)
}
if len(b) > bytesPerLine {
return nil, nil, errorf("malformed body line %q: too long", line)
}
r.Body = append(r.Body, b...)
if len(b) < bytesPerLine {
// Only the last line of a body can be short.
r = nil
}
} else {
return nil, nil, errorf("unexpected line: %q", line)
}
}
// 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
}
func splitArgs(line []byte) (string, []string) {
l := strings.TrimSuffix(string(line), "\n")
parts := strings.Split(l, " ")
return parts[0], parts[1:]
}
+35
View File
@@ -0,0 +1,35 @@
// +build gofuzz
package format
import (
"bytes"
"fmt"
"io"
"os"
)
func Fuzz(data []byte) int {
h, payload, err := Parse(bytes.NewReader(data))
if err != nil {
if h != nil {
panic("h != nil on error")
}
if payload != nil {
panic("payload != nil on error")
}
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 !bytes.Equal(w.Bytes(), data) {
fmt.Fprintf(os.Stderr, "%s\n%q\n%q\n\n", w, data, w)
panic("Marshal output different from input")
}
return 1
}
+216
View File
@@ -0,0 +1,216 @@
// Copyright 2019 Google LLC
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// Package stream implements a variant of the STREAM chunked encryption scheme.
package stream
import (
"crypto/cipher"
"errors"
"io"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/poly1305"
)
const ChunkSize = 64 * 1024
type Reader struct {
a cipher.AEAD
src io.Reader
unread []byte // decrypted but unread data, backed by buf
buf [encChunkSize]byte
err error
nonce [chacha20poly1305.NonceSize]byte
}
const (
encChunkSize = ChunkSize + poly1305.TagSize
lastChunkFlag = 0x01
)
func NewReader(key []byte, src io.Reader) (*Reader, error) {
aead, err := chacha20poly1305.New(key)
if err != nil {
return nil, err
}
return &Reader{
a: aead,
src: src,
}, nil
}
func (r *Reader) Read(p []byte) (int, error) {
if len(r.unread) > 0 {
n := copy(p, r.unread)
r.unread = r.unread[n:]
return n, nil
}
if r.err != nil {
return 0, r.err
}
if len(p) == 0 {
return 0, nil
}
last, err := r.readChunk()
if err != nil {
r.err = err
return 0, err
}
n := copy(p, r.unread)
r.unread = r.unread[n:]
if last {
r.err = io.EOF
}
return n, nil
}
// readChunk reads the next chunk of ciphertext from r.c and makes in 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) {
if len(r.unread) != 0 {
panic("stream: internal error: readChunk called with dirty buffer")
}
in := r.buf[:]
n, err := io.ReadFull(r.src, in)
switch {
case err == io.EOF:
// A message can't end without a marked chunk. This message is truncated.
return false, io.ErrUnexpectedEOF
case err == io.ErrUnexpectedEOF:
// The last chunk can be short.
in = in[:n]
last = true
setLastChunkFlag(&r.nonce)
case err != nil:
return false, err
}
outBuf := make([]byte, 0, ChunkSize)
out, err := r.a.Open(outBuf, r.nonce[:], in, nil)
if err != nil && !last {
// Check if this was a full-length final chunk.
last = true
setLastChunkFlag(&r.nonce)
out, err = r.a.Open(outBuf, r.nonce[:], in, nil)
}
if err != nil {
return false, err
}
incNonce(&r.nonce)
r.unread = r.buf[:copy(r.buf[:], out)]
return last, nil
}
func incNonce(nonce *[chacha20poly1305.NonceSize]byte) {
for i := len(nonce) - 2; i >= 0; i-- {
nonce[i]++
if nonce[i] != 0 {
break
} else if i == 0 {
// The counter is 88 bits, this is unreachable.
panic("stream: chunk counter wrapped around")
}
}
}
func setLastChunkFlag(nonce *[chacha20poly1305.NonceSize]byte) {
nonce[len(nonce)-1] = lastChunkFlag
}
type Writer struct {
a cipher.AEAD
dst io.WriteCloser
unwritten []byte // backed by buf
buf [encChunkSize]byte
nonce [chacha20poly1305.NonceSize]byte
err error
}
func NewWriter(key []byte, dst io.WriteCloser) (*Writer, error) {
aead, err := chacha20poly1305.New(key)
if err != nil {
return nil, err
}
w := &Writer{
a: aead,
dst: dst,
}
w.unwritten = w.buf[:0]
return w, nil
}
func (w *Writer) Write(p []byte) (n int, err error) {
// TODO: consider refactoring with a bytes.Buffer.
if w.err != nil {
return 0, w.err
}
if len(p) == 0 {
return 0, nil
}
total := len(p)
for len(p) > 0 {
freeBuf := w.buf[len(w.unwritten):ChunkSize]
n := copy(freeBuf, p)
p = p[n:]
w.unwritten = w.unwritten[:len(w.unwritten)+n]
if len(w.unwritten) == ChunkSize && len(p) > 0 {
if err := w.flushChunk(notLastChunk); err != nil {
w.err = err
return 0, err
}
}
}
return total, nil
}
// Close will flush the last chunk and call the underlying
// WriteCloser's Close method.
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 = errors.New("stream.Writer is already closed")
return w.dst.Close()
}
const (
lastChunk = true
notLastChunk = false
)
func (w *Writer) flushChunk(last bool) error {
if !last && len(w.unwritten) != ChunkSize {
panic("stream: internal error: flush called with partial chunk")
}
if last {
setLastChunkFlag(&w.nonce)
}
buf := w.a.Seal(w.buf[:0], w.nonce[:], w.unwritten, nil)
_, err := w.dst.Write(buf)
w.unwritten = w.buf[:0]
incNonce(&w.nonce)
return err
}
+116
View File
@@ -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 stream_test
import (
"bytes"
"crypto/rand"
"fmt"
"io"
"testing"
"filippo.io/age/internal/format"
"filippo.io/age/internal/stream"
"golang.org/x/crypto/chacha20poly1305"
)
const cs = stream.ChunkSize
func TestRoundTrip(t *testing.T) {
for _, stepSize := range []int{512, 600, 1000, cs} {
for _, length := range []int{0, 1000, cs, cs + 100} {
t.Run(fmt.Sprintf("len=%d,step=%d", length, stepSize),
func(t *testing.T) { testRoundTrip(t, stepSize, length) })
}
}
}
func testRoundTrip(t *testing.T, stepSize, length int) {
src := make([]byte, length)
if _, err := rand.Read(src); err != nil {
t.Fatal(err)
}
buf := &bytes.Buffer{}
key := make([]byte, chacha20poly1305.KeySize)
if _, err := rand.Read(key); err != nil {
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)
if err != nil {
t.Fatal(err)
}
var n int
for n < length {
b := length - n
if b > stepSize {
b = stepSize
}
nn, err := w.Write(src[n : n+b])
if err != nil {
t.Fatal(err)
}
if nn != b {
t.Errorf("Write returned %d, expected %d", nn, b)
}
n += nn
nn, err = w.Write(src[n:n])
if err != nil {
t.Fatal(err)
}
if nn != 0 {
t.Errorf("Write returned %d, expected 0", nn)
}
}
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())
r, err := stream.NewReader(key, buf)
if err != nil {
t.Fatal(err)
}
n = 0
readBuf := make([]byte, stepSize)
for n < length {
b := length - n
if b > stepSize {
b = stepSize
}
nn, err := r.Read(readBuf)
if err != nil {
t.Fatalf("Read error at index %d: %v", n, err)
}
if !bytes.Equal(readBuf[:nn], src[n:n+nn]) {
t.Errorf("wrong data at indexes %d - %d", n, n+nn)
}
n += nn
}
}
-3
View File
@@ -1,3 +0,0 @@
{
"age": "homebrew/core"
}