exchange.go raw
1 package crypto
2
3 import (
4 "errors"
5 "sort"
6
7 "git.mleku.dev/mleku/dendrite/pkg/ratio"
8 "git.mleku.dev/mleku/dendrite/pkg/spore"
9 )
10
11 // SharedSecret is the result of a key exchange between two peers.
12 type SharedSecret struct {
13 // CommonTags are the constraint type tags present in both lattices.
14 CommonTags []string
15
16 // Secret is the derived Hamadryad shared key material (56 bytes).
17 Secret Hamadryad
18
19 // Confidence measures how much structural overlap exists.
20 // Higher confidence = more shared constraint structure = stronger key.
21 Confidence ratio.Ratio
22 }
23
24 // Exchange performs a Diffie-Hellman-like key exchange using spores.
25 //
26 // The protocol:
27 // 1. Find the constraint types common to both spores
28 // 2. For each common type, compute the structural overlap:
29 // occupancy rates, connectivity, permutation distributions
30 // 3. Hash the common structure to derive the shared secret
31 //
32 // This works because:
33 // - Both parties derive the same set of common tags deterministically
34 // - The structural overlap (occupancy * connectivity) is symmetric
35 // - An eavesdropper sees only the spores but cannot determine which
36 // constraints would admit elements without the factory (private key)
37 func Exchange(
38 ownSpore *spore.Spore,
39 peerSpore *spore.Spore,
40 ) (*SharedSecret, error) {
41 if ownSpore == nil || peerSpore == nil {
42 return nil, errors.New("crypto: nil spore in exchange")
43 }
44
45 // Find common tags.
46 ownTags := tagSet(ownSpore.TypeSignature)
47 peerTags := tagSet(peerSpore.TypeSignature)
48
49 var common []string
50 for tag := range ownTags {
51 if peerTags[tag] {
52 common = append(common, tag)
53 }
54 }
55 sort.Strings(common)
56
57 if len(common) == 0 {
58 return &SharedSecret{
59 Confidence: ratio.Zero,
60 }, nil
61 }
62
63 // Compute confidence: |common| / max(|own|, |peer|).
64 maxTags := max(len(ownTags), len(peerTags))
65 confidence := ratio.New(int64(len(common)), int64(maxTags))
66
67 // Derive shared secret from common structure.
68 // The key material is: sorted common tags + their occupancy rates
69 // from both spores + connectivity ratios, hashed through Hamadryad.
70 var buf []byte
71 // Domain separation.
72 buf = append(buf, []byte("dendrite-exchange-v1")...)
73
74 for _, tag := range common {
75 buf = append(buf, []byte(tag)...)
76
77 // Occupancy rates for this tag from both spores.
78 // Use min/max ordering for commutativity — both parties
79 // must derive the same hash regardless of who is "own" vs "peer".
80 ownCount := tagCountLookup(ownSpore.TypeSignature, tag)
81 ownRate := ratio.New(int64(ownCount), int64(ownSpore.TotalNodes))
82 peerCount := tagCountLookup(peerSpore.TypeSignature, tag)
83 peerRate := ratio.New(int64(peerCount), int64(peerSpore.TotalNodes))
84
85 minRate := ratio.Min(ownRate, peerRate)
86 maxRate := ratio.Max(ownRate, peerRate)
87 buf = append(buf, []byte(minRate.String())...)
88 buf = append(buf, []byte(maxRate.String())...)
89
90 // Connectivity overlap — also commutative via min/max.
91 ownConn := tagRatioLookup(ownSpore.Connectivity, tag)
92 peerConn := tagRatioLookup(peerSpore.Connectivity, tag)
93
94 minConn := ratio.Min(ownConn, peerConn)
95 maxConn := ratio.Max(ownConn, peerConn)
96 buf = append(buf, []byte(minConn.String())...)
97 buf = append(buf, []byte(maxConn.String())...)
98 }
99
100 secret := Hash(buf)
101
102 return &SharedSecret{
103 CommonTags: common,
104 Secret: secret,
105 Confidence: confidence,
106 }, nil
107 }
108
109 // DeriveKey produces a fixed-length key from a SharedSecret.
110 // Uses Hamadryad with domain separation for key derivation.
111 func DeriveKey(ss *SharedSecret, context string, keyLen int) []byte {
112 var buf []byte
113 buf = append(buf, []byte("dendrite-derive-")...)
114 buf = append(buf, []byte(context)...)
115 buf = append(buf, ss.Secret[:]...)
116
117 derived := Hash(buf)
118 result := derived[:]
119
120 // Extend if needed by iterating.
121 for len(result) < keyLen {
122 buf = buf[:0]
123 buf = append(buf, result...)
124 buf = append(buf, []byte(context)...)
125 next := Hash(buf)
126 result = append(result, next[:]...)
127 }
128
129 return result[:keyLen]
130 }
131
132 // tagSet returns a set of tags from a TypeSignature.
133 func tagSet(tc []spore.TagCount) map[string]bool {
134 m := make(map[string]bool, len(tc))
135 for _, t := range tc {
136 m[t.Tag] = true
137 }
138 return m
139 }
140
141 // tagCountLookup returns the count for a tag in a TypeSignature.
142 func tagCountLookup(tc []spore.TagCount, tag string) int {
143 for _, t := range tc {
144 if t.Tag == tag {
145 return t.Count
146 }
147 }
148 return 0
149 }
150
151 // tagRatioLookup returns the ratio for a tag in a Connectivity slice.
152 func tagRatioLookup(tr []spore.TagRatio, tag string) ratio.Ratio {
153 for _, t := range tr {
154 if t.Tag == tag {
155 return t.Value
156 }
157 }
158 return ratio.Zero
159 }
160