From cef7f367ab6f9749fcd60662cc250476fd432a6c Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Fri, 17 May 2019 13:06:56 +0200 Subject: [PATCH] Add topic negotiation This commit add topic negotiation to the protocol. On receiving a message from a client with version >= 1, we will generate a shared key using Diffie-Hellman. We will record also which installationID has sent us a message. This key will be passed back to the above layer, which will then use to start listening to a whisper topic (the `chat` namespace has no knowledge of whisper). When sending a message to a set of InstallationIDs, we check whether we have agreed on a topic with all of them, and if so, we will send on this separate topic, otherwise we fallback on discovery. This change is backward compatible, as long as there is no downgrade of the app on the other side. A few changes: * Factored out the DB in a separate namespace as now it is being used by multiple services (TopicService and EncryptionService). * Factored out multidevice management in a separate namespace * Moved all the test to test the whole protoocl rather than just the encryption service * Moved all the filter management in status-go --- Makefile | 2 +- mailserver/migrations/bindata.go | 110 ++---- params/config.go | 2 + services/shhext/api.go | 93 ++--- services/shhext/chat/chat.proto | 50 --- services/shhext/chat/db/db.go | 244 ++++++++++++ .../chat/{ => db}/migrations/bindata.go | 212 +++++------ services/shhext/chat/encryption.pb.go | 85 +++-- services/shhext/chat/encryption.proto | 2 + services/shhext/chat/protocol.go | 100 +++-- services/shhext/chat/protocol_test.go | 73 ++-- services/shhext/chat/sql_lite_persistence.go | 243 +----------- services/shhext/chat/test.db | Bin 28672 -> 0 bytes services/shhext/chat/topic/persistence.go | 125 +++++++ services/shhext/chat/topic/service.go | 93 +++++ services/shhext/chat/topic/service_test.go | 114 ++++++ services/shhext/chat/whisper.go | 20 +- services/shhext/chat/whisper_test.go | 19 +- services/shhext/filter/service.go | 350 ++++++++++++++++++ services/shhext/filter/service_test.go | 155 ++++++++ services/shhext/service.go | 61 ++- services/shhext/signal.go | 4 + signal/events_shhext.go | 20 + static/bindata.go | 199 ++++------ .../1536754952_initial_schema.down.sql | 0 .../1536754952_initial_schema.up.sql | 0 .../1539249977_update_ratchet_info.down.sql | 0 .../1539249977_update_ratchet_info.up.sql | 0 .../1540715431_add_version.down.sql | 0 .../1540715431_add_version.up.sql | 0 .../1541164797_add_installations.down.sql | 0 .../1541164797_add_installations.up.sql | 0 .../1558084410_add_topic.down.sql | 2 + .../1558084410_add_topic.up.sql | 11 + .../static.go | 2 +- t/bindata.go | 108 ++---- 36 files changed, 1675 insertions(+), 824 deletions(-) delete mode 100644 services/shhext/chat/chat.proto create mode 100644 services/shhext/chat/db/db.go rename services/shhext/chat/{ => db}/migrations/bindata.go (67%) delete mode 100644 services/shhext/chat/test.db create mode 100644 services/shhext/chat/topic/persistence.go create mode 100644 services/shhext/chat/topic/service.go create mode 100644 services/shhext/chat/topic/service_test.go create mode 100644 services/shhext/filter/service.go create mode 100644 services/shhext/filter/service_test.go rename static/{encryption_migrations => chat_db_migrations}/1536754952_initial_schema.down.sql (100%) rename static/{encryption_migrations => chat_db_migrations}/1536754952_initial_schema.up.sql (100%) rename static/{encryption_migrations => chat_db_migrations}/1539249977_update_ratchet_info.down.sql (100%) rename static/{encryption_migrations => chat_db_migrations}/1539249977_update_ratchet_info.up.sql (100%) rename static/{encryption_migrations => chat_db_migrations}/1540715431_add_version.down.sql (100%) rename static/{encryption_migrations => chat_db_migrations}/1540715431_add_version.up.sql (100%) rename static/{encryption_migrations => chat_db_migrations}/1541164797_add_installations.down.sql (100%) rename static/{encryption_migrations => chat_db_migrations}/1541164797_add_installations.up.sql (100%) create mode 100644 static/chat_db_migrations/1558084410_add_topic.down.sql create mode 100644 static/chat_db_migrations/1558084410_add_topic.up.sql rename static/{encryption_migrations => chat_db_migrations}/static.go (82%) diff --git a/Makefile b/Makefile index 8dd2d19b8..9b1361023 100644 --- a/Makefile +++ b/Makefile @@ -171,7 +171,7 @@ setup-build: lint-install release-install gomobile-install ##@other Prepare proj setup: setup-build setup-dev tidy ##@other Prepare project for development and building generate: ##@other Regenerate assets and other auto-generated stuff - go generate ./static ./static/encryption_migrations ./static/mailserver_db_migrations ./t + go generate ./static ./static/chat_db_migrations ./static/mailserver_db_migrations ./t $(shell cd ./services/shhext/chat && exec protoc --go_out=. ./*.proto) prepare-release: clean-release diff --git a/mailserver/migrations/bindata.go b/mailserver/migrations/bindata.go index e19ee203b..042ba8619 100644 --- a/mailserver/migrations/bindata.go +++ b/mailserver/migrations/bindata.go @@ -1,15 +1,15 @@ -// Code generated by go-bindata. DO NOT EDIT. +// Code generated by go-bindata. // sources: -// 1557732988_initialize_db.down.sql (72B) -// 1557732988_initialize_db.up.sql (234B) -// static.go (178B) +// 1557732988_initialize_db.down.sql +// 1557732988_initialize_db.up.sql +// static.go +// DO NOT EDIT! package migrations import ( "bytes" "compress/gzip" - "crypto/sha256" "fmt" "io" "io/ioutil" @@ -22,7 +22,7 @@ import ( func bindataRead(data []byte, name string) ([]byte, error) { gz, err := gzip.NewReader(bytes.NewBuffer(data)) if err != nil { - return nil, fmt.Errorf("read %q: %v", name, err) + return nil, fmt.Errorf("Read %q: %v", name, err) } var buf bytes.Buffer @@ -30,7 +30,7 @@ func bindataRead(data []byte, name string) ([]byte, error) { clErr := gz.Close() if err != nil { - return nil, fmt.Errorf("read %q: %v", name, err) + return nil, fmt.Errorf("Read %q: %v", name, err) } if clErr != nil { return nil, err @@ -40,9 +40,8 @@ func bindataRead(data []byte, name string) ([]byte, error) { } type asset struct { - bytes []byte - info os.FileInfo - digest [sha256.Size]byte + bytes []byte + info os.FileInfo } type bindataFileInfo struct { @@ -86,8 +85,8 @@ func _1557732988_initialize_dbDownSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1557732988_initialize_db.down.sql", size: 72, mode: os.FileMode(0644), modTime: time.Unix(1560158346, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x77, 0x40, 0x78, 0xb7, 0x71, 0x3c, 0x20, 0x3b, 0xc9, 0xb, 0x2f, 0x49, 0xe4, 0xff, 0x1c, 0x84, 0x54, 0xa1, 0x30, 0xe3, 0x90, 0xf8, 0x73, 0xda, 0xb0, 0x2a, 0xea, 0x8e, 0xf1, 0x82, 0xe7, 0xd2}} + info := bindataFileInfo{name: "1557732988_initialize_db.down.sql", size: 72, mode: os.FileMode(420), modTime: time.Unix(1560418002, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -106,8 +105,8 @@ func _1557732988_initialize_dbUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1557732988_initialize_db.up.sql", size: 234, mode: os.FileMode(0644), modTime: time.Unix(1560158346, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x8f, 0xa, 0x31, 0xf, 0x94, 0xe, 0xd7, 0xd6, 0xaa, 0x22, 0xd6, 0x6c, 0x7a, 0xbc, 0xad, 0x6a, 0xed, 0x2e, 0x7a, 0xf0, 0x24, 0x81, 0x87, 0x14, 0xe, 0x1c, 0x8a, 0xf1, 0x45, 0xaf, 0x9e, 0x85}} + info := bindataFileInfo{name: "1557732988_initialize_db.up.sql", size: 234, mode: os.FileMode(420), modTime: time.Unix(1560418002, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -126,8 +125,8 @@ func staticGo() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "static.go", size: 178, mode: os.FileMode(0644), modTime: time.Unix(1560158346, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xab, 0x8a, 0xf4, 0x27, 0x24, 0x9d, 0x2a, 0x1, 0x7b, 0x54, 0xea, 0xae, 0x4a, 0x35, 0x40, 0x92, 0xb5, 0xf9, 0xb3, 0x54, 0x3e, 0x3a, 0x1a, 0x2b, 0xae, 0xfb, 0x9e, 0x82, 0xeb, 0x4c, 0xf, 0x6}} + info := bindataFileInfo{name: "static.go", size: 178, mode: os.FileMode(420), modTime: time.Unix(1560418002, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -135,8 +134,8 @@ func staticGo() (*asset, error) { // It returns an error if the asset could not be found or // could not be loaded. func Asset(name string) ([]byte, error) { - canonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[canonicalName]; ok { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) @@ -146,12 +145,6 @@ func Asset(name string) ([]byte, error) { return nil, fmt.Errorf("Asset %s not found", name) } -// AssetString returns the asset contents as a string (instead of a []byte). -func AssetString(name string) (string, error) { - data, err := Asset(name) - return string(data), err -} - // MustAsset is like Asset but panics when Asset would return an error. // It simplifies safe initialization of global variables. func MustAsset(name string) []byte { @@ -163,18 +156,12 @@ func MustAsset(name string) []byte { return a } -// MustAssetString is like AssetString but panics when Asset would return an -// error. It simplifies safe initialization of global variables. -func MustAssetString(name string) string { - return string(MustAsset(name)) -} - // AssetInfo loads and returns the asset info for the given name. // It returns an error if the asset could not be found or // could not be loaded. func AssetInfo(name string) (os.FileInfo, error) { - canonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[canonicalName]; ok { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) @@ -184,33 +171,6 @@ func AssetInfo(name string) (os.FileInfo, error) { return nil, fmt.Errorf("AssetInfo %s not found", name) } -// AssetDigest returns the digest of the file with the given name. It returns an -// error if the asset could not be found or the digest could not be loaded. -func AssetDigest(name string) ([sha256.Size]byte, error) { - canonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[canonicalName]; ok { - a, err := f() - if err != nil { - return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s can't read by error: %v", name, err) - } - return a.digest, nil - } - return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s not found", name) -} - -// Digests returns a map of all known files and their checksums. -func Digests() (map[string][sha256.Size]byte, error) { - mp := make(map[string][sha256.Size]byte, len(_bindata)) - for name := range _bindata { - a, err := _bindata[name]() - if err != nil { - return nil, err - } - mp[name] = a.digest - } - return mp, nil -} - // AssetNames returns the names of the assets. func AssetNames() []string { names := make([]string, 0, len(_bindata)) @@ -223,9 +183,7 @@ func AssetNames() []string { // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "1557732988_initialize_db.down.sql": _1557732988_initialize_dbDownSql, - "1557732988_initialize_db.up.sql": _1557732988_initialize_dbUpSql, - "static.go": staticGo, } @@ -238,15 +196,15 @@ var _bindata = map[string]func() (*asset, error){ // img/ // a.png // b.png -// then AssetDir("data") would return []string{"foo.txt", "img"}, -// AssetDir("data/img") would return []string{"a.png", "b.png"}, -// AssetDir("foo.txt") and AssetDir("notexist") would return an error, and +// then AssetDir("data") would return []string{"foo.txt", "img"} +// AssetDir("data/img") would return []string{"a.png", "b.png"} +// AssetDir("foo.txt") and AssetDir("notexist") would return an error // AssetDir("") will return []string{"data"}. func AssetDir(name string) ([]string, error) { node := _bintree if len(name) != 0 { - canonicalName := strings.Replace(name, "\\", "/", -1) - pathList := strings.Split(canonicalName, "/") + cannonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(cannonicalName, "/") for _, p := range pathList { node = node.Children[p] if node == nil { @@ -268,14 +226,13 @@ type bintree struct { Func func() (*asset, error) Children map[string]*bintree } - var _bintree = &bintree{nil, map[string]*bintree{ "1557732988_initialize_db.down.sql": &bintree{_1557732988_initialize_dbDownSql, map[string]*bintree{}}, - "1557732988_initialize_db.up.sql": &bintree{_1557732988_initialize_dbUpSql, map[string]*bintree{}}, - "static.go": &bintree{staticGo, map[string]*bintree{}}, + "1557732988_initialize_db.up.sql": &bintree{_1557732988_initialize_dbUpSql, map[string]*bintree{}}, + "static.go": &bintree{staticGo, map[string]*bintree{}}, }} -// RestoreAsset restores an asset under the given directory. +// RestoreAsset restores an asset under the given directory func RestoreAsset(dir, name string) error { data, err := Asset(name) if err != nil { @@ -293,10 +250,14 @@ func RestoreAsset(dir, name string) error { if err != nil { return err } - return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) + err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) + if err != nil { + return err + } + return nil } -// RestoreAssets restores an asset under the given directory recursively. +// RestoreAssets restores an asset under the given directory recursively func RestoreAssets(dir, name string) error { children, err := AssetDir(name) // File @@ -314,6 +275,7 @@ func RestoreAssets(dir, name string) error { } func _filePath(dir, name string) string { - canonicalName := strings.Replace(name, "\\", "/", -1) - return filepath.Join(append([]string{dir}, strings.Split(canonicalName, "/")...)...) + cannonicalName := strings.Replace(name, "\\", "/", -1) + return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) } + diff --git a/params/config.go b/params/config.go index 7703f9407..ce0991be4 100644 --- a/params/config.go +++ b/params/config.go @@ -368,6 +368,8 @@ 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 f845b8771..e9d312951 100644 --- a/services/shhext/api.go +++ b/services/shhext/api.go @@ -14,8 +14,10 @@ import ( "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/status-im/status-go/db" "github.com/status-im/status-go/mailserver" "github.com/status-im/status-go/services/shhext/chat" @@ -485,8 +487,15 @@ func (api *PublicAPI) SendPublicMessage(ctx context.Context, msg chat.SendPublic 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, protocolMessage) + whisperMessage := chat.PublicMessageToWhisper(msg, marshaledMessage) whisperMessage.SymKeyID = symKeyID // And dispatch @@ -510,20 +519,46 @@ func (api *PublicAPI) SendDirectMessage(ctx context.Context, msg chat.SendDirect } // This is transport layer-agnostic - var protocolMessage []byte + var protocolMessage *chat.ProtocolMessage + // The negotiated secret + var topic []byte + api.log.Info("BUILDING MESSAGE") if msg.DH { - protocolMessage, err = api.service.protocol.BuildDHMessage(privateKey, &privateKey.PublicKey, msg.Payload) + protocolMessage, topic, err = api.service.protocol.BuildDHMessage(privateKey, &privateKey.PublicKey, msg.Payload) } else { - protocolMessage, err = api.service.protocol.BuildDirectMessage(privateKey, publicKey, msg.Payload) + 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 - whisperMessage := chat.DirectMessageToWhisper(msg, protocolMessage) + 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) @@ -637,40 +672,6 @@ func (api *PublicAPI) CompleteRequest(parent context.Context, hex string) (err e return err } -// DEPRECATED: use SendDirectMessage with DH flag -// SendPairingMessage sends a 1:1 chat message to our own devices to initiate a pairing session -func (api *PublicAPI) SendPairingMessage(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 - } - - msg.PubKey = crypto.FromECDSAPub(&privateKey.PublicKey) - - protocolMessage, err := api.service.protocol.BuildDHMessage(privateKey, &privateKey.PublicKey, msg.Payload) - if err != nil { - return nil, err - } - - var response []hexutil.Bytes - - // Enrich with transport layer info - whisperMessage := chat.DirectMessageToWhisper(msg, protocolMessage) - - // And dispatch - hash, err := api.Post(ctx, whisperMessage) - if err != nil { - return nil, err - } - response = append(response, hash) - - return response, nil -} - func (api *PublicAPI) processPFSMessage(dedupMessage dedup.DeduplicateMessage) error { msg := dedupMessage.Message @@ -689,7 +690,15 @@ func (api *PublicAPI) processPFSMessage(dedupMessage dedup.DeduplicateMessage) e return err } - response, err := api.service.protocol.HandleMessage(privateKey, publicKey, msg.Payload, dedupMessage.DedupID) + // 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: @@ -703,13 +712,9 @@ func (api *PublicAPI) processPFSMessage(dedupMessage dedup.DeduplicateMessage) e handler := EnvelopeSignalHandler{} handler.DecryptMessageFailed(keyString) } - case chat.ErrNotProtocolMessage: - // Not using encryption, pass directly to the layer below - api.log.Debug("Not a protocol message", "err", err) default: // Log and pass to the client, even if failed to decrypt api.log.Error("Failed handling message with error", "err", err) - } return nil diff --git a/services/shhext/chat/chat.proto b/services/shhext/chat/chat.proto deleted file mode 100644 index 9f70a325c..000000000 --- a/services/shhext/chat/chat.proto +++ /dev/null @@ -1,50 +0,0 @@ -syntax = "proto3"; - -package chat; - -// What is sent through the wire -message ChatMessagePayload { - // Message content - string content = 1; - // MIME type - string content_type = 2; - // Message type - string message_type = 3; - // Sender's clock value for message ordering - double clock_value = 4; -} - -// ContactUpdatePayload is sent when a user updates its profile -message ContactUpdatePayload { - // Contact display name - string name = 1; - // Contact profile image, using the data URI scheme (e.g. "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAMAAAC7IEhfAAA...") - string profile_image = 2; - // Contact address - string address = 3; - // Contact Firebase Cloud Messaging token - string fcm_token = 4; -} - -// Incoming RPC messages -message OneToOneRPC { - string src = 1; - string dst = 2; - bytes payload = 3; - //ChatMessagePayload payload = 3; -} - -message ContactUpdateRPC { - string src = 1; - string dst = 2; - ContactUpdatePayload payload = 3; -} - -// Incoming messages -message ChatProtocolMessage { - bytes payload = 1; - //oneof payload { - // ChatMessagePayload one_to_one_payload = 1; - // ContactUpdatePayload contact_updated_payload = 2; - //} -} diff --git a/services/shhext/chat/db/db.go b/services/shhext/chat/db/db.go new file mode 100644 index 000000000..455180221 --- /dev/null +++ b/services/shhext/chat/db/db.go @@ -0,0 +1,244 @@ +package db + +import ( + "database/sql" + "fmt" + "os" + + sqlite "github.com/mutecomm/go-sqlcipher" // We require go sqlcipher that overrides default implementation + "github.com/status-im/migrate" + "github.com/status-im/migrate/database/sqlcipher" + "github.com/status-im/migrate/source/go_bindata" + "github.com/status-im/status-go/services/shhext/chat/db/migrations" +) + +const exportDB = "SELECT sqlcipher_export('newdb')" + +// The default number of kdf iterations in sqlcipher (from version 3.0.0) +// https://github.com/sqlcipher/sqlcipher/blob/fda4c68bb474da7e955be07a2b807bda1bb19bd2/CHANGELOG.md#300---2013-11-05 +// https://www.zetetic.net/sqlcipher/sqlcipher-api/#kdf_iter +const defaultKdfIterationsNumber = 64000 + +// The reduced number of kdf iterations (for performance reasons) which is +// currently used for derivation of the database key +// https://github.com/status-im/status-go/pull/1343 +// https://notes.status.im/i8Y_l7ccTiOYq09HVgoFwA +const KdfIterationsNumber = 3200 + +func MigrateDBFile(oldPath string, newPath string, oldKey string, newKey string) error { + _, err := os.Stat(oldPath) + + // No files, nothing to do + if os.IsNotExist(err) { + return nil + } + + // Any other error, throws + if err != nil { + return err + } + + if err := os.Rename(oldPath, newPath); err != nil { + return err + } + + db, err := Open(newPath, oldKey, defaultKdfIterationsNumber) + if err != nil { + return err + } + + keyString := fmt.Sprintf("PRAGMA rekey = '%s'", newKey) + + if _, err = db.Exec(keyString); err != nil { + return err + } + + return nil + +} + +// MigrateDBKeyKdfIterations changes the number of kdf iterations executed +// during the database key derivation. This change is necessary because +// of performance reasons. +// https://github.com/status-im/status-go/pull/1343 +// `sqlcipher_export` is used for migration, check out this link for details: +// https://www.zetetic.net/sqlcipher/sqlcipher-api/#sqlcipher_export +func MigrateDBKeyKdfIterations(oldPath string, newPath string, key string) error { + _, err := os.Stat(oldPath) + + // No files, nothing to do + if os.IsNotExist(err) { + return nil + } + + // Any other error, throws + if err != nil { + return err + } + + isEncrypted, err := sqlite.IsEncrypted(oldPath) + if err != nil { + return err + } + + // Nothing to do, move db to the next migration + if !isEncrypted { + return os.Rename(oldPath, newPath) + } + + db, err := Open(oldPath, key, defaultKdfIterationsNumber) + if err != nil { + return err + } + + attach := fmt.Sprintf( + "ATTACH DATABASE '%s' AS newdb KEY '%s'", + newPath, + key) + + if _, err = db.Exec(attach); err != nil { + return err + } + + changeKdfIter := fmt.Sprintf( + "PRAGMA newdb.kdf_iter = %d", + KdfIterationsNumber) + + if _, err = db.Exec(changeKdfIter); err != nil { + return err + } + + if _, err = db.Exec(exportDB); err != nil { + return err + } + + if err = db.Close(); err != nil { + return err + } + + return os.Remove(oldPath) +} + +// EncryptDatabase encrypts an unencrypted database with key +func EncryptDatabase(oldPath string, newPath string, key string) error { + _, err := os.Stat(oldPath) + + // No files, nothing to do + if os.IsNotExist(err) { + return nil + } + + // Any other error, throws + if err != nil { + return err + } + + isEncrypted, err := sqlite.IsEncrypted(oldPath) + if err != nil { + return err + } + + // Nothing to do, already encrypted + if isEncrypted { + return os.Rename(oldPath, newPath) + } + + db, err := Open(oldPath, "", defaultKdfIterationsNumber) + if err != nil { + return err + } + + attach := fmt.Sprintf( + "ATTACH DATABASE '%s' AS newdb KEY '%s'", + newPath, + key) + + if _, err = db.Exec(attach); err != nil { + return err + } + + changeKdfIter := fmt.Sprintf( + "PRAGMA newdb.kdf_iter = %d", + KdfIterationsNumber) + + if _, err = db.Exec(changeKdfIter); err != nil { + return err + } + + if _, err = db.Exec(exportDB); err != nil { + return err + } + + if err = db.Close(); err != nil { + return err + } + + return os.Remove(oldPath) +} + +func migrateDB(db *sql.DB) error { + resources := bindata.Resource( + migrations.AssetNames(), + func(name string) ([]byte, error) { + return migrations.Asset(name) + }, + ) + + source, err := bindata.WithInstance(resources) + if err != nil { + return err + } + + driver, err := sqlcipher.WithInstance(db, &sqlcipher.Config{}) + if err != nil { + return err + } + + m, err := migrate.NewWithInstance( + "go-bindata", + source, + "sqlcipher", + driver) + if err != nil { + return err + } + + if err = m.Up(); err != migrate.ErrNoChange { + return err + } + + return nil +} + +func Open(path string, key string, kdfIter int) (*sql.DB, error) { + db, err := sql.Open("sqlite3", path) + if err != nil { + return nil, err + } + + keyString := fmt.Sprintf("PRAGMA key = '%s'", key) + + // Disable concurrent access as not supported by the driver + db.SetMaxOpenConns(1) + + if _, err = db.Exec("PRAGMA foreign_keys=ON"); err != nil { + return nil, err + } + + if _, err = db.Exec(keyString); err != nil { + return nil, err + } + + kdfString := fmt.Sprintf("PRAGMA kdf_iter = '%d'", kdfIter) + + if _, err = db.Exec(kdfString); err != nil { + return nil, err + } + + // Migrate db + if err = migrateDB(db); err != nil { + return nil, err + } + + return db, nil +} diff --git a/services/shhext/chat/migrations/bindata.go b/services/shhext/chat/db/migrations/bindata.go similarity index 67% rename from services/shhext/chat/migrations/bindata.go rename to services/shhext/chat/db/migrations/bindata.go index 908726568..df8c0c383 100644 --- a/services/shhext/chat/migrations/bindata.go +++ b/services/shhext/chat/db/migrations/bindata.go @@ -1,21 +1,23 @@ -// Code generated by go-bindata. DO NOT EDIT. +// Code generated by go-bindata. // sources: -// 1536754952_initial_schema.down.sql (83B) -// 1536754952_initial_schema.up.sql (962B) -// 1539249977_update_ratchet_info.down.sql (311B) -// 1539249977_update_ratchet_info.up.sql (368B) -// 1540715431_add_version.down.sql (127B) -// 1540715431_add_version.up.sql (265B) -// 1541164797_add_installations.down.sql (26B) -// 1541164797_add_installations.up.sql (216B) -// static.go (188B) +// 1536754952_initial_schema.down.sql +// 1536754952_initial_schema.up.sql +// 1539249977_update_ratchet_info.down.sql +// 1539249977_update_ratchet_info.up.sql +// 1540715431_add_version.down.sql +// 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 +// static.go +// DO NOT EDIT! package migrations import ( "bytes" "compress/gzip" - "crypto/sha256" "fmt" "io" "io/ioutil" @@ -28,7 +30,7 @@ import ( func bindataRead(data []byte, name string) ([]byte, error) { gz, err := gzip.NewReader(bytes.NewBuffer(data)) if err != nil { - return nil, fmt.Errorf("read %q: %v", name, err) + return nil, fmt.Errorf("Read %q: %v", name, err) } var buf bytes.Buffer @@ -36,7 +38,7 @@ func bindataRead(data []byte, name string) ([]byte, error) { clErr := gz.Close() if err != nil { - return nil, fmt.Errorf("read %q: %v", name, err) + return nil, fmt.Errorf("Read %q: %v", name, err) } if clErr != nil { return nil, err @@ -46,9 +48,8 @@ func bindataRead(data []byte, name string) ([]byte, error) { } type asset struct { - bytes []byte - info os.FileInfo - digest [sha256.Size]byte + bytes []byte + info os.FileInfo } type bindataFileInfo struct { @@ -92,8 +93,8 @@ func _1536754952_initial_schemaDownSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1536754952_initial_schema.down.sql", size: 83, mode: os.FileMode(0644), modTime: time.Unix(1560158346, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x44, 0xcf, 0x76, 0x71, 0x1f, 0x5e, 0x9a, 0x43, 0xd8, 0xcd, 0xb8, 0xc3, 0x70, 0xc3, 0x7f, 0xfc, 0x90, 0xb4, 0x25, 0x1e, 0xf4, 0x66, 0x20, 0xb8, 0x33, 0x7e, 0xb0, 0x76, 0x1f, 0xc, 0xc0, 0x75}} + info := bindataFileInfo{name: "1536754952_initial_schema.down.sql", size: 83, mode: os.FileMode(420), modTime: time.Unix(1560418030, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -112,8 +113,8 @@ func _1536754952_initial_schemaUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1536754952_initial_schema.up.sql", size: 962, mode: os.FileMode(0644), modTime: time.Unix(1560158346, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xea, 0x90, 0x5a, 0x59, 0x3e, 0x3, 0xe2, 0x3c, 0x81, 0x42, 0xcd, 0x4c, 0x9a, 0xe8, 0xda, 0x93, 0x2b, 0x70, 0xa4, 0xd5, 0x29, 0x3e, 0xd5, 0xc9, 0x27, 0xb6, 0xb7, 0x65, 0xff, 0x0, 0xcb, 0xde}} + info := bindataFileInfo{name: "1536754952_initial_schema.up.sql", size: 962, mode: os.FileMode(420), modTime: time.Unix(1560418030, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -132,8 +133,8 @@ func _1539249977_update_ratchet_infoDownSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1539249977_update_ratchet_info.down.sql", size: 311, mode: os.FileMode(0644), modTime: time.Unix(1560158346, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x1, 0xa4, 0xeb, 0xa0, 0xe6, 0xa0, 0xd4, 0x48, 0xbb, 0xad, 0x6f, 0x7d, 0x67, 0x8c, 0xbd, 0x25, 0xde, 0x1f, 0x73, 0x9a, 0xbb, 0xa8, 0xc9, 0x30, 0xb7, 0xa9, 0x7c, 0xaf, 0xb5, 0x1, 0x61, 0xdd}} + info := bindataFileInfo{name: "1539249977_update_ratchet_info.down.sql", size: 311, mode: os.FileMode(420), modTime: time.Unix(1560418030, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -152,8 +153,8 @@ func _1539249977_update_ratchet_infoUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1539249977_update_ratchet_info.up.sql", size: 368, mode: os.FileMode(0644), modTime: time.Unix(1560158346, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xc, 0x8e, 0xbf, 0x6f, 0xa, 0xc0, 0xe1, 0x3c, 0x42, 0x28, 0x88, 0x1d, 0xdb, 0xba, 0x1c, 0x83, 0xec, 0xba, 0xd3, 0x5f, 0x5c, 0x77, 0x5e, 0xa7, 0x46, 0x36, 0xec, 0x69, 0xa, 0x4b, 0x17, 0x79}} + info := bindataFileInfo{name: "1539249977_update_ratchet_info.up.sql", size: 368, mode: os.FileMode(420), modTime: time.Unix(1560418030, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -172,8 +173,8 @@ func _1540715431_add_versionDownSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1540715431_add_version.down.sql", size: 127, mode: os.FileMode(0644), modTime: time.Unix(1560158346, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xf5, 0x9, 0x4, 0xe3, 0x76, 0x2e, 0xb8, 0x9, 0x23, 0xf0, 0x70, 0x93, 0xc4, 0x50, 0xe, 0x9d, 0x84, 0x22, 0x8c, 0x94, 0xd3, 0x24, 0x9, 0x9a, 0xc1, 0xa1, 0x48, 0x45, 0xfd, 0x40, 0x6e, 0xe6}} + info := bindataFileInfo{name: "1540715431_add_version.down.sql", size: 127, mode: os.FileMode(420), modTime: time.Unix(1560418030, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -192,8 +193,8 @@ func _1540715431_add_versionUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1540715431_add_version.up.sql", size: 265, mode: os.FileMode(0644), modTime: time.Unix(1560158346, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xc7, 0x4c, 0x36, 0x96, 0xdf, 0x16, 0x10, 0xa6, 0x27, 0x1a, 0x79, 0x8b, 0x42, 0x83, 0x23, 0xc, 0x7e, 0xb6, 0x3d, 0x2, 0xda, 0xa4, 0xb4, 0xd, 0x27, 0x55, 0xba, 0xdc, 0xb2, 0x88, 0x8f, 0xa6}} + info := bindataFileInfo{name: "1540715431_add_version.up.sql", size: 265, mode: os.FileMode(420), modTime: time.Unix(1560418030, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -212,8 +213,8 @@ func _1541164797_add_installationsDownSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1541164797_add_installations.down.sql", size: 26, mode: os.FileMode(0644), modTime: time.Unix(1560158346, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xf5, 0xfd, 0xe6, 0xd8, 0xca, 0x3b, 0x38, 0x18, 0xee, 0x0, 0x5f, 0x36, 0x9e, 0x1e, 0xd, 0x19, 0x3e, 0xb4, 0x73, 0x53, 0xe9, 0xa5, 0xac, 0xdd, 0xa1, 0x2f, 0xc7, 0x6c, 0xa8, 0xd9, 0xa, 0x88}} + info := bindataFileInfo{name: "1541164797_add_installations.down.sql", size: 26, mode: os.FileMode(420), modTime: time.Unix(1560418030, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -232,12 +233,52 @@ func _1541164797_add_installationsUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1541164797_add_installations.up.sql", size: 216, mode: os.FileMode(0644), modTime: time.Unix(1560158346, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x2d, 0x18, 0x26, 0xb8, 0x88, 0x47, 0xdb, 0x83, 0xcc, 0xb6, 0x9d, 0x1c, 0x1, 0xae, 0x2f, 0xde, 0x97, 0x82, 0x3, 0x30, 0xa8, 0x63, 0xa1, 0x78, 0x4b, 0xa5, 0x9, 0x8, 0x75, 0xa2, 0x57, 0x81}} + info := bindataFileInfo{name: "1541164797_add_installations.up.sql", size: 216, mode: os.FileMode(420), modTime: time.Unix(1560418030, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } -var _staticGo = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x54\xcc\x41\x8a\x02\x31\x10\x46\xe1\x7d\x4e\xf1\x2f\x67\x60\x3a\xb5\x9f\x13\x0c\x83\x82\xa0\x17\xa8\x4e\x17\x95\xa2\xe9\xa4\x49\x95\xe2\xf1\xdd\x28\xe2\xf2\xc1\xe3\x23\xc2\x89\xcb\xca\x2a\xf0\xe0\xb0\x02\xd9\x66\x59\xfc\x55\x5f\xff\xe7\x1f\xfc\x5d\x8e\x87\x6f\x0c\xf1\x7e\x1d\x45\x1c\xc3\xb4\x06\xac\x45\x47\x54\xc1\x6c\x8d\x87\x89\xa7\xfd\x43\x4a\x89\x48\xfb\xaf\x4a\x93\xc1\x21\xd0\x3e\xcd\xd6\x16\x0e\xc6\xb4\xaf\x8a\xcd\x74\x70\x58\x6f\x8e\xa9\x23\x67\xca\x99\x5c\xc6\xcd\x8a\x38\x79\xad\x72\x0f\x2a\x95\x83\xde\x23\x3d\x81\xac\x1d\x39\x3d\x02\x00\x00\xff\xff\x7c\xfc\xfc\x0b\xbc\x00\x00\x00") +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") + +func _1558084410_add_topicDownSqlBytes() ([]byte, error) { + return bindataRead( + __1558084410_add_topicDownSql, + "1558084410_add_topic.down.sql", + ) +} + +func _1558084410_add_topicDownSql() (*asset, error) { + bytes, err := _1558084410_add_topicDownSqlBytes() + 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)} + 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") + +func _1558084410_add_topicUpSqlBytes() ([]byte, error) { + return bindataRead( + __1558084410_add_topicUpSql, + "1558084410_add_topic.up.sql", + ) +} + +func _1558084410_add_topicUpSql() (*asset, error) { + bytes, err := _1558084410_add_topicUpSqlBytes() + 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)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _staticGo = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x54\xcc\x41\x6a\x03\x31\x0c\x46\xe1\xbd\x4f\xf1\x2f\x5b\xe8\x58\xfb\x9e\xa0\x94\x16\x0a\xcd\x05\x64\x8f\x90\xc5\x30\xf6\x60\x29\x21\xc7\xcf\x26\x21\x64\xf9\xe0\xf1\x11\xe1\x8f\xeb\xc6\x2a\xf0\xe0\xb0\x0a\xd9\x8b\xac\xfe\xa8\xb7\xef\xff\x0f\x7c\x9d\x7e\x7f\xde\x31\xc5\xc7\x79\x56\x71\x4c\xd3\x16\xb0\x1e\x03\xd1\x04\xc5\x3a\x4f\x13\x4f\xc7\x8b\x94\x12\x91\x8e\x4f\x95\x2e\x93\x43\xa0\x63\x29\xd6\x57\x0e\xc6\x72\x6c\x8a\xdd\x74\x72\xd8\xe8\x8e\x65\x20\x67\xca\x99\x5c\xe6\xc5\xaa\x38\x79\x6b\x72\x0d\xaa\x8d\x83\xd6\x42\xcf\x97\xee\x46\xd6\x81\x9c\x6e\x01\x00\x00\xff\xff\x6c\x21\xbf\x7a\xbf\x00\x00\x00") func staticGoBytes() ([]byte, error) { return bindataRead( @@ -252,8 +293,8 @@ func staticGo() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "static.go", size: 188, mode: os.FileMode(0644), modTime: time.Unix(1560158346, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x13, 0x54, 0x14, 0x7a, 0xa7, 0x6b, 0x18, 0xbb, 0xa6, 0x2e, 0x5c, 0x9f, 0x2b, 0xa5, 0xb0, 0x48, 0xfb, 0x61, 0xd7, 0x30, 0xe5, 0xdf, 0xaf, 0xcb, 0x94, 0x10, 0x79, 0xd3, 0x7b, 0xbd, 0x1f, 0xfe}} + info := bindataFileInfo{name: "static.go", size: 191, mode: os.FileMode(420), modTime: time.Unix(1560418030, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -261,8 +302,8 @@ func staticGo() (*asset, error) { // It returns an error if the asset could not be found or // could not be loaded. func Asset(name string) ([]byte, error) { - canonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[canonicalName]; ok { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) @@ -272,12 +313,6 @@ func Asset(name string) ([]byte, error) { return nil, fmt.Errorf("Asset %s not found", name) } -// AssetString returns the asset contents as a string (instead of a []byte). -func AssetString(name string) (string, error) { - data, err := Asset(name) - return string(data), err -} - // MustAsset is like Asset but panics when Asset would return an error. // It simplifies safe initialization of global variables. func MustAsset(name string) []byte { @@ -289,18 +324,12 @@ func MustAsset(name string) []byte { return a } -// MustAssetString is like AssetString but panics when Asset would return an -// error. It simplifies safe initialization of global variables. -func MustAssetString(name string) string { - return string(MustAsset(name)) -} - // AssetInfo loads and returns the asset info for the given name. // It returns an error if the asset could not be found or // could not be loaded. func AssetInfo(name string) (os.FileInfo, error) { - canonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[canonicalName]; ok { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) @@ -310,33 +339,6 @@ func AssetInfo(name string) (os.FileInfo, error) { return nil, fmt.Errorf("AssetInfo %s not found", name) } -// AssetDigest returns the digest of the file with the given name. It returns an -// error if the asset could not be found or the digest could not be loaded. -func AssetDigest(name string) ([sha256.Size]byte, error) { - canonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[canonicalName]; ok { - a, err := f() - if err != nil { - return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s can't read by error: %v", name, err) - } - return a.digest, nil - } - return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s not found", name) -} - -// Digests returns a map of all known files and their checksums. -func Digests() (map[string][sha256.Size]byte, error) { - mp := make(map[string][sha256.Size]byte, len(_bindata)) - for name := range _bindata { - a, err := _bindata[name]() - if err != nil { - return nil, err - } - mp[name] = a.digest - } - return mp, nil -} - // AssetNames returns the names of the assets. func AssetNames() []string { names := make([]string, 0, len(_bindata)) @@ -349,21 +351,15 @@ func AssetNames() []string { // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "1536754952_initial_schema.down.sql": _1536754952_initial_schemaDownSql, - "1536754952_initial_schema.up.sql": _1536754952_initial_schemaUpSql, - "1539249977_update_ratchet_info.down.sql": _1539249977_update_ratchet_infoDownSql, - "1539249977_update_ratchet_info.up.sql": _1539249977_update_ratchet_infoUpSql, - "1540715431_add_version.down.sql": _1540715431_add_versionDownSql, - "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, "static.go": staticGo, } @@ -376,15 +372,15 @@ var _bindata = map[string]func() (*asset, error){ // img/ // a.png // b.png -// then AssetDir("data") would return []string{"foo.txt", "img"}, -// AssetDir("data/img") would return []string{"a.png", "b.png"}, -// AssetDir("foo.txt") and AssetDir("notexist") would return an error, and +// then AssetDir("data") would return []string{"foo.txt", "img"} +// AssetDir("data/img") would return []string{"a.png", "b.png"} +// AssetDir("foo.txt") and AssetDir("notexist") would return an error // AssetDir("") will return []string{"data"}. func AssetDir(name string) ([]string, error) { node := _bintree if len(name) != 0 { - canonicalName := strings.Replace(name, "\\", "/", -1) - pathList := strings.Split(canonicalName, "/") + cannonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(cannonicalName, "/") for _, p := range pathList { node = node.Children[p] if node == nil { @@ -406,20 +402,21 @@ type bintree struct { Func func() (*asset, error) Children map[string]*bintree } - var _bintree = &bintree{nil, map[string]*bintree{ - "1536754952_initial_schema.down.sql": &bintree{_1536754952_initial_schemaDownSql, map[string]*bintree{}}, - "1536754952_initial_schema.up.sql": &bintree{_1536754952_initial_schemaUpSql, map[string]*bintree{}}, + "1536754952_initial_schema.down.sql": &bintree{_1536754952_initial_schemaDownSql, map[string]*bintree{}}, + "1536754952_initial_schema.up.sql": &bintree{_1536754952_initial_schemaUpSql, map[string]*bintree{}}, "1539249977_update_ratchet_info.down.sql": &bintree{_1539249977_update_ratchet_infoDownSql, map[string]*bintree{}}, - "1539249977_update_ratchet_info.up.sql": &bintree{_1539249977_update_ratchet_infoUpSql, map[string]*bintree{}}, - "1540715431_add_version.down.sql": &bintree{_1540715431_add_versionDownSql, 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{}}, - "static.go": &bintree{staticGo, map[string]*bintree{}}, + "1539249977_update_ratchet_info.up.sql": &bintree{_1539249977_update_ratchet_infoUpSql, map[string]*bintree{}}, + "1540715431_add_version.down.sql": &bintree{_1540715431_add_versionDownSql, 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{}}, + "static.go": &bintree{staticGo, map[string]*bintree{}}, }} -// RestoreAsset restores an asset under the given directory. +// RestoreAsset restores an asset under the given directory func RestoreAsset(dir, name string) error { data, err := Asset(name) if err != nil { @@ -437,10 +434,14 @@ func RestoreAsset(dir, name string) error { if err != nil { return err } - return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) + err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) + if err != nil { + return err + } + return nil } -// RestoreAssets restores an asset under the given directory recursively. +// RestoreAssets restores an asset under the given directory recursively func RestoreAssets(dir, name string) error { children, err := AssetDir(name) // File @@ -458,6 +459,7 @@ func RestoreAssets(dir, name string) error { } func _filePath(dir, name string) string { - canonicalName := strings.Replace(name, "\\", "/", -1) - return filepath.Join(append([]string{dir}, strings.Split(canonicalName, "/")...)...) + cannonicalName := strings.Replace(name, "\\", "/", -1) + return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) } + diff --git a/services/shhext/chat/encryption.pb.go b/services/shhext/chat/encryption.pb.go index adbe3684d..876c91295 100644 --- a/services/shhext/chat/encryption.pb.go +++ b/services/shhext/chat/encryption.pb.go @@ -18,7 +18,7 @@ var _ = math.Inf // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. -const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package type SignedPreKey struct { SignedPreKey []byte `protobuf:"bytes,1,opt,name=signed_pre_key,json=signedPreKey,proto3" json:"signed_pre_key,omitempty"` @@ -416,7 +416,9 @@ type ProtocolMessage struct { // One to one message, encrypted, indexed by installation_id DirectMessage map[string]*DirectMessageProtocol `protobuf:"bytes,101,rep,name=direct_message,json=directMessage,proto3" json:"direct_message,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // Public chats, not encrypted - PublicMessage []byte `protobuf:"bytes,102,opt,name=public_message,json=publicMessage,proto3" json:"public_message,omitempty"` + PublicMessage []byte `protobuf:"bytes,102,opt,name=public_message,json=publicMessage,proto3" json:"public_message,omitempty"` + // Version of the protocol + Version uint32 `protobuf:"varint,103,opt,name=version,proto3" json:"version,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -482,6 +484,13 @@ func (m *ProtocolMessage) GetPublicMessage() []byte { return nil } +func (m *ProtocolMessage) GetVersion() uint32 { + if m != nil { + return m.Version + } + return 0 +} + func init() { proto.RegisterType((*SignedPreKey)(nil), "chat.SignedPreKey") proto.RegisterType((*Bundle)(nil), "chat.Bundle") @@ -498,40 +507,40 @@ func init() { func init() { proto.RegisterFile("encryption.proto", fileDescriptor_8293a649ce9418c6) } var fileDescriptor_8293a649ce9418c6 = []byte{ - // 548 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x54, 0x51, 0x8b, 0xd3, 0x40, - 0x10, 0x26, 0x49, 0xef, 0xae, 0x9d, 0xe6, 0xd2, 0xb2, 0xa2, 0x84, 0x7a, 0x60, 0x09, 0xa7, 0x06, - 0x84, 0xc2, 0xb5, 0x3e, 0x88, 0x8f, 0x5a, 0xb1, 0x9e, 0xa8, 0xc7, 0xea, 0x83, 0x2f, 0x12, 0xb6, - 0xdd, 0xf5, 0x6e, 0x31, 0xdd, 0x84, 0xdd, 0x6d, 0xa1, 0x7f, 0xce, 0xbf, 0xe2, 0x4f, 0x51, 0xb2, - 0x9b, 0xb4, 0xdb, 0xde, 0x1d, 0xf8, 0xd6, 0x99, 0xf9, 0xf6, 0x9b, 0x6f, 0xbe, 0xe9, 0x04, 0xfa, - 0x4c, 0x2c, 0xe4, 0xa6, 0xd4, 0xbc, 0x10, 0xa3, 0x52, 0x16, 0xba, 0x40, 0xad, 0xc5, 0x0d, 0xd1, - 0xc9, 0x67, 0x08, 0xbf, 0xf2, 0x6b, 0xc1, 0xe8, 0x95, 0x64, 0x1f, 0xd9, 0x06, 0x9d, 0x43, 0xa4, - 0x4c, 0x9c, 0x95, 0x92, 0x65, 0xbf, 0xd8, 0x26, 0xf6, 0x86, 0x5e, 0x1a, 0xe2, 0x50, 0xb9, 0xa8, - 0x18, 0x4e, 0xd6, 0x4c, 0x2a, 0x5e, 0x88, 0xd8, 0x1f, 0x7a, 0xe9, 0x29, 0x6e, 0xc2, 0xe4, 0xaf, - 0x07, 0xc7, 0x6f, 0x56, 0x82, 0xe6, 0x0c, 0x0d, 0xa0, 0xcd, 0x29, 0x13, 0x9a, 0xeb, 0x86, 0x64, - 0x1b, 0xa3, 0xf7, 0xd0, 0xdb, 0x6f, 0xa3, 0x62, 0x7f, 0x18, 0xa4, 0xdd, 0xf1, 0x93, 0x51, 0x25, - 0x6b, 0x64, 0x29, 0x46, 0xae, 0x34, 0xf5, 0x4e, 0x68, 0xb9, 0xc1, 0xa7, 0xae, 0x10, 0x85, 0xce, - 0xa0, 0x53, 0x25, 0x88, 0x5e, 0x49, 0x16, 0xb7, 0x4c, 0x97, 0x5d, 0xa2, 0xaa, 0x6a, 0xbe, 0x64, - 0x4a, 0x93, 0x65, 0x19, 0x1f, 0x0d, 0xbd, 0x34, 0xc0, 0xbb, 0xc4, 0xe0, 0x1b, 0xa0, 0xdb, 0x0d, - 0x50, 0x1f, 0x82, 0x66, 0xec, 0x0e, 0xae, 0x7e, 0xa2, 0x14, 0x8e, 0xd6, 0x24, 0x5f, 0x31, 0x33, - 0x6b, 0x77, 0x8c, 0xac, 0x44, 0xf7, 0x29, 0xb6, 0x80, 0xd7, 0xfe, 0x2b, 0x2f, 0x91, 0xd0, 0xb3, - 0xea, 0xdf, 0x16, 0x42, 0x13, 0x2e, 0x98, 0x44, 0xe7, 0x70, 0x3c, 0x37, 0x29, 0xc3, 0xda, 0x1d, - 0x87, 0xee, 0x90, 0xb8, 0xae, 0xa1, 0x09, 0x3c, 0x2a, 0x25, 0x5f, 0x13, 0xcd, 0xb2, 0x83, 0x15, - 0xf8, 0x66, 0xae, 0x07, 0x75, 0xd5, 0x6d, 0x7c, 0xd9, 0x6a, 0x07, 0xfd, 0x56, 0x72, 0x09, 0xed, - 0x29, 0x9e, 0x31, 0x42, 0x99, 0x74, 0xf5, 0x87, 0x56, 0x7f, 0x08, 0x5e, 0xb3, 0x27, 0x4f, 0xa0, - 0x08, 0xfc, 0x52, 0xc4, 0x81, 0x09, 0xfd, 0xd2, 0xc4, 0x9c, 0xd6, 0xd6, 0xf9, 0x9c, 0x26, 0x67, - 0xd0, 0x9e, 0xce, 0xee, 0xe3, 0x4a, 0x5e, 0x02, 0x7c, 0x9f, 0xdc, 0x5f, 0x3f, 0x64, 0xab, 0xf5, - 0xfd, 0xf6, 0xe0, 0xe1, 0x94, 0x4b, 0xb6, 0xd0, 0x9f, 0x98, 0x52, 0xe4, 0x9a, 0x5d, 0x55, 0x7f, - 0xc1, 0x45, 0x91, 0xa3, 0x0b, 0xe8, 0x56, 0x7c, 0xd9, 0x8d, 0x21, 0xac, 0xfd, 0xe9, 0x5b, 0x7f, - 0x76, 0x8d, 0xb0, 0xdb, 0xf4, 0x05, 0x74, 0xa6, 0xb8, 0x79, 0x60, 0x57, 0x12, 0xd9, 0x07, 0x8d, - 0x07, 0x78, 0xe7, 0x46, 0x05, 0xde, 0xb2, 0xb3, 0x3d, 0xf0, 0x6c, 0x0b, 0x6e, 0x98, 0x63, 0x38, - 0x29, 0xc9, 0x26, 0x2f, 0x08, 0x35, 0xfe, 0x84, 0xb8, 0x09, 0x93, 0x3f, 0x3e, 0xf4, 0x1a, 0xcd, - 0xf5, 0x08, 0xff, 0xb9, 0xd5, 0xe7, 0xd0, 0xe3, 0x42, 0x69, 0x92, 0xe7, 0xa4, 0x3a, 0xbe, 0x8c, - 0x53, 0xa3, 0xb9, 0x83, 0x23, 0x37, 0xfd, 0x81, 0xa2, 0x67, 0x70, 0x62, 0x9f, 0xa8, 0x38, 0x30, - 0xa7, 0xb0, 0xcf, 0xd7, 0x14, 0xd1, 0x17, 0x88, 0xa8, 0xb1, 0x32, 0x5b, 0x5a, 0x21, 0x31, 0x33, - 0xf0, 0xd4, 0xc2, 0x0f, 0x54, 0x8e, 0xf6, 0x6c, 0xaf, 0x4f, 0x88, 0xba, 0x39, 0xf4, 0x14, 0xa2, - 0x72, 0x35, 0xcf, 0xf9, 0x62, 0x4b, 0xf8, 0xd3, 0x0c, 0x7f, 0x6a, 0xb3, 0x35, 0x6c, 0xf0, 0x03, - 0xd0, 0x6d, 0xae, 0x3b, 0xae, 0xe5, 0x62, 0xff, 0x5a, 0x1e, 0xd7, 0x6e, 0xdf, 0xb5, 0x7d, 0xe7, - 0x6c, 0xe6, 0xc7, 0xe6, 0xab, 0x34, 0xf9, 0x17, 0x00, 0x00, 0xff, 0xff, 0x54, 0xd8, 0xdf, 0x09, - 0xa9, 0x04, 0x00, 0x00, + // 553 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x54, 0x5d, 0x8b, 0xd3, 0x40, + 0x14, 0x25, 0x49, 0x77, 0xb7, 0xbd, 0x4d, 0xd3, 0x32, 0xa2, 0x84, 0xba, 0x60, 0x09, 0xab, 0x06, + 0x84, 0xc2, 0xb6, 0x3e, 0x88, 0x8f, 0x5a, 0xb1, 0xae, 0xa8, 0xcb, 0xe8, 0x83, 0x2f, 0x12, 0xa6, + 0xcd, 0xd8, 0x1d, 0x4c, 0x27, 0x61, 0x32, 0x2d, 0xf4, 0xcf, 0xf9, 0xbf, 0x7c, 0x52, 0x32, 0x93, + 0x69, 0x27, 0xdd, 0x5d, 0xf0, 0xad, 0xf7, 0x63, 0xce, 0x3d, 0xf7, 0xdc, 0x9c, 0xc2, 0x80, 0xf2, + 0xa5, 0xd8, 0x15, 0x92, 0xe5, 0x7c, 0x5c, 0x88, 0x5c, 0xe6, 0xa8, 0xb5, 0xbc, 0x21, 0x32, 0xfa, + 0x0c, 0xfe, 0x57, 0xb6, 0xe2, 0x34, 0xbd, 0x16, 0xf4, 0x23, 0xdd, 0xa1, 0x0b, 0x08, 0x4a, 0x15, + 0x27, 0x85, 0xa0, 0xc9, 0x2f, 0xba, 0x0b, 0x9d, 0x91, 0x13, 0xfb, 0xd8, 0x2f, 0xed, 0xae, 0x10, + 0xce, 0xb6, 0x54, 0x94, 0x2c, 0xe7, 0xa1, 0x3b, 0x72, 0xe2, 0x1e, 0x36, 0x61, 0xf4, 0xd7, 0x81, + 0xd3, 0x37, 0x1b, 0x9e, 0x66, 0x14, 0x0d, 0xa1, 0xcd, 0x52, 0xca, 0x25, 0x93, 0x06, 0x64, 0x1f, + 0xa3, 0xf7, 0xd0, 0x6f, 0x8e, 0x29, 0x43, 0x77, 0xe4, 0xc5, 0xdd, 0xc9, 0x93, 0x71, 0x45, 0x6b, + 0xac, 0x21, 0xc6, 0x36, 0xb5, 0xf2, 0x1d, 0x97, 0x62, 0x87, 0x7b, 0x36, 0x91, 0x12, 0x9d, 0x43, + 0xa7, 0x4a, 0x10, 0xb9, 0x11, 0x34, 0x6c, 0xa9, 0x29, 0x87, 0x44, 0x55, 0x95, 0x6c, 0x4d, 0x4b, + 0x49, 0xd6, 0x45, 0x78, 0x32, 0x72, 0x62, 0x0f, 0x1f, 0x12, 0xc3, 0x6f, 0x80, 0x6e, 0x0f, 0x40, + 0x03, 0xf0, 0xcc, 0xda, 0x1d, 0x5c, 0xfd, 0x44, 0x31, 0x9c, 0x6c, 0x49, 0xb6, 0xa1, 0x6a, 0xd7, + 0xee, 0x04, 0x69, 0x8a, 0xf6, 0x53, 0xac, 0x1b, 0x5e, 0xbb, 0xaf, 0x9c, 0x48, 0x40, 0x5f, 0xb3, + 0x7f, 0x9b, 0x73, 0x49, 0x18, 0xa7, 0x02, 0x5d, 0xc0, 0xe9, 0x42, 0xa5, 0x14, 0x6a, 0x77, 0xe2, + 0xdb, 0x4b, 0xe2, 0xba, 0x86, 0xa6, 0xf0, 0xa8, 0x10, 0x6c, 0x4b, 0x24, 0x4d, 0x8e, 0x4e, 0xe0, + 0xaa, 0xbd, 0x1e, 0xd4, 0x55, 0x7b, 0xf0, 0x55, 0xab, 0xed, 0x0d, 0x5a, 0xd1, 0x15, 0xb4, 0x67, + 0x78, 0x4e, 0x49, 0x4a, 0x85, 0xcd, 0xdf, 0xd7, 0xfc, 0x7d, 0x70, 0xcc, 0x9d, 0x1c, 0x8e, 0x02, + 0x70, 0x0b, 0x1e, 0x7a, 0x2a, 0x74, 0x0b, 0x15, 0xb3, 0xb4, 0x96, 0xce, 0x65, 0x69, 0x74, 0x0e, + 0xed, 0xd9, 0xfc, 0x3e, 0xac, 0xe8, 0x25, 0xc0, 0xf7, 0xe9, 0xfd, 0xf5, 0x63, 0xb4, 0x9a, 0xdf, + 0x6f, 0x07, 0x1e, 0xce, 0x98, 0xa0, 0x4b, 0xf9, 0x89, 0x96, 0x25, 0x59, 0xd1, 0xeb, 0xea, 0x13, + 0x5c, 0xe6, 0x19, 0xba, 0x84, 0x6e, 0x85, 0x97, 0xdc, 0x28, 0xc0, 0x5a, 0x9f, 0x81, 0xd6, 0xe7, + 0x30, 0x08, 0xdb, 0x43, 0x5f, 0x40, 0x67, 0x86, 0xcd, 0x03, 0x7d, 0x92, 0x40, 0x3f, 0x30, 0x1a, + 0xe0, 0x83, 0x1a, 0x55, 0xf3, 0x1e, 0x9d, 0x36, 0x9a, 0xe7, 0xfb, 0x66, 0x83, 0x1c, 0xc2, 0x59, + 0x41, 0x76, 0x59, 0x4e, 0x52, 0xa5, 0x8f, 0x8f, 0x4d, 0x18, 0xfd, 0x71, 0xa1, 0x6f, 0x38, 0xd7, + 0x2b, 0xfc, 0xe7, 0x55, 0x9f, 0x43, 0x9f, 0xf1, 0x52, 0x92, 0x2c, 0x23, 0x95, 0xf9, 0x12, 0x96, + 0x2a, 0xce, 0x1d, 0x1c, 0xd8, 0xe9, 0x0f, 0x29, 0x7a, 0x06, 0x67, 0xfa, 0x49, 0x19, 0x7a, 0xca, + 0x0a, 0x4d, 0x3c, 0x53, 0x44, 0x5f, 0x20, 0x48, 0x95, 0x94, 0xc9, 0x5a, 0x13, 0x09, 0xa9, 0x6a, + 0x8f, 0x75, 0xfb, 0x11, 0xcb, 0x71, 0x43, 0xf6, 0xda, 0x42, 0xa9, 0x9d, 0x43, 0x4f, 0x21, 0x28, + 0x36, 0x8b, 0x8c, 0x2d, 0xf7, 0x80, 0x3f, 0xd5, 0xf2, 0x3d, 0x9d, 0x35, 0x6d, 0x96, 0xe7, 0x57, + 0x0d, 0xcf, 0x0f, 0x7f, 0x00, 0xba, 0x3d, 0xe5, 0x0e, 0x1f, 0x5d, 0x36, 0x7d, 0xf4, 0xb8, 0xbe, + 0xc3, 0x5d, 0xdf, 0x85, 0x65, 0xa8, 0xc5, 0xa9, 0xfa, 0xbf, 0x9a, 0xfe, 0x0b, 0x00, 0x00, 0xff, + 0xff, 0x53, 0xcb, 0xc9, 0xb7, 0xc3, 0x04, 0x00, 0x00, } diff --git a/services/shhext/chat/encryption.proto b/services/shhext/chat/encryption.proto index 923aa662c..5ac619119 100644 --- a/services/shhext/chat/encryption.proto +++ b/services/shhext/chat/encryption.proto @@ -78,4 +78,6 @@ message ProtocolMessage { // Public chats, not encrypted bytes public_message = 102; + // Version of the protocol + uint32 version = 103; } diff --git a/services/shhext/chat/protocol.go b/services/shhext/chat/protocol.go index 45a2f21a9..1a763a8cb 100644 --- a/services/shhext/chat/protocol.go +++ b/services/shhext/chat/protocol.go @@ -5,28 +5,35 @@ import ( "errors" "github.com/ethereum/go-ethereum/log" - "github.com/golang/protobuf/proto" + "github.com/status-im/status-go/services/shhext/chat/topic" ) +const protocolCurrentVersion = 1 +const topicNegotiationVersion = 1 + type ProtocolService struct { log log.Logger encryption *EncryptionService + topic *topic.Service addedBundlesHandler func([]IdentityAndIDPair) + onNewTopicHandler func([]*topic.Secret) Enabled bool } var ErrNotProtocolMessage = errors.New("Not a protocol message") // NewProtocolService creates a new ProtocolService instance -func NewProtocolService(encryption *EncryptionService, addedBundlesHandler func([]IdentityAndIDPair)) *ProtocolService { +func NewProtocolService(encryption *EncryptionService, topic *topic.Service, addedBundlesHandler func([]IdentityAndIDPair), onNewTopicHandler func([]*topic.Secret)) *ProtocolService { return &ProtocolService{ log: log.New("package", "status-go/services/sshext.chat"), encryption: encryption, + topic: topic, addedBundlesHandler: addedBundlesHandler, + onNewTopicHandler: onNewTopicHandler, } } -func (p *ProtocolService) addBundleAndMarshal(myIdentityKey *ecdsa.PrivateKey, msg *ProtocolMessage, sendSingle bool) ([]byte, error) { +func (p *ProtocolService) addBundle(myIdentityKey *ecdsa.PrivateKey, msg *ProtocolMessage, sendSingle bool) (*ProtocolMessage, error) { // Get a bundle bundle, err := p.encryption.CreateBundle(myIdentityKey) if err != nil { @@ -42,61 +49,93 @@ func (p *ProtocolService) addBundleAndMarshal(myIdentityKey *ecdsa.PrivateKey, m msg.Bundles = []*Bundle{bundle} } - // marshal for sending to wire - marshaledMessage, err := proto.Marshal(msg) - if err != nil { - p.log.Error("encryption-service", "error marshaling message", err) - return nil, err - } - - return marshaledMessage, nil + 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) ([]byte, error) { +func (p *ProtocolService) BuildPublicMessage(myIdentityKey *ecdsa.PrivateKey, payload []byte) (*ProtocolMessage, error) { // Build message not encrypted protocolMessage := &ProtocolMessage{ InstallationId: p.encryption.config.InstallationID, PublicMessage: payload, + Version: protocolCurrentVersion, } - return p.addBundleAndMarshal(myIdentityKey, protocolMessage, false) + return p.addBundle(myIdentityKey, protocolMessage, false) } -// BuildDirectMessage marshals a 1:1 chat message 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) ([]byte, error) { +// 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) { // Encrypt payload encryptionResponse, err := p.encryption.EncryptPayload(publicKey, myIdentityKey, payload) if err != nil { p.log.Error("encryption-service", "error encrypting payload", err) - return nil, err + return nil, nil, err } // Build message protocolMessage := &ProtocolMessage{ InstallationId: p.encryption.config.InstallationID, DirectMessage: encryptionResponse, + Version: protocolCurrentVersion, } - return p.addBundleAndMarshal(myIdentityKey, protocolMessage, true) + msg, err := p.addBundle(myIdentityKey, protocolMessage, true) + if err != nil { + return nil, 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 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 + } + } + + // Call handler + if sharedSecret != nil { + p.onNewTopicHandler([]*topic.Secret{sharedSecret}) + } + + if agreed { + return msg, sharedSecret.Key, nil + } else { + return msg, nil, 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) ([]byte, error) { +func (p *ProtocolService) BuildDHMessage(myIdentityKey *ecdsa.PrivateKey, destination *ecdsa.PublicKey, payload []byte) (*ProtocolMessage, []byte, error) { // Encrypt payload encryptionResponse, err := p.encryption.EncryptPayloadWithDH(destination, payload) if err != nil { p.log.Error("encryption-service", "error encrypting payload", err) - return nil, err + return nil, nil, err } // Build message protocolMessage := &ProtocolMessage{ InstallationId: p.encryption.config.InstallationID, DirectMessage: encryptionResponse, + Version: protocolCurrentVersion, } - return p.addBundleAndMarshal(myIdentityKey, protocolMessage, true) + msg, err := p.addBundle(myIdentityKey, protocolMessage, true) + if err != nil { + return nil, nil, err + } + + return msg, nil, nil } // ProcessPublicBundle processes a received X3DH bundle. @@ -130,18 +169,11 @@ 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, payload []byte, messageID []byte) ([]byte, error) { +func (p *ProtocolService) HandleMessage(myIdentityKey *ecdsa.PrivateKey, theirPublicKey *ecdsa.PublicKey, protocolMessage *ProtocolMessage, messageID []byte) ([]byte, error) { if p.encryption == nil { return nil, errors.New("encryption service not initialized") } - // Unmarshal message - protocolMessage := &ProtocolMessage{} - - if err := proto.Unmarshal(payload, protocolMessage); err != nil { - return nil, ErrNotProtocolMessage - } - // Process bundle, deprecated, here for backward compatibility if bundle := protocolMessage.GetBundle(); bundle != nil { // Should we stop processing if the bundle cannot be verified? @@ -177,6 +209,18 @@ func (p *ProtocolService) HandleMessage(myIdentityKey *ecdsa.PrivateKey, theirPu return nil, err } + p.log.Info("Checking version") + // Handle protocol negotiation for compatible clients + if protocolMessage.Version >= topicNegotiationVersion { + p.log.Info("Version greater than 1 negotianting") + sharedSecret, err := p.topic.Receive(myIdentityKey, theirPublicKey, protocolMessage.GetInstallationId()) + if err != nil { + return nil, err + } + + p.onNewTopicHandler([]*topic.Secret{sharedSecret}) + + } return message, nil } diff --git a/services/shhext/chat/protocol_test.go b/services/shhext/chat/protocol_test.go index 0b245ec44..b17becff6 100644 --- a/services/shhext/chat/protocol_test.go +++ b/services/shhext/chat/protocol_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/crypto" - "github.com/golang/protobuf/proto" + "github.com/status-im/status-go/services/shhext/chat/topic" "github.com/stretchr/testify/suite" ) @@ -39,31 +39,36 @@ func (s *ProtocolServiceTestSuite) SetupTest() { } addedBundlesHandler := func(addedBundles []IdentityAndIDPair) {} + onNewTopicHandler := func(topic [][]byte) {} + + s.alice = NewProtocolService( + NewEncryptionService(alicePersistence, DefaultEncryptionServiceConfig("1")), + topic.NewService(alicePersistence.GetTopicStorage()), + addedBundlesHandler, + onNewTopicHandler, + ) + + s.bob = NewProtocolService( + NewEncryptionService(bobPersistence, DefaultEncryptionServiceConfig("2")), + topic.NewService(bobPersistence.GetTopicStorage()), + addedBundlesHandler, + onNewTopicHandler, + ) - s.alice = NewProtocolService(NewEncryptionService(alicePersistence, DefaultEncryptionServiceConfig("1")), addedBundlesHandler) - s.bob = NewProtocolService(NewEncryptionService(bobPersistence, DefaultEncryptionServiceConfig("2")), addedBundlesHandler) } func (s *ProtocolServiceTestSuite) TestBuildPublicMessage() { aliceKey, err := crypto.GenerateKey() s.NoError(err) - payload, err := proto.Marshal(&ChatMessagePayload{ - Content: "Test content", - ClockValue: 1, - ContentType: "a", - MessageType: "some type", - }) + payload := []byte("test") s.NoError(err) - marshaledMsg, err := s.alice.BuildPublicMessage(aliceKey, payload) + msg, err := s.alice.BuildPublicMessage(aliceKey, payload) s.NoError(err) - s.NotNil(marshaledMsg, "It creates a message") + s.NotNil(msg, "It creates a message") - unmarshaledMsg := &ProtocolMessage{} - err = proto.Unmarshal(marshaledMsg, unmarshaledMsg) - s.NoError(err) - s.NotNilf(unmarshaledMsg.GetBundles(), "It adds a bundle to the message") + s.NotNilf(msg.GetBundles(), "It adds a bundle to the message") } func (s *ProtocolServiceTestSuite) TestBuildDirectMessage() { @@ -72,24 +77,15 @@ func (s *ProtocolServiceTestSuite) TestBuildDirectMessage() { aliceKey, err := crypto.GenerateKey() s.NoError(err) - payload, err := proto.Marshal(&ChatMessagePayload{ - Content: "Test content", - ClockValue: 1, - ContentType: "a", - MessageType: "some type", - }) - s.NoError(err) + payload := []byte("test") - marshaledMsg, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, payload) + msg, _, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, payload) s.NoError(err) - s.NotNil(marshaledMsg, "It creates a message") + s.NotNil(msg, "It creates a message") - unmarshaledMsg := &ProtocolMessage{} - err = proto.Unmarshal(marshaledMsg, unmarshaledMsg) - s.NoError(err) - s.NotNilf(unmarshaledMsg.GetBundle(), "It adds a bundle to the message") + s.NotNilf(msg.GetBundle(), "It adds a bundle to the message") - directMessage := unmarshaledMsg.GetDirectMessage() + directMessage := msg.GetDirectMessage() s.NotNilf(directMessage, "It sets the direct message") encryptedPayload := directMessage["none"].GetPayload() @@ -104,18 +100,10 @@ func (s *ProtocolServiceTestSuite) TestBuildAndReadDirectMessage() { aliceKey, err := crypto.GenerateKey() s.NoError(err) - payload := ChatMessagePayload{ - Content: "Test content", - ClockValue: 1, - ContentType: "a", - MessageType: "some type", - } - - marshaledPayload, err := proto.Marshal(&payload) - s.NoError(err) + payload := []byte("test") // Message is sent with DH - marshaledMsg, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, marshaledPayload) + marshaledMsg, _, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, payload) s.NoError(err) @@ -125,9 +113,6 @@ func (s *ProtocolServiceTestSuite) TestBuildAndReadDirectMessage() { s.NotNil(unmarshaledMsg) - recoveredPayload := ChatMessagePayload{} - err = proto.Unmarshal(unmarshaledMsg, &recoveredPayload) - - s.NoError(err) - s.Equalf(proto.Equal(&payload, &recoveredPayload), true, "It successfully unmarshal the decrypted message") + recoveredPayload := []byte("test") + s.Equalf(payload, recoveredPayload, "It successfully unmarshal the decrypted message") } diff --git a/services/shhext/chat/sql_lite_persistence.go b/services/shhext/chat/sql_lite_persistence.go index 786492139..9659efa6f 100644 --- a/services/shhext/chat/sql_lite_persistence.go +++ b/services/shhext/chat/sql_lite_persistence.go @@ -3,42 +3,28 @@ package chat import ( "crypto/ecdsa" "database/sql" - "fmt" - "os" "strings" "github.com/ethereum/go-ethereum/crypto" - sqlite "github.com/mutecomm/go-sqlcipher" // We require go sqlcipher that overrides default implementation dr "github.com/status-im/doubleratchet" "github.com/status-im/migrate/v4" "github.com/status-im/migrate/v4/database/sqlcipher" "github.com/status-im/migrate/v4/source/go_bindata" ecrypto "github.com/status-im/status-go/services/shhext/chat/crypto" - "github.com/status-im/status-go/services/shhext/chat/migrations" + appDB "github.com/status-im/status-go/services/shhext/chat/db" + "github.com/status-im/status-go/services/shhext/chat/topic" ) // A safe max number of rows const maxNumberOfRows = 100000000 -// The default number of kdf iterations in sqlcipher (from version 3.0.0) -// https://github.com/sqlcipher/sqlcipher/blob/fda4c68bb474da7e955be07a2b807bda1bb19bd2/CHANGELOG.md#300---2013-11-05 -// https://www.zetetic.net/sqlcipher/sqlcipher-api/#kdf_iter -const defaultKdfIterationsNumber = 64000 - -// The reduced number of kdf iterations (for performance reasons) which is -// currently used for derivation of the database key -// https://github.com/status-im/status-go/pull/1343 -// https://notes.status.im/i8Y_l7ccTiOYq09HVgoFwA -const kdfIterationsNumber = 3200 - -const exportDB = "SELECT sqlcipher_export('newdb')" - // 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 } // SQLLiteKeysStorage represents a keys persistence service tied to an SQLite database @@ -63,187 +49,11 @@ func NewSQLLitePersistence(path string, key string) (*SQLLitePersistence, error) s.sessionStorage = NewSQLLiteSessionStorage(s.db) + s.topicStorage = topic.NewSQLLitePersistence(s.db) + return s, nil } -func MigrateDBFile(oldPath string, newPath string, oldKey string, newKey string) error { - _, err := os.Stat(oldPath) - - // No files, nothing to do - if os.IsNotExist(err) { - return nil - } - - // Any other error, throws - if err != nil { - return err - } - - if err := os.Rename(oldPath, newPath); err != nil { - return err - } - - db, err := openDB(newPath, oldKey, defaultKdfIterationsNumber) - if err != nil { - return err - } - - keyString := fmt.Sprintf("PRAGMA rekey = '%s'", newKey) - - if _, err = db.Exec(keyString); err != nil { - return err - } - - return nil - -} - -// MigrateDBKeyKdfIterations changes the number of kdf iterations executed -// during the database key derivation. This change is necessary because -// of performance reasons. -// https://github.com/status-im/status-go/pull/1343 -// `sqlcipher_export` is used for migration, check out this link for details: -// https://www.zetetic.net/sqlcipher/sqlcipher-api/#sqlcipher_export -func MigrateDBKeyKdfIterations(oldPath string, newPath string, key string) error { - _, err := os.Stat(oldPath) - - // No files, nothing to do - if os.IsNotExist(err) { - return nil - } - - // Any other error, throws - if err != nil { - return err - } - - isEncrypted, err := sqlite.IsEncrypted(oldPath) - if err != nil { - return err - } - - // Nothing to do, move db to the next migration - if !isEncrypted { - return os.Rename(oldPath, newPath) - } - - db, err := openDB(oldPath, key, defaultKdfIterationsNumber) - if err != nil { - return err - } - - attach := fmt.Sprintf( - "ATTACH DATABASE '%s' AS newdb KEY '%s'", - newPath, - key) - - if _, err = db.Exec(attach); err != nil { - return err - } - - changeKdfIter := fmt.Sprintf( - "PRAGMA newdb.kdf_iter = %d", - kdfIterationsNumber) - - if _, err = db.Exec(changeKdfIter); err != nil { - return err - } - - if _, err = db.Exec(exportDB); err != nil { - return err - } - - if err = db.Close(); err != nil { - return err - } - - return os.Remove(oldPath) -} - -// EncryptDatabase encrypts an unencrypted database with key -func EncryptDatabase(oldPath string, newPath string, key string) error { - _, err := os.Stat(oldPath) - - // No files, nothing to do - if os.IsNotExist(err) { - return nil - } - - // Any other error, throws - if err != nil { - return err - } - - isEncrypted, err := sqlite.IsEncrypted(oldPath) - if err != nil { - return err - } - - // Nothing to do, already encrypted - if isEncrypted { - return os.Rename(oldPath, newPath) - } - - db, err := openDB(oldPath, "", defaultKdfIterationsNumber) - if err != nil { - return err - } - - attach := fmt.Sprintf( - "ATTACH DATABASE '%s' AS newdb KEY '%s'", - newPath, - key) - - if _, err = db.Exec(attach); err != nil { - return err - } - - changeKdfIter := fmt.Sprintf( - "PRAGMA newdb.kdf_iter = %d", - kdfIterationsNumber) - - if _, err = db.Exec(changeKdfIter); err != nil { - return err - } - - if _, err = db.Exec(exportDB); err != nil { - return err - } - - if err = db.Close(); err != nil { - return err - } - - return os.Remove(oldPath) -} - -func openDB(path string, key string, kdfIter int) (*sql.DB, error) { - db, err := sql.Open("sqlite3", path) - if err != nil { - return nil, err - } - - keyString := fmt.Sprintf("PRAGMA key = '%s'", key) - - // Disable concurrent access as not supported by the driver - db.SetMaxOpenConns(1) - - if _, err = db.Exec("PRAGMA foreign_keys=ON"); err != nil { - return nil, err - } - - if _, err = db.Exec(keyString); err != nil { - return nil, err - } - - kdfString := fmt.Sprintf("PRAGMA kdf_iter = '%d'", kdfIter) - - if _, err = db.Exec(kdfString); err != nil { - return nil, err - } - return db, nil -} - // NewSQLLiteKeysStorage creates a new SQLLiteKeysStorage instance associated with the specified database func NewSQLLiteKeysStorage(db *sql.DB) *SQLLiteKeysStorage { return &SQLLiteKeysStorage{ @@ -268,16 +78,21 @@ func (s *SQLLitePersistence) GetSessionStorage() dr.SessionStorage { return s.sessionStorage } +// GetTopicStorage returns the associated topicStorageObject +func (s *SQLLitePersistence) GetTopicStorage() topic.PersistenceService { + return s.topicStorage +} + // Open opens a file at the specified path func (s *SQLLitePersistence) Open(path string, key string) error { - db, err := openDB(path, key, kdfIterationsNumber) + db, err := appDB.Open(path, key, appDB.KdfIterationsNumber) if err != nil { return err } s.db = db - return s.setup() + return nil } // AddPrivateBundle adds the specified BundleContainer to the database @@ -1069,37 +884,3 @@ func toKey(a []byte) dr.Key { copy(k[:], a) return k } - -func (s *SQLLitePersistence) setup() error { - resources := bindata.Resource( - migrations.AssetNames(), - func(name string) ([]byte, error) { - return migrations.Asset(name) - }, - ) - - source, err := bindata.WithInstance(resources) - if err != nil { - return err - } - - driver, err := sqlcipher.WithInstance(s.db, &sqlcipher.Config{}) - if err != nil { - return err - } - - m, err := migrate.NewWithInstance( - "go-bindata", - source, - "sqlcipher", - driver) - if err != nil { - return err - } - - if err = m.Up(); err != migrate.ErrNoChange { - return err - } - - return nil -} diff --git a/services/shhext/chat/test.db b/services/shhext/chat/test.db deleted file mode 100644 index 29a2b84c203bc0c23768295d360d94785b235a78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28672 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%0DSTm7acG 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 diff --git a/services/shhext/chat/topic/persistence.go b/services/shhext/chat/topic/persistence.go new file mode 100644 index 000000000..2d95ce199 --- /dev/null +++ b/services/shhext/chat/topic/persistence.go @@ -0,0 +1,125 @@ +package topic + +import ( + "database/sql" + "strings" +) + +type PersistenceService interface { + Add(identity []byte, secret []byte, installationID string) error + Get(identity []byte, installationIDs []string) (*Response, error) + All() ([][][]byte, error) +} + +type Response struct { + secret []byte + installationIDs map[string]bool +} + +type SQLLitePersistence struct { + db *sql.DB +} + +func NewSQLLitePersistence(db *sql.DB) *SQLLitePersistence { + return &SQLLitePersistence{db: db} +} + +func (s *SQLLitePersistence) Add(identity []byte, secret []byte, installationID string) error { + tx, err := s.db.Begin() + if err != nil { + return err + } + + insertTopicStmt, err := tx.Prepare("INSERT INTO topics(identity, secret) VALUES (?, ?)") + if err != nil { + _ = tx.Rollback() + return err + } + defer insertTopicStmt.Close() + + _, err = insertTopicStmt.Exec(identity, secret) + if err != nil { + _ = tx.Rollback() + return err + } + + insertInstallationIDStmt, err := tx.Prepare("INSERT INTO topic_installation_ids(id, identity_id) VALUES (?, ?)") + if err != nil { + _ = tx.Rollback() + return err + } + defer insertInstallationIDStmt.Close() + + _, err = insertInstallationIDStmt.Exec(installationID, identity) + if err != nil { + _ = tx.Rollback() + return err + } + return tx.Commit() +} + +func (s *SQLLitePersistence) Get(identity []byte, installationIDs []string) (*Response, error) { + response := &Response{ + installationIDs: make(map[string]bool), + } + args := make([]interface{}, len(installationIDs)+1) + args[0] = identity + for i, installationID := range installationIDs { + args[i+1] = installationID + } + + /* #nosec */ + query := `SELECT secret, id + FROM topics t + JOIN + topic_installation_ids tid + ON t.identity = tid.identity_id + WHERE + t.identity = ? + AND + tid.id IN (?` + strings.Repeat(",?", len(installationIDs)-1) + `)` + + rows, err := s.db.Query(query, args...) + if err != nil && err != sql.ErrNoRows { + return nil, err + } + + for rows.Next() { + var installationID string + var secret []byte + err = rows.Scan(&secret, &installationID) + if err != nil { + return nil, err + } + + response.secret = secret + response.installationIDs[installationID] = true + } + + return response, nil +} + +func (s *SQLLitePersistence) All() ([][][]byte, error) { + query := `SELECT identity, secret + FROM topics` + + var secrets [][][]byte + + rows, err := s.db.Query(query) + if err != nil { + return nil, err + } + + for rows.Next() { + var secret []byte + var identity []byte + err = rows.Scan(&identity, &secret) + if err != nil { + return nil, err + } + + secrets = append(secrets, [][]byte{identity, secret}) + } + + return secrets, nil +} diff --git a/services/shhext/chat/topic/service.go b/services/shhext/chat/topic/service.go new file mode 100644 index 000000000..d93e28f37 --- /dev/null +++ b/services/shhext/chat/topic/service.go @@ -0,0 +1,93 @@ +package topic + +import ( + "crypto/ecdsa" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/ecies" + "github.com/ethereum/go-ethereum/log" +) + +const sskLen = 16 + +type Service struct { + persistence PersistenceService +} + +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) + sharedKey, err := ecies.ImportECDSA(myPrivateKey).GenerateShared( + ecies.ImportECDSAPublic(theirPublicKey), + sskLen, + sskLen, + ) + if err != nil { + return nil, err + } + + theirIdentity := crypto.CompressPubkey(theirPublicKey) + if err = s.persistence.Add(theirIdentity, sharedKey, installationID); err != nil { + return nil, err + } + + return &Secret{Key: sharedKey, Identity: theirPublicKey}, err +} + +// 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) +} + +// 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) + if err != nil { + return nil, false, err + } + + theirIdentity := crypto.CompressPubkey(theirPublicKey) + response, err := s.persistence.Get(theirIdentity, theirInstallationIDs) + if err != nil { + return nil, false, err + } + + for _, installationID := range theirInstallationIDs { + if !response.installationIDs[installationID] { + return sharedKey, false, nil + } + } + + return &Secret{ + Key: response.secret, + Identity: theirPublicKey, + }, true, nil +} + +type Secret struct { + Identity *ecdsa.PublicKey + Key []byte +} + +func (s *Service) All() ([]*Secret, error) { + var secrets []*Secret + tuples, err := s.persistence.All() + if err != nil { + return nil, err + } + + for _, tuple := range tuples { + key, err := crypto.DecompressPubkey(tuple[0]) + if err != nil { + return nil, err + } + + secrets = append(secrets, &Secret{Identity: key, Key: tuple[1]}) + + } + + return secrets, nil + +} diff --git a/services/shhext/chat/topic/service_test.go b/services/shhext/chat/topic/service_test.go new file mode 100644 index 000000000..35458ac09 --- /dev/null +++ b/services/shhext/chat/topic/service_test.go @@ -0,0 +1,114 @@ +package topic + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/ethereum/go-ethereum/crypto" + appDB "github.com/status-im/status-go/services/shhext/chat/db" + "github.com/stretchr/testify/suite" +) + +func TestServiceTestSuite(t *testing.T) { + suite.Run(t, new(ServiceTestSuite)) +} + +type ServiceTestSuite struct { + suite.Suite + service *Service + path string +} + +func (s *ServiceTestSuite) SetupTest() { + dbFile, err := ioutil.TempFile(os.TempDir(), "topic") + s.Require().NoError(err) + s.path = dbFile.Name() + + db, err := appDB.Open(s.path, "", 0) + + s.Require().NoError(err) + + s.service = NewService(NewSQLLitePersistence(db)) +} + +func (s *ServiceTestSuite) TearDownTest() { + os.Remove(s.path) +} + +func (s *ServiceTestSuite) TestSingleInstallationID() { + ourInstallationID := "our" + installationID1 := "1" + installationID2 := "2" + + myKey, err := crypto.GenerateKey() + s.Require().NoError(err) + + theirKey, err := crypto.GenerateKey() + s.Require().NoError(err) + + // We receive a message from installationID1 + sharedKey1, err := s.service.Receive(myKey, &theirKey.PublicKey, installationID1) + s.Require().NoError(err) + s.Require().NotNil(sharedKey1, "it generates a shared key") + + // We want to send a message to installationID1 + sharedKey2, agreed2, err := s.service.Send(myKey, ourInstallationID, &theirKey.PublicKey, []string{installationID1}) + s.Require().NoError(err) + s.Require().True(agreed2) + s.Require().NotNil(sharedKey2, "We can retrieve a shared secret") + s.Require().Equal(sharedKey1, sharedKey2, "The shared secret is the same as the one stored") + + // We want to send a message to multiple installationIDs, one of which we haven't never communicated with + sharedKey3, agreed3, err := s.service.Send(myKey, ourInstallationID, &theirKey.PublicKey, []string{installationID1, installationID2}) + s.Require().NoError(err) + s.Require().NotNil(sharedKey3, "A shared key is returned") + s.Require().False(agreed3) + + // We receive a message from installationID2 + sharedKey4, err := s.service.Receive(myKey, &theirKey.PublicKey, installationID2) + s.Require().NoError(err) + s.Require().NotNil(sharedKey4, "it generates a shared key") + s.Require().Equal(sharedKey1, sharedKey4, "It generates the same key") + + // We want to send a message to installationID 1 & 2, both have been + sharedKey5, agreed5, err := s.service.Send(myKey, ourInstallationID, &theirKey.PublicKey, []string{installationID1, installationID2}) + s.Require().NoError(err) + s.Require().NotNil(sharedKey5, "We can retrieve a shared secret") + s.Require().True(agreed5) + s.Require().Equal(sharedKey1, sharedKey5, "The shared secret is the same as the one stored") + +} + +func (s *ServiceTestSuite) TestAll() { + installationID1 := "1" + installationID2 := "2" + + myKey, err := crypto.GenerateKey() + s.Require().NoError(err) + + theirKey1, err := crypto.GenerateKey() + s.Require().NoError(err) + + theirKey2, err := crypto.GenerateKey() + s.Require().NoError(err) + + // We receive a message from user 1 + sharedKey1, err := s.service.Receive(myKey, &theirKey1.PublicKey, installationID1) + s.Require().NoError(err) + s.Require().NotNil(sharedKey1, "it generates a shared key") + + // We receive a message from user 2 + sharedKey2, err := s.service.Receive(myKey, &theirKey2.PublicKey, installationID2) + s.Require().NoError(err) + s.Require().NotNil(sharedKey2, "it generates a shared key") + + // All the topics are there + topics, err := s.service.All() + s.Require().NoError(err) + expected := []*Secret{ + sharedKey1, + sharedKey2, + } + s.Require().Equal(expected, topics) +} diff --git a/services/shhext/chat/whisper.go b/services/shhext/chat/whisper.go index 1f6118c0f..d726835bd 100644 --- a/services/shhext/chat/whisper.go +++ b/services/shhext/chat/whisper.go @@ -8,10 +8,16 @@ import ( 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{} @@ -33,22 +39,26 @@ func PublicMessageToWhisper(rpcMsg SendPublicMessageRPC, payload []byte) whisper return msg } -func DirectMessageToWhisper(rpcMsg SendDirectMessageRPC, payload []byte) whisper.NewMessage { +func DirectMessageToWhisper(rpcMsg SendDirectMessageRPC, payload []byte, sharedSecret []byte) whisper.NewMessage { var topicBytes whisper.TopicType + msg := defaultWhisperMessage() if rpcMsg.Chat == "" { - topicBytes = discoveryTopicBytes + if sharedSecret != nil { + topicBytes = SharedSecretToTopic(sharedSecret) + } else { + topicBytes = discoveryTopicBytes + msg.PublicKey = rpcMsg.PubKey + } } else { topicBytes = toTopic(rpcMsg.Chat) + msg.PublicKey = rpcMsg.PubKey } - msg := defaultWhisperMessage() - msg.Topic = topicBytes msg.Payload = payload msg.Sig = rpcMsg.Sig - msg.PublicKey = rpcMsg.PubKey return msg } diff --git a/services/shhext/chat/whisper_test.go b/services/shhext/chat/whisper_test.go index 3c4643e1b..352c90e6f 100644 --- a/services/shhext/chat/whisper_test.go +++ b/services/shhext/chat/whisper_test.go @@ -30,10 +30,27 @@ func TestDirectMessageToWhisper(t *testing.T) { } payload := []byte("test") - whisperMessage := DirectMessageToWhisper(rpcMessage, payload) + 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/filter/service.go b/services/shhext/filter/service.go new file mode 100644 index 000000000..fabf36ecf --- /dev/null +++ b/services/shhext/filter/service.go @@ -0,0 +1,350 @@ +package filter + +import ( + "crypto/ecdsa" + "encoding/hex" + "fmt" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/status-im/status-go/services/shhext/chat/topic" + whisper "github.com/status-im/whisper/whisperv6" + "math/big" + "sync" +) + +const ( + discoveryTopic = "contact-discovery" +) + +// 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), nil +} + +type FilterAndTopic struct { + FilterID string + Topic []byte + SymKeyID string +} + +type Chat struct { + // ChatID is the identifier of the chat + ChatID string + // SymKeyID is the symmetric key id used for symmetric chats + SymKeyID string + // OneToOne tells us if we need to use asymmetric encryption for this chat + OneToOne bool + // 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 + // FilterID the whisper filter id generated + FilterID string + // Identity is the public key of the other recipient for non-public chats + Identity string + // Topic is the whisper topic + Topic []byte +} + +type Service struct { + keyID string + whisper *whisper.Whisper + topic *topic.Service + chats map[string]*Chat + mutex sync.Mutex +} + +func New(k string, w *whisper.Whisper, t *topic.Service) *Service { + return &Service{ + keyID: k, + whisper: w, + topic: t, + mutex: sync.Mutex{}, + chats: make(map[string]*Chat), + } +} + +// LoadDiscovery adds the discovery filter +func (s *Service) LoadDiscovery(myKey *ecdsa.PrivateKey) error { + s.mutex.Lock() + defer s.mutex.Unlock() + + discoveryChat := &Chat{ + ChatID: discoveryTopic, + } + + discoveryResponse, err := s.AddAsymmetricFilter(myKey, discoveryChat.ChatID, true) + if err != nil { + return err + } + + discoveryChat.Topic = discoveryResponse.Topic + discoveryChat.FilterID = discoveryResponse.FilterID + + s.chats[discoveryChat.ChatID] = discoveryChat + return nil +} + +func (s *Service) Init(chats []*Chat) error { + log.Debug("Initializing filter service") + myKey, err := s.whisper.GetPrivateKey(s.keyID) + 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) + + return err + } + + // Add discovery topic + log.Debug("Loading discovery topics") + err = s.LoadDiscovery(myKey) + if err != nil { + return 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 err + } + } + + // Add the negotiated topics + log.Debug("Loading negotiated topics") + secrets, err := s.topic.All() + if err != nil { + return err + } + + for _, secret := range secrets { + s.ProcessNegotiatedSecret(secret) + } + + return 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 { + s.mutex.Lock() + defer s.mutex.Unlock() + + if err := s.whisper.Unsubscribe(chat.ChatID); err != nil { + return err + } + 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) + if err != nil { + return err + } + + s.chats[contactCodeChatID] = &Chat{ + ChatID: contactCodeChatID, + FilterID: contactCodeFilter.FilterID, + Topic: contactCodeFilter.Topic, + SymKeyID: contactCodeFilter.SymKeyID, + Identity: identity, + } + + 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 +} + +func (s *Service) AddSymmetric(chatID string) (*FilterAndTopic, error) { + var symKey []byte + + topic := toTopic(chatID) + topics := [][]byte{topic} + + symKeyID, err := s.whisper.AddSymKeyFromPassword(chatID) + if err != nil { + log.Error("SYM KEYN FAILED", "err", err) + return nil, err + } + + if symKey, err = s.whisper.GetSymKey(symKeyID); err != nil { + return nil, err + } + + f := &whisper.Filter{ + KeySym: symKey, + PoW: 0.002, + AllowP2P: true, + Topics: topics, + Messages: s.whisper.NewMessageStore(), + } + + id, err := s.whisper.Subscribe(f) + if err != nil { + return nil, err + } + + return &FilterAndTopic{ + FilterID: id, + SymKeyID: symKeyID, + Topic: topic, + }, nil +} + +func (s *Service) AddAsymmetricFilter(keyAsym *ecdsa.PrivateKey, chatID string, listen bool) (*FilterAndTopic, error) { + var err error + var pow float64 + + if listen { + pow = 0.002 + } else { + // Set high pow so we discard messages + pow = 1 + } + + topic := toTopic(chatID) + topics := [][]byte{topic} + + f := &whisper.Filter{ + KeyAsym: keyAsym, + PoW: pow, + AllowP2P: listen, + Topics: topics, + Messages: s.whisper.NewMessageStore(), + } + + id, err := s.whisper.Subscribe(f) + if err != nil { + return nil, err + } + + return &FilterAndTopic{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 +} + +func negotiatedID(identity *ecdsa.PublicKey) string { + return fmt.Sprintf("%x-negotiated", crypto.FromECDSAPub(identity)) +} + +func (s *Service) Get(identity *ecdsa.PublicKey) *Chat { + return s.chats[negotiatedID(identity)] +} + +func (s *Service) ProcessNegotiatedSecret(secret *topic.Secret) error { + s.mutex.Lock() + defer s.mutex.Unlock() + + 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 +} diff --git a/services/shhext/filter/service_test.go b/services/shhext/filter/service_test.go new file mode 100644 index 000000000..98fd83393 --- /dev/null +++ b/services/shhext/filter/service_test.go @@ -0,0 +1,155 @@ +package filter + +import ( + "crypto/ecdsa" + "fmt" + "io/ioutil" + "os" + "testing" + + "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" + whisper "github.com/status-im/whisper/whisperv6" + "github.com/stretchr/testify/suite" +) + +func TestServiceTestSuite(t *testing.T) { + suite.Run(t, new(ServiceTestSuite)) +} + +type TestKey struct { + privateKey *ecdsa.PrivateKey + partitionedTopic int +} + +func NewTestKey(privateKey string, partitionedTopic int) (*TestKey, error) { + key, err := crypto.HexToECDSA(privateKey) + if err != nil { + return nil, err + } + + return &TestKey{ + privateKey: key, + partitionedTopic: partitionedTopic, + }, nil + +} + +func (t *TestKey) PublicKeyString() string { + return fmt.Sprintf("%x", crypto.FromECDSAPub(&t.privateKey.PublicKey)) +} + +type ServiceTestSuite struct { + suite.Suite + service *Service + path string + keys []*TestKey +} + +func (s *ServiceTestSuite) SetupTest() { + keyStrs := []string{"c6cbd7d76bc5baca530c875663711b947efa6a86a900a9e8645ce32e5821484e", "d51dd64ad19ea84968a308dca246012c00d2b2101d41bce740acd1c650acc509"} + keyTopics := []int{4490, 3991} + + dbFile, err := ioutil.TempFile(os.TempDir(), "topic") + + s.Require().NoError(err) + s.path = dbFile.Name() + + for i, k := range keyStrs { + testKey, err := NewTestKey(k, keyTopics[i]) + s.Require().NoError(err) + + s.keys = append(s.keys, testKey) + } + + db, err := appDB.Open(s.path, "", 0) + s.Require().NoError(err) + + // Build services + topicService := topic.NewService(topic.NewSQLLitePersistence(db)) + whisper := whisper.New(nil) + keyID, err := whisper.AddKeyPair(s.keys[0].privateKey) + s.Require().NoError(err) + + s.service = New(keyID, whisper, topicService) +} + +func (s *ServiceTestSuite) TearDownTest() { + os.Remove(s.path) +} + +func (s *ServiceTestSuite) TestDiscoveryAndPartitionedTopic() { + chats := []*Chat{} + partitionedTopic := fmt.Sprintf("contact-discovery-%d", s.keys[0].partitionedTopic) + contactCodeTopic := s.keys[0].PublicKeyString() + "-contact-code" + + 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") + + contactCodeFilter := s.service.chats[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") +} + +func (s *ServiceTestSuite) TestPublicAndOneToOneChats() { + chats := []*Chat{ + &Chat{ + ChatID: "status", + }, + &Chat{ + ChatID: s.keys[1].PublicKeyString(), + Identity: s.keys[1].PublicKeyString(), + OneToOne: true, + }, + } + partitionedTopic := fmt.Sprintf("contact-discovery-%d", s.keys[1].partitionedTopic) + contactCodeTopic := s.keys[1].PublicKeyString() + "-contact-code" + + 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") + + statusFilter := s.service.chats["status"] + s.Require().NotNil(statusFilter, "It creates a filter for the public chat") + s.Require().NotNil(statusFilter.SymKeyID, "It returns a sym key id") + + contactCodeFilter := s.service.chats[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") +} + +func (s *ServiceTestSuite) TestNegotiatedTopic() { + chats := []*Chat{} + + negotiatedTopic1 := s.keys[0].PublicKeyString() + "-negotiated" + negotiatedTopic2 := 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"}) + 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"}) + s.Require().NoError(err) + + 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") + + negotiatedFilter1 := s.service.chats[negotiatedTopic1] + s.Require().NotNil(negotiatedFilter1, "It adds the negotiated filter") + negotiatedFilter2 := s.service.chats[negotiatedTopic2] + s.Require().NotNil(negotiatedFilter2, "It adds the negotiated filter") +} diff --git a/services/shhext/service.go b/services/shhext/service.go index e9fe2d9fa..2b52c849f 100644 --- a/services/shhext/service.go +++ b/services/shhext/service.go @@ -9,6 +9,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p/enode" @@ -16,8 +17,12 @@ import ( "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" whisper "github.com/status-im/whisper/whisperv6" "github.com/syndtr/goleveldb/leveldb" "golang.org/x/crypto/sha3" @@ -60,6 +65,7 @@ type Service struct { cache *mailservers.Cache connManager *mailservers.ConnectionManager lastUsedMonitor *mailservers.LastUsedConnectionMonitor + filter *filter.Service } // Make sure that Service implements node.Service interface. @@ -142,11 +148,11 @@ func (s *Service) initProtocol(address, encKey, password string) error { v4Path := filepath.Join(s.dataDir, fmt.Sprintf("%s.v4.db", s.installationID)) if password != "" { - if err := chat.MigrateDBFile(v0Path, v1Path, "ON", password); err != nil { + if err := appDB.MigrateDBFile(v0Path, v1Path, "ON", password); err != nil { return err } - if err := chat.MigrateDBFile(v1Path, v2Path, password, encKey); err != nil { + 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) @@ -154,13 +160,13 @@ func (s *Service) initProtocol(address, encKey, password string) error { } } - if err := chat.MigrateDBKeyKdfIterations(v2Path, v3Path, encKey); err != nil { + if err := appDB.MigrateDBKeyKdfIterations(v2Path, v3Path, encKey); err != nil { os.Remove(v2Path) os.Remove(v3Path) } // Fix IOS not encrypting database - if err := chat.EncryptDatabase(v3Path, v4Path, encKey); err != nil { + if err := appDB.EncryptDatabase(v3Path, v4Path, encKey); err != nil { os.Remove(v3Path) os.Remove(v4Path) } @@ -189,7 +195,18 @@ func (s *Service) initProtocol(address, encKey, password string) error { } } - s.protocol = chat.NewProtocolService(chat.NewEncryptionService(persistence, chat.DefaultEncryptionServiceConfig(s.installationID)), addedBundlesHandler) + // 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 } @@ -291,5 +308,39 @@ func (s *Service) Stop() error { s.requestsRegistry.Clear() s.envelopesMonitor.Stop() s.mailMonitor.Stop() + s.filter.Stop() 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) + } + +} diff --git a/services/shhext/signal.go b/services/shhext/signal.go index 36c840d9a..614cb747b 100644 --- a/services/shhext/signal.go +++ b/services/shhext/signal.go @@ -35,3 +35,7 @@ func (h EnvelopeSignalHandler) DecryptMessageFailed(pubKey string) { func (h EnvelopeSignalHandler) BundleAdded(identity string, installationID string) { signal.SendBundleAdded(identity, installationID) } + +func (h EnvelopeSignalHandler) WhisperFilterAdded(filters []*signal.Filter) { + signal.SendWhisperFilterAdded(filters) +} diff --git a/signal/events_shhext.go b/signal/events_shhext.go index c44ff632b..62c21acfe 100644 --- a/signal/events_shhext.go +++ b/signal/events_shhext.go @@ -4,6 +4,7 @@ import ( "encoding/hex" "github.com/ethereum/go-ethereum/common" + whisper "github.com/status-im/whisper/whisperv6" ) const ( @@ -31,6 +32,9 @@ const ( // EventBundleAdded is triggered when we receive a bundle EventBundleAdded = "bundles.added" + + // EventWhisperFilterAdded is triggered when we setup a new filter or restore existing ones + EventWhisperFilterAdded = "whisper.filter.added" ) // EnvelopeSignal includes hash of the envelope. @@ -58,6 +62,18 @@ type BundleAddedSignal struct { InstallationID string `json:"installationID"` } +type Filter struct { + Identity string `json:"identity"` + FilterID string `json:"filterId"` + SymKeyID string `json:"symKeyId"` + ChatID string `json:"chatId"` + Topic whisper.TopicType `json:"topic"` +} + +type WhisperFilterAddedSignal struct { + Filters []*Filter `json:"filters"` +} + // SendEnvelopeSent triggered when envelope delivered at least to 1 peer. func SendEnvelopeSent(hash common.Hash) { send(EventEnvelopeSent, EnvelopeSignal{Hash: hash}) @@ -114,3 +130,7 @@ func SendDecryptMessageFailed(sender string) { func SendBundleAdded(identity string, installationID string) { send(EventBundleAdded, BundleAddedSignal{Identity: identity, InstallationID: installationID}) } + +func SendWhisperFilterAdded(filters []*Filter) { + send(EventWhisperFilterAdded, WhisperFilterAddedSignal{Filters: filters}) +} diff --git a/static/bindata.go b/static/bindata.go index 97119eefd..b3d46c54f 100644 --- a/static/bindata.go +++ b/static/bindata.go @@ -1,26 +1,26 @@ -// Code generated by go-bindata. DO NOT EDIT. +// Code generated by go-bindata. // sources: -// ../config/README.md (3.33kB) -// ../config/cli/fleet-eth.beta.json (3.261kB) -// ../config/cli/fleet-eth.staging.json (1.862kB) -// ../config/cli/fleet-eth.test.json (1.543kB) -// ../config/cli/les-enabled.json (58B) -// ../config/cli/mailserver-enabled.json (176B) -// ../config/status-chain-genesis.json (612B) -// keys/bootnode.key (65B) -// keys/firebaseauthkey (153B) -// keys/test-account1-status-chain.pk (489B) -// keys/test-account1.pk (491B) -// keys/test-account2-status-chain.pk (489B) -// keys/test-account2.pk (491B) -// keys/test-account3-before-eip55.pk (489B) +// ../config/README.md +// ../config/cli/fleet-eth.beta.json +// ../config/cli/fleet-eth.staging.json +// ../config/cli/fleet-eth.test.json +// ../config/cli/les-enabled.json +// ../config/cli/mailserver-enabled.json +// ../config/status-chain-genesis.json +// keys/bootnode.key +// keys/firebaseauthkey +// keys/test-account1-status-chain.pk +// keys/test-account1.pk +// keys/test-account2-status-chain.pk +// keys/test-account2.pk +// keys/test-account3-before-eip55.pk +// DO NOT EDIT! package static import ( "bytes" "compress/gzip" - "crypto/sha256" "fmt" "io" "io/ioutil" @@ -33,7 +33,7 @@ import ( func bindataRead(data []byte, name string) ([]byte, error) { gz, err := gzip.NewReader(bytes.NewBuffer(data)) if err != nil { - return nil, fmt.Errorf("read %q: %v", name, err) + return nil, fmt.Errorf("Read %q: %v", name, err) } var buf bytes.Buffer @@ -41,7 +41,7 @@ func bindataRead(data []byte, name string) ([]byte, error) { clErr := gz.Close() if err != nil { - return nil, fmt.Errorf("read %q: %v", name, err) + return nil, fmt.Errorf("Read %q: %v", name, err) } if clErr != nil { return nil, err @@ -51,9 +51,8 @@ func bindataRead(data []byte, name string) ([]byte, error) { } type asset struct { - bytes []byte - info os.FileInfo - digest [sha256.Size]byte + bytes []byte + info os.FileInfo } type bindataFileInfo struct { @@ -97,8 +96,8 @@ func ConfigReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "../config/README.md", size: 3330, mode: os.FileMode(0644), modTime: time.Unix(1560158346, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x65, 0xb9, 0xf5, 0x6, 0xbe, 0x7d, 0x85, 0x3b, 0x8, 0xbc, 0x5c, 0x71, 0x85, 0x19, 0xd1, 0xde, 0x38, 0xb5, 0xe9, 0x90, 0x5c, 0x45, 0xb2, 0xa5, 0x8a, 0x91, 0xee, 0xeb, 0x1e, 0xb4, 0xa9, 0x8f}} + info := bindataFileInfo{name: "../config/README.md", size: 3330, mode: os.FileMode(420), modTime: time.Unix(1560418002, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -117,8 +116,8 @@ func ConfigCliFleetEthBetaJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "../config/cli/fleet-eth.beta.json", size: 3261, mode: os.FileMode(0644), modTime: time.Unix(1544697617, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x2b, 0xae, 0x42, 0x4b, 0xa4, 0xd9, 0x2, 0x69, 0x99, 0x29, 0x7e, 0x1, 0x4e, 0xd9, 0x58, 0x84, 0x28, 0x3a, 0x81, 0xc4, 0xde, 0x1d, 0xea, 0x51, 0xc8, 0x21, 0xff, 0x7b, 0xff, 0x23, 0x1c, 0x16}} + info := bindataFileInfo{name: "../config/cli/fleet-eth.beta.json", size: 3261, mode: os.FileMode(420), modTime: time.Unix(1548939502, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -137,8 +136,8 @@ func ConfigCliFleetEthStagingJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "../config/cli/fleet-eth.staging.json", size: 1862, mode: os.FileMode(0644), modTime: time.Unix(1544697617, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xae, 0x85, 0xa1, 0x10, 0x16, 0x87, 0x10, 0x1c, 0xc3, 0xf4, 0xc7, 0xc, 0x2e, 0x51, 0xb7, 0x3, 0x61, 0x16, 0x99, 0x84, 0x3d, 0x5d, 0x82, 0x62, 0xfb, 0xf4, 0x5e, 0x19, 0xda, 0xb9, 0xaa, 0xc4}} + info := bindataFileInfo{name: "../config/cli/fleet-eth.staging.json", size: 1862, mode: os.FileMode(420), modTime: time.Unix(1548939502, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -157,8 +156,8 @@ func ConfigCliFleetEthTestJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "../config/cli/fleet-eth.test.json", size: 1543, mode: os.FileMode(0644), modTime: time.Unix(1544697617, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x68, 0xef, 0x71, 0xa1, 0x38, 0x37, 0xf0, 0x0, 0xbb, 0x95, 0x26, 0x2a, 0x2a, 0x65, 0x98, 0xfe, 0xe5, 0x3f, 0xbf, 0xb, 0x68, 0xa6, 0xb5, 0xa4, 0x10, 0xc1, 0x4b, 0x67, 0xb4, 0x4e, 0x32, 0xc0}} + info := bindataFileInfo{name: "../config/cli/fleet-eth.test.json", size: 1543, mode: os.FileMode(420), modTime: time.Unix(1548939502, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -177,8 +176,8 @@ func ConfigCliLesEnabledJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "../config/cli/les-enabled.json", size: 58, mode: os.FileMode(0644), modTime: time.Unix(1544697617, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x7e, 0xee, 0x27, 0xa7, 0x74, 0xa0, 0x46, 0xa1, 0x41, 0xed, 0x4d, 0x16, 0x5b, 0xf3, 0xf0, 0x7c, 0xc8, 0x2f, 0x6f, 0x47, 0xa4, 0xbb, 0x5f, 0x43, 0x33, 0xd, 0x9, 0x9d, 0xea, 0x9e, 0x15, 0xee}} + info := bindataFileInfo{name: "../config/cli/les-enabled.json", size: 58, mode: os.FileMode(420), modTime: time.Unix(1541418081, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -197,8 +196,8 @@ func ConfigCliMailserverEnabledJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "../config/cli/mailserver-enabled.json", size: 176, mode: os.FileMode(0644), modTime: time.Unix(1544697617, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x34, 0xec, 0x81, 0x8b, 0x99, 0xb6, 0xdb, 0xc0, 0x8b, 0x46, 0x97, 0x96, 0xc7, 0x58, 0x30, 0x33, 0xef, 0x54, 0x25, 0x87, 0x7b, 0xb9, 0x94, 0x6b, 0x18, 0xa4, 0x5b, 0x58, 0x67, 0x7c, 0x44, 0xa6}} + info := bindataFileInfo{name: "../config/cli/mailserver-enabled.json", size: 176, mode: os.FileMode(420), modTime: time.Unix(1541418081, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -217,8 +216,8 @@ func ConfigStatusChainGenesisJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "../config/status-chain-genesis.json", size: 612, mode: os.FileMode(0644), modTime: time.Unix(1544697617, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xb, 0xf0, 0xc, 0x1, 0x95, 0x65, 0x6, 0x55, 0x48, 0x8f, 0x83, 0xa0, 0xb4, 0x81, 0xda, 0xad, 0x30, 0x6d, 0xb2, 0x78, 0x1b, 0x26, 0x4, 0x13, 0x12, 0x9, 0x6, 0xae, 0x3a, 0x2c, 0x1, 0x71}} + info := bindataFileInfo{name: "../config/status-chain-genesis.json", size: 612, mode: os.FileMode(420), modTime: time.Unix(1541418081, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -237,8 +236,8 @@ func keysBootnodeKey() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "keys/bootnode.key", size: 65, mode: os.FileMode(0644), modTime: time.Unix(1524646110, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x31, 0xcf, 0x27, 0xd4, 0x96, 0x2e, 0x32, 0xcd, 0x58, 0x96, 0x2a, 0xe5, 0x8c, 0xa0, 0xf1, 0x73, 0x1f, 0xd6, 0xd6, 0x8b, 0xb, 0x73, 0xd3, 0x2c, 0x84, 0x1a, 0x56, 0xa4, 0x74, 0xb6, 0x95, 0x20}} + info := bindataFileInfo{name: "keys/bootnode.key", size: 65, mode: os.FileMode(420), modTime: time.Unix(1539606161, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -257,8 +256,8 @@ func keysFirebaseauthkey() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "keys/firebaseauthkey", size: 153, mode: os.FileMode(0644), modTime: time.Unix(1510765867, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xe, 0x69, 0x23, 0x64, 0x7d, 0xf9, 0x14, 0x37, 0x6f, 0x2b, 0x1, 0xf0, 0xb0, 0xa4, 0xb2, 0xd0, 0x18, 0xcd, 0xf9, 0xeb, 0x57, 0xa3, 0xfd, 0x79, 0x25, 0xa7, 0x9c, 0x3, 0xce, 0x26, 0xec, 0xe1}} + info := bindataFileInfo{name: "keys/firebaseauthkey", size: 153, mode: os.FileMode(420), modTime: time.Unix(1536843582, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -277,8 +276,8 @@ func keysTestAccount1StatusChainPk() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "keys/test-account1-status-chain.pk", size: 489, mode: os.FileMode(0644), modTime: time.Unix(1516444049, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x8f, 0xba, 0x35, 0x1, 0x2b, 0x9d, 0xad, 0xf0, 0x2d, 0x3c, 0x4d, 0x6, 0xb5, 0x22, 0x2, 0x47, 0xd4, 0x1c, 0xf4, 0x31, 0x2f, 0xb, 0x5b, 0x27, 0x5d, 0x43, 0x97, 0x58, 0x2d, 0xf0, 0xe1, 0xbe}} + info := bindataFileInfo{name: "keys/test-account1-status-chain.pk", size: 489, mode: os.FileMode(420), modTime: time.Unix(1539606161, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -297,8 +296,8 @@ func keysTestAccount1Pk() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "keys/test-account1.pk", size: 491, mode: os.FileMode(0644), modTime: time.Unix(1510765867, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x9, 0x43, 0xc2, 0xf4, 0x8c, 0xc6, 0x64, 0x25, 0x8c, 0x7, 0x8c, 0xa8, 0x89, 0x2b, 0x7b, 0x9b, 0x4f, 0x81, 0xcb, 0xce, 0x3d, 0xef, 0x82, 0x9c, 0x27, 0x27, 0xa9, 0xc5, 0x46, 0x70, 0x30, 0x38}} + info := bindataFileInfo{name: "keys/test-account1.pk", size: 491, mode: os.FileMode(420), modTime: time.Unix(1539606161, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -317,8 +316,8 @@ func keysTestAccount2StatusChainPk() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "keys/test-account2-status-chain.pk", size: 489, mode: os.FileMode(0644), modTime: time.Unix(1516444049, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x9, 0xf8, 0x5c, 0xe9, 0x92, 0x96, 0x2d, 0x88, 0x2b, 0x8e, 0x42, 0x3f, 0xa4, 0x93, 0x6c, 0xad, 0xe9, 0xc0, 0x1b, 0x8a, 0x8, 0x8c, 0x5e, 0x7a, 0x84, 0xa2, 0xf, 0x9f, 0x77, 0x58, 0x2c, 0x2c}} + info := bindataFileInfo{name: "keys/test-account2-status-chain.pk", size: 489, mode: os.FileMode(420), modTime: time.Unix(1539606161, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -337,8 +336,8 @@ func keysTestAccount2Pk() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "keys/test-account2.pk", size: 491, mode: os.FileMode(0644), modTime: time.Unix(1510765867, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x9f, 0x72, 0xd5, 0x95, 0x5c, 0x5a, 0x99, 0x9d, 0x2f, 0x21, 0x83, 0xd7, 0x10, 0x17, 0x4a, 0x3d, 0x65, 0xc9, 0x26, 0x1a, 0x2c, 0x9d, 0x65, 0x63, 0xd2, 0xa0, 0xfc, 0x7c, 0x0, 0x87, 0x38, 0x9f}} + info := bindataFileInfo{name: "keys/test-account2.pk", size: 491, mode: os.FileMode(420), modTime: time.Unix(1539606161, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -357,8 +356,8 @@ func keysTestAccount3BeforeEip55Pk() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "keys/test-account3-before-eip55.pk", size: 489, mode: os.FileMode(0644), modTime: time.Unix(1516444049, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x81, 0x40, 0x56, 0xc1, 0x5e, 0x10, 0x6e, 0x28, 0x15, 0x3, 0x4e, 0xc4, 0xc4, 0x71, 0x4d, 0x16, 0x99, 0xcc, 0x1b, 0x63, 0xee, 0x10, 0x20, 0xe4, 0x59, 0x52, 0x3f, 0xc0, 0xad, 0x15, 0x13, 0x72}} + info := bindataFileInfo{name: "keys/test-account3-before-eip55.pk", size: 489, mode: os.FileMode(420), modTime: time.Unix(1539606161, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -366,8 +365,8 @@ func keysTestAccount3BeforeEip55Pk() (*asset, error) { // It returns an error if the asset could not be found or // could not be loaded. func Asset(name string) ([]byte, error) { - canonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[canonicalName]; ok { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) @@ -377,12 +376,6 @@ func Asset(name string) ([]byte, error) { return nil, fmt.Errorf("Asset %s not found", name) } -// AssetString returns the asset contents as a string (instead of a []byte). -func AssetString(name string) (string, error) { - data, err := Asset(name) - return string(data), err -} - // MustAsset is like Asset but panics when Asset would return an error. // It simplifies safe initialization of global variables. func MustAsset(name string) []byte { @@ -394,18 +387,12 @@ func MustAsset(name string) []byte { return a } -// MustAssetString is like AssetString but panics when Asset would return an -// error. It simplifies safe initialization of global variables. -func MustAssetString(name string) string { - return string(MustAsset(name)) -} - // AssetInfo loads and returns the asset info for the given name. // It returns an error if the asset could not be found or // could not be loaded. func AssetInfo(name string) (os.FileInfo, error) { - canonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[canonicalName]; ok { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) @@ -415,33 +402,6 @@ func AssetInfo(name string) (os.FileInfo, error) { return nil, fmt.Errorf("AssetInfo %s not found", name) } -// AssetDigest returns the digest of the file with the given name. It returns an -// error if the asset could not be found or the digest could not be loaded. -func AssetDigest(name string) ([sha256.Size]byte, error) { - canonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[canonicalName]; ok { - a, err := f() - if err != nil { - return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s can't read by error: %v", name, err) - } - return a.digest, nil - } - return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s not found", name) -} - -// Digests returns a map of all known files and their checksums. -func Digests() (map[string][sha256.Size]byte, error) { - mp := make(map[string][sha256.Size]byte, len(_bindata)) - for name := range _bindata { - a, err := _bindata[name]() - if err != nil { - return nil, err - } - mp[name] = a.digest - } - return mp, nil -} - // AssetNames returns the names of the assets. func AssetNames() []string { names := make([]string, 0, len(_bindata)) @@ -454,31 +414,18 @@ func AssetNames() []string { // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "../config/README.md": ConfigReadmeMd, - "../config/cli/fleet-eth.beta.json": ConfigCliFleetEthBetaJson, - "../config/cli/fleet-eth.staging.json": ConfigCliFleetEthStagingJson, - "../config/cli/fleet-eth.test.json": ConfigCliFleetEthTestJson, - "../config/cli/les-enabled.json": ConfigCliLesEnabledJson, - "../config/cli/mailserver-enabled.json": ConfigCliMailserverEnabledJson, - "../config/status-chain-genesis.json": ConfigStatusChainGenesisJson, - "keys/bootnode.key": keysBootnodeKey, - "keys/firebaseauthkey": keysFirebaseauthkey, - "keys/test-account1-status-chain.pk": keysTestAccount1StatusChainPk, - "keys/test-account1.pk": keysTestAccount1Pk, - "keys/test-account2-status-chain.pk": keysTestAccount2StatusChainPk, - "keys/test-account2.pk": keysTestAccount2Pk, - "keys/test-account3-before-eip55.pk": keysTestAccount3BeforeEip55Pk, } @@ -491,15 +438,15 @@ var _bindata = map[string]func() (*asset, error){ // img/ // a.png // b.png -// then AssetDir("data") would return []string{"foo.txt", "img"}, -// AssetDir("data/img") would return []string{"a.png", "b.png"}, -// AssetDir("foo.txt") and AssetDir("notexist") would return an error, and +// then AssetDir("data") would return []string{"foo.txt", "img"} +// AssetDir("data/img") would return []string{"a.png", "b.png"} +// AssetDir("foo.txt") and AssetDir("notexist") would return an error // AssetDir("") will return []string{"data"}. func AssetDir(name string) ([]string, error) { node := _bintree if len(name) != 0 { - canonicalName := strings.Replace(name, "\\", "/", -1) - pathList := strings.Split(canonicalName, "/") + cannonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(cannonicalName, "/") for _, p := range pathList { node = node.Children[p] if node == nil { @@ -521,33 +468,32 @@ type bintree struct { Func func() (*asset, error) Children map[string]*bintree } - var _bintree = &bintree{nil, map[string]*bintree{ "..": &bintree{nil, map[string]*bintree{ "config": &bintree{nil, map[string]*bintree{ "README.md": &bintree{ConfigReadmeMd, map[string]*bintree{}}, "cli": &bintree{nil, map[string]*bintree{ - "fleet-eth.beta.json": &bintree{ConfigCliFleetEthBetaJson, map[string]*bintree{}}, - "fleet-eth.staging.json": &bintree{ConfigCliFleetEthStagingJson, map[string]*bintree{}}, - "fleet-eth.test.json": &bintree{ConfigCliFleetEthTestJson, map[string]*bintree{}}, - "les-enabled.json": &bintree{ConfigCliLesEnabledJson, map[string]*bintree{}}, + "fleet-eth.beta.json": &bintree{ConfigCliFleetEthBetaJson, map[string]*bintree{}}, + "fleet-eth.staging.json": &bintree{ConfigCliFleetEthStagingJson, map[string]*bintree{}}, + "fleet-eth.test.json": &bintree{ConfigCliFleetEthTestJson, map[string]*bintree{}}, + "les-enabled.json": &bintree{ConfigCliLesEnabledJson, map[string]*bintree{}}, "mailserver-enabled.json": &bintree{ConfigCliMailserverEnabledJson, map[string]*bintree{}}, }}, "status-chain-genesis.json": &bintree{ConfigStatusChainGenesisJson, map[string]*bintree{}}, }}, }}, "keys": &bintree{nil, map[string]*bintree{ - "bootnode.key": &bintree{keysBootnodeKey, map[string]*bintree{}}, - "firebaseauthkey": &bintree{keysFirebaseauthkey, map[string]*bintree{}}, + "bootnode.key": &bintree{keysBootnodeKey, map[string]*bintree{}}, + "firebaseauthkey": &bintree{keysFirebaseauthkey, map[string]*bintree{}}, "test-account1-status-chain.pk": &bintree{keysTestAccount1StatusChainPk, map[string]*bintree{}}, - "test-account1.pk": &bintree{keysTestAccount1Pk, map[string]*bintree{}}, + "test-account1.pk": &bintree{keysTestAccount1Pk, map[string]*bintree{}}, "test-account2-status-chain.pk": &bintree{keysTestAccount2StatusChainPk, map[string]*bintree{}}, - "test-account2.pk": &bintree{keysTestAccount2Pk, map[string]*bintree{}}, + "test-account2.pk": &bintree{keysTestAccount2Pk, map[string]*bintree{}}, "test-account3-before-eip55.pk": &bintree{keysTestAccount3BeforeEip55Pk, map[string]*bintree{}}, }}, }} -// RestoreAsset restores an asset under the given directory. +// RestoreAsset restores an asset under the given directory func RestoreAsset(dir, name string) error { data, err := Asset(name) if err != nil { @@ -565,10 +511,14 @@ func RestoreAsset(dir, name string) error { if err != nil { return err } - return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) + err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) + if err != nil { + return err + } + return nil } -// RestoreAssets restores an asset under the given directory recursively. +// RestoreAssets restores an asset under the given directory recursively func RestoreAssets(dir, name string) error { children, err := AssetDir(name) // File @@ -586,6 +536,7 @@ func RestoreAssets(dir, name string) error { } func _filePath(dir, name string) string { - canonicalName := strings.Replace(name, "\\", "/", -1) - return filepath.Join(append([]string{dir}, strings.Split(canonicalName, "/")...)...) + cannonicalName := strings.Replace(name, "\\", "/", -1) + return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) } + diff --git a/static/encryption_migrations/1536754952_initial_schema.down.sql b/static/chat_db_migrations/1536754952_initial_schema.down.sql similarity index 100% rename from static/encryption_migrations/1536754952_initial_schema.down.sql rename to static/chat_db_migrations/1536754952_initial_schema.down.sql diff --git a/static/encryption_migrations/1536754952_initial_schema.up.sql b/static/chat_db_migrations/1536754952_initial_schema.up.sql similarity index 100% rename from static/encryption_migrations/1536754952_initial_schema.up.sql rename to static/chat_db_migrations/1536754952_initial_schema.up.sql diff --git a/static/encryption_migrations/1539249977_update_ratchet_info.down.sql b/static/chat_db_migrations/1539249977_update_ratchet_info.down.sql similarity index 100% rename from static/encryption_migrations/1539249977_update_ratchet_info.down.sql rename to static/chat_db_migrations/1539249977_update_ratchet_info.down.sql diff --git a/static/encryption_migrations/1539249977_update_ratchet_info.up.sql b/static/chat_db_migrations/1539249977_update_ratchet_info.up.sql similarity index 100% rename from static/encryption_migrations/1539249977_update_ratchet_info.up.sql rename to static/chat_db_migrations/1539249977_update_ratchet_info.up.sql diff --git a/static/encryption_migrations/1540715431_add_version.down.sql b/static/chat_db_migrations/1540715431_add_version.down.sql similarity index 100% rename from static/encryption_migrations/1540715431_add_version.down.sql rename to static/chat_db_migrations/1540715431_add_version.down.sql diff --git a/static/encryption_migrations/1540715431_add_version.up.sql b/static/chat_db_migrations/1540715431_add_version.up.sql similarity index 100% rename from static/encryption_migrations/1540715431_add_version.up.sql rename to static/chat_db_migrations/1540715431_add_version.up.sql diff --git a/static/encryption_migrations/1541164797_add_installations.down.sql b/static/chat_db_migrations/1541164797_add_installations.down.sql similarity index 100% rename from static/encryption_migrations/1541164797_add_installations.down.sql rename to static/chat_db_migrations/1541164797_add_installations.down.sql diff --git a/static/encryption_migrations/1541164797_add_installations.up.sql b/static/chat_db_migrations/1541164797_add_installations.up.sql similarity index 100% rename from static/encryption_migrations/1541164797_add_installations.up.sql rename to static/chat_db_migrations/1541164797_add_installations.up.sql diff --git a/static/chat_db_migrations/1558084410_add_topic.down.sql b/static/chat_db_migrations/1558084410_add_topic.down.sql new file mode 100644 index 000000000..f6775d186 --- /dev/null +++ b/static/chat_db_migrations/1558084410_add_topic.down.sql @@ -0,0 +1,2 @@ +DROP TABLE topic_installation_ids; +DROP TABLE topics; diff --git a/static/chat_db_migrations/1558084410_add_topic.up.sql b/static/chat_db_migrations/1558084410_add_topic.up.sql new file mode 100644 index 000000000..44ef68d0f --- /dev/null +++ b/static/chat_db_migrations/1558084410_add_topic.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE topics ( + identity BLOB NOT NULL PRIMARY KEY ON CONFLICT IGNORE, + secret BLOB NOT NULL +); + +CREATE TABLE topic_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) +); diff --git a/static/encryption_migrations/static.go b/static/chat_db_migrations/static.go similarity index 82% rename from static/encryption_migrations/static.go rename to static/chat_db_migrations/static.go index a08d44d2a..ec9069288 100644 --- a/static/encryption_migrations/static.go +++ b/static/chat_db_migrations/static.go @@ -1,4 +1,4 @@ // Package static embeds static (JS, HTML) resources right into the binaries package static -//go:generate go-bindata -pkg migrations -o ../../services/shhext/chat/migrations/bindata.go . +//go:generate go-bindata -pkg migrations -o ../../services/shhext/chat/db/migrations/bindata.go . diff --git a/t/bindata.go b/t/bindata.go index fde0a1a18..f921feb9d 100644 --- a/t/bindata.go +++ b/t/bindata.go @@ -1,15 +1,15 @@ -// Code generated by go-bindata. DO NOT EDIT. +// Code generated by go-bindata. // sources: -// config/public-chain-accounts.json (307B) -// config/status-chain-accounts.json (543B) -// config/test-data.json (84B) +// config/public-chain-accounts.json +// config/status-chain-accounts.json +// config/test-data.json +// DO NOT EDIT! package t import ( "bytes" "compress/gzip" - "crypto/sha256" "fmt" "io" "io/ioutil" @@ -22,7 +22,7 @@ import ( func bindataRead(data []byte, name string) ([]byte, error) { gz, err := gzip.NewReader(bytes.NewBuffer(data)) if err != nil { - return nil, fmt.Errorf("read %q: %v", name, err) + return nil, fmt.Errorf("Read %q: %v", name, err) } var buf bytes.Buffer @@ -30,7 +30,7 @@ func bindataRead(data []byte, name string) ([]byte, error) { clErr := gz.Close() if err != nil { - return nil, fmt.Errorf("read %q: %v", name, err) + return nil, fmt.Errorf("Read %q: %v", name, err) } if clErr != nil { return nil, err @@ -40,9 +40,8 @@ func bindataRead(data []byte, name string) ([]byte, error) { } type asset struct { - bytes []byte - info os.FileInfo - digest [sha256.Size]byte + bytes []byte + info os.FileInfo } type bindataFileInfo struct { @@ -86,8 +85,8 @@ func configPublicChainAccountsJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "config/public-chain-accounts.json", size: 307, mode: os.FileMode(0644), modTime: time.Unix(1560158346, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x76, 0x5d, 0xc0, 0xfe, 0x57, 0x50, 0x18, 0xec, 0x2d, 0x61, 0x1b, 0xa9, 0x81, 0x11, 0x5f, 0x77, 0xf7, 0xb6, 0x67, 0x82, 0x1, 0x40, 0x68, 0x9d, 0xc5, 0x41, 0xaf, 0xce, 0x43, 0x81, 0x92, 0x96}} + info := bindataFileInfo{name: "config/public-chain-accounts.json", size: 307, mode: os.FileMode(420), modTime: time.Unix(1560418002, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -106,8 +105,8 @@ func configStatusChainAccountsJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "config/status-chain-accounts.json", size: 543, mode: os.FileMode(0644), modTime: time.Unix(1560158346, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x8e, 0xb3, 0x61, 0x51, 0x70, 0x3c, 0x12, 0x3e, 0xf1, 0x1c, 0x81, 0xfb, 0x9a, 0x7c, 0xe3, 0x63, 0xd0, 0x8f, 0x12, 0xc5, 0x2d, 0xf4, 0xea, 0x27, 0x33, 0xef, 0xca, 0xf9, 0x3f, 0x72, 0x44, 0xbf}} + info := bindataFileInfo{name: "config/status-chain-accounts.json", size: 543, mode: os.FileMode(420), modTime: time.Unix(1560418002, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -126,8 +125,8 @@ func configTestDataJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "config/test-data.json", size: 84, mode: os.FileMode(0644), modTime: time.Unix(1544697617, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xce, 0x9d, 0x80, 0xf5, 0x87, 0xfa, 0x57, 0x1d, 0xa1, 0xd5, 0x7a, 0x10, 0x3, 0xac, 0xd7, 0xf4, 0x64, 0x32, 0x96, 0x2b, 0xb7, 0x21, 0xb7, 0xa6, 0x80, 0x40, 0xe9, 0x65, 0xe3, 0xd6, 0xbd, 0x40}} + info := bindataFileInfo{name: "config/test-data.json", size: 84, mode: os.FileMode(420), modTime: time.Unix(1541418081, 0)} + a := &asset{bytes: bytes, info: info} return a, nil } @@ -135,8 +134,8 @@ func configTestDataJson() (*asset, error) { // It returns an error if the asset could not be found or // could not be loaded. func Asset(name string) ([]byte, error) { - canonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[canonicalName]; ok { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) @@ -146,12 +145,6 @@ func Asset(name string) ([]byte, error) { return nil, fmt.Errorf("Asset %s not found", name) } -// AssetString returns the asset contents as a string (instead of a []byte). -func AssetString(name string) (string, error) { - data, err := Asset(name) - return string(data), err -} - // MustAsset is like Asset but panics when Asset would return an error. // It simplifies safe initialization of global variables. func MustAsset(name string) []byte { @@ -163,18 +156,12 @@ func MustAsset(name string) []byte { return a } -// MustAssetString is like AssetString but panics when Asset would return an -// error. It simplifies safe initialization of global variables. -func MustAssetString(name string) string { - return string(MustAsset(name)) -} - // AssetInfo loads and returns the asset info for the given name. // It returns an error if the asset could not be found or // could not be loaded. func AssetInfo(name string) (os.FileInfo, error) { - canonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[canonicalName]; ok { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) @@ -184,33 +171,6 @@ func AssetInfo(name string) (os.FileInfo, error) { return nil, fmt.Errorf("AssetInfo %s not found", name) } -// AssetDigest returns the digest of the file with the given name. It returns an -// error if the asset could not be found or the digest could not be loaded. -func AssetDigest(name string) ([sha256.Size]byte, error) { - canonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[canonicalName]; ok { - a, err := f() - if err != nil { - return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s can't read by error: %v", name, err) - } - return a.digest, nil - } - return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s not found", name) -} - -// Digests returns a map of all known files and their checksums. -func Digests() (map[string][sha256.Size]byte, error) { - mp := make(map[string][sha256.Size]byte, len(_bindata)) - for name := range _bindata { - a, err := _bindata[name]() - if err != nil { - return nil, err - } - mp[name] = a.digest - } - return mp, nil -} - // AssetNames returns the names of the assets. func AssetNames() []string { names := make([]string, 0, len(_bindata)) @@ -223,9 +183,7 @@ func AssetNames() []string { // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "config/public-chain-accounts.json": configPublicChainAccountsJson, - "config/status-chain-accounts.json": configStatusChainAccountsJson, - "config/test-data.json": configTestDataJson, } @@ -238,15 +196,15 @@ var _bindata = map[string]func() (*asset, error){ // img/ // a.png // b.png -// then AssetDir("data") would return []string{"foo.txt", "img"}, -// AssetDir("data/img") would return []string{"a.png", "b.png"}, -// AssetDir("foo.txt") and AssetDir("notexist") would return an error, and +// then AssetDir("data") would return []string{"foo.txt", "img"} +// AssetDir("data/img") would return []string{"a.png", "b.png"} +// AssetDir("foo.txt") and AssetDir("notexist") would return an error // AssetDir("") will return []string{"data"}. func AssetDir(name string) ([]string, error) { node := _bintree if len(name) != 0 { - canonicalName := strings.Replace(name, "\\", "/", -1) - pathList := strings.Split(canonicalName, "/") + cannonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(cannonicalName, "/") for _, p := range pathList { node = node.Children[p] if node == nil { @@ -268,16 +226,15 @@ type bintree struct { Func func() (*asset, error) Children map[string]*bintree } - var _bintree = &bintree{nil, map[string]*bintree{ "config": &bintree{nil, map[string]*bintree{ "public-chain-accounts.json": &bintree{configPublicChainAccountsJson, map[string]*bintree{}}, "status-chain-accounts.json": &bintree{configStatusChainAccountsJson, map[string]*bintree{}}, - "test-data.json": &bintree{configTestDataJson, map[string]*bintree{}}, + "test-data.json": &bintree{configTestDataJson, map[string]*bintree{}}, }}, }} -// RestoreAsset restores an asset under the given directory. +// RestoreAsset restores an asset under the given directory func RestoreAsset(dir, name string) error { data, err := Asset(name) if err != nil { @@ -295,10 +252,14 @@ func RestoreAsset(dir, name string) error { if err != nil { return err } - return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) + err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) + if err != nil { + return err + } + return nil } -// RestoreAssets restores an asset under the given directory recursively. +// RestoreAssets restores an asset under the given directory recursively func RestoreAssets(dir, name string) error { children, err := AssetDir(name) // File @@ -316,6 +277,7 @@ func RestoreAssets(dir, name string) error { } func _filePath(dir, name string) string { - canonicalName := strings.Replace(name, "\\", "/", -1) - return filepath.Join(append([]string{dir}, strings.Split(canonicalName, "/")...)...) + cannonicalName := strings.Replace(name, "\\", "/", -1) + return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) } +