-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add the slack platform support (#71)
- Loading branch information
Showing
6 changed files
with
285 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
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 { | ||
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)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} |