diff --git a/api/backend.go b/api/backend.go index cfd554124..6f8c49eee 100644 --- a/api/backend.go +++ b/api/backend.go @@ -2,6 +2,7 @@ package api import ( "context" + "encoding/hex" "errors" "fmt" "math/big" @@ -556,6 +557,35 @@ func (b *StatusBackend) CreateContactCode() (string, error) { return bundle.ToBase64() } +// GetContactCode return the latest contact code +func (b *StatusBackend) GetContactCode(identity string) (string, error) { + st, err := b.statusNode.ShhExtService() + if err != nil { + return "", err + } + + publicKeyBytes, err := hex.DecodeString(identity) + if err != nil { + return "", err + } + + publicKey, err := ethcrypto.UnmarshalPubkey(publicKeyBytes) + if err != nil { + return "", err + } + + bundle, err := st.GetPublicBundle(publicKey) + if err != nil { + return "", err + } + + if bundle == nil { + return "", nil + } + + return bundle.ToBase64() +} + // ProcessContactCode process and adds the someone else's bundle func (b *StatusBackend) ProcessContactCode(contactCode string) error { selectedChatAccount, err := b.AccountManager().SelectedChatAccount() diff --git a/lib/library.go b/lib/library.go index a0c360cdf..ec2db1093 100644 --- a/lib/library.go +++ b/lib/library.go @@ -70,6 +70,24 @@ func ProcessContactCode(bundleString *C.char) *C.char { return nil } +// Get an X3DH bundle +//export GetContactCode +func GetContactCode(identityString *C.char) *C.char { + bundle, err := statusBackend.GetContactCode(C.GoString(identityString)) + if err != nil { + return makeJSONResponse(err) + } + + data, err := json.Marshal(struct { + ContactCode string `json:"code"` + }{ContactCode: bundle}) + if err != nil { + return makeJSONResponse(err) + } + + return C.CString(string(data)) +} + //export ExtractIdentityFromContactCode func ExtractIdentityFromContactCode(bundleString *C.char) *C.char { bundle := C.GoString(bundleString) diff --git a/mobile/status.go b/mobile/status.go index 2d7ee912d..8f4f7f35c 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -501,3 +501,21 @@ func SetMobileSignalHandler(handler SignalHandler) { func SetSignalEventCallback(cb unsafe.Pointer) { signal.SetSignalEventCallback(cb) } + +// Get an X3DH bundle +//export GetContactCode +func GetContactCode(identity string) string { + bundle, err := statusBackend.GetContactCode(identity) + if err != nil { + return makeJSONResponse(err) + } + + data, err := json.Marshal(struct { + ContactCode string `json:"code"` + }{ContactCode: bundle}) + if err != nil { + return makeJSONResponse(err) + } + + return string(data) +} diff --git a/services/shhext/api.go b/services/shhext/api.go index 3dcd148e2..28ea95d5f 100644 --- a/services/shhext/api.go +++ b/services/shhext/api.go @@ -422,7 +422,7 @@ func (api *PublicAPI) SendPublicMessage(ctx context.Context, msg chat.SendPublic } // SendDirectMessage sends a 1:1 chat message to the underlying transport -func (api *PublicAPI) SendDirectMessage(ctx context.Context, msg chat.SendDirectMessageRPC) ([]hexutil.Bytes, error) { +func (api *PublicAPI) SendDirectMessage(ctx context.Context, msg chat.SendDirectMessageRPC) (hexutil.Bytes, error) { if !api.service.pfsEnabled { return nil, ErrPFSNotEnabled } @@ -438,29 +438,26 @@ func (api *PublicAPI) SendDirectMessage(ctx context.Context, msg chat.SendDirect } // This is transport layer-agnostic - protocolMessages, err := api.service.protocol.BuildDirectMessage(privateKey, msg.Payload, publicKey) + var protocolMessage []byte + + if msg.DH { + protocolMessage, err = api.service.protocol.BuildDHMessage(privateKey, &privateKey.PublicKey, msg.Payload) + } else { + protocolMessage, err = api.service.protocol.BuildDirectMessage(privateKey, publicKey, msg.Payload) + } + if err != nil { return nil, err } - var response []hexutil.Bytes + // Enrich with transport layer info + whisperMessage := chat.DirectMessageToWhisper(msg, protocolMessage) - for key, message := range protocolMessages { - msg.PubKey = crypto.FromECDSAPub(key) - // Enrich with transport layer info - whisperMessage := chat.DirectMessageToWhisper(msg, message) - - // And dispatch - hash, err := api.Post(ctx, whisperMessage) - if err != nil { - return nil, err - } - response = append(response, hash) - - } - return response, nil + // And dispatch + return api.Post(ctx, whisperMessage) } +// 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 { @@ -477,7 +474,7 @@ func (api *PublicAPI) SendPairingMessage(ctx context.Context, msg chat.SendDirec return nil, err } - protocolMessage, err := api.service.protocol.BuildPairingMessage(privateKey, msg.Payload) + protocolMessage, err := api.service.protocol.BuildDHMessage(privateKey, &privateKey.PublicKey, msg.Payload) if err != nil { return nil, err } @@ -497,57 +494,6 @@ func (api *PublicAPI) SendPairingMessage(ctx context.Context, msg chat.SendDirec return response, nil } -// SendGroupMessage sends a group messag chat message to the underlying transport -func (api *PublicAPI) SendGroupMessage(ctx context.Context, msg chat.SendGroupMessageRPC) ([]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 - } - - var keys []*ecdsa.PublicKey - - for _, k := range msg.PubKeys { - publicKey, err := crypto.UnmarshalPubkey(k) - if err != nil { - return nil, err - } - keys = append(keys, publicKey) - } - - // This is transport layer-agnostic - protocolMessages, err := api.service.protocol.BuildDirectMessage(privateKey, msg.Payload, keys...) - if err != nil { - return nil, err - } - - var response []hexutil.Bytes - - for key, message := range protocolMessages { - directMessage := chat.SendDirectMessageRPC{ - PubKey: crypto.FromECDSAPub(key), - Payload: msg.Payload, - Sig: msg.Sig, - } - - // Enrich with transport layer info - whisperMessage := chat.DirectMessageToWhisper(directMessage, message) - - // 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(msg *whisper.Message) error { privateKeyID := api.service.w.SelectedKeyPairID() @@ -567,21 +513,26 @@ func (api *PublicAPI) processPFSMessage(msg *whisper.Message) error { response, err := api.service.protocol.HandleMessage(privateKey, publicKey, msg.Payload) - // Notify that someone tried to contact us using an invalid bundle - if err == chat.ErrDeviceNotFound && privateKey.PublicKey != *publicKey { - api.log.Warn("Device not found, sending signal", "err", err) - keyString := fmt.Sprintf("0x%x", crypto.FromECDSAPub(publicKey)) - handler := EnvelopeSignalHandler{} - handler.DecryptMessageFailed(keyString) - return nil - } else if err != nil { - // Ignore errors for now as those might be non-pfs messages + switch err { + case nil: + // Set the decrypted payload + msg.Payload = response + case chat.ErrDeviceNotFound: + // Notify that someone tried to contact us using an invalid bundle + if privateKey.PublicKey != *publicKey { + api.log.Warn("Device not found, sending signal", "err", err) + keyString := fmt.Sprintf("0x%x", crypto.FromECDSAPub(publicKey)) + handler := EnvelopeSignalHandler{} + handler.DecryptMessageFailed(keyString) + } + 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 - } - // Add unencrypted payload - msg.Payload = response + } return nil } diff --git a/services/shhext/chat/encryption.go b/services/shhext/chat/encryption.go index ecf072741..680cc625b 100644 --- a/services/shhext/chat/encryption.go +++ b/services/shhext/chat/encryption.go @@ -285,6 +285,11 @@ func (s *EncryptionService) DecryptPayload(myIdentityKey *ecdsa.PrivateKey, thei return nil, err } + // Add installations with a timestamp of 0, as we don't have bundle informations + if err = s.persistence.AddInstallations(theirIdentityKeyC, 0, []string{theirInstallationID}, true); err != nil { + return nil, err + } + // We mark the exchange as successful so we stop sending x3dh header if err = s.persistence.RatchetInfoConfirmed(drHeader.GetId(), theirIdentityKeyC, theirInstallationID); err != nil { s.log.Error("Could not confirm ratchet info", "err", err) @@ -450,13 +455,8 @@ func (s *EncryptionService) EncryptPayloadWithDH(theirIdentityKey *ecdsa.PublicK return response, nil } -// EncryptPayload returns a new DirectMessageProtocol with a given payload encrypted, given a recipient's public key and the sender private identity key -// TODO: refactor this -// nolint: gocyclo -func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, myIdentityKey *ecdsa.PrivateKey, payload []byte) (map[string]*DirectMessageProtocol, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - +// GetPublicBundle returns the active installations bundles for a given user +func (s *EncryptionService) GetPublicBundle(theirIdentityKey *ecdsa.PublicKey) (*Bundle, error) { theirIdentityKeyC := ecrypto.CompressPubkey(theirIdentityKey) installationIDs, err := s.persistence.GetActiveInstallations(s.config.MaxInstallations, theirIdentityKeyC) @@ -464,25 +464,43 @@ func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, my return nil, err } - // Get their latest bundle - theirBundle, err := s.persistence.GetPublicBundle(theirIdentityKey, installationIDs) + return s.persistence.GetPublicBundle(theirIdentityKey, installationIDs) +} + +// EncryptPayload returns a new DirectMessageProtocol with a given payload encrypted, given a recipient's public key and the sender private identity key +// TODO: refactor this +// nolint: gocyclo +func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, myIdentityKey *ecdsa.PrivateKey, payload []byte) (map[string]*DirectMessageProtocol, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + s.log.Debug("Sending message", "theirKey", theirIdentityKey) + + theirIdentityKeyC := ecrypto.CompressPubkey(theirIdentityKey) + + // Get their installationIds + installationIds, err := s.persistence.GetActiveInstallations(s.config.MaxInstallations, theirIdentityKeyC) if err != nil { return nil, err } // We don't have any, send a message with DH - if theirBundle == nil && !bytes.Equal(theirIdentityKeyC, ecrypto.CompressPubkey(&myIdentityKey.PublicKey)) { + if installationIds == nil && !bytes.Equal(theirIdentityKeyC, ecrypto.CompressPubkey(&myIdentityKey.PublicKey)) { return s.EncryptPayloadWithDH(theirIdentityKey, payload) } response := make(map[string]*DirectMessageProtocol) - for installationID, signedPreKeyContainer := range theirBundle.GetSignedPreKeys() { + for _, installationID := range installationIds { + s.log.Debug("Processing installation", "installationID", installationID) if s.config.InstallationID == installationID { continue } + bundle, err := s.persistence.GetPublicBundle(theirIdentityKey, []string{installationID}) + if err != nil { + return nil, err + } - theirSignedPreKey := signedPreKeyContainer.GetSignedPreKey() // See if a session is there already drInfo, err := s.persistence.GetAnyRatchetInfo(theirIdentityKeyC, installationID) if err != nil { @@ -490,6 +508,7 @@ func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, my } if drInfo != nil { + s.log.Debug("Found DR info", "installationID", installationID) encryptedPayload, drHeader, err := s.encryptUsingDR(theirIdentityKey, drInfo, payload) if err != nil { return nil, err @@ -511,6 +530,18 @@ func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, my continue } + theirSignedPreKeyContainer := bundle.GetSignedPreKeys()[installationID] + + // This should not be nil at this point + if theirSignedPreKeyContainer == nil { + s.log.Warn("Could not find either a ratchet info or a bundle for installationId", "installationID", installationID) + continue + + } + s.log.Debug("DR info not found, using bundle", "installationID", installationID) + + theirSignedPreKey := theirSignedPreKeyContainer.GetSignedPreKey() + sharedKey, ourEphemeralKey, err := s.keyFromActiveX3DH(theirIdentityKeyC, theirSignedPreKey, myIdentityKey) if err != nil { return nil, err @@ -549,5 +580,7 @@ func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, my } } + s.log.Debug("Built message", "theirKey", theirIdentityKey) + return response, nil } diff --git a/services/shhext/chat/protocol.go b/services/shhext/chat/protocol.go index e234faeb8..0268cf6f7 100644 --- a/services/shhext/chat/protocol.go +++ b/services/shhext/chat/protocol.go @@ -15,6 +15,8 @@ type ProtocolService struct { Enabled bool } +var ErrNotProtocolMessage = errors.New("Not a protocol message") + // NewProtocolService creates a new ProtocolService instance func NewProtocolService(encryption *EncryptionService, addedBundlesHandler func([]IdentityAndIDPair)) *ProtocolService { return &ProtocolService{ @@ -62,38 +64,27 @@ func (p *ProtocolService) BuildPublicMessage(myIdentityKey *ecdsa.PrivateKey, pa } // 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, payload []byte, theirPublicKeys ...*ecdsa.PublicKey) (map[*ecdsa.PublicKey][]byte, error) { - response := make(map[*ecdsa.PublicKey][]byte) - for _, publicKey := range theirPublicKeys { - // 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 - } - - // Build message - protocolMessage := &ProtocolMessage{ - InstallationId: p.encryption.config.InstallationID, - DirectMessage: encryptionResponse, - } - - payload, err := p.addBundleAndMarshal(myIdentityKey, protocolMessage, true) - if err != nil { - return nil, err - } - - if len(payload) != 0 { - response[publicKey] = payload - } +func (p *ProtocolService) BuildDirectMessage(myIdentityKey *ecdsa.PrivateKey, publicKey *ecdsa.PublicKey, payload []byte) ([]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 response, nil + + // Build message + protocolMessage := &ProtocolMessage{ + InstallationId: p.encryption.config.InstallationID, + DirectMessage: encryptionResponse, + } + + return p.addBundleAndMarshal(myIdentityKey, protocolMessage, true) } -// BuildPairingMessage sends a message to our own devices using DH so that it can be decrypted by any other device. -func (p *ProtocolService) BuildPairingMessage(myIdentityKey *ecdsa.PrivateKey, payload []byte) ([]byte, error) { +// 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) { // Encrypt payload - encryptionResponse, err := p.encryption.EncryptPayloadWithDH(&myIdentityKey.PublicKey, payload) + encryptionResponse, err := p.encryption.EncryptPayloadWithDH(destination, payload) if err != nil { p.log.Error("encryption-service", "error encrypting payload", err) return nil, err @@ -128,6 +119,11 @@ func (p *ProtocolService) DisableInstallation(myIdentityKey *ecdsa.PublicKey, in return p.encryption.DisableInstallation(myIdentityKey, installationID) } +// GetPublicBundle retrieves a public bundle given an identity +func (p *ProtocolService) GetPublicBundle(theirIdentityKey *ecdsa.PublicKey) (*Bundle, error) { + return p.encryption.GetPublicBundle(theirIdentityKey) +} + // 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) ([]byte, error) { if p.encryption == nil { @@ -138,7 +134,7 @@ func (p *ProtocolService) HandleMessage(myIdentityKey *ecdsa.PrivateKey, theirPu protocolMessage := &ProtocolMessage{} if err := proto.Unmarshal(payload, protocolMessage); err != nil { - return nil, err + return nil, ErrNotProtocolMessage } // Process bundle, deprecated, here for backward compatibility diff --git a/services/shhext/chat/protocol_test.go b/services/shhext/chat/protocol_test.go index b12fcb7fb..cdaef982b 100644 --- a/services/shhext/chat/protocol_test.go +++ b/services/shhext/chat/protocol_test.go @@ -80,13 +80,12 @@ func (s *ProtocolServiceTestSuite) TestBuildDirectMessage() { }) s.NoError(err) - marshaledMsg, err := s.alice.BuildDirectMessage(aliceKey, payload, &bobKey.PublicKey, &aliceKey.PublicKey) + marshaledMsg, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, payload) s.NoError(err) s.NotNil(marshaledMsg, "It creates a message") - s.NotNil(marshaledMsg[&aliceKey.PublicKey], "It creates a single message") unmarshaledMsg := &ProtocolMessage{} - err = proto.Unmarshal(marshaledMsg[&bobKey.PublicKey], unmarshaledMsg) + err = proto.Unmarshal(marshaledMsg, unmarshaledMsg) s.NoError(err) s.NotNilf(unmarshaledMsg.GetBundle(), "It adds a bundle to the message") @@ -116,12 +115,12 @@ func (s *ProtocolServiceTestSuite) TestBuildAndReadDirectMessage() { s.NoError(err) // Message is sent with DH - marshaledMsg, err := s.alice.BuildDirectMessage(aliceKey, marshaledPayload, &bobKey.PublicKey) + marshaledMsg, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, marshaledPayload) s.NoError(err) // Bob is able to decrypt the message - unmarshaledMsg, err := s.bob.HandleMessage(bobKey, &aliceKey.PublicKey, marshaledMsg[&bobKey.PublicKey]) + unmarshaledMsg, err := s.bob.HandleMessage(bobKey, &aliceKey.PublicKey, marshaledMsg) s.NoError(err) s.NotNil(unmarshaledMsg) diff --git a/services/shhext/chat/rpc.go b/services/shhext/chat/rpc.go index 7592146de..6e622e318 100644 --- a/services/shhext/chat/rpc.go +++ b/services/shhext/chat/rpc.go @@ -20,11 +20,5 @@ type SendDirectMessageRPC struct { Chat string Payload hexutil.Bytes PubKey hexutil.Bytes -} - -// SendGroupMessageRPC represents the RPC payload for the SendGroupMessage RPC method -type SendGroupMessageRPC struct { - Sig string - Payload hexutil.Bytes - PubKeys []hexutil.Bytes + DH bool } diff --git a/services/shhext/chat/sql_lite_persistence.go b/services/shhext/chat/sql_lite_persistence.go index 3c1b4cd82..2345df4d3 100644 --- a/services/shhext/chat/sql_lite_persistence.go +++ b/services/shhext/chat/sql_lite_persistence.go @@ -983,11 +983,13 @@ func (s *SQLLitePersistence) AddInstallations(identity []byte, timestamp int64, return err } - // We update timestamp if present without changing enabled + // We update timestamp if present without changing enabled, only if this is a new bundle if err != sql.ErrNoRows { stmt, err = tx.Prepare(`UPDATE installations SET timestamp = ?, enabled = ? - WHERE identity = ? AND installation_id = ?`) + WHERE identity = ? + AND installation_id = ? + AND timestamp < ?`) if err != nil { return err } @@ -997,6 +999,7 @@ func (s *SQLLitePersistence) AddInstallations(identity []byte, timestamp int64, oldEnabled, identity, installationID, + timestamp, ) if err != nil { return err diff --git a/services/shhext/service.go b/services/shhext/service.go index b0475e45c..c4bbbd029 100644 --- a/services/shhext/service.go +++ b/services/shhext/service.go @@ -204,6 +204,14 @@ func (s *Service) EnableInstallation(myIdentityKey *ecdsa.PublicKey, installatio return s.protocol.EnableInstallation(myIdentityKey, installationID) } +func (s *Service) GetPublicBundle(identityKey *ecdsa.PublicKey) (*chat.Bundle, error) { + if s.protocol == nil { + return nil, errProtocolNotInitialized + } + + return s.protocol.GetPublicBundle(identityKey) +} + // DisableInstallation disables an installation for multi-device sync. func (s *Service) DisableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error { if s.protocol == nil {