github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/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  }