/
githubmirror
/
client
Обзор
Документация
Войти
/
githubmirror
/
client
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
go/ephemeral/lib.go
1 280 строк
42 KB
chrisnojima-zoom
Version 670 - clean2 (#29122)
08 июн 2026, 19:31
Не верифицирован
08 июн 2026, 19:31
1943197
Код
Авторство
О чём код?
package ephemeral import ( "errors" "fmt" "log" "sync" "time" lru "github.com/hashicorp/golang-lru" "github.com/keybase/client/go/libkb" "github.com/keybase/client/go/protocol/gregor1" "github.com/keybase/client/go/protocol/keybase1" "github.com/keybase/client/go/teambot" "github.com/keybase/client/go/teams" "github.com/keybase/clockwork" ) const SkipKeygenNilMerkleRoot = "Skipping key generation, unable to fetch merkle root" // Maximum number of retries for key generation const ( maxRetries = 5 LibCacheEntryLifetime = time.Minute * 5 lruSize = 200 ) type EKLib struct { // map teamID||ekType -> latest ekGeneration so we know which teamEK to // fetch from storage. teamEKGenCache *lru.Cache // map teamID||botUID||generation -> TeambotEkMetdata so non-bot members // can know which teambot eks have been published and store their metadata // after deriving the teamkey. teambotEKMetadataCache *lru.Cache sync.Mutex stateMu sync.Mutex locktab *libkb.LockTable // During testing we may want to stall background work to assert cache // state. clock clockwork.Clock backgroundCreationTestCh chan bool backgroundDeletionTestCh chan bool stopCh chan struct{} } var _ libkb.EKLib = (*EKLib)(nil) func NewEKLib(mctx libkb.MetaContext) *EKLib { nlru, err := lru.New(lruSize) if err != nil { // lru.New only panics if size <= 0 log.Panicf("Could not create lru cache: %v", err) } nlru2, err := lru.New(lruSize) if err != nil { // lru.New only panics if size <= 0 log.Panicf("Could not create lru cache: %v", err) } stopCh := make(chan struct{}) ekLib := &EKLib{ teamEKGenCache: nlru, teambotEKMetadataCache: nlru2, locktab: libkb.NewLockTable(), clock: clockwork.NewRealClock(), stopCh: stopCh, } if !mctx.G().GetEnv().GetDisableEKBackgroundKeygen() { go ekLib.backgroundKeygen(mctx.BackgroundWithLogTags(), stopCh) } return ekLib } func (e *EKLib) lockKey(teamID keybase1.TeamID) string { return teamID.String() } func (e *EKLib) Shutdown(mctx libkb.MetaContext) error { e.stateMu.Lock() defer e.stateMu.Unlock() if e.stopCh != nil { mctx.Debug("stopping background eklib loop") close(e.stopCh) e.stopCh = nil } e.purgeDeviceEKsIfOneshot(mctx) return nil } func (e *EKLib) backgroundKeygen(mctx libkb.MetaContext, stopCh <-chan struct{}) { // Don't fire immediately on startup select { case <-time.After(libkb.RandomJitter(time.Second)): case <-stopCh: return } mctx = mctx.WithLogTag("EKBKG") mctx.Debug("backgroundKeygen: starting up") keygenInterval := time.Hour lastRun := time.Now() runIfNeeded := func(force bool) { now := libkb.ForceWallClock(time.Now()) shouldRun := now.Sub(lastRun) >= keygenInterval mctx.Debug("backgroundKeygen: runIfNeeded: lastRun: %v, now: %v, shouldRun: %v, force: %v", lastRun, now, shouldRun, force) if force || shouldRun { if err := e.KeygenIfNeeded(mctx); err != nil { mctx.Debug("backgroundKeygen keygenIfNeeded error: %v", err) } lastRun = time.Now() } } // Fire off on startup runIfNeeded(true /* force */) ticker := libkb.NewBgTicker(keygenInterval) state := keybase1.MobileAppState_FOREGROUND // Run every hour but also check if enough wall clock time has elapsed when // we are in a BACKGROUNDACTIVE state. for { select { case <-ticker.C: runIfNeeded(false /* force */) case state = <-mctx.G().MobileAppState.NextUpdate(&state): if state == keybase1.MobileAppState_BACKGROUNDACTIVE { // Before running we pause briefly so we don't stampede for // resources with other background tasks. libkb.BgTicker // handles this internally, so we only need to throttle on // MobileAppState change. select { case <-time.After(libkb.RandomJitter(time.Second)): runIfNeeded(false /* force */) case <-stopCh: ticker.Stop() return } } case <-stopCh: ticker.Stop() return } } } func (e *EKLib) SetClock(clock clockwork.Clock) { e.clock = clock } func (e *EKLib) setBackgroundCreationTestCh(ch chan bool) { e.backgroundCreationTestCh = ch } func (e *EKLib) setBackgroundDeleteTestCh(ch chan bool) { e.backgroundDeletionTestCh = ch } func (e *EKLib) KeygenIfNeeded(mctx libkb.MetaContext) (err error) { defer mctx.Trace("KeygenIfNeeded", &err)() e.Lock() defer e.Unlock() var merkleRoot libkb.MerkleRoot // Always try to delete keys if we are logged in even if we get an error // when checking our PUK or fetching the merkleRoot. `keygenIfNeeded` this // also tries best effort to delete with errors, but try here in case we // error before reaching that call. defer func() { if err != nil { e.cleanupStaleUserAndDeviceEKsInBackground(mctx, merkleRoot) } }() for tries := range maxRetries { mctx.Debug("keygenIfNeeded attempt #%d: %v", tries, err) merkleRootPtr, err := mctx.G().GetMerkleClient().FetchRootFromServer(mctx, libkb.EphemeralKeyMerkleFreshness) if err != nil { mctx.Debug("Unable to fetch merkle root: %v, attempting keygenIfNeeded with nil root", err) merkleRootPtr = &libkb.MerkleRoot{} } if err = e.keygenIfNeededLocked(mctx, *merkleRootPtr, true /* shouldCleanup */); err == nil { return nil } if !libkb.IsEphemeralRetryableError(err) { return err } switch err.(type) { case libkb.LoginRequiredError: return err default: // retry } select { case <-mctx.Ctx().Done(): return mctx.Ctx().Err() case <-time.After(20 * time.Millisecond): } } return err } func (e *EKLib) keygenIfNeeded(mctx libkb.MetaContext, merkleRoot libkb.MerkleRoot, shouldCleanup bool) (err error) { defer mctx.Trace("keygenIfNeeded", &err)() e.Lock() defer e.Unlock() return e.keygenIfNeededLocked(mctx, merkleRoot, shouldCleanup) } func (e *EKLib) keygenIfNeededLocked(mctx libkb.MetaContext, merkleRoot libkb.MerkleRoot, shouldCleanup bool) (err error) { defer mctx.Trace("keygenIfNeededLocked", &err)() defer func() { if shouldCleanup { e.cleanupStaleUserAndDeviceEKsInBackground(mctx, merkleRoot) } }() // Abort. We only care about calling `cleanupStaleUserAndDeviceEKs. if merkleRoot.IsNil() { return errors.New(SkipKeygenNilMerkleRoot) } if deviceEKNeeded, err := e.newDeviceEKNeeded(mctx, merkleRoot); err != nil { return err } else if deviceEKNeeded { _, err = publishNewDeviceEK(mctx, merkleRoot) if err != nil { return err } } // newUserEKNeeded checks that the current userEKStatement is signed by our // latest PUK, is accessible to a deviceEK we have access to and that the // key is not expired. It's crucial that this verifies that the latest PUK // was used since we don't want to use a key signed by an old PUK for // encryption. if userEKNeeded, err := e.newUserEKNeeded(mctx, merkleRoot); err != nil { return err } else if userEKNeeded { _, err = publishNewUserEK(mctx, merkleRoot) if err != nil { return err } } return nil } func (e *EKLib) CleanupStaleUserAndDeviceEKs(mctx libkb.MetaContext) (err error) { merkleRootPtr, err := mctx.G().GetMerkleClient().FetchRootFromServer(mctx, libkb.EphemeralKeyMerkleFreshness) if err != nil { mctx.Debug("Unable to fetch merkle root: %v, attempting deviceEK deletion with nil root", err) merkleRootPtr = &libkb.MerkleRoot{} } return e.cleanupStaleUserAndDeviceEKs(mctx, *merkleRootPtr) } func (e *EKLib) cleanupStaleUserAndDeviceEKs(mctx libkb.MetaContext, merkleRoot libkb.MerkleRoot) (err error) { defer mctx.Trace("cleanupStaleUserAndDeviceEKs", &err)() epick := libkb.FirstErrorPicker{} deviceEKStorage := mctx.G().GetDeviceEKStorage() _, err = deviceEKStorage.DeleteExpired(mctx, merkleRoot) epick.Push(err) // Abort. We only cared about deleting expired deviceEKs. if merkleRoot.IsNil() { return fmt.Errorf("skipping userEK deletion, unable to fetch merkle root") } userEKBoxStorage := mctx.G().GetUserEKBoxStorage() _, err = userEKBoxStorage.DeleteExpired(mctx, merkleRoot) epick.Push(err) return epick.Error() } func (e *EKLib) cleanupStaleUserAndDeviceEKsInBackground(mctx libkb.MetaContext, merkleRoot libkb.MerkleRoot) { go func() { mctx = mctx.BackgroundWithLogTags() if err := e.cleanupStaleUserAndDeviceEKs(mctx, merkleRoot); err != nil { mctx.Debug("Unable to cleanupStaleUserAndDeviceEKsInBackground: %v", err) } if e.backgroundDeletionTestCh != nil { e.backgroundDeletionTestCh <- true } }() } func (e *EKLib) NewDeviceEKNeeded(mctx libkb.MetaContext) (needed bool, err error) { e.Lock() defer e.Unlock() merkleRootPtr, err := mctx.G().GetMerkleClient().FetchRootFromServer(mctx, libkb.EphemeralKeyMerkleFreshness) if err != nil { return false, err } return e.newDeviceEKNeeded(mctx, *merkleRootPtr) } func (e *EKLib) newDeviceEKNeeded(mctx libkb.MetaContext, merkleRoot libkb.MerkleRoot) (needed bool, err error) { defer mctx.Trace("newDeviceEKNeeded", &err)() defer func() { if _, ok := err.(libkb.UnboxError); ok { mctx.Debug("newDeviceEKNeeded: unable to fetch latest: %v, creating new deviceEK", err) needed = true err = nil } }() s := mctx.G().GetDeviceEKStorage() maxGeneration, err := s.MaxGeneration(mctx, true) if err != nil { return false, err } else if maxGeneration < 0 { return true, nil } ek, err := s.Get(mctx, maxGeneration) if err != nil { return false, err } // Ok we can access the ek, check lifetime. mctx.Debug("nextDeviceEKNeeded at: %v", nextKeygenTime(ek.Metadata.Ctime.Time())) return keygenNeeded(ek.Metadata.Ctime.Time(), merkleRoot), nil } func (e *EKLib) NewUserEKNeeded(mctx libkb.MetaContext) (needed bool, err error) { e.Lock() defer e.Unlock() merkleRootPtr, err := mctx.G().GetMerkleClient().FetchRootFromServer(mctx, libkb.EphemeralKeyMerkleFreshness) if err != nil { return false, err } return e.newUserEKNeeded(mctx, *merkleRootPtr) } func (e *EKLib) newUserEKNeeded(mctx libkb.MetaContext, merkleRoot libkb.MerkleRoot) (needed bool, err error) { defer mctx.Trace("newUserEKNeeded", &err)() // Let's see what the latest server statement is. This verifies that the // latest statement was signed by the latest PUK and otherwise fails with // wrongKID set. statement, _, wrongKID, err := fetchUserEKStatement(mctx, mctx.G().Env.GetUID()) if wrongKID { return true, nil } else if err != nil { return false, err } if statement == nil { return true, nil } // Can we access this generation? If not, let's regenerate. s := mctx.G().GetUserEKBoxStorage() ek, err := s.Get(mctx, statement.CurrentUserEkMetadata.Generation, nil) if err != nil { switch err.(type) { case EphemeralKeyError: mctx.Debug(err.Error()) return true, nil default: return false, err } } // Ok we can access the ek, check lifetime. mctx.Debug("nextUserEKNeeded at: %v", nextKeygenTime(ek.Metadata.Ctime.Time())) return keygenNeeded(ek.Metadata.Ctime.Time(), merkleRoot), nil } func (e *EKLib) NewTeamEKNeeded(mctx libkb.MetaContext, teamID keybase1.TeamID) (needed bool, err error) { lock := e.locktab.AcquireOnName(mctx.Ctx(), mctx.G(), e.lockKey(teamID)) defer lock.Release(mctx.Ctx()) merkleRootPtr, err := mctx.G().GetMerkleClient().FetchRootFromServer(mctx, libkb.EphemeralKeyMerkleFreshness) if err != nil { return false, err } needed, _, _, err = e.newTeamEKNeeded(mctx, teamID, *merkleRootPtr) return needed, err } func (e *EKLib) newTeamEKNeeded(mctx libkb.MetaContext, teamID keybase1.TeamID, merkleRoot libkb.MerkleRoot, ) (needed, backgroundGenPossible bool, latestGeneration keybase1.EkGeneration, err error) { defer mctx.Trace("newTeamEKNeeded", &err)() // Let's see what the latest server statement is. This verifies that the // latest statement was signed by the latest PTK and otherwise fails with // wrongKID set. statement, latestGeneration, wrongKID, err := fetchTeamEKStatement(mctx, teamID) if wrongKID { return true, false, latestGeneration, nil } else if err != nil { return false, false, latestGeneration, err } // Let's see what the latest server statement is. // No statement, so we need a teamEK if statement == nil { return true, false, latestGeneration, nil } // Can we access this generation? If not, let's regenerate. s := mctx.G().GetTeamEKBoxStorage() ek, err := s.Get(mctx, teamID, statement.CurrentTeamEkMetadata.Generation, nil) if err != nil { switch err.(type) { case EphemeralKeyError: mctx.Debug(err.Error()) return true, false, latestGeneration, nil default: return false, false, latestGeneration, err } } // Ok we can access the ek, check lifetime. ctime := ek.Ctime().Time() mctx.Debug("nextTeamEKNeeded at: %v", nextKeygenTime(ctime)) if backgroundKeygenPossible(ctime, merkleRoot) { return false, true, latestGeneration, nil } return keygenNeeded(ctime, merkleRoot), false, latestGeneration, nil } type teamEKGenCacheEntry struct { Generation keybase1.EkGeneration Ctime keybase1.Time CreationInProgress bool } func (e *EKLib) newCacheEntry(generation keybase1.EkGeneration, creationInProgress bool) *teamEKGenCacheEntry { return &teamEKGenCacheEntry{ Generation: generation, Ctime: keybase1.ToTime(e.clock.Now()), CreationInProgress: creationInProgress, } } func (e *EKLib) cacheKey(teamID keybase1.TeamID, typ keybase1.TeamEphemeralKeyType) string { return fmt.Sprintf("%s-%s", teamID, typ) } func (e *EKLib) teambotCacheKey(teamID keybase1.TeamID, botUID keybase1.UID, generation keybase1.EkGeneration) string { return fmt.Sprintf("%s-%s-%d", teamID, botUID, generation) } func (e *EKLib) isEntryExpired(val any) (*teamEKGenCacheEntry, bool) { cacheEntry, ok := val.(*teamEKGenCacheEntry) if !ok || cacheEntry == nil { return nil, false } return cacheEntry, e.clock.Now().Sub(cacheEntry.Ctime.Time()) >= LibCacheEntryLifetime } func (e *EKLib) getStorageForType(mctx libkb.MetaContext, typ keybase1.TeamEphemeralKeyType) (libkb.TeamEKBoxStorage, error) { switch typ { case keybase1.TeamEphemeralKeyType_TEAM: return mctx.G().GetTeamEKBoxStorage(), nil case keybase1.TeamEphemeralKeyType_TEAMBOT: return mctx.G().GetTeambotEKBoxStorage(), nil default: return nil, fmt.Errorf("Unknown key type %v", typ) } } func (e *EKLib) PurgeTeamEKCachesForTeamID(mctx libkb.MetaContext, teamID keybase1.TeamID) { e.purgeCachesForTeamIDAndType(mctx, teamID, keybase1.TeamEphemeralKeyType_TEAM) } func (e *EKLib) PurgeTeambotEKCachesForTeamID(mctx libkb.MetaContext, teamID keybase1.TeamID) { e.purgeCachesForTeamIDAndType(mctx, teamID, keybase1.TeamEphemeralKeyType_TEAMBOT) } func (e *EKLib) purgeCachesForTeamIDAndType(mctx libkb.MetaContext, teamID keybase1.TeamID, typ keybase1.TeamEphemeralKeyType) { mctx.Debug("purgeCachesForTeamIDAndType: teamID: %v, typ: %v", teamID, typ) e.teamEKGenCache.Remove(e.cacheKey(teamID, typ)) storage, err := e.getStorageForType(mctx, typ) if err != nil { mctx.Debug("unable to purgeCachesForTeamIDAndType: %v", err) return } if err := storage.PurgeCacheForTeamID(mctx, teamID); err != nil { mctx.Debug("unable to purgeCachesForTeamIDAndType: %v", err) } } func (e *EKLib) PurgeTeamEKCachesForTeamIDAndGeneration(mctx libkb.MetaContext, teamID keybase1.TeamID, generation keybase1.EkGeneration) { e.purgeCachesForTeamIDAndTypeByGeneration(mctx, teamID, generation, keybase1.TeamEphemeralKeyType_TEAM) } func (e *EKLib) PurgeTeambotEKCachesForTeamIDAndGeneration(mctx libkb.MetaContext, teamID keybase1.TeamID, generation keybase1.EkGeneration) { e.purgeCachesForTeamIDAndTypeByGeneration(mctx, teamID, generation, keybase1.TeamEphemeralKeyType_TEAMBOT) } func (e *EKLib) PurgeAllTeambotMetadataCaches(mctx libkb.MetaContext) { mctx.Debug("PurgeAllTeambotMetadataCaches") e.teambotEKMetadataCache.Purge() } func (e *EKLib) PurgeTeambotMetadataCache(mctx libkb.MetaContext, teamID keybase1.TeamID, botUID keybase1.UID, generation keybase1.EkGeneration, ) { mctx.Debug("PurgeTeambotMetadataCache: teamID: %v, botUID: %v generation: %v", teamID, botUID, generation) cacheKey := e.teambotCacheKey(teamID, botUID, generation) e.teambotEKMetadataCache.Remove(cacheKey) } func (e *EKLib) purgeCachesForTeamIDAndTypeByGeneration(mctx libkb.MetaContext, teamID keybase1.TeamID, generation keybase1.EkGeneration, typ keybase1.TeamEphemeralKeyType, ) { mctx.Debug("purgeCachesForTeamIDAndTypeByGeneration: teamID: %v, typ: %v generation: %v", teamID, typ, generation) cacheKey := e.cacheKey(teamID, typ) val, ok := e.teamEKGenCache.Get(cacheKey) if ok { if cacheEntry, _ := e.isEntryExpired(val); cacheEntry != nil && cacheEntry.Generation != generation { e.teamEKGenCache.Remove(cacheKey) } } storage, err := e.getStorageForType(mctx, typ) if err != nil { mctx.Debug("unable to purgeCachesForTeamIDAndType: %v", err) return } if err := storage.Delete(mctx, teamID, generation); err != nil { mctx.Debug("unable to purgeCachesForTeamIDAndTypeByGeneration: %v", err) } } func (e *EKLib) GetOrCreateLatestTeamEK(mctx libkb.MetaContext, teamID keybase1.TeamID) ( ek keybase1.TeamEphemeralKey, created bool, err error, ) { mctx = mctx.WithLogTag("GOCTEK") err = teamEKRetryWrapper(mctx, func() error { lock := e.locktab.AcquireOnName(mctx.Ctx(), mctx.G(), e.lockKey(teamID)) defer lock.Release(mctx.Ctx()) ek, created, err = e.getOrCreateLatestTeamEKLocked(mctx, teamID, nil) if err != nil { return err } return err }) if err != nil { return ek, false, err } // sanity check key type typ, err := ek.KeyType() if err != nil { return ek, false, err } else if !typ.IsTeam() { return ek, false, NewIncorrectTeamEphemeralKeyTypeError(typ, keybase1.TeamEphemeralKeyType_TEAM) } return ek, created, err } func (e *EKLib) getOrCreateLatestTeamEKLocked(mctx libkb.MetaContext, teamID keybase1.TeamID, forceCreateGen *keybase1.EkGeneration) ( ek keybase1.TeamEphemeralKey, created bool, err error, ) { defer mctx.Trace("getOrCreateLatestTeamEKLocked", &err)() teamEKBoxStorage := mctx.G().GetTeamEKBoxStorage() // Check if we have a cached latest generation cacheKey := e.cacheKey(teamID, keybase1.TeamEphemeralKeyType_TEAM) val, ok := e.teamEKGenCache.Get(cacheKey) if ok { if cacheEntry, expired := e.isEntryExpired(val); !expired || cacheEntry.CreationInProgress { ek, err = teamEKBoxStorage.Get(mctx, teamID, cacheEntry.Generation, nil) if err == nil { return ek, false, nil } mctx.Debug("unable to get teamEK, attempting regen: %v", err) // kill our cached entry and possibly re-generate below e.teamEKGenCache.Remove(cacheKey) } } merkleRootPtr, err := mctx.G().GetMerkleClient().FetchRootFromServer(mctx, libkb.EphemeralKeyMerkleFreshness) if err != nil { return ek, false, err } merkleRoot := *merkleRootPtr defer func() { e.cleanupStaleUserAndDeviceEKsInBackground(mctx, merkleRoot) }() defer func() { if _, err := teamEKBoxStorage.DeleteExpired(mctx, teamID, merkleRoot); err != nil { mctx.Debug("Unable to DeleteExpired: %v", err) } }() // First publish new device or userEKs if we need to. We pass shouldCleanup // = false so we can run deletion in the background ourselves and not block // this call. if err = e.keygenIfNeeded(mctx, merkleRoot, false /* shouldCleanup */); err != nil { return ek, false, err } // newTeamEKNeeded checks that the current teamEKStatement is signed by the // latest PTK, is accessible to a userEK we have access to and that the key // is not expired. It's crucial that this verifies that the latest PTK was // used since we don't want to use a key signed by an old PTK for // encryption. teamEKNeeded, backgroundGenPossible, latestGeneration, err := e.newTeamEKNeeded(mctx, teamID, merkleRoot) switch { case err != nil: return ek, false, err case backgroundGenPossible: // Our current teamEK is *almost* expired but otherwise valid, so let's // create the new teamEK in the background. For large teams and an // empty UPAK cache it can be expensive to do this calculation and it's // unfortunate to block message sending while we otherwise have access // to a working teamEK. go func() { mctx = mctx.BackgroundWithLogTags() if e.backgroundCreationTestCh != nil { <-e.backgroundCreationTestCh } publishedMetadata, err := publishNewTeamEK(mctx, teamID, merkleRoot, nil) // Grab the lock once we finish publishing so we do don't block lock := e.locktab.AcquireOnName(mctx.Ctx(), mctx.G(), e.lockKey(teamID)) defer lock.Release(mctx.Ctx()) created := false if err != nil { // Let's just clear the cache and try again later mctx.Debug("Unable to getOrCreateLatestTeamEKLocked in the background: %v", err) e.teamEKGenCache.Remove(cacheKey) } else { e.teamEKGenCache.Add(cacheKey, e.newCacheEntry( publishedMetadata.Generation, false)) created = true } if e.backgroundCreationTestCh != nil { e.backgroundCreationTestCh <- created } }() case teamEKNeeded: publishedMetadata, err := publishNewTeamEK(mctx, teamID, merkleRoot, nil) if err != nil { return ek, false, err } latestGeneration = publishedMetadata.Generation case forceCreateGen != nil && latestGeneration == *forceCreateGen: // Teambot creation can request a new teamEK be force published. We // only want to do this once so we make sure no on else has published // before us and incremented the generation. publishedMetadata, err := publishNewTeamEK(mctx, teamID, merkleRoot, forceCreateGen) if err != nil { return ek, false, err } latestGeneration = publishedMetadata.Generation } ek, err = teamEKBoxStorage.Get(mctx, teamID, latestGeneration, nil) if err != nil { return ek, false, err } // Cache the latest generation and signal future callers if we are trying // to create the new key in the background. e.teamEKGenCache.Add(cacheKey, e.newCacheEntry(latestGeneration, backgroundGenPossible)) return ek, teamEKNeeded, nil } // GetTeamEK fetches the TeamEK for the given `generation`. If this fails and the // `generation` is also the current maxGeneration, create a new teamEK. func (e *EKLib) GetTeamEK(mctx libkb.MetaContext, teamID keybase1.TeamID, generation keybase1.EkGeneration, contentCtime *gregor1.Time, ) (ek keybase1.TeamEphemeralKey, err error) { mctx = mctx.WithLogTag("GTEK") defer mctx.Trace("GetTeamEK", &err)() teamEKBoxStorage := mctx.G().GetTeamEKBoxStorage() ek, err = teamEKBoxStorage.Get(mctx, teamID, generation, contentCtime) if err != nil { if _, ok := err.(EphemeralKeyError); ok { mctx.Debug(err.Error()) // If we are unable to get the current max generation, try to kick // off creation of a new key. go func(mctx libkb.MetaContext) { maxGeneration, err := teamEKBoxStorage.MaxGeneration(mctx, teamID, true) if err != nil { mctx.Debug("Unable to get MaxGeneration: %v", err) return } if generation == maxGeneration { _, created, cerr := e.GetOrCreateLatestTeamEK(mctx, teamID) if cerr != nil { mctx.Debug("Unable to GetOrCreateLatestTeamEK: %v", cerr) } if e.backgroundCreationTestCh != nil { e.backgroundCreationTestCh <- created } } }(libkb.NewMetaContextBackground(mctx.G())) } return ek, err } // sanity check key type typ, err := ek.KeyType() if err != nil { return ek, err } else if !typ.IsTeam() { return ek, NewIncorrectTeamEphemeralKeyTypeError(typ, keybase1.TeamEphemeralKeyType_TEAM) } return ek, err } // GetOrCreateLatestTeambotEK handles two separate cases based on the `botUID` // parameter. If `botUID == currentUID`, we are a bot member and thus can // *only* get the latest known key, we do not have the ability to create new // ones. Since bot members do not have access to the per-team-key, they must // depend on team members who do to derive and publish a new key. func (e *EKLib) GetOrCreateLatestTeambotEK(mctx libkb.MetaContext, teamID keybase1.TeamID, gBotUID gregor1.UID) ( ek keybase1.TeamEphemeralKey, created bool, err error, ) { mctx = mctx.WithLogTag("GOCLTBEK") botUID, err := keybase1.UIDFromSlice(gBotUID.Bytes()) if err != nil { return ek, false, err } // We are the bot, try to access our latest key if teambot.CurrentUserIsBot(mctx, &gBotUID) { created = false ek, err = e.getLatestTeambotEK(mctx, teamID, botUID) switch err := err.(type) { case nil: case EphemeralKeyError: // Make sure we have a valid deviceEK, we may be in a oneshot // configuration and be fighting with another bot for one. if err2 := e.KeygenIfNeeded(mctx); err2 != nil { return ek, false, err2 } // Ping team members to generate the latest key for us if err2 := teambot.NotifyTeambotEKNeeded(mctx, teamID, 0); err2 != nil { mctx.Debug("Unable to NotifyTeambotEKNeeded %v", err2) } // See if we should downgrade this to a transient error. Since // bot members get a key when added to the team this should // only happen in a tight race before the key is created or if // the TeamEK has been purged and we don't have a new one. if err.AllowTransient() { err = newTransientEphemeralKeyError(err) } return ek, false, err default: return ek, false, err } } else { // we are a team member who needs the latest bot key, get or create that puppy. err = teamEKRetryWrapper(mctx, func() error { lock := e.locktab.AcquireOnName(mctx.Ctx(), mctx.G(), e.lockKey(teamID)) defer lock.Release(mctx.Ctx()) ek, created, err = e.getOrCreateLatestTeambotEKLocked(mctx, teamID, botUID, nil) return err }) if err != nil { return ek, false, err } } // sanity check key type typ, err := ek.KeyType() if err != nil { return ek, false, err } else if !typ.IsTeambot() { return ek, false, NewIncorrectTeamEphemeralKeyTypeError(typ, keybase1.TeamEphemeralKeyType_TEAMBOT) } return ek, created, err } func (e *EKLib) ForceCreateTeambotEK(mctx libkb.MetaContext, teamID keybase1.TeamID, gBotUID gregor1.UID, forceCreateGen keybase1.EkGeneration, ) (ek keybase1.TeamEphemeralKey, created bool, err error) { mctx = mctx.WithLogTag("FCTEK") botUID, err := keybase1.UIDFromSlice(gBotUID.Bytes()) if err != nil { return ek, false, err } // We are the bot, try to access our latest key if teambot.CurrentUserIsBot(mctx, &gBotUID) { return ek, false, fmt.Errorf("Cannot force create as a bot member") } err = teamEKRetryWrapper(mctx, func() error { lock := e.locktab.AcquireOnName(mctx.Ctx(), mctx.G(), e.lockKey(teamID)) defer lock.Release(mctx.Ctx()) ek, created, err = e.getOrCreateLatestTeambotEKLocked(mctx, teamID, botUID, &forceCreateGen) return err }) if err != nil { return ek, false, err } // sanity check key type typ, err := ek.KeyType() if err != nil { return ek, false, err } else if !typ.IsTeambot() { return ek, false, NewIncorrectTeamEphemeralKeyTypeError(typ, keybase1.TeamEphemeralKeyType_TEAMBOT) } return ek, created, err } func (e *EKLib) getOrCreateLatestTeambotEKLocked(mctx libkb.MetaContext, teamID keybase1.TeamID, botUID keybase1.UID, forceCreateGen *keybase1.EkGeneration, ) (ek keybase1.TeamEphemeralKey, created bool, err error) { defer mctx.Trace("getOrCreateLatestTeambotEKLocked", &err)() // first check if we have the teamEK cached, in which case we can just // derive the teambotEK and return that. cacheKey := e.cacheKey(teamID, keybase1.TeamEphemeralKeyType_TEAM) val, ok := e.teamEKGenCache.Get(cacheKey) if ok { if cacheEntry, expired := e.isEntryExpired(val); !expired || cacheEntry.CreationInProgress { teamEK, err := mctx.G().GetTeamEKBoxStorage().Get(mctx, teamID, cacheEntry.Generation, nil) if err == nil { return e.deriveAndMaybePublishTeambotEK(mctx, teamID, teamEK, botUID) } mctx.Debug("unable to get teamEK, attempting regen: %v", err) // kill our cached entry and possibly re-generate below e.teamEKGenCache.Remove(cacheKey) } } // get the latest teamEK to derive the latest teambotEK teamEK, _, err := e.getOrCreateLatestTeamEKLocked(mctx, teamID, forceCreateGen) if err != nil { return ek, false, err } return e.deriveAndMaybePublishTeambotEK(mctx, teamID, teamEK, botUID) } func (e *EKLib) deriveAndMaybePublishTeambotEK(mctx libkb.MetaContext, teamID keybase1.TeamID, teamEK keybase1.TeamEphemeralKey, botUID keybase1.UID, ) (ek keybase1.TeamEphemeralKey, created bool, err error) { // sanity check the teamEK is of type TEAM typ, err := teamEK.KeyType() if err != nil { return ek, false, err } else if !typ.IsTeam() { return ek, false, NewIncorrectTeamEphemeralKeyTypeError(typ, keybase1.TeamEphemeralKeyType_TEAM) } seed := deriveTeambotEKFromTeamEK(mctx, teamEK.Team(), botUID) // Check our teambotEK cache and see if we should attempt to publish the // our derived key or not. cacheKey := e.teambotCacheKey(teamID, botUID, teamEK.Generation()) val, ok := e.teambotEKMetadataCache.Get(cacheKey) if ok { metadata, ok := val.(keybase1.TeambotEkMetadata) if ok { ek = keybase1.NewTeamEphemeralKeyWithTeambot(keybase1.TeambotEk{ Seed: keybase1.Bytes32(seed), Metadata: metadata, }) return ek, false, nil } } merkleRootPtr, err := mctx.G().GetMerkleClient().FetchRootFromServer(mctx, libkb.EphemeralKeyMerkleFreshness) if err != nil { return ek, false, err } merkleRoot := *merkleRootPtr metadata := keybase1.TeambotEkMetadata{ Kid: seed.DeriveDHKey().GetKID(), Generation: teamEK.Team().Metadata.Generation, Uid: botUID, HashMeta: merkleRoot.HashMeta(), // The ctime is derivable from the hash meta, by fetching the hashed // root from the server, but including it saves readers a potential // extra round trip. Ctime: keybase1.TimeFromSeconds(merkleRoot.Ctime()), } team, err := teams.Load(mctx.Ctx(), mctx.G(), keybase1.LoadTeamArg{ ID: teamID, }) if err != nil { return ek, false, err } upak, _, err := mctx.G().GetUPAKLoader().LoadV2( libkb.NewLoadUserArgWithMetaContext(mctx).WithUID(botUID)) if err != nil { return ek, false, err } role, err := team.MemberRole(mctx.Ctx(), upak.ToUserVersion()) if err != nil { return ek, false, err } // If the bot is not a restricted bot member don't try to publish the key // for them. This can happen when decrypting past content after the bot is // removed from the team. if role.IsRestrictedBot() { sig, box, err := prepareNewTeambotEK(mctx, team, botUID, seed, &metadata, merkleRoot) if err != nil { return ek, false, err } if box != nil { if err = postNewTeambotEK(mctx, team.ID, sig, box.Box); err != nil { return ek, false, err } } } e.teambotEKMetadataCache.Add(cacheKey, metadata) ek = keybase1.NewTeamEphemeralKeyWithTeambot(keybase1.TeambotEk{ Seed: keybase1.Bytes32(seed), Metadata: metadata, }) return ek, role.IsRestrictedBot(), nil } func (e *EKLib) getLatestTeambotEK(mctx libkb.MetaContext, teamID keybase1.TeamID, botUID keybase1.UID, ) (ek keybase1.TeamEphemeralKey, err error) { defer mctx.Trace("getLatestTeambotEK", &err)() lock := e.locktab.AcquireOnName(mctx.Ctx(), mctx.G(), e.lockKey(teamID)) defer lock.Release(mctx.Ctx()) storage := mctx.G().GetTeambotEKBoxStorage() // Check if we have a cached latest generation cacheKey := e.cacheKey(teamID, keybase1.TeamEphemeralKeyType_TEAMBOT) val, ok := e.teamEKGenCache.Get(cacheKey) if ok { if cacheEntry, expired := e.isEntryExpired(val); !expired { return storage.Get(mctx, teamID, cacheEntry.Generation, nil) } // kill our cached entry and possibly re-generate below e.teamEKGenCache.Remove(cacheKey) } merkleRootPtr, err := mctx.G().GetMerkleClient().FetchRootFromServer(mctx, libkb.EphemeralKeyMerkleFreshness) if err != nil { return ek, err } merkleRoot := *merkleRootPtr defer func() { if _, err := storage.DeleteExpired(mctx, teamID, merkleRoot); err != nil { mctx.Debug("Unable to DeleteExpired: %v", err) } }() // Let's see what the latest teambot ek is. This verifies that the metadata // was signed by the latest PTK and otherwise fails with wrongKID set. metadata, wrongKID, err := fetchLatestTeambotEK(mctx, teamID) switch { case metadata == nil: err = newEKMissingBoxErr(mctx, TeambotEKKind, -1) return ek, err case wrongKID: now := keybase1.ToTime(e.clock.Now()) permitted, ctime, err := teambot.TeambotEKWrongKIDPermitted(mctx, teamID, botUID, metadata.Generation, now) if err != nil { return ek, err } mctx.Debug("getLatestTeambotEK: wrongKID set, permitted: %v, ctime: %v", permitted, ctime) if !permitted { return ek, newTeambotEKWrongKIDErr(mctx, ctime, now) } // Ping other team members to create the new key for us. if err = teambot.NotifyTeambotEKNeeded(mctx, teamID, 0); err != nil { // Charge forward here, we'll try again next time we fetch this // key. mctx.Debug("Unable to NotifyTeambotEKNeeded %v", err) } case err != nil: return ek, err } ek, err = storage.Get(mctx, teamID, metadata.Generation, nil) if err != nil { return ek, err } e.teamEKGenCache.Add(cacheKey, e.newCacheEntry(ek.Generation(), false)) return ek, nil } // GetTeambotEK fetches the TeambotEK for the given `generation`. If `gBotUID` // is the current UID we fetch the boxed teambotEK if it exists. Otherwise we // derived the key from the teamEK at the given `generation`. func (e *EKLib) GetTeambotEK(mctx libkb.MetaContext, teamID keybase1.TeamID, gBotUID gregor1.UID, generation keybase1.EkGeneration, contentCtime *gregor1.Time, ) (ek keybase1.TeamEphemeralKey, err error) { mctx = mctx.WithLogTag("GTBEK") defer mctx.Trace("GetTeambotEK", &err)() botUID, err := keybase1.UIDFromSlice(gBotUID.Bytes()) if err != nil { return ek, err } // We are the bot, try to access the key if teambot.CurrentUserIsBot(mctx, &gBotUID) { ek, err = mctx.G().GetTeambotEKBoxStorage().Get(mctx, teamID, generation, contentCtime) if err != nil { if _, ok := err.(EphemeralKeyError); ok { // If we don't have access to the max generation, request // access to this key. We may not have access to earlier keys // and don't want to spam out requests for new ones. maxGeneration, err2 := mctx.G().GetTeambotEKBoxStorage().MaxGeneration(mctx, teamID, true) if err2 != nil { mctx.Debug("Unable to get MaxGeneration: %v", err) return ek, err } if generation == maxGeneration { // Ping team members to generate the latest key for us if err2 = teambot.NotifyTeambotEKNeeded(mctx, teamID, 0); err2 != nil { mctx.Debug("Unable to NotifyTeambotEKNeeded %v", err2) } } // NOTE we don't downgrade this error to transient since the // bot may have added a device after the EK was generated, and // will never get access to it. } return ek, err } } else { teamEK, err := e.GetTeamEK(mctx, teamID, generation, contentCtime) if err != nil { return ek, err } ek, _, err = e.deriveAndMaybePublishTeambotEK(mctx, teamID, teamEK, botUID) if err != nil { return ek, err } } // Sanity check key type typ, err := ek.KeyType() if err != nil { return ek, err } else if !typ.IsTeambot() { return ek, NewIncorrectTeamEphemeralKeyTypeError(typ, keybase1.TeamEphemeralKeyType_TEAMBOT) } return ek, err } func (e *EKLib) NewEphemeralSeed() (seed keybase1.Bytes32, err error) { return makeNewRandomSeed() } func (e *EKLib) DeriveDeviceDHKey(seed keybase1.Bytes32) *libkb.NaclDHKeyPair { deviceEKSeed := DeviceEKSeed(seed) return deviceEKSeed.DeriveDHKey() } func (e *EKLib) SignedDeviceEKStatementFromSeed(mctx libkb.MetaContext, generation keybase1.EkGeneration, seed keybase1.Bytes32, signingKey libkb.GenericKey, ) (statement keybase1.DeviceEkStatement, signedStatement string, err error) { defer mctx.Trace("SignedDeviceEKStatementFromSeed", &err)() merkleRootPtr, err := mctx.G().GetMerkleClient().FetchRootFromServer(mctx, libkb.EphemeralKeyMerkleFreshness) if err != nil { return statement, signedStatement, err } dhKeypair := e.DeriveDeviceDHKey(seed) return signDeviceEKStatement(generation, dhKeypair, signingKey, *merkleRootPtr) } // For device provisioning func (e *EKLib) BoxLatestUserEK(mctx libkb.MetaContext, receiverKey libkb.NaclDHKeyPair, deviceEKGeneration keybase1.EkGeneration, ) (userEKBox *keybase1.UserEkBoxed, err error) { defer mctx.Trace("BoxLatestUserEK", &err)() // Let's make sure we are up to date with keys first and we have the latest userEK cached. if err = e.KeygenIfNeeded(mctx); err != nil { return nil, err } userEKBoxStorage := mctx.G().GetUserEKBoxStorage() maxGeneration, err := userEKBoxStorage.MaxGeneration(mctx, false) if err != nil { return nil, err } if maxGeneration < 0 { mctx.Debug("No userEK found") return nil, nil } userEK, err := userEKBoxStorage.Get(mctx, maxGeneration, nil) if err != nil { return nil, err } box, err := receiverKey.EncryptToString(userEK.Seed[:], nil) if err != nil { return nil, err } return &keybase1.UserEkBoxed{ Box: box, DeviceEkGeneration: deviceEKGeneration, Metadata: userEK.Metadata, }, nil } func (e *EKLib) PrepareNewUserEK(mctx libkb.MetaContext, merkleRoot libkb.MerkleRoot, pukSeed libkb.PerUserKeySeed) (sig string, boxes []keybase1.UserEkBoxMetadata, newMetadata keybase1.UserEkMetadata, myBox *keybase1.UserEkBoxed, err error, ) { signingKey, err := pukSeed.DeriveSigningKey() if err != nil { return "", nil, newMetadata, nil, err } return prepareNewUserEK(mctx, merkleRoot, signingKey) } func (e *EKLib) BoxLatestTeamEK(mctx libkb.MetaContext, teamID keybase1.TeamID, recipients []keybase1.UID) (teamEKBoxes *[]keybase1.TeamEkBoxMetadata, err error) { defer mctx.Trace("BoxLatestTeamEK", &err)() // If we need a new teamEK let's just create it when needed, the new // members will be part of the team and will have access to it via the // normal mechanisms. if teamEKNeeded, err := e.NewTeamEKNeeded(mctx, teamID); err != nil { return nil, err } else if teamEKNeeded { return nil, nil } lock := e.locktab.AcquireOnName(mctx.Ctx(), mctx.G(), e.lockKey(teamID)) defer lock.Release(mctx.Ctx()) merkleRootPtr, err := mctx.G().GetMerkleClient().FetchRootFromServer(mctx, libkb.EphemeralKeyMerkleFreshness) if err != nil { return nil, err } statementMap, err := fetchUserEKStatements(mctx, recipients) if err != nil { return nil, err } usersMetadata, err := activeUserEKMetadata(mctx, statementMap, *merkleRootPtr) if err != nil { return nil, err } teamEKBoxStorage := mctx.G().GetTeamEKBoxStorage() maxGeneration, err := teamEKBoxStorage.MaxGeneration(mctx, teamID, false) if err != nil { return nil, err } ek, err := teamEKBoxStorage.Get(mctx, teamID, maxGeneration, nil) if err != nil { return nil, err } typ, err := ek.KeyType() if err != nil { return nil, err } else if !typ.IsTeam() { return nil, NewIncorrectTeamEphemeralKeyTypeError(typ, keybase1.TeamEphemeralKeyType_TEAM) } teamEK := ek.Team() boxes, _, err := boxTeamEKForUsers(mctx, usersMetadata, teamEK) return boxes, err } func (e *EKLib) PrepareNewTeamEK(mctx libkb.MetaContext, teamID keybase1.TeamID, signingKey libkb.NaclSigningKeyPair, recipients []keybase1.UID, ) (sig string, boxes *[]keybase1.TeamEkBoxMetadata, newMetadata keybase1.TeamEkMetadata, myBox *keybase1.TeamEkBoxed, err error) { // If we need a new teamEK let's just create it when needed, the new // members will be part of the team and will have access to it via the // normal mechanisms. if teamEKNeeded, err := e.NewTeamEKNeeded(mctx, teamID); err != nil { return "", nil, newMetadata, nil, err } else if teamEKNeeded { return "", nil, newMetadata, nil, nil } lock := e.locktab.AcquireOnName(mctx.Ctx(), mctx.G(), e.lockKey(teamID)) defer lock.Release(mctx.Ctx()) merkleRootPtr, err := mctx.G().GetMerkleClient().FetchRootFromServer(mctx, libkb.EphemeralKeyMerkleFreshness) if err != nil { return "", nil, newMetadata, nil, err } merkleRoot := *merkleRootPtr statementMap, err := fetchUserEKStatements(mctx, recipients) if err != nil { return "", nil, newMetadata, nil, err } usersMetadata, err := activeUserEKMetadata(mctx, statementMap, merkleRoot) if err != nil { return "", nil, newMetadata, nil, err } return prepareNewTeamEK(mctx, teamID, signingKey, usersMetadata, merkleRoot) } func (e *EKLib) ClearCaches(mctx libkb.MetaContext) { defer mctx.Trace("EKLib.ClearCaches", nil)() mctx.Debug("| EKLib.ClearCaches teamEKGenCache") e.teamEKGenCache.Purge() e.teambotEKMetadataCache.Purge() mctx.Debug("| EKLib.ClearCaches deviceEKStorage") if s := mctx.G().GetDeviceEKStorage(); s != nil { s.ClearCache() } mctx.Debug("| EKLib.ClearCaches userEKBoxStorage") if s := mctx.G().GetUserEKBoxStorage(); s != nil { s.ClearCache() } mctx.Debug("| EKLib.ClearCaches teamEKBoxStorage") if s := mctx.G().GetTeamEKBoxStorage(); s != nil { s.ClearCache() } mctx.Debug("| EKLib.ClearCaches teambotEKBoxStorage") if s := mctx.G().GetTeambotEKBoxStorage(); s != nil { s.ClearCache() } } func (e *EKLib) purgeDeviceEKsIfOneshot(mctx libkb.MetaContext) { if deviceEKStorage := mctx.G().GetDeviceEKStorage(); deviceEKStorage != nil { if isOneshot, err := mctx.G().IsOneshot(mctx.Ctx()); err != nil { mctx.Debug("unable to check IsOneshot %v", err) } else if isOneshot { username := mctx.G().Env.GetUsername() mctx.Debug("Calling ForceDeleteAll because %q is running in oneshot mode", username) if err := deviceEKStorage.ForceDeleteAll(mctx, username); err != nil { mctx.Debug("Unable to complete ForceDeleteAll: %v", err) } } } } func (e *EKLib) OnLogin(mctx libkb.MetaContext) error { keygen := func(mctx libkb.MetaContext) { if err := e.KeygenIfNeeded(mctx); err != nil { mctx.Debug("OnLogin error: %v", err) } } if mctx.G().Standalone { // If we are in standalone run this synchronously to avoid racing if we // are attempting logout. keygen(mctx) } else { e.stateMu.Lock() defer e.stateMu.Unlock() stopCh := e.stopCh if stopCh == nil { return nil } go func() { select { case <-time.After(libkb.RandomJitter(time.Second)): case <-stopCh: return } keygen(mctx.BackgroundWithLogTags()) }() } if deviceEKStorage := mctx.G().GetDeviceEKStorage(); deviceEKStorage != nil { deviceEKStorage.SetLogPrefix(mctx) } return nil } func (e *EKLib) OnLogout(mctx libkb.MetaContext) error { e.ClearCaches(mctx) if deviceEKStorage := mctx.G().GetDeviceEKStorage(); deviceEKStorage != nil { deviceEKStorage.SetLogPrefix(mctx) } e.purgeDeviceEKsIfOneshot(mctx) return nil } func (e *EKLib) OnDbNuke(mctx libkb.MetaContext) error { e.ClearCaches(mctx) return nil }