github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/libkb/key_pseudonym.go (about)

     1  // Copyright 2017 Keybase Inc. All rights reserved.
     2  // Use of this source code is governed by a BSD
     3  // license that can be found in the LICENSE file.
     4  
     5  package libkb
     6  
     7  import (
     8  	"crypto/hmac"
     9  	"crypto/sha256"
    10  	"encoding/hex"
    11  	"fmt"
    12  	"strings"
    13  
    14  	keybase1 "github.com/keybase/client/go/protocol/keybase1"
    15  	"github.com/keybase/go-codec/codec"
    16  )
    17  
    18  // KeyPseudonym is a "random looking" identifier which refers to a specific application key belonging to a user or team
    19  // (the key is referred to by the application id, the generation number and the user or team id).
    20  // Pseudonyms are only used for saltpack encryption at the moment, but more applications are possible. They are used
    21  // to avoid that, when decrypting a message encrypted with a team key, a user has to loop through all the keys of all the
    22  // teams he is part of to find the right one. To avoid this, when encrypting for a team, the sender generates such a pseudonym
    23  // (which refers to the key it used to encrypt), sends it to the server and includes it as a recipient identifier for the
    24  // appropriare recipient payload box in the header of the saltpack message. When decrypting a saltpack message, a recipient
    25  // can ask the server if any of the recipient identifiers in the message correspond to known pseudonyms (for teams the user is part of),
    26  // and use the response from the server to identify which key to use for decryption. This mechanism substitutes an older kbfs based one
    27  // (which is still supported to allow decryptions of old saltpack messages).
    28  //
    29  // A pseudonym is computed as an HMAC (with a random nonce as a key) of the information it refers to in order to
    30  // prevent the server from tampering with the mapping (we rely on collision resistance and not unforgeability,
    31  // as the server knows the nonce, so a simple SHA256 would have worked as well).
    32  type KeyPseudonym [32]byte
    33  
    34  func (p KeyPseudonym) String() string {
    35  	return hex.EncodeToString(p[:])
    36  }
    37  
    38  func (p *KeyPseudonym) MarshalJSON() ([]byte, error) {
    39  	return keybase1.Quote(p.String()), nil
    40  }
    41  
    42  func (p *KeyPseudonym) UnmarshalJSON(b []byte) error {
    43  	n, err := hex.Decode((*p)[:], keybase1.UnquoteBytes(b))
    44  	if err != nil {
    45  		return err
    46  	}
    47  	if n != len(*p) {
    48  		return NewKeyPseudonymError("KeyPseudonym has wrong length")
    49  	}
    50  	return nil
    51  }
    52  
    53  func (p KeyPseudonym) Eq(r KeyPseudonym) bool {
    54  	return hmac.Equal(p[:], r[:])
    55  }
    56  
    57  type KeyPseudonymNonce [32]byte
    58  
    59  func (p KeyPseudonymNonce) String() string {
    60  	return hex.EncodeToString(p[:])
    61  }
    62  
    63  func (p *KeyPseudonymNonce) MarshalJSON() ([]byte, error) {
    64  	return keybase1.Quote(p.String()), nil
    65  }
    66  
    67  func (p *KeyPseudonymNonce) UnmarshalJSON(b []byte) error {
    68  	n, err := hex.Decode((*p)[:], keybase1.UnquoteBytes(b))
    69  	if err != nil {
    70  		return err
    71  	}
    72  	if n != len(*p) {
    73  		return NewKeyPseudonymError("KeyPseudonymNonce has wrong length")
    74  	}
    75  	return nil
    76  }
    77  
    78  const KeyPseudonymVersion = 1
    79  
    80  // KeyPseudonymInfo contains the KeyPseudonym as well as information about the key it represents.
    81  type KeyPseudonymInfo struct {
    82  	KeyPseudonym KeyPseudonym
    83  	ID           keybase1.UserOrTeamID
    84  	Application  keybase1.TeamApplication
    85  	KeyGen       KeyGen
    86  	Nonce        KeyPseudonymNonce
    87  }
    88  
    89  // keyPseudonymReq is what the server needs to store a pseudonym
    90  type keyPseudonymReq struct {
    91  	Pseudonym   KeyPseudonym          `json:"key_pseudonym"`   // hex
    92  	ID          keybase1.UserOrTeamID `json:"user_or_team_id"` // hex
    93  	Application int                   `json:"app_id"`
    94  	KeyGen      int                   `json:"key_gen"`
    95  	Nonce       KeyPseudonymNonce     `json:"nonce"` // hex
    96  }
    97  
    98  // keyPseudonymContents is the data packed inside the HMAC
    99  type keyPseudonymContents struct {
   100  	_struct     bool `codec:",toarray"` //nolint
   101  	Version     int
   102  	ID          [16]byte                 // keybase1.UserOrTeamId as a byte array
   103  	Application keybase1.TeamApplication // int
   104  	KeyGen      KeyGen                   // int
   105  }
   106  
   107  type getKeyPseudonymsRes struct {
   108  	KeyPseudonyms []getKeyPseudonymRes `json:"key_pseudonyms"`
   109  	Status        AppStatus            `json:"status"`
   110  }
   111  
   112  func (r *getKeyPseudonymsRes) GetAppStatus() *AppStatus {
   113  	return &r.Status
   114  }
   115  
   116  type getKeyPseudonymRes struct {
   117  	Err  *PseudonymGetError `json:"err"`
   118  	Info *struct {
   119  		ID          keybase1.UserOrTeamID `json:"user_or_team_id"`
   120  		Application int                   `json:"app_id"`
   121  		KeyGen      int                   `json:"key_gen"`
   122  		Nonce       KeyPseudonymNonce     `json:"nonce"`
   123  	} `json:"info"`
   124  }
   125  
   126  type KeyPseudonymOrError struct {
   127  	// Exactly one of these 2 fields is nil.
   128  	Err  error
   129  	Info *KeyPseudonymInfo
   130  }
   131  
   132  // MakePseudonym makes a key pseudonym from the given input.
   133  func MakeKeyPseudonym(info KeyPseudonymInfo) (KeyPseudonym, error) {
   134  	var idBytes [16]byte
   135  	id := info.ID.ToBytes()
   136  
   137  	copy(idBytes[:], id)
   138  
   139  	input := keyPseudonymContents{
   140  		Version:     KeyPseudonymVersion,
   141  		ID:          idBytes,
   142  		Application: info.Application,
   143  		KeyGen:      info.KeyGen,
   144  	}
   145  	mh := codec.MsgpackHandle{WriteExt: true}
   146  	var buf []byte
   147  	enc := codec.NewEncoderBytes(&buf, &mh)
   148  	if err := enc.Encode(input); err != nil {
   149  		return [32]byte{}, err
   150  	}
   151  
   152  	mac := hmac.New(sha256.New, info.Nonce[:])
   153  	_, err := mac.Write(buf)
   154  	if err != nil {
   155  		return [32]byte{}, err
   156  	}
   157  	hmac := MakeByte32(mac.Sum(nil))
   158  	return hmac, nil
   159  }
   160  
   161  // MakeAndPostKeyPseudonyms fills the KeyPseudonym field of each of the pnymInfos with the appropriate KeyPseudonym.
   162  func MakeAndPostKeyPseudonyms(m MetaContext, pnymInfos *[]KeyPseudonymInfo) (err error) {
   163  	var pnymReqs []keyPseudonymReq
   164  
   165  	if len(*pnymInfos) == 0 {
   166  		return
   167  	}
   168  
   169  	for i, info := range *pnymInfos {
   170  		// Compute the pseudonym
   171  		var pnym KeyPseudonym
   172  		pnym, err = MakeKeyPseudonym(info)
   173  		if err != nil {
   174  			return
   175  		}
   176  		(*pnymInfos)[i].KeyPseudonym = pnym
   177  		pnymReqs = append(pnymReqs, keyPseudonymReq{
   178  			Pseudonym:   pnym,
   179  			ID:          info.ID,
   180  			Application: int(info.Application),
   181  			KeyGen:      int(info.KeyGen),
   182  			Nonce:       info.Nonce,
   183  		})
   184  	}
   185  
   186  	payload := make(JSONPayload)
   187  	payload["key_pseudonyms"] = pnymReqs
   188  
   189  	_, err = m.G().API.PostJSON(m, APIArg{
   190  		Endpoint:    "team/key_pseudonym",
   191  		JSONPayload: payload,
   192  		SessionType: APISessionTypeREQUIRED,
   193  	})
   194  	if err != nil {
   195  		return
   196  	}
   197  	return nil
   198  }
   199  
   200  // GetKeyPseudonyms fetches info for a list of pseudonyms.
   201  // The output structs are returned in the order corresponding to the inputs.
   202  // The top-level error is filled if the entire request fails.
   203  // The error in each of the returned structs may be filled for per-pseudonym errors.
   204  func GetKeyPseudonyms(m MetaContext, pnyms []KeyPseudonym) ([]KeyPseudonymOrError, error) {
   205  	var pnymStrings []string
   206  	for _, x := range pnyms {
   207  		pnymStrings = append(pnymStrings, x.String())
   208  	}
   209  
   210  	var res getKeyPseudonymsRes
   211  	err := m.G().API.GetDecode(m,
   212  		APIArg{
   213  			Endpoint:    "team/key_pseudonym",
   214  			SessionType: APISessionTypeREQUIRED,
   215  			Args: HTTPArgs{
   216  				"key_pseudonyms": S{Val: strings.Join(pnymStrings, ",")},
   217  			},
   218  		},
   219  		&res)
   220  	if err != nil {
   221  		return nil, err
   222  	}
   223  
   224  	// Validate the response
   225  	if len(res.KeyPseudonyms) != len(pnyms) {
   226  		return nil, &KeyPseudonymGetError{fmt.Sprintf("invalid server response for pseudonym get: len %v != %v",
   227  			len(res.KeyPseudonyms), len(pnyms))}
   228  	}
   229  	var resList []KeyPseudonymOrError
   230  	for i, received := range res.KeyPseudonyms {
   231  		resList = append(resList, checkAndConvertKeyPseudonymFromServer(pnyms[i], received))
   232  	}
   233  
   234  	return resList, nil
   235  }
   236  
   237  func checkAndConvertKeyPseudonymFromServer(req KeyPseudonym, received getKeyPseudonymRes) KeyPseudonymOrError {
   238  	mkErr := func(err error) KeyPseudonymOrError {
   239  		return KeyPseudonymOrError{
   240  			Info: nil,
   241  			Err:  err,
   242  		}
   243  	}
   244  
   245  	x := KeyPseudonymOrError{}
   246  
   247  	// This check is necessary because of sneaky typed nil.
   248  	// received.Err's type is lower than x.Err
   249  	// So doing `x.Err = received.Err` is bad if received.Err is nil.
   250  	// https://golang.org/doc/faq#nil_error
   251  	// https://play.golang.org/p/BnjVTGh-gO
   252  	if received.Err != nil {
   253  		x.Err = received.Err
   254  	}
   255  
   256  	if received.Info != nil {
   257  		info := KeyPseudonymInfo{}
   258  
   259  		info.ID = received.Info.ID
   260  		info.KeyGen = KeyGen(received.Info.KeyGen)
   261  		info.Application = keybase1.TeamApplication(received.Info.Application)
   262  		info.Nonce = received.Info.Nonce
   263  
   264  		x.Info = &info
   265  	}
   266  
   267  	err := checkKeyPseudonymFromServer(req, x)
   268  	if err != nil {
   269  		return mkErr(err)
   270  	}
   271  	return x
   272  }
   273  
   274  func checkKeyPseudonymFromServer(req KeyPseudonym, received KeyPseudonymOrError) error {
   275  	// Exactly one of Info and Err should exist
   276  	if (received.Info == nil) == (received.Err == nil) {
   277  		return &KeyPseudonymGetError{fmt.Sprintf("invalid server response for key_pseudonym get: %s", req)}
   278  	}
   279  
   280  	// Errors don't need validation
   281  	if received.Info == nil {
   282  		return nil
   283  	}
   284  
   285  	// Check that the pseudonym info matches the query.
   286  	pn, err := MakeKeyPseudonym(*received.Info)
   287  	if err != nil {
   288  		// Error creating pseudonym locally
   289  		return &KeyPseudonymGetError{err.Error()}
   290  	}
   291  	if !req.Eq(pn) {
   292  		return &KeyPseudonymGetError{fmt.Sprintf("returned data does not match key_pseudonym: %s != %s",
   293  			req, pn)}
   294  	}
   295  
   296  	return nil
   297  }
   298  
   299  // RandomPseudonymNonce returns a random nonce, which is used as an HMAC key.
   300  func RandomPseudonymNonce() KeyPseudonymNonce {
   301  	slice, err := RandBytes(32)
   302  	if err != nil {
   303  		panic(err)
   304  	}
   305  	return KeyPseudonymNonce(MakeByte32(slice))
   306  }