1package teambot
2
3import (
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
18type MemberKeyer struct {
19	locktab *libkb.LockTable
20	sync.RWMutex
21	lru *lru.Cache
22}
23
24var _ libkb.TeambotMemberKeyer = (*MemberKeyer)(nil)
25
26func 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.
41func (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
61func (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
70func (k *MemberKeyer) lockKey(teamID keybase1.TeamID, app keybase1.TeamApplication) string {
71	return fmt.Sprintf("%s-%d", teamID.String(), app)
72}
73
74func (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.
82func (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
101func (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
156func (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
164func (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
183func (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
257func (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
265func (k *MemberKeyer) PurgeCache(mctx libkb.MetaContext) {
266	k.Lock()
267	defer k.Unlock()
268	k.lru.Purge()
269}
270
271func (k *MemberKeyer) OnLogout(mctx libkb.MetaContext) error {
272	k.PurgeCache(mctx)
273	return nil
274}
275
276func (k *MemberKeyer) OnDbNuke(mctx libkb.MetaContext) error {
277	k.PurgeCache(mctx)
278	return nil
279}
280