status-go/services/shhext/chat/protocol.go

318 lines
10 KiB
Go

package chat
import (
"crypto/ecdsa"
"errors"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/services/shhext/chat/multidevice"
"github.com/status-im/status-go/services/shhext/chat/protobuf"
"github.com/status-im/status-go/services/shhext/chat/sharedsecret"
)
const ProtocolVersion = 1
const sharedSecretNegotiationVersion = 1
const partitionedTopicMinVersion = 1
type ProtocolService struct {
log log.Logger
encryption *EncryptionService
secret *sharedsecret.Service
multidevice *multidevice.Service
addedBundlesHandler func([]multidevice.IdentityAndIDPair)
onNewSharedSecretHandler func([]*sharedsecret.Secret)
Enabled bool
}
var ErrNotProtocolMessage = errors.New("Not a protocol message")
// NewProtocolService creates a new ProtocolService instance
func NewProtocolService(encryption *EncryptionService, secret *sharedsecret.Service, multidevice *multidevice.Service, addedBundlesHandler func([]multidevice.IdentityAndIDPair), onNewSharedSecretHandler func([]*sharedsecret.Secret)) *ProtocolService {
return &ProtocolService{
log: log.New("package", "status-go/services/sshext.chat"),
encryption: encryption,
secret: secret,
multidevice: multidevice,
addedBundlesHandler: addedBundlesHandler,
onNewSharedSecretHandler: onNewSharedSecretHandler,
}
}
func (p *ProtocolService) addBundle(myIdentityKey *ecdsa.PrivateKey, msg *protobuf.ProtocolMessage, sendSingle bool) (*protobuf.ProtocolMessage, error) {
// Get a bundle
installations, err := p.multidevice.GetOurActiveInstallations(&myIdentityKey.PublicKey)
if err != nil {
return nil, err
}
bundle, err := p.encryption.CreateBundle(myIdentityKey, installations)
if err != nil {
p.log.Error("encryption-service", "error creating bundle", err)
return nil, err
}
if sendSingle {
// DEPRECATED: This is only for backward compatibility, remove once not
// an issue anymore
msg.Bundle = bundle
} else {
msg.Bundles = []*protobuf.Bundle{bundle}
}
return msg, nil
}
// BuildPublicMessage marshals a public chat message given the user identity private key and a payload
func (p *ProtocolService) BuildPublicMessage(myIdentityKey *ecdsa.PrivateKey, payload []byte) (*protobuf.ProtocolMessage, error) {
// Build message not encrypted
protocolMessage := &protobuf.ProtocolMessage{
InstallationId: p.encryption.config.InstallationID,
PublicMessage: payload,
}
return p.addBundle(myIdentityKey, protocolMessage, false)
}
type ProtocolMessageSpec struct {
Message *protobuf.ProtocolMessage
// Installations is the targeted devices
Installations []*multidevice.Installation
// SharedSecret is a shared secret established among the installations
SharedSecret []byte
}
func (p *ProtocolMessageSpec) MinVersion() uint32 {
var version uint32
for _, installation := range p.Installations {
if installation.Version < version {
version = installation.Version
}
}
return version
}
func (p *ProtocolMessageSpec) PartitionedTopic() bool {
return p.MinVersion() >= partitionedTopicMinVersion
}
// BuildDirectMessage returns a 1:1 chat message and optionally a negotiated topic given the user identity private key, the recipient's public key, and a payload
func (p *ProtocolService) BuildDirectMessage(myIdentityKey *ecdsa.PrivateKey, publicKey *ecdsa.PublicKey, payload []byte) (*ProtocolMessageSpec, error) {
activeInstallations, err := p.multidevice.GetActiveInstallations(publicKey)
if err != nil {
return nil, err
}
// Encrypt payload
encryptionResponse, installations, err := p.encryption.EncryptPayload(publicKey, myIdentityKey, activeInstallations, payload)
if err != nil {
p.log.Error("encryption-service", "error encrypting payload", err)
return nil, err
}
// Build message
protocolMessage := &protobuf.ProtocolMessage{
InstallationId: p.encryption.config.InstallationID,
DirectMessage: encryptionResponse,
}
msg, err := p.addBundle(myIdentityKey, protocolMessage, true)
if err != nil {
return nil, err
}
// Check who we are sending the message to, and see if we have a shared secret
// across devices
var installationIDs []string
var sharedSecret *sharedsecret.Secret
var agreed bool
for installationID := range protocolMessage.GetDirectMessage() {
if installationID != noInstallationID {
installationIDs = append(installationIDs, installationID)
}
}
sharedSecret, agreed, err = p.secret.Send(myIdentityKey, p.encryption.config.InstallationID, publicKey, installationIDs)
if err != nil {
return nil, err
}
// Call handler
if sharedSecret != nil {
p.onNewSharedSecretHandler([]*sharedsecret.Secret{sharedSecret})
}
response := &ProtocolMessageSpec{
Message: msg,
Installations: installations,
}
if agreed {
response.SharedSecret = sharedSecret.Key
}
return response, nil
}
// BuildDHMessage builds a message with DH encryption so that it can be decrypted by any other device.
func (p *ProtocolService) BuildDHMessage(myIdentityKey *ecdsa.PrivateKey, destination *ecdsa.PublicKey, payload []byte) (*ProtocolMessageSpec, error) {
// Encrypt payload
encryptionResponse, err := p.encryption.EncryptPayloadWithDH(destination, payload)
if err != nil {
p.log.Error("encryption-service", "error encrypting payload", err)
return nil, err
}
// Build message
protocolMessage := &protobuf.ProtocolMessage{
InstallationId: p.encryption.config.InstallationID,
DirectMessage: encryptionResponse,
}
msg, err := p.addBundle(myIdentityKey, protocolMessage, true)
if err != nil {
return nil, err
}
return &ProtocolMessageSpec{Message: msg}, nil
}
// ProcessPublicBundle processes a received X3DH bundle.
func (p *ProtocolService) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, bundle *protobuf.Bundle) ([]multidevice.IdentityAndIDPair, error) {
if err := p.encryption.ProcessPublicBundle(myIdentityKey, bundle); err != nil {
return nil, err
}
theirIdentityKey, err := ExtractIdentity(bundle)
if err != nil {
return nil, err
}
return p.multidevice.ProcessPublicBundle(myIdentityKey, theirIdentityKey, bundle)
}
// GetBundle retrieves or creates a X3DH bundle, given a private identity key.
func (p *ProtocolService) GetBundle(myIdentityKey *ecdsa.PrivateKey) (*protobuf.Bundle, error) {
installations, err := p.multidevice.GetOurActiveInstallations(&myIdentityKey.PublicKey)
if err != nil {
return nil, err
}
return p.encryption.CreateBundle(myIdentityKey, installations)
}
// EnableInstallation enables an installation for multi-device sync.
func (p *ProtocolService) EnableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error {
return p.multidevice.EnableInstallation(myIdentityKey, installationID)
}
// DisableInstallation disables an installation for multi-device sync.
func (p *ProtocolService) DisableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error {
return p.multidevice.DisableInstallation(myIdentityKey, installationID)
}
// GetPublicBundle retrieves a public bundle given an identity
func (p *ProtocolService) GetPublicBundle(theirIdentityKey *ecdsa.PublicKey) (*protobuf.Bundle, error) {
installations, err := p.multidevice.GetActiveInstallations(theirIdentityKey)
if err != nil {
return nil, err
}
return p.encryption.GetPublicBundle(theirIdentityKey, installations)
}
// ConfirmMessagesProcessed confirms and deletes message keys for the given messages
func (p *ProtocolService) ConfirmMessagesProcessed(messageIDs [][]byte) error {
return p.encryption.ConfirmMessagesProcessed(messageIDs)
}
// HandleMessage unmarshals a message and processes it, decrypting it if it is a 1:1 message.
func (p *ProtocolService) HandleMessage(myIdentityKey *ecdsa.PrivateKey, theirPublicKey *ecdsa.PublicKey, protocolMessage *protobuf.ProtocolMessage, messageID []byte) ([]byte, error) {
if p.encryption == nil {
return nil, errors.New("encryption service not initialized")
}
// Process bundle, deprecated, here for backward compatibility
if bundle := protocolMessage.GetBundle(); bundle != nil {
// Should we stop processing if the bundle cannot be verified?
addedBundles, err := p.ProcessPublicBundle(myIdentityKey, bundle)
if err != nil {
return nil, err
}
p.addedBundlesHandler(addedBundles)
}
// Process bundles
for _, bundle := range protocolMessage.GetBundles() {
// Should we stop processing if the bundle cannot be verified?
addedBundles, err := p.ProcessPublicBundle(myIdentityKey, bundle)
if err != nil {
return nil, err
}
p.addedBundlesHandler(addedBundles)
}
// Check if it's a public message
if publicMessage := protocolMessage.GetPublicMessage(); publicMessage != nil {
// Nothing to do, as already in cleartext
return publicMessage, nil
}
// Decrypt message
if directMessage := protocolMessage.GetDirectMessage(); directMessage != nil {
message, err := p.encryption.DecryptPayload(myIdentityKey, theirPublicKey, protocolMessage.GetInstallationId(), directMessage, messageID)
if err != nil {
return nil, err
}
var bundles []*protobuf.Bundle
p.log.Info("Checking version")
// Handle protocol negotiation for compatible clients
p.log.Info("bundle", "bundles", protocolMessage)
bundles = append(protocolMessage.GetBundles(), protocolMessage.GetBundle())
version := getProtocolVersion(bundles, protocolMessage.GetInstallationId())
if version >= sharedSecretNegotiationVersion {
p.log.Info("Version greater than 1 negotianting")
sharedSecret, err := p.secret.Receive(myIdentityKey, theirPublicKey, protocolMessage.GetInstallationId())
if err != nil {
return nil, err
}
p.onNewSharedSecretHandler([]*sharedsecret.Secret{sharedSecret})
}
return message, nil
}
// Return error
return nil, errors.New("no payload")
}
func getProtocolVersion(bundles []*protobuf.Bundle, installationID string) uint32 {
if installationID == "" {
return 0
}
for _, bundle := range bundles {
if bundle != nil {
signedPreKeys := bundle.GetSignedPreKeys()
if signedPreKeys == nil {
continue
}
signedPreKey := signedPreKeys[installationID]
if signedPreKey == nil {
return 0
}
return signedPreKey.GetProtocolVersion()
}
}
return 0
}