github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/teams/seitan_invite_helper.go (about) 1 package teams 2 3 import ( 4 "errors" 5 "fmt" 6 7 "github.com/keybase/client/go/libkb" 8 "github.com/keybase/client/go/protocol/keybase1" 9 ) 10 11 func ParseAndAcceptSeitanToken(mctx libkb.MetaContext, ui keybase1.TeamsUiInterface, 12 tok string) (wasSeitan bool, err error) { 13 14 seitanVersion, err := DeriveSeitanVersionFromToken(tok) 15 if err != nil { 16 return false, err 17 } 18 switch seitanVersion { 19 case SeitanVersion1: 20 wasSeitan, err = parseAndAcceptSeitanTokenV1(mctx, tok) 21 case SeitanVersion2: 22 wasSeitan, err = parseAndAcceptSeitanTokenV2(mctx, tok) 23 case SeitanVersionInvitelink: 24 wasSeitan, err = parseAndAcceptSeitanTokenInvitelink(mctx, ui, tok) 25 default: 26 wasSeitan = false 27 err = fmt.Errorf("Unexpected SeitanVersion %d", seitanVersion) 28 } 29 return wasSeitan, err 30 } 31 32 // Seitan V1: 33 34 type acceptedSeitanV1 struct { 35 unixNow int64 36 inviteID SCTeamInviteID 37 akey SeitanAKey 38 encoded string // base64 encoded akey 39 } 40 41 func generateAcceptanceSeitanV1(ikey SeitanIKey, uv keybase1.UserVersion, unixNow int64) (ret acceptedSeitanV1, err error) { 42 sikey, err := ikey.GenerateSIKey() 43 if err != nil { 44 return ret, err 45 } 46 47 inviteID, err := sikey.GenerateTeamInviteID() 48 if err != nil { 49 return ret, err 50 } 51 52 akey, encoded, err := sikey.GenerateAcceptanceKey(uv.Uid, uv.EldestSeqno, unixNow) 53 if err != nil { 54 return ret, err 55 } 56 57 return acceptedSeitanV1{ 58 unixNow: unixNow, 59 inviteID: inviteID, 60 akey: akey, 61 encoded: encoded, 62 }, nil 63 } 64 65 func postSeitanV1(mctx libkb.MetaContext, acceptedSeitan acceptedSeitanV1) error { 66 arg := apiArg("team/seitan") 67 arg.Args.Add("akey", libkb.S{Val: acceptedSeitan.encoded}) 68 arg.Args.Add("now", libkb.I64{Val: acceptedSeitan.unixNow}) 69 arg.Args.Add("invite_id", libkb.S{Val: string(acceptedSeitan.inviteID)}) 70 _, err := mctx.G().API.Post(mctx, arg) 71 return err 72 } 73 74 func parseAndAcceptSeitanTokenV1(mctx libkb.MetaContext, tok string) (wasSeitan bool, err error) { 75 seitan, err := ParseIKeyFromString(tok) 76 if err != nil { 77 mctx.Debug("ParseIKeyFromString error: %s", err) 78 mctx.Debug("returning TeamInviteBadToken instead") 79 return false, libkb.TeamInviteBadTokenError{} 80 } 81 uv := mctx.CurrentUserVersion() 82 unixNow := mctx.G().Clock().Now().Unix() 83 acpt, err := generateAcceptanceSeitanV1(seitan, uv, unixNow) 84 if err != nil { 85 return true, err 86 } 87 err = postSeitanV1(mctx, acpt) 88 return true, err 89 } 90 91 // Seitan V2: 92 93 type acceptedSeitanV2 struct { 94 now keybase1.Time 95 inviteID SCTeamInviteID 96 sig SeitanSig 97 encoded string // base64 encoded sig 98 } 99 100 func generateAcceptanceSeitanV2(ikey SeitanIKeyV2, uv keybase1.UserVersion, timeNow keybase1.Time) (ret acceptedSeitanV2, err error) { 101 sikey, err := ikey.GenerateSIKey() 102 if err != nil { 103 return ret, err 104 } 105 106 inviteID, err := sikey.GenerateTeamInviteID() 107 if err != nil { 108 return ret, err 109 } 110 111 sig, encoded, err := sikey.GenerateSignature(uv.Uid, uv.EldestSeqno, inviteID, timeNow) 112 if err != nil { 113 return ret, err 114 } 115 116 return acceptedSeitanV2{ 117 now: timeNow, 118 inviteID: inviteID, 119 sig: sig, 120 encoded: encoded, 121 }, nil 122 } 123 124 func postSeitanV2(mctx libkb.MetaContext, acceptedSeitan acceptedSeitanV2) error { 125 arg := apiArg("team/seitan_v2") 126 arg.Args.Add("sig", libkb.S{Val: acceptedSeitan.encoded}) 127 arg.Args.Add("now", libkb.HTTPTime{Val: acceptedSeitan.now}) 128 arg.Args.Add("invite_id", libkb.S{Val: string(acceptedSeitan.inviteID)}) 129 _, err := mctx.G().API.Post(mctx, arg) 130 return err 131 } 132 133 func parseAndAcceptSeitanTokenV2(mctx libkb.MetaContext, tok string) (wasSeitan bool, err error) { 134 seitan, err := ParseIKeyV2FromString(tok) 135 if err != nil { 136 mctx.Debug("ParseIKeyV2FromString error: %s", err) 137 mctx.Debug("returning TeamInviteBadToken instead") 138 return false, libkb.TeamInviteBadTokenError{} 139 } 140 uv := mctx.CurrentUserVersion() 141 timeNow := keybase1.ToTime(mctx.G().Clock().Now()) 142 acpt, err := generateAcceptanceSeitanV2(seitan, uv, timeNow) 143 if err != nil { 144 return true, nil 145 } 146 err = postSeitanV2(mctx, acpt) 147 return true, err 148 149 } 150 151 // Seitan V3 (Seitan Invite Link): 152 153 type acceptedSeitanInviteLink struct { 154 unixNow int64 155 inviteID SCTeamInviteID 156 akey SeitanAKey 157 encoded string // base64 encoded akey 158 } 159 160 func generateAcceptanceSeitanInviteLink(ikey keybase1.SeitanIKeyInvitelink, uv keybase1.UserVersion, unixNow int64) (ret acceptedSeitanInviteLink, err error) { 161 sikey, err := GenerateSIKeyInvitelink(ikey) 162 if err != nil { 163 return ret, err 164 } 165 166 inviteID, err := sikey.GenerateTeamInviteID() 167 if err != nil { 168 return ret, err 169 } 170 171 akey, encoded, err := GenerateSeitanInvitelinkAcceptanceKey(sikey[:], uv.Uid, uv.EldestSeqno, unixNow) 172 if err != nil { 173 return ret, err 174 } 175 176 return acceptedSeitanInviteLink{ 177 unixNow: unixNow, 178 inviteID: inviteID, 179 akey: akey, 180 encoded: encoded, 181 }, nil 182 } 183 184 func postSeitanInviteLink(mctx libkb.MetaContext, acceptedSeitan acceptedSeitanInviteLink) error { 185 arg := apiArg("team/seitan_invitelink") 186 arg.Args.Add("akey", libkb.S{Val: acceptedSeitan.encoded}) 187 arg.Args.Add("unix_timestamp", libkb.I64{Val: acceptedSeitan.unixNow}) 188 arg.Args.Add("invite_id", libkb.S{Val: string(acceptedSeitan.inviteID)}) 189 _, err := mctx.G().API.Post(mctx, arg) 190 return err 191 } 192 193 // presentInviteLinkInUI calls the TeamsUI ConfirmInviteLinkAccept which should 194 // present a modal or CLI prompt for the user, where they can confirm or 195 // decline accepting the invite. 196 func presentInviteLinkInUI(mctx libkb.MetaContext, ui keybase1.TeamsUiInterface, inviteID SCTeamInviteID) error { 197 teamInviteID, err := inviteID.TeamInviteID() 198 if err != nil { 199 return err 200 } 201 details, err := GetInviteLinkDetails(mctx, teamInviteID) 202 if err != nil { 203 mctx.Debug("failed to get invite details for %v: %v", inviteID, err) 204 return err 205 } 206 if details.IsMember { 207 return libkb.AppStatusError{ 208 Code: libkb.SCTeamMemberExists, 209 Name: "TEAM_MEMBER_EXISTS", 210 Desc: fmt.Sprintf("You're already a member of %s!", details.TeamName), 211 } 212 } 213 accepted, err := ui.ConfirmInviteLinkAccept(mctx.Ctx(), keybase1.ConfirmInviteLinkAcceptArg{Details: details}) 214 if err != nil { 215 mctx.Debug("failed to confirm invite link %v: %v", inviteID, err) 216 return err 217 } 218 if !accepted { 219 mctx.Debug("invite link %v not accepted", inviteID) 220 return errors.New("invite acceptance not confirmed") 221 } 222 return nil 223 } 224 225 func parseAndAcceptSeitanTokenInvitelink(mctx libkb.MetaContext, ui keybase1.TeamsUiInterface, 226 tok string) (wasSeitan bool, err error) { 227 228 seitan, err := ParseIKeyInvitelinkFromString(tok) 229 if err != nil { 230 mctx.Debug("ParseIKeyInvitelinkFromString error: %s", err) 231 mctx.Debug("returning TeamInviteBadToken instead") 232 return false, libkb.TeamInviteBadTokenError{} 233 } 234 uv := mctx.CurrentUserVersion() 235 unixNow := mctx.G().Clock().Now().Unix() 236 acpt, err := generateAcceptanceSeitanInviteLink(seitan, uv, unixNow) 237 if err != nil { 238 return true, err 239 } 240 241 if ui != nil { 242 err = presentInviteLinkInUI(mctx, ui, acpt.inviteID) 243 if err != nil { 244 return true, err 245 } 246 } else { 247 mctx.Debug("`ui` is nil, skipping presentInviteLinkInUI") 248 } 249 250 err = postSeitanInviteLink(mctx, acpt) 251 return true, err 252 }