github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/teambot/member_keyer.go (about) 1 package teambot 2 3 import ( 4 "crypto/hmac" 5 "crypto/sha256" 6 "encoding/json" 7 "fmt" 8 "log" 9 "sync" 10 11 lru "github.com/hashicorp/golang-lru" 12 "github.com/keybase/client/go/libkb" 13 "github.com/keybase/client/go/protocol/gregor1" 14 "github.com/keybase/client/go/protocol/keybase1" 15 "github.com/keybase/client/go/teams" 16 ) 17 18 type MemberKeyer struct { 19 locktab *libkb.LockTable 20 sync.RWMutex 21 lru *lru.Cache 22 } 23 24 var _ libkb.TeambotMemberKeyer = (*MemberKeyer)(nil) 25 26 func NewMemberKeyer(mctx libkb.MetaContext) *MemberKeyer { 27 nlru, err := lru.New(lruSize) 28 if err != nil { 29 // lru.New only panics if size <= 0 30 log.Panicf("Could not create lru cache: %v", err) 31 } 32 return &MemberKeyer{ 33 lru: nlru, 34 locktab: libkb.NewLockTable(), 35 } 36 } 37 38 // There are plenty of race conditions where the PTK membership list can change 39 // out from under us while we're in the middle of posting a new key, causing 40 // the post to fail. Detect these conditions and retry. 41 func (k *MemberKeyer) retryWrapper(mctx libkb.MetaContext, retryFn func() error) (err error) { 42 for tries := 0; tries < maxRetries; tries++ { 43 if err = retryFn(); err == nil { 44 return nil 45 } 46 if !libkb.IsEphemeralRetryableError(err) { 47 return err 48 } 49 mctx.Debug("MemberKeyer#retryWrapper found a retryable error on try %d: %v", 50 tries, err) 51 select { 52 case <-mctx.Ctx().Done(): 53 return mctx.Ctx().Err() 54 default: 55 // continue retrying 56 } 57 } 58 return err 59 } 60 61 func (k *MemberKeyer) lockForTeamIDAndApp(mctx libkb.MetaContext, teamID keybase1.TeamID, app keybase1.TeamApplication) func() { 62 k.RLock() 63 lock := k.locktab.AcquireOnName(mctx.Ctx(), mctx.G(), k.lockKey(teamID, app)) 64 return func() { 65 k.RUnlock() 66 lock.Release(mctx.Ctx()) 67 } 68 } 69 70 func (k *MemberKeyer) lockKey(teamID keybase1.TeamID, app keybase1.TeamApplication) string { 71 return fmt.Sprintf("%s-%d", teamID.String(), app) 72 } 73 74 func (k *MemberKeyer) cacheKey(teamID keybase1.TeamID, botUID keybase1.UID, 75 app keybase1.TeamApplication, generation keybase1.TeambotKeyGeneration) string { 76 return fmt.Sprintf("%s-%s-%d-%d", teamID, botUID, app, generation) 77 } 78 79 // GetOrCreateTeambotKey derives a TeambotKey from the given `appKey`, and 80 // posts the result to the server if necessary. An in memory cache is kept of 81 // keys that have already been posted so we don't hit the server each time. 82 func (k *MemberKeyer) GetOrCreateTeambotKey(mctx libkb.MetaContext, teamID keybase1.TeamID, 83 gBotUID gregor1.UID, appKey keybase1.TeamApplicationKey) ( 84 key keybase1.TeambotKey, created bool, err error) { 85 mctx = mctx.WithLogTag("GOCTBK") 86 87 botUID, err := keybase1.UIDFromSlice(gBotUID.Bytes()) 88 if err != nil { 89 return key, false, err 90 } 91 92 err = k.retryWrapper(mctx, func() error { 93 unlock := k.lockForTeamIDAndApp(mctx, teamID, appKey.Application) 94 defer unlock() 95 key, created, err = k.getOrCreateTeambotKeyLocked(mctx, teamID, botUID, appKey) 96 return err 97 }) 98 return key, created, err 99 } 100 101 func (k *MemberKeyer) getOrCreateTeambotKeyLocked(mctx libkb.MetaContext, teamID keybase1.TeamID, 102 botUID keybase1.UID, appKey keybase1.TeamApplicationKey) ( 103 key keybase1.TeambotKey, created bool, err error) { 104 defer mctx.Trace(fmt.Sprintf("getOrCreateTeambotKeyLocked: teamID: %v, botUID: %v", teamID, botUID), &err)() 105 106 seed := k.deriveTeambotKeyFromAppKey(mctx, appKey, botUID) 107 108 // Check our cache and see if we should attempt to publish the our derived 109 // key or not. 110 cacheKey := k.cacheKey(teamID, botUID, appKey.Application, keybase1.TeambotKeyGeneration(appKey.KeyGeneration)) 111 entry, ok := k.lru.Get(cacheKey) 112 if ok { 113 metadata, ok := entry.(keybase1.TeambotKeyMetadata) 114 if !ok { 115 return key, false, fmt.Errorf("unable to load teambotkey metadata from cache found %T, expected %T", 116 entry, keybase1.TeambotKeyMetadata{}) 117 } 118 key = keybase1.TeambotKey{ 119 Seed: seed, 120 Metadata: metadata, 121 } 122 return key, false, nil 123 } 124 125 team, err := teams.Load(mctx.Ctx(), mctx.G(), keybase1.LoadTeamArg{ 126 ID: teamID, 127 }) 128 if err != nil { 129 return key, false, err 130 } 131 132 sig, box, isRestrictedBotMember, err := k.prepareNewTeambotKey(mctx, team, botUID, appKey) 133 if err != nil { 134 return key, false, err 135 } 136 137 // If the bot is not a restricted bot member don't try to publish the key 138 // for them. This can happen when decrypting past content after the bot is 139 // removed from the team. 140 metadata := box.Metadata 141 if isRestrictedBotMember { 142 if err = k.postNewTeambotKey(mctx, team.ID, sig, box.Box); err != nil { 143 return key, false, err 144 } 145 } 146 147 k.lru.Add(cacheKey, metadata) 148 key = keybase1.TeambotKey{ 149 Seed: seed, 150 Metadata: metadata, 151 } 152 153 return key, isRestrictedBotMember, nil 154 } 155 156 func (k *MemberKeyer) deriveTeambotKeyFromAppKey(mctx libkb.MetaContext, applicationKey keybase1.TeamApplicationKey, botUID keybase1.UID) keybase1.Bytes32 { 157 hasher := hmac.New(sha256.New, applicationKey.Key[:]) 158 _, _ = hasher.Write(botUID.ToBytes()) 159 _, _ = hasher.Write([]byte{byte(applicationKey.Application)}) 160 _, _ = hasher.Write([]byte(libkb.EncryptionReasonTeambotKey)) 161 return libkb.MakeByte32(hasher.Sum(nil)) 162 } 163 164 func (k *MemberKeyer) postNewTeambotKey(mctx libkb.MetaContext, teamID keybase1.TeamID, 165 sig, box string) (err error) { 166 defer mctx.Trace("MemberKeyer#postNewTeambotKey", &err)() 167 168 apiArg := libkb.APIArg{ 169 Endpoint: "teambot/key", 170 SessionType: libkb.APISessionTypeREQUIRED, 171 Args: libkb.HTTPArgs{ 172 "team_id": libkb.S{Val: string(teamID)}, 173 "sig": libkb.S{Val: sig}, 174 "box": libkb.S{Val: box}, 175 "is_ephemeral": libkb.B{Val: false}, 176 }, 177 AppStatusCodes: []int{libkb.SCOk, libkb.SCTeambotKeyGenerationExists}, 178 } 179 _, err = mctx.G().GetAPI().Post(mctx, apiArg) 180 return err 181 } 182 183 func (k *MemberKeyer) prepareNewTeambotKey(mctx libkb.MetaContext, team *teams.Team, 184 botUID keybase1.UID, appKey keybase1.TeamApplicationKey) ( 185 sig string, box *keybase1.TeambotKeyBoxed, isRestrictedBotMember bool, err error) { 186 defer mctx.Trace(fmt.Sprintf("MemberKeyer#prepareNewTeambotKey: teamID: %v, botUID: %v", team.ID, botUID), 187 &err)() 188 189 upak, _, err := mctx.G().GetUPAKLoader().LoadV2( 190 libkb.NewLoadUserArgWithMetaContext(mctx).WithUID(botUID)) 191 if err != nil { 192 return "", nil, false, err 193 } 194 195 latestPUK := upak.Current.GetLatestPerUserKey() 196 if latestPUK == nil { 197 // The latest PUK might be stale. Force a reload, then check this over again. 198 upak, _, err = mctx.G().GetUPAKLoader().LoadV2( 199 libkb.NewLoadUserArgWithMetaContext(mctx).WithUID(botUID).WithForceReload()) 200 if err != nil { 201 return "", nil, false, err 202 } 203 latestPUK = upak.Current.GetLatestPerUserKey() 204 if latestPUK == nil { 205 return "", nil, false, fmt.Errorf("No PUK") 206 } 207 } 208 209 seed := k.deriveTeambotKeyFromAppKey(mctx, appKey, botUID) 210 211 recipientKey, err := libkb.ImportKeypairFromKID(latestPUK.EncKID) 212 if err != nil { 213 return "", nil, false, err 214 } 215 216 metadata := keybase1.TeambotKeyMetadata{ 217 Kid: deriveTeambotDHKey(seed).GetKID(), 218 Generation: keybase1.TeambotKeyGeneration(appKey.KeyGeneration), 219 Uid: botUID, 220 PukGeneration: keybase1.PerUserKeyGeneration(latestPUK.Gen), 221 Application: appKey.Application, 222 } 223 224 // Encrypting with a nil sender means we'll generate a random sender 225 // private key. 226 boxedSeed, err := recipientKey.EncryptToString(seed[:], nil) 227 if err != nil { 228 return "", nil, false, err 229 } 230 231 boxed := keybase1.TeambotKeyBoxed{ 232 Box: boxedSeed, 233 Metadata: metadata, 234 } 235 236 metadataJSON, err := json.Marshal(metadata) 237 if err != nil { 238 return "", nil, false, err 239 } 240 241 signingKey, err := team.SigningKey(mctx.Ctx()) 242 if err != nil { 243 return "", nil, false, err 244 } 245 sig, _, err = signingKey.SignToString(metadataJSON) 246 if err != nil { 247 return "", nil, false, err 248 } 249 250 role, err := team.MemberRole(mctx.Ctx(), upak.ToUserVersion()) 251 if err != nil { 252 return "", nil, false, err 253 } 254 return sig, &boxed, role.IsRestrictedBot(), nil 255 } 256 257 func (k *MemberKeyer) PurgeCacheAtGeneration(mctx libkb.MetaContext, teamID keybase1.TeamID, 258 botUID keybase1.UID, app keybase1.TeamApplication, generation keybase1.TeambotKeyGeneration) { 259 unlock := k.lockForTeamIDAndApp(mctx, teamID, app) 260 defer unlock() 261 cacheKey := k.cacheKey(teamID, botUID, app, generation) 262 k.lru.Remove(cacheKey) 263 } 264 265 func (k *MemberKeyer) PurgeCache(mctx libkb.MetaContext) { 266 k.Lock() 267 defer k.Unlock() 268 k.lru.Purge() 269 } 270 271 func (k *MemberKeyer) OnLogout(mctx libkb.MetaContext) error { 272 k.PurgeCache(mctx) 273 return nil 274 } 275 276 func (k *MemberKeyer) OnDbNuke(mctx libkb.MetaContext) error { 277 k.PurgeCache(mctx) 278 return nil 279 }