1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
|
package cryptokit
import (
"bytes"
"errors"
"fmt"
"io"
"strings"
"filippo.io/age"
"filippo.io/age/agessh"
agearmor "filippo.io/age/armor"
)
const maxCryptoMessageBytes = 64 << 20
func parseAgeRecipient(value string) (age.Recipient, error) {
value = strings.TrimSpace(value)
if recipient, err := age.ParseX25519Recipient(value); err == nil {
return recipient, nil
}
if recipient, err := agessh.ParseRecipient(value); err == nil {
return recipient, nil
}
return nil, errors.New("unsupported age recipient: expected native X25519 or SSH Ed25519/RSA")
}
func parseAgeIdentity(value string) (age.Identity, error) {
value = strings.TrimSpace(value)
if identity, err := age.ParseX25519Identity(value); err == nil {
return identity, nil
}
if identity, err := agessh.ParseIdentity([]byte(value)); err == nil {
return identity, nil
}
return nil, errors.New("unsupported age identity: expected native X25519 or SSH Ed25519/RSA private key")
}
// EncryptAge encrypts to a user-provided native age X25519 recipient or SSH
// Ed25519/RSA recipient and returns ASCII-armored age text.
func EncryptAge(message []byte, recipientKey string) ([]byte, error) {
recipient, err := parseAgeRecipient(recipientKey)
if err != nil {
return nil, fmt.Errorf("parse age recipient: %w", err)
}
var output bytes.Buffer
armored := agearmor.NewWriter(&output)
writer, err := age.Encrypt(armored, recipient)
if err != nil {
_ = armored.Close()
return nil, fmt.Errorf("create age encryption: %w", err)
}
if _, err := writer.Write(message); err != nil {
_ = writer.Close()
_ = armored.Close()
return nil, fmt.Errorf("write age message: %w", err)
}
if err := writer.Close(); err != nil {
_ = armored.Close()
return nil, fmt.Errorf("close age message: %w", err)
}
if err := armored.Close(); err != nil {
return nil, fmt.Errorf("close age armor: %w", err)
}
return output.Bytes(), nil
}
// DecryptAge decrypts an armored or binary age message with a user-provided
// native X25519 identity or an unencrypted SSH Ed25519/RSA private key.
func DecryptAge(message []byte, identityKey string) ([]byte, error) {
identity, err := parseAgeIdentity(identityKey)
if err != nil {
return nil, fmt.Errorf("parse age identity: %w", err)
}
var input io.Reader = bytes.NewReader(message)
if bytes.HasPrefix(bytes.TrimSpace(message), []byte(agearmor.Header)) {
input = agearmor.NewReader(input)
}
reader, err := age.Decrypt(input, identity)
if err != nil {
return nil, fmt.Errorf("create age decryption: %w", err)
}
plaintext, err := io.ReadAll(io.LimitReader(reader, maxCryptoMessageBytes+1))
if err != nil {
return nil, fmt.Errorf("read age message: %w", err)
}
if len(plaintext) > maxCryptoMessageBytes {
return nil, errors.New("decrypted age message exceeds the 64 MiB limit")
}
return plaintext, nil
}
|