Add GetContactCode call, add DH flag (#1367)

This PR does a few things:

1) Add a call GetContactCode to check whether we have a bundle for a
given user.

2) Add a DH flag to the API (non-breaking change), for those messages
that we want to target all devices (contact-requests for example).

3) Fixes a few small issues with installations, namely if for example a
messages is sent without a bundle (currently not done by any client),
we still infer installation info, so that we can communicate securely
and making it truly optional.
This commit is contained in:
Andrea Maria Piana 2019-02-12 12:07:13 +01:00 committed by GitHub
parent a249151d05
commit 2a4382369a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 187 additions and 137 deletions

View File

@ -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()

View File

@ -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)

View File

@ -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)
}

View File

@ -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
for key, message := range protocolMessages {
msg.PubKey = crypto.FromECDSAPub(key)
// Enrich with transport layer info
whisperMessage := chat.DirectMessageToWhisper(msg, message)
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
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)
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 err == chat.ErrDeviceNotFound && privateKey.PublicKey != *publicKey {
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)
return nil
} else if err != nil {
// Ignore errors for now as those might be non-pfs messages
api.log.Error("Failed handling message with error", "err", err)
return nil
}
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)
// Add unencrypted payload
msg.Payload = response
}
return nil
}

View File

@ -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
}

View File

@ -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,9 +64,7 @@ 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 {
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 {
@ -78,22 +78,13 @@ func (p *ProtocolService) BuildDirectMessage(myIdentityKey *ecdsa.PrivateKey, pa
DirectMessage: encryptionResponse,
}
payload, err := p.addBundleAndMarshal(myIdentityKey, protocolMessage, true)
if err != nil {
return nil, err
}
if len(payload) != 0 {
response[publicKey] = payload
}
}
return response, nil
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

View File

@ -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)

View File

@ -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
}

View File

@ -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

View File

@ -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 {