github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/teams/hidden/manager.go (about) 1 package hidden 2 3 import ( 4 "fmt" 5 "strings" 6 "time" 7 8 libkb "github.com/keybase/client/go/libkb" 9 keybase1 "github.com/keybase/client/go/protocol/keybase1" 10 storage "github.com/keybase/client/go/teams/storage" 11 ) 12 13 const ( 14 HiddenChainFlagCacheTime = 24 * time.Hour 15 ) 16 17 // ChainManager manages a hidden team chain, and wraps put/gets to mem/disk storage. 18 // Accesses are single-flighted by TeamID. Implements that libkb.HiddenTeamChainManager 19 // interface. 20 type ChainManager struct { 21 // single-flight lock on TeamID 22 locktab *libkb.LockTable 23 24 hiddenSupportStorage *storage.SupportsHiddenFlagStorage 25 26 // Hold onto FastTeamLoad by-products as long as we have room, and store 27 // them persistently to disk. 28 storage *storage.HiddenStorage 29 } 30 31 var _ libkb.HiddenTeamChainManager = (*ChainManager)(nil) 32 33 type loadArg struct { 34 id keybase1.TeamID 35 mutate func(libkb.MetaContext, *keybase1.HiddenTeamChain) (bool, error) 36 } 37 38 func (m *ChainManager) TeamSupportsHiddenChain(mctx libkb.MetaContext, id keybase1.TeamID) (state bool, err error) { 39 supportsHiddenState := m.hiddenSupportStorage.Get(mctx, id) 40 // if we never checked before or the chain was not supported but the cache 41 // expired, check again. Once enabled, hidden support cannot be revoked 42 // regardless of the cache staleness. 43 if supportsHiddenState != nil { 44 mctx.Debug("ChainManager#TeamSupportsHiddenChain(%v): current state is %+v", id, *supportsHiddenState) 45 } else { 46 mctx.Debug("ChainManager#TeamSupportsHiddenChain(%v): current state is nil", id) 47 } 48 if supportsHiddenState == nil || (!supportsHiddenState.State && mctx.G().Clock().Now().After(supportsHiddenState.CacheUntil)) { 49 mctx.Debug("ChainManager#TeamSupportsHiddenChain(%v): querying the server", id) 50 state, err = featureGateForTeamFromServer(mctx, id) 51 if err != nil { 52 mctx.Debug("ChainManager#TeamSupportsHiddenChain(%v): got error %v", id) 53 return false, err 54 } 55 supportsHiddenState = &storage.HiddenChainSupportState{TeamID: id, State: state, CacheUntil: mctx.G().Clock().Now().Add(HiddenChainFlagCacheTime)} 56 m.hiddenSupportStorage.Put(mctx, supportsHiddenState) 57 } 58 mctx.Debug("ChainManager#TeamSupportsHiddenChain(%s): returning %v", id, supportsHiddenState.State) 59 return supportsHiddenState.State, nil 60 } 61 62 func ShouldClearSupportFlagOnError(err error) bool { 63 if err == nil { 64 return false 65 } 66 return !strings.Contains(err.Error(), "API network error") 67 } 68 69 func (m *ChainManager) ClearSupportFlagIfFalse(mctx libkb.MetaContext, teamID keybase1.TeamID) { 70 mctx.Debug("ChainManager#ClearSupportFlagIfFalse(%v)", teamID) 71 m.hiddenSupportStorage.ClearEntryIfFalse(mctx, teamID) 72 } 73 74 // Tail returns the furthest known tail of the hidden team chain, as known to our local cache. 75 // Needed when posting new main chain links that point back to the most recently known tail. 76 func (m *ChainManager) Tail(mctx libkb.MetaContext, id keybase1.TeamID) (*keybase1.LinkTriple, error) { 77 mctx = withLogTag(mctx) 78 state, err := m.loadAndMutate(mctx, loadArg{id: id}) 79 if err != nil { 80 return nil, err 81 } 82 if state == nil { 83 return nil, nil 84 } 85 return state.MaxTriple(), nil 86 } 87 88 func (m *ChainManager) loadLocked(mctx libkb.MetaContext, arg loadArg) (ret *keybase1.HiddenTeamChain, frozen bool, err error) { 89 state, frozen, tombstoned := m.storage.Get(mctx, arg.id, arg.id.IsPublic()) 90 if tombstoned { 91 return nil, false, NewTombstonedError("cannot load hidden chain for tombstoned team") 92 } 93 return state, frozen, nil 94 } 95 96 func withLogTag(mctx libkb.MetaContext) libkb.MetaContext { 97 return mctx.WithLogTag("HTCM") 98 } 99 100 // Load hidden team chain data from storage, either mem or disk. Will not hit the network. 101 func (m *ChainManager) Load(mctx libkb.MetaContext, id keybase1.TeamID) (ret *keybase1.HiddenTeamChain, err error) { 102 mctx = withLogTag(mctx) 103 ret, err = m.loadAndMutate(mctx, loadArg{id: id}) 104 return ret, err 105 } 106 107 func (m *ChainManager) checkFrozen(mctx libkb.MetaContext, newState *keybase1.HiddenTeamChain, frozenState *keybase1.HiddenTeamChain) (err error) { 108 if frozenState == nil || frozenState.Last == keybase1.Seqno(0) { 109 return nil 110 } 111 if newState == nil { 112 return NewManagerError("previously frozen state was non-nil, but this state is nil") 113 } 114 link, ok := frozenState.Outer[frozenState.Last] 115 if !ok { 116 return NewManagerError("bad frozen state, couldn't find link for %d", frozenState.Last) 117 } 118 newLink, ok := newState.Outer[frozenState.Last] 119 if !ok { 120 return NewManagerError("On thaw, new state is missing link at %d", frozenState.Last) 121 } 122 if !newLink.Eq(link) { 123 return NewManagerError("On thaw, hash mismatch at link %d (%s != %s)", frozenState.Last, newLink, link) 124 } 125 return nil 126 } 127 128 // Load hidden team chain data from storage, either mem or disk. Will not hit the network. 129 func (m *ChainManager) HintLatestSeqno(mctx libkb.MetaContext, id keybase1.TeamID, q keybase1.Seqno) (err error) { 130 mctx = withLogTag(mctx) 131 defer mctx.Trace(fmt.Sprintf("hidden.ChainManager#HintLatestSeqno(%d)", q), &err)() 132 _, err = m.loadAndMutate(mctx, loadArg{ 133 id: id, 134 mutate: func(mctx libkb.MetaContext, state *keybase1.HiddenTeamChain) (bool, error) { 135 changed := false 136 if state.LatestSeqnoHint < q { 137 mctx.Debug("For %s: update LatestSeqnoHint from %d -> %d", id, state.LatestSeqnoHint, q) 138 state.LatestSeqnoHint = q 139 changed = true 140 } else if state.LatestSeqnoHint == q { 141 mctx.Debug("For %s: update LatestSeqnoHint dupe update at %d", id, q) 142 } else { 143 mctx.Debug("For %s: refusing to backtrack from %d -> %d", id, state.LatestSeqnoHint, q) 144 } 145 return changed, nil 146 147 }, 148 }) 149 return err 150 } 151 152 func (m *ChainManager) loadAndMutate(mctx libkb.MetaContext, arg loadArg) (state *keybase1.HiddenTeamChain, err error) { 153 defer mctx.Trace(fmt.Sprintf("ChainManager#load(%+v)", arg), &err)() 154 lock := m.locktab.AcquireOnName(mctx.Ctx(), mctx.G(), arg.id.String()) 155 defer lock.Release(mctx.Ctx()) 156 157 var frozenState *keybase1.HiddenTeamChain 158 var frozen bool 159 frozenState, frozen, err = m.loadLocked(mctx, arg) 160 if err != nil { 161 return nil, err 162 } 163 if !frozen { 164 state = frozenState 165 } 166 167 if arg.mutate == nil { 168 return state, nil 169 } 170 171 var changed bool 172 if state == nil { 173 state = keybase1.NewHiddenTeamChain(arg.id) 174 } 175 changed, err = arg.mutate(mctx, state) 176 if err != nil { 177 return nil, err 178 } 179 if !changed { 180 return state, nil 181 } 182 if frozen { 183 err = m.checkFrozen(mctx, state, frozenState) 184 if err != nil { 185 return nil, err 186 } 187 } 188 state.CachedAt = keybase1.ToTime(mctx.G().Clock().Now()) 189 m.storage.Put(mctx, state) 190 191 return state, nil 192 } 193 194 func (m *ChainManager) ratchet(mctx libkb.MetaContext, state *keybase1.HiddenTeamChain, ratchet keybase1.HiddenTeamChainRatchetSet) (ret bool, err error) { 195 err = checkRatchets(mctx, state, ratchet) 196 if err != nil { 197 return false, err 198 } 199 updated := state.RatchetSet.Merge(ratchet) 200 return updated, nil 201 } 202 203 // Ratchet should be called when we know about advances in this chain but don't necessarily have the links to back the 204 // ratchet up. We'll check them later when next we refresh. But we do check that the ratchet is consistent with the known 205 // data (and ratchets) that we have. 206 func (m *ChainManager) Ratchet(mctx libkb.MetaContext, id keybase1.TeamID, ratchets keybase1.HiddenTeamChainRatchetSet) (err error) { 207 mctx = withLogTag(mctx) 208 defer mctx.Trace(fmt.Sprintf("hidden.ChainManager#Ratchet(%s, %+v)", id, ratchets), &err)() 209 arg := loadArg{ 210 id: id, 211 mutate: func(mctx libkb.MetaContext, state *keybase1.HiddenTeamChain) (bool, error) { 212 return m.ratchet(mctx, state, ratchets) 213 }, 214 } 215 _, err = m.loadAndMutate(mctx, arg) 216 if err != nil { 217 return err 218 } 219 return nil 220 } 221 222 func (m *ChainManager) checkPrev(mctx libkb.MetaContext, state *keybase1.HiddenTeamChain, newData keybase1.HiddenTeamChain, expectedPrev *keybase1.LinkTriple) (err error) { 223 224 // nothing to check if no new links 225 if len(newData.Outer) == 0 { 226 return nil 227 } 228 229 if expectedPrev == nil { 230 _, ok := newData.Outer[keybase1.Seqno(1)] 231 if !ok { 232 return NewManagerError("if no prev given, a head link is required") 233 } 234 return nil 235 } 236 link, ok := state.Outer[expectedPrev.Seqno] 237 if !ok { 238 return NewManagerError("update at %v left a chain gap", *expectedPrev) 239 } 240 if !link.Eq(expectedPrev.LinkID) { 241 return NewManagerError("prev mismatch at %v", *expectedPrev) 242 } 243 return nil 244 } 245 246 func (m *ChainManager) advance(mctx libkb.MetaContext, state *keybase1.HiddenTeamChain, newData keybase1.HiddenTeamChain, expectedPrev *keybase1.LinkTriple) (update bool, err error) { 247 err = m.checkRatchetsOnAdvance(mctx, state.RatchetSet, newData) 248 if err != nil { 249 return false, err 250 } 251 err = m.checkPrev(mctx, state, newData, expectedPrev) 252 if err != nil { 253 return false, err 254 } 255 update, err = state.Merge(newData) 256 if err != nil { 257 return false, err 258 } 259 return update, nil 260 } 261 262 func (m *ChainManager) checkRatchetOnAdvance(mctx libkb.MetaContext, r keybase1.LinkTripleAndTime, newData keybase1.HiddenTeamChain) (err error) { 263 q := r.Triple.Seqno 264 link, ok := newData.Outer[q] 265 if ok && !link.Eq(r.Triple.LinkID) { 266 return NewManagerError("update data failed to match ratchet %+v", r) 267 } 268 return nil 269 } 270 271 func (m *ChainManager) checkRatchetsOnAdvance(mctx libkb.MetaContext, ratchets keybase1.HiddenTeamChainRatchetSet, newData keybase1.HiddenTeamChain) (err error) { 272 for _, r := range ratchets.Flat() { 273 err = m.checkRatchetOnAdvance(mctx, r, newData) 274 if err != nil { 275 return err 276 } 277 } 278 return nil 279 } 280 281 // Advance the stored hidden team storage by the given update. Before this function is called, we should 282 // have checked many things: 283 // - that the PTKs match the unverified seeds sent down by the server. 284 // - that the postImages of the seedChecks are continuous, given a consistent set of seeds 285 // - that all full (unstubbed links) have valid reverse signatures 286 // - that all prevs are self consistent, and consistent with any preloaded data 287 // - that if the update starts in the middle of the chain, that its head has a prev, and that prev is consistent. 288 // - that the updates are consistent with any known ratchets 289 // 290 // See hidden.go for and the caller of this function for where that happens. 291 func (m *ChainManager) Advance(mctx libkb.MetaContext, dat keybase1.HiddenTeamChain, expectedPrev *keybase1.LinkTriple) (err error) { 292 mctx = withLogTag(mctx) 293 defer mctx.Trace(fmt.Sprintf("hidden.ChainManager#Advance(%s)", dat.ID()), &err)() 294 arg := loadArg{ 295 id: dat.ID(), 296 mutate: func(mctx libkb.MetaContext, state *keybase1.HiddenTeamChain) (bool, error) { 297 return m.advance(mctx, state, dat, expectedPrev) 298 }, 299 } 300 _, err = m.loadAndMutate(mctx, arg) 301 if err != nil { 302 return err 303 } 304 return nil 305 } 306 307 func NewChainManager(g *libkb.GlobalContext) *ChainManager { 308 return &ChainManager{ 309 storage: storage.NewHiddenStorage(g), 310 hiddenSupportStorage: storage.NewSupportsHiddenFlagStorage(g), 311 locktab: libkb.NewLockTable(), 312 } 313 } 314 315 func NewChainManagerAndInstall(g *libkb.GlobalContext) *ChainManager { 316 ret := NewChainManager(g) 317 g.SetHiddenTeamChainManager(ret) 318 g.AddLogoutHook(ret, "HiddenTeamChainManager") 319 g.AddDbNukeHook(ret, "HiddenTeamChainManager") 320 return ret 321 } 322 323 func (m *ChainManager) Shutdown(mctx libkb.MetaContext) { 324 m.storage.Shutdown() 325 m.hiddenSupportStorage.Shutdown() 326 } 327 328 // OnLogout is called when the user logs out, which purges the LRU. 329 func (m *ChainManager) OnLogout(mctx libkb.MetaContext) error { 330 m.storage.ClearMem() 331 m.hiddenSupportStorage.ClearMem() 332 return nil 333 } 334 335 // OnDbNuke is called when the disk cache is cleared, which purges the LRU. 336 func (m *ChainManager) OnDbNuke(mctx libkb.MetaContext) error { 337 m.storage.ClearMem() 338 m.hiddenSupportStorage.ClearMem() 339 return nil 340 } 341 342 func (m *ChainManager) Tombstone(mctx libkb.MetaContext, id keybase1.TeamID) (err error) { 343 mctx = withLogTag(mctx) 344 defer mctx.Trace(fmt.Sprintf("hidden.ChainManager#Tombstone(%s)", id), &err)() 345 arg := loadArg{ 346 id: id, 347 mutate: func(mctx libkb.MetaContext, state *keybase1.HiddenTeamChain) (bool, error) { 348 return state.Tombstone(), nil 349 }, 350 } 351 _, err = m.loadAndMutate(mctx, arg) 352 return err 353 } 354 355 func (m *ChainManager) Freeze(mctx libkb.MetaContext, id keybase1.TeamID) (err error) { 356 mctx = withLogTag(mctx) 357 defer mctx.Trace(fmt.Sprintf("hidden.ChainManager#Freeze(%s)", id), &err)() 358 arg := loadArg{ 359 id: id, 360 mutate: func(mctx libkb.MetaContext, state *keybase1.HiddenTeamChain) (bool, error) { 361 return state.Freeze(), nil 362 }, 363 } 364 _, err = m.loadAndMutate(mctx, arg) 365 return err 366 }