github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/teams/handler.go (about) 1 package teams 2 3 import ( 4 "context" 5 "encoding/base64" 6 "errors" 7 "fmt" 8 9 "github.com/keybase/client/go/engine" 10 "github.com/keybase/client/go/gregor" 11 "github.com/keybase/client/go/libkb" 12 "github.com/keybase/client/go/protocol/keybase1" 13 ) 14 15 func HandleRotateRequest(ctx context.Context, g *libkb.GlobalContext, msg keybase1.TeamCLKRMsg) (err error) { 16 ctx = libkb.WithLogTag(ctx, "CLKR") 17 defer g.CTrace(ctx, fmt.Sprintf("HandleRotateRequest(%s,%d)", msg.TeamID, msg.Generation), &err)() 18 19 teamID := msg.TeamID 20 21 var needTeamReload bool 22 loadTeamArg := keybase1.LoadTeamArg{ 23 ID: teamID, 24 Public: teamID.IsPublic(), 25 ForceRepoll: true, 26 } 27 team, err := Load(ctx, g, loadTeamArg) 28 if err != nil { 29 return err 30 } 31 32 isAdmin := func() bool { 33 role, err := team.myRole(ctx) 34 return err == nil && role.IsOrAbove(keybase1.TeamRole_ADMIN) 35 } 36 if len(msg.ResetUsersUntrusted) > 0 && team.IsOpen() && isAdmin() { 37 // NOTE: One day, this code path will be unused. Server should not 38 // issue CLKRs with ResetUsersUntrusted for open teams. Instead, there 39 // is a new work type to sweep reset users: OPENSWEEP. See 40 // `HandleOpenTeamSweepRequest`. 41 42 // Even though this is open team, and we are aiming to not rotate them, 43 // the server asked us specifically to do so with this CLKR. We have to 44 // obey, otherwise that CLKR will stay undone and server will keep 45 // asking users to rotate. 46 postedLink, err := sweepOpenTeamResetAndDeletedMembers(ctx, g, team, msg.ResetUsersUntrusted, true /* rotate */) 47 if err != nil { 48 g.Log.CDebugf(ctx, "Failed to sweep deleted members: %s", err) 49 } else { 50 // If sweepOpenTeamResetAndDeletedMembers does not do anything to 51 // the team, do not load team again later. Otherwise, if new link 52 // was posted, we need to reload. 53 needTeamReload = postedLink 54 } 55 56 // * NOTE * Still call the regular rotate key routine even if sweep 57 // succeeds and posts link. 58 59 // In normal case, it will reload team, see that generation is higher 60 // than one requested in CLKR (because we rotated key during sweeping), 61 // and then bail out. 62 } 63 64 return RetryIfPossible(ctx, g, func(ctx context.Context, _ int) error { 65 if needTeamReload { 66 team2, err := Load(ctx, g, loadTeamArg) 67 if err != nil { 68 return err 69 } 70 team = team2 71 } 72 needTeamReload = true // subsequent calls to Load here need repoll. 73 74 if team.Generation() > msg.Generation { 75 g.Log.CDebugf(ctx, "current team generation %d > team.clkr generation %d, not rotating", 76 team.Generation(), msg.Generation) 77 return nil 78 } 79 80 g.Log.CDebugf(ctx, "rotating team %s (%s)", team.Name(), teamID) 81 82 rotationType := keybase1.RotationType_CLKR 83 if teamID.IsPublic() { 84 rotationType = keybase1.RotationType_VISIBLE 85 } 86 87 if err := team.Rotate(ctx, rotationType); err != nil { 88 g.Log.CDebugf(ctx, "rotating team %s (%s) error: %s", team.Name(), teamID, err) 89 return err 90 } 91 92 g.Log.CDebugf(ctx, "success rotating team %s (%s)", team.Name(), teamID) 93 return nil 94 }) 95 } 96 97 func HandleOpenTeamSweepRequest(ctx context.Context, g *libkb.GlobalContext, msg keybase1.TeamOpenSweepMsg) (err error) { 98 ctx = libkb.WithLogTag(ctx, "CLKR") 99 defer g.CTrace(ctx, fmt.Sprintf("HandleOpenTeamSweepRequest(teamID=%s,len(resetUsers)=%d)", msg.TeamID, len(msg.ResetUsersUntrusted)), &err)() 100 101 team, err := Load(ctx, g, keybase1.LoadTeamArg{ 102 ID: msg.TeamID, 103 Public: msg.TeamID.IsPublic(), 104 ForceRepoll: true, 105 }) 106 if err != nil { 107 return err 108 } 109 110 if !team.IsOpen() { 111 return fmt.Errorf("OpenSweep request for team %s that is not open", team.ID) 112 } 113 114 role, err := team.myRole(ctx) 115 if err != nil { 116 return err 117 } 118 if !role.IsOrAbove(keybase1.TeamRole_ADMIN) { 119 return fmt.Errorf("OpenSweep request for team %s but our role is: %s", team.ID, role.String()) 120 } 121 122 rotate := !team.CanSkipKeyRotation() 123 _, err = sweepOpenTeamResetAndDeletedMembers(ctx, g, team, msg.ResetUsersUntrusted, rotate) 124 return err 125 } 126 127 func sweepOpenTeamResetAndDeletedMembers(ctx context.Context, g *libkb.GlobalContext, 128 team *Team, resetUsersUntrusted []keybase1.TeamCLKRResetUser, rotate bool) (postedLink bool, err error) { 129 // When CLKR is invoked because of account reset and it's an open team, 130 // we go ahead and boot reset readers and writers out of the team. Key 131 // is also rotated in the process (in the same ChangeMembership link). 132 defer g.CTrace(ctx, fmt.Sprintf("sweepOpenTeamResetAndDeletedMembers(rotate=%t)", rotate), 133 &err)() 134 135 // Go through resetUsersUntrusted and fetch non-cached latest 136 // EldestSeqnos/Status. 137 type seqnoAndStatus struct { 138 eldestSeqno keybase1.Seqno 139 status keybase1.StatusCode 140 } 141 resetUsers := make(map[keybase1.UID]seqnoAndStatus) 142 for _, u := range resetUsersUntrusted { 143 if _, found := resetUsers[u.Uid]; found { 144 // User was in the list more than once. 145 continue 146 } 147 148 arg := libkb.NewLoadUserArg(g). 149 WithNetContext(ctx). 150 WithUID(u.Uid). 151 WithPublicKeyOptional(). 152 WithForcePoll(true) 153 upak, _, err := g.GetUPAKLoader().LoadV2(arg) 154 if err == nil { 155 resetUsers[u.Uid] = seqnoAndStatus{ 156 eldestSeqno: upak.Current.EldestSeqno, 157 status: upak.Current.Status, 158 } 159 } else { 160 g.Log.CDebugf(ctx, "Could not load uid:%s through UPAKLoader: %s", u.Uid) 161 } 162 } 163 164 err = RetryIfPossible(ctx, g, func(ctx context.Context, attempt int) error { 165 if attempt > 0 { 166 var err error 167 team, err = Load(ctx, g, keybase1.LoadTeamArg{ 168 ID: team.ID, 169 Public: team.ID.IsPublic(), 170 ForceRepoll: true, 171 }) 172 if err != nil { 173 return err 174 } 175 } 176 177 changeReq := keybase1.TeamChangeReq{None: []keybase1.UserVersion{}} 178 179 // We are iterating through resetUsers map, which is map of 180 // uid->EldestSeqno that we loaded via UPAKLoader. Do not rely 181 // on server provided resetUsersUntrusted for EldestSeqnos, 182 // just use UIDs and see if these users are reset. 183 184 // We do not need to consider PUKless members here, because we 185 // are not auto-adding PUKless people to open teams (server 186 // doesn't send PUKless TARs in OPENREQ msg), so it shouldn't 187 // be an issue. 188 for uid, u := range resetUsers { 189 members := team.AllUserVersionsByUID(ctx, uid) 190 for _, memberUV := range members { 191 if memberUV.EldestSeqno == u.eldestSeqno && u.status != keybase1.StatusCode_SCDeleted { 192 // Member is the current incarnation of the user 193 // (or user has never reset). 194 continue 195 } 196 role, err := team.MemberRole(ctx, memberUV) 197 if err != nil { 198 continue 199 } 200 switch role { 201 case 202 keybase1.TeamRole_RESTRICTEDBOT, 203 keybase1.TeamRole_BOT, 204 keybase1.TeamRole_READER, 205 keybase1.TeamRole_WRITER: 206 changeReq.None = append(changeReq.None, memberUV) 207 } 208 } 209 } 210 211 if len(changeReq.None) == 0 { 212 // no one to kick out 213 g.Log.CDebugf(ctx, "No one to remove from a CLKR list of %d users, after UPAKLoading %d of them", 214 len(resetUsersUntrusted), len(resetUsers)) 215 return nil 216 } 217 218 g.Log.CDebugf(ctx, "Posting ChangeMembership with %d removals (CLKR list was %d)", 219 len(changeReq.None), len(resetUsersUntrusted)) 220 221 opts := ChangeMembershipOptions{ 222 // Make it possible for user to come back in once they reprovision. 223 Permanent: false, 224 SkipKeyRotation: !rotate, 225 } 226 if err := team.ChangeMembershipWithOptions(ctx, changeReq, opts); err != nil { 227 return err 228 } 229 230 // Notify the caller that we posted a sig and they have to 231 // load team again. 232 postedLink = true 233 return nil 234 }) 235 236 return postedLink, err 237 } 238 239 func invalidateCaches(mctx libkb.MetaContext, teamID keybase1.TeamID) { 240 // refresh the KBFS Favorites cache since it no longer should contain 241 // this team. 242 mctx.G().NotifyRouter.HandleFavoritesChanged(mctx.G().GetMyUID()) 243 if ekLib := mctx.G().GetEKLib(); ekLib != nil { 244 ekLib.PurgeTeamEKCachesForTeamID(mctx, teamID) 245 ekLib.PurgeTeambotEKCachesForTeamID(mctx, teamID) 246 } 247 if keyer := mctx.G().GetTeambotMemberKeyer(); keyer != nil { 248 keyer.PurgeCache(mctx) 249 } 250 } 251 252 func handleChangeSingle(ctx context.Context, g *libkb.GlobalContext, row keybase1.TeamChangeRow, change keybase1.TeamChangeSet) (changedMetadata bool, err error) { 253 change.KeyRotated = row.KeyRotated 254 change.MembershipChanged = row.MembershipChanged 255 change.Misc = row.Misc 256 mctx := libkb.NewMetaContext(ctx, g) 257 258 defer mctx.Trace(fmt.Sprintf("team.handleChangeSingle [%s] (%+v, %+v)", g.Env.GetUsername(), row, change), 259 &err)() 260 261 // Any errors are already logged in their respective functions. 262 _ = HintLatestSeqno(mctx, row.Id, row.LatestSeqno) 263 _ = HintLatestHiddenSeqno(mctx, row.Id, row.LatestHiddenSeqno) 264 265 // If we're handling a rename we should also purge the resolver cache and 266 // the KBFS favorites cache 267 if change.Renamed { 268 if err = PurgeResolverTeamID(ctx, g, row.Id); err != nil { 269 mctx.Warning("error in PurgeResolverTeamID: %v", err) 270 err = nil // non-fatal 271 } 272 invalidateCaches(mctx, row.Id) 273 } 274 // Send teamID and teamName in two separate notifications. It is 275 // server-trust that they are the same team. 276 g.NotifyRouter.HandleTeamChangedByBothKeys(ctx, row.Id, row.Name, row.LatestSeqno, row.ImplicitTeam, change, 277 row.LatestHiddenSeqno, row.LatestOffchainSeqno, keybase1.TeamChangedSource_SERVER) 278 279 // Note we only get updates about new subteams we create because the flow 280 // is that we join the team as an admin when we create them and then 281 // immediately leave. 282 if change.Renamed || change.MembershipChanged || change.Misc { 283 changedMetadata = true 284 } 285 if change.MembershipChanged { 286 g.NotifyRouter.HandleCanUserPerformChanged(ctx, row.Name) 287 } 288 return changedMetadata, nil 289 } 290 291 func HandleChangeNotification(ctx context.Context, g *libkb.GlobalContext, rows []keybase1.TeamChangeRow, changes keybase1.TeamChangeSet) (err error) { 292 ctx = libkb.WithLogTag(ctx, "THCN") 293 defer g.CTrace(ctx, "HandleChangeNotification", &err)() 294 var anyChangedMetadata bool 295 for _, row := range rows { 296 if changedMetadata, err := handleChangeSingle(ctx, g, row, changes); err != nil { 297 return err 298 } else if changedMetadata { 299 anyChangedMetadata = true 300 } 301 } 302 if anyChangedMetadata { 303 g.NotifyRouter.HandleTeamMetadataUpdate(ctx) 304 } 305 return nil 306 } 307 308 func HandleTeamMemberShowcaseChange(ctx context.Context, g *libkb.GlobalContext) (err error) { 309 defer g.CTrace(ctx, "HandleTeamMemberShowcaseChange", &err)() 310 g.NotifyRouter.HandleTeamMetadataUpdate(ctx) 311 return nil 312 } 313 314 func HandleDeleteNotification(ctx context.Context, g *libkb.GlobalContext, rows []keybase1.TeamChangeRow) (err error) { 315 mctx := libkb.NewMetaContext(ctx, g) 316 defer mctx.Trace(fmt.Sprintf("team.HandleDeleteNotification(%v)", len(rows)), 317 &err)() 318 319 g.NotifyRouter.HandleTeamMetadataUpdate(ctx) 320 321 for _, row := range rows { 322 g.Log.CDebugf(ctx, "team.HandleDeleteNotification: (%+v)", row) 323 if err := TombstoneTeam(libkb.NewMetaContext(ctx, g), row.Id); err != nil { 324 g.Log.CDebugf(ctx, "team.HandleDeleteNotification: failed to Tombstone: %s", err) 325 } 326 invalidateCaches(mctx, row.Id) 327 g.NotifyRouter.HandleTeamDeleted(ctx, row.Id) 328 } 329 330 return nil 331 } 332 333 func HandleExitNotification(ctx context.Context, g *libkb.GlobalContext, rows []keybase1.TeamExitRow) (err error) { 334 mctx := libkb.NewMetaContext(ctx, g) 335 defer mctx.Trace(fmt.Sprintf("team.HandleExitNotification(%v)", len(rows)), 336 &err)() 337 338 g.NotifyRouter.HandleTeamMetadataUpdate(ctx) 339 for _, row := range rows { 340 mctx.Debug("team.HandleExitNotification: (%+v)", row) 341 if err := FreezeTeam(mctx, row.Id); err != nil { 342 mctx.Debug("team.HandleExitNotification: failed to FreezeTeam: %s", err) 343 } 344 invalidateCaches(mctx, row.Id) 345 mctx.G().NotifyRouter.HandleTeamExit(ctx, row.Id) 346 } 347 return nil 348 } 349 350 func HandleNewlyAddedToTeamNotification(ctx context.Context, g *libkb.GlobalContext, rows []keybase1.TeamNewlyAddedRow) (err error) { 351 mctx := libkb.NewMetaContext(ctx, g) 352 defer mctx.Trace(fmt.Sprintf("team.HandleNewlyAddedToTeamNotification(%v)", len(rows)), 353 &err)() 354 for _, row := range rows { 355 mctx.Debug("team.HandleNewlyAddedToTeamNotification: (%+v)", row) 356 mctx.G().NotifyRouter.HandleNewlyAddedToTeam(mctx.Ctx(), row.Id) 357 invalidateCaches(mctx, row.Id) 358 } 359 return nil 360 } 361 362 func HandleSBSRequest(ctx context.Context, g *libkb.GlobalContext, msg keybase1.TeamSBSMsg) (err error) { 363 ctx = libkb.WithLogTag(ctx, "CLKR") 364 defer g.CTrace(ctx, "HandleSBSRequest", &err)() 365 for _, invitee := range msg.Invitees { 366 if err := handleSBSSingle(ctx, g, msg.TeamID, invitee); err != nil { 367 return err 368 } 369 } 370 return nil 371 } 372 373 func handleSBSSingle(ctx context.Context, g *libkb.GlobalContext, teamID keybase1.TeamID, untrustedInviteeFromGregor keybase1.TeamInvitee) (err error) { 374 defer g.CTrace(ctx, fmt.Sprintf("team.handleSBSSingle(teamID: %v, invitee: %+v)", teamID, untrustedInviteeFromGregor), &err)() 375 376 return RetryIfPossible(ctx, g, func(ctx context.Context, _ int) error { 377 team, err := Load(ctx, g, keybase1.LoadTeamArg{ 378 ID: teamID, 379 Public: teamID.IsPublic(), 380 ForceRepoll: true, 381 }) 382 if err != nil { 383 g.Log.CDebugf(ctx, "Load of team failed") 384 return err 385 } 386 387 // verify the invite info: 388 389 // find the invite in the team chain 390 inviteMD, found := team.chain().FindActiveInviteMDByID(untrustedInviteeFromGregor.InviteID) 391 if !found { 392 g.Log.CDebugf(ctx, "FindActiveInviteByID failed for invite %s", untrustedInviteeFromGregor.InviteID) 393 return libkb.NotFoundError{Msg: "Invite not found"} 394 } 395 invite := inviteMD.Invite 396 g.Log.CDebugf(ctx, "Found invite: %+v", invite) 397 category, err := invite.Type.C() 398 if err != nil { 399 return err 400 } 401 ityp, err := invite.Type.String() 402 if err != nil { 403 return err 404 } 405 switch category { 406 case keybase1.TeamInviteCategory_SBS: 407 // resolve assertion in link (with uid in invite msg) 408 assertion := fmt.Sprintf("%s@%s+uid:%s", string(invite.Name), ityp, untrustedInviteeFromGregor.Uid) 409 410 arg := keybase1.Identify2Arg{ 411 UserAssertion: assertion, 412 UseDelegateUI: false, 413 Reason: keybase1.IdentifyReason{Reason: "process team invite"}, 414 CanSuppressUI: true, 415 IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_GUI, 416 } 417 eng := engine.NewResolveThenIdentify2(g, &arg) 418 m := libkb.NewMetaContext(ctx, g) 419 if err := engine.RunEngine2(m, eng); err != nil { 420 return err 421 } 422 case keybase1.TeamInviteCategory_EMAIL, keybase1.TeamInviteCategory_PHONE: 423 // nothing to verify, need to trust the server 424 case keybase1.TeamInviteCategory_KEYBASE: 425 // Check if UV in `untrustedInviteeFromGregor` is the same 426 // person as in `invite`, and that we can bring them in. 427 if err := assertCanAcceptKeybaseInvite(ctx, g, untrustedInviteeFromGregor, invite); err != nil { 428 g.Log.CDebugf(ctx, "Failed assertCanAcceptKeybaseInvite") 429 return err 430 } 431 default: 432 return fmt.Errorf("no verification implemented for invite category %s (%+v)", category, invite) 433 } 434 435 // It's fine to use untrustedInviteeFromGregor Uid and EldestSeqno. 436 // Code above verifies that Uid/Eldest passed by the server really 437 // belongs to crypto-person described in invite in sigchain. So 438 // right now untrustedInviteeFromGregor is *verified*. 439 verifiedInvitee := untrustedInviteeFromGregor 440 uv := NewUserVersion(verifiedInvitee.Uid, verifiedInvitee.EldestSeqno) 441 442 currentRole, err := team.MemberRole(ctx, uv) 443 if err != nil { 444 g.Log.CDebugf(ctx, "Failed to lookup memberRole for %+v", uv) 445 return err 446 } 447 448 if currentRole.IsOrAbove(invite.Role) { 449 if team.IsImplicit() { 450 g.Log.CDebugf(ctx, "This is implicit team SBS resolution, mooting invite %s", invite.Id) 451 req := keybase1.TeamChangeReq{} 452 req.CompletedInvites = make(SCMapInviteIDToUV) 453 req.CompletedInvites[invite.Id] = uv.PercentForm() 454 return team.ChangeMembership(ctx, req) 455 } 456 457 g.Log.CDebugf(ctx, "User already has same or higher role, canceling invite %s", invite.Id) 458 return removeInviteID(ctx, team, invite.Id) 459 } 460 461 tx := CreateAddMemberTx(team) 462 // NOTE: AddMemberBySBS errors out when encountering PUK-less users, 463 // and this transaction is also set to default AllowPUKless=false. 464 if err := tx.AddMemberBySBS(ctx, verifiedInvitee, invite.Role); err != nil { 465 return err 466 } 467 if err := tx.Post(libkb.NewMetaContext(ctx, g)); err != nil { 468 return err 469 } 470 471 // Send chat welcome message 472 if team.IsImplicit() { 473 // Do not send messages about keybase-type invites being resolved. 474 // They are supposed to be transparent for the users and look like 475 // a real members even though they have to be SBS-ed in. 476 if category != keybase1.TeamInviteCategory_KEYBASE { 477 iteamName, err := team.ImplicitTeamDisplayNameString(ctx) 478 if err != nil { 479 return err 480 } 481 g.Log.CDebugf(ctx, 482 "sending resolution message for successful SBS handle") 483 SendChatSBSResolutionMessage(ctx, g, iteamName, 484 string(invite.Name), ityp, verifiedInvitee.Uid) 485 } 486 } else { 487 g.Log.CDebugf(ctx, "sending welcome message for successful SBS handle") 488 SendChatInviteWelcomeMessage(ctx, g, team.Name().String(), category, invite.Inviter.Uid, 489 verifiedInvitee.Uid, invite.Role) 490 } 491 492 return nil 493 }) 494 } 495 496 func assertCanAcceptKeybaseInvite(ctx context.Context, g *libkb.GlobalContext, untrustedInviteeFromGregor keybase1.TeamInvitee, chainInvite keybase1.TeamInvite) error { 497 chainUV, err := chainInvite.KeybaseUserVersion() 498 if err != nil { 499 return err 500 } 501 if chainUV.Uid.NotEqual(untrustedInviteeFromGregor.Uid) { 502 return fmt.Errorf("chain keybase invite link uid %s does not match uid %s in team.sbs message", chainUV.Uid, untrustedInviteeFromGregor.Uid) 503 } 504 505 if chainUV.EldestSeqno.Eq(untrustedInviteeFromGregor.EldestSeqno) { 506 return nil 507 } 508 509 if chainUV.EldestSeqno == 0 { 510 g.Log.CDebugf(ctx, "team.sbs invitee eldest seqno: %d, allowing it to take the invite for eldest seqno 0 (reset account)", untrustedInviteeFromGregor.EldestSeqno) 511 return nil 512 } 513 514 return fmt.Errorf("chain keybase invite link eldest seqno %d does not match eldest seqno %d in team.sbs message", chainUV.EldestSeqno, untrustedInviteeFromGregor.EldestSeqno) 515 } 516 517 func HandleOpenTeamAccessRequest(ctx context.Context, g *libkb.GlobalContext, msg keybase1.TeamOpenReqMsg) (err error) { 518 ctx = libkb.WithLogTag(ctx, "CLKR") 519 defer g.CTrace(ctx, "HandleOpenTeamAccessRequest", &err)() 520 521 return RetryIfPossible(ctx, g, func(ctx context.Context, _ int) error { 522 team, err := Load(ctx, g, keybase1.LoadTeamArg{ 523 ID: msg.TeamID, 524 Public: msg.TeamID.IsPublic(), 525 ForceRepoll: true, 526 }) 527 if err != nil { 528 return err 529 } 530 531 if !team.IsOpen() { 532 g.Log.CDebugf(ctx, "team %q is not an open team", team.Name()) 533 return nil // Not an error - let the handler dismiss the message. 534 } 535 536 joinAsRole := team.chain().inner.OpenTeamJoinAs 537 switch joinAsRole { 538 case keybase1.TeamRole_READER, keybase1.TeamRole_WRITER: 539 default: 540 return fmt.Errorf("unexpected role to add to open team: %v", joinAsRole) 541 } 542 543 tx := CreateAddMemberTx(team) 544 for _, tar := range msg.Tars { 545 uv := NewUserVersion(tar.Uid, tar.EldestSeqno) 546 err := tx.AddMemberByUV(ctx, uv, joinAsRole, nil) 547 g.Log.CDebugf(ctx, "Open team request: adding %v, returned err: %v", uv, err) 548 } 549 550 if tx.IsEmpty() { 551 g.Log.CDebugf(ctx, "Nothing to do - transaction is empty") 552 return nil 553 } 554 555 return tx.Post(libkb.NewMetaContext(ctx, g)) 556 }) 557 } 558 559 type chatSeitanRecip struct { 560 inviter keybase1.UID 561 invitee keybase1.UID 562 role keybase1.TeamRole 563 } 564 565 func HandleTeamSeitan(ctx context.Context, g *libkb.GlobalContext, msg keybase1.TeamSeitanMsg) (err error) { 566 ctx = libkb.WithLogTag(ctx, "SEIT") 567 mctx := libkb.NewMetaContext(ctx, g) 568 defer mctx.Trace("HandleTeamSeitan", &err)() 569 570 team, err := Load(ctx, g, keybase1.LoadTeamArg{ 571 ID: msg.TeamID, 572 Public: msg.TeamID.IsPublic(), 573 ForceRepoll: true, 574 }) 575 if err != nil { 576 return err 577 } 578 579 var chats []chatSeitanRecip 580 581 // we only reject invalid or used up invites after the transaction was 582 // correctly submitted. 583 var invitesToReject []keybase1.TeamSeitanRequest 584 585 // Only allow crypto-members added through 'team.change_membership' to be 586 // added for Seitan invites (AllowPUKless=false). 587 tx := CreateAddMemberTx(team) 588 589 for _, seitan := range msg.Seitans { 590 inviteMD, found := team.chain().FindActiveInviteMDByID(seitan.InviteID) 591 if !found { 592 mctx.Debug("Couldn't find specified invite id %q; skipping", seitan.InviteID) 593 continue 594 } 595 invite := inviteMD.Invite 596 597 mctx.Debug("Processing Seitan acceptance for invite %s", invite.Id) 598 599 err := verifySeitanSingle(ctx, g, team, invite, seitan) 600 if err != nil { 601 if _, ok := err.(InviteLinkAcceptanceError); ok { 602 mctx.Debug("Provided AKey failed to verify with error: %v; ignoring and scheduling for rejection", err) 603 invitesToReject = append(invitesToReject, seitan) 604 } else { 605 mctx.Debug("Provided AKey failed to verify with error: %v; ignoring", err) 606 } 607 continue 608 } 609 610 uv := NewUserVersion(seitan.Uid, seitan.EldestSeqno) 611 currentRole, err := team.MemberRole(ctx, uv) 612 if err != nil { 613 mctx.Debug("Failure in team.MemberRole: %v", err) 614 return err 615 } 616 617 err = tx.CanConsumeInvite(ctx, invite.Id) 618 if err != nil { 619 if _, ok := err.(InviteLinkAcceptanceError); ok { 620 mctx.Debug("Can't use invite: %s; ignoring and scheduling for rejection", err) 621 invitesToReject = append(invitesToReject, seitan) 622 } else { 623 mctx.Debug("Can't use invite: %s", err) 624 } 625 continue 626 } 627 628 isNewStyle, err := IsNewStyleInvite(invite) 629 if err != nil { 630 mctx.Debug("Error checking whether invite is new-style: %s", isNewStyle) 631 continue 632 } 633 634 if currentRole.IsOrAbove(invite.Role) { 635 mctx.Debug("User already has same or higher role.") 636 if !isNewStyle { 637 mctx.Debug("User already has same or higher role; since is not a new-style invite, cancelling invite.") 638 tx.CancelInvite(invite.Id, uv.Uid) 639 } else { 640 mctx.Debug("User already has same or higher role; scheduling for rejection.") 641 invitesToReject = append(invitesToReject, seitan) 642 } 643 continue 644 } 645 646 err = tx.AddMemberByUV(ctx, uv, invite.Role, nil) 647 if err != nil { 648 mctx.Debug("Failed to add %v to transaction: %v", uv, err) 649 continue 650 } 651 652 // Only allow adding members as cryptomembers. Server should never send 653 // us PUKless users accepting seitan tokens. When PUKless user accepts 654 // seitan token invite status is set to WAITING_FOR_PUK and team_rekeyd 655 // hold on it till user gets a PUK and status is set to ACCEPTED. 656 err = tx.ConsumeInviteByID(ctx, invite.Id, uv) 657 if err != nil { 658 mctx.Debug("Failed to consume invite: %v", err) 659 continue 660 } 661 662 chats = append(chats, chatSeitanRecip{ 663 inviter: invite.Inviter.Uid, 664 invitee: seitan.Uid, 665 role: invite.Role, 666 }) 667 } 668 669 if tx.IsEmpty() { 670 mctx.Debug("Transaction is empty - nothing to post") 671 } else { 672 err = tx.Post(mctx) 673 if err != nil { 674 return fmt.Errorf("HandleTeamSeitan: Error posting transaction: %w", err) 675 } 676 677 // Send chats 678 for _, chat := range chats { 679 mctx.Debug("sending welcome message for successful Seitan handle: inviter: %s invitee: %s, role: %v", 680 chat.inviter, chat.invitee, chat.role) 681 SendChatInviteWelcomeMessage(ctx, g, team.Name().String(), keybase1.TeamInviteCategory_SEITAN, 682 chat.inviter, chat.invitee, chat.role) 683 } 684 } 685 686 if err = rejectInviteLinkAcceptances(mctx, invitesToReject); err != nil { 687 // the transaction posted correctly, and rejecting an invite is not a critical step, so just log and swallow the error 688 mctx.Debug("HandleTeamSeitan: error rejecting invite acceptances: %v", err) 689 } 690 691 return nil 692 } 693 694 func rejectInviteLinkAcceptances(mctx libkb.MetaContext, requests []keybase1.TeamSeitanRequest) error { 695 failed := 0 696 var lastErr error 697 for _, request := range requests { 698 arg := libkb.APIArg{ 699 Endpoint: "team/reject_invite_acceptance", 700 SessionType: libkb.APISessionTypeREQUIRED, 701 Args: libkb.HTTPArgs{ 702 "invite_id": libkb.S{Val: string(request.InviteID)}, 703 "uid": libkb.S{Val: request.Uid.String()}, 704 "eldest_seqno": libkb.I{Val: int(request.EldestSeqno)}, 705 "akey": libkb.S{Val: string(request.Akey)}, 706 }, 707 } 708 709 if _, err := mctx.G().API.Post(mctx, arg); err != nil { 710 failed++ 711 mctx.Debug("rejectInviteLinkAcceptances: failed to call cancel_invite_acceptance(%v,%v,%v): %s", request.InviteID, request.Uid, request.EldestSeqno, err) 712 lastErr = err 713 } 714 } 715 716 if failed > 0 { 717 return fmt.Errorf("Failed to reject %v (out of %v) InviteLink Acceptance requests. Last error: %w", failed, len(requests), lastErr) 718 } 719 return nil 720 } 721 722 func verifySeitanSingle(ctx context.Context, g *libkb.GlobalContext, team *Team, invite keybase1.TeamInvite, seitan keybase1.TeamSeitanRequest) (err error) { 723 pkey, err := SeitanDecodePKey(string(invite.Name)) 724 if err != nil { 725 return err 726 } 727 728 keyAndLabel, err := pkey.DecryptKeyAndLabel(ctx, team) 729 if err != nil { 730 return err 731 } 732 733 labelversion, err := keyAndLabel.V() 734 if err != nil { 735 return fmt.Errorf("while parsing KeyAndLabel: %s", err) 736 } 737 738 category, err := invite.Type.C() 739 if err != nil { 740 return err 741 } 742 743 switch labelversion { 744 case keybase1.SeitanKeyAndLabelVersion_V1: 745 if category != keybase1.TeamInviteCategory_SEITAN { 746 return fmt.Errorf("HandleTeamSeitan wanted to claim an invite with category %v; wanted seitan", category) 747 } 748 return verifySeitanSingleV1(keyAndLabel.V1().I, invite, seitan) 749 case keybase1.SeitanKeyAndLabelVersion_V2: 750 if category != keybase1.TeamInviteCategory_SEITAN { 751 return fmt.Errorf("HandleTeamSeitan wanted to claim an invite with category %v; wanted seitan", category) 752 } 753 return verifySeitanSingleV2(keyAndLabel.V2().K, invite, seitan) 754 case keybase1.SeitanKeyAndLabelVersion_Invitelink: 755 if category != keybase1.TeamInviteCategory_INVITELINK { 756 return fmt.Errorf("HandleTeamSeitan wanted to claim an invite with category %v; wanted invitelink", category) 757 } 758 return verifySeitanSingleInvitelink(ctx, g, team, keyAndLabel.Invitelink().I, invite, seitan) 759 default: 760 return fmt.Errorf("unknown KeyAndLabel version: %v", labelversion) 761 } 762 } 763 764 func verifySeitanSingleV1(key keybase1.SeitanIKey, invite keybase1.TeamInvite, seitan keybase1.TeamSeitanRequest) (err error) { 765 // We repeat the steps that user does when they request access using the 766 // invite ID and see if we get the same answer for the same parameters (UV 767 // and unixCTime). 768 ikey := SeitanIKey(key) 769 uv := keybase1.UserVersion{ 770 Uid: seitan.Uid, 771 EldestSeqno: seitan.EldestSeqno, 772 } 773 ourAccept, err := generateAcceptanceSeitanV1(ikey, uv, seitan.UnixCTime) 774 if err != nil { 775 return fmt.Errorf("failed to generate acceptance key to test: %w", err) 776 } 777 778 if !ourAccept.inviteID.Eq(invite.Id) { 779 return errors.New("invite ID mismatch (seitan)") 780 } 781 782 // Decode AKey received from the user to be able to do secure hash 783 // comparison. 784 decodedAKey, err := base64.StdEncoding.DecodeString(string(seitan.Akey)) 785 if err != nil { 786 return err 787 } 788 789 if !libkb.SecureByteArrayEq(ourAccept.akey, decodedAKey) { 790 return fmt.Errorf("did not end up with the same AKey") 791 } 792 793 return nil 794 } 795 796 func verifySeitanSingleV2(key keybase1.SeitanPubKey, invite keybase1.TeamInvite, seitan keybase1.TeamSeitanRequest) (err error) { 797 // Do the public key signature verification. Signature coming from the user 798 // is encoded in seitan.Akey. Recreate message using UV and ctime, then 799 // verify signature. 800 pubKey, err := ImportSeitanPubKey(key) 801 if err != nil { 802 return err 803 } 804 805 // For V2 the server responds with sig in the akey field. 806 var sig SeitanSig 807 decodedSig, err := base64.StdEncoding.DecodeString(string(seitan.Akey)) 808 if err != nil || len(sig) != len(decodedSig) { 809 return errors.New("Signature length verification failed (seitan)") 810 } 811 copy(sig[:], decodedSig) 812 813 // For V2 this is ms since the epoch, not seconds (like in V1 or InviteLink) 814 now := keybase1.Time(seitan.UnixCTime) 815 // NOTE: Since we are re-serializing the values from seitan here to 816 // generate the message, if we want to change the fields present in the 817 // signature in the future, old clients will not be compatible. 818 msg, err := GenerateSeitanSignatureMessage(seitan.Uid, seitan.EldestSeqno, SCTeamInviteID(seitan.InviteID), now) 819 if err != nil { 820 return err 821 } 822 823 err = VerifySeitanSignatureMessage(pubKey, msg, sig) 824 if err != nil { 825 return err 826 } 827 828 return nil 829 } 830 831 func verifySeitanSingleInvitelink(ctx context.Context, g *libkb.GlobalContext, team *Team, ikey keybase1.SeitanIKeyInvitelink, invite keybase1.TeamInvite, seitan keybase1.TeamSeitanRequest) (err error) { 832 // We repeat the steps that user does when they request access using the 833 // invite ID and see if we get the same answer for the same parameters (UV 834 // and unixCTime). 835 // 836 // Also, we check that the state of the user in the team has not changed 837 // since they signed the acceptance. 838 uv := keybase1.UserVersion{ 839 Uid: seitan.Uid, 840 EldestSeqno: seitan.EldestSeqno, 841 } 842 ourAccept, err := generateAcceptanceSeitanInviteLink(ikey, uv, seitan.UnixCTime) 843 if err != nil { 844 return NewInviteLinkAcceptanceError("failed to generate acceptance key to test: %w", err) 845 } 846 847 if !ourAccept.inviteID.Eq(invite.Id) { 848 return NewInviteLinkAcceptanceError("invite ID mismatch (seitan invitelink)") 849 } 850 851 // Decode AKey received from the user to be able to do secure hash 852 // comparison. 853 decodedAKey, err := base64.StdEncoding.DecodeString(string(seitan.Akey)) 854 if err != nil { 855 return NewInviteLinkAcceptanceError("Unable to decode AKey: %s", err) 856 } 857 858 if !libkb.SecureByteArrayEq(ourAccept.akey, decodedAKey) { 859 return NewInviteLinkAcceptanceError("did not end up with the same invitelink AKey") 860 } 861 862 roleChangeTime, wasMember := team.UserLastRoleChangeTime(uv) 863 if wasMember && seitan.UnixCTime < roleChangeTime.UnixSeconds() { 864 return NewInviteLinkAcceptanceError("invite link was accepted before the user last changed their role") 865 } 866 867 if g.Clock().Now().Unix() < seitan.UnixCTime { 868 // In this case we do not return an InviteLinkAcceptanceError to avoid 869 // triggering a rejection in case this client's clock is off. If the 870 // server is honest, clients shouldn't get asked to process these invite 871 // links anyways, and if it is malicious than it does not matter if we 872 // ask it to reject. Moreover, not rejecting might be cause a slight 873 // performance degradation that can be detected server side, while 874 // rejecting erroneously means people can't get into the teams they want 875 // with no immediate feedback error. 876 return fmt.Errorf("acceptance was produced with a future timestamp: ignoring (without rejecting)") 877 } 878 879 return nil 880 } 881 882 func HandleForceRepollNotification(ctx context.Context, g *libkb.GlobalContext, dtime gregor.TimeOrOffset) error { 883 e1 := g.GetTeamLoader().ForceRepollUntil(ctx, dtime) 884 e2 := g.GetFastTeamLoader().ForceRepollUntil(libkb.NewMetaContext(ctx, g), dtime) 885 if e1 != nil { 886 return e1 887 } 888 return e2 889 }