github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/teams/seitan_invitelink.go (about) 1 package teams 2 3 import ( 4 "context" 5 cryptorand "crypto/rand" 6 "fmt" 7 "regexp" 8 "strings" 9 10 "github.com/keybase/client/go/libkb" 11 "github.com/keybase/client/go/msgpack" 12 keybase1 "github.com/keybase/client/go/protocol/keybase1" 13 ) 14 15 // documented in go/teams/seitan.go 16 const SeitanEncodedIKeyInvitelinkLength = 28 17 const seitanEncodedIKeyInvitelinkPlusOffset = 7 18 19 func GenerateSeitanIKeyInvitelink() (ikey keybase1.SeitanIKeyInvitelink, err error) { 20 str, err := generateIKey(SeitanEncodedIKeyInvitelinkLength, seitanEncodedIKeyInvitelinkPlusOffset) 21 if err != nil { 22 return ikey, err 23 } 24 return keybase1.SeitanIKeyInvitelink(str), nil 25 } 26 27 func ParseIKeyInvitelinkFromString(token string) (ikey keybase1.SeitanIKeyInvitelink, err error) { 28 if len(token) != SeitanEncodedIKeyInvitelinkLength { 29 return ikey, fmt.Errorf("invalid token length: expected %d characters, got %d", SeitanEncodedIKeyLength, len(token)) 30 } 31 32 return keybase1.SeitanIKeyInvitelink(strings.ToLower(token)), nil 33 } 34 35 type SeitanSIKeyInvitelink [SeitanScryptKeylen]byte 36 37 func GenerateSIKeyInvitelink(ikey keybase1.SeitanIKeyInvitelink) (sikey SeitanSIKeyInvitelink, err error) { 38 buf, err := generateSIKey(ikey.String()) 39 if err != nil { 40 return sikey, err 41 } 42 copy(sikey[:], buf) 43 return sikey, nil 44 } 45 46 func (sikey SeitanSIKeyInvitelink) generateMsgpackPayload() ([]byte, error) { 47 return msgpack.Encode(NewSeitanInviteIDPayload(SeitanVersionInvitelink)) 48 } 49 50 func (sikey SeitanSIKeyInvitelink) GenerateTeamInviteID() (id SCTeamInviteID, err error) { 51 payload, err := sikey.generateMsgpackPayload() 52 if err != nil { 53 return id, err 54 } 55 return generateTeamInviteID(sikey[:], payload) 56 } 57 58 func (sikey SeitanSIKeyInvitelink) GenerateShortTeamInviteID() (id SCTeamInviteIDShort, err error) { 59 payload, err := sikey.generateMsgpackPayload() 60 if err != nil { 61 return id, err 62 } 63 return generateShortTeamInviteID(sikey[:], payload) 64 } 65 66 func generatePackedEncryptedKeyWithSecretKeyInvitelink(ikey keybase1.SeitanIKeyInvitelink, 67 secretKey keybase1.Bytes32, gen keybase1.PerTeamKeyGeneration, nonce keybase1.BoxNonce, 68 label keybase1.SeitanKeyLabel) (pkey SeitanPKey, encoded string, err error) { 69 var keyAndLabel keybase1.SeitanKeyAndLabelInvitelink 70 keyAndLabel.I = ikey 71 keyAndLabel.L = label 72 73 packedKeyAndLabel, err := msgpack.Encode(keybase1.NewSeitanKeyAndLabelWithInvitelink(keyAndLabel)) 74 if err != nil { 75 return pkey, encoded, err 76 } 77 return packAndEncryptKeyWithSecretKey(secretKey, gen, nonce, packedKeyAndLabel, SeitanVersionInvitelink) 78 } 79 80 func GeneratePackedEncryptedKeyInvitelink(ctx context.Context, ikey keybase1.SeitanIKeyInvitelink, 81 team *Team, label keybase1.SeitanKeyLabel) (pkey SeitanPKey, encoded string, err error) { 82 appKey, err := team.SeitanInviteTokenKeyLatest(ctx) 83 if err != nil { 84 return pkey, encoded, err 85 } 86 87 var nonce keybase1.BoxNonce 88 if _, err = cryptorand.Read(nonce[:]); err != nil { 89 return pkey, encoded, err 90 } 91 92 return generatePackedEncryptedKeyWithSecretKeyInvitelink(ikey, appKey.Key, 93 appKey.KeyGeneration, nonce, label) 94 } 95 96 func GenerateSeitanInvitelinkAcceptanceKey(sikey []byte, uid keybase1.UID, eldestSeqno keybase1.Seqno, unixTimestampSeconds int64) (akey SeitanAKey, encoded string, err error) { 97 type AKeyPayload struct { 98 Stage string `codec:"stage" json:"stage"` 99 UID keybase1.UID `codec:"uid" json:"uid"` 100 EldestSeqno keybase1.Seqno `codec:"eldest_seqno" json:"eldest_seqno"` 101 CTime int64 `codec:"ctime" json:"ctime"` 102 Version SeitanVersion `codec:"version" json:"version"` 103 } 104 105 akeyPayload, err := msgpack.Encode(AKeyPayload{ 106 Stage: "accept", 107 UID: uid, 108 EldestSeqno: eldestSeqno, 109 CTime: unixTimestampSeconds, 110 Version: SeitanVersionInvitelink, 111 }) 112 if err != nil { 113 return akey, encoded, err 114 } 115 return generateAcceptanceKey(akeyPayload, sikey) 116 } 117 118 // bound from SeitanEncodedIKeyInvitelinkLength 119 var invitelinkIKeyRxx = regexp.MustCompile(`/i/t/([a-zA-Z0-9]{16,28})#([a-z0-9+]{16,28})`) 120 121 func generateInvitelinkURLPrefix(mctx libkb.MetaContext) string { 122 // NOTE: if you change this url, change invitelinkIKeyRxx too! 123 return fmt.Sprintf("%s/i/t/", libkb.SiteURILookup[mctx.G().Env.GetRunMode()]) 124 } 125 126 func GenerateInvitelinkURL( 127 mctx libkb.MetaContext, 128 ikey keybase1.SeitanIKeyInvitelink, 129 id SCTeamInviteIDShort, 130 ) string { 131 return fmt.Sprintf("%s%s#%s", generateInvitelinkURLPrefix(mctx), id, ikey) 132 } 133 134 type TeamInviteLinkDetails struct { 135 libkb.AppStatusEmbed 136 InviterResetOrDel bool `json:"inviter_reset_or_del"` 137 InviterUID keybase1.UID `json:"inviter_uid"` 138 InviterUsername string `json:"inviter_username"` 139 IsMember bool `json:"is_member"` 140 TeamAvatars map[keybase1.AvatarFormat]keybase1.AvatarUrl `json:"team_avatars"` 141 TeamDescription string `json:"team_desc"` 142 TeamID keybase1.TeamID `json:"team_id"` 143 TeamIsOpen bool `json:"team_is_open"` 144 TeamName string `json:"team_name"` 145 TeamNumMembers int `json:"team_num_members"` 146 } 147 148 func GetInviteLinkDetails(mctx libkb.MetaContext, inviteID keybase1.TeamInviteID) (info keybase1.InviteLinkDetails, err error) { 149 arg := libkb.APIArg{ 150 Endpoint: "team/get_invite_details", 151 SessionType: libkb.APISessionTypeOPTIONAL, 152 Args: libkb.HTTPArgs{ 153 "invite_id": libkb.S{Val: string(inviteID)}, 154 }, 155 } 156 157 var resp TeamInviteLinkDetails 158 if err = mctx.G().API.GetDecode(mctx, arg, &resp); err != nil { 159 // The server knows about invite IDs (but not keys), so it is fine to 160 // put this in the log. 161 mctx.Debug("GetInviteLinkDetails: failed to get team invite details for %v: %s", inviteID, err) 162 return info, err 163 } 164 165 teamName, err := keybase1.TeamNameFromString(resp.TeamName) 166 if err != nil { 167 return info, err 168 } 169 170 return keybase1.InviteLinkDetails{ 171 InviteID: inviteID, 172 InviterResetOrDel: resp.InviterResetOrDel, 173 InviterUID: resp.InviterUID, 174 InviterUsername: resp.InviterUsername, 175 IsMember: resp.IsMember, 176 TeamAvatars: resp.TeamAvatars, 177 TeamDesc: resp.TeamDescription, 178 TeamID: resp.TeamID, 179 TeamIsOpen: resp.TeamIsOpen, 180 TeamName: teamName, 181 TeamNumMembers: resp.TeamNumMembers, 182 }, nil 183 }