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 }