github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/teams/seitan_v2_test.go (about) 1 package teams 2 3 import ( 4 "errors" 5 "testing" 6 "time" 7 8 "encoding/base64" 9 10 "golang.org/x/net/context" 11 12 "github.com/davecgh/go-spew/spew" 13 "github.com/keybase/client/go/kbtest" 14 "github.com/keybase/client/go/libkb" 15 "github.com/keybase/client/go/protocol/keybase1" 16 "github.com/stretchr/testify/require" 17 ) 18 19 // TestSeitanV2Encryption does an offline test run of seitan crypto 20 // functions. 21 func TestSeitanV2Encryption(t *testing.T) { 22 tc := SetupTest(t, "team", 1) 23 defer tc.Cleanup() 24 25 user, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 26 require.NoError(t, err) 27 28 name := createTeam(tc) 29 30 team, err := Load(context.TODO(), tc.G, keybase1.LoadTeamArg{ 31 Name: name, 32 NeedAdmin: true, 33 ForceRepoll: true, 34 }) 35 require.NoError(t, err) 36 37 ikey, err := GenerateIKeyV2() 38 require.NoError(t, err) 39 t.Logf("ikey is: %q (%d)\n", ikey, len(ikey)) 40 41 sikey, err := ikey.GenerateSIKey() 42 require.NoError(t, err) 43 t.Logf("sikey is: %v (%d)\n", sikey, len(sikey)) 44 45 keyPair, err := sikey.generateKeyPair() 46 require.NoError(t, err) 47 pubKey := keybase1.SeitanPubKey(keyPair.Public.GetKID()[:]) 48 49 inviteID, err := sikey.GenerateTeamInviteID() 50 require.NoError(t, err) 51 t.Logf("Invite id is: %s\n", inviteID) 52 require.Equal(t, len(string(inviteID)), 32) 53 54 var expectedLabelSms keybase1.SeitanKeyLabelSms 55 expectedLabelSms.F = "edwin powell hubble" 56 expectedLabelSms.N = "+48123zz3045" 57 58 expectedLabel := keybase1.NewSeitanKeyLabelWithSms(expectedLabelSms) 59 60 pkey, encoded, err := sikey.GeneratePackedEncryptedKey(context.TODO(), team, expectedLabel) 61 require.NoError(t, err) 62 require.EqualValues(t, pkey.Version, 2) 63 require.EqualValues(t, pkey.TeamKeyGeneration, 1) 64 require.NotZero(tc.T, pkey.RandomNonce) 65 66 t.Logf("Encrypted ikey with gen: %d\n", pkey.TeamKeyGeneration) 67 t.Logf("Armored output: %s\n", encoded) 68 69 expectedPKey, err := SeitanDecodePKey(encoded) 70 require.NoError(t, err) 71 require.Equal(t, expectedPKey.Version, pkey.Version) 72 require.Equal(t, expectedPKey.TeamKeyGeneration, pkey.TeamKeyGeneration) 73 require.Equal(t, expectedPKey.RandomNonce, pkey.RandomNonce) 74 require.Equal(t, expectedPKey.EncryptedKeyAndLabel, pkey.EncryptedKeyAndLabel) 75 76 keyAndLabel, err := pkey.DecryptKeyAndLabel(context.TODO(), team) 77 require.NoError(t, err) 78 keyAndLabelType, err := keyAndLabel.V() 79 require.NoError(t, err) 80 require.Equal(t, keybase1.SeitanKeyAndLabelVersion_V2, keyAndLabelType) 81 keyAndLabelV2 := keyAndLabel.V2() 82 require.EqualValues(t, pubKey, keyAndLabelV2.K) 83 84 label := keyAndLabelV2.L 85 labelType, err := label.T() 86 require.NoError(t, err) 87 require.Equal(t, keybase1.SeitanKeyLabelType_SMS, labelType) 88 89 labelSms := label.Sms() 90 require.Equal(t, expectedLabelSms.F, labelSms.F) 91 require.Equal(t, expectedLabelSms.N, labelSms.N) 92 93 t.Logf("Decrypted pubKey is %q\n", keyAndLabelV2.K) 94 95 uid := user.User.GetUID() 96 eldestSeqno := user.EldestSeqno 97 ctime := keybase1.ToTime(time.Now()) 98 msg, err := GenerateSeitanSignatureMessage(uid, eldestSeqno, inviteID, ctime) 99 require.NoError(t, err) 100 sig, _, err := sikey.GenerateSignature(uid, eldestSeqno, inviteID, ctime) 101 require.NoError(t, err) 102 103 require.NoError(t, VerifySeitanSignatureMessage(SeitanPubKey(keyPair.Public), msg, sig)) 104 } 105 106 func TestSeitanBadSignatures(t *testing.T) { 107 tc := SetupTest(t, "team", 1) 108 defer tc.Cleanup() 109 110 user, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 111 require.NoError(t, err) 112 113 ikey1, err := GenerateIKeyV2() 114 require.NoError(t, err) 115 116 sikey1, err := ikey1.GenerateSIKey() 117 require.NoError(t, err) 118 119 inviteID1, err := sikey1.GenerateTeamInviteID() 120 require.NoError(t, err) 121 122 keyPair1, err := sikey1.generateKeyPair() 123 require.NoError(t, err) 124 125 uid := user.User.GetUID() 126 eldestSeqno := user.EldestSeqno 127 ctime := keybase1.ToTime(time.Now()) 128 sig, _, err := sikey1.GenerateSignature(uid, eldestSeqno, inviteID1, ctime) 129 require.NoError(t, err) 130 131 // Check signature verification failure for using the wrong sikey 132 ikey2, err := GenerateIKeyV2() 133 require.NoError(t, err) 134 135 sikey2, err := ikey2.GenerateSIKey() 136 require.NoError(t, err) 137 138 keyPair2, err := sikey2.generateKeyPair() 139 require.NoError(t, err) 140 141 msg, err := GenerateSeitanSignatureMessage(uid, eldestSeqno, inviteID1, ctime) 142 require.NoError(t, err) 143 require.Error(t, VerifySeitanSignatureMessage(SeitanPubKey(keyPair2.Public), msg, sig)) 144 145 type Badmsg struct { 146 msg []byte 147 err error 148 } 149 badMsgs := make([]Badmsg, 4) 150 151 // Check signature verification failure for a bad uid 152 msgBadUID, errBadUID := GenerateSeitanSignatureMessage(uid+"a", eldestSeqno, inviteID1, ctime) 153 badMsgs = append(badMsgs, Badmsg{msgBadUID, errBadUID}) 154 155 // Check signature verification failure for a bad EldestSeqno 156 msgBadEldest, errBadEldest := GenerateSeitanSignatureMessage(uid, eldestSeqno+1, inviteID1, ctime) 157 badMsgs = append(badMsgs, Badmsg{msgBadEldest, errBadEldest}) 158 159 // Check signature verification failure for a bad InviteID 160 msgBadInviteID, errBadInviteID := GenerateSeitanSignatureMessage(uid, eldestSeqno, inviteID1+"a", ctime) 161 badMsgs = append(badMsgs, Badmsg{msgBadInviteID, errBadInviteID}) 162 163 // Check signature verification failure for a bad ctime 164 msgBadCTime, errBadCTime := GenerateSeitanSignatureMessage(uid, eldestSeqno, inviteID1, ctime+1) 165 badMsgs = append(badMsgs, Badmsg{msgBadCTime, errBadCTime}) 166 167 for _, bad := range badMsgs { 168 require.NoError(t, bad.err) 169 require.Error(t, VerifySeitanSignatureMessage(SeitanPubKey(keyPair1.Public), bad.msg, sig)) 170 } 171 172 } 173 174 // TestSeitanV2KnownSamples runs offline seitan crypto chain using known 175 // inputs and compares results with known samples generated using 176 // server test library. 177 func TestSeitanV2KnownSamples(t *testing.T) { 178 fromB64 := func(b string) (ret []byte) { 179 ret, err := base64.StdEncoding.DecodeString(b) 180 require.NoError(t, err) 181 return ret 182 } 183 184 // secret_key: GBsy8q2vgQ6jEHmQZiNJcxvgxVNlG4IsxKf/zcxtJIA= 185 // ikey: 4uywza+b3cga7rd6yc 186 // sikey: Il9ZFgI1yP2b6Hvt53jWIoo8sDre3puyNH8b2es9TTQ= 187 // inviteID: 6303ec43bd61d21edb95a433faf06227 188 // pkey: lAIBxBgxk+0rwtlCacCIzNK8apyoiiN69+tTU1HEgze4fphJmImUv7wFm54ioO9dB876yLUciHsuUItYXH1cSq6cShz2HrjVCSUQCJVxDNQwb3A6x2zv6/mrbUselphhjzxrJFGb6mS7N0cA3cYfdk+WByNEUOVqi6qwzgvAYuwEqM1sAYYb+NgrLEH5+4Tlr5mcWfAtLynLngX3Z4Ef4Mf1 189 // label: {"sms":{"f":"Alice","n":"111-555-222"},"t":1} 190 191 var expectedSIKey SeitanSIKeyV2 192 copy(expectedSIKey[:], fromB64("Il9ZFgI1yP2b6Hvt53jWIoo8sDre3puyNH8b2es9TTQ=")) 193 expectedInviteID := SCTeamInviteID("6303ec43bd61d21edb95a433faf06227") 194 195 var secretKey keybase1.Bytes32 196 copy(secretKey[:], fromB64("GBsy8q2vgQ6jEHmQZiNJcxvgxVNlG4IsxKf/zcxtJIA=")) 197 198 pkeyBase64 := "lAIBxBgxk+0rwtlCacCIzNK8apyoiiN69+tTU1HEgze4fphJmImUv7wFm54ioO9dB876yLUciHsuUItYXH1cSq6cShz2HrjVCSUQCJVxDNQwb3A6x2zv6/mrbUselphhjzxrJFGb6mS7N0cA3cYfdk+WByNEUOVqi6qwzgvAYuwEqM1sAYYb+NgrLEH5+4Tlr5mcWfAtLynLngX3Z4Ef4Mf1" 199 200 ikey := SeitanIKeyV2("4uywza+b3cga7rd6yc") 201 sikey, err := ikey.GenerateSIKey() 202 require.NoError(t, err) 203 require.Equal(t, sikey, expectedSIKey) 204 205 inviteID, err := sikey.GenerateTeamInviteID() 206 require.NoError(t, err) 207 require.Equal(t, inviteID, expectedInviteID) 208 209 keyPair, err := sikey.generateKeyPair() 210 require.NoError(t, err) 211 212 expectedPKey, err := SeitanDecodePKey(pkeyBase64) 213 require.NoError(t, err) 214 require.EqualValues(t, 2, expectedPKey.Version) 215 require.EqualValues(t, 1, expectedPKey.TeamKeyGeneration) 216 217 keyAndLabel, err := expectedPKey.decryptKeyAndLabelWithSecretKey(secretKey) 218 require.NoError(t, err) // only encoded map or array can be decoded into a struct 219 220 keyAndLabelVersion, err := keyAndLabel.V() 221 require.NoError(t, err) 222 require.Equal(t, keybase1.SeitanKeyAndLabelVersion_V2, keyAndLabelVersion) 223 keyAndLabelV2 := keyAndLabel.V2() 224 pubKey := keyAndLabelV2.K 225 226 require.Equal(t, keybase1.SeitanPubKey(keyPair.GetKID().String()), pubKey) 227 228 label := keyAndLabelV2.L 229 labelType, err := label.T() 230 require.NoError(t, err) 231 require.Equal(t, keybase1.SeitanKeyLabelType_SMS, labelType) 232 233 labelSms := label.Sms() 234 require.Equal(t, "Alice", labelSms.F) 235 require.Equal(t, "111-555-222", labelSms.N) 236 237 pkey, _, err := sikey.generatePackedEncryptedKeyWithSecretKey(secretKey, keybase1.PerTeamKeyGeneration(1), expectedPKey.RandomNonce, keyAndLabelV2.L) 238 require.NoError(t, err) 239 require.Equal(t, expectedPKey.Version, pkey.Version) 240 require.Equal(t, expectedPKey.TeamKeyGeneration, pkey.TeamKeyGeneration) 241 require.Equal(t, expectedPKey.RandomNonce, pkey.RandomNonce) 242 require.Equal(t, expectedPKey.EncryptedKeyAndLabel, pkey.EncryptedKeyAndLabel) 243 } 244 245 // TestIsSeitanyAndAlphabetCoverage tests two unrelated things at once: (1) that 246 // the IsSeitany function correctly identifies Seitan tokens; and (2) that all 247 // letters of the Seitan alphabet are hit by generating a sufficient number of 248 // tokens. It would be bad, for instance, if we only hit 10% of the characters. 249 func TestIsSeitanyAndAlphabetCoverage(t *testing.T) { 250 251 ikeyV1Gen := func() (s string, err error) { 252 ikey, err := GenerateIKey() 253 return ikey.String(), err 254 } 255 256 ikeyV2Gen := func() (s string, err error) { 257 ikey, err := GenerateIKeyV2() 258 return ikey.String(), err 259 } 260 261 verifyCoverage := func(ikeyGen func() (s string, err error)) { 262 coverage := make(map[byte]bool) 263 for i := 0; i < 100; i++ { 264 s, err := ikeyGen() 265 require.NoError(t, err) 266 require.True(t, IsSeitany(s)) 267 require.True(t, IsSeitany(s[2:10])) 268 require.True(t, IsSeitany(s[3:13])) 269 for _, b := range []byte(s) { 270 coverage[b] = true 271 } 272 } 273 274 // This test can fail with probability 1-(29/30)^(1800), which is 275 // approximately (1 - 2^-88) 276 for _, b := range []byte(KBase30EncodeStd) { 277 require.True(t, coverage[b], "covered all chars") 278 } 279 } 280 verifyCoverage(ikeyV1Gen) 281 verifyCoverage(ikeyV2Gen) 282 } 283 284 func TestTeamHandleMultipleSeitans(t *testing.T) { 285 tc := SetupTest(t, "team", 1) 286 defer tc.Cleanup() 287 288 tc.Tp.SkipSendingSystemChatMessages = true 289 290 users := make([]*kbtest.FakeUser, 4) 291 for i := range users { 292 u, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 293 require.NoError(t, err) 294 kbtest.Logout(tc) 295 users[i] = u 296 } 297 298 ann, bee, dan, mel := users[0], users[1], users[2], users[3] 299 err := ann.Login(tc.G) 300 require.NoError(t, err) 301 302 teamName, teamID := createTeam2(tc) 303 t.Logf("Created team %s", teamName.String()) 304 305 _, err = AddMember(context.TODO(), tc.G, teamName.String(), dan.Username, keybase1.TeamRole_WRITER, nil /* botSettings */) 306 require.NoError(t, err) 307 308 addSeitanV2 := func(F, N string, role keybase1.TeamRole) keybase1.SeitanIKeyV2 { 309 label := keybase1.NewSeitanKeyLabelWithSms(keybase1.SeitanKeyLabelSms{ 310 F: F, 311 N: N, 312 }) 313 ikeyV2, err := CreateSeitanTokenV2(context.TODO(), tc.G, teamName.String(), role, label) 314 require.NoError(t, err) 315 return ikeyV2 316 } 317 318 tokenForBee := addSeitanV2("bee", "123", keybase1.TeamRole_WRITER) 319 // tokenForDan := addSeitanV2("dan", "555", keybase1.TeamRole_READER) 320 anotherToken := addSeitanV2("someone", "666", keybase1.TeamRole_READER) 321 322 teamObj, err := Load(context.TODO(), tc.G, keybase1.LoadTeamArg{ 323 Name: teamName.String(), 324 NeedAdmin: true, 325 }) 326 require.NoError(t, err) 327 328 invites := teamObj.GetActiveAndObsoleteInvites() 329 require.Len(t, invites, 2) 330 for _, invite := range invites { 331 invtype, err := invite.Type.C() 332 require.NoError(t, err) 333 require.Equal(t, keybase1.TeamInviteCategory_SEITAN, invtype) 334 } 335 336 acceptSeitan := func(u *kbtest.FakeUser, ikey keybase1.SeitanIKeyV2, corrupt bool) keybase1.TeamSeitanRequest { 337 kbtest.LogoutAndLoginAs(tc, u) 338 339 uv := u.GetUserVersion() 340 now := keybase1.ToTime(time.Now()) 341 accepted, err := generateAcceptanceSeitanV2(SeitanIKeyV2(ikey), uv, now) 342 require.NoError(t, err) 343 344 if corrupt { 345 // Ruin the acceptance sig so request is no longer valid 346 accepted.sig[0] ^= 0xF0 347 accepted.sig[1] ^= 0x0F 348 accepted.encoded = base64.StdEncoding.EncodeToString(accepted.sig[:]) 349 } 350 351 // We need to send this request so HandleTeamSeitan links can 352 // do completed_invites, otherwise server will reject these. 353 err = postSeitanV2(tc.MetaContext(), accepted) 354 require.NoError(t, err) 355 356 return keybase1.TeamSeitanRequest{ 357 InviteID: keybase1.TeamInviteID(accepted.inviteID), 358 Uid: uv.Uid, 359 EldestSeqno: uv.EldestSeqno, 360 Akey: keybase1.SeitanAKey(accepted.encoded), 361 UnixCTime: int64(now), 362 } 363 } 364 365 msg := keybase1.TeamSeitanMsg{ 366 TeamID: teamID, 367 Seitans: []keybase1.TeamSeitanRequest{ 368 acceptSeitan(bee, tokenForBee, false /* corrupt */), 369 // TODO: Accepting seitan while you are already in team is disabled because 370 // of Y2K-1898. Re-enable this after. 371 // acceptSeitan(dan, tokenForDan, false /* corrupt */), 372 acceptSeitan(mel, anotherToken, true /* corrupt */), 373 }, 374 } 375 376 kbtest.LogoutAndLoginAs(tc, ann) 377 378 API := libkb.NewAPIArgRecorder(tc.G.API) 379 tc.G.API = API 380 err = HandleTeamSeitan(context.TODO(), tc.G, msg) 381 require.NoError(t, err) 382 records := API.GetFilteredRecordsAndReset(func(rec *libkb.APIRecord) bool { 383 return rec.Arg.Endpoint == "team/reject_invite_acceptance" 384 }) 385 require.Len(t, records, 0, "no invite link acceptances were rejected") 386 387 teamObj, err = Load(context.TODO(), tc.G, keybase1.LoadTeamArg{ 388 Name: teamName.String(), 389 NeedAdmin: true, 390 }) 391 require.NoError(t, err) 392 393 // Ann is still an owner 394 role, err := teamObj.MemberRole(context.Background(), ann.GetUserVersion()) 395 require.NoError(t, err) 396 require.Equal(t, keybase1.TeamRole_OWNER, role) 397 398 // Bee got added as a writer 399 role, err = teamObj.MemberRole(context.Background(), bee.GetUserVersion()) 400 require.NoError(t, err) 401 require.Equal(t, keybase1.TeamRole_WRITER, role) 402 403 // Dan stayed writer 404 role, err = teamObj.MemberRole(context.Background(), dan.GetUserVersion()) 405 require.NoError(t, err) 406 require.Equal(t, keybase1.TeamRole_WRITER, role) 407 408 // Mel didn't get in 409 role, err = teamObj.MemberRole(context.Background(), mel.GetUserVersion()) 410 require.NoError(t, err) 411 require.Equal(t, keybase1.TeamRole_NONE, role) 412 413 // And invite that Mel tried (and failed) to use is still there. 414 require.Equal(t, 1, teamObj.NumActiveInvites(), "NumActiveInvites") 415 allInvites := teamObj.GetActiveAndObsoleteInvites() 416 require.Len(t, allInvites, 1) 417 for _, invite := range allInvites { 418 // Ignore errors, we went through this path before in seitan 419 // processing and acceptance. 420 sikey, _ := SeitanIKeyV2(anotherToken).GenerateSIKey() 421 inviteID, _ := sikey.GenerateTeamInviteID() 422 require.EqualValues(t, inviteID, invite.Id) 423 invtype, err := invite.Type.C() 424 require.NoError(t, err) 425 require.Equal(t, keybase1.TeamInviteCategory_SEITAN, invtype) 426 } 427 } 428 429 func TestTeamInviteSeitanV2Failures(t *testing.T) { 430 tc := SetupTest(t, "team", 1) 431 defer tc.Cleanup() 432 433 tc.Tp.SkipSendingSystemChatMessages = true 434 435 user2, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 436 require.NoError(t, err) 437 kbtest.Logout(tc) 438 439 admin, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 440 require.NoError(t, err) 441 442 teamName, teamID := createTeam2(tc) 443 t.Logf("Created team %q", teamName.String()) 444 445 token, err := CreateSeitanTokenV2(context.Background(), tc.G, 446 teamName.String(), keybase1.TeamRole_WRITER, keybase1.SeitanKeyLabel{}) 447 require.NoError(t, err) 448 449 t.Logf("Created token %q", token) 450 451 kbtest.LogoutAndLoginAs(tc, user2) 452 453 // Generate invitation id, but make Signature with different IKey. 454 // Simulate "replay attack" or similar. 455 ikey, err := ParseIKeyV2FromString(string(token)) 456 require.NoError(t, err) 457 sikey, err := ikey.GenerateSIKey() 458 require.NoError(t, err) 459 inviteID, err := sikey.GenerateTeamInviteID() 460 require.NoError(t, err) 461 462 ikey2, err := GenerateIKeyV2() // ikey2 is not the ikey from token. 463 require.NoError(t, err) 464 sikey2, err := ikey2.GenerateSIKey() 465 require.NoError(t, err) 466 now := keybase1.ToTime(time.Now()) 467 badSig, badEncoded, err := sikey2.GenerateSignature(user2.GetUID(), user2.EldestSeqno, inviteID, now) 468 require.NoError(t, err) 469 470 err = postSeitanV2(tc.MetaContext(), acceptedSeitanV2{ 471 sig: badSig, 472 encoded: badEncoded, 473 now: now, 474 inviteID: inviteID, 475 }) 476 require.NoError(t, err) 477 478 teamInviteID, err := inviteID.TeamInviteID() 479 require.NoError(t, err) 480 481 t.Logf("handle synthesized rekeyd command") 482 kbtest.LogoutAndLoginAs(tc, admin) 483 484 msg := keybase1.TeamSeitanMsg{ 485 TeamID: teamID, 486 Seitans: []keybase1.TeamSeitanRequest{{ 487 InviteID: teamInviteID, 488 Uid: user2.GetUID(), 489 EldestSeqno: user2.EldestSeqno, 490 Akey: keybase1.SeitanAKey(badEncoded), 491 Role: keybase1.TeamRole_WRITER, 492 UnixCTime: int64(now), 493 }}, 494 } 495 API := libkb.NewAPIArgRecorder(tc.G.API) 496 tc.G.API = API 497 err = HandleTeamSeitan(context.TODO(), tc.G, msg) 498 // Seitan handler does not fail, but ignores the request. 499 require.NoError(t, err) 500 records := API.GetFilteredRecordsAndReset(func(rec *libkb.APIRecord) bool { 501 return rec.Arg.Endpoint == "team/reject_invite_acceptance" 502 }) 503 require.Len(t, records, 0, "no invite link acceptances were rejected") 504 505 t.Logf("invite should still be there") 506 t0, err := GetTeamByNameForTest(context.Background(), tc.G, teamName.String(), false /* public */, true /* needAdmin */) 507 require.NoError(t, err) 508 require.Equal(t, 1, t0.NumActiveInvites(), "invite should still be active") 509 require.EqualValues(t, t0.CurrentSeqno(), 2) 510 511 t.Logf("user should not be in team") 512 role, err := t0.MemberRole(context.Background(), user2.GetUserVersion()) 513 require.NoError(t, err) 514 require.Equal(t, keybase1.TeamRole_NONE, role, "user role") 515 } 516 517 func TestSeitanPukless(t *testing.T) { 518 // Test what happens if client receives handle Seitan notification with an 519 // acceptance that's of a PUKless user. If a user can't be added as a 520 // crypto-member (using 'team.change_membership' link), they should not be 521 // added at all during Seitan resolution, because adding a type='keybase' 522 // invitation using 'team.invite' link cannot complete Seitan invite 523 // properly. 524 525 tc := SetupTest(t, "team", 1) 526 defer tc.Cleanup() 527 528 tc.Tp.SkipSendingSystemChatMessages = true 529 530 admin, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 531 require.NoError(t, err) 532 t.Logf("Admin username: %s", admin.Username) 533 534 teamName, teamID := createTeam2(tc) 535 t.Logf("Created team %q", teamName.String()) 536 537 token, err := CreateSeitanTokenV2(context.Background(), tc.G, 538 teamName.String(), keybase1.TeamRole_WRITER, keybase1.SeitanKeyLabel{}) 539 require.NoError(t, err) 540 541 t.Logf("Created token %q", token) 542 543 kbtest.Logout(tc) 544 545 // Create a PUKless user 546 tc.Tp.DisableUpgradePerUserKey = true 547 user, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 548 require.NoError(t, err) 549 550 t.Logf("User: %s", user.Username) 551 552 timeNow := keybase1.ToTime(tc.G.Clock().Now()) 553 seitanRet, err := generateAcceptanceSeitanV2(SeitanIKeyV2(token), user.GetUserVersion(), timeNow) 554 require.NoError(t, err) 555 556 // Can't post this acceptance when we don't have a PUK. 557 err = postSeitanV2(tc.MetaContext(), seitanRet) 558 require.Error(t, err) 559 require.IsType(t, libkb.AppStatusError{}, err) 560 require.EqualValues(t, keybase1.StatusCode_SCTeamSeitanInviteNeedPUK, err.(libkb.AppStatusError).Code) 561 562 // But server could still send it to us, e.g. due to a bug. 563 kbtest.LogoutAndLoginAs(tc, admin) 564 565 inviteID, err := seitanRet.inviteID.TeamInviteID() 566 require.NoError(t, err) 567 568 msg := keybase1.TeamSeitanMsg{ 569 TeamID: teamID, 570 Seitans: []keybase1.TeamSeitanRequest{{ 571 InviteID: inviteID, 572 Uid: user.GetUID(), 573 EldestSeqno: user.EldestSeqno, 574 Akey: keybase1.SeitanAKey(seitanRet.encoded), 575 Role: keybase1.TeamRole_WRITER, 576 UnixCTime: int64(timeNow), 577 }}, 578 } 579 err = HandleTeamSeitan(context.Background(), tc.G, msg) 580 require.NoError(t, err) 581 582 // HandleTeamSeitan should not have added an invite for user. If it has, it 583 // also hasn't completed invite properly (`team.invite` link can't complete 584 // invite), which means the invite has been used but left active. 585 team, err := Load(context.TODO(), tc.G, keybase1.LoadTeamArg{ 586 Name: teamName.String(), 587 NeedAdmin: true, 588 ForceRepoll: true, 589 }) 590 require.NoError(t, err) 591 592 invite, _, found := team.FindActiveKeybaseInvite(user.GetUID()) 593 require.False(t, found, "Expected not to find invite for user: %s", spew.Sdump(invite)) 594 } 595 596 func TestSeitanMultipleRequestForOneInvite(t *testing.T) { 597 // Test server sending a Seitan notifications with multiple request for one 598 // Seitan invite. Seitan V1/V2 can never be multiple use, so at most one 599 // request should be handled. 600 601 tc := SetupTest(t, "team", 1) 602 defer tc.Cleanup() 603 604 tc.Tp.SkipSendingSystemChatMessages = true 605 606 admin, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 607 require.NoError(t, err) 608 609 teamName, teamID := createTeam2(tc) 610 611 token, err := CreateSeitanTokenV2(context.Background(), tc.G, 612 teamName.String(), keybase1.TeamRole_WRITER, keybase1.SeitanKeyLabel{}) 613 require.NoError(t, err) 614 615 // Create two users 616 var users [2]*kbtest.FakeUser 617 for i := range users { 618 kbtest.Logout(tc) 619 620 user, err := kbtest.CreateAndSignupFakeUser("team", tc.G) 621 require.NoError(t, err) 622 users[i] = user 623 } 624 625 timeNow := keybase1.ToTime(tc.G.Clock().Now()) 626 627 var acceptances [2]acceptedSeitanV2 628 for i, user := range users { 629 kbtest.LogoutAndLoginAs(tc, user) 630 seitanRet, err := generateAcceptanceSeitanV2(SeitanIKeyV2(token), user.GetUserVersion(), timeNow) 631 require.NoError(t, err) 632 acceptances[i] = seitanRet 633 634 if i == 0 { 635 // First user has to PostSeitan so invite is changed to ACCEPTED on 636 // the server. 637 err = postSeitanV2(tc.MetaContext(), seitanRet) 638 require.NoError(t, err) 639 } 640 } 641 642 kbtest.LogoutAndLoginAs(tc, admin) 643 644 inviteID, err := acceptances[0].inviteID.TeamInviteID() 645 require.NoError(t, err) 646 647 var seitans [2]keybase1.TeamSeitanRequest 648 for i, user := range users { 649 seitans[i] = keybase1.TeamSeitanRequest{ 650 InviteID: inviteID, 651 Uid: user.GetUID(), 652 EldestSeqno: user.EldestSeqno, 653 Akey: keybase1.SeitanAKey(acceptances[i].encoded), 654 Role: keybase1.TeamRole_WRITER, 655 UnixCTime: int64(timeNow), 656 } 657 } 658 msg := keybase1.TeamSeitanMsg{ 659 TeamID: teamID, 660 Seitans: seitans[:], 661 } 662 err = HandleTeamSeitan(context.Background(), tc.G, msg) 663 if err != nil { 664 if err, ok := errors.Unwrap(err).(libkb.AppStatusError); ok { 665 // We are expecting no error, but if there's a specific bug that we can 666 // recognize, inform about it. 667 if err.Code == int(keybase1.StatusCode_SCTeamInviteCompletionMissing) { 668 require.FailNowf(t, 669 "Got error which suggests that bad change_membership was sent to the server.", 670 "%s", err.Error()) 671 } 672 } 673 } 674 require.NoError(t, err) 675 676 // First request should have been fulfilled, so users[0] should have been 677 // added. Second request should have been ignored. 678 team, err := Load(context.TODO(), tc.G, keybase1.LoadTeamArg{ 679 Name: teamName.String(), 680 NeedAdmin: true, 681 ForceRepoll: true, 682 }) 683 require.NoError(t, err) 684 685 require.True(t, team.IsMember(context.TODO(), users[0].GetUserVersion())) 686 require.False(t, team.IsMember(context.TODO(), users[1].GetUserVersion())) 687 }