aboutsummaryrefslogtreecommitdiffstats
path: root/crypto.go
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-03-09 17:36:21 -0700
committerbndw <ben@bdw.to>2026-03-09 17:36:21 -0700
commiteca962d7c26bbea57801576935b98f3540e43da6 (patch)
tree19653f192107b5b74a8113af570d4b9c979a7e0d /crypto.go
parent9886a5c9054f3308482fdcca0fa545c8befbcf5b (diff)
fix: harden DM crypto — HKDF key derivation, AEAD associated data, ModInverse nil check
- Derive symmetric key via HKDF-SHA256 instead of using raw X25519 shared secret - Bind sender + recipient pubkeys as ChaCha20-Poly1305 associated data to prevent key-confusion attacks - Guard against ModInverse panic on degenerate public keys (y=1) - Wrap DecryptDM error instead of swallowing it - Update JS client to match Go implementation - Document encryption details in PROTOCOL.md
Diffstat (limited to 'crypto.go')
-rw-r--r--crypto.go59
1 files changed, 51 insertions, 8 deletions
diff --git a/crypto.go b/crypto.go
index 402d06b..2d8605c 100644
--- a/crypto.go
+++ b/crypto.go
@@ -3,13 +3,16 @@ package axon
3import ( 3import (
4 "crypto/ed25519" 4 "crypto/ed25519"
5 "crypto/rand" 5 "crypto/rand"
6 "crypto/sha256"
6 "crypto/sha512" 7 "crypto/sha512"
7 "errors" 8 "errors"
8 "fmt" 9 "fmt"
10 "io"
9 "math/big" 11 "math/big"
10 12
11 "golang.org/x/crypto/chacha20poly1305" 13 "golang.org/x/crypto/chacha20poly1305"
12 "golang.org/x/crypto/curve25519" 14 "golang.org/x/crypto/curve25519"
15 "golang.org/x/crypto/hkdf"
13) 16)
14 17
15// p is the prime for Curve25519: 2^255 - 19. 18// p is the prime for Curve25519: 2^255 - 19.
@@ -59,7 +62,9 @@ func Ed25519PubKeyToX25519(pubKey ed25519.PublicKey) ([32]byte, error) {
59 62
60 den := new(big.Int).Sub(one, y) 63 den := new(big.Int).Sub(one, y)
61 den.Mod(den, p) 64 den.Mod(den, p)
62 den.ModInverse(den, p) 65 if den.ModInverse(den, p) == nil {
66 return [32]byte{}, errors.New("axon: degenerate public key (y=1)")
67 }
63 68
64 u := new(big.Int).Mul(num, den) 69 u := new(big.Int).Mul(num, den)
65 u.Mod(u, p) 70 u.Mod(u, p)
@@ -91,16 +96,43 @@ func DHSharedSecret(localPriv ed25519.PrivateKey, remotePub ed25519.PublicKey) (
91 return out, nil 96 return out, nil
92} 97}
93 98
99// dmKey derives a symmetric key from the raw DH shared secret using
100// HKDF-SHA256. The salt is nil (HKDF uses a zero-filled hash-length salt)
101// and the info string binds the key to the axon DM context.
102func dmKey(shared [32]byte) ([32]byte, error) {
103 r := hkdf.New(sha256.New, shared[:], nil, []byte("axon-dm-v1"))
104 var key [32]byte
105 if _, err := io.ReadFull(r, key[:]); err != nil {
106 return [32]byte{}, fmt.Errorf("axon: hkdf: %w", err)
107 }
108 return key, nil
109}
110
111// dmAD builds the associated data for DM encryption by concatenating the
112// sender and recipient Ed25519 public keys, preventing key-confusion attacks.
113func dmAD(senderPub, recipientPub ed25519.PublicKey) []byte {
114 ad := make([]byte, 0, len(senderPub)+len(recipientPub))
115 ad = append(ad, senderPub...)
116 ad = append(ad, recipientPub...)
117 return ad
118}
119
94// EncryptDM encrypts plaintext for a DM using ChaCha20-Poly1305. The 120// EncryptDM encrypts plaintext for a DM using ChaCha20-Poly1305. The
95// encryption key is the X25519 shared secret derived from senderPriv and 121// encryption key is derived via HKDF-SHA256 from the X25519 shared secret
96// recipientPub. The returned blob is: nonce (12 bytes) || ciphertext. 122// of senderPriv and recipientPub. The sender and recipient public keys are
123// bound as associated data. The returned blob is: nonce (12 bytes) || ciphertext.
97func EncryptDM(senderPriv ed25519.PrivateKey, recipientPub ed25519.PublicKey, plaintext []byte) ([]byte, error) { 124func EncryptDM(senderPriv ed25519.PrivateKey, recipientPub ed25519.PublicKey, plaintext []byte) ([]byte, error) {
98 shared, err := DHSharedSecret(senderPriv, recipientPub) 125 shared, err := DHSharedSecret(senderPriv, recipientPub)
99 if err != nil { 126 if err != nil {
100 return nil, fmt.Errorf("axon: dh: %w", err) 127 return nil, fmt.Errorf("axon: dh: %w", err)
101 } 128 }
102 129
103 aead, err := chacha20poly1305.New(shared[:]) 130 key, err := dmKey(shared)
131 if err != nil {
132 return nil, err
133 }
134
135 aead, err := chacha20poly1305.New(key[:])
104 if err != nil { 136 if err != nil {
105 return nil, fmt.Errorf("axon: create aead: %w", err) 137 return nil, fmt.Errorf("axon: create aead: %w", err)
106 } 138 }
@@ -110,7 +142,10 @@ func EncryptDM(senderPriv ed25519.PrivateKey, recipientPub ed25519.PublicKey, pl
110 return nil, fmt.Errorf("axon: generate nonce: %w", err) 142 return nil, fmt.Errorf("axon: generate nonce: %w", err)
111 } 143 }
112 144
113 ct := aead.Seal(nil, nonce, plaintext, nil) 145 senderPub := senderPriv.Public().(ed25519.PublicKey)
146 ad := dmAD(senderPub, recipientPub)
147
148 ct := aead.Seal(nil, nonce, plaintext, ad)
114 return append(nonce, ct...), nil 149 return append(nonce, ct...), nil
115} 150}
116 151
@@ -123,7 +158,12 @@ func DecryptDM(recipientPriv ed25519.PrivateKey, senderPub ed25519.PublicKey, bl
123 return nil, fmt.Errorf("axon: dh: %w", err) 158 return nil, fmt.Errorf("axon: dh: %w", err)
124 } 159 }
125 160
126 aead, err := chacha20poly1305.New(shared[:]) 161 key, err := dmKey(shared)
162 if err != nil {
163 return nil, err
164 }
165
166 aead, err := chacha20poly1305.New(key[:])
127 if err != nil { 167 if err != nil {
128 return nil, fmt.Errorf("axon: create aead: %w", err) 168 return nil, fmt.Errorf("axon: create aead: %w", err)
129 } 169 }
@@ -136,9 +176,12 @@ func DecryptDM(recipientPriv ed25519.PrivateKey, senderPub ed25519.PublicKey, bl
136 nonce := blob[:nonceSize] 176 nonce := blob[:nonceSize]
137 ct := blob[nonceSize:] 177 ct := blob[nonceSize:]
138 178
139 pt, err := aead.Open(nil, nonce, ct, nil) 179 recipientPub := recipientPriv.Public().(ed25519.PublicKey)
180 ad := dmAD(senderPub, recipientPub)
181
182 pt, err := aead.Open(nil, nonce, ct, ad)
140 if err != nil { 183 if err != nil {
141 return nil, fmt.Errorf("axon: decrypt: authentication failed") 184 return nil, fmt.Errorf("axon: decrypt: %w", err)
142 } 185 }
143 return pt, nil 186 return pt, nil
144} 187}