2019-07-08 09:21:21 +00:00
package encryption
import (
2019-07-16 10:43:07 +00:00
"bytes"
2019-07-08 09:21:21 +00:00
"crypto/ecdsa"
"errors"
"fmt"
2019-07-16 10:43:07 +00:00
"log"
2019-07-08 09:21:21 +00:00
"github.com/ethereum/go-ethereum/crypto"
"github.com/status-im/status-protocol-go/encryption/multidevice"
2019-07-16 10:43:07 +00:00
"github.com/status-im/status-protocol-go/encryption/publisher"
2019-07-08 09:21:21 +00:00
"github.com/status-im/status-protocol-go/encryption/sharedsecret"
)
//go:generate protoc --go_out=. ./protocol_message.proto
2019-07-16 10:43:07 +00:00
const (
protocolVersion = 1
sharedSecretNegotiationVersion = 1
partitionedTopicMinVersion = 1
defaultMinVersion = 0
)
2019-07-08 09:21:21 +00:00
2019-07-16 10:43:07 +00:00
type PartitionTopicMode int
2019-07-08 09:21:21 +00:00
const (
2019-07-16 10:43:07 +00:00
PartitionTopicNoSupport PartitionTopicMode = iota
2019-07-08 09:21:21 +00:00
PartitionTopicV1
)
2019-07-16 10:43:07 +00:00
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 )
2019-07-08 09:21:21 +00:00
onNewSharedSecretHandler func ( [ ] * sharedsecret . Secret )
2019-07-16 10:43:07 +00:00
onSendContactCodeHandler func ( * ProtocolMessageSpec )
2019-07-08 09:21:21 +00:00
}
var (
2019-07-16 10:43:07 +00:00
// ErrNoPayload means that there was no payload found in the received protocol message.
ErrNoPayload = errors . New ( "no payload" )
2019-07-08 09:21:21 +00:00
)
2019-07-16 10:43:07 +00:00
// New creates a new ProtocolService instance
func New (
dataDir string ,
dbKey string ,
installationID string ,
addedBundlesHandler func ( [ ] * multidevice . Installation ) ,
onNewSharedSecretHandler func ( [ ] * sharedsecret . Secret ) ,
onSendContactCodeHandler func ( * ProtocolMessageSpec ) ,
) ( * Protocol , error ) {
return NewWithEncryptorConfig (
dataDir ,
dbKey ,
installationID ,
defaultEncryptorConfig ( installationID ) ,
addedBundlesHandler ,
onNewSharedSecretHandler ,
onSendContactCodeHandler ,
)
}
func NewWithEncryptorConfig (
dataDir string ,
dbKey string ,
installationID string ,
encryptorConfig encryptorConfig ,
addedBundlesHandler func ( [ ] * multidevice . Installation ) ,
onNewSharedSecretHandler func ( [ ] * sharedsecret . Secret ) ,
onSendContactCodeHandler func ( * ProtocolMessageSpec ) ,
) ( * Protocol , error ) {
encryptor , err := newEncryptor ( dataDir , dbKey , encryptorConfig )
if err != nil {
return nil , err
2019-07-08 09:21:21 +00:00
}
2019-07-16 10:43:07 +00:00
// DB and migrations are shared between encryption package
// and its sub-packages.
db := encryptor . persistence . DB
return & Protocol {
encryptor : encryptor ,
secret : sharedsecret . New ( db ) ,
multidevice : multidevice . New ( db , & multidevice . Config {
MaxInstallations : 3 ,
ProtocolVersion : protocolVersion ,
InstallationID : installationID ,
} ) ,
publisher : publisher . New ( db ) ,
onAddedBundlesHandler : addedBundlesHandler ,
onNewSharedSecretHandler : onNewSharedSecretHandler ,
onSendContactCodeHandler : onSendContactCodeHandler ,
} , nil
2019-07-08 09:21:21 +00:00
}
2019-07-16 10:43:07 +00:00
func ( p * Protocol ) Start ( myIdentity * ecdsa . PrivateKey ) {
// Handle Publisher system messages.
publisherCh := p . publisher . Start ( )
go func ( ) {
for range publisherCh {
messageSpec , err := p . buildContactCodeMessage ( myIdentity )
if err != nil {
log . Printf ( "[Protocol::Start] failed to build contact code message: %v" , err )
continue
}
p . onSendContactCodeHandler ( messageSpec )
}
} ( )
}
2019-07-08 09:21:21 +00:00
2019-07-16 10:43:07 +00:00
func ( p * Protocol ) addBundle ( myIdentityKey * ecdsa . PrivateKey , msg * ProtocolMessage , sendSingle bool ) error {
2019-07-08 09:21:21 +00:00
// Get a bundle
installations , err := p . multidevice . GetOurActiveInstallations ( & myIdentityKey . PublicKey )
if err != nil {
2019-07-16 10:43:07 +00:00
return err
2019-07-08 09:21:21 +00:00
}
2019-07-16 10:43:07 +00:00
log . Printf ( "[Protocol::addBundle] adding bundle to the message with installatoins %#v" , installations )
bundle , err := p . encryptor . CreateBundle ( myIdentityKey , installations )
2019-07-08 09:21:21 +00:00
if err != nil {
2019-07-16 10:43:07 +00:00
return err
2019-07-08 09:21:21 +00:00
}
if sendSingle {
// DEPRECATED: This is only for backward compatibility, remove once not
// an issue anymore
msg . Bundle = bundle
} else {
msg . Bundles = [ ] * Bundle { bundle }
}
2019-07-16 10:43:07 +00:00
return nil
2019-07-08 09:21:21 +00:00
}
// BuildPublicMessage marshals a public chat message given the user identity private key and a payload
2019-07-16 10:43:07 +00:00
func ( p * Protocol ) BuildPublicMessage ( myIdentityKey * ecdsa . PrivateKey , payload [ ] byte ) ( * ProtocolMessageSpec , error ) {
2019-07-08 09:21:21 +00:00
// Build message not encrypted
2019-07-16 10:43:07 +00:00
message := & ProtocolMessage {
InstallationId : p . encryptor . config . InstallationID ,
2019-07-08 09:21:21 +00:00
PublicMessage : payload ,
}
2019-07-16 10:43:07 +00:00
err := p . addBundle ( myIdentityKey , message , false )
if err != nil {
return nil , err
2019-07-08 09:21:21 +00:00
}
2019-07-16 10:43:07 +00:00
return & ProtocolMessageSpec { Message : message , Public : true } , nil
2019-07-08 09:21:21 +00:00
}
2019-07-16 10:43:07 +00:00
// 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 )
2019-07-08 09:21:21 +00:00
}
// 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
2019-07-16 10:43:07 +00:00
func ( p * Protocol ) BuildDirectMessage ( myIdentityKey * ecdsa . PrivateKey , publicKey * ecdsa . PublicKey , payload [ ] byte ) ( * ProtocolMessageSpec , error ) {
log . Printf ( "[Protocol::BuildDirectMessage] to %#x" , crypto . FromECDSAPub ( publicKey ) )
// Get recipients installations.
2019-07-08 09:21:21 +00:00
activeInstallations , err := p . multidevice . GetActiveInstallations ( publicKey )
if err != nil {
return nil , err
}
// Encrypt payload
2019-07-16 10:43:07 +00:00
directMessage , installations , err := p . encryptor . EncryptPayload ( publicKey , myIdentityKey , activeInstallations , payload )
2019-07-08 09:21:21 +00:00
if err != nil {
return nil , err
}
// Build message
2019-07-16 10:43:07 +00:00
message := & ProtocolMessage {
InstallationId : p . encryptor . config . InstallationID ,
DirectMessage : directMessage ,
2019-07-08 09:21:21 +00:00
}
2019-07-16 10:43:07 +00:00
err = p . addBundle ( myIdentityKey , message , true )
2019-07-08 09:21:21 +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
2019-07-16 10:43:07 +00:00
for installationID := range message . GetDirectMessage ( ) {
2019-07-08 09:21:21 +00:00
if installationID != noInstallationID {
installationIDs = append ( installationIDs , installationID )
}
}
2019-07-16 10:43:07 +00:00
sharedSecret , agreed , err := p . secret . Agreed ( myIdentityKey , p . encryptor . config . InstallationID , publicKey , installationIDs )
2019-07-08 09:21:21 +00:00
if err != nil {
return nil , err
}
2019-07-16 10:43:07 +00:00
log . Printf ( "[Protocol::BuildDirectMessage] found shared secret %t and agreed %t" , sharedSecret != nil , agreed )
2019-07-08 09:21:21 +00:00
// Call handler
if sharedSecret != nil {
p . onNewSharedSecretHandler ( [ ] * sharedsecret . Secret { sharedSecret } )
}
2019-07-16 10:43:07 +00:00
spec := & ProtocolMessageSpec {
Message : message ,
2019-07-08 09:21:21 +00:00
Installations : installations ,
}
if agreed {
2019-07-16 10:43:07 +00:00
spec . SharedSecret = sharedSecret . Key
2019-07-08 09:21:21 +00:00
}
2019-07-16 10:43:07 +00:00
return spec , nil
2019-07-08 09:21:21 +00:00
}
// BuildDHMessage builds a message with DH encryption so that it can be decrypted by any other device.
2019-07-16 10:43:07 +00:00
func ( p * Protocol ) BuildDHMessage ( myIdentityKey * ecdsa . PrivateKey , destination * ecdsa . PublicKey , payload [ ] byte ) ( * ProtocolMessageSpec , error ) {
2019-07-08 09:21:21 +00:00
// Encrypt payload
2019-07-16 10:43:07 +00:00
encryptionResponse , err := p . encryptor . EncryptPayloadWithDH ( destination , payload )
2019-07-08 09:21:21 +00:00
if err != nil {
return nil , err
}
// Build message
2019-07-16 10:43:07 +00:00
message := & ProtocolMessage {
InstallationId : p . encryptor . config . InstallationID ,
2019-07-08 09:21:21 +00:00
DirectMessage : encryptionResponse ,
}
2019-07-16 10:43:07 +00:00
err = p . addBundle ( myIdentityKey , message , true )
2019-07-08 09:21:21 +00:00
if err != nil {
return nil , err
}
2019-07-16 10:43:07 +00:00
return & ProtocolMessageSpec { Message : message } , nil
2019-07-08 09:21:21 +00:00
}
// ProcessPublicBundle processes a received X3DH bundle.
2019-07-16 10:43:07 +00:00
func ( p * Protocol ) ProcessPublicBundle ( myIdentityKey * ecdsa . PrivateKey , bundle * Bundle ) ( [ ] * multidevice . Installation , error ) {
log . Printf ( "[Protocol::ProcessPublicBundle] processing public bundle" )
2019-07-08 09:21:21 +00:00
2019-07-16 10:43:07 +00:00
if err := p . encryptor . ProcessPublicBundle ( myIdentityKey , bundle ) ; err != nil {
2019-07-08 09:21:21 +00:00
return nil , err
}
2019-07-16 10:43:07 +00:00
installations , enabled , err := p . recoverInstallationsFromBundle ( myIdentityKey , bundle )
2019-07-08 09:21:21 +00:00
if err != nil {
return nil , err
}
2019-07-16 10:43:07 +00:00
log . Printf ( "[Protocol::ProcessPublicBundle] recovered %d installation enabled %t" , len ( installations ) , enabled )
2019-07-08 09:21:21 +00:00
// 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.
2019-07-16 10:43:07 +00:00
identityFromBundle := bundle . GetIdentity ( )
theirIdentity , err := ExtractIdentity ( bundle )
if err != nil {
panic ( err )
}
compressedIdentity := crypto . CompressPubkey ( theirIdentity )
if ! bytes . Equal ( identityFromBundle , compressedIdentity ) {
panic ( "identity from bundle and compressed are not equal" )
}
return p . multidevice . AddInstallations ( bundle . GetIdentity ( ) , bundle . GetTimestamp ( ) , installations , enabled )
2019-07-08 09:21:21 +00:00
}
// 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.
2019-07-16 10:43:07 +00:00
func ( p * Protocol ) recoverInstallationsFromBundle ( myIdentityKey * ecdsa . PrivateKey , bundle * Bundle ) ( [ ] * multidevice . Installation , bool , error ) {
2019-07-08 09:21:21 +00:00
var installations [ ] * multidevice . Installation
theirIdentity , err := ExtractIdentity ( bundle )
if err != nil {
return nil , false , err
}
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
2019-07-16 10:43:07 +00:00
// be explicitly enabled.
enabled := theirIdentityStr != myIdentityStr
2019-07-08 09:21:21 +00:00
signedPreKeys := bundle . GetSignedPreKeys ( )
for installationID , signedPreKey := range signedPreKeys {
2019-07-16 10:43:07 +00:00
log . Printf ( "[Protocol::recoverInstallationsFromBundle] recovered installation %s" , installationID )
2019-07-08 09:21:21 +00:00
if installationID != p . multidevice . InstallationID ( ) {
installations = append ( installations , & multidevice . Installation {
Identity : theirIdentityStr ,
ID : installationID ,
Version : signedPreKey . GetProtocolVersion ( ) ,
} )
}
}
2019-07-16 10:43:07 +00:00
return installations , enabled , nil
2019-07-08 09:21:21 +00:00
}
// GetBundle retrieves or creates a X3DH bundle, given a private identity key.
2019-07-16 10:43:07 +00:00
func ( p * Protocol ) GetBundle ( myIdentityKey * ecdsa . PrivateKey ) ( * Bundle , error ) {
2019-07-08 09:21:21 +00:00
installations , err := p . multidevice . GetOurActiveInstallations ( & myIdentityKey . PublicKey )
if err != nil {
return nil , err
}
2019-07-16 10:43:07 +00:00
return p . encryptor . CreateBundle ( myIdentityKey , installations )
2019-07-08 09:21:21 +00:00
}
// EnableInstallation enables an installation for multi-device sync.
2019-07-16 10:43:07 +00:00
func ( p * Protocol ) EnableInstallation ( myIdentityKey * ecdsa . PublicKey , installationID string ) error {
2019-07-08 09:21:21 +00:00
return p . multidevice . EnableInstallation ( myIdentityKey , installationID )
}
// DisableInstallation disables an installation for multi-device sync.
2019-07-16 10:43:07 +00:00
func ( p * Protocol ) DisableInstallation ( myIdentityKey * ecdsa . PublicKey , installationID string ) error {
2019-07-08 09:21:21 +00:00
return p . multidevice . DisableInstallation ( myIdentityKey , installationID )
}
// GetOurInstallations returns all the installations available given an identity
2019-07-16 10:43:07 +00:00
func ( p * Protocol ) GetOurInstallations ( myIdentityKey * ecdsa . PublicKey ) ( [ ] * multidevice . Installation , error ) {
2019-07-08 09:21:21 +00:00
return p . multidevice . GetOurInstallations ( myIdentityKey )
}
// SetInstallationMetadata sets the metadata for our own installation
2019-07-16 10:43:07 +00:00
func ( p * Protocol ) SetInstallationMetadata ( myIdentityKey * ecdsa . PublicKey , installationID string , data * multidevice . InstallationMetadata ) error {
2019-07-08 09:21:21 +00:00
return p . multidevice . SetInstallationMetadata ( myIdentityKey , installationID , data )
}
// GetPublicBundle retrieves a public bundle given an identity
2019-07-16 10:43:07 +00:00
func ( p * Protocol ) GetPublicBundle ( theirIdentityKey * ecdsa . PublicKey ) ( * Bundle , error ) {
2019-07-08 09:21:21 +00:00
installations , err := p . multidevice . GetActiveInstallations ( theirIdentityKey )
if err != nil {
return nil , err
}
2019-07-16 10:43:07 +00:00
return p . encryptor . GetPublicBundle ( theirIdentityKey , installations )
2019-07-08 09:21:21 +00:00
}
2019-07-16 10:43:07 +00:00
// ConfirmMessageProcessed confirms and deletes message keys for the given messages
func ( p * Protocol ) ConfirmMessageProcessed ( messageID [ ] byte ) error {
return p . encryptor . ConfirmMessageProcessed ( messageID )
2019-07-08 09:21:21 +00:00
}
// HandleMessage unmarshals a message and processes it, decrypting it if it is a 1:1 message.
2019-07-16 10:43:07 +00:00
func ( p * Protocol ) HandleMessage (
myIdentityKey * ecdsa . PrivateKey ,
theirPublicKey * ecdsa . PublicKey ,
protocolMessage * ProtocolMessage ,
messageID [ ] byte ,
) ( [ ] byte , error ) {
log . Printf ( "[Protocol::HandleMessage] received a protocol message from %#x" , crypto . FromECDSAPub ( theirPublicKey ) )
if p . encryptor == nil {
2019-07-08 09:21:21 +00:00
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
}
2019-07-16 10:43:07 +00:00
p . onAddedBundlesHandler ( addedBundles )
2019-07-08 09:21:21 +00:00
}
// 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
}
2019-07-16 10:43:07 +00:00
p . onAddedBundlesHandler ( addedBundles )
2019-07-08 09:21:21 +00:00
}
// Check if it's a public message
if publicMessage := protocolMessage . GetPublicMessage ( ) ; publicMessage != nil {
2019-07-16 10:43:07 +00:00
log . Printf ( "[Protocol::HandleMessage] received a public message in direct message" )
2019-07-08 09:21:21 +00:00
// Nothing to do, as already in cleartext
return publicMessage , nil
}
// Decrypt message
if directMessage := protocolMessage . GetDirectMessage ( ) ; directMessage != nil {
2019-07-16 10:43:07 +00:00
log . Printf ( "[Protocol::HandleMessage] processing direct message" )
message , err := p . encryptor . DecryptPayload (
myIdentityKey ,
theirPublicKey ,
protocolMessage . GetInstallationId ( ) ,
directMessage ,
messageID ,
)
2019-07-08 09:21:21 +00:00
if err != nil {
return nil , err
}
// Handle protocol negotiation for compatible clients
bundles := append ( protocolMessage . GetBundles ( ) , protocolMessage . GetBundle ( ) )
version := getProtocolVersion ( bundles , protocolMessage . GetInstallationId ( ) )
2019-07-16 10:43:07 +00:00
log . Printf ( "[Protocol::HandleMessage] direct message version: %d" , version )
2019-07-08 09:21:21 +00:00
if version >= sharedSecretNegotiationVersion {
2019-07-16 10:43:07 +00:00
log . Printf ( "[Protocol::HandleMessage] negotiating shared secret for %#x" , crypto . FromECDSAPub ( theirPublicKey ) )
sharedSecret , err := p . secret . Generate ( myIdentityKey , theirPublicKey , protocolMessage . GetInstallationId ( ) )
2019-07-08 09:21:21 +00:00
if err != nil {
return nil , err
}
p . onNewSharedSecretHandler ( [ ] * sharedsecret . Secret { sharedSecret } )
}
return message , nil
}
// Return error
return nil , ErrNoPayload
}
2019-07-16 10:43:07 +00:00
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 )
}
2019-07-08 09:21:21 +00:00
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
}