github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/libkb/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 13 "github.com/keybase/go-codec/codec" 14 15 context "golang.org/x/net/context" 16 ) 17 18 // TLFPseudonym is an identifier for a key in a tlf 19 type TlfPseudonym [32]byte 20 21 type KeyGen int 22 type tlfID [16]byte 23 24 const tlfPseudonymVersion = 1 25 26 // TlfPseudonymInfo is what a pseudonym represents 27 type TlfPseudonymInfo struct { 28 // TLF name like: /keybase/private/a,b 29 Name string 30 // TLF id 31 ID tlfID 32 KeyGen KeyGen 33 HmacKey [32]byte 34 } 35 36 // The server returns the current full name of the TLF, in addition to the TlfPseudonymInfo above. 37 type TlfPseudonymServerInfo struct { 38 TlfPseudonymInfo 39 40 // Using untrusted data during decryption is safe because we don't rely on 41 // TLF keys for sender authenticity. (Note however that senders must not 42 // use untrusted keys, or else they'd lose all privacy.) 43 UntrustedCurrentName string 44 } 45 46 func (t TlfPseudonym) String() string { 47 return hex.EncodeToString(t[:]) 48 } 49 50 func (t TlfPseudonym) Eq(r TlfPseudonym) bool { 51 return hmac.Equal(t[:], r[:]) 52 } 53 54 // tlfPseudonymReq is what the server needs to store a pseudonym 55 type tlfPseudonymReq struct { 56 Pseudonym string `json:"tlf_pseudonym"` // hex 57 Name string `json:"tlf_name"` 58 ID string `json:"tlf_id"` // hex 59 KeyGen int `json:"tlf_key_gen"` 60 HmacKey string `json:"hmac_key"` // hex 61 } 62 63 // tlfPseudonymContents is the data packed inside the HMAC 64 type tlfPseudonymContents struct { 65 _struct bool `codec:",toarray"` //nolint 66 Version int 67 Name string 68 ID tlfID 69 KeyGen KeyGen 70 } 71 72 type getTlfPseudonymsRes struct { 73 TlfPseudonyms []getTlfPseudonymRes `json:"tlf_pseudonyms"` 74 Status AppStatus `json:"status"` 75 } 76 77 func (r *getTlfPseudonymsRes) GetAppStatus() *AppStatus { 78 return &r.Status 79 } 80 81 type getTlfPseudonymRes struct { 82 Err *PseudonymGetError `json:"err"` 83 Info *struct { 84 Name string `json:"tlf_name"` 85 UntrustedCurrentName string `json:"untrusted_current_name"` 86 ID string `json:"tlf_id"` // hex 87 KeyGen int `json:"tlf_key_gen"` 88 HmacKey string `json:"hmac_key"` // hex 89 } `json:"info"` 90 } 91 92 type GetTlfPseudonymEither struct { 93 // Exactly one of these 2 fields is nil. 94 Err error 95 Info *TlfPseudonymServerInfo 96 } 97 98 // MakePseudonym makes a TLF pseudonym from the given input. 99 func MakePseudonym(info TlfPseudonymInfo) (TlfPseudonym, error) { 100 input := tlfPseudonymContents{ 101 Version: tlfPseudonymVersion, 102 Name: info.Name, 103 ID: info.ID, 104 KeyGen: info.KeyGen, 105 } 106 mh := codec.MsgpackHandle{WriteExt: true} 107 var buf []byte 108 enc := codec.NewEncoderBytes(&buf, &mh) 109 err := enc.Encode(input) 110 if err != nil { 111 return [32]byte{}, err 112 } 113 mac := hmac.New(sha256.New, info.HmacKey[:]) 114 _, err = mac.Write(buf) 115 if err != nil { 116 return [32]byte{}, err 117 } 118 hmac := MakeByte32(mac.Sum(nil)) 119 return hmac, nil 120 } 121 122 func PostTlfPseudonyms(ctx context.Context, g *GlobalContext, pnymInfos []TlfPseudonymInfo) ([]TlfPseudonym, error) { 123 var pnymReqs []tlfPseudonymReq 124 var pnyms []TlfPseudonym 125 for _, info := range pnymInfos { 126 // Compute the pseudonym 127 pn, err := MakePseudonym(info) 128 if err != nil { 129 return nil, err 130 } 131 pnyms = append(pnyms, pn) 132 pnymReqs = append(pnymReqs, tlfPseudonymReq{ 133 Pseudonym: pn.String(), 134 Name: info.Name, 135 ID: hex.EncodeToString(info.ID[:]), 136 KeyGen: int(info.KeyGen), 137 HmacKey: hex.EncodeToString(info.HmacKey[:]), 138 }) 139 } 140 141 payload := make(JSONPayload) 142 payload["tlf_pseudonyms"] = pnymReqs 143 mctx := NewMetaContext(ctx, g) 144 145 _, err := g.API.PostJSON(mctx, APIArg{ 146 Endpoint: "kbfs/pseudonym/put", 147 JSONPayload: payload, 148 SessionType: APISessionTypeREQUIRED, 149 }) 150 if err != nil { 151 return nil, err 152 } 153 return pnyms, nil 154 } 155 156 // GetTlfPseudonyms fetches info for a list of pseudonyms. 157 // The output structs are returned in the order corresponding to the inputs. 158 // The top-level error is filled if the entire request fails. 159 // The each-struct errors may be filled for per-pseudonym errors. 160 func GetTlfPseudonyms(ctx context.Context, g *GlobalContext, pnyms []TlfPseudonym) ([]GetTlfPseudonymEither, error) { 161 var pnymStrings []string 162 for _, x := range pnyms { 163 pnymStrings = append(pnymStrings, x.String()) 164 } 165 166 payload := make(JSONPayload) 167 payload["tlf_pseudonyms"] = pnymStrings 168 169 var res getTlfPseudonymsRes 170 mctx := NewMetaContext(ctx, g) 171 err := g.API.PostDecode(mctx, 172 APIArg{ 173 Endpoint: "kbfs/pseudonym/get", 174 SessionType: APISessionTypeREQUIRED, 175 JSONPayload: payload, 176 }, 177 &res) 178 if err != nil { 179 return nil, err 180 } 181 182 // Validate the response 183 if len(res.TlfPseudonyms) != len(pnyms) { 184 return nil, &PseudonymGetError{fmt.Sprintf("invalid server response for pseudonym get: len %v != %v", 185 len(res.TlfPseudonyms), len(pnyms))} 186 } 187 var resList []GetTlfPseudonymEither 188 for i, received := range res.TlfPseudonyms { 189 resList = append(resList, checkAndConvertTlfPseudonymFromServer(ctx, g, pnyms[i], received)) 190 } 191 192 return resList, nil 193 } 194 195 func checkAndConvertTlfPseudonymFromServer(ctx context.Context, g *GlobalContext, req TlfPseudonym, received getTlfPseudonymRes) GetTlfPseudonymEither { 196 mkErr := func(err error) GetTlfPseudonymEither { 197 return GetTlfPseudonymEither{ 198 Info: nil, 199 Err: err, 200 } 201 } 202 203 x := GetTlfPseudonymEither{} 204 205 // This check is necessary because of sneaky typed nil. 206 // received.Err's type is lower than x.Err 207 // So doing `x.Err = received.Err` is bad if received.Err is nil. 208 // https://golang.org/doc/faq#nil_error 209 // https://play.golang.org/p/BnjVTGh-gO 210 if received.Err != nil { 211 x.Err = received.Err 212 } 213 214 if received.Info != nil { 215 info := TlfPseudonymServerInfo{} 216 217 info.Name = received.Info.Name 218 info.UntrustedCurrentName = received.Info.UntrustedCurrentName 219 220 err := DecodeHexFixed(info.ID[:], []byte(received.Info.ID)) 221 if err != nil { 222 return mkErr(err) 223 } 224 225 info.KeyGen = KeyGen(received.Info.KeyGen) 226 227 err = DecodeHexFixed(info.HmacKey[:], []byte(received.Info.HmacKey)) 228 if err != nil { 229 return mkErr(err) 230 } 231 232 x.Info = &info 233 } 234 235 err := checkTlfPseudonymFromServer(ctx, g, req, x) 236 if err != nil { 237 return mkErr(err) 238 } 239 return x 240 } 241 242 func checkTlfPseudonymFromServer(ctx context.Context, g *GlobalContext, req TlfPseudonym, received GetTlfPseudonymEither) error { 243 // Exactly one of Info and Err should exist 244 if (received.Info == nil) == (received.Err == nil) { 245 return &PseudonymGetError{fmt.Sprintf("invalid server response for pseudonym get: %s", req)} 246 } 247 248 // Errors don't need validation 249 if received.Info == nil { 250 return nil 251 } 252 253 // Check that the pseudonym info matches the query. 254 pn, err := MakePseudonym(received.Info.TlfPseudonymInfo) 255 if err != nil { 256 // Error creating pseudonym locally 257 return &PseudonymGetError{err.Error()} 258 } 259 if !req.Eq(pn) { 260 return &PseudonymGetError{fmt.Sprintf("returned data does not match pseudonym: %s != %s", 261 req, pn)} 262 } 263 264 return nil 265 } 266 267 func RandomHmacKey() [32]byte { 268 slice, err := RandBytes(32) 269 if err != nil { 270 panic(err) 271 } 272 return MakeByte32(slice) 273 }