status-go/protocol/encryption/protocol.go

498 lines
15 KiB
Go

package encryption
import (
"bytes"
"crypto/ecdsa"
"database/sql"
"fmt"
"go.uber.org/zap"
"github.com/pkg/errors"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/protocol/encryption/multidevice"
"github.com/status-im/status-go/protocol/encryption/publisher"
"github.com/status-im/status-go/protocol/encryption/sharedsecret"
)
//go:generate protoc --go_out=. ./protocol_message.proto
const (
protocolVersion = 1
sharedSecretNegotiationVersion = 1
partitionedTopicMinVersion = 1
defaultMinVersion = 0
)
type PartitionTopicMode int
const (
PartitionTopicNoSupport PartitionTopicMode = iota
PartitionTopicV1
)
type ProtocolMessageSpec struct {
Message *ProtocolMessage
// Installations is the targeted devices
Installations []*multidevice.Installation
// SharedSecret is a shared secret established among the installations
SharedSecret *sharedsecret.Secret
// AgreedSecret indicates whether the shared secret has been agreed
AgreedSecret bool
// Public means that the spec contains a public wrapped message
Public bool
}
func (p *ProtocolMessageSpec) MinVersion() uint32 {
if len(p.Installations) == 0 {
return defaultMinVersion
}
version := p.Installations[0].Version
for _, installation := range p.Installations[1:] {
if installation.Version < version {
version = installation.Version
}
}
return version
}
func (p *ProtocolMessageSpec) PartitionedTopicMode() PartitionTopicMode {
if p.MinVersion() >= partitionedTopicMinVersion {
return PartitionTopicV1
}
return PartitionTopicNoSupport
}
type Protocol struct {
encryptor *encryptor
secret *sharedsecret.SharedSecret
multidevice *multidevice.Multidevice
publisher *publisher.Publisher
subscriptions *Subscriptions
logger *zap.Logger
}
var (
// ErrNoPayload means that there was no payload found in the received protocol message.
ErrNoPayload = errors.New("no payload")
)
// New creates a new ProtocolService instance
func New(
db *sql.DB,
installationID string,
logger *zap.Logger,
) *Protocol {
return NewWithEncryptorConfig(
db,
installationID,
defaultEncryptorConfig(installationID, logger),
logger,
)
}
// DB and migrations are shared between encryption package
// and its sub-packages.
func NewWithEncryptorConfig(
db *sql.DB,
installationID string,
encryptorConfig encryptorConfig,
logger *zap.Logger,
) *Protocol {
return &Protocol{
encryptor: newEncryptor(db, encryptorConfig),
secret: sharedsecret.New(db, logger),
multidevice: multidevice.New(db, &multidevice.Config{
MaxInstallations: 3,
ProtocolVersion: protocolVersion,
InstallationID: installationID,
}),
publisher: publisher.New(logger),
logger: logger.With(zap.Namespace("Protocol")),
}
}
type Subscriptions struct {
SharedSecrets []*sharedsecret.Secret
SendContactCode <-chan struct{}
Quit chan struct{}
}
func (p *Protocol) Start(myIdentity *ecdsa.PrivateKey) (*Subscriptions, error) {
// Propagate currently cached shared secrets.
secrets, err := p.secret.All()
if err != nil {
return nil, errors.Wrap(err, "failed to get all secrets")
}
p.subscriptions = &Subscriptions{
SharedSecrets: secrets,
SendContactCode: p.publisher.Start(),
Quit: make(chan struct{}),
}
return p.subscriptions, nil
}
func (p *Protocol) Stop() error {
p.publisher.Stop()
if p.subscriptions != nil {
close(p.subscriptions.Quit)
}
return nil
}
func (p *Protocol) addBundle(myIdentityKey *ecdsa.PrivateKey, msg *ProtocolMessage) error {
logger := p.logger.With(zap.String("site", "addBundle"))
// Get a bundle
installations, err := p.multidevice.GetOurActiveInstallations(&myIdentityKey.PublicKey)
if err != nil {
return err
}
logger.Info("adding bundle to the message",
zap.Any("installations", installations),
)
bundle, err := p.encryptor.CreateBundle(myIdentityKey, installations)
if err != nil {
return err
}
msg.Bundles = []*Bundle{bundle}
return nil
}
// BuildPublicMessage marshals a public chat message given the user identity private key and a payload
func (p *Protocol) BuildPublicMessage(myIdentityKey *ecdsa.PrivateKey, payload []byte) (*ProtocolMessageSpec, error) {
// Build message not encrypted
message := &ProtocolMessage{
InstallationId: p.encryptor.config.InstallationID,
PublicMessage: payload,
}
err := p.addBundle(myIdentityKey, message)
if err != nil {
return nil, err
}
return &ProtocolMessageSpec{Message: message, Public: true}, nil
}
// 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 *Protocol) BuildDirectMessage(myIdentityKey *ecdsa.PrivateKey, publicKey *ecdsa.PublicKey, payload []byte) (*ProtocolMessageSpec, error) {
logger := p.logger.With(
zap.String("site", "BuildDirectMessage"),
zap.Binary("public-key", crypto.FromECDSAPub(publicKey)),
)
logger.Debug("building direct message")
// Get recipients installations.
activeInstallations, err := p.multidevice.GetActiveInstallations(publicKey)
if err != nil {
return nil, err
}
// Encrypt payload
directMessage, installations, err := p.encryptor.EncryptPayload(publicKey, myIdentityKey, activeInstallations, payload)
if err != nil {
return nil, err
}
// Build message
message := &ProtocolMessage{
InstallationId: p.encryptor.config.InstallationID,
DirectMessage: directMessage,
}
err = p.addBundle(myIdentityKey, message)
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
for installationID := range message.GetDirectMessage() {
if installationID != noInstallationID {
installationIDs = append(installationIDs, installationID)
}
}
sharedSecret, agreed, err := p.secret.Agreed(myIdentityKey, p.encryptor.config.InstallationID, publicKey, installationIDs)
if err != nil {
return nil, err
}
logger.Debug("shared secret agreement",
zap.Bool("has-shared-secret", sharedSecret != nil),
zap.Bool("agreed", agreed))
spec := &ProtocolMessageSpec{
SharedSecret: sharedSecret,
AgreedSecret: agreed,
Message: message,
Installations: installations,
}
return spec, nil
}
// BuildDHMessage builds a message with DH encryption so that it can be decrypted by any other device.
func (p *Protocol) BuildDHMessage(myIdentityKey *ecdsa.PrivateKey, destination *ecdsa.PublicKey, payload []byte) (*ProtocolMessageSpec, error) {
// Encrypt payload
encryptionResponse, err := p.encryptor.EncryptPayloadWithDH(destination, payload)
if err != nil {
return nil, err
}
// Build message
message := &ProtocolMessage{
InstallationId: p.encryptor.config.InstallationID,
DirectMessage: encryptionResponse,
}
err = p.addBundle(myIdentityKey, message)
if err != nil {
return nil, err
}
return &ProtocolMessageSpec{Message: message}, nil
}
// ProcessPublicBundle processes a received X3DH bundle.
func (p *Protocol) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, bundle *Bundle) ([]*multidevice.Installation, error) {
logger := p.logger.With(zap.String("site", "ProcessPublicBundle"))
logger.Debug("processing public bundle")
if err := p.encryptor.ProcessPublicBundle(myIdentityKey, bundle); err != nil {
return nil, err
}
installations, enabled, err := p.recoverInstallationsFromBundle(myIdentityKey, bundle)
if err != nil {
return nil, err
}
logger.Debug("recovered installations",
zap.Int("installations", len(installations)),
zap.Bool("enabled", enabled))
// TODO(adam): why do we add installations using identity obtained from GetIdentity()
// instead of the output of crypto.CompressPubkey()? I tried the second option
// and the unit tests TestTopic and TestMaxDevices fail.
identityFromBundle := bundle.GetIdentity()
theirIdentity, err := ExtractIdentity(bundle)
if err != nil {
logger.Panic("unrecoverable error extracting identity", zap.Error(err))
}
compressedIdentity := crypto.CompressPubkey(theirIdentity)
if !bytes.Equal(identityFromBundle, compressedIdentity) {
logger.Panic("identity from bundle and compressed are not equal")
}
return p.multidevice.AddInstallations(bundle.GetIdentity(), bundle.GetTimestamp(), installations, enabled)
}
// recoverInstallationsFromBundle extracts installations from the bundle.
// It returns extracted installations and true if the installations
// are ours, i.e. the bundle was created by our identity key.
func (p *Protocol) recoverInstallationsFromBundle(myIdentityKey *ecdsa.PrivateKey, bundle *Bundle) ([]*multidevice.Installation, bool, error) {
var installations []*multidevice.Installation
theirIdentity, err := ExtractIdentity(bundle)
if err != nil {
return nil, false, err
}
logger := p.logger.With(zap.String("site", "recoverInstallationsFromBundle"))
myIdentityStr := fmt.Sprintf("0x%x", crypto.FromECDSAPub(&myIdentityKey.PublicKey))
theirIdentityStr := fmt.Sprintf("0x%x", crypto.FromECDSAPub(theirIdentity))
// Any device from other peers will be considered enabled, ours needs to
// be explicitly enabled.
enabled := theirIdentityStr != myIdentityStr
signedPreKeys := bundle.GetSignedPreKeys()
for installationID, signedPreKey := range signedPreKeys {
logger.Info("recovered installation", zap.String("installation-id", installationID))
if installationID != p.multidevice.InstallationID() {
installations = append(installations, &multidevice.Installation{
Identity: theirIdentityStr,
ID: installationID,
Version: signedPreKey.GetProtocolVersion(),
})
}
}
return installations, enabled, nil
}
// GetBundle retrieves or creates a X3DH bundle, given a private identity key.
func (p *Protocol) GetBundle(myIdentityKey *ecdsa.PrivateKey) (*Bundle, error) {
installations, err := p.multidevice.GetOurActiveInstallations(&myIdentityKey.PublicKey)
if err != nil {
return nil, err
}
return p.encryptor.CreateBundle(myIdentityKey, installations)
}
// EnableInstallation enables an installation for multi-device sync.
func (p *Protocol) EnableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error {
return p.multidevice.EnableInstallation(myIdentityKey, installationID)
}
// DisableInstallation disables an installation for multi-device sync.
func (p *Protocol) DisableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error {
return p.multidevice.DisableInstallation(myIdentityKey, installationID)
}
// GetOurInstallations returns all the installations available given an identity
func (p *Protocol) GetOurInstallations(myIdentityKey *ecdsa.PublicKey) ([]*multidevice.Installation, error) {
return p.multidevice.GetOurInstallations(myIdentityKey)
}
// GetOurActiveInstallations returns all the active installations available given an identity
func (p *Protocol) GetOurActiveInstallations(myIdentityKey *ecdsa.PublicKey) ([]*multidevice.Installation, error) {
return p.multidevice.GetOurActiveInstallations(myIdentityKey)
}
// SetInstallationMetadata sets the metadata for our own installation
func (p *Protocol) SetInstallationMetadata(myIdentityKey *ecdsa.PublicKey, installationID string, data *multidevice.InstallationMetadata) error {
return p.multidevice.SetInstallationMetadata(myIdentityKey, installationID, data)
}
// GetPublicBundle retrieves a public bundle given an identity
func (p *Protocol) GetPublicBundle(theirIdentityKey *ecdsa.PublicKey) (*Bundle, error) {
installations, err := p.multidevice.GetActiveInstallations(theirIdentityKey)
if err != nil {
return nil, err
}
return p.encryptor.GetPublicBundle(theirIdentityKey, installations)
}
// ConfirmMessageProcessed confirms and deletes message keys for the given messages
func (p *Protocol) ConfirmMessageProcessed(messageID []byte) error {
logger := p.logger.With(zap.String("site", "ConfirmMessageProcessed"))
logger.Debug("confirming message", zap.Binary("message-id", messageID))
return p.encryptor.ConfirmMessageProcessed(messageID)
}
type DecryptMessageResponse struct {
DecryptedMessage []byte
Installations []*multidevice.Installation
SharedSecrets []*sharedsecret.Secret
}
// HandleMessage unmarshals a message and processes it, decrypting it if it is a 1:1 message.
func (p *Protocol) HandleMessage(
myIdentityKey *ecdsa.PrivateKey,
theirPublicKey *ecdsa.PublicKey,
protocolMessage *ProtocolMessage,
messageID []byte,
) (*DecryptMessageResponse, error) {
logger := p.logger.With(zap.String("site", "HandleMessage"))
response := &DecryptMessageResponse{}
logger.Debug("received a protocol message", zap.Binary("sender-public-key", crypto.FromECDSAPub(theirPublicKey)), zap.Binary("message-id", messageID))
if p.encryptor == nil {
return nil, errors.New("encryption service not initialized")
}
// Process bundles
for _, bundle := range protocolMessage.GetBundles() {
// Should we stop processing if the bundle cannot be verified?
newInstallations, err := p.ProcessPublicBundle(myIdentityKey, bundle)
if err != nil {
return nil, err
}
response.Installations = newInstallations
}
// Check if it's a public message
if publicMessage := protocolMessage.GetPublicMessage(); publicMessage != nil {
logger.Debug("received a public message in direct message")
// Nothing to do, as already in cleartext
response.DecryptedMessage = publicMessage
return response, nil
}
// Decrypt message
if directMessage := protocolMessage.GetDirectMessage(); directMessage != nil {
logger.Debug("processing direct message")
message, err := p.encryptor.DecryptPayload(
myIdentityKey,
theirPublicKey,
protocolMessage.GetInstallationId(),
directMessage,
messageID,
)
if err != nil {
return nil, err
}
bundles := protocolMessage.GetBundles()
version := getProtocolVersion(bundles, protocolMessage.GetInstallationId())
logger.Debug("direct message version", zap.Uint32("version", version))
if version >= sharedSecretNegotiationVersion {
logger.Debug("negotiating shared secret",
zap.Binary("public-key", crypto.FromECDSAPub(theirPublicKey)))
sharedSecret, err := p.secret.Generate(myIdentityKey, theirPublicKey, protocolMessage.GetInstallationId())
if err != nil {
return nil, err
}
response.SharedSecrets = []*sharedsecret.Secret{sharedSecret}
}
response.DecryptedMessage = message
return response, nil
}
// Return error
return nil, ErrNoPayload
}
func (p *Protocol) ShouldAdvertiseBundle(publicKey *ecdsa.PublicKey, time int64) (bool, error) {
return p.publisher.ShouldAdvertiseBundle(publicKey, time)
}
func (p *Protocol) ConfirmBundleAdvertisement(publicKey *ecdsa.PublicKey, time int64) {
p.publisher.SetLastAck(publicKey, time)
}
func (p *Protocol) BuildBundleAdvertiseMessage(myIdentityKey *ecdsa.PrivateKey, publicKey *ecdsa.PublicKey) (*ProtocolMessageSpec, error) {
return p.BuildDHMessage(myIdentityKey, publicKey, nil)
}
func getProtocolVersion(bundles []*Bundle, installationID string) uint32 {
if installationID == "" {
return defaultMinVersion
}
for _, bundle := range bundles {
if bundle != nil {
signedPreKeys := bundle.GetSignedPreKeys()
if signedPreKeys == nil {
continue
}
signedPreKey := signedPreKeys[installationID]
if signedPreKey == nil {
return defaultMinVersion
}
return signedPreKey.GetProtocolVersion()
}
}
return defaultMinVersion
}