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  }