2019-07-17 22:25:42 +00:00
package encryption
import (
"bytes"
"crypto/ecdsa"
2019-07-30 06:14:13 +00:00
"database/sql"
2019-07-17 22:25:42 +00:00
"fmt"
"go.uber.org/zap"
"github.com/ethereum/go-ethereum/crypto"
"github.com/pkg/errors"
2019-11-21 16:19:22 +00:00
"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"
2019-07-17 22:25:42 +00:00
)
//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 [ ] byte
// 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
onAddedBundlesHandler func ( [ ] * multidevice . Installation )
onNewSharedSecretHandler func ( [ ] * sharedsecret . Secret )
onSendContactCodeHandler func ( * ProtocolMessageSpec )
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 (
2019-07-30 06:14:13 +00:00
db * sql . DB ,
2019-07-17 22:25:42 +00:00
installationID string ,
addedBundlesHandler func ( [ ] * multidevice . Installation ) ,
onNewSharedSecretHandler func ( [ ] * sharedsecret . Secret ) ,
onSendContactCodeHandler func ( * ProtocolMessageSpec ) ,
logger * zap . Logger ,
2019-07-30 06:14:13 +00:00
) * Protocol {
2019-07-17 22:25:42 +00:00
return NewWithEncryptorConfig (
2019-07-30 06:14:13 +00:00
db ,
2019-07-17 22:25:42 +00:00
installationID ,
defaultEncryptorConfig ( installationID , logger ) ,
addedBundlesHandler ,
onNewSharedSecretHandler ,
onSendContactCodeHandler ,
logger ,
)
}
2019-07-30 06:14:13 +00:00
// DB and migrations are shared between encryption package
// and its sub-packages.
2019-07-17 22:25:42 +00:00
func NewWithEncryptorConfig (
2019-07-30 06:14:13 +00:00
db * sql . DB ,
2019-07-17 22:25:42 +00:00
installationID string ,
encryptorConfig encryptorConfig ,
addedBundlesHandler func ( [ ] * multidevice . Installation ) ,
onNewSharedSecretHandler func ( [ ] * sharedsecret . Secret ) ,
onSendContactCodeHandler func ( * ProtocolMessageSpec ) ,
logger * zap . Logger ,
2019-07-30 06:14:13 +00:00
) * Protocol {
2019-07-17 22:25:42 +00:00
return & Protocol {
2019-07-30 06:14:13 +00:00
encryptor : newEncryptor ( db , encryptorConfig ) ,
2019-07-17 22:25:42 +00:00
secret : sharedsecret . New ( db , logger ) ,
multidevice : multidevice . New ( db , & multidevice . Config {
MaxInstallations : 3 ,
ProtocolVersion : protocolVersion ,
InstallationID : installationID ,
} ) ,
2019-08-27 12:04:15 +00:00
publisher : publisher . New ( logger ) ,
2019-07-17 22:25:42 +00:00
onAddedBundlesHandler : addedBundlesHandler ,
onNewSharedSecretHandler : onNewSharedSecretHandler ,
onSendContactCodeHandler : onSendContactCodeHandler ,
logger : logger . With ( zap . Namespace ( "Protocol" ) ) ,
2019-07-30 06:14:13 +00:00
}
2019-07-17 22:25:42 +00:00
}
func ( p * Protocol ) Start ( myIdentity * ecdsa . PrivateKey ) error {
// Propagate currently cached shared secrets.
secrets , err := p . secret . All ( )
if err != nil {
return errors . Wrap ( err , "failed to get all secrets" )
}
p . onNewSharedSecretHandler ( secrets )
// Handle Publisher system messages.
publisherCh := p . publisher . Start ( )
go func ( ) {
for range publisherCh {
messageSpec , err := p . buildContactCodeMessage ( myIdentity )
if err != nil {
p . logger . Error ( "failed to build contact code message" ,
zap . String ( "site" , "Start" ) ,
zap . Error ( err ) )
continue
}
p . onSendContactCodeHandler ( messageSpec )
}
} ( )
return nil
}
2019-09-26 07:01:17 +00:00
func ( p * Protocol ) addBundle ( myIdentityKey * ecdsa . PrivateKey , msg * ProtocolMessage ) error {
2019-07-17 22:25:42 +00:00
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
}
2019-09-26 07:01:17 +00:00
msg . Bundles = [ ] * Bundle { bundle }
2019-07-17 22:25:42 +00:00
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 ,
}
2019-09-26 07:01:17 +00:00
err := p . addBundle ( myIdentityKey , message )
2019-07-17 22:25:42 +00:00
if err != nil {
return nil , err
}
return & ProtocolMessageSpec { Message : message , Public : true } , nil
}
// buildContactCodeMessage creates a contact code message. It's a public message
// without any data but it carries bundle information.
func ( p * Protocol ) buildContactCodeMessage ( myIdentityKey * ecdsa . PrivateKey ) ( * ProtocolMessageSpec , error ) {
return p . BuildPublicMessage ( myIdentityKey , 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 ,
}
2019-09-26 07:01:17 +00:00
err = p . addBundle ( myIdentityKey , message )
2019-07-17 22:25:42 +00:00
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 ) )
// Call handler
if sharedSecret != nil {
p . onNewSharedSecretHandler ( [ ] * sharedsecret . Secret { sharedSecret } )
}
spec := & ProtocolMessageSpec {
Message : message ,
Installations : installations ,
}
if agreed {
spec . SharedSecret = sharedSecret . Key
}
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 ,
}
2019-09-26 07:01:17 +00:00
err = p . addBundle ( myIdentityKey , message )
2019-07-17 22:25:42 +00:00
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 )
}
// 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 {
2019-08-20 11:20:25 +00:00
logger := p . logger . With ( zap . String ( "site" , "ConfirmMessageProcessed" ) )
logger . Debug ( "confirming message" , zap . Binary ( "message-id" , messageID ) )
2019-07-17 22:25:42 +00:00
return p . encryptor . ConfirmMessageProcessed ( messageID )
}
// 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 ,
) ( [ ] byte , error ) {
logger := p . logger . With ( zap . String ( "site" , "HandleMessage" ) )
2019-08-20 11:20:25 +00:00
logger . Debug ( "received a protocol message" , zap . Binary ( "sender-public-key" , crypto . FromECDSAPub ( theirPublicKey ) ) , zap . Binary ( "message-id" , messageID ) )
2019-07-17 22:25:42 +00:00
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?
addedBundles , err := p . ProcessPublicBundle ( myIdentityKey , bundle )
if err != nil {
return nil , err
}
p . onAddedBundlesHandler ( addedBundles )
}
// 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
return publicMessage , 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
}
2019-09-26 07:01:17 +00:00
bundles := protocolMessage . GetBundles ( )
2019-07-17 22:25:42 +00:00
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
}
p . onNewSharedSecretHandler ( [ ] * sharedsecret . Secret { sharedSecret } )
}
return message , 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
}