Author SHA1 Message Date
Filippo Valsorda 31500bfa2f age: clean up examples of hardcoded data 2020-09-20 12:57:16 +02:00
Filippo Valsorda 22e598d458 age: replace ParseX25519Identities with ParseIdentities
The latter returns a []Identity that can be used with Decrypt directly.
2020-09-20 12:54:15 +02:00
Filippo Valsorda 65f171a239 age: add ParseX25519Identities and key management docs 2020-09-20 12:17:15 +02:00
Filippo Valsorda 2194f6962c age: mitigate multi-key attacks on ChaCha20Poly1305
It's possible to craft ChaCha20Poly1305 ciphertexts that decrypt under
multiple keys. (I know, it's wild.)

The impact is different for different recipients, but in general only
applies to Chosen Ciphertext Attacks against online decryption oracles:

* With the scrypt recipient, it lets the attacker make a recipient
  stanza that decrypts with multiple passwords, speeding up a bruteforce
  in terms of oracle queries (but not scrypt work, which can be
  precomputed) to logN by binary search.

  Limiting the ciphertext size limits the keys to two, which makes this
  acceptable: it's a loss of only one bit of security in a scenario
  (online decryption oracles) that is not recommended.

* With the X25519 recipient, it lets the attacker search for accepted
  public keys without using multiple recipient stanzas in the message.
  That lets the attacker bypass the 20 recipients limit (which was not
  actually intended to defend against deanonymization attacks).

  This is not really in the threat model for age: we make no attempt to
  provide anonymity in an online CCA scenario.

  Anyway, limiting the keys to two by enforcing short ciphertexts
  mitigates the attack: it only lets the attacker test 40 keys per
  message instead of 20.

* With the ssh-ed25519 recipient, the attack should be irrelevant, since
  the recipient stanza includes a 32-bit hash of the public key, making
  it decidedly not anonymous.

  Also to avoid breaking the abstraction in the agessh package, we don't
  mitigate the attack for this recipient, but we document the lack of
  anonymity.

This was reported by Paul Grubbs in the context of the upcoming paper
"Partitioning Oracle Attacks", USENIX Security 2021 (to appear), by
Julia Len, Paul Grubbs, and Thomas Ristenpart.
2020-09-19 18:52:59 +02:00
Shimmy Xu 07c72f3b69 internal/bech32: fix vet error by making rune conversion explicit (#141)
Fixes #138
2020-09-07 16:07:10 +02:00
Filippo Valsorda 21a7203f6a README: add link to Go documentation 2020-07-20 19:34:08 -04:00
Filippo Valsorda 0c650f815d age: fix filippo.io/age/agessh name in package docs
Thanks to @Mohammed90 for pointing this out.
2020-06-27 23:47:17 -04:00
Filippo Valsorda 189041b668 age: move package from filippo.io/age/age to filippo.io/age 🤦‍♂️ 2020-06-27 22:06:32 -04:00
22 changed files with 277 additions and 105 deletions

No files matched your search

+3 -1
View File
@@ -1,6 +1,8 @@
# age
age is a simple, modern and secure file encryption tool.
[![pkg.go.dev](https://pkg.go.dev/badge/filippo.io/age)](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
View File
@@ -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)
}
+76 -15
View File
@@ -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
View File
@@ -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)
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"crypto/rsa"
"fmt"
"filippo.io/age/age"
"filippo.io/age"
"golang.org/x/crypto/ssh"
)
+1 -1
View File
@@ -16,7 +16,7 @@ import (
"strings"
"testing"
"filippo.io/age/age"
"filippo.io/age"
"filippo.io/age/armor"
)
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"os"
"time"
"filippo.io/age/age"
"filippo.io/age"
"golang.org/x/crypto/ssh/terminal"
)
+4 -4
View File
@@ -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
View File
@@ -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)
})
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"fmt"
"os"
"filippo.io/age/age"
"filippo.io/age"
"golang.org/x/crypto/ssh/terminal"
)
+16 -40
View File
@@ -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
}
+6
View File
@@ -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
+6
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
# created: 2020-09-19T18:42:11+02:00
# public key: age1uc8zlurjyjpenrslc2thyl28u7ylz6x8c2g9yphvjha6xm8ppf3slq0l25
AGE-SECRET-KEY-1D8JAD8SXNFVQEFHAUNNAX4QCE3K5CUKMT7YYHNGTUSSP97YGWL4STV89UH
+1 -1
View File
@@ -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},
+11 -1
View File
@@ -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"
)
+14 -4
View File
@@ -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
}
+5
View File
@@ -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鍯
+2
View File
@@ -0,0 +1,2 @@
# Test key for ExampleParseX25519Identities.
AGE-SECRET-KEY-184JMZMVQH3E6U0PSL869004Y3U2NYV7R30EU99CSEDNPH02YUVFSZW44VU
+47 -7
View File
@@ -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
}