Skip to content

Commit 84581c3

Browse files
committed
Use age for message encryption
1 parent 1fdd253 commit 84581c3

File tree

9 files changed

+219
-93
lines changed

9 files changed

+219
-93
lines changed

configs/hashup.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
[main]
22
nats_server_url = "http://user:secret@localhost:4222"
3-
encryption_key = "super-secret-key"
3+
# IMPORTANT: replace this key
4+
#
5+
# `hashup keygen` or `age-keygen` (https://github.com/FiloSottile/age)
6+
# can be used to generate it.
7+
#
8+
encryption_key = "AGE-SECRET-KEY-1FDQ7Q24T2Q3CC6SFLS33PV5P3A59RH89PCQ0PAU6FQ8GNWD9HNASTSQP57"
49
nats_stream = "HASHUP"
510
nats_subject = "FILES"
611

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/rubiojr/hashup
33
go 1.23.4
44

55
require (
6+
filippo.io/age v1.2.1
67
github.com/BurntSushi/toml v1.4.0
78
github.com/VictoriaMetrics/fastcache v1.12.2
89
github.com/a-h/templ v0.3.856
@@ -16,6 +17,7 @@ require (
1617
github.com/stretchr/testify v1.10.0
1718
github.com/urfave/cli/v2 v2.27.5
1819
github.com/vmihailenco/msgpack/v5 v5.4.1
20+
golang.org/x/term v0.30.0
1921
)
2022

2123
require (
@@ -35,7 +37,6 @@ require (
3537
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
3638
golang.org/x/crypto v0.36.0 // indirect
3739
golang.org/x/sys v0.31.0 // indirect
38-
golang.org/x/text v0.23.0 // indirect
3940
golang.org/x/time v0.11.0 // indirect
4041
gopkg.in/yaml.v3 v3.0.1 // indirect
4142
)

go.sum

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0=
2+
c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w=
3+
filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o=
4+
filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004=
15
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
26
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
37
github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI=
@@ -68,8 +72,8 @@ golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
6872
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
6973
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
7074
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
71-
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
72-
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
75+
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
76+
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
7377
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
7478
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
7579
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

internal/crypto/aes.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package crypto
2+
3+
import (
4+
"crypto/aes"
5+
"crypto/cipher"
6+
"crypto/rand"
7+
"crypto/sha256"
8+
"fmt"
9+
"io"
10+
)
11+
12+
type aesMachine struct {
13+
key []byte
14+
}
15+
16+
// NewEncryptor creates a new Encryptor instance with the given key
17+
func NewAES(key []byte) (Machine, error) {
18+
// Derive a 32-byte key for AES-256 from the encryption key
19+
hasher := sha256.New()
20+
hasher.Write(key)
21+
ek := hasher.Sum(nil)
22+
if len(ek) != 32 {
23+
return nil, fmt.Errorf("invalid key length")
24+
}
25+
return &aesMachine{key: ek}, nil
26+
}
27+
28+
// encrypt uses AES-GCM to encrypt data with the processor's key
29+
func (e *aesMachine) Encrypt(data []byte) ([]byte, error) {
30+
// Create a new cipher block from the key
31+
block, err := aes.NewCipher(e.key)
32+
if err != nil {
33+
return nil, err
34+
}
35+
36+
// Create a new GCM
37+
gcm, err := cipher.NewGCM(block)
38+
if err != nil {
39+
return nil, err
40+
}
41+
42+
// Create a nonce (12 bytes for GCM)
43+
nonce := make([]byte, gcm.NonceSize())
44+
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
45+
return nil, err
46+
}
47+
48+
// Encrypt and seal the data
49+
ciphertext := gcm.Seal(nonce, nonce, data, nil)
50+
return ciphertext, nil
51+
}
52+
53+
// decrypt uses AES-GCM to decrypt data with the processor's key
54+
func (e *aesMachine) Decrypt(data []byte) ([]byte, error) {
55+
block, err := aes.NewCipher(e.key)
56+
if err != nil {
57+
return nil, err
58+
}
59+
60+
// Create a new GCM
61+
gcm, err := cipher.NewGCM(block)
62+
if err != nil {
63+
return nil, err
64+
}
65+
66+
// Get the nonce size
67+
nonceSize := gcm.NonceSize()
68+
if len(data) < nonceSize {
69+
return nil, fmt.Errorf("ciphertext too short")
70+
}
71+
72+
// Extract the nonce and ciphertext
73+
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
74+
75+
// Decrypt the data
76+
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
77+
if err != nil {
78+
return nil, err
79+
}
80+
81+
return plaintext, nil
82+
}

internal/crypto/age.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package crypto
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"io"
7+
8+
"filippo.io/age"
9+
"filippo.io/age/armor"
10+
)
11+
12+
type ageX25519Machine struct {
13+
recipient age.Recipient
14+
identity age.Identity
15+
}
16+
17+
func NewAge(privateKey string) (Machine, error) {
18+
identity, err := age.ParseX25519Identity(privateKey)
19+
if err != nil {
20+
return nil, fmt.Errorf("failed to parse private key: %w", err)
21+
}
22+
23+
return &ageX25519Machine{
24+
recipient: identity.Recipient(),
25+
identity: identity,
26+
}, nil
27+
}
28+
29+
func GenerateAgeKeyPair() (publicKey string, privateKey string, err error) {
30+
identity, err := age.GenerateX25519Identity()
31+
if err != nil {
32+
return "", "", err
33+
}
34+
35+
return identity.Recipient().String(), identity.String(), nil
36+
}
37+
38+
func DerivePublicKey(privateKey string) (string, error) {
39+
identity, err := age.ParseX25519Identity(privateKey)
40+
if err != nil {
41+
return "", fmt.Errorf("failed to parse private key: %w", err)
42+
}
43+
44+
return identity.Recipient().String(), nil
45+
}
46+
47+
func (a *ageX25519Machine) Encrypt(data []byte) ([]byte, error) {
48+
var buf bytes.Buffer
49+
50+
armorWriter := armor.NewWriter(&buf)
51+
ageWriter, err := age.Encrypt(armorWriter, a.recipient)
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
if _, err := ageWriter.Write(data); err != nil {
57+
return nil, err
58+
}
59+
60+
if err := ageWriter.Close(); err != nil {
61+
return nil, err
62+
}
63+
64+
if err := armorWriter.Close(); err != nil {
65+
return nil, err
66+
}
67+
68+
return buf.Bytes(), nil
69+
}
70+
71+
func (a *ageX25519Machine) Decrypt(data []byte) ([]byte, error) {
72+
reader := bytes.NewReader(data)
73+
74+
armorReader := armor.NewReader(reader)
75+
ageReader, err := age.Decrypt(armorReader, a.identity)
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
var buf bytes.Buffer
81+
if _, err := io.Copy(&buf, ageReader); err != nil {
82+
return nil, err
83+
}
84+
85+
return buf.Bytes(), nil
86+
}

internal/crypto/crypto.go

Lines changed: 3 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,6 @@
11
package crypto
22

3-
import (
4-
"crypto/aes"
5-
"crypto/cipher"
6-
"crypto/rand"
7-
"fmt"
8-
"io"
9-
)
10-
11-
type Machine struct {
12-
key []byte
13-
}
14-
15-
// NewEncryptor creates a new Encryptor instance with the given key
16-
func New(key []byte) (*Machine, error) {
17-
if len(key) != 32 {
18-
return nil, fmt.Errorf("invalid key length")
19-
}
20-
return &Machine{key: key}, nil
21-
}
22-
23-
// encrypt uses AES-GCM to encrypt data with the processor's key
24-
func (e *Machine) Encrypt(data []byte) ([]byte, error) {
25-
// Create a new cipher block from the key
26-
block, err := aes.NewCipher(e.key)
27-
if err != nil {
28-
return nil, err
29-
}
30-
31-
// Create a new GCM
32-
gcm, err := cipher.NewGCM(block)
33-
if err != nil {
34-
return nil, err
35-
}
36-
37-
// Create a nonce (12 bytes for GCM)
38-
nonce := make([]byte, gcm.NonceSize())
39-
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
40-
return nil, err
41-
}
42-
43-
// Encrypt and seal the data
44-
ciphertext := gcm.Seal(nonce, nonce, data, nil)
45-
return ciphertext, nil
46-
}
47-
48-
// decrypt uses AES-GCM to decrypt data with the processor's key
49-
func (e *Machine) Decrypt(data []byte) ([]byte, error) {
50-
block, err := aes.NewCipher(e.key)
51-
if err != nil {
52-
return nil, err
53-
}
54-
55-
// Create a new GCM
56-
gcm, err := cipher.NewGCM(block)
57-
if err != nil {
58-
return nil, err
59-
}
60-
61-
// Get the nonce size
62-
nonceSize := gcm.NonceSize()
63-
if len(data) < nonceSize {
64-
return nil, fmt.Errorf("ciphertext too short")
65-
}
66-
67-
// Extract the nonce and ciphertext
68-
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
69-
70-
// Decrypt the data
71-
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
72-
if err != nil {
73-
return nil, err
74-
}
75-
76-
return plaintext, nil
3+
type Machine interface {
4+
Encrypt(data []byte) ([]byte, error)
5+
Decrypt(data []byte) ([]byte, error)
776
}

internal/processors/nats/nats.go

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package nats
22

33
import (
44
"context"
5-
"crypto/sha256"
65
"fmt"
76
"time"
87

@@ -29,7 +28,7 @@ type natsProcessor struct {
2928
encrypt bool // field to control encryption behavior
3029
cache *cache.FileCache
3130
statsChan chan Stats
32-
crypto *crypto.Machine
31+
crypto crypto.Machine
3332
clientCert string
3433
clientKey string
3534
caCert string
@@ -47,10 +46,7 @@ func WithStatsChannel(ch chan Stats) Option {
4746
// WithEncryptionKey sets a specific encryption key
4847
func WithEncryptionKey(key string) Option {
4948
return func(np *natsProcessor) {
50-
// Convert the key to a fixed length by hashing it
51-
hasher := sha256.New()
52-
hasher.Write([]byte(key))
53-
np.encryptKey = hasher.Sum(nil)
49+
np.encryptKey = []byte(key)
5450
}
5551
}
5652
func WithClientCert(cert string) Option {
@@ -137,7 +133,7 @@ func NewNATSProcessor(ctx context.Context, url, streamName, subject string, time
137133
return nil, fmt.Errorf("encryption enabled but no key provided")
138134
}
139135

140-
processor.crypto, err = crypto.New(processor.encryptKey)
136+
processor.crypto, err = crypto.NewAge(string(processor.encryptKey))
141137
if err != nil {
142138
return nil, err
143139
}

internal/store/listener.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package store
22

33
import (
44
"context"
5-
"crypto/sha256"
65
"fmt"
76
"time"
87

@@ -146,13 +145,7 @@ func (l *natsListener) Listen(ctx context.Context) error {
146145
}
147146
defer sub.Unsubscribe()
148147

149-
var ek []byte
150-
151-
// Derive a 32-byte key for AES-256 from the encryption key
152-
hasher := sha256.New()
153-
hasher.Write([]byte(l.natsEncryptionKey))
154-
ek = hasher.Sum(nil)
155-
cryptom, err := crypto.New(ek)
148+
cryptom, err := crypto.NewAge(l.natsEncryptionKey)
156149
if err != nil {
157150
return fmt.Errorf("failed to create crypto instance: %v", err)
158151
}

0 commit comments

Comments
 (0)