aboutsummaryrefslogtreecommitdiffstats
path: root/crypto.go
blob: 402d06bf6af0a1ba675eb70111260826f1b54222 (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
package axon

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

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

// 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)
	den.ModInverse(den, p)

	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
}

// EncryptDM encrypts plaintext for a DM using ChaCha20-Poly1305. The
// encryption key is the X25519 shared secret derived from senderPriv and
// recipientPub. 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)
	}

	aead, err := chacha20poly1305.New(shared[:])
	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)
	}

	ct := aead.Seal(nil, nonce, plaintext, nil)
	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)
	}

	aead, err := chacha20poly1305.New(shared[:])
	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:]

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