mirror of
https://github.com/FiloSottile/age.git
synced 2026-11-08 12:26:58 +00:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31500bfa2f | ||
|
|
22e598d458 | ||
|
|
65f171a239 | ||
|
|
2194f6962c | ||
|
|
07c72f3b69 | ||
|
|
21a7203f6a | ||
|
|
0c650f815d | ||
|
|
189041b668 |
No files matched your search
@@ -1,6 +1,8 @@
|
||||
# age
|
||||
|
||||
age is a simple, modern and secure file encryption tool.
|
||||
[](https://pkg.go.dev/filippo.io/age)
|
||||
|
||||
age is a simple, modern and secure file encryption tool, format, and library.
|
||||
|
||||
It features small explicit keys, no config options, and UNIX-style composability.
|
||||
|
||||
|
||||
+37
-9
@@ -12,8 +12,29 @@
|
||||
// ScryptRecipient and ScryptIdentity. For compatibility with existing SSH keys
|
||||
// use the filippo.io/age/agessh package.
|
||||
//
|
||||
// Age encrypted files are binary and not malleable, for encoding them as text,
|
||||
// Age encrypted files are binary and not malleable. For encoding them as text,
|
||||
// use the filippo.io/age/armor package.
|
||||
//
|
||||
// Key management
|
||||
//
|
||||
// Age does not have a global keyring. Instead, since age keys are small,
|
||||
// textual, and cheap, you are encoraged to generate dedicated keys for each
|
||||
// task and application.
|
||||
//
|
||||
// Recipient public keys can be passed around as command line flags and in
|
||||
// config files, while secret keys should be stored in dedicated files, through
|
||||
// secret management systems, or as environment variables.
|
||||
//
|
||||
// There is no default path for age keys. Instead, they should be stored at
|
||||
// application-specific paths. The CLI supports files where private keys are
|
||||
// listed one per line, ignoring empty lines and lines starting with "#". These
|
||||
// files can be parsed with ParseIdentities.
|
||||
//
|
||||
// When integrating age into a new system, it's recommended that you only
|
||||
// support X25519 keys, and not SSH keys. The latter are supported for manual
|
||||
// encryption operations. If you need to tie into existing key management
|
||||
// infrastructure, you might want to consider implementing your own Recipient
|
||||
// and Identity.
|
||||
package age
|
||||
|
||||
import (
|
||||
@@ -66,17 +87,22 @@ type Stanza struct {
|
||||
Body []byte
|
||||
}
|
||||
|
||||
// Encrypt returns a WriteCloser. Writes to the returned value are encrypted and
|
||||
// written to dst as an age file. Every recipient will be able to decrypt the file.
|
||||
const fileKeySize = 16
|
||||
const streamNonceSize = 16
|
||||
|
||||
// Encrypt encrypts a file to one or more recipients.
|
||||
//
|
||||
// The caller must call Close on the returned value when done for the last chunk
|
||||
// to be encrypted and flushed to dst.
|
||||
// Writes to the returned WriteCloser are encrypted and written to dst as an age
|
||||
// file. Every recipient will be able to decrypt the file.
|
||||
//
|
||||
// The caller must call Close on the WriteCloser when done for the last chunk to
|
||||
// be encrypted and flushed to dst.
|
||||
func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
|
||||
if len(recipients) == 0 {
|
||||
return nil, errors.New("no recipients specified")
|
||||
}
|
||||
|
||||
fileKey := make([]byte, 16)
|
||||
fileKey := make([]byte, fileKeySize)
|
||||
if _, err := rand.Read(fileKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -102,7 +128,7 @@ func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
|
||||
return nil, fmt.Errorf("failed to write header: %v", err)
|
||||
}
|
||||
|
||||
nonce := make([]byte, 16)
|
||||
nonce := make([]byte, streamNonceSize)
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -113,7 +139,9 @@ func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
|
||||
return stream.NewWriter(streamKey(fileKey, nonce), dst)
|
||||
}
|
||||
|
||||
// Decrypt returns a Reader reading the decrypted plaintext of the age file read
|
||||
// Decrypt decrypts a file encrypted to one or more identities.
|
||||
//
|
||||
// It returns a Reader reading the decrypted plaintext of the age file read
|
||||
// from src. All identities will be tried until one successfully decrypts the file.
|
||||
func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {
|
||||
if len(identities) == 0 {
|
||||
@@ -173,7 +201,7 @@ RecipientsLoop:
|
||||
return nil, errors.New("bad header MAC")
|
||||
}
|
||||
|
||||
nonce := make([]byte, 16)
|
||||
nonce := make([]byte, streamNonceSize)
|
||||
if _, err := io.ReadFull(payload, nonce); err != nil {
|
||||
return nil, fmt.Errorf("failed to read nonce: %v", err)
|
||||
}
|
||||
@@ -8,14 +8,15 @@ package age_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age/age"
|
||||
"filippo.io/age"
|
||||
)
|
||||
|
||||
func ExampleEncrypt() {
|
||||
@@ -43,31 +44,59 @@ func ExampleEncrypt() {
|
||||
// Encrypted file size: 219
|
||||
}
|
||||
|
||||
var fileContents, _ = hex.DecodeString("6167652d656e6372797074696f6e2e6f72" +
|
||||
"672f76310a2d3e20583235353139203868726c4d2b5a4247334464346646322b61353" +
|
||||
"8337a64544957446b382f5234316b43595a7376775457340a794f345059646c4d5744" +
|
||||
"4a2b437867554e527159355a30542f6d2b6733464368356a4978474c62435658630a2" +
|
||||
"d2d2d20492f696d65765a7a79383132304a537a6d4a6e6d6e2f4b4d6b337035413131" +
|
||||
"5638334e6b34316d394e50450a70c5e53624a1520753f92c5ad10ecab273ba4d61178" +
|
||||
"07713e83820417a1df2ca08182272c8f85c857734a1311a3b75e98d0eaf")
|
||||
// DO NOT hardcode the private key. Store it in a secret storage solution,
|
||||
// on disk if the local machine is trusted, or have the user provide it.
|
||||
var privateKey string
|
||||
|
||||
var privateKey = "AGE-SECRET-KEY-184JMZMVQH3E6U0PSL869004Y3U2NYV7R30EU99CSEDNPH02YUVFSZW44VU"
|
||||
func init() {
|
||||
privateKey = "AGE-SECRET-KEY-184JMZMVQH3E6U0PSL869004Y3U2NYV7R30EU99CSEDNPH02YUVFSZW44VU"
|
||||
}
|
||||
|
||||
func ExampleDecrypt() {
|
||||
// DO NOT hardcode the private key. Store it in a secret storage solution,
|
||||
// on disk if the local machine is trusted, or have the user provide it.
|
||||
identity, err := age.ParseX25519Identity(privateKey)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse private key %q: %v", privateKey, err)
|
||||
log.Fatalf("Failed to parse private key: %v", err)
|
||||
}
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
f := bytes.NewReader(fileContents)
|
||||
f, err := os.Open("testdata/example.age")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open file: %v", err)
|
||||
}
|
||||
|
||||
r, err := age.Decrypt(f, identity)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open encrypted file: %v", err)
|
||||
}
|
||||
out := &bytes.Buffer{}
|
||||
if _, err := io.Copy(out, r); err != nil {
|
||||
log.Fatalf("Failed to read encrypted file: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("File contents: %q\n", out.Bytes())
|
||||
// Output:
|
||||
// File contents: "Black lives matter."
|
||||
}
|
||||
|
||||
func ExampleParseIdentities() {
|
||||
keyFile, err := os.Open("testdata/keys.txt")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open private keys file: %v", err)
|
||||
}
|
||||
identities, err := age.ParseIdentities(keyFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse private key: %v", err)
|
||||
}
|
||||
|
||||
f, err := os.Open("testdata/example.age")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open file: %v", err)
|
||||
}
|
||||
|
||||
r, err := age.Decrypt(f, identities...)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open encrypted file: %v", err)
|
||||
}
|
||||
out := &bytes.Buffer{}
|
||||
if _, err := io.Copy(out, r); err != nil {
|
||||
log.Fatalf("Failed to read encrypted file: %v", err)
|
||||
}
|
||||
@@ -162,3 +191,35 @@ func TestEncryptDecryptScrypt(t *testing.T) {
|
||||
t.Errorf("wrong data: %q, excepted %q", outBytes, helloWorld)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIdentities(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
wantCount int
|
||||
wantErr bool
|
||||
file string
|
||||
}{
|
||||
{"valid", 2, false, `
|
||||
# this is a comment
|
||||
# AGE-SECRET-KEY-1705XN76M8EYQ8M9PY4E2G3KA8DN7NSCGT3V4HMN20H3GCX4AS6HSSTG8D3
|
||||
#
|
||||
|
||||
AGE-SECRET-KEY-1D6K0SGAX3NU66R4GYFZY0UQWCLM3UUSF3CXLW4KXZM342WQSJ82QKU59QJ
|
||||
AGE-SECRET-KEY-19WUMFE89H3928FRJ5U3JYRNHM6CERQGKSQ584AQ8QY7T7R09D32SWE4DYH`},
|
||||
{"invalid", 0, true, `
|
||||
AGE-SECRET-KEY-1705XN76M8EYQ8M9PY4E2G3KA8DN7NSCGT3V4HMN20H3GCX4AS6HSSTG8D3
|
||||
AGE-SECRET-KEY--1D6K0SGAX3NU66R4GYFZY0UQWCLM3UUSF3CXLW4KXZM342WQSJ82QKU59Q`},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := age.ParseIdentities(strings.NewReader(tt.file))
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ParseIdentities() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if len(got) != tt.wantCount {
|
||||
t.Errorf("ParseIdentities() returned %d identities, want %d", len(got), tt.wantCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+14
-5
@@ -5,11 +5,14 @@
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
// Package agessh provides age.Identity and age.Recipient implementations of
|
||||
// types "ssh-rsa" and "ssh-ed25519", which allow reusing existing SSH key files
|
||||
// for encryption with age-encryption.org/v1.
|
||||
// types "ssh-rsa" and "ssh-ed25519", which allow reusing existing SSH keys for
|
||||
// encryption with age-encryption.org/v1.
|
||||
//
|
||||
// These should only be used for compatibility with existing keys, and native
|
||||
// X25519 keys should be preferred otherwise.
|
||||
// These recipient types should only be used for compatibility with existing
|
||||
// keys, and native X25519 keys should be preferred otherwise.
|
||||
//
|
||||
// Note that these recipient types are not anonymous: the encrypted message will
|
||||
// include a short 32-bit ID of the public key,
|
||||
package agessh
|
||||
|
||||
import (
|
||||
@@ -23,7 +26,7 @@ import (
|
||||
"io"
|
||||
"math/big"
|
||||
|
||||
"filippo.io/age/age"
|
||||
"filippo.io/age"
|
||||
"filippo.io/age/internal/format"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
@@ -346,6 +349,12 @@ func (i *Ed25519Identity) Unwrap(block *age.Stanza) ([]byte, error) {
|
||||
}
|
||||
|
||||
// aeadEncrypt and aeadDecrypt are copied from package age.
|
||||
//
|
||||
// They don't limit the file key size because multi-key attacks are irrelevant
|
||||
// against the ssh-ed25519 recipient. Being an asymmetric recipient, it would
|
||||
// only allow a more efficient search for accepted public keys against a
|
||||
// decryption oracle, but the ssh-X recipients are not anonymous (they have a
|
||||
// short recipient hash).
|
||||
|
||||
func aeadEncrypt(key, plaintext []byte) ([]byte, error) {
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
|
||||
"filippo.io/age/age"
|
||||
"filippo.io/age"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age/age"
|
||||
"filippo.io/age"
|
||||
"filippo.io/age/armor"
|
||||
)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"filippo.io/age/age"
|
||||
"filippo.io/age"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
|
||||
+4
-4
@@ -16,7 +16,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"filippo.io/age/age"
|
||||
"filippo.io/age"
|
||||
"filippo.io/age/armor"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
@@ -239,12 +239,12 @@ func decrypt(keys []string, in io.Reader, out io.Writer) {
|
||||
&LazyScryptIdentity{passphrasePrompt},
|
||||
}
|
||||
|
||||
// TODO: use the default location if no arguments are provided:
|
||||
// os.UserConfigDir()/age/keys.txt, ~/.ssh/id_rsa, ~/.ssh/id_ed25519
|
||||
// TODO: check the default SSH location if no arguments are provided
|
||||
// (~/.ssh/id_rsa, ~/.ssh/id_ed25519).
|
||||
for _, name := range keys {
|
||||
ids, err := parseIdentitiesFile(name)
|
||||
if err != nil {
|
||||
logFatalf("Error: %v", err)
|
||||
logFatalf("Error reading %q: %v", name, err)
|
||||
}
|
||||
identities = append(identities, ids...)
|
||||
}
|
||||
|
||||
+26
-13
@@ -13,20 +13,27 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age/age"
|
||||
"filippo.io/age"
|
||||
)
|
||||
|
||||
func TestVectors(t *testing.T) {
|
||||
files, _ := filepath.Glob("testdata/*.age")
|
||||
for _, f := range files {
|
||||
name := strings.TrimSuffix(strings.TrimPrefix(f, "testdata/"), ".age")
|
||||
expectFailure := strings.HasPrefix(name, "fail_")
|
||||
t.Run(name, func(t *testing.T) {
|
||||
identities, err := parseIdentitiesFile("testdata/" + name + "_key.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
var identities []age.Identity
|
||||
ids, err := parseIdentitiesFile("testdata/" + name + "_key.txt")
|
||||
if err == nil {
|
||||
identities = append(identities, ids...)
|
||||
}
|
||||
for _, i := range identities {
|
||||
t.Logf("%s", i.Type())
|
||||
password, err := ioutil.ReadFile("testdata/" + name + "_password.txt")
|
||||
if err == nil {
|
||||
i, err := age.NewScryptIdentity(string(password))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
identities = append(identities, i)
|
||||
}
|
||||
|
||||
in, err := os.Open("testdata/" + name + ".age")
|
||||
@@ -34,14 +41,20 @@ func TestVectors(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, err := age.Decrypt(in, identities...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
if expectFailure {
|
||||
if err == nil {
|
||||
t.Fatal("expected Decrypt failure")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("%s", out)
|
||||
}
|
||||
out, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("%s", out)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"filippo.io/age/age"
|
||||
"filippo.io/age"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
|
||||
+16
-40
@@ -8,14 +8,13 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"filippo.io/age/age"
|
||||
"filippo.io/age"
|
||||
"filippo.io/age/agessh"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
@@ -31,8 +30,6 @@ func parseRecipient(arg string) (age.Recipient, error) {
|
||||
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 {
|
||||
@@ -40,45 +37,24 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
contents, err := ioutil.ReadAll(io.LimitReader(f, privateKeySizeLimit))
|
||||
b := bufio.NewReader(f)
|
||||
const pemHeader = "-----BEGIN"
|
||||
if peeked, _ := b.Peek(len(pemHeader)); string(peeked) == pemHeader {
|
||||
const privateKeySizeLimit = 1 << 14 // 16 KiB
|
||||
contents, err := ioutil.ReadAll(io.LimitReader(b, 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)
|
||||
}
|
||||
return parseSSHIdentity(name, contents)
|
||||
}
|
||||
|
||||
ids, err := age.ParseIdentities(b)
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
age-encryption.org/v1
|
||||
-> scrypt qeKad+OgIkBbr/ndSa7J3Q 1
|
||||
C2tmV7/uZjRafxqaQd1JhYkM2KxuHHBy3/d2dJNEZEh8rZCqYfvE/eJUXqiqZsZa
|
||||
6kWgG1qa6Q6sXPz0vIIpYHGf4gzxG9oTVonMke2kHC4
|
||||
--- FQeacPQobvFBd0tuIQnQDd/NEDR4G4MfylkXiq9ZqZ0
|
||||
Ãù€pÛÂt€t�˜Ë3qòÆ)¬ÌÈŒv´¨QÆoÌšK÷¶7ÜØ)ø%a
|
||||
@@ -0,0 +1 @@
|
||||
dog-old-little-breeze-novel-razor-battle-replace-lake-horse
|
||||
@@ -0,0 +1,6 @@
|
||||
age-encryption.org/v1
|
||||
-> X25519 /Gt0E6JT7yuYHlwsGW5LbpEEJawOc+QMeMAS+hoOIgw
|
||||
XU/4Zkz4MksDhge0kosiMTJF8tHnOP0ZSi+6aaMqLMS1PlMIs95nKz3H7JGesTwA
|
||||
tsxuQrj+TuoGouNB1O0VshA9vsHGurn0Dtw5e7bkw9Q
|
||||
--- jQNSF6blozj2QFYJ/2iqy0wUcPuz/8vCS7RgKH8wjNI
|
||||
Þ9¶‡yË_£§R\¯ÿm\¥û©Uv6Qȶ»ñmK»a¢©êv®©˜2
|
||||
@@ -0,0 +1,3 @@
|
||||
# created: 2020-09-19T18:42:11+02:00
|
||||
# public key: age1uc8zlurjyjpenrslc2thyl28u7ylz6x8c2g9yphvjha6xm8ppf3slq0l25
|
||||
AGE-SECRET-KEY-1D8JAD8SXNFVQEFHAUNNAX4QCE3K5CUKMT7YYHNGTUSSP97YGWL4STV89UH
|
||||
@@ -43,7 +43,7 @@ func TestBech32(t *testing.T) {
|
||||
{"split1a2y9w", false}, // too short data part
|
||||
{"1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", false}, // empty hrp
|
||||
// invalid character (DEL) in hrp
|
||||
{"spl" + string(127) + "t1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", false},
|
||||
{"spl" + string(rune(127)) + "t1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", false},
|
||||
// too long
|
||||
{"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", false},
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ package age
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"filippo.io/age/internal/format"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
// aeadEncrypt encrypts a message with a one-time key.
|
||||
func aeadEncrypt(key, plaintext []byte) ([]byte, error) {
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
@@ -30,11 +32,19 @@ func aeadEncrypt(key, plaintext []byte) ([]byte, error) {
|
||||
return aead.Seal(nil, nonce, plaintext, nil), nil
|
||||
}
|
||||
|
||||
func aeadDecrypt(key, ciphertext []byte) ([]byte, error) {
|
||||
// aeadDecrypt decrypts a message of an expected fixed size.
|
||||
//
|
||||
// The message size is limited to mitigate multi-key attacks, where a ciphertext
|
||||
// can be crafted that decrypts successfully under multiple keys. Short
|
||||
// ciphertexts can only target two keys, which has limited impact.
|
||||
func aeadDecrypt(key []byte, size int, ciphertext []byte) ([]byte, error) {
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ciphertext) != size+aead.Overhead() {
|
||||
return nil, fmt.Errorf("encrypted message has unexpected length")
|
||||
}
|
||||
nonce := make([]byte, chacha20poly1305.NonceSize)
|
||||
return aead.Open(nil, nonce, ciphertext, nil)
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age/age"
|
||||
"filippo.io/age"
|
||||
"filippo.io/age/internal/format"
|
||||
)
|
||||
|
||||
@@ -19,7 +19,8 @@ import (
|
||||
|
||||
const scryptLabel = "age-encryption.org/v1/scrypt"
|
||||
|
||||
// ScryptRecipient is a password-based recipient.
|
||||
// ScryptRecipient is a password-based recipient. Anyone with the password can
|
||||
// decrypt the message.
|
||||
//
|
||||
// If a ScryptRecipient is used, it must be the only recipient for the file: it
|
||||
// can't be mixed with other recipient types and can't be used multiple times
|
||||
@@ -60,8 +61,10 @@ func (r *ScryptRecipient) SetWorkFactor(logN int) {
|
||||
r.workFactor = logN
|
||||
}
|
||||
|
||||
const scryptSaltSize = 16
|
||||
|
||||
func (r *ScryptRecipient) Wrap(fileKey []byte) (*Stanza, error) {
|
||||
salt := make([]byte, 16)
|
||||
salt := make([]byte, scryptSaltSize)
|
||||
if _, err := rand.Read(salt[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -133,7 +136,7 @@ func (i *ScryptIdentity) Unwrap(block *Stanza) ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse scrypt salt: %v", err)
|
||||
}
|
||||
if len(salt) != 16 {
|
||||
if len(salt) != scryptSaltSize {
|
||||
return nil, errors.New("invalid scrypt recipient block")
|
||||
}
|
||||
logN, err := strconv.Atoi(block.Args[1])
|
||||
@@ -153,7 +156,14 @@ func (i *ScryptIdentity) Unwrap(block *Stanza) ([]byte, error) {
|
||||
return nil, fmt.Errorf("failed to generate scrypt hash: %v", err)
|
||||
}
|
||||
|
||||
fileKey, err := aeadDecrypt(k, block.Body)
|
||||
// This AEAD is not robust, so an attacker could craft a message that
|
||||
// decrypts under two different keys (meaning two different passphrases) and
|
||||
// then use an error side-channel in an online decryption oracle to learn if
|
||||
// either key is correct. This is deemed acceptable because the usa case (an
|
||||
// online decryption oracle) is not recommended, and the security loss is
|
||||
// only one bit. This also does not bypass any scrypt work, but that work
|
||||
// can be precomputed in an online oracle scenario.
|
||||
fileKey, err := aeadDecrypt(k, fileKeySize, block.Body)
|
||||
if err != nil {
|
||||
return nil, ErrIncorrectIdentity
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
age-encryption.org/v1
|
||||
-> X25519 8hrlM+ZBG3Dd4fF2+a583zdTIWDk8/R41kCYZsvwTW4
|
||||
yO4PYdlMWDJ+CxgUNRqY5Z0T/m+g3FCh5jIxGLbCVXc
|
||||
--- I/imevZzy8120JSzmJnmn/KMk3p5A11V83Nk41m9NPE
|
||||
pĹĺ6$ˇRSů,ZŃʲsşMa€wč8 AzňĘ"rČř\…w4ˇ1;u鍯
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
# Test key for ExampleParseX25519Identities.
|
||||
AGE-SECRET-KEY-184JMZMVQH3E6U0PSL869004Y3U2NYV7R30EU99CSEDNPH02YUVFSZW44VU
|
||||
@@ -7,6 +7,7 @@
|
||||
package age
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
@@ -23,7 +24,11 @@ import (
|
||||
|
||||
const x25519Label = "age-encryption.org/v1/X25519"
|
||||
|
||||
// X25519Recipient is the standard age public key, based on a Curve25519 point.
|
||||
// X25519Recipient is the standard age public key. Messages encrypted to this
|
||||
// recipient can be decrypted with the corresponding X25519Identity.
|
||||
//
|
||||
// This recipient is anonymous, in the sense that an attacker can't tell from
|
||||
// the message alone if it is encrypted to a certain recipient.
|
||||
type X25519Recipient struct {
|
||||
theirPublicKey []byte
|
||||
}
|
||||
@@ -105,7 +110,8 @@ func (r *X25519Recipient) String() string {
|
||||
return s
|
||||
}
|
||||
|
||||
// X25519Identity is the standard age private key, based on a Curve25519 scalar.
|
||||
// X25519Identity is the standard age private key, which can decrypt messages
|
||||
// encrypted to the corresponding X25519Recipient.
|
||||
type X25519Identity struct {
|
||||
secretKey, ourPublicKey []byte
|
||||
}
|
||||
@@ -136,23 +142,57 @@ func GenerateX25519Identity() (*X25519Identity, error) {
|
||||
return newX25519IdentityFromScalar(secretKey)
|
||||
}
|
||||
|
||||
// ParseX25519Identity returns a new X25519Recipient from a Bech32 private key
|
||||
// ParseX25519Identity returns a new X25519Identity from a Bech32 private key
|
||||
// encoding with the "AGE-SECRET-KEY-1" prefix.
|
||||
func ParseX25519Identity(s string) (*X25519Identity, error) {
|
||||
t, k, err := bech32.Decode(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed secret key %q: %v", s, err)
|
||||
return nil, fmt.Errorf("malformed secret key: %v", err)
|
||||
}
|
||||
if t != "AGE-SECRET-KEY-" {
|
||||
return nil, fmt.Errorf("malformed secret key %q: invalid type %q", s, t)
|
||||
return nil, fmt.Errorf("malformed secret key: unknown type %q", t)
|
||||
}
|
||||
r, err := newX25519IdentityFromScalar(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed secret key %q: %v", s, err)
|
||||
return nil, fmt.Errorf("malformed secret key: %v", err)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// ParseIdentities parses a file with one or more private key encodings, one per
|
||||
// line. Empty lines and lines starting with "#" are ignored.
|
||||
//
|
||||
// This is the same syntax as the private key files accepted by the CLI, except
|
||||
// the CLI also accepts SSH private keys, which are not recommended for the
|
||||
// average application.
|
||||
//
|
||||
// Currently, all returned values are of type X25519Identity, but different
|
||||
// types might be returned in the future.
|
||||
func ParseIdentities(f io.Reader) ([]Identity, error) {
|
||||
const privateKeySizeLimit = 1 << 24 // 16 MiB
|
||||
var ids []Identity
|
||||
scanner := bufio.NewScanner(io.LimitReader(f, privateKeySizeLimit))
|
||||
var n int
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "#") || line == "" {
|
||||
continue
|
||||
}
|
||||
i, err := ParseX25519Identity(line)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error at line %d: %v", n, err)
|
||||
}
|
||||
ids = append(ids, i)
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read secret keys file: %v", err)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil, fmt.Errorf("no secret keys found")
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (i *X25519Identity) Unwrap(block *Stanza) ([]byte, error) {
|
||||
if block.Type != "X25519" {
|
||||
return nil, ErrIncorrectIdentity
|
||||
@@ -182,7 +222,7 @@ func (i *X25519Identity) Unwrap(block *Stanza) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileKey, err := aeadDecrypt(wrappingKey, block.Body)
|
||||
fileKey, err := aeadDecrypt(wrappingKey, fileKeySize, block.Body)
|
||||
if err != nil {
|
||||
return nil, ErrIncorrectIdentity
|
||||
}
|
||||
Reference in new issue
Block a user