Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the slack platform support #71

Merged
merged 23 commits into from
Feb 28, 2023
6 changes: 6 additions & 0 deletions config/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type PlatformConfig struct {
Telegram TelegramPlatformConfig `json:"telegram"`
Ethereum EthereumPlatformConfig `json:"ethereum"`
Discord DiscordPlatformConfig `json:"discord"`
Slack SlackPlatformConfig `json:"slack"`
}

type ArweaveConfig struct {
Expand All @@ -57,6 +58,11 @@ type TelegramPlatformConfig struct {
PublicChannelName string `json:"public_channel_name"`
}

type SlackPlatformConfig struct {
ApiToken string `json:"api_token"`
PublicChannelID string `json:"public_channel_id"`
}

type EthereumPlatformConfig struct {
RPCServer string `json:"rpc_server"`
}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ require (
github.com/segmentio/asm v1.2.0 // indirect
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/slack-go/slack v0.12.1 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.4.1 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
Expand Down Expand Up @@ -651,6 +652,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/slack-go/slack v0.12.1 h1:X97b9g2hnITDtNsNe5GkGx6O2/Sz/uC20ejRZN6QxOw=
github.com/slack-go/slack v0.12.1/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
Expand Down
2 changes: 2 additions & 0 deletions types/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var Platforms = struct {
ENS Platform
Steam Platform
ActivityPub Platform
Slack Platform
}{
Github: "github",
NextID: "nextid",
Expand All @@ -33,4 +34,5 @@ var Platforms = struct {
ENS: "ens",
Steam: "steam",
ActivityPub: "activitypub",
Slack: "slack",
}
197 changes: 197 additions & 0 deletions validator/slack/slack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package slack

import (
"bufio"
"encoding/json"
"fmt"
"net/url"
"regexp"
"strconv"
"strings"

"github.com/nextdotid/proof_server/config"
types "github.com/nextdotid/proof_server/types"
util "github.com/nextdotid/proof_server/util"
mycrypto "github.com/nextdotid/proof_server/util/crypto"
"github.com/sirupsen/logrus"
"github.com/slack-go/slack"
slackClient "github.com/slack-go/slack"
"golang.org/x/xerrors"

validator "github.com/nextdotid/proof_server/validator"
)

// Slack represents the validator for Slack platform
type Slack struct {
*validator.Base
}

const (
matchTemplate = "^Sig: (.*)$"
)

var (
client *slackClient.Client
l = logrus.WithFields(logrus.Fields{"module": "validator", "validator": "slack"})
re = regexp.MustCompile(matchTemplate)
postStruct = map[string]string{
"default": "🎭 Verifying my Slack ID @%s for @NextDotID.\nSig: %%SIG_BASE64%%\n\nPowered by Next.ID - Connect All Digital Identities.\n",
"en_US": "🎭 Verifying my Slack ID @%s for @NextDotID.\nSig: %%SIG_BASE64%%\n\nPowered by Next.ID - Connect All Digital Identities.\n",
"zh_CN": "🎭 正在通过 @NextDotID 验证我的 Slack 帐号 @%s 。\nSig: %%SIG_BASE64%%\n\n由 Next.ID 支持 - 连接全域数字身份。\n",
}
)

// Init initializes the Slack validator
func Init() {
initClient()
if validator.PlatformFactories == nil {
validator.PlatformFactories = make(map[types.Platform]func(*validator.Base) validator.IValidator)
}
validator.PlatformFactories[types.Platforms.Slack] = func(base *validator.Base) validator.IValidator {
slack := &Slack{base}
return slack
}
}

// GeneratePostPayload generates the post payload for Slack
func (s *Slack) GeneratePostPayload() (post map[string]string) {
post = make(map[string]string)
for langCode, template := range postStruct {
post[langCode] = fmt.Sprintf(template, s.Identity)
}
return post
}

// GenerateSignPayload generates the signature payload for Slack
func (slack *Slack) GenerateSignPayload() (payload string) {
payloadStruct := validator.H{
"action": string(slack.Action),
"identity": slack.Identity,
"platform": "slack",
"created_at": util.TimeToTimestampString(slack.CreatedAt),
"uuid": slack.Uuid.String(),
}
if slack.Previous != "" {
payloadStruct["prev"] = slack.Previous
}

payloadBytes, err := json.Marshal(payloadStruct)
if err != nil {
l.Warnf("Error when marshaling struct: %s", err.Error())
return ""
}

return string(payloadBytes)
}

func (slack *Slack) Validate() (err error) {
initClient()
slack.Identity = strings.ToLower(slack.Identity)
slack.SignaturePayload = slack.GenerateSignPayload()

if slack.Action == types.Actions.Delete {
return mycrypto.ValidatePersonalSignature(slack.SignaturePayload, slack.Signature, slack.Pubkey)
}

u, err := url.Parse(slack.ProofLocation)
ash0521 marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return xerrors.Errorf("Error when parsing slack proof location: %v", err)
}
msgPath := strings.Trim(u.Path, "/")
parts := strings.Split(msgPath, "/")
if len(parts) != 2 {
return xerrors.Errorf("Error: malformatted slack proof location: %v", slack.ProofLocation)
}
channelID := parts[0]
messageID, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return xerrors.Errorf("Error when parsing slack message ID %s: %s", slack.ProofLocation, err.Error())
}
const (
maxPages = 10 // maximum number of pages to fetch
historyPageSize = 1000 // Slack API max limit per page
)
pageCount := 0
var foundMsg *slackClient.Message
var latestTs string

for {
if pageCount >= maxPages {
return xerrors.Errorf("Reached max number of pages (%d) while searching for message with ID %d in conversation history", maxPages, messageID)
}

// Get conversation history
history, err := client.GetConversationHistory(&slackClient.GetConversationHistoryParameters{
ChannelID: channelID,
Latest: latestTs,
Inclusive: true,
Oldest: "",
Limit: historyPageSize,
})
if err != nil {
return xerrors.Errorf("Error getting the conversation history from slack: %w", err)
}

for _, msg := range history.Messages {
if msg.ClientMsgID == strconv.FormatInt(messageID, 10) {
foundMsg = &msg
break
}
}

if foundMsg != nil {
break
}

if !history.HasMore {
ash0521 marked this conversation as resolved.
Show resolved Hide resolved
return xerrors.Errorf("Could not find message with ID %d in conversation history", messageID)
}

latestTs = history.Messages[len(history.Messages)-1].Timestamp
pageCount++
}

userInt, err := strconv.ParseInt(foundMsg.User, 10, 64)
if err != nil {
return xerrors.Errorf("failed to parse user ID as int64: %v", err)
}
userID := strconv.FormatInt(userInt, 10)
if !strings.EqualFold(userID, slack.Identity) {
return xerrors.Errorf("slack userID mismatch: expect %s - actual %s", slack.Identity, userID)
}

slack.Text = foundMsg.Text
slack.AltID = userID
slack.Identity = userID

return slack.validateText()
}

func (slack *Slack) validateText() (err error) {
scanner := bufio.NewScanner(strings.NewReader(slack.Text))
for scanner.Scan() {
matched := re.FindStringSubmatch(scanner.Text())
if len(matched) < 2 {
continue // Search for next line
}

sigBase64 := matched[1]
sigBytes, err := util.DecodeString(sigBase64)
if err != nil {
return xerrors.Errorf("Error when decoding signature %s: %s", sigBase64, err.Error())
}
slack.Signature = sigBytes
return mycrypto.ValidatePersonalSignature(slack.SignaturePayload, sigBytes, slack.Pubkey)
}
return xerrors.Errorf("Signature not found in the slack message.")
}

func initClient() {
if client != nil {
return
}
client = slack.New(config.C.Platform.Slack.ApiToken)
if _, err := client.AuthTest(); err != nil {
panic(fmt.Errorf("failed to authenticate the slack: %v", err))
}
}
76 changes: 76 additions & 0 deletions validator/slack/slack_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package slack

import (
"strconv"
"testing"

"github.com/google/uuid"
"github.com/nextdotid/proof_server/config"
"github.com/nextdotid/proof_server/types"
"github.com/nextdotid/proof_server/util"
"github.com/nextdotid/proof_server/util/crypto"
"github.com/nextdotid/proof_server/validator"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)

func before_each(t *testing.T) {
logrus.SetLevel(logrus.DebugLevel)
config.Init("../../config/config.test.json")
}

func generate() Slack {
pubkey, _ := crypto.StringToPubkey("0x4ec73e36f64ea6e2aa28c101dcae56203e02bd56b4b08c7848b5e791c7bfb9ca2b30f657bd822756533731e201faf57a0aaf6af36bd51f921f7132c9830c6fdf")
created_at, _ := util.TimestampStringToTime("1677339048")
uuid := uuid.MustParse("5032b8b3-d91d-434e-be3f-f172267e4006")

return Slack{
Base: &validator.Base{
Platform: types.Platforms.Slack,
Previous: "",
Action: types.Actions.Create,
Pubkey: pubkey,
Identity: "ashfaqur",
ProofLocation: "https://ashfaqur.slack.com/archives/C04Q3P6H7TK/p1677499644698189",
CreatedAt: created_at,
Uuid: uuid,
},
}
}

func Test_GeneratePostPayload(t *testing.T) {
t.Run("success", func(t *testing.T) {
before_each(t)

slack := generate()
post := slack.GeneratePostPayload()
post_default, ok := post["default"]
require.True(t, ok)
require.Contains(t, post_default, "Verifying my Slack ID")
require.Contains(t, post_default, slack.Identity)
require.Contains(t, post_default, slack.Uuid.String())
require.Contains(t, post_default, "%SIG_BASE64%")
})
}

func Test_GenerateSignPayload(t *testing.T) {
t.Run("success", func(t *testing.T) {
before_each(t)

slack := generate()
payload := slack.GenerateSignPayload()
require.Contains(t, payload, slack.Uuid.String())
require.Contains(t, payload, strconv.FormatInt(slack.CreatedAt.Unix(), 10))
require.Contains(t, payload, slack.Identity)
})
}

func Test_Validate(t *testing.T) {
t.Run("success", func(t *testing.T) {
before_each(t)

slack := generate()
require.NoError(t, slack.Validate())
require.Equal(t, "U04Q3NRDWHX", slack.AltID)
})
}