status-go/protocol/messenger_share_urls.go

523 lines
14 KiB
Go

package protocol
import (
"crypto/ecdsa"
"fmt"
"regexp"
"strings"
"github.com/golang/protobuf/proto"
"github.com/status-im/status-go/api/multiformat"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/communities"
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/protocol/requests"
"github.com/status-im/status-go/protocol/urls"
)
type CommunityURLData struct {
DisplayName string `json:"displayName"`
Description string `json:"description"`
MembersCount uint32 `json:"membersCount"`
Color string `json:"color"`
TagIndices []uint32 `json:"tagIndices"`
}
type CommunityChannelURLData struct {
Emoji string `json:"emoji"`
DisplayName string `json:"displayName"`
Description string `json:"description"`
Color string `json:"color"`
}
type ContactURLData struct {
DisplayName string `json:"displayName"`
Description string `json:"description"`
}
type URLDataResponse struct {
Community CommunityURLData `json:"community"`
Channel CommunityChannelURLData `json:"channel"`
Contact ContactURLData `json:"contact"`
}
const baseShareURL = "https://status.app"
const channelUUIDRegExp = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$"
func (m *Messenger) SerializePublicKey(compressedKey types.HexBytes) (string, error) {
rawKey, err := crypto.DecompressPubkey(compressedKey)
if err != nil {
return "", err
}
pubKey := types.EncodeHex(crypto.FromECDSAPub(rawKey))
secp256k1Code := "0xe701"
base58btc := "z"
multiCodecKey := secp256k1Code + strings.TrimPrefix(pubKey, "0x")
cpk, err := multiformat.SerializePublicKey(multiCodecKey, base58btc)
if err != nil {
return "", err
}
return cpk, nil
}
func (m *Messenger) DeserializePublicKey(compressedKey string) (types.HexBytes, error) {
rawKey, err := multiformat.DeserializePublicKey(compressedKey, "f")
if err != nil {
return nil, err
}
secp256k1Code := "fe701"
pubKeyBytes := "0x" + strings.TrimPrefix(rawKey, secp256k1Code)
pubKey, err := common.HexToPubkey(pubKeyBytes)
if err != nil {
return nil, err
}
return crypto.CompressPubkey(pubKey), nil
}
func (m *Messenger) ShareCommunityURLWithChatKey(communityID types.HexBytes) (string, error) {
shortKey, err := m.SerializePublicKey(communityID)
if err != nil {
return "", err
}
return fmt.Sprintf("%s/c#%s", baseShareURL, shortKey), nil
}
func (m *Messenger) prepareCommunityData(community *communities.Community) CommunityURLData {
return CommunityURLData{
DisplayName: community.Identity().DisplayName,
Description: community.DescriptionText(),
MembersCount: uint32(community.MembersCount()),
Color: community.Identity().GetColor(),
TagIndices: community.TagsIndices(),
}
}
func (m *Messenger) parseCommunityURLWithChatKey(urlData string) (*URLDataResponse, error) {
communityID, err := m.DeserializePublicKey(urlData)
if err != nil {
return nil, err
}
community, err := m.GetCommunityByID(communityID)
if err != nil {
return nil, err
}
if community == nil {
return nil, fmt.Errorf("community with communityID %s not found", communityID)
}
return &URLDataResponse{
Community: m.prepareCommunityData(community),
}, nil
}
func (m *Messenger) prepareEncodedRawData(rawData []byte, privateKey *ecdsa.PrivateKey) (string, string, error) {
encodedData, err := urls.EncodeDataURL(rawData)
if err != nil {
return "", "", err
}
signature, err := crypto.SignBytes([]byte(encodedData), privateKey)
if err != nil {
return "", "", err
}
encodedSignature, err := urls.EncodeDataURL(signature)
if err != nil {
return "", "", err
}
return encodedData, encodedSignature, nil
}
func (m *Messenger) prepareEncodedCommunityData(community *communities.Community) (string, string, error) {
communityProto := &protobuf.Community{
DisplayName: community.Identity().DisplayName,
Description: community.DescriptionText(),
MembersCount: uint32(community.MembersCount()),
Color: community.Identity().GetColor(),
TagIndices: community.TagsIndices(),
}
communityData, err := proto.Marshal(communityProto)
if err != nil {
return "", "", err
}
return m.prepareEncodedRawData(communityData, community.PrivateKey())
}
func (m *Messenger) ShareCommunityURLWithData(communityID types.HexBytes) (string, error) {
community, err := m.GetCommunityByID(communityID)
if err != nil {
return "", err
}
if community == nil {
return "", fmt.Errorf("community with communityID %s not found", communityID)
}
data, signature, err := m.prepareEncodedCommunityData(community)
if err != nil {
return "", err
}
return fmt.Sprintf("%s/c/%s#%s", baseShareURL, data, signature), nil
}
func (m *Messenger) verifySignature(data string, rawSignature string) (*ecdsa.PublicKey, error) {
signature, err := urls.DecodeDataURL(rawSignature)
if err != nil {
return nil, err
}
return crypto.SigToPub(crypto.Keccak256([]byte(data)), signature)
}
func (m *Messenger) parseCommunityURLWithData(data string, signature string) (*URLDataResponse, error) {
_, err := m.verifySignature(data, signature)
if err != nil {
return nil, err
}
communityData, err := urls.DecodeDataURL(data)
if err != nil {
return nil, err
}
var communityProto protobuf.Community
err = proto.Unmarshal(communityData, &communityProto)
if err != nil {
return nil, err
}
return &URLDataResponse{
Community: CommunityURLData{
DisplayName: communityProto.DisplayName,
Description: communityProto.Description,
MembersCount: communityProto.MembersCount,
Color: communityProto.Color,
TagIndices: communityProto.TagIndices,
},
}, nil
}
func (m *Messenger) ShareCommunityChannelURLWithChatKey(request *requests.CommunityChannelShareURL) (string, error) {
if err := request.Validate(); err != nil {
return "", err
}
shortKey, err := m.SerializePublicKey(request.CommunityID)
if err != nil {
return "", err
}
valid, err := regexp.MatchString(channelUUIDRegExp, request.ChannelID)
if err != nil {
return "", err
}
if !valid {
return "", fmt.Errorf("channelID should be UUID, got %s", request.ChannelID)
}
return fmt.Sprintf("%s/cc/%s#%s", baseShareURL, request.ChannelID, shortKey), nil
}
func (m *Messenger) prepareCommunityChannelData(channel *protobuf.CommunityChat) CommunityChannelURLData {
return CommunityChannelURLData{
Emoji: channel.Identity.Emoji,
DisplayName: channel.Identity.DisplayName,
Description: channel.Identity.Description,
Color: channel.Identity.Color,
}
}
func (m *Messenger) parseCommunityChannelURLWithChatKey(channelID string, publickKey string) (*URLDataResponse, error) {
valid, err := regexp.MatchString(channelUUIDRegExp, channelID)
if err != nil {
return nil, err
}
if !valid {
return nil, fmt.Errorf("channelID should be UUID, got %s", channelID)
}
communityID, err := m.DeserializePublicKey(publickKey)
if err != nil {
return nil, err
}
community, err := m.GetCommunityByID(communityID)
if err != nil {
return nil, err
}
if community == nil {
return nil, fmt.Errorf("community with communityID %s not found", communityID)
}
channel, ok := community.Chats()[channelID]
if !ok {
return nil, fmt.Errorf("channel with channelID %s not found", channelID)
}
return &URLDataResponse{
Community: m.prepareCommunityData(community),
Channel: m.prepareCommunityChannelData(channel),
}, nil
}
func (m *Messenger) prepareEncodedCommunityChannelData(community *communities.Community, channel *protobuf.CommunityChat, channelID string) (string, string, error) {
communityProto := &protobuf.Community{
DisplayName: community.Identity().DisplayName,
Description: community.DescriptionText(),
MembersCount: uint32(community.MembersCount()),
Color: community.Identity().GetColor(),
TagIndices: community.TagsIndices(),
}
channelProto := &protobuf.Channel{
DisplayName: channel.Identity.DisplayName,
Description: channel.Identity.Description,
Emoji: channel.Identity.Emoji,
Color: channel.GetIdentity().Color,
Community: communityProto,
Uuid: channelID,
}
channelData, err := proto.Marshal(channelProto)
if err != nil {
return "", "", err
}
return m.prepareEncodedRawData(channelData, community.PrivateKey())
}
func (m *Messenger) ShareCommunityChannelURLWithData(request *requests.CommunityChannelShareURL) (string, error) {
if err := request.Validate(); err != nil {
return "", err
}
valid, err := regexp.MatchString(channelUUIDRegExp, request.ChannelID)
if err != nil {
return "", err
}
if !valid {
return "nil", fmt.Errorf("channelID should be UUID, got %s", request.ChannelID)
}
community, err := m.GetCommunityByID(request.CommunityID)
if err != nil {
return "", err
}
channel := community.Chats()[request.ChannelID]
if channel == nil {
return "", fmt.Errorf("channel with channelID %s not found", request.ChannelID)
}
data, signature, err := m.prepareEncodedCommunityChannelData(community, channel, request.ChannelID)
if err != nil {
return "", err
}
return fmt.Sprintf("%s/cc/%s#%s", baseShareURL, data, signature), nil
}
func (m *Messenger) parseCommunityChannelURLWithData(data string, signature string) (*URLDataResponse, error) {
_, err := m.verifySignature(data, signature)
if err != nil {
return nil, err
}
channelData, err := urls.DecodeDataURL(data)
if err != nil {
return nil, err
}
var channelProto protobuf.Channel
err = proto.Unmarshal(channelData, &channelProto)
if err != nil {
return nil, err
}
return &URLDataResponse{
Community: CommunityURLData{
DisplayName: channelProto.Community.DisplayName,
Description: channelProto.Community.Description,
MembersCount: channelProto.Community.MembersCount,
Color: channelProto.Community.Color,
TagIndices: channelProto.Community.TagIndices,
},
Channel: CommunityChannelURLData{
Emoji: channelProto.Emoji,
DisplayName: channelProto.DisplayName,
Description: channelProto.Description,
Color: channelProto.Color,
},
}, nil
}
func (m *Messenger) ShareUserURLWithChatKey(contactID string) (string, error) {
publicKey, err := common.HexToPubkey(contactID)
if err != nil {
return "", err
}
shortKey, err := m.SerializePublicKey(crypto.CompressPubkey(publicKey))
if err != nil {
return "", err
}
return fmt.Sprintf("%s/u#%s", baseShareURL, shortKey), nil
}
func (m *Messenger) prepareContactData(contact *Contact) ContactURLData {
return ContactURLData{
DisplayName: contact.DisplayName,
}
}
func (m *Messenger) parseUserURLWithChatKey(urlData string) (*URLDataResponse, error) {
pubKeyBytes, err := m.DeserializePublicKey(urlData)
if err != nil {
return nil, err
}
pubKey, err := crypto.DecompressPubkey(pubKeyBytes)
if err != nil {
return nil, err
}
contactID := common.PubkeyToHex(pubKey)
contact, ok := m.allContacts.Load(contactID)
if !ok {
return nil, ErrContactNotFound
}
return &URLDataResponse{
Contact: m.prepareContactData(contact),
}, nil
}
func (m *Messenger) ShareUserURLWithENS(contactID string) (string, error) {
contact, ok := m.allContacts.Load(contactID)
if !ok {
return "", ErrContactNotFound
}
return fmt.Sprintf("%s/u#%s", baseShareURL, contact.EnsName), nil
}
func (m *Messenger) parseUserURLWithENS(ensName string) (*URLDataResponse, error) {
// TODO: fetch contact by ens name
return nil, fmt.Errorf("not implemented yet")
}
func (m *Messenger) prepareEncodedUserData(contact *Contact) (string, string, error) {
userProto := &protobuf.User{
DisplayName: contact.DisplayName,
}
userData, err := proto.Marshal(userProto)
if err != nil {
return "", "", err
}
return m.prepareEncodedRawData(userData, m.identity)
}
func (m *Messenger) ShareUserURLWithData(contactID string) (string, error) {
contact, ok := m.allContacts.Load(contactID)
if !ok {
return "", ErrContactNotFound
}
data, signature, err := m.prepareEncodedUserData(contact)
if err != nil {
return "", err
}
return fmt.Sprintf("%s/u/%s#%s", baseShareURL, data, signature), nil
}
func (m *Messenger) parseUserURLWithData(data string, signature string) (*URLDataResponse, error) {
_, err := m.verifySignature(data, signature)
if err != nil {
return nil, err
}
userData, err := urls.DecodeDataURL(data)
if err != nil {
return nil, err
}
var userProto protobuf.User
err = proto.Unmarshal(userData, &userProto)
if err != nil {
return nil, err
}
return &URLDataResponse{
Contact: ContactURLData{
DisplayName: userProto.DisplayName,
Description: userProto.Description,
},
}, nil
}
func (m *Messenger) ParseSharedURL(url string) (*URLDataResponse, error) {
if !strings.HasPrefix(url, baseShareURL) {
return nil, fmt.Errorf("url should start with '%s'", baseShareURL)
}
urlContents := regexp.MustCompile(`\#`).Split(strings.TrimPrefix(url, baseShareURL+"/"), 2)
if len(urlContents) != 2 {
return nil, fmt.Errorf("url should contain at least one `#` separator")
}
if urlContents[0] == "c" {
return m.parseCommunityURLWithChatKey(urlContents[1])
}
if strings.HasPrefix(urlContents[0], "c/") {
return m.parseCommunityURLWithData(strings.TrimPrefix(urlContents[0], "c/"), urlContents[1])
}
if strings.HasPrefix(urlContents[0], "cc/") {
first := strings.TrimPrefix(urlContents[0], "cc/")
isChannel, err := regexp.MatchString(channelUUIDRegExp, first)
if err != nil {
return nil, err
}
if isChannel {
return m.parseCommunityChannelURLWithChatKey(first, urlContents[1])
}
return m.parseCommunityChannelURLWithData(first, urlContents[1])
}
if urlContents[0] == "u" {
if strings.HasPrefix(urlContents[1], "zQ3sh") {
return m.parseUserURLWithChatKey(urlContents[1])
}
return m.parseUserURLWithENS(urlContents[1])
}
if strings.HasPrefix(urlContents[0], "u/") {
return m.parseUserURLWithData(strings.TrimPrefix(urlContents[0], "u/"), urlContents[1])
}
return nil, fmt.Errorf("unhandled shared url: %s", url)
}