aboutsummaryrefslogtreecommitdiffstats
path: root/crypto.go
blob: 2d8605ca4ca98eafe2122db4380c86d202fef129 (plain)
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
package axon

import (
	"crypto/ed25519"
	"crypto/rand"
	"crypto/sha256"
	"crypto/sha512"
	"errors"
	"fmt"
	"io"
	"math/big"

	"golang.org/x/crypto/chacha20poly1305"
	"golang.org/x/crypto/curve25519"
	"golang.org/x/crypto/hkdf"
)

// p is the prime for Curve25519: 2^255 - 19.
var curveP = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 255), big.NewInt(19))

// Ed25519PrivKeyToX25519 converts an Ed25519 private key to an X25519 scalar
// by clamping the SHA-512 hash of the 32-byte seed (per RFC 8032 ยง5.1.5).
func Ed25519PrivKeyToX25519(privKey ed25519.PrivateKey) [32]byte {
	seed := privKey.Seed()
	h := sha512.Sum512(seed)
	// Clamp per RFC 8032.
	h[0] &= 248
	h[31] &= 127
	h[31] |= 64
	var scalar [32]byte
	copy(scalar[:], h[:32])
	return scalar
}

// Ed25519PubKeyToX25519 converts an Ed25519 public key (compressed Edwards
// y-coordinate) to the corresponding Curve25519 Montgomery u-coordinate via
// the birational equivalence: u = (1 + y) / (1 - y) mod p.
func Ed25519PubKeyToX25519(pubKey ed25519.PublicKey) ([32]byte, error) {
	if len(pubKey) != 32 {
		return [32]byte{}, fmt.Errorf("axon: ed25519 pubkey must be 32 bytes, got %d", len(pubKey))
	}

	// Extract the y coordinate: clear the sign bit in byte 31.
	yBytes := make([]byte, 32)
	copy(yBytes, pubKey)
	yBytes[31] &= 0x7f

	// big.Int expects big-endian; reverse the little-endian bytes.
	reversedY := make([]byte, 32)
	for i, b := range yBytes {
		reversedY[31-i] = b
	}
	y := new(big.Int).SetBytes(reversedY)

	p := curveP

	// u = (1 + y) * modInverse(1 - y, p) mod p
	one := big.NewInt(1)

	num := new(big.Int).Add(one, y)
	num.Mod(num, p)

	den := new(big.Int).Sub(one, y)
	den.Mod(den, p)
	if den.ModInverse(den, p) == nil {
		return [32]byte{}, errors.New("axon: degenerate public key (y=1)")
	}

	u := new(big.Int).Mul(num, den)
	u.Mod(u, p)

	// Encode as little-endian 32 bytes.
	uBE := u.Bytes() // big-endian, variable length
	var out [32]byte
	for i, b := range uBE {
		out[len(uBE)-1-i] = b
	}
	return out, nil
}

// DHSharedSecret computes the X25519 Diffie-Hellman shared secret between a
// local Ed25519 private key and a remote Ed25519 public key. Both keys are
// converted to their X25519 equivalents before the scalar multiplication.
func DHSharedSecret(localPriv ed25519.PrivateKey, remotePub ed25519.PublicKey) ([32]byte, error) {
	scalar := Ed25519PrivKeyToX25519(localPriv)
	point, err := Ed25519PubKeyToX25519(remotePub)
	if err != nil {
		return [32]byte{}, fmt.Errorf("axon: convert remote pubkey to x25519: %w", err)
	}
	shared, err := curve25519.X25519(scalar[:], point[:])
	if err != nil {
		return [32]byte{}, fmt.Errorf("axon: x25519: %w", err)
	}
	var out [32]byte
	copy(out[:], shared)
	return out, nil
}

// dmKey derives a symmetric key from the raw DH shared secret using
// HKDF-SHA256. The salt is nil (HKDF uses a zero-filled hash-length salt)
// and the info string binds the key to the axon DM context.
func dmKey(shared [32]byte) ([32]byte, error) {
	r := hkdf.New(sha256.New, shared[:], nil, []byte("axon-dm-v1"))
	var key [32]byte
	if _, err := io.ReadFull(r, key[:]); err != nil {
		return [32]byte{}, fmt.Errorf("axon: hkdf: %w", err)
	}
	return key, nil
}

// dmAD builds the associated data for DM encryption by concatenating the
// sender and recipient Ed25519 public keys, preventing key-confusion attacks.
func dmAD(senderPub, recipientPub ed25519.PublicKey) []byte {
	ad := make([]byte, 0, len(senderPub)+len(recipientPub))
	ad = append(ad, senderPub...)
	ad = append(ad, recipientPub...)
	return ad
}

// EncryptDM encrypts plaintext for a DM using ChaCha20-Poly1305. The
// encryption key is derived via HKDF-SHA256 from the X25519 shared secret
// of senderPriv and recipientPub. The sender and recipient public keys are
// bound as associated data. The returned blob is: nonce (12 bytes) || ciphertext.
func EncryptDM(senderPriv ed25519.PrivateKey, recipientPub ed25519.PublicKey, plaintext []byte) ([]byte, error) {
	shared, err := DHSharedSecret(senderPriv, recipientPub)
	if err != nil {
		return nil, fmt.Errorf("axon: dh: %w", err)
	}

	key, err := dmKey(shared)
	if err != nil {
		return nil, err
	}

	aead, err := chacha20poly1305.New(key[:])
	if err != nil {
		return nil, fmt.Errorf("axon: create aead: %w", err)
	}

	nonce := make([]byte, aead.NonceSize()) // 12 bytes
	if _, err := rand.Read(nonce); err != nil {
		return nil, fmt.Errorf("axon: generate nonce: %w", err)
	}

	senderPub := senderPriv.Public().(ed25519.PublicKey)
	ad := dmAD(senderPub, recipientPub)

	ct := aead.Seal(nil, nonce, plaintext, ad)
	return append(nonce, ct...), nil
}

// DecryptDM decrypts a blob produced by EncryptDM. recipientPriv is the
// recipient's Ed25519 private key; senderPub is the sender's Ed25519 public
// key. The blob must be nonce (12 bytes) || ciphertext.
func DecryptDM(recipientPriv ed25519.PrivateKey, senderPub ed25519.PublicKey, blob []byte) ([]byte, error) {
	shared, err := DHSharedSecret(recipientPriv, senderPub)
	if err != nil {
		return nil, fmt.Errorf("axon: dh: %w", err)
	}

	key, err := dmKey(shared)
	if err != nil {
		return nil, err
	}

	aead, err := chacha20poly1305.New(key[:])
	if err != nil {
		return nil, fmt.Errorf("axon: create aead: %w", err)
	}

	nonceSize := aead.NonceSize()
	if len(blob) < nonceSize {
		return nil, errors.New("axon: ciphertext too short to contain nonce")
	}

	nonce := blob[:nonceSize]
	ct := blob[nonceSize:]

	recipientPub := recipientPriv.Public().(ed25519.PublicKey)
	ad := dmAD(senderPub, recipientPub)

	pt, err := aead.Open(nil, nonce, ct, ad)
	if err != nil {
		return nil, fmt.Errorf("axon: decrypt: %w", err)
	}
	return pt, nil
}