mirror of
https://github.com/FiloSottile/age.git
synced 2026-11-08 12:26:58 +00:00
Compare commits
39
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbab440e19 | ||
|
|
158b2ce899 | ||
|
|
c434eee6e8 | ||
|
|
b142e0fd01 | ||
|
|
9a84e437b1 | ||
|
|
8c600131ec | ||
|
|
0369d9b7fb | ||
|
|
93ea79628d | ||
|
|
aefae7ca0f | ||
|
|
470186da64 | ||
|
|
1a5bcfeab9 | ||
|
|
7ef2aa8a4e | ||
|
|
a070570595 | ||
|
|
025d8d5c0b | ||
|
|
03f7237541 | ||
|
|
9821fcefc9 | ||
|
|
f90681e0df | ||
|
|
5ea3ee2f63 | ||
|
|
779b033c8b | ||
|
|
884b6f365d | ||
|
|
4c4e446f72 | ||
|
|
c624abc0ad | ||
|
|
2cc62919a6 | ||
|
|
8f7e6dcd45 | ||
|
|
a5bf40786d | ||
|
|
2a0aef5803 | ||
|
|
11fc3e293a | ||
|
|
2d009c8eaf | ||
|
|
fe362d8f0a | ||
|
|
a99e57c53c | ||
|
|
c4269b6185 | ||
|
|
dd0939ffaa | ||
|
|
7f61cf23bf | ||
|
|
022002171c | ||
|
|
0940f184fb | ||
|
|
37d95cc84a | ||
|
|
e9c118cea0 | ||
|
|
52dbe9eecf | ||
|
|
06cbe4f91e |
No files matched your search
@@ -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>
|
||||
```
|
||||
@@ -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>
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
os: linux
|
||||
arch: arm64
|
||||
dist: bionic
|
||||
language: go
|
||||
go: 1.x
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,7 @@
|
||||
age is meant to be a simple, secure and modern encryption tool with small explicit keys, no config options, and UNIX-style composability. The spec is at [age-encryption.org/v1](https://age-encryption.org/v1).
|
||||
|
||||
This implementation is in progress, and things will keep moving around, so it's not yet a good time to contribute, and it's definitely not a good time to rely on it.
|
||||
|
||||
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.
|
||||
|
||||
Development is sometimes livestreamed at [twitch.tv/filosottile](https://www.twitch.tv/filosottile).
|
||||
@@ -0,0 +1,58 @@
|
||||
// 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"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"filippo.io/age/internal/age"
|
||||
)
|
||||
|
||||
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 io.Writer) {
|
||||
k, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
log.Fatalf("Internal error: %v", err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
// 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
|
||||
}
|
||||
|
||||
func main() {
|
||||
_log.SetFlags(0)
|
||||
|
||||
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 ]")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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=
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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:]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"age": "homebrew/core"
|
||||
}
|
||||
Reference in new issue
Block a user