github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/teams/hidden/ratchet.go (about) 1 package hidden 2 3 import ( 4 "crypto/hmac" 5 "crypto/sha512" 6 "encoding/base64" 7 "encoding/hex" 8 "fmt" 9 10 "github.com/keybase/client/go/libkb" 11 "github.com/keybase/client/go/msgpack" 12 "github.com/keybase/client/go/protocol/keybase1" 13 "github.com/keybase/client/go/sig3" 14 ) 15 16 // Hidden Ratchet computation, parsing, and manipulation libraries. 17 // 18 // In main chain links, we might now see ratchets that look like this: 19 // 20 // body.teams.ratchets = [ "1e1e39427938aa0dffe2adc6323493f9edcbd4c09f4b05b4b884b09ee98fd2b1" ] 21 // 22 // When such a link is returned from the server via team/get, it should also be accompanied with 23 // "blinding" keys, for the purposes of unblinding, such as: 24 // 25 // "ratchet_blinding_keys": "kYOhYoOhaMQgV2dLp8XOVd9wzL/jbWJOVsIUp7qK+oTe0HCH1K2dEeihcwKhdBGhcoKha8QgX8+MXRs5K99h5pRAYz3qNQOKkdH0lzr8WUe+xEPiYeOhcsQgHh45Qnk4qg3/4q3GMjST+e3L1MCfSwW0uISwnumP0rGhdgE=", 26 // 27 // This field is of type EncodedRatchedBlindingKeySet; when base64-decoded, and unmsgpacked, it 28 // fits into a RatchetObj; so for instance: 29 // 30 // [ { b: 31 // { h: <Buffer 57 67 4b a7 c5 ce 55 df 70 cc bf e3 6d 62 4e 56 c2 14 a7 ba 8a fa 84 de d0 70 87 d4 ad 9d 11 e8>, 32 // s: 2, t: 17 }, 33 // r: 34 // { k: <Buffer 5f cf 8c 5d 1b 39 2b df 61 e6 94 40 63 3d ea 35 03 8a 91 d1 f4 97 3a fc 59 47 be c4 43 e2 61 e3>, 35 // r: <Buffer 1e 1e 39 42 79 38 aa 0d ff e2 ad c6 32 34 93 f9 ed cb d4 c0 9f 4b 05 b4 b8 84 b0 9e e9 8f d2 b1> }, 36 // v: 1 } ] 37 // 38 // As we can see, r.r corresponds to what was sent in the visible team chain link. r.k is the blinding key. 39 // When we compute HMAC-SHA512(r.k, pack(b)), whe should get r.r 40 // 41 // This file handles encoding/decoding, packing/unpacking, marshalling/unmarshalling of this data. 42 // 43 44 // EncodedRatchetBlindingKeySet is a b64-encoded, msgpacked map of a RatchetBlindingKeySet, used to POST up to the 45 // server 1 new ratchet. Note that even in the case of multiple signatures (inside a TX), it only is really necessary 46 // to ratchet the hidden chain once. So this suffices. 47 type EncodedRatchetBlindingKeySet string 48 49 func (e EncodedRatchetBlindingKeySet) IsNil() bool { return len(e) == 0 } 50 func (e EncodedRatchetBlindingKeySet) String() string { return string(e) } 51 52 // BlindingKey is a 32-byte random byte array that is used to blind ratchets, so that they can be 53 // selectively hidden via access control. 54 type BlindingKey [32]byte 55 56 // SCTeamRatchet is the result of HMAC-SHA512(k,v)[0:32], where k is a random Blinding Key, 57 // and v is the msgpack of a sig3.Tail. 58 type SCTeamRatchet [32]byte 59 60 // RatchetVersion is always 1, for now. 61 type RatchetVersion int 62 63 const RatchetVersion1 = RatchetVersion(1) 64 65 type RatchetBlind struct { 66 Hash SCTeamRatchet `codec:"r"` 67 Key BlindingKey `codec:"k"` 68 } 69 70 type RatchetObj struct { 71 Body sig3.Tail `codec:"b"` 72 RatchetBlind RatchetBlind `codec:"r"` 73 Version RatchetVersion `codec:"v"` 74 } 75 76 // Ratchet is an object that's used in the teams/teams* and teams/transaction* world to make a visible team chain 77 // link incorporate one hidden team ratchet. This means we have to post data both into the signature field (the blinded ratchet) 78 // and also data into the sig POST, the blinding keys, etc. This little object conveniniently encapsulates all of that. 79 type Ratchet struct { 80 encoded EncodedRatchetBlindingKeySet 81 decoded RatchetObj 82 } 83 84 // RatchetBlindingKeySet is sent down from the server when we are reading a set of blinding ratchets from 85 // the team/get response. 86 type RatchetBlindingKeySet struct { 87 m map[SCTeamRatchet]RatchetObj 88 } 89 90 func (r *RatchetBlindingKeySet) Add(ratchet Ratchet) { 91 if r.m == nil { 92 r.m = make(map[SCTeamRatchet]RatchetObj) 93 } 94 o := ratchet.decoded 95 r.m[o.RatchetBlind.Hash] = o 96 } 97 98 func (r SCTeamRatchet) String() string { 99 return hex.EncodeToString(r[:]) 100 } 101 102 // UnmarshalJSON is implicitly used in chain_parse.go move SCTeamRatchets into and out of JSON 103 // from the hidden team chain. 104 func (r *SCTeamRatchet) UnmarshalJSON(b []byte) error { 105 unquoted := keybase1.UnquoteBytes(b) 106 if len(unquoted) == 0 { 107 return nil 108 } 109 b, err := hex.DecodeString(string(unquoted)) 110 if err != nil { 111 return err 112 } 113 if len(b) != len(*r) { 114 return newRatchetError("cannot decode team ratchet; wrong size") 115 } 116 copy((*r)[:], b) 117 return nil 118 } 119 120 // Get the chain tail that corresponds to the given ratchet. Return nil if we fail to find it, and 121 // an object if we find it. 122 func (r *RatchetBlindingKeySet) Get(ratchet SCTeamRatchet) *sig3.Tail { 123 if r == nil || r.m == nil { 124 return nil 125 } 126 obj, ok := r.m[ratchet] 127 if !ok { 128 return nil 129 } 130 return &obj.Body 131 } 132 133 // UnmarshalJSON is implicitly used in rawTeam-based API calls to move RatchetBlindingKeySets into and out of JSON 134 // from the hidden team chain. 135 func (r *RatchetBlindingKeySet) UnmarshalJSON(b []byte) error { 136 r.m = make(map[SCTeamRatchet]RatchetObj) 137 if string(b) == "null" { 138 return nil 139 } 140 unquoted := keybase1.UnquoteBytes(b) 141 if len(unquoted) == 0 { 142 return nil 143 } 144 b, err := base64.StdEncoding.DecodeString(string(unquoted)) 145 if err != nil { 146 return err 147 } 148 var arr []RatchetObj 149 err = msgpack.Decode(&arr, b) 150 if err != nil { 151 return err 152 } 153 for _, e := range arr { 154 err = e.check() 155 if err != nil { 156 return err 157 } 158 r.m[e.RatchetBlind.Hash] = e 159 } 160 return err 161 } 162 163 func (r *SCTeamRatchet) MarshalJSON() ([]byte, error) { 164 s := hex.EncodeToString((*r)[:]) 165 b := keybase1.Quote(s) 166 return b, nil 167 } 168 169 func (r *Ratchet) ToTeamSection() []SCTeamRatchet { 170 if r == nil { 171 return nil 172 } 173 return []SCTeamRatchet{r.decoded.RatchetBlind.Hash} 174 } 175 176 func (r *Ratchet) ToSigPayload() (ret EncodedRatchetBlindingKeySet) { 177 if r == nil { 178 return ret 179 } 180 return r.encoded 181 } 182 183 func generateBlindingKey() (BlindingKey, error) { 184 var ret BlindingKey 185 tmp, err := libkb.RandBytes(len(ret)) 186 if err != nil { 187 return ret, err 188 } 189 copy(ret[:], tmp) 190 return ret, nil 191 } 192 193 func (r *RatchetBlind) computeToSelf(tail sig3.Tail) (err error) { 194 h, err := r.compute(tail) 195 if err != nil { 196 return err 197 } 198 r.Hash = h 199 return nil 200 } 201 202 // check the internal consistency of this blinded ratchet against itself. 203 func (r *RatchetObj) check() (err error) { 204 return r.RatchetBlind.check(r.Body) 205 } 206 207 // check the internal consistency of this blinded ratchet against the input Tail value. 208 func (r *RatchetBlind) check(tail sig3.Tail) (err error) { 209 computed, err := r.compute(tail) 210 if err != nil { 211 return err 212 } 213 if !hmac.Equal(computed[:], r.Hash[:]) { 214 return newRatchetError("blinding check failed %x v %x", computed[:], r.Hash[:]) 215 } 216 return nil 217 } 218 219 // compute combines the internal ratchet blinding key and in the input sig3.Tail to 220 // make a blinded ratchet, as we would post into sigchain links. 221 func (r *RatchetBlind) compute(tail sig3.Tail) (ret SCTeamRatchet, err error) { 222 223 b, err := msgpack.Encode(tail) 224 if err != nil { 225 return ret, err 226 } 227 h := hmac.New(sha512.New, r.Key[:]) 228 _, err = h.Write(b) 229 if err != nil { 230 return ret, err 231 } 232 d := h.Sum(nil)[0:32] 233 copy(ret[:], d) 234 return ret, nil 235 } 236 237 func (r *RatchetObj) generate(mctx libkb.MetaContext) (err error) { 238 r.RatchetBlind.Key, err = generateBlindingKey() 239 if err != nil { 240 return err 241 } 242 err = r.RatchetBlind.computeToSelf(r.Body) 243 if err != nil { 244 return err 245 } 246 return nil 247 } 248 249 func (r *Ratchet) encode(mctx libkb.MetaContext) (err error) { 250 var rbk RatchetBlindingKeySet 251 rbk.Add(*r) 252 r.encoded, err = rbk.encode() 253 if err != nil { 254 return err 255 } 256 return nil 257 } 258 259 func (r RatchetBlindingKeySet) encode() (ret EncodedRatchetBlindingKeySet, err error) { 260 var arr []RatchetObj 261 for _, v := range r.m { 262 arr = append(arr, v) 263 } 264 b, err := msgpack.Encode(arr) 265 if err != nil { 266 return ret, err 267 } 268 return EncodedRatchetBlindingKeySet(base64.StdEncoding.EncodeToString(b)), nil 269 } 270 271 // generateRatchet, cooking up a new blinding key, and computing the encoding and blinding of 272 // the ratchet. 273 func generateRatchet(mctx libkb.MetaContext, b sig3.Tail) (ret *Ratchet, err error) { 274 ret = &Ratchet{ 275 decoded: RatchetObj{ 276 Version: RatchetVersion1, 277 Body: b, 278 }, 279 } 280 err = ret.decoded.generate(mctx) 281 if err != nil { 282 return nil, err 283 } 284 err = ret.encode(mctx) 285 if err != nil { 286 return nil, err 287 } 288 return ret, nil 289 } 290 291 // MakeRatchet constructs a new Ratchet object for the given team's hidden tail, blinds 292 // it with a randomly-generated blinding key, and then packages all relevant info up into 293 // and encoding that can be easily posted to the API server. 294 func MakeRatchet(mctx libkb.MetaContext, state *keybase1.HiddenTeamChain) (ret *Ratchet, err error) { 295 if state == nil { 296 mctx.Debug("hidden.MakeRatchet: returning a nil ratchet since hidden team is nil") 297 return nil, nil 298 } 299 id := state.Id 300 301 defer mctx.Trace(fmt.Sprintf("hidden.MakeRatchet(%s)", id), &err)() 302 303 err = CheckFeatureGateForSupport(mctx, id) 304 if err != nil { 305 mctx.VLogf(libkb.VLog0, "skipping ratchet for team id %s due to feature-flag", id) 306 return nil, nil 307 } 308 tail := state.TailTriple() 309 if tail == nil || tail.Seqno == keybase1.Seqno(0) { 310 mctx.Debug("no tail found") 311 return nil, nil 312 } 313 itail, err := sig3.ImportTail(*tail) 314 if err != nil { 315 return nil, err 316 } 317 mctx.Debug("ratcheting at tail (%s,%d)", itail.Hash, itail.Seqno) 318 ret, err = generateRatchet(mctx, *itail) 319 if err != nil { 320 return nil, err 321 } 322 return ret, nil 323 } 324 325 const jsonPayloadKey = "ratchet_blinding_keys" 326 327 // AddToJSONPayload is used to add the ratching blinding information to an API POST 328 func (e EncodedRatchetBlindingKeySet) AddToJSONPayload(p libkb.JSONPayload) { 329 if e.IsNil() { 330 return 331 } 332 p[jsonPayloadKey] = e.String() 333 } 334 335 func (r RatchetBlindingKeySet) AddToJSONPayload(p libkb.JSONPayload) error { 336 if len(r.m) == 0 { 337 return nil 338 } 339 encoded, err := r.encode() 340 if err != nil { 341 return err 342 } 343 encoded.AddToJSONPayload(p) 344 return nil 345 } 346 347 // AddToJSONPayload is used to add the ratching blinding information to an API POST 348 func (r *Ratchet) AddToJSONPayload(p libkb.JSONPayload) { 349 if r == nil { 350 return 351 } 352 r.ToSigPayload().AddToJSONPayload(p) 353 } 354 355 // checkRatchet against what we have in state, and error out if it clashes. 356 func checkRatchet(mctx libkb.MetaContext, state *keybase1.HiddenTeamChain, ratchet keybase1.LinkTripleAndTime) (err error) { 357 if state == nil { 358 return nil 359 } 360 if ratchet.Triple.SeqType != sig3.ChainTypeTeamPrivateHidden { 361 return newRatchetError("bad chain type: %s", ratchet.Triple.SeqType) 362 } 363 364 // The new ratchet can't clash the existing accepted ratchets 365 for _, accepted := range state.RatchetSet.Flat() { 366 if accepted.Clashes(ratchet) { 367 return newRatchetError("bad ratchet, clashes existing pin: %+v != %v", accepted, accepted) 368 } 369 } 370 371 q := ratchet.Triple.Seqno 372 link, ok := state.Outer[q] 373 374 // If either the ratchet didn't match a known link, or equals what's already there, great. 375 if ok && !link.Eq(ratchet.Triple.LinkID) { 376 return newRatchetError("Ratchet failed to match a currently accepted chainlink: %+v", ratchet) 377 } 378 379 return nil 380 } 381 382 // checkRatchets iterates over the given RatchetSet and checks each one for clashes against our current state. 383 func checkRatchets(mctx libkb.MetaContext, state *keybase1.HiddenTeamChain, ratchets keybase1.HiddenTeamChainRatchetSet) (err error) { 384 for _, r := range ratchets.Flat() { 385 err = checkRatchet(mctx, state, r) 386 if err != nil { 387 return err 388 } 389 } 390 return nil 391 }