github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/teams/teamplayer.txt (about) 1 # Pseudocode rules 2 # 3 # - there are some basics types 4 # - `func foo(int i, bar[] bars) baz` is a function that takes args of type `i`, an array of `bar`s and 5 # returns a baz. 6 # - all calls by value, so no one ever modifies their arguments 7 # - only "storage" should have any side effects 8 # 9 10 type teamSnapshot 11 - id: the teamID of this team 12 - fqName: fully qualified name 13 - seeds: Team key seeds for all generations 14 - Full Membership, without implied admins 15 - links: list of <seqno, linkHash, stubbed> triples for all links in the chain; if the 'stubbed' bool is set, then the link was stubbed, and we didn't actually verify the sig. 16 - adminBookends: the admins for this team; each admin can have multiple bookends if he/she was added/deleted multiple times 17 - parent: teamID of parent 18 19 # Intended usage: 20 # - Chat encrypt: LoadTeam() then get the latest key 21 # - Chat decrypt: LoadTeam(NeedKeyGeneration: receivedKeyGen, WantMembers: [sender]) then checks membership.. um how will # it check membership for people who are no longer in the team?# 22 # - Chat UI: Load() then get whatever it needs cosmetically 23 # - CLI team management: LoadTeam(NeedAdmin: true, ForceSync: true) 24 # - KBFS: similar to chat, bust also wants cache busting notifications 25 26 LoadArg 27 - teamID 28 - needAdmin: bool, whether we need to be an admin. Will fail unless we are an admin in the returned Team. It is unreasonable to look at invites and list subteams with this set to false. 29 - needKeyGen: int, Load at least up to the keygen. Returns an error if the keygen is not loaded. 30 - neededMembers: UIDs, Refresh if these members are not current members of the team in the cache. Does not guarantee these members will be present in the returned team. 31 - forceRepoll: bool, force a sync with merkle, if the caller knows they want this for some reason. 32 33 func load(loadArg arg, storage storage) 34 ret := playchain(arg.teamID, arg.needAdmin, arg.forceRepoll, nil, storage) 35 if !arg.forceRepoll && (!hasAllMembers(ret, arg.neededMembers) || !hasKeyGeneration(ret, arg.neededKeyGen)) 36 ret = playchain(arg.teamID, arg.needAdmin, true, nil, s) 37 return ret 38 39 func assertProperlyStubbed(link link, seqno[] neededSeqnos, bool needAdmin) 40 if !isStubbed(link) 41 return 42 if (link.seqno in neededSeqnos) || needAdmin 43 throw "needed an unstubbed link" 44 if !linkCanBeStubbed(link) 45 throw "link type can't be stubbed" 46 47 func playchain(teamID t, bool needAdmin, bool forceRepoll, seqno[] neededSeqnos, storage storage) teamSnaphot 48 # Load the team out of cold storage 49 ret, storedTime := get(storage, t) 50 51 if hasStubbedLinks(ret) && needAdmin 52 ret = nil 53 54 lastSeqno := nil 55 lastLinkID := nil 56 if (now() - storedTime > cacheTime) || ret.lastSeqno() < max(neededSeqnos) || forceRepoll 57 # Reference the merkle tree to fetch the last available sequence ID 58 # for the team in question. 59 lastSeqno, lastLinkID = fetchLastSeqnoFromServerMerkleTree(t) 60 else 61 lastSeqno = ret.lastSeqno() 62 lastLinkID = ret.lastLinkID() 63 64 proofSet := newProofSet() 65 parentChildOperations := [] 66 67 if neededSeqnos != nil 68 ret, proofSet, parentChildOperations = fillInStubbedLinks(ret, neededSeqnos, ret.lastSeqno(), proofSet, parentChildOperations, storage) 69 70 teamUpdate := nil 71 if ret == nil || ret.lastSeqno() < lastSeqno 72 teamUpdate = getNewLinksFromServer(t, ret.lastSeqno(), needAdmin) 73 74 prev := ret.links[-1].link 75 for link in teamUpdate.Links 76 77 assertProperlyStubbed(link, neededSeqnos, needAdmin) 78 79 assertHashEquality(link.prev, hash(prev)) 80 81 proofSet = verifyLink(ret, link, proofSet, storage) 82 83 ## ParentChildOperations affect a parent and child chain in lockstep. 84 ## So far they are: subteam create, and subteam rename 85 ## TODO need a new server ticket so that child chain changes on each rename too 86 if isParentChildOperation(link) 87 parentChildOperations.push(toParentChildOperation(link)) 88 89 prev = link 90 ret = patchWithNewLink(ret, link) 91 92 if lastLinkID != ret.lastLinkID() 93 throw "link id mismatch" 94 checkParentChildOperations(ret.parent, parentChildOperations) 95 checkProofs(team, proofSet, storage) 96 97 ret = addSecrets(ret, teamUpdate.box, teamsUpdate.prevs, teamUpdate.readerKeyMasks) 98 99 put(storage, t, ret) 100 101 checkNeededSeqnos(ret, neededSeqnos) 102 103 return ret 104 105 # An adminBookend on a sigchain that is delegated at the start, and maybe revoked 106 # by the time we get to end. 107 type adminBookend 108 - admin: userID 109 - start: link 110 - end: *link 111 112 type teamUpdate 113 - links: list of links 114 - box: box for the current user 115 - prevs: prevs for previous shared keys 116 - readerKeyMasks: reader key masks for all apps for all generations for user 117 118 type parentChildOperations 119 - childOperation: the actual operation that happened 120 - parent: Seqno where the operation appears in the parent 121 122 type storage 123 - key -> value 124 125 type leafID 126 - a UID or a teamID 127 128 type proofTerm 129 - leafID: the UID or teamID for this proofTerm 130 - seqno: the seqno in the local chain 131 - linkHash: the hash of that link in the local chain 132 - merkleSeqno: the merkle seqno signed into the link 133 - merkleHashMeta: the meta hash signed into the link 134 135 # We need proof that a happens before b, (i.e., a < b) 136 type proof 137 - a: proofTerm 138 - b: proofTerm 139 140 type proofSet 141 - proof[]: a list of proofs that are needed to be proven by keybase 142 143 func verifyLink(teamSnapshot ts, link link, proofSet proofSet, storage storage) proofSet 144 145 // Note that it's possible to check this signature, but we'd need a way to lookup 146 // users from deviceIDs, and I'm not sure how much it's buying us. 147 if isStubbed(link) 148 return proofSet 149 150 // Check that inner and outer fields are in harmony 151 checkLinkOuterInnerMatch(link) 152 153 // just using what the signature says, verify it, and figure out 154 // which public key was used in the verification 155 kid := verifySignatureAndExtractKID(link) 156 157 // Load the user given the inner info in the sig's Body.Key 158 // section. Also load the key object specified there 159 user, key := loadUserAndKeyFromLinkInner(link, kid) 160 161 assert(key.KID == kid) 162 163 proofSet = verifyUserSignature(user, link, proofSet) 164 165 needsAdmin := linkNeedsAdmin(link) 166 167 if needsAdmin 168 # TODO needs a server change to be explicit about which implicit admin to use, 169 # and where on the chain this privilege was granted 170 ancestorTeamID, ancestorTeamSeqno := getTeamIdAndSeqnoOfAdminUsed(link) 171 172 if ancestorTeamID == nil 173 throw "need admin but link didn't specify which team admin to use" 174 tmp := ts 175 while tmp.id != ancestorTeamID 176 tmp = playchain(tmp.parent, false, false, {ancestorTeamSeqno}, storage) 177 if tmp == nil 178 throw "didn't find parent in chain" 179 proofSet = verifyUserIsAdmin(tmp, user, link, proofSet) 180 else 181 verifyUserIsWriter(ts, user, link) 182 183 return proofSet 184 185 func verifyUserSignature(user user, link link, proofSet proofSet) proofSet 186 k := getKeyFromLink(link) 187 assertSigned(k, link) 188 a,b := findKeyInUserSigchain(user, k, link.merkleSeqno) 189 proofSet = happensBefore(proofSet, a, link) 190 if b != nil 191 proofSet = happensBefore(proofSet, link, b) 192 return proofSet 193 194 func findKeyInUserSigchain(user user, key k, seqno merkleSeqno) 195 # iterate over all of the user's links, looking for the latest 196 # provisioning of k before merkleSeqo. Return that link, and 197 # also, if available, the next revocation of k after that link. 198 199 func verifyUserIsAdmin(teamSnapshot ts, user user, link link, proofSet proofSet) 200 for pb in ts.adminBookends 201 if user.uid == pb.admin && (pb.start.merkleSeqno <= link.merkleSeqno) && (pb.end == nil && link.merklSeqno <= pb.end.merkleSeqno) 202 proofSet = happensBefore(proofSet, pb.start, link) 203 if pb.end != nil 204 proofSet = happensBefore(proofSet, link, pb.end) 205 return proofSet 206 throw "user wasn't admin in team" 207 208 func checkParentChildOperations(teamID parentID, parentChildOperations[] parentChildOperations, storage stroage) 209 neededSeqnos := [] 210 for pco in parentChildOperations 211 neededSeqnos.push(pco.parent) 212 parent := playchain(parentID, false, false, neededSeqnos, storage) 213 for pco in parentChildOperations 214 parentOp := linkToOperation(parent.links[pco.parent]) 215 assertOperationEqual(parentOp, pco.childOperation) 216 217 func checkProofs(teamSnapshot team, proofSet proofSet, storage storage) 218 for proof in proofSet 219 checkProof(team proof, storage) 220 221 func checkProof(teamSnapshot team, proof proof, storage storage) 222 merklePath := getMerklePathFromRootToLeaf(proof.b.merkleSeqno, proof.a.leafID) 223 verifyMerklePath(merklePath, proof.a.leafID, proof.a.linkHash) 224 verifyMetaHash(merklePath.metaHash, proof.b.merkleMetaHash) 225 chainTail := merklePath[proof.a.leafID] 226 assert(chainTail.Seqno >= proof.a.seqno) 227 linkList = nil 228 if isUser(proof.a.leafID) 229 user := loadUPAK2(proof.a.leafID, storage) 230 # TODO this means we'll have to store all verified link hashes in a UPAK2 231 # including tracker link hashes. 232 linkList = user.links 233 else 234 if team.id != proof.a.leafID 235 team = playchain(proof.a.leafID, false, false, {proof.a.seqno}, storage) 236 linkList = team.links 237 assert(listList[chainTail.seqno] == chainTail.linkHash) 238 239 func checkNeededSeqnos(teamSnapshot team, seqno[] neededSeqnos) 240 for seqno in neededSeqnos 241 if team.links[seqno].stubbed 242 throw "needed link not filled" 243 244 func fillInStubbedLinks(teamSnapshot ret, seqno[] neededSeqnos, seqno upperLimit, proofSet proofSet, parentChildOperations, storage storage) 245 -> (teamSnapshot, proofSet, parentChildOperations) 246 # seqnos needed from the server 247 newLinkSeqnos := [] 248 for seqno in neededSeqnos 249 if ret.links[seqno].stubbed && seqno <= upperLimit 250 newLinkSeqnos.push(seqno) 251 252 ## TODO need a new server endpoint for this 253 newLinks := getLinksFromServer(ret.id, newLinkSeqnos) 254 for link in newLinks 255 assertIsntStubbed(link) 256 proofSet = verifyLink(pret, link, proofSet, storage) 257 ret.links[seqno].stubbed = false 258 ret = patchWithNewLink(ret, link) 259 if isParentChildOperation(link) 260 parentChildOperations = append(parentChildOperations, toParentChildOperation(link)) 261 262 return ret, proofSet, parentChildOperations 263 264 func happensBefore(proofSet proofSet, link a, link b) proofSet 265 a := toProofTerm(a) 266 b := toProofTerm(b) 267 for proof in proofSet by -1 # walk backwards to avoid O(n^2) hidden work factor 268 if a.leafID == proof.a.leafID && b.leafID == proof.b.leafID && proof.a.seqno <= a.seqno && b.seqno <= proof.b.seqno 269 proof.a = proofTermMax(proof.a, a) 270 proof.b = proofTermMin(proof.b, b) 271 return proofSet 272 proofSet.push(proof(a,b)) 273 return proofSet 274 275 func proofTermMax(proofTerm a, proofTerm b) proofTerm 276 if a.seqno > b.seqno 277 return a 278 else 279 return b 280 281 func proofTermMin(proofTerm a, proofTerm b) proofTerm 282 if a.seqno < b.seqno 283 return a 284 else 285 return b 286 287 func toProofTerm(link a) proofTerm 288 # for the given sigchain link, extract the seqno in the chain, the leafID of the chain 289 # (i.e., the UID or the teamID), the hash of the link, and the location in the merkle 290 # tree seen at the time the link was signed 291 292 func addSecrets(teamSnapshot ts,....) teamSnapshot 293 # Update the proof set with the given secret values as fetched from the server 294 # output the new snapshot. 295 # Make sure the secrets are in sync and match the sigchain state. 296 297 func patchWithNewLink(teamSnapshot ts, link link) teamSnapshot 298 # Update the teamSnapshot with the given link, and output a new teamSnapshot 299 # reflect the delta in the link. Should be idempotent, since we might call it twice 300 # in the case of fillInStubbedLinks.