aboutsummaryrefslogtreecommitdiffstats
path: root/crypto.go
diff options
context:
space:
mode:
Diffstat (limited to 'crypto.go')
-rw-r--r--crypto.go144
1 files changed, 144 insertions, 0 deletions
diff --git a/crypto.go b/crypto.go
new file mode 100644
index 0000000..402d06b
--- /dev/null
+++ b/crypto.go
@@ -0,0 +1,144 @@
1package axon
2
3import (
4 "crypto/ed25519"
5 "crypto/rand"
6 "crypto/sha512"
7 "errors"
8 "fmt"
9 "math/big"
10
11 "golang.org/x/crypto/chacha20poly1305"
12 "golang.org/x/crypto/curve25519"
13)
14
15// p is the prime for Curve25519: 2^255 - 19.
16var curveP = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 255), big.NewInt(19))
17
18// Ed25519PrivKeyToX25519 converts an Ed25519 private key to an X25519 scalar
19// by clamping the SHA-512 hash of the 32-byte seed (per RFC 8032 ยง5.1.5).
20func Ed25519PrivKeyToX25519(privKey ed25519.PrivateKey) [32]byte {
21 seed := privKey.Seed()
22 h := sha512.Sum512(seed)
23 // Clamp per RFC 8032.
24 h[0] &= 248
25 h[31] &= 127
26 h[31] |= 64
27 var scalar [32]byte
28 copy(scalar[:], h[:32])
29 return scalar
30}
31
32// Ed25519PubKeyToX25519 converts an Ed25519 public key (compressed Edwards
33// y-coordinate) to the corresponding Curve25519 Montgomery u-coordinate via
34// the birational equivalence: u = (1 + y) / (1 - y) mod p.
35func Ed25519PubKeyToX25519(pubKey ed25519.PublicKey) ([32]byte, error) {
36 if len(pubKey) != 32 {
37 return [32]byte{}, fmt.Errorf("axon: ed25519 pubkey must be 32 bytes, got %d", len(pubKey))
38 }
39
40 // Extract the y coordinate: clear the sign bit in byte 31.
41 yBytes := make([]byte, 32)
42 copy(yBytes, pubKey)
43 yBytes[31] &= 0x7f
44
45 // big.Int expects big-endian; reverse the little-endian bytes.
46 reversedY := make([]byte, 32)
47 for i, b := range yBytes {
48 reversedY[31-i] = b
49 }
50 y := new(big.Int).SetBytes(reversedY)
51
52 p := curveP
53
54 // u = (1 + y) * modInverse(1 - y, p) mod p
55 one := big.NewInt(1)
56
57 num := new(big.Int).Add(one, y)
58 num.Mod(num, p)
59
60 den := new(big.Int).Sub(one, y)
61 den.Mod(den, p)
62 den.ModInverse(den, p)
63
64 u := new(big.Int).Mul(num, den)
65 u.Mod(u, p)
66
67 // Encode as little-endian 32 bytes.
68 uBE := u.Bytes() // big-endian, variable length
69 var out [32]byte
70 for i, b := range uBE {
71 out[len(uBE)-1-i] = b
72 }
73 return out, nil
74}
75
76// DHSharedSecret computes the X25519 Diffie-Hellman shared secret between a
77// local Ed25519 private key and a remote Ed25519 public key. Both keys are
78// converted to their X25519 equivalents before the scalar multiplication.
79func DHSharedSecret(localPriv ed25519.PrivateKey, remotePub ed25519.PublicKey) ([32]byte, error) {
80 scalar := Ed25519PrivKeyToX25519(localPriv)
81 point, err := Ed25519PubKeyToX25519(remotePub)
82 if err != nil {
83 return [32]byte{}, fmt.Errorf("axon: convert remote pubkey to x25519: %w", err)
84 }
85 shared, err := curve25519.X25519(scalar[:], point[:])
86 if err != nil {
87 return [32]byte{}, fmt.Errorf("axon: x25519: %w", err)
88 }
89 var out [32]byte
90 copy(out[:], shared)
91 return out, nil
92}
93
94// EncryptDM encrypts plaintext for a DM using ChaCha20-Poly1305. The
95// encryption key is the X25519 shared secret derived from senderPriv and
96// recipientPub. The returned blob is: nonce (12 bytes) || ciphertext.
97func EncryptDM(senderPriv ed25519.PrivateKey, recipientPub ed25519.PublicKey, plaintext []byte) ([]byte, error) {
98 shared, err := DHSharedSecret(senderPriv, recipientPub)
99 if err != nil {
100 return nil, fmt.Errorf("axon: dh: %w", err)
101 }
102
103 aead, err := chacha20poly1305.New(shared[:])
104 if err != nil {
105 return nil, fmt.Errorf("axon: create aead: %w", err)
106 }
107
108 nonce := make([]byte, aead.NonceSize()) // 12 bytes
109 if _, err := rand.Read(nonce); err != nil {
110 return nil, fmt.Errorf("axon: generate nonce: %w", err)
111 }
112
113 ct := aead.Seal(nil, nonce, plaintext, nil)
114 return append(nonce, ct...), nil
115}
116
117// DecryptDM decrypts a blob produced by EncryptDM. recipientPriv is the
118// recipient's Ed25519 private key; senderPub is the sender's Ed25519 public
119// key. The blob must be nonce (12 bytes) || ciphertext.
120func DecryptDM(recipientPriv ed25519.PrivateKey, senderPub ed25519.PublicKey, blob []byte) ([]byte, error) {
121 shared, err := DHSharedSecret(recipientPriv, senderPub)
122 if err != nil {
123 return nil, fmt.Errorf("axon: dh: %w", err)
124 }
125
126 aead, err := chacha20poly1305.New(shared[:])
127 if err != nil {
128 return nil, fmt.Errorf("axon: create aead: %w", err)
129 }
130
131 nonceSize := aead.NonceSize()
132 if len(blob) < nonceSize {
133 return nil, errors.New("axon: ciphertext too short to contain nonce")
134 }
135
136 nonce := blob[:nonceSize]
137 ct := blob[nonceSize:]
138
139 pt, err := aead.Open(nil, nonce, ct, nil)
140 if err != nil {
141 return nil, fmt.Errorf("axon: decrypt: authentication failed")
142 }
143 return pt, nil
144}