From 1aa3e2812ad30846287f1dc04c47ecd2ff238ebf Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Thu, 23 May 2019 10:47:20 +0200 Subject: [PATCH] Add support for partitioned topic --- Makefile | 2 +- api/backend.go | 118 ++--- lib/library.go | 76 +-- mobile/status.go | 131 ++--- params/config.go | 2 - services/shhext/api.go | 156 +----- services/shhext/chat/db/migrations/bindata.go | 42 +- services/shhext/chat/encryption.go | 146 ++---- .../chat/encryption_multi_device_test.go | 105 ++-- services/shhext/chat/encryption_test.go | 266 ++++++---- .../shhext/chat/multidevice/persistence.go | 12 + services/shhext/chat/multidevice/service.go | 93 ++++ .../chat/multidevice/sql_lite_persistence.go | 168 ++++++ .../multidevice/sql_lite_persistence_test.go | 241 +++++++++ services/shhext/chat/persistence.go | 28 +- .../chat/{ => protobuf}/encryption.pb.go | 96 ++-- .../chat/{ => protobuf}/encryption.proto | 2 +- services/shhext/chat/protocol.go | 197 ++++--- services/shhext/chat/protocol_test.go | 91 +++- .../{topic => sharedsecret}/persistence.go | 16 +- .../chat/{topic => sharedsecret}/service.go | 21 +- .../{topic => sharedsecret}/service_test.go | 10 +- services/shhext/chat/sql_lite_persistence.go | 203 ++------ .../shhext/chat/sql_lite_persistence_test.go | 232 +-------- services/shhext/chat/whisper.go | 64 --- services/shhext/chat/whisper_test.go | 56 -- services/shhext/chat/x3dh.go | 58 +-- services/shhext/chat/x3dh_test.go | 52 +- services/shhext/filter/service.go | 487 +++++++++++------- services/shhext/filter/service_test.go | 100 +++- services/shhext/publisher/service.go | 446 ++++++++++++++++ services/shhext/publisher/signal.go | 20 + services/shhext/service.go | 198 +------ services/shhext/whisperutils/whisper.go | 23 + signal/events_shhext.go | 17 +- .../1558084410_add_secret.down.sql | 2 + ...ic.up.sql => 1558084410_add_secret.up.sql} | 6 +- .../1558084410_add_topic.down.sql | 2 - .../go-ethereum/whisper/whisperv6/filter.go | 263 ++++++++++ .../status-im/whisper/whisperv6/filter.go | 2 + 40 files changed, 2459 insertions(+), 1791 deletions(-) create mode 100644 services/shhext/chat/multidevice/persistence.go create mode 100644 services/shhext/chat/multidevice/service.go create mode 100644 services/shhext/chat/multidevice/sql_lite_persistence.go create mode 100644 services/shhext/chat/multidevice/sql_lite_persistence_test.go rename services/shhext/chat/{ => protobuf}/encryption.pb.go (78%) rename services/shhext/chat/{ => protobuf}/encryption.proto (98%) rename services/shhext/chat/{topic => sharedsecret}/persistence.go (85%) rename services/shhext/chat/{topic => sharedsecret}/service.go (80%) rename services/shhext/chat/{topic => sharedsecret}/service_test.go (94%) delete mode 100644 services/shhext/chat/whisper.go delete mode 100644 services/shhext/chat/whisper_test.go create mode 100644 services/shhext/publisher/service.go create mode 100644 services/shhext/publisher/signal.go create mode 100644 services/shhext/whisperutils/whisper.go create mode 100644 static/chat_db_migrations/1558084410_add_secret.down.sql rename static/chat_db_migrations/{1558084410_add_topic.up.sql => 1558084410_add_secret.up.sql} (60%) delete mode 100644 static/chat_db_migrations/1558084410_add_topic.down.sql create mode 100644 vendor/github.com/ethereum/go-ethereum/whisper/whisperv6/filter.go diff --git a/Makefile b/Makefile index 9b1361023..1d6c8f6b3 100644 --- a/Makefile +++ b/Makefile @@ -172,7 +172,7 @@ setup: setup-build setup-dev tidy ##@other Prepare project for development and b generate: ##@other Regenerate assets and other auto-generated stuff go generate ./static ./static/chat_db_migrations ./static/mailserver_db_migrations ./t - $(shell cd ./services/shhext/chat && exec protoc --go_out=. ./*.proto) + $(shell cd ./services/shhext/chat/protobuf && exec protoc --go_out=. ./*.proto) prepare-release: clean-release mkdir -p $(RELEASE_DIR) diff --git a/api/backend.go b/api/backend.go index 4a65a9011..5af3dab41 100644 --- a/api/backend.go +++ b/api/backend.go @@ -2,7 +2,6 @@ package api import ( "context" - "encoding/hex" "errors" "fmt" "math/big" @@ -26,8 +25,8 @@ import ( "github.com/status-im/status-go/rpc" "github.com/status-im/status-go/services/personal" "github.com/status-im/status-go/services/rpcfilters" - "github.com/status-im/status-go/services/shhext/chat" "github.com/status-im/status-go/services/shhext/chat/crypto" + "github.com/status-im/status-go/services/shhext/filter" "github.com/status-im/status-go/services/subscriptions" "github.com/status-im/status-go/services/typeddata" "github.com/status-im/status-go/signal" @@ -645,91 +644,6 @@ func appendIf(condition bool, services []gethnode.ServiceConstructor, service ge return append(services, service) } -// CreateContactCode create or return the latest contact code -func (b *StatusBackend) CreateContactCode() (string, error) { - selectedChatAccount, err := b.AccountManager().SelectedChatAccount() - if err != nil { - return "", err - } - - st, err := b.statusNode.ShhExtService() - if err != nil { - return "", err - } - - bundle, err := st.GetBundle(selectedChatAccount.AccountKey.PrivateKey) - if err != nil { - return "", err - } - - return bundle.ToBase64() -} - -// GetContactCode return the latest contact code -func (b *StatusBackend) GetContactCode(identity string) (string, error) { - st, err := b.statusNode.ShhExtService() - if err != nil { - return "", err - } - - publicKeyBytes, err := hex.DecodeString(identity) - if err != nil { - return "", err - } - - publicKey, err := ethcrypto.UnmarshalPubkey(publicKeyBytes) - if err != nil { - return "", err - } - - bundle, err := st.GetPublicBundle(publicKey) - if err != nil { - return "", err - } - - if bundle == nil { - return "", nil - } - - return bundle.ToBase64() -} - -// ProcessContactCode process and adds the someone else's bundle -func (b *StatusBackend) ProcessContactCode(contactCode string) error { - selectedChatAccount, err := b.AccountManager().SelectedChatAccount() - if err != nil { - return err - } - - st, err := b.statusNode.ShhExtService() - if err != nil { - return err - } - - bundle, err := chat.FromBase64(contactCode) - if err != nil { - b.log.Error("error decoding base64", "err", err) - return err - } - - if _, err := st.ProcessPublicBundle(selectedChatAccount.AccountKey.PrivateKey, bundle); err != nil { - b.log.Error("error adding bundle", "err", err) - return err - } - - return nil -} - -// ExtractIdentityFromContactCode extract the identity of the user generating the contact code -func (b *StatusBackend) ExtractIdentityFromContactCode(contactCode string) (string, error) { - bundle, err := chat.FromBase64(contactCode) - if err != nil { - return "", err - } - - return chat.ExtractIdentity(bundle) -} - // ExtractGroupMembershipSignatures extract signatures from tuples of content/signature func (b *StatusBackend) ExtractGroupMembershipSignatures(signaturePairs [][2]string) ([]string, error) { return crypto.ExtractSignatures(signaturePairs) @@ -745,6 +659,36 @@ func (b *StatusBackend) SignGroupMembership(content string) (string, error) { return crypto.Sign(content, selectedChatAccount.AccountKey.PrivateKey) } +// LoadFilters loads filter on sshext +func (b *StatusBackend) LoadFilters(chats []*filter.Chat) ([]*filter.Chat, error) { + st, err := b.statusNode.ShhExtService() + if err != nil { + return nil, err + } + + return st.LoadFilters(chats) +} + +// LoadFilter loads filter on sshext +func (b *StatusBackend) LoadFilter(chat *filter.Chat) ([]*filter.Chat, error) { + st, err := b.statusNode.ShhExtService() + if err != nil { + return nil, err + } + + return st.LoadFilter(chat) +} + +// RemoveFilter remove a filter +func (b *StatusBackend) RemoveFilter(chat *filter.Chat) error { + st, err := b.statusNode.ShhExtService() + if err != nil { + return err + } + + return st.RemoveFilter(chat) +} + // EnableInstallation enables an installation for multi-device sync. func (b *StatusBackend) EnableInstallation(installationID string) error { selectedChatAccount, err := b.AccountManager().SelectedChatAccount() diff --git a/lib/library.go b/lib/library.go index e52aa55a9..91d76af0e 100644 --- a/lib/library.go +++ b/lib/library.go @@ -18,6 +18,7 @@ import ( "github.com/status-im/status-go/params" "github.com/status-im/status-go/profiling" "github.com/status-im/status-go/services/personal" + "github.com/status-im/status-go/services/shhext/filter" "github.com/status-im/status-go/services/typeddata" "github.com/status-im/status-go/signal" "github.com/status-im/status-go/transactions" @@ -51,40 +52,23 @@ func StopNode() *C.char { return makeJSONResponse(nil) } -// Create an X3DH bundle -//export CreateContactCode -func CreateContactCode() *C.char { - bundle, err := statusBackend.CreateContactCode() - if err != nil { +// LoadFilters load all whisper filters +//export LoadFilters +func LoadFilters(chatsStr *C.char) *C.char { + var chats []*filter.Chat + + if err := json.Unmarshal([]byte(C.GoString(chatsStr)), &chats); err != nil { return makeJSONResponse(err) } - cstr := C.CString(bundle) - - return cstr -} - -//export ProcessContactCode -func ProcessContactCode(bundleString *C.char) *C.char { - err := statusBackend.ProcessContactCode(C.GoString(bundleString)) - if err != nil { - return makeJSONResponse(err) - } - - return nil -} - -// Get an X3DH bundle -//export GetContactCode -func GetContactCode(identityString *C.char) *C.char { - bundle, err := statusBackend.GetContactCode(C.GoString(identityString)) + response, err := statusBackend.LoadFilters(chats) if err != nil { return makeJSONResponse(err) } data, err := json.Marshal(struct { - ContactCode string `json:"code"` - }{ContactCode: bundle}) + Chats []*filter.Chat `json:"result"` + }{Chats: response}) if err != nil { return makeJSONResponse(err) } @@ -92,22 +76,48 @@ func GetContactCode(identityString *C.char) *C.char { return C.CString(string(data)) } -//export ExtractIdentityFromContactCode -func ExtractIdentityFromContactCode(bundleString *C.char) *C.char { - bundle := C.GoString(bundleString) +// LoadFilter load a whisper filter +//export LoadFilter +func LoadFilter(chatStr *C.char) *C.char { + var chat *filter.Chat - identity, err := statusBackend.ExtractIdentityFromContactCode(bundle) + if err := json.Unmarshal([]byte(C.GoString(chatStr)), &chat); err != nil { + return makeJSONResponse(err) + } + + response, err := statusBackend.LoadFilter(chat) if err != nil { return makeJSONResponse(err) } - if err := statusBackend.ProcessContactCode(bundle); err != nil { + data, err := json.Marshal(struct { + Chats []*filter.Chat `json:"result"` + }{Chats: response}) + + if err != nil { + return makeJSONResponse(err) + } + + return C.CString(string(data)) +} + +// RemoveFilter load a whisper filter +//export RemoveFilter +func RemoveFilter(chatStr *C.char) *C.char { + var chat *filter.Chat + + if err := json.Unmarshal([]byte(C.GoString(chatStr)), &chat); err != nil { + return makeJSONResponse(err) + } + + err := statusBackend.RemoveFilter(chat) + if err != nil { return makeJSONResponse(err) } data, err := json.Marshal(struct { - Identity string `json:"identity"` - }{Identity: identity}) + Response string `json:"response"` + }{Response: "ok"}) if err != nil { return makeJSONResponse(err) } diff --git a/mobile/status.go b/mobile/status.go index d45020668..24ee9b6b9 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -16,6 +16,7 @@ import ( "github.com/status-im/status-go/params" "github.com/status-im/status-go/profiling" "github.com/status-im/status-go/services/personal" + "github.com/status-im/status-go/services/shhext/filter" "github.com/status-im/status-go/services/typeddata" "github.com/status-im/status-go/signal" "github.com/status-im/status-go/transactions" @@ -64,48 +65,6 @@ func StopNode() string { return makeJSONResponse(nil) } -// CreateContactCode creates an X3DH bundle. -func CreateContactCode() string { - bundle, err := statusBackend.CreateContactCode() - if err != nil { - return makeJSONResponse(err) - } - - return bundle -} - -// ProcessContactCode processes an X3DH bundle. -// TODO(adam): it looks like the return should be error. -func ProcessContactCode(bundle string) string { - err := statusBackend.ProcessContactCode(bundle) - if err != nil { - return makeJSONResponse(err) - } - - return "" -} - -// ExtractIdentityFromContactCode extracts an identity from an X3DH bundle. -func ExtractIdentityFromContactCode(bundle string) string { - identity, err := statusBackend.ExtractIdentityFromContactCode(bundle) - if err != nil { - return makeJSONResponse(err) - } - - if err := statusBackend.ProcessContactCode(bundle); err != nil { - return makeJSONResponse(err) - } - - data, err := json.Marshal(struct { - Identity string `json:"identity"` - }{Identity: identity}) - if err != nil { - return makeJSONResponse(err) - } - - return string(data) -} - // ExtractGroupMembershipSignatures extract public keys from tuples of content/signature. func ExtractGroupMembershipSignatures(signaturePairsStr string) string { var signaturePairs [][2]string @@ -617,24 +576,6 @@ func SetSignalEventCallback(cb unsafe.Pointer) { signal.SetSignalEventCallback(cb) } -// Get an X3DH bundle -//export GetContactCode -func GetContactCode(identity string) string { - bundle, err := statusBackend.GetContactCode(identity) - if err != nil { - return makeJSONResponse(err) - } - - data, err := json.Marshal(struct { - ContactCode string `json:"code"` - }{ContactCode: bundle}) - if err != nil { - return makeJSONResponse(err) - } - - return string(data) -} - // ExportNodeLogs reads current node log and returns content to a caller. //export ExportNodeLogs func ExportNodeLogs() string { @@ -673,3 +614,73 @@ func SignHash(hexEncodedHash string) string { return hexEncodedSignature } + +// LoadFilters load all whisper filters +func LoadFilters(chatsStr string) string { + var chats []*filter.Chat + + if err := json.Unmarshal([]byte(chatsStr), &chats); err != nil { + return makeJSONResponse(err) + } + + response, err := statusBackend.LoadFilters(chats) + if err != nil { + return makeJSONResponse(err) + } + + data, err := json.Marshal(struct { + Chats []*filter.Chat `json:"result"` + }{Chats: response}) + if err != nil { + return makeJSONResponse(err) + } + + return string(data) +} + +// LoadFilter load a whisper filter +func LoadFilter(chatStr string) string { + var chat *filter.Chat + + if err := json.Unmarshal([]byte(chatStr), &chat); err != nil { + return makeJSONResponse(err) + } + + response, err := statusBackend.LoadFilter(chat) + if err != nil { + return makeJSONResponse(err) + } + + data, err := json.Marshal(struct { + Chats []*filter.Chat `json:"result"` + }{Chats: response}) + if err != nil { + return makeJSONResponse(err) + } + + return string(data) +} + +// RemoveFilter load a whisper filter +//export RemoveFilter +func RemoveFilter(chatStr string) string { + var chat *filter.Chat + + if err := json.Unmarshal([]byte(chatStr), &chat); err != nil { + return makeJSONResponse(err) + } + + err := statusBackend.RemoveFilter(chat) + if err != nil { + return makeJSONResponse(err) + } + + data, err := json.Marshal(struct { + Response string `json:"response"` + }{Response: "ok"}) + if err != nil { + return makeJSONResponse(err) + } + + return string(data) +} diff --git a/params/config.go b/params/config.go index ce0991be4..7703f9407 100644 --- a/params/config.go +++ b/params/config.go @@ -368,8 +368,6 @@ type WalletConfig struct { // ShhextConfig defines options used by shhext service. type ShhextConfig struct { - // AsymKeyID the key id of the selected account - AsymKeyID string PFSEnabled bool // BackupDisabledDataDir is the file system folder the node should use for any data storage needs that it doesn't want backed up. BackupDisabledDataDir string diff --git a/services/shhext/api.go b/services/shhext/api.go index e9d312951..237610528 100644 --- a/services/shhext/api.go +++ b/services/shhext/api.go @@ -9,15 +9,12 @@ import ( "math/big" "time" - "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/golang/protobuf/proto" + "github.com/ethereum/go-ethereum/rlp" + "github.com/status-im/status-go/db" "github.com/status-im/status-go/mailserver" "github.com/status-im/status-go/services/shhext/chat" @@ -424,13 +421,11 @@ func (api *PublicAPI) GetNewFilterMessages(filterID string) ([]dedup.Deduplicate dedupMessages := api.service.deduplicator.Deduplicate(msgs) - if api.service.pfsEnabled { - // Attempt to decrypt message, otherwise leave unchanged - for _, dedupMessage := range dedupMessages { + // Attempt to decrypt message, otherwise leave unchanged + for _, dedupMessage := range dedupMessages { - if err := api.processPFSMessage(dedupMessage); err != nil { - return nil, err - } + if err := api.service.ProcessMessage(dedupMessage); err != nil { + return nil, err } } @@ -462,7 +457,7 @@ func (api *PublicAPI) ConfirmMessagesProcessed(messages []*whisper.Message) (err // ConfirmMessagesProcessedByID is a method to confirm that messages was consumed by // the client side. func (api *PublicAPI) ConfirmMessagesProcessedByID(messageIDs [][]byte) error { - if err := api.service.protocol.ConfirmMessagesProcessed(messageIDs); err != nil { + if err := api.service.ConfirmMessagesProcessed(messageIDs); err != nil { return err } @@ -471,97 +466,12 @@ func (api *PublicAPI) ConfirmMessagesProcessedByID(messageIDs [][]byte) error { // SendPublicMessage sends a public chat message to the underlying transport func (api *PublicAPI) SendPublicMessage(ctx context.Context, msg chat.SendPublicMessageRPC) (hexutil.Bytes, error) { - privateKey, err := api.service.w.GetPrivateKey(msg.Sig) - if err != nil { - return nil, err - } - - // This is transport layer agnostic - protocolMessage, err := api.service.protocol.BuildPublicMessage(privateKey, msg.Payload) - if err != nil { - return nil, err - } - - symKeyID, err := api.service.w.AddSymKeyFromPassword(msg.Chat) - if err != nil { - return nil, err - } - - // marshal for sending to wire - marshaledMessage, err := proto.Marshal(protocolMessage) - if err != nil { - api.log.Error("encryption-service", "error marshaling message", err) - return nil, err - } - - // Enrich with transport layer info - whisperMessage := chat.PublicMessageToWhisper(msg, marshaledMessage) - whisperMessage.SymKeyID = symKeyID - - // And dispatch - return api.Post(ctx, whisperMessage) + return api.service.SendPublicMessage(ctx, msg) } // SendDirectMessage sends a 1:1 chat message to the underlying transport func (api *PublicAPI) SendDirectMessage(ctx context.Context, msg chat.SendDirectMessageRPC) (hexutil.Bytes, error) { - if !api.service.pfsEnabled { - return nil, ErrPFSNotEnabled - } - // To be completely agnostic from whisper we should not be using whisper to store the key - privateKey, err := api.service.w.GetPrivateKey(msg.Sig) - if err != nil { - return nil, err - } - - publicKey, err := crypto.UnmarshalPubkey(msg.PubKey) - if err != nil { - return nil, err - } - - // This is transport layer-agnostic - var protocolMessage *chat.ProtocolMessage - // The negotiated secret - var topic []byte - - api.log.Info("BUILDING MESSAGE") - if msg.DH { - protocolMessage, topic, err = api.service.protocol.BuildDHMessage(privateKey, &privateKey.PublicKey, msg.Payload) - } else { - protocolMessage, topic, err = api.service.protocol.BuildDirectMessage(privateKey, publicKey, msg.Payload) - } - - api.log.Info("BUILT MESSAGE", "topic", topic) - - if err != nil { - return nil, err - } - - // marshal for sending to wire - marshaledMessage, err := proto.Marshal(protocolMessage) - if err != nil { - api.log.Error("encryption-service", "error marshaling message", err) - return nil, err - } - - // TODO: Refactor this as it's not quite the right abstraction anymore - whisperMessage := chat.DirectMessageToWhisper(msg, marshaledMessage, topic) - // Enrich with transport layer info - if topic != nil { - api.log.Info("GETTING SYM KEY", "symkey", api.service.GetNegotiatedChat(publicKey)) - - chat := api.service.GetNegotiatedChat(publicKey) - - if chat != nil { - whisperMessage.SymKeyID = chat.SymKeyID - whisperMessage.Topic = whisper.BytesToTopic(chat.Topic) - whisperMessage.PublicKey = nil - } - } - - api.log.Info("WHISPER MESSAGE", "message", whisperMessage) - - // And dispatch - return api.Post(ctx, whisperMessage) + return api.service.SendDirectMessage(ctx, msg) } func (api *PublicAPI) requestMessagesUsingPayload(request db.HistoryRequest, peer, symkeyID string, payload []byte, force bool, timeout time.Duration, topics []whisper.TopicType) (hash common.Hash, err error) { @@ -672,54 +582,6 @@ func (api *PublicAPI) CompleteRequest(parent context.Context, hex string) (err e return err } -func (api *PublicAPI) processPFSMessage(dedupMessage dedup.DeduplicateMessage) error { - msg := dedupMessage.Message - - privateKeyID := api.service.w.SelectedKeyPairID() - if privateKeyID == "" { - return errors.New("no key selected") - } - - privateKey, err := api.service.w.GetPrivateKey(privateKeyID) - if err != nil { - return err - } - - publicKey, err := crypto.UnmarshalPubkey(msg.Sig) - if err != nil { - return err - } - - // Unmarshal message - protocolMessage := &chat.ProtocolMessage{} - - if err := proto.Unmarshal(msg.Payload, protocolMessage); err != nil { - api.log.Debug("Not a protocol message", "err", err) - return nil - } - - response, err := api.service.protocol.HandleMessage(privateKey, publicKey, protocolMessage, dedupMessage.DedupID) - - switch err { - case nil: - // Set the decrypted payload - msg.Payload = response - case chat.ErrDeviceNotFound: - // Notify that someone tried to contact us using an invalid bundle - if privateKey.PublicKey != *publicKey { - api.log.Warn("Device not found, sending signal", "err", err) - keyString := fmt.Sprintf("0x%x", crypto.FromECDSAPub(publicKey)) - handler := EnvelopeSignalHandler{} - handler.DecryptMessageFailed(keyString) - } - default: - // Log and pass to the client, even if failed to decrypt - api.log.Error("Failed handling message with error", "err", err) - } - - return nil -} - // ----- // HELPER // ----- diff --git a/services/shhext/chat/db/migrations/bindata.go b/services/shhext/chat/db/migrations/bindata.go index 006ed5863..26eac2e96 100644 --- a/services/shhext/chat/db/migrations/bindata.go +++ b/services/shhext/chat/db/migrations/bindata.go @@ -8,8 +8,8 @@ // 1540715431_add_version.up.sql // 1541164797_add_installations.down.sql // 1541164797_add_installations.up.sql -// 1558084410_add_topic.down.sql -// 1558084410_add_topic.up.sql +// 1558084410_add_secret.down.sql +// 1558084410_add_secret.up.sql // 1558588866_add_version.up.sql // static.go // DO NOT EDIT! @@ -239,42 +239,42 @@ func _1541164797_add_installationsUpSql() (*asset, error) { return a, nil } -var __1558084410_add_topicDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\x28\xc9\x2f\xc8\x4c\x8e\xcf\xcc\x2b\x2e\x49\xcc\xc9\x49\x2c\xc9\xcc\xcf\x8b\xcf\x4c\x29\xb6\xe6\x42\x57\x52\x6c\xcd\x05\x08\x00\x00\xff\xff\xf0\xe3\x8a\xc7\x36\x00\x00\x00") +var __1558084410_add_secretDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\x28\x4e\x4d\x2e\x4a\x2d\x89\xcf\xcc\x2b\x2e\x49\xcc\xc9\x49\x2c\xc9\xcc\xcf\x8b\xcf\x4c\x29\xb6\xe6\xc2\x50\x53\x6c\xcd\x05\x08\x00\x00\xff\xff\xd3\xcd\x41\x83\x38\x00\x00\x00") -func _1558084410_add_topicDownSqlBytes() ([]byte, error) { +func _1558084410_add_secretDownSqlBytes() ([]byte, error) { return bindataRead( - __1558084410_add_topicDownSql, - "1558084410_add_topic.down.sql", + __1558084410_add_secretDownSql, + "1558084410_add_secret.down.sql", ) } -func _1558084410_add_topicDownSql() (*asset, error) { - bytes, err := _1558084410_add_topicDownSqlBytes() +func _1558084410_add_secretDownSql() (*asset, error) { + bytes, err := _1558084410_add_secretDownSqlBytes() if err != nil { return nil, err } - info := bindataFileInfo{name: "1558084410_add_topic.down.sql", size: 54, mode: os.FileMode(420), modTime: time.Unix(1560418030, 0)} + info := bindataFileInfo{name: "1558084410_add_secret.down.sql", size: 56, mode: os.FileMode(420), modTime: time.Unix(1560418252, 0)} a := &asset{bytes: bytes, info: info} return a, nil } -var __1558084410_add_topicUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x74\x90\x41\x6b\x85\x30\x10\x84\xef\xf9\x15\x73\x54\xf0\x1f\xf4\xa4\x61\x95\xd0\x74\xd3\xa6\x11\xea\x49\xc4\x78\x58\x10\x2d\x35\x97\xfe\xfb\x62\x79\x4f\x94\xc7\x3b\xcf\xcc\xce\x7c\xab\x3d\x95\x81\x10\xca\xca\x12\xd2\xfa\x2d\xe3\x86\x4c\x01\x12\xa7\x25\x49\xfa\x45\x65\x5d\x05\x76\x01\xdc\x5a\x8b\x77\x6f\xde\x4a\xdf\xe1\x95\x3a\x38\x86\x76\x5c\x5b\xa3\x03\x4c\xc3\xce\x53\xa1\x80\x6d\x1a\x7f\xa6\x74\x8d\xa9\xfc\x45\xa9\xc7\xa6\x5e\x96\x2d\x0d\xf3\x3c\x24\x59\x97\x5e\xe2\xbd\x19\x81\xbe\xc2\x11\x2e\x4e\x6b\x7a\x89\xd7\xcb\xbb\xd8\xb2\xf9\x68\x29\x93\x58\x9c\x7d\xf9\x93\x7d\xb5\xf3\x64\x1a\xfe\x27\xc8\x2e\x7e\x4f\x35\x79\x62\x4d\x9f\xb7\x47\x1c\x72\xbe\x03\xfc\x05\x00\x00\xff\xff\xf3\xa6\x3d\xc3\x2a\x01\x00\x00") +var __1558084410_add_secretUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x74\x50\xcf\x0a\x82\x30\x1c\xbe\xef\x29\xbe\xa3\x82\x6f\xd0\x49\xc7\x4f\x19\xad\xdf\x6a\x4d\xc8\x93\x48\xf3\x30\x10\x83\xdc\xa5\xb7\x0f\x23\x45\xa1\xce\xdf\xff\x4f\x5a\xca\x1d\xc1\xe5\x85\x26\x4c\xfd\xfd\xd9\xc7\x09\x89\x00\x82\xef\xc7\x18\xe2\x0b\x85\x36\x05\xd8\x38\x70\xad\x35\xce\x56\x9d\x72\xdb\xe0\x48\x0d\x0c\x43\x1a\x2e\xb5\x92\x0e\xaa\x62\x63\x29\x13\xf8\x9a\xec\x65\x22\x3d\x08\xf1\x23\xaa\x0d\xe3\x14\xbb\x61\xe8\x62\x78\x8c\x6d\xf0\x4b\x34\x1c\xdd\xdc\xaa\xce\x36\x75\xda\xe0\xf7\xd6\x33\x58\xb3\xba\xd4\x94\x04\x9f\x6d\x79\xe9\x9f\x82\xa5\xb1\xa4\x2a\xfe\x4c\x48\x76\x7c\x4b\x25\x59\x62\x49\xd7\xe5\x8a\x15\x4f\xe7\x09\xef\x00\x00\x00\xff\xff\xa6\xbb\x2c\x23\x2d\x01\x00\x00") -func _1558084410_add_topicUpSqlBytes() ([]byte, error) { +func _1558084410_add_secretUpSqlBytes() ([]byte, error) { return bindataRead( - __1558084410_add_topicUpSql, - "1558084410_add_topic.up.sql", + __1558084410_add_secretUpSql, + "1558084410_add_secret.up.sql", ) } -func _1558084410_add_topicUpSql() (*asset, error) { - bytes, err := _1558084410_add_topicUpSqlBytes() +func _1558084410_add_secretUpSql() (*asset, error) { + bytes, err := _1558084410_add_secretUpSqlBytes() if err != nil { return nil, err } - info := bindataFileInfo{name: "1558084410_add_topic.up.sql", size: 298, mode: os.FileMode(420), modTime: time.Unix(1560418030, 0)} + info := bindataFileInfo{name: "1558084410_add_secret.up.sql", size: 301, mode: os.FileMode(420), modTime: time.Unix(1560418252, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -294,7 +294,7 @@ func _1558588866_add_versionUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1558588866_add_version.up.sql", size: 57, mode: os.FileMode(420), modTime: time.Unix(1558588995, 0)} + info := bindataFileInfo{name: "1558588866_add_version.up.sql", size: 57, mode: os.FileMode(420), modTime: time.Unix(1560418251, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -379,8 +379,8 @@ var _bindata = map[string]func() (*asset, error){ "1540715431_add_version.up.sql": _1540715431_add_versionUpSql, "1541164797_add_installations.down.sql": _1541164797_add_installationsDownSql, "1541164797_add_installations.up.sql": _1541164797_add_installationsUpSql, - "1558084410_add_topic.down.sql": _1558084410_add_topicDownSql, - "1558084410_add_topic.up.sql": _1558084410_add_topicUpSql, + "1558084410_add_secret.down.sql": _1558084410_add_secretDownSql, + "1558084410_add_secret.up.sql": _1558084410_add_secretUpSql, "1558588866_add_version.up.sql": _1558588866_add_versionUpSql, "static.go": staticGo, } @@ -433,8 +433,8 @@ var _bintree = &bintree{nil, map[string]*bintree{ "1540715431_add_version.up.sql": &bintree{_1540715431_add_versionUpSql, map[string]*bintree{}}, "1541164797_add_installations.down.sql": &bintree{_1541164797_add_installationsDownSql, map[string]*bintree{}}, "1541164797_add_installations.up.sql": &bintree{_1541164797_add_installationsUpSql, map[string]*bintree{}}, - "1558084410_add_topic.down.sql": &bintree{_1558084410_add_topicDownSql, map[string]*bintree{}}, - "1558084410_add_topic.up.sql": &bintree{_1558084410_add_topicUpSql, map[string]*bintree{}}, + "1558084410_add_secret.down.sql": &bintree{_1558084410_add_secretDownSql, map[string]*bintree{}}, + "1558084410_add_secret.up.sql": &bintree{_1558084410_add_secretUpSql, map[string]*bintree{}}, "1558588866_add_version.up.sql": &bintree{_1558588866_add_versionUpSql, map[string]*bintree{}}, "static.go": &bintree{staticGo, map[string]*bintree{}}, }} diff --git a/services/shhext/chat/encryption.go b/services/shhext/chat/encryption.go index 7f075abbb..6c2744bf6 100644 --- a/services/shhext/chat/encryption.go +++ b/services/shhext/chat/encryption.go @@ -1,11 +1,9 @@ package chat import ( - "bytes" "crypto/ecdsa" "encoding/hex" "errors" - "fmt" "sync" "time" @@ -15,6 +13,8 @@ import ( dr "github.com/status-im/doubleratchet" "github.com/status-im/status-go/services/shhext/chat/crypto" + "github.com/status-im/status-go/services/shhext/chat/multidevice" + "github.com/status-im/status-go/services/shhext/chat/protobuf" ) var ErrSessionNotFound = errors.New("session not found") @@ -51,8 +51,6 @@ type EncryptionServiceConfig struct { BundleRefreshInterval int64 } -type IdentityAndIDPair [2]string - // DefaultEncryptionServiceConfig returns the default values used by the encryption service func DefaultEncryptionServiceConfig(installationID string) EncryptionServiceConfig { return EncryptionServiceConfig{ @@ -132,19 +130,9 @@ func (s *EncryptionService) ConfirmMessagesProcessed(messageIDs [][]byte) error } // CreateBundle retrieves or creates an X3DH bundle given a private key -func (s *EncryptionService) CreateBundle(privateKey *ecdsa.PrivateKey) (*Bundle, error) { +func (s *EncryptionService) CreateBundle(privateKey *ecdsa.PrivateKey, installations []*multidevice.Installation) (*protobuf.Bundle, error) { ourIdentityKeyC := ecrypto.CompressPubkey(&privateKey.PublicKey) - installations, err := s.persistence.GetActiveInstallations(s.config.MaxInstallations-1, ourIdentityKeyC) - if err != nil { - return nil, err - } - - installations = append(installations, &Installation{ - ID: s.config.InstallationID, - Version: protocolCurrentVersion, - }) - bundleContainer, err := s.persistence.GetAnyPrivateBundle(ourIdentityKeyC, installations) if err != nil { return nil, err @@ -176,7 +164,7 @@ func (s *EncryptionService) CreateBundle(privateKey *ecdsa.PrivateKey) (*Bundle, return nil, err } - return s.CreateBundle(privateKey) + return s.CreateBundle(privateKey, installations) } // DecryptWithDH decrypts message sent with a DH key exchange, and throws away the key after decryption @@ -224,55 +212,13 @@ func (s *EncryptionService) keyFromPassiveX3DH(myIdentityKey *ecdsa.PrivateKey, return key, nil } -func (s *EncryptionService) EnableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error { - myIdentityKeyC := ecrypto.CompressPubkey(myIdentityKey) - return s.persistence.EnableInstallation(myIdentityKeyC, installationID) -} - -func (s *EncryptionService) DisableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error { - myIdentityKeyC := ecrypto.CompressPubkey(myIdentityKey) - return s.persistence.DisableInstallation(myIdentityKeyC, installationID) -} - -// ProcessPublicBundle persists a bundle and returns a list of tuples identity/installationID -func (s *EncryptionService) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, b *Bundle) ([]IdentityAndIDPair, error) { - // Make sure the bundle belongs to who signed it - identity, err := ExtractIdentity(b) - if err != nil { - return nil, err - } - signedPreKeys := b.GetSignedPreKeys() - var response []IdentityAndIDPair - var installations []*Installation - myIdentityStr := fmt.Sprintf("0x%x", ecrypto.FromECDSAPub(&myIdentityKey.PublicKey)) - - // Any device from other peers will be considered enabled, ours needs to - // be explicitly enabled - fromOurIdentity := identity != myIdentityStr - - for installationID, signedPreKey := range signedPreKeys { - if installationID != s.config.InstallationID { - installations = append(installations, &Installation{ - ID: installationID, - Version: signedPreKey.GetProtocolVersion(), - }) - response = append(response, IdentityAndIDPair{identity, installationID}) - } - } - - if err = s.persistence.AddInstallations(b.GetIdentity(), b.GetTimestamp(), installations, fromOurIdentity); err != nil { - return nil, err - } - - if err = s.persistence.AddPublicBundle(b); err != nil { - return nil, err - } - - return response, nil +// ProcessPublicBundle persists a bundle +func (s *EncryptionService) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, b *protobuf.Bundle) error { + return s.persistence.AddPublicBundle(b) } // DecryptPayload decrypts the payload of a DirectMessageProtocol, given an identity private key and the sender's public key -func (s *EncryptionService) DecryptPayload(myIdentityKey *ecdsa.PrivateKey, theirIdentityKey *ecdsa.PublicKey, theirInstallationID string, msgs map[string]*DirectMessageProtocol, messageID []byte) ([]byte, error) { +func (s *EncryptionService) DecryptPayload(myIdentityKey *ecdsa.PrivateKey, theirIdentityKey *ecdsa.PublicKey, theirInstallationID string, msgs map[string]*protobuf.DirectMessageProtocol, messageID []byte) ([]byte, error) { s.mutex.Lock() defer s.mutex.Unlock() @@ -328,11 +274,6 @@ func (s *EncryptionService) DecryptPayload(myIdentityKey *ecdsa.PrivateKey, thei return nil, err } - // Add installations with a timestamp of 0, as we don't have bundle informations - if err = s.persistence.AddInstallations(theirIdentityKeyC, 0, []*Installation{{ID: theirInstallationID, Version: 0}}, true); err != nil { - return nil, err - } - // We mark the exchange as successful so we stop sending x3dh header if err = s.persistence.RatchetInfoConfirmed(drHeader.GetId(), theirIdentityKeyC, theirInstallationID); err != nil { s.log.Error("Could not confirm ratchet info", "err", err) @@ -396,7 +337,7 @@ func (s *EncryptionService) createNewSession(drInfo *RatchetInfo, sk [32]byte, k return session, err } -func (s *EncryptionService) encryptUsingDR(theirIdentityKey *ecdsa.PublicKey, drInfo *RatchetInfo, payload []byte) ([]byte, *DRHeader, error) { +func (s *EncryptionService) encryptUsingDR(theirIdentityKey *ecdsa.PublicKey, drInfo *RatchetInfo, payload []byte) ([]byte, *protobuf.DRHeader, error) { var err error var session dr.Session @@ -430,7 +371,7 @@ func (s *EncryptionService) encryptUsingDR(theirIdentityKey *ecdsa.PublicKey, dr return nil, nil, err } - header := &DRHeader{ + header := &protobuf.DRHeader{ Id: drInfo.BundleID, Key: response.Header.DH[:], N: response.Header.N, @@ -474,7 +415,7 @@ func (s *EncryptionService) decryptUsingDR(theirIdentityKey *ecdsa.PublicKey, dr return plaintext, nil } -func (s *EncryptionService) encryptWithDH(theirIdentityKey *ecdsa.PublicKey, payload []byte) (*DirectMessageProtocol, error) { +func (s *EncryptionService) encryptWithDH(theirIdentityKey *ecdsa.PublicKey, payload []byte) (*protobuf.DirectMessageProtocol, error) { symmetricKey, ourEphemeralKey, err := PerformActiveDH(theirIdentityKey) if err != nil { return nil, err @@ -485,16 +426,16 @@ func (s *EncryptionService) encryptWithDH(theirIdentityKey *ecdsa.PublicKey, pay return nil, err } - return &DirectMessageProtocol{ - DHHeader: &DHHeader{ + return &protobuf.DirectMessageProtocol{ + DHHeader: &protobuf.DHHeader{ Key: ecrypto.CompressPubkey(ourEphemeralKey), }, Payload: encryptedPayload, }, nil } -func (s *EncryptionService) EncryptPayloadWithDH(theirIdentityKey *ecdsa.PublicKey, payload []byte) (map[string]*DirectMessageProtocol, error) { - response := make(map[string]*DirectMessageProtocol) +func (s *EncryptionService) EncryptPayloadWithDH(theirIdentityKey *ecdsa.PublicKey, payload []byte) (map[string]*protobuf.DirectMessageProtocol, error) { + response := make(map[string]*protobuf.DirectMessageProtocol) dmp, err := s.encryptWithDH(theirIdentityKey, payload) if err != nil { return nil, err @@ -505,21 +446,15 @@ func (s *EncryptionService) EncryptPayloadWithDH(theirIdentityKey *ecdsa.PublicK } // GetPublicBundle returns the active installations bundles for a given user -func (s *EncryptionService) GetPublicBundle(theirIdentityKey *ecdsa.PublicKey) (*Bundle, error) { - theirIdentityKeyC := ecrypto.CompressPubkey(theirIdentityKey) - - installations, err := s.persistence.GetActiveInstallations(s.config.MaxInstallations, theirIdentityKeyC) - if err != nil { - return nil, err - } - +func (s *EncryptionService) GetPublicBundle(theirIdentityKey *ecdsa.PublicKey, installations []*multidevice.Installation) (*protobuf.Bundle, error) { return s.persistence.GetPublicBundle(theirIdentityKey, installations) } // EncryptPayload returns a new DirectMessageProtocol with a given payload encrypted, given a recipient's public key and the sender private identity key -// TODO: refactor this -// nolint: gocyclo -func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, myIdentityKey *ecdsa.PrivateKey, payload []byte) (map[string]*DirectMessageProtocol, error) { +func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, myIdentityKey *ecdsa.PrivateKey, installations []*multidevice.Installation, payload []byte) (map[string]*protobuf.DirectMessageProtocol, []*multidevice.Installation, error) { + // Which installations we are sending the message to + var targetedInstallations []*multidevice.Installation + s.mutex.Lock() defer s.mutex.Unlock() @@ -527,18 +462,13 @@ func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, my theirIdentityKeyC := ecrypto.CompressPubkey(theirIdentityKey) - // Get their installationIds - installations, err := s.persistence.GetActiveInstallations(s.config.MaxInstallations, theirIdentityKeyC) - if err != nil { - return nil, err - } - // We don't have any, send a message with DH - if installations == nil && !bytes.Equal(theirIdentityKeyC, ecrypto.CompressPubkey(&myIdentityKey.PublicKey)) { - return s.EncryptPayloadWithDH(theirIdentityKey, payload) + if len(installations) == 0 { + encryptedPayload, err := s.EncryptPayloadWithDH(theirIdentityKey, payload) + return encryptedPayload, targetedInstallations, err } - response := make(map[string]*DirectMessageProtocol) + response := make(map[string]*protobuf.DirectMessageProtocol) for _, installation := range installations { installationID := installation.ID @@ -546,31 +476,33 @@ func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, my if s.config.InstallationID == installationID { continue } - bundle, err := s.persistence.GetPublicBundle(theirIdentityKey, []*Installation{installation}) + bundle, err := s.persistence.GetPublicBundle(theirIdentityKey, []*multidevice.Installation{installation}) if err != nil { - return nil, err + return nil, nil, err } // See if a session is there already drInfo, err := s.persistence.GetAnyRatchetInfo(theirIdentityKeyC, installationID) if err != nil { - return nil, err + return nil, nil, err } + targetedInstallations = append(targetedInstallations, installation) + if drInfo != nil { s.log.Debug("Found DR info", "installationID", installationID) encryptedPayload, drHeader, err := s.encryptUsingDR(theirIdentityKey, drInfo, payload) if err != nil { - return nil, err + return nil, nil, err } - dmp := DirectMessageProtocol{ + dmp := protobuf.DirectMessageProtocol{ Payload: encryptedPayload, DRHeader: drHeader, } if drInfo.EphemeralKey != nil { - dmp.X3DHHeader = &X3DHHeader{ + dmp.X3DHHeader = &protobuf.X3DHHeader{ Key: drInfo.EphemeralKey, Id: drInfo.BundleID, } @@ -594,33 +526,33 @@ func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, my sharedKey, ourEphemeralKey, err := s.keyFromActiveX3DH(theirIdentityKeyC, theirSignedPreKey, myIdentityKey) if err != nil { - return nil, err + return nil, nil, err } theirIdentityKeyC := ecrypto.CompressPubkey(theirIdentityKey) ourEphemeralKeyC := ecrypto.CompressPubkey(ourEphemeralKey) err = s.persistence.AddRatchetInfo(sharedKey, theirIdentityKeyC, theirSignedPreKey, ourEphemeralKeyC, installationID) if err != nil { - return nil, err + return nil, nil, err } - x3dhHeader := &X3DHHeader{ + x3dhHeader := &protobuf.X3DHHeader{ Key: ourEphemeralKeyC, Id: theirSignedPreKey, } drInfo, err = s.persistence.GetRatchetInfo(theirSignedPreKey, theirIdentityKeyC, installationID) if err != nil { - return nil, err + return nil, nil, err } if drInfo != nil { encryptedPayload, drHeader, err := s.encryptUsingDR(theirIdentityKey, drInfo, payload) if err != nil { - return nil, err + return nil, nil, err } - dmp := &DirectMessageProtocol{ + dmp := &protobuf.DirectMessageProtocol{ Payload: encryptedPayload, X3DHHeader: x3dhHeader, DRHeader: drHeader, @@ -632,5 +564,5 @@ func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, my s.log.Debug("Built message", "theirKey", theirIdentityKey) - return response, nil + return response, targetedInstallations, nil } diff --git a/services/shhext/chat/encryption_multi_device_test.go b/services/shhext/chat/encryption_multi_device_test.go index 80dc6e4db..96299abdb 100644 --- a/services/shhext/chat/encryption_multi_device_test.go +++ b/services/shhext/chat/encryption_multi_device_test.go @@ -8,6 +8,9 @@ import ( "os" "sort" "testing" + + "github.com/status-im/status-go/services/shhext/chat/multidevice" + "github.com/status-im/status-go/services/shhext/chat/sharedsecret" ) const ( @@ -20,8 +23,8 @@ func TestEncryptionServiceMultiDeviceSuite(t *testing.T) { } type serviceAndKey struct { - encryptionServices []*EncryptionService - key *ecdsa.PrivateKey + services []*ProtocolService + key *ecdsa.PrivateKey } type EncryptionServiceMultiDeviceSuite struct { @@ -36,8 +39,8 @@ func setupUser(user string, s *EncryptionServiceMultiDeviceSuite, n int) error { } s.services[user] = &serviceAndKey{ - key: key, - encryptionServices: make([]*EncryptionService, n), + key: key, + services: make([]*ProtocolService, n), } for i := 0; i < n; i++ { @@ -50,11 +53,27 @@ func setupUser(user string, s *EncryptionServiceMultiDeviceSuite, n int) error { if err != nil { return err } + // Initialize sharedsecret + multideviceConfig := &multidevice.Config{ + MaxInstallations: n - 1, + InstallationID: installationID, + ProtocolVersion: 1, + } - config := DefaultEncryptionServiceConfig(installationID) - config.MaxInstallations = n - 1 + sharedSecretService := sharedsecret.NewService(persistence.GetSharedSecretStorage()) + multideviceService := multidevice.New(multideviceConfig, persistence.GetMultideviceStorage()) - s.services[user].encryptionServices[i] = NewEncryptionService(persistence, config) + protocol := NewProtocolService( + NewEncryptionService( + persistence, + DefaultEncryptionServiceConfig(installationID)), + sharedSecretService, + multideviceService, + func(s []multidevice.IdentityAndIDPair) {}, + func(s []*sharedsecret.Secret) {}, + ) + + s.services[user].services[i] = protocol } @@ -73,43 +92,47 @@ func (s *EncryptionServiceMultiDeviceSuite) SetupTest() { func (s *EncryptionServiceMultiDeviceSuite) TestProcessPublicBundle() { aliceKey := s.services[aliceUser].key - alice2Bundle, err := s.services[aliceUser].encryptionServices[1].CreateBundle(aliceKey) + alice2Bundle, err := s.services[aliceUser].services[1].GetBundle(aliceKey) s.Require().NoError(err) - alice2Identity, err := ExtractIdentity(alice2Bundle) + alice2IdentityPK, err := ExtractIdentity(alice2Bundle) s.Require().NoError(err) - alice3Bundle, err := s.services[aliceUser].encryptionServices[2].CreateBundle(aliceKey) + alice2Identity := fmt.Sprintf("0x%x", crypto.FromECDSAPub(alice2IdentityPK)) + + alice3Bundle, err := s.services[aliceUser].services[2].GetBundle(aliceKey) s.Require().NoError(err) - alice3Identity, err := ExtractIdentity(alice2Bundle) + alice3IdentityPK, err := ExtractIdentity(alice2Bundle) s.Require().NoError(err) + alice3Identity := fmt.Sprintf("0x%x", crypto.FromECDSAPub(alice3IdentityPK)) + // Add alice2 bundle - response, err := s.services[aliceUser].encryptionServices[0].ProcessPublicBundle(aliceKey, alice2Bundle) + response, err := s.services[aliceUser].services[0].ProcessPublicBundle(aliceKey, alice2Bundle) s.Require().NoError(err) - s.Require().Equal(IdentityAndIDPair{alice2Identity, "alice2"}, response[0]) + s.Require().Equal(multidevice.IdentityAndIDPair{alice2Identity, "alice2"}, response[0]) // Add alice3 bundle - response, err = s.services[aliceUser].encryptionServices[0].ProcessPublicBundle(aliceKey, alice3Bundle) + response, err = s.services[aliceUser].services[0].ProcessPublicBundle(aliceKey, alice3Bundle) s.Require().NoError(err) - s.Require().Equal(IdentityAndIDPair{alice3Identity, "alice3"}, response[0]) + s.Require().Equal(multidevice.IdentityAndIDPair{alice3Identity, "alice3"}, response[0]) // No installation is enabled - alice1MergedBundle1, err := s.services[aliceUser].encryptionServices[0].CreateBundle(aliceKey) + alice1MergedBundle1, err := s.services[aliceUser].services[0].GetBundle(aliceKey) s.Require().NoError(err) s.Require().Equal(1, len(alice1MergedBundle1.GetSignedPreKeys())) s.Require().NotNil(alice1MergedBundle1.GetSignedPreKeys()["alice1"]) // We enable the installations - err = s.services[aliceUser].encryptionServices[0].EnableInstallation(&aliceKey.PublicKey, "alice2") + err = s.services[aliceUser].services[0].EnableInstallation(&aliceKey.PublicKey, "alice2") s.Require().NoError(err) - err = s.services[aliceUser].encryptionServices[0].EnableInstallation(&aliceKey.PublicKey, "alice3") + err = s.services[aliceUser].services[0].EnableInstallation(&aliceKey.PublicKey, "alice3") s.Require().NoError(err) - alice1MergedBundle2, err := s.services[aliceUser].encryptionServices[0].CreateBundle(aliceKey) + alice1MergedBundle2, err := s.services[aliceUser].services[0].GetBundle(aliceKey) s.Require().NoError(err) // We get back a bundle with all the installations @@ -118,21 +141,21 @@ func (s *EncryptionServiceMultiDeviceSuite) TestProcessPublicBundle() { s.Require().NotNil(alice1MergedBundle2.GetSignedPreKeys()["alice2"]) s.Require().NotNil(alice1MergedBundle2.GetSignedPreKeys()["alice3"]) - response, err = s.services[aliceUser].encryptionServices[0].ProcessPublicBundle(aliceKey, alice1MergedBundle2) + response, err = s.services[aliceUser].services[0].ProcessPublicBundle(aliceKey, alice1MergedBundle2) s.Require().NoError(err) sort.Slice(response, func(i, j int) bool { return response[i][1] < response[j][1] }) // We only get back installationIDs not equal to us s.Require().Equal(2, len(response)) - s.Require().Equal(IdentityAndIDPair{alice2Identity, "alice2"}, response[0]) - s.Require().Equal(IdentityAndIDPair{alice2Identity, "alice3"}, response[1]) + s.Require().Equal(multidevice.IdentityAndIDPair{alice2Identity, "alice2"}, response[0]) + s.Require().Equal(multidevice.IdentityAndIDPair{alice2Identity, "alice3"}, response[1]) // We disable the installations - err = s.services[aliceUser].encryptionServices[0].DisableInstallation(&aliceKey.PublicKey, "alice2") + err = s.services[aliceUser].services[0].DisableInstallation(&aliceKey.PublicKey, "alice2") s.Require().NoError(err) - alice1MergedBundle3, err := s.services[aliceUser].encryptionServices[0].CreateBundle(aliceKey) + alice1MergedBundle3, err := s.services[aliceUser].services[0].GetBundle(aliceKey) s.Require().NoError(err) // We get back a bundle with all the installations @@ -146,23 +169,23 @@ func (s *EncryptionServiceMultiDeviceSuite) TestProcessPublicBundleOutOfOrder() s.Require().NoError(err) // Alice1 creates a bundle - alice1Bundle, err := s.services[aliceUser].encryptionServices[0].CreateBundle(aliceKey) + alice1Bundle, err := s.services[aliceUser].services[0].GetBundle(aliceKey) s.Require().NoError(err) // Alice2 Receives the bundle - _, err = s.services[aliceUser].encryptionServices[1].ProcessPublicBundle(aliceKey, alice1Bundle) + _, err = s.services[aliceUser].services[1].ProcessPublicBundle(aliceKey, alice1Bundle) s.Require().NoError(err) // Alice2 Creates a Bundle - _, err = s.services[aliceUser].encryptionServices[1].CreateBundle(aliceKey) + _, err = s.services[aliceUser].services[1].GetBundle(aliceKey) s.Require().NoError(err) // We enable the installation - err = s.services[aliceUser].encryptionServices[1].EnableInstallation(&aliceKey.PublicKey, "alice1") + err = s.services[aliceUser].services[1].EnableInstallation(&aliceKey.PublicKey, "alice1") s.Require().NoError(err) // It should contain both bundles - alice2MergedBundle1, err := s.services[aliceUser].encryptionServices[1].CreateBundle(aliceKey) + alice2MergedBundle1, err := s.services[aliceUser].services[1].GetBundle(aliceKey) s.Require().NoError(err) s.Require().NotNil(alice2MergedBundle1.GetSignedPreKeys()["alice1"]) @@ -170,9 +193,9 @@ func (s *EncryptionServiceMultiDeviceSuite) TestProcessPublicBundleOutOfOrder() } func pairDevices(s *serviceAndKey, target int) error { - device := s.encryptionServices[target] - for i := 0; i < len(s.encryptionServices); i++ { - b, err := s.encryptionServices[i].CreateBundle(s.key) + device := s.services[target] + for i := 0; i < len(s.services); i++ { + b, err := s.services[i].GetBundle(s.key) if err != nil { return err @@ -183,7 +206,7 @@ func pairDevices(s *serviceAndKey, target int) error { return err } - err = device.EnableInstallation(&s.key.PublicKey, s.encryptionServices[i].config.InstallationID) + err = device.EnableInstallation(&s.key.PublicKey, s.services[i].encryption.config.InstallationID) if err != nil { return nil } @@ -194,14 +217,14 @@ func pairDevices(s *serviceAndKey, target int) error { func (s *EncryptionServiceMultiDeviceSuite) TestMaxDevices() { err := pairDevices(s.services[aliceUser], 0) s.Require().NoError(err) - alice1 := s.services[aliceUser].encryptionServices[0] - bob1 := s.services[bobUser].encryptionServices[0] + alice1 := s.services[aliceUser].services[0] + bob1 := s.services[bobUser].services[0] aliceKey := s.services[aliceUser].key bobKey := s.services[bobUser].key // Check bundle is ok // No installation is enabled - aliceBundle, err := alice1.CreateBundle(aliceKey) + aliceBundle, err := alice1.GetBundle(aliceKey) s.Require().NoError(err) // Check all installations are correctly working, and that the oldest device is not there @@ -218,19 +241,20 @@ func (s *EncryptionServiceMultiDeviceSuite) TestMaxDevices() { s.Require().NoError(err) // Bob sends a message to alice - payload, err := bob1.EncryptPayload(&aliceKey.PublicKey, bobKey, []byte("test")) + msg, err := bob1.BuildDirectMessage(bobKey, &aliceKey.PublicKey, []byte("test")) s.Require().NoError(err) + payload := msg.Message.GetDirectMessage() s.Require().Equal(3, len(payload)) s.Require().NotNil(payload["alice1"]) s.Require().NotNil(payload["alice3"]) s.Require().NotNil(payload["alice4"]) // We disable the last installation - err = s.services[aliceUser].encryptionServices[0].DisableInstallation(&aliceKey.PublicKey, "alice4") + err = s.services[aliceUser].services[0].DisableInstallation(&aliceKey.PublicKey, "alice4") s.Require().NoError(err) // We check the bundle is updated - aliceBundle, err = alice1.CreateBundle(aliceKey) + aliceBundle, err = alice1.GetBundle(aliceKey) s.Require().NoError(err) // Check all installations are there @@ -247,8 +271,9 @@ func (s *EncryptionServiceMultiDeviceSuite) TestMaxDevices() { s.Require().NoError(err) // Bob sends a message to alice - payload, err = bob1.EncryptPayload(&aliceKey.PublicKey, bobKey, []byte("test")) + msg, err = bob1.BuildDirectMessage(bobKey, &aliceKey.PublicKey, []byte("test")) s.Require().NoError(err) + payload = msg.Message.GetDirectMessage() s.Require().Equal(3, len(payload)) s.Require().NotNil(payload["alice1"]) s.Require().NotNil(payload["alice2"]) diff --git a/services/shhext/chat/encryption_test.go b/services/shhext/chat/encryption_test.go index 2f84b49c3..d88f237de 100644 --- a/services/shhext/chat/encryption_test.go +++ b/services/shhext/chat/encryption_test.go @@ -13,6 +13,9 @@ import ( "time" "github.com/ethereum/go-ethereum/crypto" + "github.com/status-im/status-go/services/shhext/chat/multidevice" + "github.com/status-im/status-go/services/shhext/chat/protobuf" + "github.com/status-im/status-go/services/shhext/chat/sharedsecret" "github.com/stretchr/testify/suite" ) @@ -27,8 +30,8 @@ func TestEncryptionServiceTestSuite(t *testing.T) { type EncryptionServiceTestSuite struct { suite.Suite - alice *EncryptionService - bob *EncryptionService + alice *ProtocolService + bob *ProtocolService aliceDBPath string bobDBPath string } @@ -56,21 +59,57 @@ func (s *EncryptionServiceTestSuite) initDatabases(baseConfig *EncryptionService bobDBKey = "bob" ) + aliceMultideviceConfig := &multidevice.Config{ + MaxInstallations: 3, + InstallationID: aliceInstallationID, + ProtocolVersion: 1, + } + alicePersistence, err := NewSQLLitePersistence(aliceDBPath, aliceDBKey) if err != nil { panic(err) } + baseConfig.InstallationID = aliceInstallationID + aliceEncryptionService := NewEncryptionService(alicePersistence, *baseConfig) + + aliceSharedSecretService := sharedsecret.NewService(alicePersistence.GetSharedSecretStorage()) + aliceMultideviceService := multidevice.New(aliceMultideviceConfig, alicePersistence.GetMultideviceStorage()) + + s.alice = NewProtocolService( + aliceEncryptionService, + aliceSharedSecretService, + aliceMultideviceService, + func(s []multidevice.IdentityAndIDPair) {}, + func(s []*sharedsecret.Secret) {}, + ) + bobPersistence, err := NewSQLLitePersistence(bobDBPath, bobDBKey) if err != nil { panic(err) } - baseConfig.InstallationID = aliceInstallationID - s.alice = NewEncryptionService(alicePersistence, *baseConfig) + bobMultideviceConfig := &multidevice.Config{ + MaxInstallations: 3, + InstallationID: bobInstallationID, + ProtocolVersion: 1, + } + + bobMultideviceService := multidevice.New(bobMultideviceConfig, bobPersistence.GetMultideviceStorage()) + + bobSharedSecretService := sharedsecret.NewService(bobPersistence.GetSharedSecretStorage()) baseConfig.InstallationID = bobInstallationID - s.bob = NewEncryptionService(bobPersistence, *baseConfig) + bobEncryptionService := NewEncryptionService(bobPersistence, *baseConfig) + + s.bob = NewProtocolService( + bobEncryptionService, + bobSharedSecretService, + bobMultideviceService, + func(s []multidevice.IdentityAndIDPair) {}, + func(s []*sharedsecret.Secret) {}, + ) + } func (s *EncryptionServiceTestSuite) SetupTest() { @@ -82,14 +121,14 @@ func (s *EncryptionServiceTestSuite) TearDownTest() { os.Remove(s.bobDBPath) } -func (s *EncryptionServiceTestSuite) TestCreateBundle() { +func (s *EncryptionServiceTestSuite) TestGetBundle() { aliceKey, err := crypto.GenerateKey() s.Require().NoError(err) - aliceBundle1, err := s.alice.CreateBundle(aliceKey) + aliceBundle1, err := s.alice.GetBundle(aliceKey) s.Require().NoError(err) s.NotNil(aliceBundle1, "It creates a bundle") - aliceBundle2, err := s.alice.CreateBundle(aliceKey) + aliceBundle2, err := s.alice.GetBundle(aliceKey) s.Require().NoError(err) s.Equal(aliceBundle1, aliceBundle2, "It returns the same bundle") } @@ -105,9 +144,11 @@ func (s *EncryptionServiceTestSuite) TestEncryptPayloadNoBundle() { aliceKey, err := crypto.GenerateKey() s.Require().NoError(err) - encryptionResponse1, err := s.alice.EncryptPayload(&bobKey.PublicKey, aliceKey, cleartext) + response1, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, cleartext) s.Require().NoError(err) + encryptionResponse1 := response1.Message.GetDirectMessage() + installationResponse1 := encryptionResponse1["none"] // That's for any device s.Require().NotNil(installationResponse1) @@ -119,14 +160,16 @@ func (s *EncryptionServiceTestSuite) TestEncryptPayloadNoBundle() { s.NotEqual(cyphertext1, cleartext, "It encrypts the payload correctly") // On the receiver side, we should be able to decrypt using our private key and the ephemeral just sent - decryptedPayload1, err := s.bob.DecryptPayload(bobKey, &aliceKey.PublicKey, aliceInstallationID, encryptionResponse1, defaultMessageID) + decryptedPayload1, err := s.bob.HandleMessage(bobKey, &aliceKey.PublicKey, response1.Message, defaultMessageID) s.Require().NoError(err) s.Equal(cleartext, decryptedPayload1, "It correctly decrypts the payload using DH") // The next message will not be re-using the same key - encryptionResponse2, err := s.alice.EncryptPayload(&bobKey.PublicKey, aliceKey, cleartext) + response2, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, cleartext) s.Require().NoError(err) + encryptionResponse2 := response2.Message.GetDirectMessage() + installationResponse2 := encryptionResponse2[aliceInstallationID] cyphertext2 := installationResponse2.GetPayload() @@ -134,7 +177,7 @@ func (s *EncryptionServiceTestSuite) TestEncryptPayloadNoBundle() { s.NotEqual(cyphertext1, cyphertext2, "It does not re-use the symmetric key") s.NotEqual(ephemeralKey1, ephemeralKey2, "It does not re-use the ephemeral key") - decryptedPayload2, err := s.bob.DecryptPayload(bobKey, &aliceKey.PublicKey, aliceInstallationID, encryptionResponse2, defaultMessageID) + decryptedPayload2, err := s.bob.HandleMessage(bobKey, &aliceKey.PublicKey, response2.Message, defaultMessageID) s.Require().NoError(err) s.Equal(cleartext, decryptedPayload2, "It correctly decrypts the payload using DH") } @@ -150,7 +193,7 @@ func (s *EncryptionServiceTestSuite) TestEncryptPayloadBundle() { s.Require().NoError(err) // Create a bundle - bobBundle, err := s.bob.CreateBundle(bobKey) + bobBundle, err := s.bob.GetBundle(bobKey) s.Require().NoError(err) // We add bob bundle @@ -158,9 +201,11 @@ func (s *EncryptionServiceTestSuite) TestEncryptPayloadBundle() { s.Require().NoError(err) // We send a message using the bundle - encryptionResponse1, err := s.alice.EncryptPayload(&bobKey.PublicKey, aliceKey, cleartext) + response1, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, cleartext) s.Require().NoError(err) + encryptionResponse1 := response1.Message.GetDirectMessage() + installationResponse1 := encryptionResponse1[bobInstallationID] s.Require().NotNil(installationResponse1) @@ -186,7 +231,7 @@ func (s *EncryptionServiceTestSuite) TestEncryptPayloadBundle() { s.Equal(uint32(0), drHeader.GetPn(), "It adds the correct length of the message chain") // Bob is able to decrypt it using the bundle - decryptedPayload1, err := s.bob.DecryptPayload(bobKey, &aliceKey.PublicKey, aliceInstallationID, encryptionResponse1, defaultMessageID) + decryptedPayload1, err := s.bob.HandleMessage(bobKey, &aliceKey.PublicKey, response1.Message, defaultMessageID) s.Require().NoError(err) s.Equal(cleartext, decryptedPayload1, "It correctly decrypts the payload using X3DH") } @@ -209,7 +254,7 @@ func (s *EncryptionServiceTestSuite) TestConsequentMessagesBundle() { s.Require().NoError(err) // Create a bundle - bobBundle, err := s.bob.CreateBundle(bobKey) + bobBundle, err := s.bob.GetBundle(bobKey) s.Require().NoError(err) // We add bob bundle @@ -217,12 +262,13 @@ func (s *EncryptionServiceTestSuite) TestConsequentMessagesBundle() { s.Require().NoError(err) // We send a message using the bundle - _, err = s.alice.EncryptPayload(&bobKey.PublicKey, aliceKey, cleartext1) + _, err = s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, cleartext1) s.Require().NoError(err) // We send another message using the bundle - encryptionResponse, err := s.alice.EncryptPayload(&bobKey.PublicKey, aliceKey, cleartext2) + response, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, cleartext2) s.Require().NoError(err) + encryptionResponse := response.Message.GetDirectMessage() installationResponse := encryptionResponse[bobInstallationID] s.Require().NotNil(installationResponse) @@ -250,7 +296,7 @@ func (s *EncryptionServiceTestSuite) TestConsequentMessagesBundle() { s.Equal(uint32(0), drHeader.GetPn(), "It adds the correct length of the message chain") // Bob is able to decrypt it using the bundle - decryptedPayload1, err := s.bob.DecryptPayload(bobKey, &aliceKey.PublicKey, aliceInstallationID, encryptionResponse, defaultMessageID) + decryptedPayload1, err := s.bob.HandleMessage(bobKey, &aliceKey.PublicKey, response.Message, defaultMessageID) s.Require().NoError(err) s.Equal(cleartext2, decryptedPayload1, "It correctly decrypts the payload using X3DH") @@ -274,11 +320,11 @@ func (s *EncryptionServiceTestSuite) TestConversation() { s.Require().NoError(err) // Create a bundle - bobBundle, err := s.bob.CreateBundle(bobKey) + bobBundle, err := s.bob.GetBundle(bobKey) s.Require().NoError(err) // Create a bundle - aliceBundle, err := s.alice.CreateBundle(aliceKey) + aliceBundle, err := s.alice.GetBundle(aliceKey) s.Require().NoError(err) // We add bob bundle @@ -290,24 +336,25 @@ func (s *EncryptionServiceTestSuite) TestConversation() { s.Require().NoError(err) // Alice sends a message - encryptionResponse, err := s.alice.EncryptPayload(&bobKey.PublicKey, aliceKey, cleartext1) + response, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, cleartext1) s.Require().NoError(err) // Bob receives the message - _, err = s.bob.DecryptPayload(bobKey, &aliceKey.PublicKey, aliceInstallationID, encryptionResponse, defaultMessageID) + _, err = s.bob.HandleMessage(bobKey, &aliceKey.PublicKey, response.Message, defaultMessageID) s.Require().NoError(err) // Bob replies to the message - encryptionResponse, err = s.bob.EncryptPayload(&aliceKey.PublicKey, bobKey, cleartext1) + response, err = s.bob.BuildDirectMessage(bobKey, &aliceKey.PublicKey, cleartext1) s.Require().NoError(err) // Alice receives the message - _, err = s.alice.DecryptPayload(aliceKey, &bobKey.PublicKey, bobInstallationID, encryptionResponse, defaultMessageID) + _, err = s.alice.HandleMessage(aliceKey, &bobKey.PublicKey, response.Message, defaultMessageID) s.Require().NoError(err) // We send another message using the bundle - encryptionResponse, err = s.alice.EncryptPayload(&bobKey.PublicKey, aliceKey, cleartext2) + response, err = s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, cleartext2) s.Require().NoError(err) + encryptionResponse := response.Message.GetDirectMessage() installationResponse := encryptionResponse[bobInstallationID] s.Require().NotNil(installationResponse) @@ -333,7 +380,7 @@ func (s *EncryptionServiceTestSuite) TestConversation() { s.Equal(uint32(1), drHeader.GetPn(), "It adds the correct length of the message chain") // Bob is able to decrypt it using the bundle - decryptedPayload1, err := s.bob.DecryptPayload(bobKey, &aliceKey.PublicKey, aliceInstallationID, encryptionResponse, defaultMessageID) + decryptedPayload1, err := s.bob.HandleMessage(bobKey, &aliceKey.PublicKey, response.Message, defaultMessageID) s.Require().NoError(err) s.Equal(cleartext2, decryptedPayload1, "It correctly decrypts the payload using X3DH") @@ -354,7 +401,7 @@ func (s *EncryptionServiceTestSuite) TestMaxSkipKeys() { s.Require().NoError(err) // Create a bundle - bobBundle, err := s.bob.CreateBundle(bobKey) + bobBundle, err := s.bob.GetBundle(bobKey) s.Require().NoError(err) // We add bob bundle @@ -362,7 +409,7 @@ func (s *EncryptionServiceTestSuite) TestMaxSkipKeys() { s.Require().NoError(err) // Create a bundle - aliceBundle, err := s.alice.CreateBundle(aliceKey) + aliceBundle, err := s.alice.GetBundle(aliceKey) s.Require().NoError(err) // We add alice bundle @@ -371,30 +418,30 @@ func (s *EncryptionServiceTestSuite) TestMaxSkipKeys() { // Bob sends a message - for i := 0; i < s.alice.config.MaxSkip; i++ { - _, err = s.bob.EncryptPayload(&aliceKey.PublicKey, bobKey, bobText) + for i := 0; i < s.alice.encryption.config.MaxSkip; i++ { + _, err = s.bob.BuildDirectMessage(bobKey, &aliceKey.PublicKey, bobText) s.Require().NoError(err) } // Bob sends a message - bobMessage1, err := s.bob.EncryptPayload(&aliceKey.PublicKey, bobKey, bobText) + bobMessage1, err := s.bob.BuildDirectMessage(bobKey, &aliceKey.PublicKey, bobText) s.Require().NoError(err) // Alice receives the message - _, err = s.alice.DecryptPayload(aliceKey, &bobKey.PublicKey, bobInstallationID, bobMessage1, defaultMessageID) + _, err = s.alice.HandleMessage(aliceKey, &bobKey.PublicKey, bobMessage1.Message, defaultMessageID) s.Require().NoError(err) // Bob sends a message - _, err = s.bob.EncryptPayload(&aliceKey.PublicKey, bobKey, bobText) + _, err = s.bob.BuildDirectMessage(bobKey, &aliceKey.PublicKey, bobText) s.Require().NoError(err) // Bob sends a message - bobMessage2, err := s.bob.EncryptPayload(&aliceKey.PublicKey, bobKey, bobText) + bobMessage2, err := s.bob.BuildDirectMessage(bobKey, &aliceKey.PublicKey, bobText) s.Require().NoError(err) // Alice receives the message, we should have maxSkip + 1 keys in the db, but // we should not throw an error - _, err = s.alice.DecryptPayload(aliceKey, &bobKey.PublicKey, bobInstallationID, bobMessage2, defaultMessageID) + _, err = s.alice.HandleMessage(aliceKey, &bobKey.PublicKey, bobMessage2.Message, defaultMessageID) s.Require().NoError(err) } @@ -409,7 +456,7 @@ func (s *EncryptionServiceTestSuite) TestMaxSkipKeysError() { s.Require().NoError(err) // Create a bundle - bobBundle, err := s.bob.CreateBundle(bobKey) + bobBundle, err := s.bob.GetBundle(bobKey) s.Require().NoError(err) // We add bob bundle @@ -417,7 +464,7 @@ func (s *EncryptionServiceTestSuite) TestMaxSkipKeysError() { s.Require().NoError(err) // Create a bundle - aliceBundle, err := s.alice.CreateBundle(aliceKey) + aliceBundle, err := s.alice.GetBundle(aliceKey) s.Require().NoError(err) // We add alice bundle @@ -426,17 +473,17 @@ func (s *EncryptionServiceTestSuite) TestMaxSkipKeysError() { // Bob sends a message - for i := 0; i < s.alice.config.MaxSkip+1; i++ { - _, err = s.bob.EncryptPayload(&aliceKey.PublicKey, bobKey, bobText) + for i := 0; i < s.alice.encryption.config.MaxSkip+1; i++ { + _, err = s.bob.BuildDirectMessage(bobKey, &aliceKey.PublicKey, bobText) s.Require().NoError(err) } // Bob sends a message - bobMessage1, err := s.bob.EncryptPayload(&aliceKey.PublicKey, bobKey, bobText) + bobMessage1, err := s.bob.BuildDirectMessage(bobKey, &aliceKey.PublicKey, bobText) s.Require().NoError(err) // Alice receives the message - _, err = s.alice.DecryptPayload(aliceKey, &bobKey.PublicKey, bobInstallationID, bobMessage1, defaultMessageID) + _, err = s.alice.HandleMessage(aliceKey, &bobKey.PublicKey, bobMessage1.Message, defaultMessageID) s.Require().Equal(errors.New("can't skip current chain message keys: too many messages"), err) } @@ -457,7 +504,7 @@ func (s *EncryptionServiceTestSuite) TestMaxMessageKeysPerSession() { s.Require().NoError(err) // Create a bundle - bobBundle, err := s.bob.CreateBundle(bobKey) + bobBundle, err := s.bob.GetBundle(bobKey) s.Require().NoError(err) // We add bob bundle @@ -465,7 +512,7 @@ func (s *EncryptionServiceTestSuite) TestMaxMessageKeysPerSession() { s.Require().NoError(err) // Create a bundle - aliceBundle, err := s.alice.CreateBundle(aliceKey) + aliceBundle, err := s.alice.GetBundle(aliceKey) s.Require().NoError(err) // We add alice bundle @@ -474,27 +521,27 @@ func (s *EncryptionServiceTestSuite) TestMaxMessageKeysPerSession() { // We create just enough messages so that the first key should be deleted - nMessages := s.alice.config.MaxMessageKeysPerSession - messages := make([]map[string]*DirectMessageProtocol, nMessages) + nMessages := s.alice.encryption.config.MaxMessageKeysPerSession + messages := make([]*protobuf.ProtocolMessage, nMessages) for i := 0; i < nMessages; i++ { - m, err := s.bob.EncryptPayload(&aliceKey.PublicKey, bobKey, bobText) + m, err := s.bob.BuildDirectMessage(bobKey, &aliceKey.PublicKey, bobText) s.Require().NoError(err) - messages[i] = m + messages[i] = m.Message } // Another message to trigger the deletion - m, err := s.bob.EncryptPayload(&aliceKey.PublicKey, bobKey, bobText) + m, err := s.bob.BuildDirectMessage(bobKey, &aliceKey.PublicKey, bobText) s.Require().NoError(err) - _, err = s.alice.DecryptPayload(aliceKey, &bobKey.PublicKey, bobInstallationID, m, defaultMessageID) + _, err = s.alice.HandleMessage(aliceKey, &bobKey.PublicKey, m.Message, defaultMessageID) s.Require().NoError(err) // We decrypt the first message, and it should fail - _, err = s.alice.DecryptPayload(aliceKey, &bobKey.PublicKey, bobInstallationID, messages[0], defaultMessageID) + _, err = s.alice.HandleMessage(aliceKey, &bobKey.PublicKey, messages[0], defaultMessageID) s.Require().Equal(errors.New("can't skip current chain message keys: bad until: probably an out-of-order message that was deleted"), err) // We decrypt the second message, and it should be decrypted - _, err = s.alice.DecryptPayload(aliceKey, &bobKey.PublicKey, bobInstallationID, messages[1], defaultMessageID) + _, err = s.alice.HandleMessage(aliceKey, &bobKey.PublicKey, messages[1], defaultMessageID) s.Require().NoError(err) } @@ -514,7 +561,7 @@ func (s *EncryptionServiceTestSuite) TestMaxKeep() { s.Require().NoError(err) // Create a bundle - bobBundle, err := s.bob.CreateBundle(bobKey) + bobBundle, err := s.bob.GetBundle(bobKey) s.Require().NoError(err) // We add bob bundle @@ -522,7 +569,7 @@ func (s *EncryptionServiceTestSuite) TestMaxKeep() { s.Require().NoError(err) // Create a bundle - aliceBundle, err := s.alice.CreateBundle(aliceKey) + aliceBundle, err := s.alice.GetBundle(aliceKey) s.Require().NoError(err) // We add alice bundle @@ -530,15 +577,15 @@ func (s *EncryptionServiceTestSuite) TestMaxKeep() { s.Require().NoError(err) // We decrypt all messages but 1 & 2 - messages := make([]map[string]*DirectMessageProtocol, s.alice.config.MaxKeep) - for i := 0; i < s.alice.config.MaxKeep; i++ { - m, err := s.bob.EncryptPayload(&aliceKey.PublicKey, bobKey, bobText) - messages[i] = m + messages := make([]*protobuf.ProtocolMessage, s.alice.encryption.config.MaxKeep) + for i := 0; i < s.alice.encryption.config.MaxKeep; i++ { + m, err := s.bob.BuildDirectMessage(bobKey, &aliceKey.PublicKey, bobText) + messages[i] = m.Message s.Require().NoError(err) if i != 0 && i != 1 { messageID := []byte(fmt.Sprintf("%d", i)) - _, err = s.alice.DecryptPayload(aliceKey, &bobKey.PublicKey, bobInstallationID, m, messageID) + _, err = s.alice.HandleMessage(aliceKey, &bobKey.PublicKey, m.Message, messageID) s.Require().NoError(err) err = s.alice.ConfirmMessagesProcessed([][]byte{messageID}) s.Require().NoError(err) @@ -547,11 +594,11 @@ func (s *EncryptionServiceTestSuite) TestMaxKeep() { } // We decrypt the first message, and it should fail, as it should have been removed - _, err = s.alice.DecryptPayload(aliceKey, &bobKey.PublicKey, bobInstallationID, messages[0], defaultMessageID) + _, err = s.alice.HandleMessage(aliceKey, &bobKey.PublicKey, messages[0], defaultMessageID) s.Require().Equal(errors.New("can't skip current chain message keys: bad until: probably an out-of-order message that was deleted"), err) // We decrypt the second message, and it should be decrypted - _, err = s.alice.DecryptPayload(aliceKey, &bobKey.PublicKey, bobInstallationID, messages[1], defaultMessageID) + _, err = s.alice.HandleMessage(aliceKey, &bobKey.PublicKey, messages[1], defaultMessageID) s.Require().NoError(err) } @@ -576,7 +623,7 @@ func (s *EncryptionServiceTestSuite) TestConcurrentBundles() { s.Require().NoError(err) // Create a bundle - bobBundle, err := s.bob.CreateBundle(bobKey) + bobBundle, err := s.bob.GetBundle(bobKey) s.Require().NoError(err) // We add bob bundle @@ -584,7 +631,7 @@ func (s *EncryptionServiceTestSuite) TestConcurrentBundles() { s.Require().NoError(err) // Create a bundle - aliceBundle, err := s.alice.CreateBundle(aliceKey) + aliceBundle, err := s.alice.GetBundle(aliceKey) s.Require().NoError(err) // We add alice bundle @@ -592,44 +639,44 @@ func (s *EncryptionServiceTestSuite) TestConcurrentBundles() { s.Require().NoError(err) // Alice sends a message - aliceMessage1, err := s.alice.EncryptPayload(&bobKey.PublicKey, aliceKey, aliceText1) + aliceMessage1, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, aliceText1) s.Require().NoError(err) // Bob sends a message - bobMessage1, err := s.bob.EncryptPayload(&aliceKey.PublicKey, bobKey, bobText1) + bobMessage1, err := s.bob.BuildDirectMessage(bobKey, &aliceKey.PublicKey, bobText1) s.Require().NoError(err) // Bob receives the message - _, err = s.bob.DecryptPayload(bobKey, &aliceKey.PublicKey, aliceInstallationID, aliceMessage1, defaultMessageID) + _, err = s.bob.HandleMessage(bobKey, &aliceKey.PublicKey, aliceMessage1.Message, defaultMessageID) s.Require().NoError(err) // Alice receives the message - _, err = s.alice.DecryptPayload(aliceKey, &bobKey.PublicKey, bobInstallationID, bobMessage1, defaultMessageID) + _, err = s.alice.HandleMessage(aliceKey, &bobKey.PublicKey, bobMessage1.Message, defaultMessageID) s.Require().NoError(err) // Bob replies to the message - bobMessage2, err := s.bob.EncryptPayload(&aliceKey.PublicKey, bobKey, bobText2) + bobMessage2, err := s.bob.BuildDirectMessage(bobKey, &aliceKey.PublicKey, bobText2) s.Require().NoError(err) // Alice sends a message - aliceMessage2, err := s.alice.EncryptPayload(&bobKey.PublicKey, aliceKey, aliceText2) + aliceMessage2, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, aliceText2) s.Require().NoError(err) // Alice receives the message - _, err = s.alice.DecryptPayload(aliceKey, &bobKey.PublicKey, bobInstallationID, bobMessage2, defaultMessageID) + _, err = s.alice.HandleMessage(aliceKey, &bobKey.PublicKey, bobMessage2.Message, defaultMessageID) s.Require().NoError(err) // Bob receives the message - _, err = s.bob.DecryptPayload(bobKey, &aliceKey.PublicKey, aliceInstallationID, aliceMessage2, defaultMessageID) + _, err = s.bob.HandleMessage(bobKey, &aliceKey.PublicKey, aliceMessage2.Message, defaultMessageID) s.Require().NoError(err) } func publisher( - e *EncryptionService, + e *ProtocolService, privateKey *ecdsa.PrivateKey, publicKey *ecdsa.PublicKey, errChan chan error, - output chan map[string]*DirectMessageProtocol, + output chan *protobuf.ProtocolMessage, ) { var wg sync.WaitGroup @@ -642,13 +689,13 @@ func publisher( go func() { defer wg.Done() time.Sleep(time.Duration(rand.Intn(50)) * time.Millisecond) - response, err := e.EncryptPayload(publicKey, privateKey, cleartext) + response, err := e.BuildDirectMessage(privateKey, publicKey, cleartext) if err != nil { errChan <- err return } - output <- response + output <- response.Message }() } } @@ -658,17 +705,16 @@ func publisher( } func receiver( - s *EncryptionService, + s *ProtocolService, privateKey *ecdsa.PrivateKey, publicKey *ecdsa.PublicKey, - installationID string, errChan chan error, - input chan map[string]*DirectMessageProtocol, + input chan *protobuf.ProtocolMessage, ) { i := 0 for payload := range input { - actualCleartext, err := s.DecryptPayload(privateKey, publicKey, installationID, payload, defaultMessageID) + actualCleartext, err := s.HandleMessage(privateKey, publicKey, payload, defaultMessageID) if err != nil { errChan <- err return @@ -697,7 +743,7 @@ func (s *EncryptionServiceTestSuite) TestRandomised() { s.Require().NoError(err) // Create a bundle - bobBundle, err := s.bob.CreateBundle(bobKey) + bobBundle, err := s.bob.GetBundle(bobKey) s.Require().NoError(err) // We add bob bundle @@ -705,15 +751,15 @@ func (s *EncryptionServiceTestSuite) TestRandomised() { s.Require().NoError(err) // Create a bundle - aliceBundle, err := s.alice.CreateBundle(aliceKey) + aliceBundle, err := s.alice.GetBundle(aliceKey) s.Require().NoError(err) // We add alice bundle _, err = s.bob.ProcessPublicBundle(bobKey, aliceBundle) s.Require().NoError(err) - aliceChan := make(chan map[string]*DirectMessageProtocol, 100) - bobChan := make(chan map[string]*DirectMessageProtocol, 100) + aliceChan := make(chan *protobuf.ProtocolMessage, 100) + bobChan := make(chan *protobuf.ProtocolMessage, 100) alicePublisherErrChan := make(chan error, 1) bobPublisherErrChan := make(chan error, 1) @@ -727,10 +773,10 @@ func (s *EncryptionServiceTestSuite) TestRandomised() { go publisher(s.bob, bobKey, &aliceKey.PublicKey, bobPublisherErrChan, aliceChan) // Set up bob receiver - go receiver(s.bob, bobKey, &aliceKey.PublicKey, aliceInstallationID, bobReceiverErrChan, bobChan) + go receiver(s.bob, bobKey, &aliceKey.PublicKey, bobReceiverErrChan, bobChan) // Set up alice receiver - go receiver(s.alice, aliceKey, &bobKey.PublicKey, bobInstallationID, aliceReceiverErrChan, aliceChan) + go receiver(s.alice, aliceKey, &bobKey.PublicKey, aliceReceiverErrChan, aliceChan) aliceErr := <-alicePublisherErrChan s.Require().NoError(aliceErr) @@ -771,11 +817,11 @@ func (s *EncryptionServiceTestSuite) TestBundleNotExisting() { s.Require().NoError(err) // Alice sends a message - aliceMessage, err := s.alice.EncryptPayload(&bobKey.PublicKey, aliceKey, aliceText) + aliceMessage, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, aliceText) s.Require().NoError(err) // Bob receives the message, and returns a bundlenotfound error - _, err = s.bob.DecryptPayload(bobKey, &aliceKey.PublicKey, aliceInstallationID, aliceMessage, defaultMessageID) + _, err = s.bob.HandleMessage(bobKey, &aliceKey.PublicKey, aliceMessage.Message, defaultMessageID) s.Require().Error(err) s.Equal(ErrSessionNotFound, err) } @@ -804,11 +850,11 @@ func (s *EncryptionServiceTestSuite) TestDeviceNotIncluded() { s.Require().NoError(err) // Alice sends a message - aliceMessage, err := s.alice.EncryptPayload(&bobKey.PublicKey, aliceKey, []byte("does not matter")) + aliceMessage, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, []byte("does not matter")) s.Require().NoError(err) // Bob receives the message, and returns a bundlenotfound error - _, err = s.bob.DecryptPayload(bobKey, &aliceKey.PublicKey, aliceInstallationID, aliceMessage, defaultMessageID) + _, err = s.bob.HandleMessage(bobKey, &aliceKey.PublicKey, aliceMessage.Message, defaultMessageID) s.Require().Error(err) s.Equal(ErrDeviceNotFound, err) } @@ -829,7 +875,7 @@ func (s *EncryptionServiceTestSuite) TestRefreshedBundle() { s.Require().NoError(err) // Create bundles - bobBundle1, err := s.bob.CreateBundle(bobKey) + bobBundle1, err := s.bob.GetBundle(bobKey) s.Require().NoError(err) s.Require().Equal(uint32(1), bobBundle1.GetSignedPreKeys()[bobInstallationID].GetVersion()) @@ -837,7 +883,7 @@ func (s *EncryptionServiceTestSuite) TestRefreshedBundle() { time.Sleep(time.Duration(config.BundleRefreshInterval) * time.Millisecond) // Create bundles - bobBundle2, err := s.bob.CreateBundle(bobKey) + bobBundle2, err := s.bob.GetBundle(bobKey) s.Require().NoError(err) s.Require().Equal(uint32(2), bobBundle2.GetSignedPreKeys()[bobInstallationID].GetVersion()) @@ -846,8 +892,9 @@ func (s *EncryptionServiceTestSuite) TestRefreshedBundle() { s.Require().NoError(err) // Alice sends a message - encryptionResponse1, err := s.alice.EncryptPayload(&bobKey.PublicKey, aliceKey, []byte("anything")) + response1, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, []byte("anything")) s.Require().NoError(err) + encryptionResponse1 := response1.Message.GetDirectMessage() installationResponse1 := encryptionResponse1[bobInstallationID] s.Require().NotNil(installationResponse1) @@ -859,7 +906,7 @@ func (s *EncryptionServiceTestSuite) TestRefreshedBundle() { s.Equal(bobBundle1.GetSignedPreKeys()[bobInstallationID].GetSignedPreKey(), x3dhHeader1.GetId()) // Bob decrypts the message - _, err = s.bob.DecryptPayload(bobKey, &aliceKey.PublicKey, aliceInstallationID, encryptionResponse1, defaultMessageID) + _, err = s.bob.HandleMessage(bobKey, &aliceKey.PublicKey, response1.Message, defaultMessageID) s.Require().NoError(err) // We add the second bob bundle @@ -867,8 +914,9 @@ func (s *EncryptionServiceTestSuite) TestRefreshedBundle() { s.Require().NoError(err) // Alice sends a message - encryptionResponse2, err := s.alice.EncryptPayload(&bobKey.PublicKey, aliceKey, []byte("anything")) + response2, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, []byte("anything")) s.Require().NoError(err) + encryptionResponse2 := response2.Message.GetDirectMessage() installationResponse2 := encryptionResponse2[bobInstallationID] s.Require().NotNil(installationResponse2) @@ -880,7 +928,7 @@ func (s *EncryptionServiceTestSuite) TestRefreshedBundle() { s.Equal(bobBundle2.GetSignedPreKeys()[bobInstallationID].GetSignedPreKey(), x3dhHeader2.GetId()) // Bob decrypts the message - _, err = s.bob.DecryptPayload(bobKey, &aliceKey.PublicKey, aliceInstallationID, encryptionResponse2, defaultMessageID) + _, err = s.bob.HandleMessage(bobKey, &aliceKey.PublicKey, response2.Message, defaultMessageID) s.Require().NoError(err) } @@ -894,7 +942,7 @@ func (s *EncryptionServiceTestSuite) TestMessageConfirmation() { s.Require().NoError(err) // Create a bundle - bobBundle, err := s.bob.CreateBundle(bobKey) + bobBundle, err := s.bob.GetBundle(bobKey) s.Require().NoError(err) // We add bob bundle @@ -902,7 +950,7 @@ func (s *EncryptionServiceTestSuite) TestMessageConfirmation() { s.Require().NoError(err) // Create a bundle - aliceBundle, err := s.alice.CreateBundle(aliceKey) + aliceBundle, err := s.alice.GetBundle(aliceKey) s.Require().NoError(err) // We add alice bundle @@ -910,16 +958,16 @@ func (s *EncryptionServiceTestSuite) TestMessageConfirmation() { s.Require().NoError(err) // Bob sends a message - bobMessage1, err := s.bob.EncryptPayload(&aliceKey.PublicKey, bobKey, bobText1) + bobMessage1, err := s.bob.BuildDirectMessage(bobKey, &aliceKey.PublicKey, bobText1) s.Require().NoError(err) bobMessage1ID := []byte("bob-message-1-id") // Alice receives the message once - _, err = s.alice.DecryptPayload(aliceKey, &bobKey.PublicKey, bobInstallationID, bobMessage1, bobMessage1ID) + _, err = s.alice.HandleMessage(aliceKey, &bobKey.PublicKey, bobMessage1.Message, bobMessage1ID) s.Require().NoError(err) // Alice receives the message twice - _, err = s.alice.DecryptPayload(aliceKey, &bobKey.PublicKey, bobInstallationID, bobMessage1, bobMessage1ID) + _, err = s.alice.HandleMessage(aliceKey, &bobKey.PublicKey, bobMessage1.Message, bobMessage1ID) s.Require().NoError(err) // Alice confirms the message @@ -927,33 +975,33 @@ func (s *EncryptionServiceTestSuite) TestMessageConfirmation() { s.Require().NoError(err) // Alice decrypts it again, it should fail - _, err = s.alice.DecryptPayload(aliceKey, &bobKey.PublicKey, bobInstallationID, bobMessage1, bobMessage1ID) + _, err = s.alice.HandleMessage(aliceKey, &bobKey.PublicKey, bobMessage1.Message, bobMessage1ID) s.Require().Equal(errors.New("can't skip current chain message keys: bad until: probably an out-of-order message that was deleted"), err) // Bob sends a message - bobMessage2, err := s.bob.EncryptPayload(&aliceKey.PublicKey, bobKey, bobText1) + bobMessage2, err := s.bob.BuildDirectMessage(bobKey, &aliceKey.PublicKey, bobText1) s.Require().NoError(err) bobMessage2ID := []byte("bob-message-2-id") // Bob sends a message - bobMessage3, err := s.bob.EncryptPayload(&aliceKey.PublicKey, bobKey, bobText1) + bobMessage3, err := s.bob.BuildDirectMessage(bobKey, &aliceKey.PublicKey, bobText1) s.Require().NoError(err) bobMessage3ID := []byte("bob-message-3-id") // Alice receives message 3 once - _, err = s.alice.DecryptPayload(aliceKey, &bobKey.PublicKey, bobInstallationID, bobMessage3, bobMessage3ID) + _, err = s.alice.HandleMessage(aliceKey, &bobKey.PublicKey, bobMessage3.Message, bobMessage3ID) s.Require().NoError(err) // Alice receives message 3 twice - _, err = s.alice.DecryptPayload(aliceKey, &bobKey.PublicKey, bobInstallationID, bobMessage3, bobMessage3ID) + _, err = s.alice.HandleMessage(aliceKey, &bobKey.PublicKey, bobMessage3.Message, bobMessage3ID) s.Require().NoError(err) // Alice receives message 2 once - _, err = s.alice.DecryptPayload(aliceKey, &bobKey.PublicKey, bobInstallationID, bobMessage2, bobMessage2ID) + _, err = s.alice.HandleMessage(aliceKey, &bobKey.PublicKey, bobMessage2.Message, bobMessage2ID) s.Require().NoError(err) // Alice receives message 2 twice - _, err = s.alice.DecryptPayload(aliceKey, &bobKey.PublicKey, bobInstallationID, bobMessage2, bobMessage2ID) + _, err = s.alice.HandleMessage(aliceKey, &bobKey.PublicKey, bobMessage2.Message, bobMessage2ID) s.Require().NoError(err) // Alice confirms the messages @@ -961,10 +1009,10 @@ func (s *EncryptionServiceTestSuite) TestMessageConfirmation() { s.Require().NoError(err) // Alice decrypts it again, it should fail - _, err = s.alice.DecryptPayload(aliceKey, &bobKey.PublicKey, bobInstallationID, bobMessage3, bobMessage3ID) + _, err = s.alice.HandleMessage(aliceKey, &bobKey.PublicKey, bobMessage3.Message, bobMessage3ID) s.Require().Equal(errors.New("can't skip current chain message keys: bad until: probably an out-of-order message that was deleted"), err) // Alice decrypts it again, it should fail - _, err = s.alice.DecryptPayload(aliceKey, &bobKey.PublicKey, bobInstallationID, bobMessage2, bobMessage2ID) + _, err = s.alice.HandleMessage(aliceKey, &bobKey.PublicKey, bobMessage2.Message, bobMessage2ID) s.Require().Equal(errors.New("can't skip current chain message keys: bad until: probably an out-of-order message that was deleted"), err) } diff --git a/services/shhext/chat/multidevice/persistence.go b/services/shhext/chat/multidevice/persistence.go new file mode 100644 index 000000000..27017a745 --- /dev/null +++ b/services/shhext/chat/multidevice/persistence.go @@ -0,0 +1,12 @@ +package multidevice + +type Persistence interface { + // GetActiveInstallations returns the active installations for a given identity. + GetActiveInstallations(maxInstallations int, identity []byte) ([]*Installation, error) + // EnableInstallation enables the installation. + EnableInstallation(identity []byte, installationID string) error + // DisableInstallation disable the installation. + DisableInstallation(identity []byte, installationID string) error + // AddInstallations adds the installations for a given identity, maintaining the enabled flag + AddInstallations(identity []byte, timestamp int64, installations []*Installation, defaultEnabled bool) error +} diff --git a/services/shhext/chat/multidevice/service.go b/services/shhext/chat/multidevice/service.go new file mode 100644 index 000000000..6cf208e67 --- /dev/null +++ b/services/shhext/chat/multidevice/service.go @@ -0,0 +1,93 @@ +package multidevice + +import ( + "crypto/ecdsa" + "fmt" + "github.com/ethereum/go-ethereum/crypto" + "github.com/status-im/status-go/services/shhext/chat/protobuf" +) + +type Installation struct { + ID string + Version uint32 +} + +type Config struct { + MaxInstallations int + ProtocolVersion uint32 + InstallationID string +} + +func New(config *Config, persistence Persistence) *Service { + return &Service{ + config: config, + persistence: persistence, + } +} + +type Service struct { + persistence Persistence + config *Config +} + +type IdentityAndIDPair [2]string + +func (s *Service) GetActiveInstallations(identity *ecdsa.PublicKey) ([]*Installation, error) { + identityC := crypto.CompressPubkey(identity) + return s.persistence.GetActiveInstallations(s.config.MaxInstallations, identityC) +} + +func (s *Service) GetOurActiveInstallations(identity *ecdsa.PublicKey) ([]*Installation, error) { + identityC := crypto.CompressPubkey(identity) + installations, err := s.persistence.GetActiveInstallations(s.config.MaxInstallations-1, identityC) + if err != nil { + return nil, err + } + // Move to layer above + installations = append(installations, &Installation{ + ID: s.config.InstallationID, + Version: s.config.ProtocolVersion, + }) + + return installations, nil +} + +func (s *Service) EnableInstallation(identity *ecdsa.PublicKey, installationID string) error { + identityC := crypto.CompressPubkey(identity) + return s.persistence.EnableInstallation(identityC, installationID) +} + +func (s *Service) DisableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error { + myIdentityKeyC := crypto.CompressPubkey(myIdentityKey) + return s.persistence.DisableInstallation(myIdentityKeyC, installationID) +} + +// ProcessPublicBundle persists a bundle and returns a list of tuples identity/installationID +func (s *Service) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, theirIdentity *ecdsa.PublicKey, b *protobuf.Bundle) ([]IdentityAndIDPair, error) { + signedPreKeys := b.GetSignedPreKeys() + var response []IdentityAndIDPair + var installations []*Installation + + myIdentityStr := fmt.Sprintf("0x%x", crypto.FromECDSAPub(&myIdentityKey.PublicKey)) + theirIdentityStr := fmt.Sprintf("0x%x", crypto.FromECDSAPub(theirIdentity)) + + // Any device from other peers will be considered enabled, ours needs to + // be explicitly enabled + fromOurIdentity := theirIdentityStr != myIdentityStr + + for installationID, signedPreKey := range signedPreKeys { + if installationID != s.config.InstallationID { + installations = append(installations, &Installation{ + ID: installationID, + Version: signedPreKey.GetProtocolVersion(), + }) + response = append(response, IdentityAndIDPair{theirIdentityStr, installationID}) + } + } + + if err := s.persistence.AddInstallations(b.GetIdentity(), b.GetTimestamp(), installations, fromOurIdentity); err != nil { + return nil, err + } + + return response, nil +} diff --git a/services/shhext/chat/multidevice/sql_lite_persistence.go b/services/shhext/chat/multidevice/sql_lite_persistence.go new file mode 100644 index 000000000..ef270091e --- /dev/null +++ b/services/shhext/chat/multidevice/sql_lite_persistence.go @@ -0,0 +1,168 @@ +package multidevice + +import ( + "database/sql" +) + +// SQLLitePersistence represents a persistence service tied to an SQLite database +type SQLLitePersistence struct { + db *sql.DB +} + +// NewSQLLitePersistence creates a new SQLLitePersistence instance, given a path and a key +func NewSQLLitePersistence(db *sql.DB) *SQLLitePersistence { + return &SQLLitePersistence{db: db} +} + +// GetActiveInstallations returns the active installations for a given identity +func (s *SQLLitePersistence) GetActiveInstallations(maxInstallations int, identity []byte) ([]*Installation, error) { + stmt, err := s.db.Prepare(`SELECT installation_id, version + FROM installations + WHERE enabled = 1 AND identity = ? + ORDER BY timestamp DESC + LIMIT ?`) + if err != nil { + return nil, err + } + + var installations []*Installation + rows, err := stmt.Query(identity, maxInstallations) + if err != nil { + return nil, err + } + + for rows.Next() { + var installationID string + var version uint32 + err = rows.Scan( + &installationID, + &version, + ) + if err != nil { + return nil, err + } + installations = append(installations, &Installation{ + ID: installationID, + Version: version, + }) + + } + + return installations, nil + +} + +// AddInstallations adds the installations for a given identity, maintaining the enabled flag +func (s *SQLLitePersistence) AddInstallations(identity []byte, timestamp int64, installations []*Installation, defaultEnabled bool) error { + tx, err := s.db.Begin() + if err != nil { + return nil + } + + for _, installation := range installations { + stmt, err := tx.Prepare(`SELECT enabled, version + FROM installations + WHERE identity = ? AND installation_id = ? + LIMIT 1`) + if err != nil { + return err + } + defer stmt.Close() + + var oldEnabled bool + // We don't override version once we saw one + var oldVersion uint32 + latestVersion := installation.Version + + err = stmt.QueryRow(identity, installation.ID).Scan(&oldEnabled, &oldVersion) + if err != nil && err != sql.ErrNoRows { + return err + } + + // We update timestamp if present without changing enabled, only if this is a new bundle + // and we set the version to the latest we ever saw + if err != sql.ErrNoRows { + if oldVersion > installation.Version { + latestVersion = oldVersion + } + + stmt, err = tx.Prepare(`UPDATE installations + SET timestamp = ?, enabled = ?, version = ? + WHERE identity = ? + AND installation_id = ? + AND timestamp < ?`) + if err != nil { + return err + } + + _, err = stmt.Exec( + timestamp, + oldEnabled, + latestVersion, + identity, + installation.ID, + timestamp, + ) + if err != nil { + return err + } + defer stmt.Close() + + } else { + stmt, err = tx.Prepare(`INSERT INTO installations(identity, installation_id, timestamp, enabled, version) + VALUES (?, ?, ?, ?, ?)`) + if err != nil { + return err + } + + _, err = stmt.Exec( + identity, + installation.ID, + timestamp, + defaultEnabled, + latestVersion, + ) + if err != nil { + return err + } + defer stmt.Close() + } + + } + + if err := tx.Commit(); err != nil { + _ = tx.Rollback() + return err + } + + return nil + +} + +// EnableInstallation enables the installation +func (s *SQLLitePersistence) EnableInstallation(identity []byte, installationID string) error { + stmt, err := s.db.Prepare(`UPDATE installations + SET enabled = 1 + WHERE identity = ? AND installation_id = ?`) + if err != nil { + return err + } + + _, err = stmt.Exec(identity, installationID) + return err + +} + +// DisableInstallation disable the installation +func (s *SQLLitePersistence) DisableInstallation(identity []byte, installationID string) error { + + stmt, err := s.db.Prepare(`UPDATE installations + SET enabled = 0 + WHERE identity = ? AND installation_id = ?`) + if err != nil { + return err + } + + _, err = stmt.Exec(identity, installationID) + return err +} diff --git a/services/shhext/chat/multidevice/sql_lite_persistence_test.go b/services/shhext/chat/multidevice/sql_lite_persistence_test.go new file mode 100644 index 000000000..3ff6ce2cf --- /dev/null +++ b/services/shhext/chat/multidevice/sql_lite_persistence_test.go @@ -0,0 +1,241 @@ +package multidevice + +import ( + "database/sql" + "os" + "testing" + + appDB "github.com/status-im/status-go/services/shhext/chat/db" + "github.com/stretchr/testify/suite" +) + +const ( + dbPath = "/tmp/status-key-store.db" +) + +func TestSQLLitePersistenceTestSuite(t *testing.T) { + suite.Run(t, new(SQLLitePersistenceTestSuite)) +} + +type SQLLitePersistenceTestSuite struct { + suite.Suite + // nolint: structcheck, megacheck + db *sql.DB + service Persistence +} + +func (s *SQLLitePersistenceTestSuite) SetupTest() { + os.Remove(dbPath) + + db, err := appDB.Open(dbPath, "", 0) + s.Require().NoError(err) + + s.service = NewSQLLitePersistence(db) +} + +func (s *SQLLitePersistenceTestSuite) TestAddInstallations() { + identity := []byte("alice") + installations := []*Installation{ + {ID: "alice-1", Version: 1}, + {ID: "alice-2", Version: 2}, + } + err := s.service.AddInstallations( + identity, + 1, + installations, + true, + ) + + s.Require().NoError(err) + + enabledInstallations, err := s.service.GetActiveInstallations(5, identity) + s.Require().NoError(err) + + s.Require().Equal(installations, enabledInstallations) +} + +func (s *SQLLitePersistenceTestSuite) TestAddInstallationVersions() { + identity := []byte("alice") + installations := []*Installation{ + {ID: "alice-1", Version: 1}, + } + err := s.service.AddInstallations( + identity, + 1, + installations, + true, + ) + + s.Require().NoError(err) + + enabledInstallations, err := s.service.GetActiveInstallations(5, identity) + s.Require().NoError(err) + + s.Require().Equal(installations, enabledInstallations) + + installationsWithDowngradedVersion := []*Installation{ + {ID: "alice-1", Version: 0}, + } + + err = s.service.AddInstallations( + identity, + 3, + installationsWithDowngradedVersion, + true, + ) + s.Require().NoError(err) + + enabledInstallations, err = s.service.GetActiveInstallations(5, identity) + s.Require().NoError(err) + s.Require().Equal(installations, enabledInstallations) +} + +func (s *SQLLitePersistenceTestSuite) TestAddInstallationsLimit() { + identity := []byte("alice") + + installations := []*Installation{ + {ID: "alice-1", Version: 1}, + {ID: "alice-2", Version: 2}, + } + + err := s.service.AddInstallations( + identity, + 1, + installations, + true, + ) + s.Require().NoError(err) + + installations = []*Installation{ + {ID: "alice-1", Version: 1}, + {ID: "alice-3", Version: 3}, + } + + err = s.service.AddInstallations( + identity, + 2, + installations, + true, + ) + s.Require().NoError(err) + + installations = []*Installation{ + {ID: "alice-2", Version: 2}, + {ID: "alice-3", Version: 3}, + {ID: "alice-4", Version: 4}, + } + + err = s.service.AddInstallations( + identity, + 3, + installations, + true, + ) + s.Require().NoError(err) + + enabledInstallations, err := s.service.GetActiveInstallations(3, identity) + s.Require().NoError(err) + + s.Require().Equal(installations, enabledInstallations) +} + +func (s *SQLLitePersistenceTestSuite) TestAddInstallationsDisabled() { + identity := []byte("alice") + + installations := []*Installation{ + {ID: "alice-1", Version: 1}, + {ID: "alice-2", Version: 2}, + } + + err := s.service.AddInstallations( + identity, + 1, + installations, + false, + ) + s.Require().NoError(err) + + actualInstallations, err := s.service.GetActiveInstallations(3, identity) + s.Require().NoError(err) + + s.Require().Nil(actualInstallations) +} + +func (s *SQLLitePersistenceTestSuite) TestDisableInstallation() { + identity := []byte("alice") + + installations := []*Installation{ + {ID: "alice-1", Version: 1}, + {ID: "alice-2", Version: 2}, + } + + err := s.service.AddInstallations( + identity, + 1, + installations, + true, + ) + s.Require().NoError(err) + + err = s.service.DisableInstallation(identity, "alice-1") + s.Require().NoError(err) + + // We add the installations again + installations = []*Installation{ + {ID: "alice-1", Version: 1}, + {ID: "alice-2", Version: 2}, + } + + err = s.service.AddInstallations( + identity, + 1, + installations, + true, + ) + s.Require().NoError(err) + + actualInstallations, err := s.service.GetActiveInstallations(3, identity) + s.Require().NoError(err) + + expected := []*Installation{{ID: "alice-2", Version: 2}} + s.Require().Equal(expected, actualInstallations) +} + +func (s *SQLLitePersistenceTestSuite) TestEnableInstallation() { + identity := []byte("alice") + + installations := []*Installation{ + {ID: "alice-1", Version: 1}, + {ID: "alice-2", Version: 2}, + } + + err := s.service.AddInstallations( + identity, + 1, + installations, + true, + ) + s.Require().NoError(err) + + err = s.service.DisableInstallation(identity, "alice-1") + s.Require().NoError(err) + + actualInstallations, err := s.service.GetActiveInstallations(3, identity) + s.Require().NoError(err) + + expected := []*Installation{{ID: "alice-2", Version: 2}} + s.Require().Equal(expected, actualInstallations) + + err = s.service.EnableInstallation(identity, "alice-1") + s.Require().NoError(err) + + actualInstallations, err = s.service.GetActiveInstallations(3, identity) + s.Require().NoError(err) + + expected = []*Installation{ + {ID: "alice-1", Version: 1}, + {ID: "alice-2", Version: 2}, + } + s.Require().Equal(expected, actualInstallations) + +} diff --git a/services/shhext/chat/persistence.go b/services/shhext/chat/persistence.go index 669784638..b8a1fe3f7 100644 --- a/services/shhext/chat/persistence.go +++ b/services/shhext/chat/persistence.go @@ -4,13 +4,10 @@ import ( "crypto/ecdsa" dr "github.com/status-im/doubleratchet" + "github.com/status-im/status-go/services/shhext/chat/multidevice" + "github.com/status-im/status-go/services/shhext/chat/protobuf" ) -type Installation struct { - ID string - Version uint32 -} - // RatchetInfo holds the current ratchet state type RatchetInfo struct { ID []byte @@ -30,17 +27,17 @@ type PersistenceService interface { // GetSessionStorage returns the associated double ratchet SessionStorage object. GetSessionStorage() dr.SessionStorage - // GetPublicBundle retrieves an existing Bundle for the specified public key & installationIDs. - GetPublicBundle(*ecdsa.PublicKey, []*Installation) (*Bundle, error) + // GetPublicBundle retrieves an existing Bundle for the specified public key & installations + GetPublicBundle(*ecdsa.PublicKey, []*multidevice.Installation) (*protobuf.Bundle, error) // AddPublicBundle persists a specified Bundle - AddPublicBundle(*Bundle) error + AddPublicBundle(*protobuf.Bundle) error - // GetAnyPrivateBundle retrieves any bundle for our identity & installationIDs - GetAnyPrivateBundle([]byte, []*Installation) (*BundleContainer, error) + // GetAnyPrivateBundle retrieves any bundle for our identity & installations + GetAnyPrivateBundle([]byte, []*multidevice.Installation) (*protobuf.BundleContainer, error) // GetPrivateKeyBundle retrieves a BundleContainer with the specified signed prekey. GetPrivateKeyBundle([]byte) ([]byte, error) // AddPrivateBundle persists a BundleContainer. - AddPrivateBundle(*BundleContainer) error + AddPrivateBundle(*protobuf.BundleContainer) error // MarkBundleExpired marks a private bundle as expired, not to be used for encryption anymore. MarkBundleExpired([]byte) error @@ -53,13 +50,4 @@ type PersistenceService interface { // RatchetInfoConfirmed clears the ephemeral key in the RatchetInfo // associated with the specified bundle ID and interlocutor identity public key. RatchetInfoConfirmed([]byte, []byte, string) error - - // GetActiveInstallations returns the active installations for a given identity. - GetActiveInstallations(maxInstallations int, identity []byte) ([]*Installation, error) - // AddInstallations adds the installations for a given identity. - AddInstallations(identity []byte, timestamp int64, installations []*Installation, enabled bool) error - // EnableInstallation enables the installation. - EnableInstallation(identity []byte, installationID string) error - // DisableInstallation disable the installation. - DisableInstallation(identity []byte, installationID string) error } diff --git a/services/shhext/chat/encryption.pb.go b/services/shhext/chat/protobuf/encryption.pb.go similarity index 78% rename from services/shhext/chat/encryption.pb.go rename to services/shhext/chat/protobuf/encryption.pb.go index 88e0a90c6..760de5cdc 100644 --- a/services/shhext/chat/encryption.pb.go +++ b/services/shhext/chat/protobuf/encryption.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // source: encryption.proto -package chat +package protobuf import ( fmt "fmt" @@ -491,56 +491,56 @@ func (m *ProtocolMessage) GetPublicMessage() []byte { } func init() { - proto.RegisterType((*SignedPreKey)(nil), "chat.SignedPreKey") - proto.RegisterType((*Bundle)(nil), "chat.Bundle") - proto.RegisterMapType((map[string]*SignedPreKey)(nil), "chat.Bundle.SignedPreKeysEntry") - proto.RegisterType((*BundleContainer)(nil), "chat.BundleContainer") - proto.RegisterType((*DRHeader)(nil), "chat.DRHeader") - proto.RegisterType((*DHHeader)(nil), "chat.DHHeader") - proto.RegisterType((*X3DHHeader)(nil), "chat.X3DHHeader") - proto.RegisterType((*DirectMessageProtocol)(nil), "chat.DirectMessageProtocol") - proto.RegisterType((*ProtocolMessage)(nil), "chat.ProtocolMessage") - proto.RegisterMapType((map[string]*DirectMessageProtocol)(nil), "chat.ProtocolMessage.DirectMessageEntry") + proto.RegisterType((*SignedPreKey)(nil), "protobuf.SignedPreKey") + proto.RegisterType((*Bundle)(nil), "protobuf.Bundle") + proto.RegisterMapType((map[string]*SignedPreKey)(nil), "protobuf.Bundle.SignedPreKeysEntry") + proto.RegisterType((*BundleContainer)(nil), "protobuf.BundleContainer") + proto.RegisterType((*DRHeader)(nil), "protobuf.DRHeader") + proto.RegisterType((*DHHeader)(nil), "protobuf.DHHeader") + proto.RegisterType((*X3DHHeader)(nil), "protobuf.X3DHHeader") + proto.RegisterType((*DirectMessageProtocol)(nil), "protobuf.DirectMessageProtocol") + proto.RegisterType((*ProtocolMessage)(nil), "protobuf.ProtocolMessage") + proto.RegisterMapType((map[string]*DirectMessageProtocol)(nil), "protobuf.ProtocolMessage.DirectMessageEntry") } func init() { proto.RegisterFile("encryption.proto", fileDescriptor_8293a649ce9418c6) } var fileDescriptor_8293a649ce9418c6 = []byte{ - // 562 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x54, 0x61, 0x8b, 0xd3, 0x4c, - 0x10, 0x26, 0x49, 0xef, 0xda, 0x4e, 0xd3, 0xb4, 0xec, 0xcb, 0x2b, 0xa1, 0x1e, 0x58, 0xc2, 0xa9, - 0x11, 0xa1, 0x70, 0xad, 0x1f, 0xc4, 0x8f, 0x5a, 0xb1, 0x9e, 0x88, 0xc7, 0x2a, 0xe2, 0x17, 0x09, - 0xdb, 0x66, 0xbd, 0x5b, 0x4c, 0x93, 0xb0, 0xbb, 0x2d, 0xe4, 0xcf, 0xf9, 0x57, 0xfc, 0x29, 0x4a, - 0x76, 0xb3, 0xed, 0xb6, 0x77, 0x07, 0x7e, 0xeb, 0xcc, 0x3c, 0xfb, 0xcc, 0x33, 0xcf, 0x74, 0x02, - 0x43, 0x9a, 0xaf, 0x78, 0x55, 0x4a, 0x56, 0xe4, 0x93, 0x92, 0x17, 0xb2, 0x40, 0xad, 0xd5, 0x0d, - 0x91, 0x51, 0x05, 0xfe, 0x67, 0x76, 0x9d, 0xd3, 0xf4, 0x8a, 0xd3, 0x0f, 0xb4, 0x42, 0xe7, 0x10, - 0x08, 0x15, 0x27, 0x25, 0xa7, 0xc9, 0x4f, 0x5a, 0x85, 0xce, 0xd8, 0x89, 0x7d, 0xec, 0x0b, 0x1b, - 0x15, 0x42, 0x7b, 0x4b, 0xb9, 0x60, 0x45, 0x1e, 0xba, 0x63, 0x27, 0xee, 0x63, 0x13, 0xa2, 0x67, - 0x30, 0x54, 0xf4, 0xab, 0x22, 0x4b, 0x0c, 0xc4, 0x53, 0x90, 0x81, 0xc9, 0x7f, 0xd5, 0xe9, 0xe8, - 0x8f, 0x03, 0xa7, 0xaf, 0x37, 0x79, 0x9a, 0x51, 0x34, 0x82, 0x0e, 0x4b, 0x69, 0x2e, 0x99, 0x34, - 0xfd, 0x76, 0x31, 0x7a, 0x07, 0x83, 0x43, 0x45, 0x22, 0x74, 0xc7, 0x5e, 0xdc, 0x9b, 0x3e, 0x9a, - 0xd4, 0x13, 0x4c, 0x34, 0xc5, 0xc4, 0x9e, 0x42, 0xbc, 0xcd, 0x25, 0xaf, 0x70, 0xdf, 0xd6, 0x2c, - 0xd0, 0x19, 0x74, 0xeb, 0x04, 0x91, 0x1b, 0x4e, 0xc3, 0x96, 0xea, 0xb2, 0x4f, 0xd4, 0x55, 0xc9, - 0xd6, 0x54, 0x48, 0xb2, 0x2e, 0xc3, 0x93, 0xb1, 0x13, 0x7b, 0x78, 0x9f, 0x18, 0x7d, 0x01, 0x74, - 0xbb, 0x01, 0x1a, 0x82, 0x67, 0x1c, 0xea, 0xe2, 0xfa, 0x27, 0x8a, 0xe1, 0x64, 0x4b, 0xb2, 0x0d, - 0x55, 0xb6, 0xf4, 0xa6, 0x48, 0x4b, 0xb4, 0x9f, 0x62, 0x0d, 0x78, 0xe5, 0xbe, 0x74, 0x22, 0x0e, - 0x03, 0xad, 0xfe, 0x4d, 0x91, 0x4b, 0xc2, 0x72, 0xca, 0xd1, 0x39, 0x9c, 0x2e, 0x55, 0x4a, 0xb1, - 0xf6, 0xa6, 0xbe, 0x3d, 0x24, 0x6e, 0x6a, 0x68, 0x06, 0x0f, 0x4a, 0xce, 0xb6, 0x44, 0xd2, 0xe4, - 0x68, 0x5b, 0xae, 0x9a, 0xeb, 0xbf, 0xa6, 0x6a, 0x37, 0xbe, 0x6c, 0x75, 0xbc, 0x61, 0x2b, 0xba, - 0x84, 0xce, 0x1c, 0x2f, 0x28, 0x49, 0x29, 0xb7, 0xf5, 0xfb, 0x5a, 0xbf, 0x0f, 0x8e, 0x59, 0xa9, - 0x93, 0xa3, 0x00, 0xdc, 0xd2, 0xac, 0xcf, 0x2d, 0x55, 0xcc, 0xd2, 0xc6, 0x3a, 0x97, 0xa5, 0xd1, - 0x19, 0x74, 0xe6, 0x8b, 0xfb, 0xb8, 0xa2, 0x17, 0x00, 0xdf, 0x66, 0xf7, 0xd7, 0x8f, 0xd9, 0x1a, - 0x7d, 0xbf, 0x1c, 0xf8, 0x7f, 0xce, 0x38, 0x5d, 0xc9, 0x8f, 0x54, 0x08, 0x72, 0x4d, 0xaf, 0x9a, - 0xbf, 0x0d, 0xba, 0x80, 0x5e, 0xcd, 0x97, 0xdc, 0x28, 0xc2, 0xc6, 0x9f, 0xa1, 0xf6, 0x67, 0xdf, - 0x08, 0xdb, 0x4d, 0x9f, 0x43, 0x77, 0x8e, 0xcd, 0x03, 0xbd, 0x92, 0x40, 0x3f, 0x30, 0x1e, 0xe0, - 0xbd, 0x1b, 0x35, 0x78, 0xc7, 0x4e, 0x0f, 0xc0, 0x8b, 0x1d, 0xd8, 0x30, 0x87, 0xd0, 0x2e, 0x49, - 0x95, 0x15, 0x24, 0x55, 0xfe, 0xf8, 0xd8, 0x84, 0xd1, 0x6f, 0x17, 0x06, 0x46, 0x73, 0x33, 0xc2, - 0x3f, 0x6e, 0xf5, 0x29, 0x0c, 0x58, 0x2e, 0x24, 0xc9, 0x32, 0x52, 0xdf, 0x69, 0xc2, 0x52, 0xa5, - 0xb9, 0x8b, 0x03, 0x3b, 0xfd, 0x3e, 0x45, 0x4f, 0xa0, 0xad, 0x9f, 0x88, 0xd0, 0x53, 0xa7, 0x70, - 0xc8, 0x67, 0x8a, 0xe8, 0x13, 0x04, 0xa9, 0xb2, 0x32, 0x59, 0x6b, 0x21, 0x21, 0x55, 0xf0, 0x58, - 0xc3, 0x8f, 0x54, 0x4e, 0x0e, 0x6c, 0x6f, 0x4e, 0x28, 0xb5, 0x73, 0xe8, 0x31, 0x04, 0xe5, 0x66, - 0x99, 0xb1, 0xd5, 0x8e, 0xf0, 0x87, 0x1a, 0xbe, 0xaf, 0xb3, 0x0d, 0x6c, 0xf4, 0x1d, 0xd0, 0x6d, - 0xae, 0x3b, 0xae, 0xe5, 0xe2, 0xf0, 0x5a, 0x1e, 0x36, 0x6e, 0xdf, 0xb5, 0x7d, 0xeb, 0x6c, 0x96, - 0xa7, 0xea, 0x4b, 0x32, 0xfb, 0x1b, 0x00, 0x00, 0xff, 0xff, 0x9e, 0x75, 0x6d, 0x59, 0xd4, 0x04, - 0x00, 0x00, + // 566 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x53, 0xdd, 0x6a, 0xdb, 0x4c, + 0x10, 0x45, 0x52, 0xe2, 0x9f, 0xb1, 0xfc, 0xc3, 0x7e, 0x5f, 0x83, 0x30, 0x81, 0x1a, 0xb5, 0xa5, + 0x6e, 0x09, 0x2e, 0xd8, 0x0d, 0x94, 0x5e, 0xb6, 0x2e, 0xb8, 0x09, 0x85, 0xb0, 0x81, 0x92, 0x3b, + 0xb1, 0xb6, 0x36, 0xe9, 0x52, 0x79, 0x25, 0x76, 0xd7, 0x06, 0x3d, 0x41, 0xdf, 0xad, 0x2f, 0xd3, + 0x57, 0x28, 0x5a, 0x69, 0xad, 0xb5, 0x9d, 0x5c, 0xf4, 0xca, 0x9e, 0xb3, 0x73, 0xce, 0xcc, 0x9c, + 0xd1, 0xc0, 0x80, 0xf2, 0x95, 0xc8, 0x33, 0xc5, 0x52, 0x3e, 0xc9, 0x44, 0xaa, 0x52, 0xd4, 0xd2, + 0x3f, 0xcb, 0xcd, 0x7d, 0x98, 0x83, 0x7f, 0xcb, 0x1e, 0x38, 0x8d, 0x6f, 0x04, 0xbd, 0xa6, 0x39, + 0x7a, 0x09, 0x3d, 0xa9, 0xe3, 0x28, 0x13, 0x34, 0xfa, 0x49, 0xf3, 0xc0, 0x19, 0x39, 0x63, 0x1f, + 0xfb, 0xd2, 0xce, 0x0a, 0xa0, 0xb9, 0xa5, 0x42, 0xb2, 0x94, 0x07, 0xee, 0xc8, 0x19, 0x77, 0xb1, + 0x09, 0xd1, 0x1b, 0x18, 0x68, 0xed, 0x55, 0x9a, 0x44, 0x26, 0xc5, 0xd3, 0x29, 0x7d, 0x83, 0x7f, + 0x2f, 0xe1, 0xf0, 0x97, 0x0b, 0x8d, 0x4f, 0x1b, 0x1e, 0x27, 0x14, 0x0d, 0xa1, 0xc5, 0x62, 0xca, + 0x15, 0x53, 0xa6, 0xde, 0x2e, 0x46, 0xd7, 0xd0, 0xdf, 0xef, 0x48, 0x06, 0xee, 0xc8, 0x1b, 0x77, + 0xa6, 0x2f, 0x26, 0x66, 0x8a, 0x49, 0x29, 0x33, 0xb1, 0x27, 0x91, 0x5f, 0xb8, 0x12, 0x39, 0xee, + 0xda, 0x7d, 0x4b, 0x74, 0x0e, 0xed, 0x02, 0x20, 0x6a, 0x23, 0x68, 0x70, 0xa2, 0x2b, 0xd5, 0x40, + 0xf1, 0xaa, 0xd8, 0x9a, 0x4a, 0x45, 0xd6, 0x59, 0x70, 0x3a, 0x72, 0xc6, 0x1e, 0xae, 0x81, 0xe1, + 0x1d, 0xa0, 0xe3, 0x02, 0x68, 0x00, 0x9e, 0x71, 0xa9, 0x8d, 0x8b, 0xbf, 0xe8, 0x02, 0x4e, 0xb7, + 0x24, 0xd9, 0x50, 0x6d, 0x4d, 0x67, 0x7a, 0x56, 0xb7, 0x69, 0xd3, 0x71, 0x99, 0xf4, 0xd1, 0xfd, + 0xe0, 0x84, 0x5b, 0xe8, 0x97, 0x13, 0x7c, 0x4e, 0xb9, 0x22, 0x8c, 0x53, 0x81, 0xc6, 0xd0, 0x58, + 0x6a, 0x48, 0x2b, 0x77, 0xa6, 0x83, 0xc3, 0x61, 0x71, 0xf5, 0x8e, 0x66, 0x70, 0x96, 0x09, 0xb6, + 0x25, 0x8a, 0x46, 0x07, 0x9b, 0x73, 0xf5, 0x7c, 0xff, 0x55, 0xaf, 0x76, 0xf1, 0xab, 0x93, 0x96, + 0x37, 0x38, 0x09, 0xaf, 0xa0, 0x35, 0xc7, 0x0b, 0x4a, 0x62, 0x2a, 0xec, 0x39, 0xfc, 0x72, 0x0e, + 0x1f, 0x1c, 0xb3, 0x5e, 0x87, 0xa3, 0x1e, 0xb8, 0x99, 0x59, 0xa5, 0x9b, 0xe9, 0x98, 0xc5, 0x95, + 0x85, 0x2e, 0x8b, 0xc3, 0x73, 0x68, 0xcd, 0x17, 0x4f, 0x69, 0x85, 0xef, 0x01, 0xee, 0x66, 0x4f, + 0xbf, 0x1f, 0xaa, 0x55, 0xfd, 0xfd, 0x76, 0xe0, 0xd9, 0x9c, 0x09, 0xba, 0x52, 0xdf, 0xa8, 0x94, + 0xe4, 0x81, 0xde, 0x54, 0x9f, 0x10, 0xba, 0x84, 0x4e, 0xa1, 0x17, 0xfd, 0xd0, 0x82, 0x95, 0x47, + 0xff, 0xd7, 0x1e, 0xd5, 0xc5, 0xb0, 0x5d, 0xf8, 0x1d, 0xb4, 0xe7, 0xd8, 0x90, 0xca, 0xf5, 0xa0, + 0x9a, 0x64, 0xbc, 0xc0, 0xb5, 0x2b, 0x05, 0x61, 0x57, 0x85, 0x1e, 0x11, 0x16, 0x3b, 0x82, 0xa9, + 0x10, 0x40, 0x33, 0x23, 0x79, 0x92, 0x92, 0x58, 0x7b, 0xe5, 0x63, 0x13, 0x86, 0x7f, 0x5c, 0xe8, + 0x9b, 0xfe, 0xab, 0x71, 0xfe, 0x61, 0xcb, 0xaf, 0xa1, 0xcf, 0xb8, 0x54, 0x24, 0x49, 0x48, 0x71, + 0xc7, 0x11, 0x8b, 0x75, 0xff, 0x6d, 0xdc, 0xb3, 0xe1, 0xaf, 0x31, 0x7a, 0x0b, 0xcd, 0x92, 0x22, + 0x03, 0x4f, 0x9f, 0xc9, 0xb1, 0xa6, 0x49, 0x40, 0xb7, 0xd0, 0x8b, 0xb5, 0xbd, 0xd1, 0xba, 0x6c, + 0x28, 0xa0, 0x9a, 0x72, 0x51, 0x53, 0x0e, 0x3a, 0x9e, 0xec, 0xad, 0xa3, 0x3a, 0xb1, 0xd8, 0xc6, + 0xd0, 0x2b, 0xe8, 0x65, 0x9b, 0x65, 0xc2, 0x56, 0x3b, 0xd1, 0x7b, 0x6d, 0x44, 0xb7, 0x44, 0xab, + 0xb4, 0x21, 0x01, 0x74, 0xac, 0xf5, 0xc8, 0x35, 0x5d, 0xee, 0x5f, 0xd3, 0x73, 0xcb, 0xfd, 0xc7, + 0xbe, 0x0c, 0xeb, 0xac, 0x96, 0x0d, 0x9d, 0x3a, 0xfb, 0x1b, 0x00, 0x00, 0xff, 0xff, 0xcd, 0x2d, + 0x0e, 0xc8, 0x00, 0x05, 0x00, 0x00, } diff --git a/services/shhext/chat/encryption.proto b/services/shhext/chat/protobuf/encryption.proto similarity index 98% rename from services/shhext/chat/encryption.proto rename to services/shhext/chat/protobuf/encryption.proto index 54e374489..c8b10277d 100644 --- a/services/shhext/chat/encryption.proto +++ b/services/shhext/chat/protobuf/encryption.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package chat; +package protobuf; message SignedPreKey { bytes signed_pre_key = 1; diff --git a/services/shhext/chat/protocol.go b/services/shhext/chat/protocol.go index 5de322cd1..f004de885 100644 --- a/services/shhext/chat/protocol.go +++ b/services/shhext/chat/protocol.go @@ -5,37 +5,48 @@ import ( "errors" "github.com/ethereum/go-ethereum/log" - "github.com/status-im/status-go/services/shhext/chat/topic" + "github.com/status-im/status-go/services/shhext/chat/multidevice" + "github.com/status-im/status-go/services/shhext/chat/protobuf" + "github.com/status-im/status-go/services/shhext/chat/sharedsecret" ) -const protocolCurrentVersion = 1 -const topicNegotiationVersion = 1 +const ProtocolVersion = 1 +const sharedSecretNegotiationVersion = 1 +const partitionedTopicMinVersion = 1 type ProtocolService struct { - log log.Logger - encryption *EncryptionService - topic *topic.Service - addedBundlesHandler func([]IdentityAndIDPair) - onNewTopicHandler func([]*topic.Secret) - Enabled bool + log log.Logger + encryption *EncryptionService + secret *sharedsecret.Service + multidevice *multidevice.Service + addedBundlesHandler func([]multidevice.IdentityAndIDPair) + onNewSharedSecretHandler func([]*sharedsecret.Secret) + Enabled bool } var ErrNotProtocolMessage = errors.New("Not a protocol message") // NewProtocolService creates a new ProtocolService instance -func NewProtocolService(encryption *EncryptionService, topic *topic.Service, addedBundlesHandler func([]IdentityAndIDPair), onNewTopicHandler func([]*topic.Secret)) *ProtocolService { +func NewProtocolService(encryption *EncryptionService, secret *sharedsecret.Service, multidevice *multidevice.Service, addedBundlesHandler func([]multidevice.IdentityAndIDPair), onNewSharedSecretHandler func([]*sharedsecret.Secret)) *ProtocolService { return &ProtocolService{ - log: log.New("package", "status-go/services/sshext.chat"), - encryption: encryption, - topic: topic, - addedBundlesHandler: addedBundlesHandler, - onNewTopicHandler: onNewTopicHandler, + log: log.New("package", "status-go/services/sshext.chat"), + encryption: encryption, + secret: secret, + multidevice: multidevice, + addedBundlesHandler: addedBundlesHandler, + onNewSharedSecretHandler: onNewSharedSecretHandler, } } -func (p *ProtocolService) addBundle(myIdentityKey *ecdsa.PrivateKey, msg *ProtocolMessage, sendSingle bool) (*ProtocolMessage, error) { +func (p *ProtocolService) addBundle(myIdentityKey *ecdsa.PrivateKey, msg *protobuf.ProtocolMessage, sendSingle bool) (*protobuf.ProtocolMessage, error) { + // Get a bundle - bundle, err := p.encryption.CreateBundle(myIdentityKey) + installations, err := p.multidevice.GetOurActiveInstallations(&myIdentityKey.PublicKey) + if err != nil { + return nil, err + } + + bundle, err := p.encryption.CreateBundle(myIdentityKey, installations) if err != nil { p.log.Error("encryption-service", "error creating bundle", err) return nil, err @@ -46,16 +57,16 @@ func (p *ProtocolService) addBundle(myIdentityKey *ecdsa.PrivateKey, msg *Protoc // an issue anymore msg.Bundle = bundle } else { - msg.Bundles = []*Bundle{bundle} + msg.Bundles = []*protobuf.Bundle{bundle} } return msg, nil } // BuildPublicMessage marshals a public chat message given the user identity private key and a payload -func (p *ProtocolService) BuildPublicMessage(myIdentityKey *ecdsa.PrivateKey, payload []byte) (*ProtocolMessage, error) { +func (p *ProtocolService) BuildPublicMessage(myIdentityKey *ecdsa.PrivateKey, payload []byte) (*protobuf.ProtocolMessage, error) { // Build message not encrypted - protocolMessage := &ProtocolMessage{ + protocolMessage := &protobuf.ProtocolMessage{ InstallationId: p.encryption.config.InstallationID, PublicMessage: payload, } @@ -63,100 +74,153 @@ func (p *ProtocolService) BuildPublicMessage(myIdentityKey *ecdsa.PrivateKey, pa return p.addBundle(myIdentityKey, protocolMessage, false) } +type ProtocolMessageSpec struct { + Message *protobuf.ProtocolMessage + // Installations is the targeted devices + Installations []*multidevice.Installation + // SharedSecret is a shared secret established among the installations + SharedSecret []byte +} + +func (p *ProtocolMessageSpec) MinVersion() uint32 { + + var version uint32 + + for _, installation := range p.Installations { + if installation.Version < version { + version = installation.Version + } + } + return version + +} + +func (p *ProtocolMessageSpec) PartitionedTopic() bool { + + return p.MinVersion() >= partitionedTopicMinVersion + +} + // BuildDirectMessage returns a 1:1 chat message and optionally a negotiated topic given the user identity private key, the recipient's public key, and a payload -func (p *ProtocolService) BuildDirectMessage(myIdentityKey *ecdsa.PrivateKey, publicKey *ecdsa.PublicKey, payload []byte) (*ProtocolMessage, []byte, error) { +func (p *ProtocolService) BuildDirectMessage(myIdentityKey *ecdsa.PrivateKey, publicKey *ecdsa.PublicKey, payload []byte) (*ProtocolMessageSpec, error) { + activeInstallations, err := p.multidevice.GetActiveInstallations(publicKey) + if err != nil { + return nil, err + } + // Encrypt payload - encryptionResponse, err := p.encryption.EncryptPayload(publicKey, myIdentityKey, payload) + encryptionResponse, installations, err := p.encryption.EncryptPayload(publicKey, myIdentityKey, activeInstallations, payload) if err != nil { p.log.Error("encryption-service", "error encrypting payload", err) - return nil, nil, err + return nil, err } // Build message - protocolMessage := &ProtocolMessage{ + protocolMessage := &protobuf.ProtocolMessage{ InstallationId: p.encryption.config.InstallationID, DirectMessage: encryptionResponse, } msg, err := p.addBundle(myIdentityKey, protocolMessage, true) if err != nil { - return nil, nil, err + return nil, err } // Check who we are sending the message to, and see if we have a shared secret // across devices var installationIDs []string - var sharedSecret *topic.Secret + var sharedSecret *sharedsecret.Secret var agreed bool for installationID := range protocolMessage.GetDirectMessage() { if installationID != noInstallationID { installationIDs = append(installationIDs, installationID) } } - if len(installationIDs) != 0 { - sharedSecret, agreed, err = p.topic.Send(myIdentityKey, p.encryption.config.InstallationID, publicKey, installationIDs) - if err != nil { - return nil, nil, err - } + + sharedSecret, agreed, err = p.secret.Send(myIdentityKey, p.encryption.config.InstallationID, publicKey, installationIDs) + if err != nil { + return nil, err } // Call handler if sharedSecret != nil { - p.onNewTopicHandler([]*topic.Secret{sharedSecret}) + p.onNewSharedSecretHandler([]*sharedsecret.Secret{sharedSecret}) + } + response := &ProtocolMessageSpec{ + Message: msg, + Installations: installations, } if agreed { - return msg, sharedSecret.Key, nil + response.SharedSecret = sharedSecret.Key } - return msg, nil, nil + return response, nil } // BuildDHMessage builds a message with DH encryption so that it can be decrypted by any other device. -func (p *ProtocolService) BuildDHMessage(myIdentityKey *ecdsa.PrivateKey, destination *ecdsa.PublicKey, payload []byte) (*ProtocolMessage, []byte, error) { +func (p *ProtocolService) BuildDHMessage(myIdentityKey *ecdsa.PrivateKey, destination *ecdsa.PublicKey, payload []byte) (*ProtocolMessageSpec, error) { // Encrypt payload encryptionResponse, err := p.encryption.EncryptPayloadWithDH(destination, payload) if err != nil { p.log.Error("encryption-service", "error encrypting payload", err) - return nil, nil, err + return nil, err } // Build message - protocolMessage := &ProtocolMessage{ + protocolMessage := &protobuf.ProtocolMessage{ InstallationId: p.encryption.config.InstallationID, DirectMessage: encryptionResponse, } msg, err := p.addBundle(myIdentityKey, protocolMessage, true) if err != nil { - return nil, nil, err + return nil, err } - return msg, nil, nil + return &ProtocolMessageSpec{Message: msg}, nil } // ProcessPublicBundle processes a received X3DH bundle. -func (p *ProtocolService) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, bundle *Bundle) ([]IdentityAndIDPair, error) { - return p.encryption.ProcessPublicBundle(myIdentityKey, bundle) +func (p *ProtocolService) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, bundle *protobuf.Bundle) ([]multidevice.IdentityAndIDPair, error) { + if err := p.encryption.ProcessPublicBundle(myIdentityKey, bundle); err != nil { + return nil, err + } + + theirIdentityKey, err := ExtractIdentity(bundle) + if err != nil { + return nil, err + } + + return p.multidevice.ProcessPublicBundle(myIdentityKey, theirIdentityKey, bundle) } // GetBundle retrieves or creates a X3DH bundle, given a private identity key. -func (p *ProtocolService) GetBundle(myIdentityKey *ecdsa.PrivateKey) (*Bundle, error) { - return p.encryption.CreateBundle(myIdentityKey) +func (p *ProtocolService) GetBundle(myIdentityKey *ecdsa.PrivateKey) (*protobuf.Bundle, error) { + installations, err := p.multidevice.GetOurActiveInstallations(&myIdentityKey.PublicKey) + if err != nil { + return nil, err + } + + return p.encryption.CreateBundle(myIdentityKey, installations) } // EnableInstallation enables an installation for multi-device sync. func (p *ProtocolService) EnableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error { - return p.encryption.EnableInstallation(myIdentityKey, installationID) + return p.multidevice.EnableInstallation(myIdentityKey, installationID) } // DisableInstallation disables an installation for multi-device sync. func (p *ProtocolService) DisableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error { - return p.encryption.DisableInstallation(myIdentityKey, installationID) + return p.multidevice.DisableInstallation(myIdentityKey, installationID) } // GetPublicBundle retrieves a public bundle given an identity -func (p *ProtocolService) GetPublicBundle(theirIdentityKey *ecdsa.PublicKey) (*Bundle, error) { - return p.encryption.GetPublicBundle(theirIdentityKey) +func (p *ProtocolService) GetPublicBundle(theirIdentityKey *ecdsa.PublicKey) (*protobuf.Bundle, error) { + installations, err := p.multidevice.GetActiveInstallations(theirIdentityKey) + if err != nil { + return nil, err + } + return p.encryption.GetPublicBundle(theirIdentityKey, installations) } // ConfirmMessagesProcessed confirms and deletes message keys for the given messages @@ -165,7 +229,7 @@ func (p *ProtocolService) ConfirmMessagesProcessed(messageIDs [][]byte) error { } // HandleMessage unmarshals a message and processes it, decrypting it if it is a 1:1 message. -func (p *ProtocolService) HandleMessage(myIdentityKey *ecdsa.PrivateKey, theirPublicKey *ecdsa.PublicKey, protocolMessage *ProtocolMessage, messageID []byte) ([]byte, error) { +func (p *ProtocolService) HandleMessage(myIdentityKey *ecdsa.PrivateKey, theirPublicKey *ecdsa.PublicKey, protocolMessage *protobuf.ProtocolMessage, messageID []byte) ([]byte, error) { if p.encryption == nil { return nil, errors.New("encryption service not initialized") } @@ -173,7 +237,7 @@ func (p *ProtocolService) HandleMessage(myIdentityKey *ecdsa.PrivateKey, theirPu // Process bundle, deprecated, here for backward compatibility if bundle := protocolMessage.GetBundle(); bundle != nil { // Should we stop processing if the bundle cannot be verified? - addedBundles, err := p.encryption.ProcessPublicBundle(myIdentityKey, bundle) + addedBundles, err := p.ProcessPublicBundle(myIdentityKey, bundle) if err != nil { return nil, err } @@ -184,7 +248,7 @@ func (p *ProtocolService) HandleMessage(myIdentityKey *ecdsa.PrivateKey, theirPu // Process bundles for _, bundle := range protocolMessage.GetBundles() { // Should we stop processing if the bundle cannot be verified? - addedBundles, err := p.encryption.ProcessPublicBundle(myIdentityKey, bundle) + addedBundles, err := p.ProcessPublicBundle(myIdentityKey, bundle) if err != nil { return nil, err } @@ -205,17 +269,20 @@ func (p *ProtocolService) HandleMessage(myIdentityKey *ecdsa.PrivateKey, theirPu return nil, err } + var bundles []*protobuf.Bundle p.log.Info("Checking version") // Handle protocol negotiation for compatible clients - version := getProtocolVersion(protocolMessage.GetBundles(), protocolMessage.GetInstallationId()) - if version >= topicNegotiationVersion { + p.log.Info("bundle", "bundles", protocolMessage) + bundles = append(protocolMessage.GetBundles(), protocolMessage.GetBundle()) + version := getProtocolVersion(bundles, protocolMessage.GetInstallationId()) + if version >= sharedSecretNegotiationVersion { p.log.Info("Version greater than 1 negotianting") - sharedSecret, err := p.topic.Receive(myIdentityKey, theirPublicKey, protocolMessage.GetInstallationId()) + sharedSecret, err := p.secret.Receive(myIdentityKey, theirPublicKey, protocolMessage.GetInstallationId()) if err != nil { return nil, err } - p.onNewTopicHandler([]*topic.Secret{sharedSecret}) + p.onNewSharedSecretHandler([]*sharedsecret.Secret{sharedSecret}) } return message, nil @@ -225,23 +292,25 @@ func (p *ProtocolService) HandleMessage(myIdentityKey *ecdsa.PrivateKey, theirPu return nil, errors.New("no payload") } -func getProtocolVersion(bundles []*Bundle, installationID string) uint32 { +func getProtocolVersion(bundles []*protobuf.Bundle, installationID string) uint32 { if installationID == "" { return 0 } for _, bundle := range bundles { - signedPreKeys := bundle.GetSignedPreKeys() - if signedPreKeys == nil { - continue - } + if bundle != nil { + signedPreKeys := bundle.GetSignedPreKeys() + if signedPreKeys == nil { + continue + } - signedPreKey := signedPreKeys[installationID] - if signedPreKey == nil { - return 0 - } + signedPreKey := signedPreKeys[installationID] + if signedPreKey == nil { + return 0 + } - return signedPreKey.GetProtocolVersion() + return signedPreKey.GetProtocolVersion() + } } return 0 diff --git a/services/shhext/chat/protocol_test.go b/services/shhext/chat/protocol_test.go index f3e741c5c..3ab845cd8 100644 --- a/services/shhext/chat/protocol_test.go +++ b/services/shhext/chat/protocol_test.go @@ -5,7 +5,8 @@ import ( "testing" "github.com/ethereum/go-ethereum/crypto" - "github.com/status-im/status-go/services/shhext/chat/topic" + "github.com/status-im/status-go/services/shhext/chat/multidevice" + "github.com/status-im/status-go/services/shhext/chat/sharedsecret" "github.com/stretchr/testify/suite" ) @@ -38,21 +39,35 @@ func (s *ProtocolServiceTestSuite) SetupTest() { panic(err) } - addedBundlesHandler := func(addedBundles []IdentityAndIDPair) {} - onNewTopicHandler := func(topic []*topic.Secret) {} + addedBundlesHandler := func(addedBundles []multidevice.IdentityAndIDPair) {} + onNewSharedSecretHandler := func(secret []*sharedsecret.Secret) {} + + aliceMultideviceConfig := &multidevice.Config{ + MaxInstallations: 3, + InstallationID: "1", + ProtocolVersion: ProtocolVersion, + } s.alice = NewProtocolService( NewEncryptionService(alicePersistence, DefaultEncryptionServiceConfig("1")), - topic.NewService(alicePersistence.GetTopicStorage()), + sharedsecret.NewService(alicePersistence.GetSharedSecretStorage()), + multidevice.New(aliceMultideviceConfig, alicePersistence.GetMultideviceStorage()), addedBundlesHandler, - onNewTopicHandler, + onNewSharedSecretHandler, ) + bobMultideviceConfig := &multidevice.Config{ + MaxInstallations: 3, + InstallationID: "2", + ProtocolVersion: ProtocolVersion, + } + s.bob = NewProtocolService( NewEncryptionService(bobPersistence, DefaultEncryptionServiceConfig("2")), - topic.NewService(bobPersistence.GetTopicStorage()), + sharedsecret.NewService(bobPersistence.GetSharedSecretStorage()), + multidevice.New(bobMultideviceConfig, bobPersistence.GetMultideviceStorage()), addedBundlesHandler, - onNewTopicHandler, + onNewSharedSecretHandler, ) } @@ -79,9 +94,12 @@ func (s *ProtocolServiceTestSuite) TestBuildDirectMessage() { payload := []byte("test") - msg, _, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, payload) + msgSpec, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, payload) s.NoError(err) - s.NotNil(msg, "It creates a message") + s.NotNil(msgSpec, "It creates a message spec") + + msg := msgSpec.Message + s.NotNil(msg, "It creates a messages") s.NotNilf(msg.GetBundle(), "It adds a bundle to the message") @@ -95,6 +113,32 @@ func (s *ProtocolServiceTestSuite) TestBuildDirectMessage() { } func (s *ProtocolServiceTestSuite) TestBuildAndReadDirectMessage() { + bobKey, err := crypto.GenerateKey() + s.Require().NoError(err) + aliceKey, err := crypto.GenerateKey() + s.Require().NoError(err) + + payload := []byte("test") + + // Message is sent with DH + msgSpec, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, payload) + s.Require().NoError(err) + s.Require().NotNil(msgSpec) + + msg := msgSpec.Message + s.Require().NotNil(msg) + + // Bob is able to decrypt the message + unmarshaledMsg, err := s.bob.HandleMessage(bobKey, &aliceKey.PublicKey, msg, []byte("message-id")) + s.NoError(err) + s.NotNil(unmarshaledMsg) + + recoveredPayload := []byte("test") + s.Equalf(payload, recoveredPayload, "It successfully unmarshal the decrypted message") +} + +func (s *ProtocolServiceTestSuite) TestSecretNegotiation() { + var secretResponse []*sharedsecret.Secret bobKey, err := crypto.GenerateKey() s.NoError(err) aliceKey, err := crypto.GenerateKey() @@ -102,17 +146,26 @@ func (s *ProtocolServiceTestSuite) TestBuildAndReadDirectMessage() { payload := []byte("test") - // Message is sent with DH - marshaledMsg, _, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, payload) + s.bob.onNewSharedSecretHandler = func(secret []*sharedsecret.Secret) { + secretResponse = secret + } + msgSpec, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, payload) + s.NoError(err) + s.NotNil(msgSpec, "It creates a message spec") + bundle := msgSpec.Message.GetBundle() + s.Require().NotNil(bundle) + + signedPreKeys := bundle.GetSignedPreKeys() + s.Require().NotNil(signedPreKeys) + + signedPreKey := signedPreKeys["1"] + s.Require().NotNil(signedPreKey) + + s.Require().Equal(uint32(1), signedPreKey.GetProtocolVersion()) + + _, err = s.bob.HandleMessage(bobKey, &aliceKey.PublicKey, msgSpec.Message, []byte("message-id")) s.NoError(err) - // Bob is able to decrypt the message - unmarshaledMsg, err := s.bob.HandleMessage(bobKey, &aliceKey.PublicKey, marshaledMsg, []byte("message-id")) - s.NoError(err) - - s.NotNil(unmarshaledMsg) - - recoveredPayload := []byte("test") - s.Equalf(payload, recoveredPayload, "It successfully unmarshal the decrypted message") + s.Require().NotNil(secretResponse) } diff --git a/services/shhext/chat/topic/persistence.go b/services/shhext/chat/sharedsecret/persistence.go similarity index 85% rename from services/shhext/chat/topic/persistence.go rename to services/shhext/chat/sharedsecret/persistence.go index 2d95ce199..9a7769f0e 100644 --- a/services/shhext/chat/topic/persistence.go +++ b/services/shhext/chat/sharedsecret/persistence.go @@ -1,4 +1,4 @@ -package topic +package sharedsecret import ( "database/sql" @@ -30,20 +30,20 @@ func (s *SQLLitePersistence) Add(identity []byte, secret []byte, installationID return err } - insertTopicStmt, err := tx.Prepare("INSERT INTO topics(identity, secret) VALUES (?, ?)") + insertSecretStmt, err := tx.Prepare("INSERT INTO secrets(identity, secret) VALUES (?, ?)") if err != nil { _ = tx.Rollback() return err } - defer insertTopicStmt.Close() + defer insertSecretStmt.Close() - _, err = insertTopicStmt.Exec(identity, secret) + _, err = insertSecretStmt.Exec(identity, secret) if err != nil { _ = tx.Rollback() return err } - insertInstallationIDStmt, err := tx.Prepare("INSERT INTO topic_installation_ids(id, identity_id) VALUES (?, ?)") + insertInstallationIDStmt, err := tx.Prepare("INSERT INTO secret_installation_ids(id, identity_id) VALUES (?, ?)") if err != nil { _ = tx.Rollback() return err @@ -70,9 +70,9 @@ func (s *SQLLitePersistence) Get(identity []byte, installationIDs []string) (*Re /* #nosec */ query := `SELECT secret, id - FROM topics t + FROM secrets t JOIN - topic_installation_ids tid + secret_installation_ids tid ON t.identity = tid.identity_id WHERE t.identity = ? @@ -101,7 +101,7 @@ func (s *SQLLitePersistence) Get(identity []byte, installationIDs []string) (*Re func (s *SQLLitePersistence) All() ([][][]byte, error) { query := `SELECT identity, secret - FROM topics` + FROM secrets` var secrets [][][]byte diff --git a/services/shhext/chat/topic/service.go b/services/shhext/chat/sharedsecret/service.go similarity index 80% rename from services/shhext/chat/topic/service.go rename to services/shhext/chat/sharedsecret/service.go index d93e28f37..f674852fe 100644 --- a/services/shhext/chat/topic/service.go +++ b/services/shhext/chat/sharedsecret/service.go @@ -1,4 +1,4 @@ -package topic +package sharedsecret import ( "crypto/ecdsa" @@ -17,8 +17,8 @@ func NewService(persistence PersistenceService) *Service { return &Service{persistence: persistence} } -func (s *Service) setupTopic(myPrivateKey *ecdsa.PrivateKey, theirPublicKey *ecdsa.PublicKey, installationID string) (*Secret, error) { - log.Info("Setup topic called for", "installationID", installationID) +func (s *Service) setup(myPrivateKey *ecdsa.PrivateKey, theirPublicKey *ecdsa.PublicKey, installationID string) (*Secret, error) { + log.Info("Setup called for", "installationID", installationID) sharedKey, err := ecies.ImportECDSA(myPrivateKey).GenerateShared( ecies.ImportECDSAPublic(theirPublicKey), sskLen, @@ -38,16 +38,20 @@ func (s *Service) setupTopic(myPrivateKey *ecdsa.PrivateKey, theirPublicKey *ecd // Receive will generate a shared secret for a given identity, and return it func (s *Service) Receive(myPrivateKey *ecdsa.PrivateKey, theirPublicKey *ecdsa.PublicKey, installationID string) (*Secret, error) { - return s.setupTopic(myPrivateKey, theirPublicKey, installationID) + return s.setup(myPrivateKey, theirPublicKey, installationID) } // Send returns a shared key and whether it has been acknowledged from all the installationIDs func (s *Service) Send(myPrivateKey *ecdsa.PrivateKey, myInstallationID string, theirPublicKey *ecdsa.PublicKey, theirInstallationIDs []string) (*Secret, bool, error) { - sharedKey, err := s.setupTopic(myPrivateKey, theirPublicKey, myInstallationID) + secret, err := s.setup(myPrivateKey, theirPublicKey, myInstallationID) if err != nil { return nil, false, err } + if len(theirInstallationIDs) == 0 { + return secret, false, nil + } + theirIdentity := crypto.CompressPubkey(theirPublicKey) response, err := s.persistence.Get(theirIdentity, theirInstallationIDs) if err != nil { @@ -56,14 +60,11 @@ func (s *Service) Send(myPrivateKey *ecdsa.PrivateKey, myInstallationID string, for _, installationID := range theirInstallationIDs { if !response.installationIDs[installationID] { - return sharedKey, false, nil + return secret, false, nil } } - return &Secret{ - Key: response.secret, - Identity: theirPublicKey, - }, true, nil + return secret, true, nil } type Secret struct { diff --git a/services/shhext/chat/topic/service_test.go b/services/shhext/chat/sharedsecret/service_test.go similarity index 94% rename from services/shhext/chat/topic/service_test.go rename to services/shhext/chat/sharedsecret/service_test.go index 35458ac09..892782a00 100644 --- a/services/shhext/chat/topic/service_test.go +++ b/services/shhext/chat/sharedsecret/service_test.go @@ -1,4 +1,4 @@ -package topic +package sharedsecret import ( "io/ioutil" @@ -21,7 +21,7 @@ type ServiceTestSuite struct { } func (s *ServiceTestSuite) SetupTest() { - dbFile, err := ioutil.TempFile(os.TempDir(), "topic") + dbFile, err := ioutil.TempFile(os.TempDir(), "sharedsecret") s.Require().NoError(err) s.path = dbFile.Name() @@ -103,12 +103,12 @@ func (s *ServiceTestSuite) TestAll() { s.Require().NoError(err) s.Require().NotNil(sharedKey2, "it generates a shared key") - // All the topics are there - topics, err := s.service.All() + // All the secrets are there + secrets, err := s.service.All() s.Require().NoError(err) expected := []*Secret{ sharedKey1, sharedKey2, } - s.Require().Equal(expected, topics) + s.Require().Equal(expected, secrets) } diff --git a/services/shhext/chat/sql_lite_persistence.go b/services/shhext/chat/sql_lite_persistence.go index 6af73f8ee..ff7aecb2f 100644 --- a/services/shhext/chat/sql_lite_persistence.go +++ b/services/shhext/chat/sql_lite_persistence.go @@ -13,7 +13,9 @@ import ( "github.com/status-im/migrate/v4/source/go_bindata" ecrypto "github.com/status-im/status-go/services/shhext/chat/crypto" appDB "github.com/status-im/status-go/services/shhext/chat/db" - "github.com/status-im/status-go/services/shhext/chat/topic" + "github.com/status-im/status-go/services/shhext/chat/multidevice" + "github.com/status-im/status-go/services/shhext/chat/protobuf" + "github.com/status-im/status-go/services/shhext/chat/sharedsecret" ) // A safe max number of rows @@ -21,10 +23,11 @@ const maxNumberOfRows = 100000000 // SQLLitePersistence represents a persistence service tied to an SQLite database type SQLLitePersistence struct { - db *sql.DB - keysStorage dr.KeysStorage - sessionStorage dr.SessionStorage - topicStorage topic.PersistenceService + db *sql.DB + keysStorage dr.KeysStorage + sessionStorage dr.SessionStorage + secretStorage sharedsecret.PersistenceService + multideviceStorage multidevice.Persistence } // SQLLiteKeysStorage represents a keys persistence service tied to an SQLite database @@ -49,7 +52,9 @@ func NewSQLLitePersistence(path string, key string) (*SQLLitePersistence, error) s.sessionStorage = NewSQLLiteSessionStorage(s.db) - s.topicStorage = topic.NewSQLLitePersistence(s.db) + s.secretStorage = sharedsecret.NewSQLLitePersistence(s.db) + + s.multideviceStorage = multidevice.NewSQLLitePersistence(s.db) return s, nil } @@ -78,9 +83,14 @@ func (s *SQLLitePersistence) GetSessionStorage() dr.SessionStorage { return s.sessionStorage } -// GetTopicStorage returns the associated topicStorageObject -func (s *SQLLitePersistence) GetTopicStorage() topic.PersistenceService { - return s.topicStorage +// GetSharedSecretStorage returns the associated secretStorageObject +func (s *SQLLitePersistence) GetSharedSecretStorage() sharedsecret.PersistenceService { + return s.secretStorage +} + +// GetMultideviceStorage returns the associated multideviceStorage +func (s *SQLLitePersistence) GetMultideviceStorage() multidevice.Persistence { + return s.multideviceStorage } // Open opens a file at the specified path @@ -96,7 +106,7 @@ func (s *SQLLitePersistence) Open(path string, key string) error { } // AddPrivateBundle adds the specified BundleContainer to the database -func (s *SQLLitePersistence) AddPrivateBundle(bc *BundleContainer) error { +func (s *SQLLitePersistence) AddPrivateBundle(bc *protobuf.BundleContainer) error { tx, err := s.db.Begin() if err != nil { return err @@ -150,7 +160,7 @@ func (s *SQLLitePersistence) AddPrivateBundle(bc *BundleContainer) error { } // AddPublicBundle adds the specified Bundle to the database -func (s *SQLLitePersistence) AddPublicBundle(b *Bundle) error { +func (s *SQLLitePersistence) AddPublicBundle(b *protobuf.Bundle) error { tx, err := s.db.Begin() if err != nil { @@ -203,7 +213,7 @@ func (s *SQLLitePersistence) AddPublicBundle(b *Bundle) error { } // GetAnyPrivateBundle retrieves any bundle from the database containing a private key -func (s *SQLLitePersistence) GetAnyPrivateBundle(myIdentityKey []byte, installations []*Installation) (*BundleContainer, error) { +func (s *SQLLitePersistence) GetAnyPrivateBundle(myIdentityKey []byte, installations []*multidevice.Installation) (*protobuf.BundleContainer, error) { versions := make(map[string]uint32) /* #nosec */ @@ -239,11 +249,11 @@ func (s *SQLLitePersistence) GetAnyPrivateBundle(myIdentityKey []byte, installat defer rows.Close() - bundle := &Bundle{ - SignedPreKeys: make(map[string]*SignedPreKey), + bundle := &protobuf.Bundle{ + SignedPreKeys: make(map[string]*protobuf.SignedPreKey), } - bundleContainer := &BundleContainer{ + bundleContainer := &protobuf.BundleContainer{ Bundle: bundle, } @@ -267,7 +277,7 @@ func (s *SQLLitePersistence) GetAnyPrivateBundle(myIdentityKey []byte, installat bundle.Timestamp = timestamp } - bundle.SignedPreKeys[installationID] = &SignedPreKey{ + bundle.SignedPreKeys[installationID] = &protobuf.SignedPreKey{ SignedPreKey: signedPreKey, Version: version, ProtocolVersion: versions[installationID], @@ -323,7 +333,7 @@ func (s *SQLLitePersistence) MarkBundleExpired(identity []byte) error { } // GetPublicBundle retrieves an existing Bundle for the specified public key from the database -func (s *SQLLitePersistence) GetPublicBundle(publicKey *ecdsa.PublicKey, installations []*Installation) (*Bundle, error) { +func (s *SQLLitePersistence) GetPublicBundle(publicKey *ecdsa.PublicKey, installations []*multidevice.Installation) (*protobuf.Bundle, error) { if len(installations) == 0 { return nil, nil @@ -360,9 +370,9 @@ func (s *SQLLitePersistence) GetPublicBundle(publicKey *ecdsa.PublicKey, install defer rows.Close() - bundle := &Bundle{ + bundle := &protobuf.Bundle{ Identity: identity, - SignedPreKeys: make(map[string]*SignedPreKey), + SignedPreKeys: make(map[string]*protobuf.SignedPreKey), } for rows.Next() { @@ -379,7 +389,7 @@ func (s *SQLLitePersistence) GetPublicBundle(publicKey *ecdsa.PublicKey, install return nil, err } - bundle.SignedPreKeys[installationID] = &SignedPreKey{ + bundle.SignedPreKeys[installationID] = &protobuf.SignedPreKey{ SignedPreKey: signedPreKey, Version: version, ProtocolVersion: versions[installationID], @@ -754,159 +764,6 @@ func (s *SQLLiteSessionStorage) Load(id []byte) (*dr.State, error) { } } -// GetActiveInstallations returns the active installations for a given identity -func (s *SQLLitePersistence) GetActiveInstallations(maxInstallations int, identity []byte) ([]*Installation, error) { - stmt, err := s.db.Prepare(`SELECT installation_id, version - FROM installations - WHERE enabled = 1 AND identity = ? - ORDER BY timestamp DESC - LIMIT ?`) - if err != nil { - return nil, err - } - - var installations []*Installation - rows, err := stmt.Query(identity, maxInstallations) - if err != nil { - return nil, err - } - - for rows.Next() { - var installationID string - var version uint32 - err = rows.Scan( - &installationID, - &version, - ) - if err != nil { - return nil, err - } - installations = append(installations, &Installation{ - ID: installationID, - Version: version, - }) - - } - - return installations, nil - -} - -// AddInstallations adds the installations for a given identity, maintaining the enabled flag -func (s *SQLLitePersistence) AddInstallations(identity []byte, timestamp int64, installations []*Installation, defaultEnabled bool) error { - tx, err := s.db.Begin() - if err != nil { - return nil - } - - for _, installation := range installations { - stmt, err := tx.Prepare(`SELECT enabled, version - FROM installations - WHERE identity = ? AND installation_id = ? - LIMIT 1`) - if err != nil { - return err - } - defer stmt.Close() - - var oldEnabled bool - // We don't override version once we saw one - var oldVersion uint32 - latestVersion := installation.Version - - err = stmt.QueryRow(identity, installation.ID).Scan(&oldEnabled, &oldVersion) - if err != nil && err != sql.ErrNoRows { - return err - } - - // We update timestamp if present without changing enabled, only if this is a new bundle - // and we set the version to the latest we ever saw - if err != sql.ErrNoRows { - if oldVersion > installation.Version { - latestVersion = oldVersion - } - - stmt, err = tx.Prepare(`UPDATE installations - SET timestamp = ?, enabled = ?, version = ? - WHERE identity = ? - AND installation_id = ? - AND timestamp < ?`) - if err != nil { - return err - } - - _, err = stmt.Exec( - timestamp, - oldEnabled, - latestVersion, - identity, - installation.ID, - timestamp, - ) - if err != nil { - return err - } - defer stmt.Close() - - } else { - stmt, err = tx.Prepare(`INSERT INTO installations(identity, installation_id, timestamp, enabled, version) - VALUES (?, ?, ?, ?, ?)`) - if err != nil { - return err - } - - _, err = stmt.Exec( - identity, - installation.ID, - timestamp, - defaultEnabled, - latestVersion, - ) - if err != nil { - return err - } - defer stmt.Close() - } - - } - - if err := tx.Commit(); err != nil { - _ = tx.Rollback() - return err - } - - return nil - -} - -// EnableInstallation enables the installation -func (s *SQLLitePersistence) EnableInstallation(identity []byte, installationID string) error { - stmt, err := s.db.Prepare(`UPDATE installations - SET enabled = 1 - WHERE identity = ? AND installation_id = ?`) - if err != nil { - return err - } - - _, err = stmt.Exec(identity, installationID) - return err - -} - -// DisableInstallation disable the installation -func (s *SQLLitePersistence) DisableInstallation(identity []byte, installationID string) error { - - stmt, err := s.db.Prepare(`UPDATE installations - SET enabled = 0 - WHERE identity = ? AND installation_id = ?`) - if err != nil { - return err - } - - _, err = stmt.Exec(identity, installationID) - return err -} - func toKey(a []byte) dr.Key { var k [32]byte copy(k[:], a) diff --git a/services/shhext/chat/sql_lite_persistence_test.go b/services/shhext/chat/sql_lite_persistence_test.go index 097ffc66c..e7dbfac30 100644 --- a/services/shhext/chat/sql_lite_persistence_test.go +++ b/services/shhext/chat/sql_lite_persistence_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/crypto" + "github.com/status-im/status-go/services/shhext/chat/multidevice" "github.com/stretchr/testify/suite" ) @@ -53,7 +54,7 @@ func (s *SQLLitePersistenceTestSuite) TestPrivateBundle() { s.Require().NoError(err, "Error was not returned even though bundle is not there") s.Nil(actualKey) - anyPrivateBundle, err := s.service.GetAnyPrivateBundle([]byte("non-existing-id"), []*Installation{{ID: installationID, Version: 1}}) + anyPrivateBundle, err := s.service.GetAnyPrivateBundle([]byte("non-existing-id"), []*multidevice.Installation{{ID: installationID, Version: 1}}) s.Require().NoError(err) s.Nil(anyPrivateBundle) @@ -70,7 +71,7 @@ func (s *SQLLitePersistenceTestSuite) TestPrivateBundle() { s.Equal(bundle.GetPrivateSignedPreKey(), actualKey, "It returns the same key") identity := crypto.CompressPubkey(&key.PublicKey) - anyPrivateBundle, err = s.service.GetAnyPrivateBundle(identity, []*Installation{{ID: installationID, Version: 1}}) + anyPrivateBundle, err = s.service.GetAnyPrivateBundle(identity, []*multidevice.Installation{{ID: installationID, Version: 1}}) s.Require().NoError(err) s.NotNil(anyPrivateBundle) s.Equal(bundle.GetBundle().GetSignedPreKeys()[installationID].SignedPreKey, anyPrivateBundle.GetBundle().GetSignedPreKeys()[installationID].SignedPreKey, "It returns the same bundle") @@ -80,7 +81,7 @@ func (s *SQLLitePersistenceTestSuite) TestPublicBundle() { key, err := crypto.GenerateKey() s.Require().NoError(err) - actualBundle, err := s.service.GetPublicBundle(&key.PublicKey, []*Installation{{ID: "1", Version: 1}}) + actualBundle, err := s.service.GetPublicBundle(&key.PublicKey, []*multidevice.Installation{{ID: "1", Version: 1}}) s.Require().NoError(err, "Error was not returned even though bundle is not there") s.Nil(actualBundle) @@ -91,7 +92,7 @@ func (s *SQLLitePersistenceTestSuite) TestPublicBundle() { err = s.service.AddPublicBundle(bundle) s.Require().NoError(err) - actualBundle, err = s.service.GetPublicBundle(&key.PublicKey, []*Installation{{ID: "1", Version: 1}}) + actualBundle, err = s.service.GetPublicBundle(&key.PublicKey, []*multidevice.Installation{{ID: "1", Version: 1}}) s.Require().NoError(err) s.Equal(bundle.GetIdentity(), actualBundle.GetIdentity(), "It sets the right identity") s.Equal(bundle.GetSignedPreKeys(), actualBundle.GetSignedPreKeys(), "It sets the right prekeys") @@ -101,7 +102,7 @@ func (s *SQLLitePersistenceTestSuite) TestUpdatedBundle() { key, err := crypto.GenerateKey() s.Require().NoError(err) - actualBundle, err := s.service.GetPublicBundle(&key.PublicKey, []*Installation{{ID: "1", Version: 1}}) + actualBundle, err := s.service.GetPublicBundle(&key.PublicKey, []*multidevice.Installation{{ID: "1", Version: 1}}) s.Require().NoError(err, "Error was not returned even though bundle is not there") s.Nil(actualBundle) @@ -123,7 +124,7 @@ func (s *SQLLitePersistenceTestSuite) TestUpdatedBundle() { err = s.service.AddPublicBundle(bundle) s.Require().NoError(err) - actualBundle, err = s.service.GetPublicBundle(&key.PublicKey, []*Installation{{ID: "1", Version: 1}}) + actualBundle, err = s.service.GetPublicBundle(&key.PublicKey, []*multidevice.Installation{{ID: "1", Version: 1}}) s.Require().NoError(err) s.Equal(bundle.GetIdentity(), actualBundle.GetIdentity(), "It sets the right identity") s.Equal(bundle.GetSignedPreKeys(), actualBundle.GetSignedPreKeys(), "It sets the right prekeys") @@ -133,7 +134,7 @@ func (s *SQLLitePersistenceTestSuite) TestOutOfOrderBundles() { key, err := crypto.GenerateKey() s.Require().NoError(err) - actualBundle, err := s.service.GetPublicBundle(&key.PublicKey, []*Installation{{ID: "1", Version: 1}}) + actualBundle, err := s.service.GetPublicBundle(&key.PublicKey, []*multidevice.Installation{{ID: "1", Version: 1}}) s.Require().NoError(err, "Error was not returned even though bundle is not there") s.Nil(actualBundle) @@ -160,7 +161,7 @@ func (s *SQLLitePersistenceTestSuite) TestOutOfOrderBundles() { err = s.service.AddPublicBundle(bundle1) s.Require().NoError(err) - actualBundle, err = s.service.GetPublicBundle(&key.PublicKey, []*Installation{{ID: "1", Version: 1}}) + actualBundle, err = s.service.GetPublicBundle(&key.PublicKey, []*multidevice.Installation{{ID: "1", Version: 1}}) s.Require().NoError(err) s.Equal(bundle2.GetIdentity(), actualBundle.GetIdentity(), "It sets the right identity") s.Equal(bundle2.GetSignedPreKeys()["1"].GetVersion(), uint32(1)) @@ -171,7 +172,7 @@ func (s *SQLLitePersistenceTestSuite) TestMultiplePublicBundle() { key, err := crypto.GenerateKey() s.Require().NoError(err) - actualBundle, err := s.service.GetPublicBundle(&key.PublicKey, []*Installation{{ID: "1", Version: 1}}) + actualBundle, err := s.service.GetPublicBundle(&key.PublicKey, []*multidevice.Installation{{ID: "1", Version: 1}}) s.Require().NoError(err, "Error was not returned even though bundle is not there") s.Nil(actualBundle) @@ -197,7 +198,7 @@ func (s *SQLLitePersistenceTestSuite) TestMultiplePublicBundle() { s.Require().NoError(err) // Returns the most recent bundle - actualBundle, err = s.service.GetPublicBundle(&key.PublicKey, []*Installation{{ID: "1", Version: 1}}) + actualBundle, err = s.service.GetPublicBundle(&key.PublicKey, []*multidevice.Installation{{ID: "1", Version: 1}}) s.Require().NoError(err) s.Equal(bundle.GetIdentity(), actualBundle.GetIdentity(), "It sets the identity") @@ -209,7 +210,7 @@ func (s *SQLLitePersistenceTestSuite) TestMultiDevicePublicBundle() { key, err := crypto.GenerateKey() s.Require().NoError(err) - actualBundle, err := s.service.GetPublicBundle(&key.PublicKey, []*Installation{{ID: "1", Version: 1}}) + actualBundle, err := s.service.GetPublicBundle(&key.PublicKey, []*multidevice.Installation{{ID: "1", Version: 1}}) s.Require().NoError(err, "Error was not returned even though bundle is not there") s.Nil(actualBundle) @@ -234,7 +235,7 @@ func (s *SQLLitePersistenceTestSuite) TestMultiDevicePublicBundle() { // Returns the most recent bundle actualBundle, err = s.service.GetPublicBundle(&key.PublicKey, - []*Installation{ + []*multidevice.Installation{ {ID: "1", Version: 1}, {ID: "2", Version: 1}, }) @@ -347,211 +348,4 @@ func (s *SQLLitePersistenceTestSuite) TestRatchetInfoNoBundle() { s.Nil(ratchetInfo, "It returns nil when no bundle is there") } -func (s *SQLLitePersistenceTestSuite) TestAddInstallations() { - identity := []byte("alice") - installations := []*Installation{ - {ID: "alice-1", Version: 1}, - {ID: "alice-2", Version: 2}, - } - err := s.service.AddInstallations( - identity, - 1, - installations, - true, - ) - - s.Require().NoError(err) - - enabledInstallations, err := s.service.GetActiveInstallations(5, identity) - s.Require().NoError(err) - - s.Require().Equal(installations, enabledInstallations) -} - -func (s *SQLLitePersistenceTestSuite) TestAddInstallationVersions() { - identity := []byte("alice") - installations := []*Installation{ - {ID: "alice-1", Version: 1}, - } - err := s.service.AddInstallations( - identity, - 1, - installations, - true, - ) - - s.Require().NoError(err) - - enabledInstallations, err := s.service.GetActiveInstallations(5, identity) - s.Require().NoError(err) - - s.Require().Equal(installations, enabledInstallations) - - installationsWithDowngradedVersion := []*Installation{ - {ID: "alice-1", Version: 0}, - } - - err = s.service.AddInstallations( - identity, - 3, - installationsWithDowngradedVersion, - true, - ) - s.Require().NoError(err) - - enabledInstallations, err = s.service.GetActiveInstallations(5, identity) - s.Require().NoError(err) - s.Require().Equal(installations, enabledInstallations) -} - -func (s *SQLLitePersistenceTestSuite) TestAddInstallationsLimit() { - identity := []byte("alice") - - installations := []*Installation{ - {ID: "alice-1", Version: 1}, - {ID: "alice-2", Version: 2}, - } - - err := s.service.AddInstallations( - identity, - 1, - installations, - true, - ) - s.Require().NoError(err) - - installations = []*Installation{ - {ID: "alice-1", Version: 1}, - {ID: "alice-3", Version: 3}, - } - - err = s.service.AddInstallations( - identity, - 2, - installations, - true, - ) - s.Require().NoError(err) - - installations = []*Installation{ - {ID: "alice-2", Version: 2}, - {ID: "alice-3", Version: 3}, - {ID: "alice-4", Version: 4}, - } - - err = s.service.AddInstallations( - identity, - 3, - installations, - true, - ) - s.Require().NoError(err) - - enabledInstallations, err := s.service.GetActiveInstallations(3, identity) - s.Require().NoError(err) - - s.Require().Equal(installations, enabledInstallations) -} - -func (s *SQLLitePersistenceTestSuite) TestAddInstallationsDisabled() { - identity := []byte("alice") - - installations := []*Installation{ - {ID: "alice-1", Version: 1}, - {ID: "alice-2", Version: 2}, - } - - err := s.service.AddInstallations( - identity, - 1, - installations, - false, - ) - s.Require().NoError(err) - - actualInstallations, err := s.service.GetActiveInstallations(3, identity) - s.Require().NoError(err) - - s.Require().Nil(actualInstallations) -} - -func (s *SQLLitePersistenceTestSuite) TestDisableInstallation() { - identity := []byte("alice") - - installations := []*Installation{ - {ID: "alice-1", Version: 1}, - {ID: "alice-2", Version: 2}, - } - - err := s.service.AddInstallations( - identity, - 1, - installations, - true, - ) - s.Require().NoError(err) - - err = s.service.DisableInstallation(identity, "alice-1") - s.Require().NoError(err) - - // We add the installations again - installations = []*Installation{ - {ID: "alice-1", Version: 1}, - {ID: "alice-2", Version: 2}, - } - - err = s.service.AddInstallations( - identity, - 1, - installations, - true, - ) - s.Require().NoError(err) - - actualInstallations, err := s.service.GetActiveInstallations(3, identity) - s.Require().NoError(err) - - expected := []*Installation{{ID: "alice-2", Version: 2}} - s.Require().Equal(expected, actualInstallations) -} - -func (s *SQLLitePersistenceTestSuite) TestEnableInstallation() { - identity := []byte("alice") - - installations := []*Installation{ - {ID: "alice-1", Version: 1}, - {ID: "alice-2", Version: 2}, - } - - err := s.service.AddInstallations( - identity, - 1, - installations, - true, - ) - s.Require().NoError(err) - - err = s.service.DisableInstallation(identity, "alice-1") - s.Require().NoError(err) - - actualInstallations, err := s.service.GetActiveInstallations(3, identity) - s.Require().NoError(err) - - expected := []*Installation{{ID: "alice-2", Version: 2}} - s.Require().Equal(expected, actualInstallations) - - err = s.service.EnableInstallation(identity, "alice-1") - s.Require().NoError(err) - - actualInstallations, err = s.service.GetActiveInstallations(3, identity) - s.Require().NoError(err) - - expected = []*Installation{ - {ID: "alice-1", Version: 1}, - {ID: "alice-2", Version: 2}, - } - s.Require().Equal(expected, actualInstallations) - -} - // TODO: Add test for MarkBundleExpired diff --git a/services/shhext/chat/whisper.go b/services/shhext/chat/whisper.go deleted file mode 100644 index d726835bd..000000000 --- a/services/shhext/chat/whisper.go +++ /dev/null @@ -1,64 +0,0 @@ -package chat - -import ( - "github.com/ethereum/go-ethereum/crypto" - whisper "github.com/status-im/whisper/whisperv6" -) - -var discoveryTopic = "contact-discovery" -var discoveryTopicBytes = toTopic(discoveryTopic) - -var topicSalt = []byte{0x01, 0x02, 0x03, 0x04} - -func toTopic(s string) whisper.TopicType { - return whisper.BytesToTopic(crypto.Keccak256([]byte(s))) -} - -func SharedSecretToTopic(secret []byte) whisper.TopicType { - return whisper.BytesToTopic(crypto.Keccak256(append(secret, topicSalt...))) -} - -func defaultWhisperMessage() whisper.NewMessage { - msg := whisper.NewMessage{} - - msg.TTL = 10 - msg.PowTarget = 0.002 - msg.PowTime = 1 - - return msg -} - -func PublicMessageToWhisper(rpcMsg SendPublicMessageRPC, payload []byte) whisper.NewMessage { - msg := defaultWhisperMessage() - - msg.Topic = toTopic(rpcMsg.Chat) - - msg.Payload = payload - msg.Sig = rpcMsg.Sig - - return msg -} - -func DirectMessageToWhisper(rpcMsg SendDirectMessageRPC, payload []byte, sharedSecret []byte) whisper.NewMessage { - var topicBytes whisper.TopicType - msg := defaultWhisperMessage() - - if rpcMsg.Chat == "" { - if sharedSecret != nil { - topicBytes = SharedSecretToTopic(sharedSecret) - } else { - topicBytes = discoveryTopicBytes - msg.PublicKey = rpcMsg.PubKey - } - } else { - topicBytes = toTopic(rpcMsg.Chat) - msg.PublicKey = rpcMsg.PubKey - } - - msg.Topic = topicBytes - - msg.Payload = payload - msg.Sig = rpcMsg.Sig - - return msg -} diff --git a/services/shhext/chat/whisper_test.go b/services/shhext/chat/whisper_test.go deleted file mode 100644 index 352c90e6f..000000000 --- a/services/shhext/chat/whisper_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package chat - -import ( - whisper "github.com/status-im/whisper/whisperv6" - - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestPublicMessageToWhisper(t *testing.T) { - rpcMessage := SendPublicMessageRPC{ - Chat: "test-chat", - Sig: "test", - } - - payload := []byte("test") - whisperMessage := PublicMessageToWhisper(rpcMessage, payload) - - assert.Equalf(t, uint32(10), whisperMessage.TTL, "It sets the TTL") - assert.Equalf(t, 0.002, whisperMessage.PowTarget, "It sets the pow target") - assert.Equalf(t, uint32(1), whisperMessage.PowTime, "It sets the pow time") - assert.Equalf(t, whisper.TopicType{0xa4, 0xab, 0xdf, 0x64}, whisperMessage.Topic, "It sets the topic") -} - -func TestDirectMessageToWhisper(t *testing.T) { - rpcMessage := SendDirectMessageRPC{ - PubKey: []byte("some pubkey"), - Sig: "test", - } - - payload := []byte("test") - whisperMessage := DirectMessageToWhisper(rpcMessage, payload, nil) - - assert.Equalf(t, uint32(10), whisperMessage.TTL, "It sets the TTL") - assert.Equalf(t, 0.002, whisperMessage.PowTarget, "It sets the pow target") - assert.Equalf(t, uint32(1), whisperMessage.PowTime, "It sets the pow time") - assert.Equalf(t, whisper.TopicType{0xf8, 0x94, 0x6a, 0xac}, whisperMessage.Topic, "It sets the discovery topic") -} - -func TestDirectMessageToWhisperWithSharedSecret(t *testing.T) { - rpcMessage := SendDirectMessageRPC{ - PubKey: []byte("some pubkey"), - Sig: "test", - } - - payload := []byte("test") - secret := []byte("test-secret") - - whisperMessage := DirectMessageToWhisper(rpcMessage, payload, secret) - - assert.Equalf(t, uint32(10), whisperMessage.TTL, "It sets the TTL") - assert.Equalf(t, 0.002, whisperMessage.PowTarget, "It sets the pow target") - assert.Equalf(t, uint32(1), whisperMessage.PowTime, "It sets the pow time") - assert.Equalf(t, whisper.TopicType{0xd8, 0xa2, 0xf3, 0x64}, whisperMessage.Topic, "It sets the discovery topic") -} diff --git a/services/shhext/chat/x3dh.go b/services/shhext/chat/x3dh.go index 082410676..5f77533a8 100644 --- a/services/shhext/chat/x3dh.go +++ b/services/shhext/chat/x3dh.go @@ -2,16 +2,14 @@ package chat import ( "crypto/ecdsa" - "encoding/base64" "errors" - "fmt" "sort" "strconv" "time" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/ecies" - "github.com/golang/protobuf/proto" + "github.com/status-im/status-go/services/shhext/chat/protobuf" ) const ( @@ -19,33 +17,7 @@ const ( sskLen = 16 ) -// ToBase64 returns a Base64 encoding representation of the protobuf Bundle message -func (bundle *Bundle) ToBase64() (string, error) { - marshaledMessage, err := proto.Marshal(bundle) - if err != nil { - return "", err - } - - return base64.StdEncoding.EncodeToString(marshaledMessage), nil -} - -// FromBase64 unmarshals a Bundle from a Base64 encoding representation of the protobuf Bundle message -func FromBase64(str string) (*Bundle, error) { - bundle := &Bundle{} - - decodedBundle, err := base64.StdEncoding.DecodeString(str) - if err != nil { - return nil, err - } - - if err := proto.Unmarshal(decodedBundle, bundle); err != nil { - return nil, err - } - - return bundle, nil -} - -func buildSignatureMaterial(bundle *Bundle) []byte { +func buildSignatureMaterial(bundle *protobuf.Bundle) []byte { signedPreKeys := bundle.GetSignedPreKeys() timestamp := bundle.GetTimestamp() var keys []string @@ -73,7 +45,7 @@ func buildSignatureMaterial(bundle *Bundle) []byte { } -func SignBundle(identity *ecdsa.PrivateKey, bundleContainer *BundleContainer) error { +func SignBundle(identity *ecdsa.PrivateKey, bundleContainer *protobuf.BundleContainer) error { signatureMaterial := buildSignatureMaterial(bundleContainer.GetBundle()) signature, err := crypto.Sign(crypto.Keccak256(signatureMaterial), identity) @@ -85,7 +57,7 @@ func SignBundle(identity *ecdsa.PrivateKey, bundleContainer *BundleContainer) er } // NewBundleContainer creates a new BundleContainer from an identity private key -func NewBundleContainer(identity *ecdsa.PrivateKey, installationID string) (*BundleContainer, error) { +func NewBundleContainer(identity *ecdsa.PrivateKey, installationID string) (*protobuf.BundleContainer, error) { preKey, err := crypto.GenerateKey() if err != nil { return nil, err @@ -95,35 +67,35 @@ func NewBundleContainer(identity *ecdsa.PrivateKey, installationID string) (*Bun compressedIdentityKey := crypto.CompressPubkey(&identity.PublicKey) encodedPreKey := crypto.FromECDSA(preKey) - signedPreKeys := make(map[string]*SignedPreKey) - signedPreKeys[installationID] = &SignedPreKey{ - ProtocolVersion: protocolCurrentVersion, + signedPreKeys := make(map[string]*protobuf.SignedPreKey) + signedPreKeys[installationID] = &protobuf.SignedPreKey{ + ProtocolVersion: ProtocolVersion, SignedPreKey: compressedPreKey, } - bundle := Bundle{ + bundle := protobuf.Bundle{ Timestamp: time.Now().UnixNano(), Identity: compressedIdentityKey, SignedPreKeys: signedPreKeys, } - return &BundleContainer{ + return &protobuf.BundleContainer{ Bundle: &bundle, PrivateSignedPreKey: encodedPreKey, }, nil } // VerifyBundle checks that a bundle is valid -func VerifyBundle(bundle *Bundle) error { +func VerifyBundle(bundle *protobuf.Bundle) error { _, err := ExtractIdentity(bundle) return err } // ExtractIdentity extracts the identity key from a given bundle -func ExtractIdentity(bundle *Bundle) (string, error) { +func ExtractIdentity(bundle *protobuf.Bundle) (*ecdsa.PublicKey, error) { bundleIdentityKey, err := crypto.DecompressPubkey(bundle.GetIdentity()) if err != nil { - return "", err + return nil, err } signatureMaterial := buildSignatureMaterial(bundle) @@ -133,14 +105,14 @@ func ExtractIdentity(bundle *Bundle) (string, error) { bundle.GetSignature(), ) if err != nil { - return "", err + return nil, err } if crypto.PubkeyToAddress(*recoveredKey) != crypto.PubkeyToAddress(*bundleIdentityKey) { - return "", errors.New("identity key and signature mismatch") + return nil, errors.New("identity key and signature mismatch") } - return fmt.Sprintf("0x%x", crypto.FromECDSAPub(recoveredKey)), nil + return recoveredKey, nil } // PerformDH generates a shared key given a private and a public key diff --git a/services/shhext/chat/x3dh_test.go b/services/shhext/chat/x3dh_test.go index 5222c3973..e2a9ac587 100644 --- a/services/shhext/chat/x3dh_test.go +++ b/services/shhext/chat/x3dh_test.go @@ -6,6 +6,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/ecies" + "github.com/status-im/status-go/services/shhext/chat/protobuf" "github.com/stretchr/testify/require" ) @@ -14,12 +15,11 @@ const ( aliceEphemeralKey = "11111111111111111111111111111111" bobPrivateKey = "22222222222222222222222222222222" bobSignedPreKey = "33333333333333333333333333333333" - base64Bundle = "CiECkJmdu/QwNL/7HdU+rB60wzpOocT0i6WFz944MIQPBVUSKAoBMhIjCiECPHKt20/fCa+U8MlNf+kqOGp+cM+KHYWRY4a7JTXHsbEiQT9Wse3UkJgo6/1HzxQfHcZNBaMH0j+0eylfBf1ropsLZ7yZM98k/qDQ3ZW5uHXQ4zhY8E1Q7HDytqm62k5JIPYA" ) var sharedKey = []byte{0xa4, 0xe9, 0x23, 0xd0, 0xaf, 0x8f, 0xe7, 0x8a, 0x5, 0x63, 0x63, 0xbe, 0x20, 0xe7, 0x1c, 0xa, 0x58, 0xe5, 0x69, 0xea, 0x8f, 0xc1, 0xf7, 0x92, 0x89, 0xec, 0xa1, 0xd, 0x9f, 0x68, 0x13, 0x3a} -func bobBundle() (*Bundle, error) { +func bobBundle() (*protobuf.Bundle, error) { privateKey, err := crypto.ToECDSA([]byte(bobPrivateKey)) if err != nil { return nil, err @@ -37,10 +37,10 @@ func bobBundle() (*Bundle, error) { return nil, err } - signedPreKeys := make(map[string]*SignedPreKey) - signedPreKeys[bobInstallationID] = &SignedPreKey{SignedPreKey: compressedPreKey} + signedPreKeys := make(map[string]*protobuf.SignedPreKey) + signedPreKeys[bobInstallationID] = &protobuf.SignedPreKey{SignedPreKey: compressedPreKey} - bundle := Bundle{ + bundle := protobuf.Bundle{ Identity: crypto.CompressPubkey(&privateKey.PublicKey), SignedPreKeys: signedPreKeys, Signature: signature, @@ -76,8 +76,8 @@ func TestNewBundleContainer(t *testing.T) { require.Equal( t, - &privateKey.PublicKey, - recoveredPublicKey, + privateKey.PublicKey, + *recoveredPublicKey, "The correct public key should be recovered", ) } @@ -94,7 +94,7 @@ func TestSignBundle(t *testing.T) { // We add a signed pre key signedPreKeys := bundle1.GetSignedPreKeys() - signedPreKeys["2"] = &SignedPreKey{SignedPreKey: []byte("key")} + signedPreKeys["2"] = &protobuf.SignedPreKey{SignedPreKey: []byte("key")} err = SignBundle(privateKey, bundleContainer1) require.NoError(t, err) @@ -115,40 +115,12 @@ func TestSignBundle(t *testing.T) { require.Equal( t, - &privateKey.PublicKey, - recoveredPublicKey, + privateKey.PublicKey, + *recoveredPublicKey, "The correct public key should be recovered", ) } -func TestToBase64(t *testing.T) { - bundle, err := bobBundle() - require.NoError(t, err, "Test bundle should be generated without errors") - - actualBase64Bundle, err := bundle.ToBase64() - require.NoError(t, err, "No error should be reported") - require.Equal( - t, - base64Bundle, - actualBase64Bundle, - "The correct bundle should be generated", - ) -} - -func TestFromBase64(t *testing.T) { - expectedBundle, err := bobBundle() - require.NoError(t, err, "Test bundle should be generated without errors") - - actualBundle, err := FromBase64(base64Bundle) - require.NoError(t, err, "Bundle should be unmarshaled without errors") - require.Equal( - t, - expectedBundle, - actualBundle, - "The correct bundle should be generated", - ) -} - func TestExtractIdentity(t *testing.T) { privateKey, err := crypto.ToECDSA([]byte(alicePrivateKey)) require.NoError(t, err, "Private key should be generated without errors") @@ -168,8 +140,8 @@ func TestExtractIdentity(t *testing.T) { require.Equal( t, - "0x042ed557f5ad336b31a49857e4e9664954ac33385aa20a93e2d64bfe7f08f51277bcb27c1259f802a52ed3ea7ac939043f0cc864e27400294bf121f23877995852", - recoveredPublicKey, + privateKey.PublicKey, + *recoveredPublicKey, "The correct public key should be recovered", ) } diff --git a/services/shhext/filter/service.go b/services/shhext/filter/service.go index 9aac3536b..6bf8469ea 100644 --- a/services/shhext/filter/service.go +++ b/services/shhext/filter/service.go @@ -3,10 +3,11 @@ package filter import ( "crypto/ecdsa" "encoding/hex" + "errors" "fmt" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" - "github.com/status-im/status-go/services/shhext/chat/topic" + "github.com/status-im/status-go/services/shhext/chat/sharedsecret" whisper "github.com/status-im/whisper/whisperv6" "math/big" "sync" @@ -18,79 +19,262 @@ const ( // The number of partitions var nPartitions = big.NewInt(5000) - -func toTopic(s string) []byte { - return crypto.Keccak256([]byte(s))[:whisper.TopicLength] -} - -func chatIDToPartitionedTopic(identity string) (string, error) { - partition := big.NewInt(0) - publicKeyBytes, err := hex.DecodeString(identity) - if err != nil { - return "", err - } - - publicKey, err := crypto.UnmarshalPubkey(publicKeyBytes) - if err != nil { - return "", err - } - - partition.Mod(publicKey.X, nPartitions) - - return fmt.Sprintf("contact-discovery-%d", partition.Int64()), nil -} +var minPow = 0.0 type Filter struct { FilterID string - Topic []byte + Topic whisper.TopicType SymKeyID string } type Chat struct { // ChatID is the identifier of the chat - ChatID string + ChatID string `json:"chatId"` // SymKeyID is the symmetric key id used for symmetric chats - SymKeyID string + SymKeyID string `json:"symKeyId"` // OneToOne tells us if we need to use asymmetric encryption for this chat - OneToOne bool + OneToOne bool `json:"oneToOne"` // Listen is whether we are actually listening for messages on this chat, or the filter is only created in order to be able to post on the topic - Listen bool + Listen bool `json:"listen"` // FilterID the whisper filter id generated - FilterID string + FilterID string `json:"filterId"` // Identity is the public key of the other recipient for non-public chats - Identity string + Identity string `json:"identity"` // Topic is the whisper topic - Topic []byte + Topic whisper.TopicType `json:"topic"` } type Service struct { - keyID string whisper *whisper.Whisper - topic *topic.Service + secret *sharedsecret.Service chats map[string]*Chat mutex sync.Mutex } -func New(k string, w *whisper.Whisper, t *topic.Service) *Service { +// New returns a new filter service +func New(w *whisper.Whisper, s *sharedsecret.Service) *Service { return &Service{ - keyID: k, whisper: w, - topic: t, + secret: s, mutex: sync.Mutex{}, chats: make(map[string]*Chat), } } -// LoadDiscovery adds the discovery filter -func (s *Service) LoadDiscovery(myKey *ecdsa.PrivateKey) error { +// LoadChat should return a list of newly chats loaded +func (s *Service) Init(chats []*Chat) ([]*Chat, error) { + log.Debug("Initializing filter service", "chats", chats) + keyID := s.whisper.SelectedKeyPairID() + if keyID == "" { + return nil, errors.New("no key selected") + } + myKey, err := s.whisper.GetPrivateKey(keyID) + if err != nil { + return nil, err + } + + // Add our own topic + log.Debug("Loading one to one chats") + identityStr := fmt.Sprintf("%x", crypto.FromECDSAPub(&myKey.PublicKey)) + _, err = s.loadOneToOne(myKey, identityStr, true) + if err != nil { + log.Error("Error loading one to one chats", "err", err) + + return nil, err + } + + // Add discovery topic + log.Debug("Loading discovery topics") + err = s.loadDiscovery(myKey) + if err != nil { + return nil, err + } + + // Add the various one to one and public chats + log.Debug("Loading chats") + for _, chat := range chats { + _, err = s.load(myKey, chat) + if err != nil { + return nil, err + } + } + + // Add the negotiated secrets + log.Debug("Loading negotiated topics") + secrets, err := s.secret.All() + if err != nil { + return nil, err + } + + for _, secret := range secrets { + if _, err := s.ProcessNegotiatedSecret(secret); err != nil { + return nil, err + } + } + s.mutex.Lock() defer s.mutex.Unlock() - discoveryChat := &Chat{ - ChatID: discoveryTopic, + var allChats []*Chat + for _, chat := range s.chats { + allChats = append(allChats, chat) + } + return allChats, nil +} + +// Stop removes all the filters +func (s *Service) Stop() error { + for _, chat := range s.chats { + if err := s.Remove(chat); err != nil { + return err + } + } + return nil +} + +// Remove remove all the filters associated with a chat/identity +func (s *Service) Remove(chat *Chat) error { + s.mutex.Lock() + defer s.mutex.Unlock() + + if err := s.whisper.Unsubscribe(chat.FilterID); err != nil { + return err + } + if chat.SymKeyID != "" { + s.whisper.DeleteSymKey(chat.SymKeyID) + } + delete(s.chats, chat.ChatID) + + return nil +} + +// LoadPartitioned creates a filter for a partitioned topic +func (s *Service) LoadPartitioned(myKey *ecdsa.PrivateKey, theirPublicKey *ecdsa.PublicKey, listen bool) (*Chat, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + chatID := PublicKeyToPartitionedTopic(theirPublicKey) + + if _, ok := s.chats[chatID]; ok { + return s.chats[chatID], nil } - discoveryResponse, err := s.AddAsymmetricFilter(myKey, discoveryChat.ChatID, true) + // We set up a filter so we can publish, but we discard envelopes if listen is false + filter, err := s.addAsymmetricFilter(myKey, chatID, listen) + if err != nil { + return nil, err + } + + chat := &Chat{ + ChatID: chatID, + FilterID: filter.FilterID, + Topic: filter.Topic, + Listen: listen, + } + + s.chats[chatID] = chat + + return chat, nil +} + +// Load creates filters for a given chat, and returns all the created filters +func (s *Service) Load(chat *Chat) ([]*Chat, error) { + keyID := s.whisper.SelectedKeyPairID() + if keyID == "" { + return nil, errors.New("no key selected") + } + myKey, err := s.whisper.GetPrivateKey(keyID) + + if err != nil { + return nil, err + } + return s.load(myKey, chat) +} + +// Get returns a negotiated filter given an identity +func (s *Service) GetNegotiated(identity *ecdsa.PublicKey) *Chat { + s.mutex.Lock() + defer s.mutex.Unlock() + + return s.chats[negotiatedID(identity)] +} + +// GetByID returns a filter by chatID +func (s *Service) GetByID(chatID string) *Chat { + s.mutex.Lock() + defer s.mutex.Unlock() + + return s.chats[chatID] +} + +// ProcessNegotiatedSecret adds a filter based on the agreed secret +func (s *Service) ProcessNegotiatedSecret(secret *sharedsecret.Secret) (*Chat, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + chatID := negotiatedID(secret.Identity) + // If we already have a filter do nothing + if _, ok := s.chats[chatID]; ok { + return s.chats[chatID], nil + } + + keyString := fmt.Sprintf("%x", secret.Key) + filter, err := s.addSymmetric(keyString) + if err != nil { + return nil, err + } + + identityStr := fmt.Sprintf("%x", crypto.FromECDSAPub(secret.Identity)) + + chat := &Chat{ + ChatID: chatID, + Topic: filter.Topic, + SymKeyID: filter.SymKeyID, + FilterID: filter.FilterID, + Identity: identityStr, + Listen: true, + } + + log.Info("PROCESSING SECRET", "chat-id", chatID, "topic", filter.Topic, "symKey", keyString) + + s.chats[chat.ChatID] = chat + return chat, nil +} + +// ToTopic converts a string to a whisper topic +func ToTopic(s string) []byte { + return crypto.Keccak256([]byte(s))[:whisper.TopicLength] +} + +// PublicKeyToPartitionedTopic returns the associated partitioned topic string +// with the given public key +func PublicKeyToPartitionedTopic(publicKey *ecdsa.PublicKey) string { + partition := big.NewInt(0) + partition.Mod(publicKey.X, nPartitions) + return fmt.Sprintf("contact-discovery-%d", partition.Int64()) +} + +// PublicKeyToPartitionedTopicBytes returns the bytes of the partitioned topic +// associated with the given public key +func PublicKeyToPartitionedTopicBytes(publicKey *ecdsa.PublicKey) []byte { + return ToTopic(PublicKeyToPartitionedTopic(publicKey)) +} + +// loadDiscovery adds the discovery filter +func (s *Service) loadDiscovery(myKey *ecdsa.PrivateKey) error { + s.mutex.Lock() + defer s.mutex.Unlock() + + if _, ok := s.chats[discoveryTopic]; ok { + return nil + } + + discoveryChat := &Chat{ + ChatID: discoveryTopic, + Listen: true, + } + + discoveryResponse, err := s.addAsymmetricFilter(myKey, discoveryChat.ChatID, true) if err != nil { return err } @@ -102,125 +286,93 @@ func (s *Service) LoadDiscovery(myKey *ecdsa.PrivateKey) error { return nil } -func (s *Service) Init(chats []*Chat) error { - log.Debug("Initializing filter service") - myKey, err := s.whisper.GetPrivateKey(s.keyID) +// loadPublic adds a filter for a public chat +func (s *Service) loadPublic(chat *Chat) error { + s.mutex.Lock() + defer s.mutex.Unlock() + + if _, ok := s.chats[chat.ChatID]; ok { + return nil + } + + filterAndTopic, err := s.addSymmetric(chat.ChatID) if err != nil { return err } - // Add our own topic - log.Debug("Loading one to one chats") - identityStr := fmt.Sprintf("%x", crypto.FromECDSAPub(&myKey.PublicKey)) - err = s.LoadOneToOne(myKey, identityStr, true) - if err != nil { - log.Error("Error loading one to one chats", "err", err) + chat.FilterID = filterAndTopic.FilterID + chat.SymKeyID = filterAndTopic.SymKeyID + chat.Topic = filterAndTopic.Topic + chat.Listen = true - return err + s.chats[chat.ChatID] = chat + return nil +} + +// loadOneToOne creates two filters for a given chat, one listening to the contact codes +// and another on the partitioned topic, if listen is specified. +func (s *Service) loadOneToOne(myKey *ecdsa.PrivateKey, identity string, listen bool) ([]*Chat, error) { + var chats []*Chat + contactCodeChat, err := s.loadContactCode(identity) + if err != nil { + return nil, err } - // Add discovery topic - log.Debug("Loading discovery topics") - err = s.LoadDiscovery(myKey) - if err != nil { - return err - } + chats = append(chats, contactCodeChat) - // Add the various one to one and public chats - log.Debug("Loading chats") - for _, chat := range chats { - err = s.Load(myKey, chat) + if listen { + publicKeyBytes, err := hex.DecodeString(identity) if err != nil { - return err + return nil, err } - } - // Add the negotiated topics - log.Debug("Loading negotiated topics") - secrets, err := s.topic.All() - if err != nil { - return err - } - - for _, secret := range secrets { - if err := s.ProcessNegotiatedSecret(secret); err != nil { - return err + publicKey, err := crypto.UnmarshalPubkey(publicKeyBytes) + if err != nil { + return nil, err } - } - return nil + partitionedChat, err := s.LoadPartitioned(myKey, publicKey, listen) + if err != nil { + return nil, err + } + + chats = append(chats, partitionedChat) + } + return chats, nil } -func (s *Service) Stop() error { - for _, chat := range s.chats { - if err := s.Remove(chat); err != nil { - return err - } - } - return nil -} - -func (s *Service) Remove(chat *Chat) error { +// loadContactCode creates a filter for the topic are advertised for a given identity +func (s *Service) loadContactCode(identity string) (*Chat, error) { s.mutex.Lock() defer s.mutex.Unlock() - if err := s.whisper.Unsubscribe(chat.ChatID); err != nil { - return err + chatID := "0x" + identity + "-contact-code" + if _, ok := s.chats[chatID]; ok { + return s.chats[chatID], nil } - if chat.SymKeyID != "" { - s.whisper.DeleteSymKey(chat.SymKeyID) - } - delete(s.chats, chat.ChatID) - return nil - -} - -// LoadOneToOne creates two filters for a given chat, one listening to the contact codes -// and another on the partitioned topic. We pass a listen parameter to indicated whether -// we are listening to messages on the partitioned topic -func (s *Service) LoadOneToOne(myKey *ecdsa.PrivateKey, identity string, listen bool) error { - s.mutex.Lock() - defer s.mutex.Unlock() - - contactCodeChatID := identity + "-contact-code" - contactCodeFilter, err := s.AddSymmetric(contactCodeChatID) + contactCodeFilter, err := s.addSymmetric(chatID) if err != nil { - return err + return nil, err } - - s.chats[contactCodeChatID] = &Chat{ - ChatID: contactCodeChatID, + chat := &Chat{ + ChatID: chatID, FilterID: contactCodeFilter.FilterID, Topic: contactCodeFilter.Topic, SymKeyID: contactCodeFilter.SymKeyID, Identity: identity, + Listen: true, } - partitionedTopicChatID, err := chatIDToPartitionedTopic(identity) - if err != nil { - return err - } - // We set up a filter so we can publish, but we discard envelopes if listen is false - partitionedTopicFilter, err := s.AddAsymmetricFilter(myKey, partitionedTopicChatID, listen) - if err != nil { - return err - } - s.chats[partitionedTopicChatID] = &Chat{ - ChatID: partitionedTopicChatID, - FilterID: partitionedTopicFilter.FilterID, - Topic: partitionedTopicFilter.Topic, - Identity: identity, - Listen: listen, - } - - return nil + s.chats[chatID] = chat + return chat, nil } -func (s *Service) AddSymmetric(chatID string) (*Filter, error) { +// addSymmetric adds a symmetric key filter +func (s *Service) addSymmetric(chatID string) (*Filter, error) { var symKey []byte - topic := toTopic(chatID) + topic := ToTopic(chatID) topics := [][]byte{topic} symKeyID, err := s.whisper.AddSymKeyFromPassword(chatID) @@ -235,7 +387,7 @@ func (s *Service) AddSymmetric(chatID string) (*Filter, error) { f := &whisper.Filter{ KeySym: symKey, - PoW: 0.002, + PoW: minPow, AllowP2P: true, Topics: topics, Messages: s.whisper.NewMessageStore(), @@ -249,28 +401,29 @@ func (s *Service) AddSymmetric(chatID string) (*Filter, error) { return &Filter{ FilterID: id, SymKeyID: symKeyID, - Topic: topic, + Topic: whisper.BytesToTopic(topic), }, nil } -func (s *Service) AddAsymmetricFilter(keyAsym *ecdsa.PrivateKey, chatID string, listen bool) (*Filter, error) { +// addAsymmetricFilter adds a filter with our privatekey, and set minPow according to the listen parameter +func (s *Service) addAsymmetricFilter(keyAsym *ecdsa.PrivateKey, chatID string, listen bool) (*Filter, error) { var err error var pow float64 if listen { - pow = 0.002 + pow = minPow } else { // Set high pow so we discard messages pow = 1 } - topic := toTopic(chatID) + topic := ToTopic(chatID) topics := [][]byte{topic} f := &whisper.Filter{ KeyAsym: keyAsym, PoW: pow, - AllowP2P: listen, + AllowP2P: true, Topics: topics, Messages: s.whisper.NewMessageStore(), } @@ -280,73 +433,19 @@ func (s *Service) AddAsymmetricFilter(keyAsym *ecdsa.PrivateKey, chatID string, return nil, err } - return &Filter{FilterID: id, Topic: topic}, nil -} - -func (s *Service) LoadPublic(chat *Chat) error { - s.mutex.Lock() - defer s.mutex.Unlock() - - filterAndTopic, err := s.AddSymmetric(chat.ChatID) - if err != nil { - return err - } - - // Add mutex - chat.FilterID = filterAndTopic.FilterID - chat.SymKeyID = filterAndTopic.SymKeyID - chat.Topic = filterAndTopic.Topic - s.chats[chat.ChatID] = chat - return nil -} - -func (s *Service) Load(myKey *ecdsa.PrivateKey, chat *Chat) error { - var err error - log.Debug("Loading chat", "chatID", chat.ChatID) - - // Check we haven't already loaded the chat - if _, ok := s.chats[chat.ChatID]; !ok { - if chat.OneToOne { - err = s.LoadOneToOne(myKey, chat.Identity, false) - - } else { - err = s.LoadPublic(chat) - } - if err != nil { - return err - } - - } - return nil + return &Filter{FilterID: id, Topic: whisper.BytesToTopic(topic)}, nil } func negotiatedID(identity *ecdsa.PublicKey) string { - return fmt.Sprintf("%x-negotiated", crypto.FromECDSAPub(identity)) + return fmt.Sprintf("0x%x-negotiated", crypto.FromECDSAPub(identity)) } -func (s *Service) Get(identity *ecdsa.PublicKey) *Chat { - return s.chats[negotiatedID(identity)] -} +func (s *Service) load(myKey *ecdsa.PrivateKey, chat *Chat) ([]*Chat, error) { + log.Debug("Loading chat", "chatID", chat.ChatID) -func (s *Service) ProcessNegotiatedSecret(secret *topic.Secret) error { - s.mutex.Lock() - defer s.mutex.Unlock() + if chat.OneToOne { + return s.loadOneToOne(myKey, chat.Identity, false) - keyString := fmt.Sprintf("%x", secret.Key) - filter, err := s.AddSymmetric(keyString) - if err != nil { - return err } - - identityStr := fmt.Sprintf("0x%x", crypto.FromECDSAPub(secret.Identity)) - - chat := &Chat{ - ChatID: negotiatedID(secret.Identity), - Topic: filter.Topic, - SymKeyID: filter.SymKeyID, - Identity: identityStr, - } - - s.chats[chat.ChatID] = chat - return nil + return []*Chat{chat}, s.loadPublic(chat) } diff --git a/services/shhext/filter/service_test.go b/services/shhext/filter/service_test.go index 55968933c..627c49901 100644 --- a/services/shhext/filter/service_test.go +++ b/services/shhext/filter/service_test.go @@ -9,7 +9,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" appDB "github.com/status-im/status-go/services/shhext/chat/db" - "github.com/status-im/status-go/services/shhext/chat/topic" + "github.com/status-im/status-go/services/shhext/chat/sharedsecret" whisper "github.com/status-im/whisper/whisperv6" "github.com/stretchr/testify/suite" ) @@ -51,7 +51,7 @@ func (s *ServiceTestSuite) SetupTest() { keyStrs := []string{"c6cbd7d76bc5baca530c875663711b947efa6a86a900a9e8645ce32e5821484e", "d51dd64ad19ea84968a308dca246012c00d2b2101d41bce740acd1c650acc509"} keyTopics := []int{4490, 3991} - dbFile, err := ioutil.TempFile(os.TempDir(), "topic") + dbFile, err := ioutil.TempFile(os.TempDir(), "filter") s.Require().NoError(err) s.path = dbFile.Name() @@ -67,12 +67,12 @@ func (s *ServiceTestSuite) SetupTest() { s.Require().NoError(err) // Build services - topicService := topic.NewService(topic.NewSQLLitePersistence(db)) + sharedSecretService := sharedsecret.NewService(sharedsecret.NewSQLLitePersistence(db)) whisper := whisper.New(nil) - keyID, err := whisper.AddKeyPair(s.keys[0].privateKey) + _, err = whisper.AddKeyPair(s.keys[0].privateKey) s.Require().NoError(err) - s.service = New(keyID, whisper, topicService) + s.service = New(whisper, sharedSecretService) } func (s *ServiceTestSuite) TearDownTest() { @@ -82,21 +82,24 @@ func (s *ServiceTestSuite) TearDownTest() { func (s *ServiceTestSuite) TestDiscoveryAndPartitionedTopic() { chats := []*Chat{} partitionedTopic := fmt.Sprintf("contact-discovery-%d", s.keys[0].partitionedTopic) - contactCodeTopic := s.keys[0].PublicKeyString() + "-contact-code" + contactCodeTopic := "0x" + s.keys[0].PublicKeyString() + "-contact-code" - err := s.service.Init(chats) + _, err := s.service.Init(chats) s.Require().NoError(err) s.Require().Equal(3, len(s.service.chats), "It creates two filters") discoveryFilter := s.service.chats[discoveryTopic] s.Require().NotNil(discoveryFilter, "It adds the discovery filter") + s.Require().True(discoveryFilter.Listen) contactCodeFilter := s.service.chats[contactCodeTopic] s.Require().NotNil(contactCodeFilter, "It adds the contact code filter") + s.Require().True(contactCodeFilter.Listen) partitionedFilter := s.service.chats[partitionedTopic] s.Require().NotNil(partitionedFilter, "It adds the partitioned filter") + s.Require().True(partitionedFilter.Listen) } func (s *ServiceTestSuite) TestPublicAndOneToOneChats() { @@ -110,46 +113,95 @@ func (s *ServiceTestSuite) TestPublicAndOneToOneChats() { OneToOne: true, }, } - partitionedTopic := fmt.Sprintf("contact-discovery-%d", s.keys[1].partitionedTopic) - contactCodeTopic := s.keys[1].PublicKeyString() + "-contact-code" + contactCodeTopic := "0x" + s.keys[1].PublicKeyString() + "-contact-code" - err := s.service.Init(chats) + response, err := s.service.Init(chats) s.Require().NoError(err) - s.Require().Equal(6, len(s.service.chats), "It creates two additional filters for the one to one and one for the public chat") + actualChats := make(map[string]*Chat) - statusFilter := s.service.chats["status"] + for _, chat := range response { + actualChats[chat.ChatID] = chat + } + + s.Require().Equal(5, len(actualChats), "It creates two additional filters for the one to one and one for the public chat") + + statusFilter := actualChats["status"] s.Require().NotNil(statusFilter, "It creates a filter for the public chat") s.Require().NotNil(statusFilter.SymKeyID, "It returns a sym key id") + s.Require().True(statusFilter.Listen) - contactCodeFilter := s.service.chats[contactCodeTopic] + contactCodeFilter := actualChats[contactCodeTopic] s.Require().NotNil(contactCodeFilter, "It adds the contact code filter") - - partitionedFilter := s.service.chats[partitionedTopic] - s.Require().NotNil(partitionedFilter, "It adds the partitioned filter") + s.Require().True(contactCodeFilter.Listen) } func (s *ServiceTestSuite) TestNegotiatedTopic() { chats := []*Chat{} - negotiatedTopic1 := s.keys[0].PublicKeyString() + "-negotiated" - negotiatedTopic2 := s.keys[1].PublicKeyString() + "-negotiated" + negotiatedTopic1 := "0x" + s.keys[0].PublicKeyString() + "-negotiated" + negotiatedTopic2 := "0x" + s.keys[1].PublicKeyString() + "-negotiated" // We send a message to ourselves - _, _, err := s.service.topic.Send(s.keys[0].privateKey, "0-1", &s.keys[0].privateKey.PublicKey, []string{"0-2"}) + _, _, err := s.service.secret.Send(s.keys[0].privateKey, "0-1", &s.keys[0].privateKey.PublicKey, []string{"0-2"}) s.Require().NoError(err) // We send a message to someone else - _, _, err = s.service.topic.Send(s.keys[0].privateKey, "0-1", &s.keys[1].privateKey.PublicKey, []string{"0-2"}) + _, _, err = s.service.secret.Send(s.keys[0].privateKey, "0-1", &s.keys[1].privateKey.PublicKey, []string{"0-2"}) s.Require().NoError(err) - err = s.service.Init(chats) + response, err := s.service.Init(chats) s.Require().NoError(err) - s.Require().Equal(5, len(s.service.chats), "It creates two additional filters for the negotiated topics") + actualChats := make(map[string]*Chat) - negotiatedFilter1 := s.service.chats[negotiatedTopic1] + for _, chat := range response { + actualChats[chat.ChatID] = chat + } + + s.Require().Equal(5, len(actualChats), "It creates two additional filters for the negotiated topics") + + negotiatedFilter1 := actualChats[negotiatedTopic1] s.Require().NotNil(negotiatedFilter1, "It adds the negotiated filter") - negotiatedFilter2 := s.service.chats[negotiatedTopic2] + negotiatedFilter2 := actualChats[negotiatedTopic2] s.Require().NotNil(negotiatedFilter2, "It adds the negotiated filter") } + +func (s *ServiceTestSuite) TestLoadChat() { + chats := []*Chat{} + + _, err := s.service.Init(chats) + s.Require().NoError(err) + + // We add a public chat + response1, err := s.service.Load(&Chat{ChatID: "status"}) + + s.Require().NoError(err) + s.Require().Equal(1, len(response1)) + s.Require().Equal("status", response1[0].ChatID) + s.Require().True(response1[0].Listen) +} + +func (s *ServiceTestSuite) TestNoInstallationIDs() { + chats := []*Chat{} + + negotiatedTopic1 := "0x" + s.keys[1].PublicKeyString() + "-negotiated" + + // We send a message to someone else, but without any installation ID + _, _, err := s.service.secret.Send(s.keys[0].privateKey, "0-1", &s.keys[1].privateKey.PublicKey, []string{}) + s.Require().NoError(err) + + response, err := s.service.Init(chats) + s.Require().NoError(err) + + actualChats := make(map[string]*Chat) + + for _, chat := range response { + actualChats[chat.ChatID] = chat + } + + s.Require().Equal(4, len(actualChats), "It creates two additional filters for the negotiated topics") + + negotiatedFilter1 := actualChats[negotiatedTopic1] + s.Require().NotNil(negotiatedFilter1, "It adds the negotiated filter") +} diff --git a/services/shhext/publisher/service.go b/services/shhext/publisher/service.go new file mode 100644 index 000000000..d33638e3a --- /dev/null +++ b/services/shhext/publisher/service.go @@ -0,0 +1,446 @@ +package publisher + +import ( + "context" + "crypto/ecdsa" + "errors" + "fmt" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/golang/protobuf/proto" + "github.com/status-im/status-go/services/shhext/chat" + appDB "github.com/status-im/status-go/services/shhext/chat/db" + "github.com/status-im/status-go/services/shhext/chat/multidevice" + "github.com/status-im/status-go/services/shhext/chat/protobuf" + "github.com/status-im/status-go/services/shhext/chat/sharedsecret" + "github.com/status-im/status-go/services/shhext/dedup" + "github.com/status-im/status-go/services/shhext/filter" + "github.com/status-im/status-go/services/shhext/whisperutils" + "github.com/status-im/status-go/signal" + whisper "github.com/status-im/whisper/whisperv6" + "golang.org/x/crypto/sha3" + "os" + "path/filepath" + "time" +) + +const ( + tickerInterval = 120 + maxInstallations = 3 +) + +var ( + errProtocolNotInitialized = errors.New("procotol is not initialized") + // ErrPFSNotEnabled is returned when an endpoint PFS only is called but + // PFS is disabled + ErrPFSNotEnabled = errors.New("pfs not enabled") +) + +//type Persistence interface { +//} + +type Service struct { + whisper *whisper.Whisper + whisperAPI *whisper.PublicWhisperAPI + protocol *chat.ProtocolService + // persistence Persistence + log log.Logger + filter *filter.Service + config *Config + quit chan struct{} + ticker *time.Ticker +} + +type Config struct { + PfsEnabled bool + DataDir string + InstallationID string +} + +func New(config *Config, w *whisper.Whisper) *Service { + return &Service{ + config: config, + whisper: w, + whisperAPI: whisper.NewPublicWhisperAPI(w), + log: log.New("package", "status-go/services/publisher.Service"), + } +} + +// InitProtocolWithPassword creates an instance of ProtocolService given an address and password used to generate an encryption key. +func (s *Service) InitProtocolWithPassword(address string, password string) error { + digest := sha3.Sum256([]byte(password)) + encKey := fmt.Sprintf("%x", digest) + return s.initProtocol(address, encKey, password) +} + +// InitProtocolWithEncyptionKey creates an instance of ProtocolService given an address and encryption key. +func (s *Service) InitProtocolWithEncyptionKey(address string, encKey string) error { + return s.initProtocol(address, encKey, "") +} + +func (s *Service) initProtocol(address, encKey, password string) error { + if !s.config.PfsEnabled { + return nil + } + + if err := os.MkdirAll(filepath.Clean(s.config.DataDir), os.ModePerm); err != nil { + return err + } + v0Path := filepath.Join(s.config.DataDir, fmt.Sprintf("%x.db", address)) + v1Path := filepath.Join(s.config.DataDir, fmt.Sprintf("%s.db", s.config.InstallationID)) + v2Path := filepath.Join(s.config.DataDir, fmt.Sprintf("%s.v2.db", s.config.InstallationID)) + v3Path := filepath.Join(s.config.DataDir, fmt.Sprintf("%s.v3.db", s.config.InstallationID)) + v4Path := filepath.Join(s.config.DataDir, fmt.Sprintf("%s.v4.db", s.config.InstallationID)) + + if password != "" { + if err := appDB.MigrateDBFile(v0Path, v1Path, "ON", password); err != nil { + return err + } + + if err := appDB.MigrateDBFile(v1Path, v2Path, password, encKey); err != nil { + // Remove db file as created with a blank password and never used, + // and there's no need to rekey in this case + os.Remove(v1Path) + os.Remove(v2Path) + } + } + + if err := appDB.MigrateDBKeyKdfIterations(v2Path, v3Path, encKey); err != nil { + os.Remove(v2Path) + os.Remove(v3Path) + } + + // Fix IOS not encrypting database + if err := appDB.EncryptDatabase(v3Path, v4Path, encKey); err != nil { + os.Remove(v3Path) + os.Remove(v4Path) + } + + // Desktop was passing a network dependent directory, which meant that + // if running on testnet it would not access the right db. This copies + // the db from mainnet to the root location. + networkDependentPath := filepath.Join(s.config.DataDir, "ethereum", "mainnet_rpc", fmt.Sprintf("%s.v4.db", s.config.InstallationID)) + if _, err := os.Stat(networkDependentPath); err == nil { + if err := os.Rename(networkDependentPath, v4Path); err != nil { + return err + } + } else if !os.IsNotExist(err) { + return err + } + + persistence, err := chat.NewSQLLitePersistence(v4Path, encKey) + if err != nil { + return err + } + + addedBundlesHandler := func(addedBundles []multidevice.IdentityAndIDPair) { + handler := SignalHandler{} + for _, bundle := range addedBundles { + handler.BundleAdded(bundle[0], bundle[1]) + } + } + + // Initialize sharedsecret + sharedSecretService := sharedsecret.NewService(persistence.GetSharedSecretStorage()) + // Initialize filter + filterService := filter.New(s.whisper, sharedSecretService) + s.filter = filterService + + // Initialize multidevice + multideviceConfig := &multidevice.Config{ + InstallationID: s.config.InstallationID, + ProtocolVersion: chat.ProtocolVersion, + MaxInstallations: maxInstallations, + } + multideviceService := multidevice.New(multideviceConfig, persistence.GetMultideviceStorage()) + + s.protocol = chat.NewProtocolService( + chat.NewEncryptionService( + persistence, + chat.DefaultEncryptionServiceConfig(s.config.InstallationID)), + sharedSecretService, + multideviceService, + addedBundlesHandler, + s.onNewSharedSecretHandler) + + return nil +} + +func (s *Service) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, bundle *protobuf.Bundle) ([]multidevice.IdentityAndIDPair, error) { + if s.protocol == nil { + return nil, errProtocolNotInitialized + } + + return s.protocol.ProcessPublicBundle(myIdentityKey, bundle) +} + +func (s *Service) GetBundle(myIdentityKey *ecdsa.PrivateKey) (*protobuf.Bundle, error) { + if s.protocol == nil { + return nil, errProtocolNotInitialized + } + + return s.protocol.GetBundle(myIdentityKey) +} + +// EnableInstallation enables an installation for multi-device sync. +func (s *Service) EnableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error { + if s.protocol == nil { + return errProtocolNotInitialized + } + + return s.protocol.EnableInstallation(myIdentityKey, installationID) +} + +func (s *Service) GetPublicBundle(identityKey *ecdsa.PublicKey) (*protobuf.Bundle, error) { + if s.protocol == nil { + return nil, errProtocolNotInitialized + } + + return s.protocol.GetPublicBundle(identityKey) +} + +// DisableInstallation disables an installation for multi-device sync. +func (s *Service) DisableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error { + if s.protocol == nil { + return errProtocolNotInitialized + } + + return s.protocol.DisableInstallation(myIdentityKey, installationID) +} + +func (s *Service) Start() error { + s.startTicker() + return nil +} +func (s *Service) Stop() error { + if s.filter != nil { + if err := s.filter.Stop(); err != nil { + log.Error("Failed to stop filter service with error", "err", err) + } + } + + return nil +} + +func (s *Service) GetNegotiatedChat(identity *ecdsa.PublicKey) *filter.Chat { + return s.filter.GetNegotiated(identity) +} + +func (s *Service) LoadFilters(chats []*filter.Chat) ([]*filter.Chat, error) { + return s.filter.Init(chats) +} + +func (s *Service) LoadFilter(chat *filter.Chat) ([]*filter.Chat, error) { + return s.filter.Load(chat) +} + +func (s *Service) RemoveFilter(chat *filter.Chat) error { + return s.filter.Remove(chat) +} + +func (s *Service) onNewSharedSecretHandler(sharedSecrets []*sharedsecret.Secret) { + var filters []*signal.Filter + for _, sharedSecret := range sharedSecrets { + chat, err := s.filter.ProcessNegotiatedSecret(sharedSecret) + if err != nil { + log.Error("Failed to process negotiated secret", "err", err) + return + } + + filter := &signal.Filter{ + ChatID: chat.ChatID, + SymKeyID: chat.SymKeyID, + Listen: chat.Listen, + FilterID: chat.FilterID, + Identity: chat.Identity, + Topic: chat.Topic, + } + + filters = append(filters, filter) + + } + if len(filters) != 0 { + handler := SignalHandler{} + handler.WhisperFilterAdded(filters) + } + +} + +func (s *Service) ProcessMessage(dedupMessage dedup.DeduplicateMessage) error { + if !s.config.PfsEnabled { + return nil + } + msg := dedupMessage.Message + + privateKeyID := s.whisper.SelectedKeyPairID() + if privateKeyID == "" { + return errors.New("no key selected") + } + + privateKey, err := s.whisper.GetPrivateKey(privateKeyID) + if err != nil { + return err + } + + publicKey, err := crypto.UnmarshalPubkey(msg.Sig) + if err != nil { + return err + } + + // Unmarshal message + protocolMessage := &protobuf.ProtocolMessage{} + + if err := proto.Unmarshal(msg.Payload, protocolMessage); err != nil { + s.log.Debug("Not a protocol message", "err", err) + return nil + } + + response, err := s.protocol.HandleMessage(privateKey, publicKey, protocolMessage, dedupMessage.DedupID) + + switch err { + case nil: + // Set the decrypted payload + msg.Payload = response + case chat.ErrDeviceNotFound: + // Notify that someone tried to contact us using an invalid bundle + if privateKey.PublicKey != *publicKey { + s.log.Warn("Device not found, sending signal", "err", err) + keyString := fmt.Sprintf("0x%x", crypto.FromECDSAPub(publicKey)) + handler := SignalHandler{} + handler.DecryptMessageFailed(keyString) + } + default: + // Log and pass to the client, even if failed to decrypt + s.log.Error("Failed handling message with error", "err", err) + } + + return nil +} + +// SendDirectMessage sends a 1:1 chat message to the underlying transport +func (s *Service) SendDirectMessage(ctx context.Context, msg chat.SendDirectMessageRPC) (hexutil.Bytes, error) { + if !s.config.PfsEnabled { + return nil, ErrPFSNotEnabled + } + + privateKey, err := s.whisper.GetPrivateKey(msg.Sig) + if err != nil { + return nil, err + } + + publicKey, err := crypto.UnmarshalPubkey(msg.PubKey) + if err != nil { + return nil, err + } + + var msgSpec *chat.ProtocolMessageSpec + + if msg.DH { + s.log.Debug("Building dh message") + msgSpec, err = s.protocol.BuildDHMessage(privateKey, publicKey, msg.Payload) + } else { + s.log.Debug("Building direct message") + msgSpec, err = s.protocol.BuildDirectMessage(privateKey, publicKey, msg.Payload) + } + if err != nil { + return nil, err + } + + whisperMessage, err := s.directMessageToWhisper(privateKey, publicKey, msg.PubKey, msg.Sig, msgSpec) + if err != nil { + s.log.Error("sshext-service", "error building whisper message", err) + return nil, err + } + + return s.whisperAPI.Post(ctx, *whisperMessage) +} + +func (s *Service) directMessageToWhisper(myPrivateKey *ecdsa.PrivateKey, theirPublicKey *ecdsa.PublicKey, destination hexutil.Bytes, signature string, spec *chat.ProtocolMessageSpec) (*whisper.NewMessage, error) { + // marshal for sending to wire + marshaledMessage, err := proto.Marshal(spec.Message) + if err != nil { + s.log.Error("encryption-service", "error marshaling message", err) + return nil, err + } + + whisperMessage := whisperutils.DefaultWhisperMessage() + whisperMessage.Payload = marshaledMessage + whisperMessage.Sig = signature + + if spec.SharedSecret != nil { + chat := s.GetNegotiatedChat(theirPublicKey) + if chat != nil { + s.log.Debug("Sending on negotiated topic") + whisperMessage.SymKeyID = chat.SymKeyID + whisperMessage.Topic = chat.Topic + whisperMessage.PublicKey = nil + return &whisperMessage, nil + } + } else if spec.PartitionedTopic() { + s.log.Debug("Sending on partitioned topic") + // Create filter on demand + if _, err := s.filter.LoadPartitioned(myPrivateKey, theirPublicKey, false); err != nil { + return nil, err + } + t := filter.PublicKeyToPartitionedTopicBytes(theirPublicKey) + whisperMessage.Topic = whisper.BytesToTopic(t) + whisperMessage.PublicKey = destination + return &whisperMessage, nil + } + + s.log.Debug("Sending on old discovery topic") + whisperMessage.Topic = whisperutils.DiscoveryTopicBytes + whisperMessage.PublicKey = destination + + return &whisperMessage, nil +} + +// SendPublicMessage sends a public chat message to the underlying transport +func (s *Service) SendPublicMessage(ctx context.Context, msg chat.SendPublicMessageRPC) (hexutil.Bytes, error) { + if !s.config.PfsEnabled { + return nil, ErrPFSNotEnabled + } + + filter := s.filter.GetByID(msg.Chat) + if filter == nil { + return nil, errors.New("not subscribed to chat") + } + + // Enrich with transport layer info + whisperMessage := whisperutils.DefaultWhisperMessage() + whisperMessage.Payload = msg.Payload + whisperMessage.Sig = msg.Sig + whisperMessage.Topic = whisperutils.ToTopic(msg.Chat) + whisperMessage.SymKeyID = filter.SymKeyID + + // And dispatch + return s.whisperAPI.Post(ctx, whisperMessage) +} + +func (s *Service) ConfirmMessagesProcessed(ids [][]byte) error { + return s.protocol.ConfirmMessagesProcessed(ids) +} + +func (s *Service) startTicker() { + s.ticker = time.NewTicker(tickerInterval * time.Second) + s.quit = make(chan struct{}) + go func() { + for { + select { + case <-s.ticker.C: + err := s.perform() + if err != nil { + s.log.Error("could not execute tick", "err", err) + } + case <-s.quit: + s.ticker.Stop() + return + } + } + }() +} + +func (s *Service) perform() error { + return nil +} diff --git a/services/shhext/publisher/signal.go b/services/shhext/publisher/signal.go new file mode 100644 index 000000000..f18cafc4b --- /dev/null +++ b/services/shhext/publisher/signal.go @@ -0,0 +1,20 @@ +package publisher + +import ( + "github.com/status-im/status-go/signal" +) + +// SignalHandler sends signals on protocol events +type SignalHandler struct{} + +func (h SignalHandler) DecryptMessageFailed(pubKey string) { + signal.SendDecryptMessageFailed(pubKey) +} + +func (h SignalHandler) BundleAdded(identity string, installationID string) { + signal.SendBundleAdded(identity, installationID) +} + +func (h SignalHandler) WhisperFilterAdded(filters []*signal.Filter) { + signal.SendWhisperFilterAdded(filters) +} diff --git a/services/shhext/service.go b/services/shhext/service.go index 37ab2cbe0..f431fd9a9 100644 --- a/services/shhext/service.go +++ b/services/shhext/service.go @@ -2,10 +2,6 @@ package shhext import ( "crypto/ecdsa" - "errors" - "fmt" - "os" - "path/filepath" "time" "github.com/ethereum/go-ethereum/common" @@ -16,16 +12,12 @@ import ( "github.com/ethereum/go-ethereum/rpc" "github.com/status-im/status-go/db" "github.com/status-im/status-go/params" - "github.com/status-im/status-go/services/shhext/chat" - appDB "github.com/status-im/status-go/services/shhext/chat/db" - "github.com/status-im/status-go/services/shhext/chat/topic" "github.com/status-im/status-go/services/shhext/dedup" "github.com/status-im/status-go/services/shhext/filter" "github.com/status-im/status-go/services/shhext/mailservers" - "github.com/status-im/status-go/signal" + "github.com/status-im/status-go/services/shhext/publisher" whisper "github.com/status-im/whisper/whisperv6" "github.com/syndtr/goleveldb/leveldb" - "golang.org/x/crypto/sha3" ) const ( @@ -35,8 +27,6 @@ const ( defaultTimeoutWaitAdded = 5 * time.Second ) -var errProtocolNotInitialized = errors.New("protocol is not initialized") - // EnvelopeEventsHandler used for two different event types. type EnvelopeEventsHandler interface { EnvelopeSent(common.Hash) @@ -47,6 +37,7 @@ type EnvelopeEventsHandler interface { // Service is a service that provides some additional Whisper API. type Service struct { + *publisher.Service storage db.TransactionalStorage w *whisper.Whisper config params.ShhextConfig @@ -57,10 +48,6 @@ type Service struct { server *p2p.Server nodeID *ecdsa.PrivateKey deduplicator *dedup.Deduplicator - protocol *chat.ProtocolService - dataDir string - installationID string - pfsEnabled bool peerStore *mailservers.PeerStore cache *mailservers.Cache connManager *mailservers.ConnectionManager @@ -71,7 +58,7 @@ type Service struct { // Make sure that Service implements node.Service interface. var _ node.Service = (*Service)(nil) -// New returns a new Service. dataDir is a folder path to a network-independent location +// New returns a new Service. func New(w *whisper.Whisper, handler EnvelopeEventsHandler, ldb *leveldb.DB, config params.ShhextConfig) *Service { cache := mailservers.NewCache(ldb) ps := mailservers.NewPeerStore(cache) @@ -88,7 +75,14 @@ func New(w *whisper.Whisper, handler EnvelopeEventsHandler, ldb *leveldb.DB, con requestsRegistry: requestsRegistry, } envelopesMonitor := NewEnvelopesMonitor(w, handler, config.MailServerConfirmations, ps, config.MaxMessageDeliveryAttempts) + publisherConfig := &publisher.Config{ + PfsEnabled: config.PFSEnabled, + DataDir: config.BackupDisabledDataDir, + InstallationID: config.InstallationID, + } + publisherService := publisher.New(publisherConfig, w) return &Service{ + Service: publisherService, storage: db.NewLevelDBStorage(ldb), w: w, config: config, @@ -97,9 +91,6 @@ func New(w *whisper.Whisper, handler EnvelopeEventsHandler, ldb *leveldb.DB, con requestsRegistry: requestsRegistry, historyUpdates: historyUpdates, deduplicator: dedup.NewDeduplicator(w, ldb), - dataDir: config.BackupDisabledDataDir, - installationID: config.InstallationID, - pfsEnabled: config.PFSEnabled, peerStore: ps, cache: cache, } @@ -121,138 +112,6 @@ func (s *Service) Protocols() []p2p.Protocol { return []p2p.Protocol{} } -// InitProtocolWithPassword creates an instance of ProtocolService given an address and password used to generate an encryption key. -func (s *Service) InitProtocolWithPassword(address string, password string) error { - digest := sha3.Sum256([]byte(password)) - encKey := fmt.Sprintf("%x", digest) - return s.initProtocol(address, encKey, password) -} - -// InitProtocolWithEncyptionKey creates an instance of ProtocolService given an address and encryption key. -func (s *Service) InitProtocolWithEncyptionKey(address string, encKey string) error { - return s.initProtocol(address, encKey, "") -} - -func (s *Service) initProtocol(address, encKey, password string) error { - if !s.pfsEnabled { - return nil - } - - if err := os.MkdirAll(filepath.Clean(s.dataDir), os.ModePerm); err != nil { - return err - } - v0Path := filepath.Join(s.dataDir, fmt.Sprintf("%x.db", address)) - v1Path := filepath.Join(s.dataDir, fmt.Sprintf("%s.db", s.installationID)) - v2Path := filepath.Join(s.dataDir, fmt.Sprintf("%s.v2.db", s.installationID)) - v3Path := filepath.Join(s.dataDir, fmt.Sprintf("%s.v3.db", s.installationID)) - v4Path := filepath.Join(s.dataDir, fmt.Sprintf("%s.v4.db", s.installationID)) - - if password != "" { - if err := appDB.MigrateDBFile(v0Path, v1Path, "ON", password); err != nil { - return err - } - - if err := appDB.MigrateDBFile(v1Path, v2Path, password, encKey); err != nil { - // Remove db file as created with a blank password and never used, - // and there's no need to rekey in this case - os.Remove(v1Path) - os.Remove(v2Path) - } - } - - if err := appDB.MigrateDBKeyKdfIterations(v2Path, v3Path, encKey); err != nil { - os.Remove(v2Path) - os.Remove(v3Path) - } - - // Fix IOS not encrypting database - if err := appDB.EncryptDatabase(v3Path, v4Path, encKey); err != nil { - os.Remove(v3Path) - os.Remove(v4Path) - } - - // Desktop was passing a network dependent directory, which meant that - // if running on testnet it would not access the right db. This copies - // the db from mainnet to the root location. - networkDependentPath := filepath.Join(s.dataDir, "ethereum", "mainnet_rpc", fmt.Sprintf("%s.v4.db", s.installationID)) - if _, err := os.Stat(networkDependentPath); err == nil { - if err := os.Rename(networkDependentPath, v4Path); err != nil { - return err - } - } else if !os.IsNotExist(err) { - return err - } - - persistence, err := chat.NewSQLLitePersistence(v4Path, encKey) - if err != nil { - return err - } - - addedBundlesHandler := func(addedBundles []chat.IdentityAndIDPair) { - handler := EnvelopeSignalHandler{} - for _, bundle := range addedBundles { - handler.BundleAdded(bundle[0], bundle[1]) - } - } - - // Initialize topics - topicService := topic.NewService(persistence.GetTopicStorage()) - filterService := filter.New(s.config.AsymKeyID, s.w, topicService) - s.filter = filterService - - s.protocol = chat.NewProtocolService( - chat.NewEncryptionService( - persistence, - chat.DefaultEncryptionServiceConfig(s.installationID)), - topicService, - addedBundlesHandler, - s.onNewTopicHandler) - - return nil -} - -func (s *Service) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, bundle *chat.Bundle) ([]chat.IdentityAndIDPair, error) { - if s.protocol == nil { - return nil, errProtocolNotInitialized - } - - return s.protocol.ProcessPublicBundle(myIdentityKey, bundle) -} - -func (s *Service) GetBundle(myIdentityKey *ecdsa.PrivateKey) (*chat.Bundle, error) { - if s.protocol == nil { - return nil, errProtocolNotInitialized - } - - return s.protocol.GetBundle(myIdentityKey) -} - -// EnableInstallation enables an installation for multi-device sync. -func (s *Service) EnableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error { - if s.protocol == nil { - return errProtocolNotInitialized - } - - return s.protocol.EnableInstallation(myIdentityKey, installationID) -} - -func (s *Service) GetPublicBundle(identityKey *ecdsa.PublicKey) (*chat.Bundle, error) { - if s.protocol == nil { - return nil, errProtocolNotInitialized - } - - return s.protocol.GetPublicBundle(identityKey) -} - -// DisableInstallation disables an installation for multi-device sync. -func (s *Service) DisableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error { - if s.protocol == nil { - return errProtocolNotInitialized - } - - return s.protocol.DisableInstallation(myIdentityKey, installationID) -} - // APIs returns a list of new APIs. func (s *Service) APIs() []rpc.API { apis := []rpc.API{ @@ -293,7 +152,7 @@ func (s *Service) Start(server *p2p.Server) error { s.mailMonitor.Start() s.nodeID = server.PrivateKey s.server = server - return nil + return s.Service.Start() } // Stop is run when a service is stopped. @@ -314,38 +173,5 @@ func (s *Service) Stop() error { } } - return nil -} - -func (s *Service) GetNegotiatedChat(identity *ecdsa.PublicKey) *filter.Chat { - return s.filter.Get(identity) -} - -func (s *Service) LoadFilters(chats []*filter.Chat) error { - return s.filter.Init(chats) -} - -func (s *Service) RemoveFilter(chat *filter.Chat) { - // remove filter -} - -func (s *Service) onNewTopicHandler(sharedSecrets []*topic.Secret) { - var filters []*signal.Filter - log.Info("NEW TOPIC HANDLER", "secrets", sharedSecrets) - for _, sharedSecret := range sharedSecrets { - err := s.filter.ProcessNegotiatedSecret(sharedSecret) - if err != nil { - log.Error("Failed to process negotiated secret", "err", err) - return - } - - } - // TODO: send back chat filter - log.Info("FILTER IDS", "filter", filters) - if len(filters) != 0 { - log.Info("SENDING FILTERS") - handler := EnvelopeSignalHandler{} - handler.WhisperFilterAdded(filters) - } - + return s.Service.Stop() } diff --git a/services/shhext/whisperutils/whisper.go b/services/shhext/whisperutils/whisper.go new file mode 100644 index 000000000..d920b81dc --- /dev/null +++ b/services/shhext/whisperutils/whisper.go @@ -0,0 +1,23 @@ +package whisperutils + +import ( + "github.com/ethereum/go-ethereum/crypto" + whisper "github.com/status-im/whisper/whisperv6" +) + +var discoveryTopic = "contact-discovery" +var DiscoveryTopicBytes = ToTopic(discoveryTopic) + +func ToTopic(s string) whisper.TopicType { + return whisper.BytesToTopic(crypto.Keccak256([]byte(s))) +} + +func DefaultWhisperMessage() whisper.NewMessage { + msg := whisper.NewMessage{} + + msg.TTL = 10 + msg.PowTarget = 0.002 + msg.PowTime = 1 + + return msg +} diff --git a/signal/events_shhext.go b/signal/events_shhext.go index 62c21acfe..8e4f93897 100644 --- a/signal/events_shhext.go +++ b/signal/events_shhext.go @@ -63,11 +63,18 @@ type BundleAddedSignal struct { } type Filter struct { - Identity string `json:"identity"` - FilterID string `json:"filterId"` - SymKeyID string `json:"symKeyId"` - ChatID string `json:"chatId"` - Topic whisper.TopicType `json:"topic"` + // ChatID is the identifier of the chat + ChatID string `json:"chatId"` + // SymKeyID is the symmetric key id used for symmetric chats + SymKeyID string `json:"symKeyId"` + // OneToOne tells us if we need to use asymmetric encryption for this chat + Listen bool `json:"listen"` + // FilterID the whisper filter id generated + FilterID string `json:"filterId"` + // Identity is the public key of the other recipient for non-public chats + Identity string `json:"identity"` + // Topic is the whisper topic + Topic whisper.TopicType `json:"topic"` } type WhisperFilterAddedSignal struct { diff --git a/static/chat_db_migrations/1558084410_add_secret.down.sql b/static/chat_db_migrations/1558084410_add_secret.down.sql new file mode 100644 index 000000000..1d1e25a51 --- /dev/null +++ b/static/chat_db_migrations/1558084410_add_secret.down.sql @@ -0,0 +1,2 @@ +DROP TABLE secret_installation_ids; +DROP TABLE secrets; diff --git a/static/chat_db_migrations/1558084410_add_topic.up.sql b/static/chat_db_migrations/1558084410_add_secret.up.sql similarity index 60% rename from static/chat_db_migrations/1558084410_add_topic.up.sql rename to static/chat_db_migrations/1558084410_add_secret.up.sql index 44ef68d0f..baf69f08b 100644 --- a/static/chat_db_migrations/1558084410_add_topic.up.sql +++ b/static/chat_db_migrations/1558084410_add_secret.up.sql @@ -1,11 +1,11 @@ -CREATE TABLE topics ( +CREATE TABLE secrets ( identity BLOB NOT NULL PRIMARY KEY ON CONFLICT IGNORE, secret BLOB NOT NULL ); -CREATE TABLE topic_installation_ids ( +CREATE TABLE secret_installation_ids ( id TEXT NOT NULL, identity_id BLOB NOT NULL, UNIQUE(id, identity_id) ON CONFLICT IGNORE, - FOREIGN KEY (identity_id) REFERENCES topics(identity) + FOREIGN KEY (identity_id) REFERENCES secrets(identity) ); diff --git a/static/chat_db_migrations/1558084410_add_topic.down.sql b/static/chat_db_migrations/1558084410_add_topic.down.sql deleted file mode 100644 index f6775d186..000000000 --- a/static/chat_db_migrations/1558084410_add_topic.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP TABLE topic_installation_ids; -DROP TABLE topics; diff --git a/vendor/github.com/ethereum/go-ethereum/whisper/whisperv6/filter.go b/vendor/github.com/ethereum/go-ethereum/whisper/whisperv6/filter.go new file mode 100644 index 000000000..c37c8997c --- /dev/null +++ b/vendor/github.com/ethereum/go-ethereum/whisper/whisperv6/filter.go @@ -0,0 +1,263 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package whisperv6 + +import ( + "crypto/ecdsa" + "fmt" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" +) + +// Filter represents a Whisper message filter +type Filter struct { + Src *ecdsa.PublicKey // Sender of the message + KeyAsym *ecdsa.PrivateKey // Private Key of recipient + KeySym []byte // Key associated with the Topic + Topics [][]byte // Topics to filter messages with + PoW float64 // Proof of work as described in the Whisper spec + AllowP2P bool // Indicates whether this filter is interested in direct peer-to-peer messages + SymKeyHash common.Hash // The Keccak256Hash of the symmetric key, needed for optimization + id string // unique identifier + + Messages map[common.Hash]*ReceivedMessage + mutex sync.RWMutex +} + +// Filters represents a collection of filters +type Filters struct { + watchers map[string]*Filter + + topicMatcher map[TopicType]map[*Filter]struct{} // map a topic to the filters that are interested in being notified when a message matches that topic + allTopicsMatcher map[*Filter]struct{} // list all the filters that will be notified of a new message, no matter what its topic is + + whisper *Whisper + mutex sync.RWMutex +} + +// NewFilters returns a newly created filter collection +func NewFilters(w *Whisper) *Filters { + return &Filters{ + watchers: make(map[string]*Filter), + topicMatcher: make(map[TopicType]map[*Filter]struct{}), + allTopicsMatcher: make(map[*Filter]struct{}), + whisper: w, + } +} + +// Install will add a new filter to the filter collection +func (fs *Filters) Install(watcher *Filter) (string, error) { + if watcher.KeySym != nil && watcher.KeyAsym != nil { + return "", fmt.Errorf("filters must choose between symmetric and asymmetric keys") + } + + if watcher.Messages == nil { + watcher.Messages = make(map[common.Hash]*ReceivedMessage) + } + + id, err := GenerateRandomID() + if err != nil { + return "", err + } + + fs.mutex.Lock() + defer fs.mutex.Unlock() + + if fs.watchers[id] != nil { + return "", fmt.Errorf("failed to generate unique ID") + } + + if watcher.expectsSymmetricEncryption() { + watcher.SymKeyHash = crypto.Keccak256Hash(watcher.KeySym) + } + + watcher.id = id + fs.watchers[id] = watcher + fs.addTopicMatcher(watcher) + return id, err +} + +// Uninstall will remove a filter whose id has been specified from +// the filter collection +func (fs *Filters) Uninstall(id string) bool { + fs.mutex.Lock() + defer fs.mutex.Unlock() + if fs.watchers[id] != nil { + fs.removeFromTopicMatchers(fs.watchers[id]) + delete(fs.watchers, id) + return true + } + return false +} + +// addTopicMatcher adds a filter to the topic matchers. +// If the filter's Topics array is empty, it will be tried on every topic. +// Otherwise, it will be tried on the topics specified. +func (fs *Filters) addTopicMatcher(watcher *Filter) { + if len(watcher.Topics) == 0 { + fs.allTopicsMatcher[watcher] = struct{}{} + } else { + for _, t := range watcher.Topics { + topic := BytesToTopic(t) + if fs.topicMatcher[topic] == nil { + fs.topicMatcher[topic] = make(map[*Filter]struct{}) + } + fs.topicMatcher[topic][watcher] = struct{}{} + } + } +} + +// removeFromTopicMatchers removes a filter from the topic matchers +func (fs *Filters) removeFromTopicMatchers(watcher *Filter) { + delete(fs.allTopicsMatcher, watcher) + for _, topic := range watcher.Topics { + delete(fs.topicMatcher[BytesToTopic(topic)], watcher) + } +} + +// getWatchersByTopic returns a slice containing the filters that +// match a specific topic +func (fs *Filters) getWatchersByTopic(topic TopicType) []*Filter { + res := make([]*Filter, 0, len(fs.allTopicsMatcher)) + for watcher := range fs.allTopicsMatcher { + res = append(res, watcher) + } + for watcher := range fs.topicMatcher[topic] { + res = append(res, watcher) + } + return res +} + +// Get returns a filter from the collection with a specific ID +func (fs *Filters) Get(id string) *Filter { + fs.mutex.RLock() + defer fs.mutex.RUnlock() + return fs.watchers[id] +} + +// NotifyWatchers notifies any filter that has declared interest +// for the envelope's topic. +func (fs *Filters) NotifyWatchers(env *Envelope, p2pMessage bool) { + var msg *ReceivedMessage + + fs.mutex.RLock() + defer fs.mutex.RUnlock() + + candidates := fs.getWatchersByTopic(env.Topic) + for _, watcher := range candidates { + if p2pMessage && !watcher.AllowP2P { + log.Trace(fmt.Sprintf("msg [%x], filter [%s]: p2p messages are not allowed", env.Hash(), watcher.id)) + continue + } + + var match bool + if msg != nil { + match = watcher.MatchMessage(msg) + } else { + match = watcher.MatchEnvelope(env) + if match { + msg = env.Open(watcher) + if msg == nil { + log.Trace("processing message: failed to open", "message", env.Hash().Hex(), "filter", watcher.id) + } + } else { + log.Trace("processing message: does not match", "message", env.Hash().Hex(), "filter", watcher.id) + } + } + + if match && msg != nil { + log.Trace("processing message: decrypted", "hash", env.Hash().Hex()) + if watcher.Src == nil || IsPubKeyEqual(msg.Src, watcher.Src) { + watcher.Trigger(msg) + } + } + } +} + +func (f *Filter) expectsAsymmetricEncryption() bool { + return f.KeyAsym != nil +} + +func (f *Filter) expectsSymmetricEncryption() bool { + return f.KeySym != nil +} + +// Trigger adds a yet-unknown message to the filter's list of +// received messages. +func (f *Filter) Trigger(msg *ReceivedMessage) { + f.mutex.Lock() + defer f.mutex.Unlock() + + if _, exist := f.Messages[msg.EnvelopeHash]; !exist { + f.Messages[msg.EnvelopeHash] = msg + } +} + +// Retrieve will return the list of all received messages associated +// to a filter. +func (f *Filter) Retrieve() (all []*ReceivedMessage) { + f.mutex.Lock() + defer f.mutex.Unlock() + + all = make([]*ReceivedMessage, 0, len(f.Messages)) + for _, msg := range f.Messages { + all = append(all, msg) + } + + f.Messages = make(map[common.Hash]*ReceivedMessage) // delete old messages + return all +} + +// MatchMessage checks if the filter matches an already decrypted +// message (i.e. a Message that has already been handled by +// MatchEnvelope when checked by a previous filter). +// Topics are not checked here, since this is done by topic matchers. +func (f *Filter) MatchMessage(msg *ReceivedMessage) bool { + if f.PoW > 0 && msg.PoW < f.PoW { + return false + } + + if f.expectsAsymmetricEncryption() && msg.isAsymmetricEncryption() { + return IsPubKeyEqual(&f.KeyAsym.PublicKey, msg.Dst) + } else if f.expectsSymmetricEncryption() && msg.isSymmetricEncryption() { + return f.SymKeyHash == msg.SymKeyHash + } + return false +} + +// MatchEnvelope checks if it's worth decrypting the message. If +// it returns `true`, client code is expected to attempt decrypting +// the message and subsequently call MatchMessage. +// Topics are not checked here, since this is done by topic matchers. +func (f *Filter) MatchEnvelope(envelope *Envelope) bool { + log.Trace("checking pow", "filter", f.PoW, "envelope", envelope.pow) + return f.PoW <= 0 || envelope.pow >= f.PoW +} + +// IsPubKeyEqual checks that two public keys are equal +func IsPubKeyEqual(a, b *ecdsa.PublicKey) bool { + if !ValidatePublicKey(a) { + return false + } else if !ValidatePublicKey(b) { + return false + } + // the curve is always the same, just compare the points + return a.X.Cmp(b.X) == 0 && a.Y.Cmp(b.Y) == 0 +} diff --git a/vendor/github.com/status-im/whisper/whisperv6/filter.go b/vendor/github.com/status-im/whisper/whisperv6/filter.go index b75f7b3c8..1b3849a94 100644 --- a/vendor/github.com/status-im/whisper/whisperv6/filter.go +++ b/vendor/github.com/status-im/whisper/whisperv6/filter.go @@ -197,6 +197,7 @@ func (fs *Filters) NotifyWatchers(env *Envelope, p2pMessage bool) { fs.mutex.RLock() defer fs.mutex.RUnlock() + log.Info("Got envelope for topic", "topic", env.Topic) candidates := fs.getWatchersByTopic(env.Topic) for _, watcher := range candidates { if p2pMessage && !watcher.AllowP2P { @@ -279,6 +280,7 @@ func (f *Filter) MatchMessage(msg *ReceivedMessage) bool { // the message and subsequently call MatchMessage. // Topics are not checked here, since this is done by topic matchers. func (f *Filter) MatchEnvelope(envelope *Envelope) bool { + log.Trace("checking pow", "filter", f.PoW, "envelope", envelope.pow) return f.PoW <= 0 || envelope.pow >= f.PoW }