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/pkg/errors"
2019-11-23 17:57:05 +00:00
"github.com/status-im/status-go/eth-node/crypto"
2020-11-24 12:36:52 +00:00
"github.com/status-im/status-go/eth-node/types"
2019-11-23 17:57:05 +00:00
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
2020-07-31 12:22:05 +00:00
SharedSecret * sharedsecret . Secret
// AgreedSecret indicates whether the shared secret has been agreed
AgreedSecret bool
2019-07-17 22:25:42 +00:00
// 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 {
2020-07-31 09:08:09 +00:00
encryptor * encryptor
secret * sharedsecret . SharedSecret
multidevice * multidevice . Multidevice
publisher * publisher . Publisher
subscriptions * Subscriptions
2019-07-17 22:25:42 +00:00
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 ,
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 ) ,
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 ,
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 ,
} ) ,
2020-07-31 12:22:05 +00:00
publisher : publisher . New ( logger ) ,
logger : logger . With ( zap . Namespace ( "Protocol" ) ) ,
2019-07-30 06:14:13 +00:00
}
2019-07-17 22:25:42 +00:00
}
2020-07-31 09:08:09 +00:00
type Subscriptions struct {
2020-07-31 12:22:05 +00:00
SharedSecrets [ ] * sharedsecret . Secret
SendContactCode <- chan struct { }
Quit chan struct { }
2020-07-31 09:08:09 +00:00
}
func ( p * Protocol ) Start ( myIdentity * ecdsa . PrivateKey ) ( * Subscriptions , error ) {
2019-07-17 22:25:42 +00:00
// Propagate currently cached shared secrets.
secrets , err := p . secret . All ( )
if err != nil {
2020-07-31 09:08:09 +00:00
return nil , errors . Wrap ( err , "failed to get all secrets" )
}
p . subscriptions = & Subscriptions {
2020-07-31 12:22:05 +00:00
SharedSecrets : secrets ,
SendContactCode : p . publisher . Start ( ) ,
Quit : make ( chan struct { } ) ,
2020-07-31 09:46:38 +00:00
}
2020-07-31 09:08:09 +00:00
return p . subscriptions , nil
}
func ( p * Protocol ) Stop ( ) error {
p . publisher . Stop ( )
if p . subscriptions != nil {
close ( p . subscriptions . Quit )
}
2019-07-17 22:25:42 +00:00
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
// Get a bundle
installations , err := p . multidevice . GetOurActiveInstallations ( & myIdentityKey . PublicKey )
if err != nil {
return err
}
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
}
2021-09-21 15:47:04 +00:00
// BuildEncryptedMessage 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 ) BuildEncryptedMessage ( myIdentityKey * ecdsa . PrivateKey , publicKey * ecdsa . PublicKey , payload [ ] byte ) ( * ProtocolMessageSpec , error ) {
2019-07-17 22:25:42 +00:00
// Get recipients installations.
activeInstallations , err := p . multidevice . GetActiveInstallations ( publicKey )
if err != nil {
return nil , err
}
// Encrypt payload
2021-09-21 15:47:04 +00:00
encryptedMessagesByInstalls , installations , err := p . encryptor . EncryptPayload ( publicKey , myIdentityKey , activeInstallations , payload )
2019-07-17 22:25:42 +00:00
if err != nil {
return nil , err
}
// Build message
message := & ProtocolMessage {
2021-09-21 15:47:04 +00:00
InstallationId : p . encryptor . config . InstallationID ,
EncryptedMessage : encryptedMessagesByInstalls ,
2019-07-17 22:25:42 +00:00
}
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
2021-09-21 15:47:04 +00:00
for installationID := range message . GetEncryptedMessage ( ) {
2019-07-17 22:25:42 +00:00
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
}
spec := & ProtocolMessageSpec {
2020-07-31 12:22:05 +00:00
SharedSecret : sharedSecret ,
AgreedSecret : agreed ,
2019-07-17 22:25:42 +00:00
Message : message ,
Installations : installations ,
}
return spec , nil
}
2022-05-27 09:14:40 +00:00
func ( p * Protocol ) GenerateHashRatchetKey ( groupID [ ] byte ) ( uint32 , error ) {
return p . encryptor . GenerateHashRatchetKey ( groupID )
}
2021-09-21 15:47:04 +00:00
// BuildHashRatchetKeyExchangeMessage builds a 1:1 message
// containing newly generated hash ratchet key
2022-05-27 09:14:40 +00:00
func ( p * Protocol ) BuildHashRatchetKeyExchangeMessage ( myIdentityKey * ecdsa . PrivateKey , publicKey * ecdsa . PublicKey , groupID [ ] byte , keyID uint32 ) ( * ProtocolMessageSpec , error ) {
2021-09-21 15:47:04 +00:00
2022-05-27 09:14:40 +00:00
keyData , err := p . encryptor . persistence . GetHashRatchetKeyByID ( groupID , keyID , 0 )
2021-09-21 15:47:04 +00:00
if err != nil {
return nil , err
}
response , err := p . BuildEncryptedMessage ( myIdentityKey , publicKey , keyData . Key )
if err != nil {
return nil , err
}
// Loop through installations and assign HRHeader
// SeqNo=0 has a special meaning for HandleMessage
// and signifies a message with hash ratchet key payload
for _ , v := range response . Message . EncryptedMessage {
v . HRHeader = & HRHeader {
KeyId : keyID ,
SeqNo : 0 ,
GroupId : groupID ,
}
}
return response , err
}
2022-05-27 09:14:40 +00:00
func ( p * Protocol ) GetCurrentKeyForGroup ( groupID [ ] byte ) ( uint32 , error ) {
return p . encryptor . persistence . GetCurrentKeyForGroup ( groupID )
}
2021-09-21 15:47:04 +00:00
// BuildHashRatchetMessage returns a hash ratchet chat message
func ( p * Protocol ) BuildHashRatchetMessage ( groupID [ ] byte , payload [ ] byte ) ( * ProtocolMessageSpec , error ) {
2022-05-27 09:14:40 +00:00
keyID , err := p . encryptor . persistence . GetCurrentKeyForGroup ( groupID )
2021-09-21 15:47:04 +00:00
if err != nil {
return nil , err
}
// Encrypt payload
encryptedMessagesByInstalls , err := p . encryptor . EncryptHashRatchetPayload ( groupID , keyID , payload )
if err != nil {
return nil , err
}
// Build message
message := & ProtocolMessage {
InstallationId : p . encryptor . config . InstallationID ,
EncryptedMessage : encryptedMessagesByInstalls ,
}
spec := & ProtocolMessageSpec {
Message : message ,
}
return spec , nil
}
2022-05-27 09:14:40 +00:00
func ( p * Protocol ) GetKeyExMessageSpecs ( communityID [ ] byte , identity * ecdsa . PrivateKey , recipients [ ] * ecdsa . PublicKey , forceRekey bool ) ( [ ] * ProtocolMessageSpec , error ) {
var communityKeyID uint32
var err error
if ! forceRekey {
communityKeyID , err = p . GetCurrentKeyForGroup ( communityID )
if err != nil {
return nil , err
}
}
if communityKeyID == 0 || forceRekey {
communityKeyID , err = p . GenerateHashRatchetKey ( communityID )
if err != nil {
return nil , err
}
}
specs := make ( [ ] * ProtocolMessageSpec , len ( recipients ) )
for i , recipient := range recipients {
keyExMsg , err := p . BuildHashRatchetKeyExchangeMessage ( identity , recipient , communityID , communityKeyID )
if err != nil {
return nil , err
}
specs [ i ] = keyExMsg
}
return specs , nil
}
2019-07-17 22:25:42 +00:00
// 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 {
2021-09-21 15:47:04 +00:00
InstallationId : p . encryptor . config . InstallationID ,
EncryptedMessage : encryptionResponse ,
2019-07-17 22:25:42 +00:00
}
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" ) )
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
}
// 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
}
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 {
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 )
Move to protobuf for Message type (#1706)
* Use a single Message type `v1/message.go` and `message.go` are the same now, and they embed `protobuf.ChatMessage`
* Use `SendChatMessage` for sending chat messages, this is basically the old `Send` but a bit more flexible so we can send different message types (stickers,commands), and not just text.
* Remove dedup from services/shhext. Because now we process in status-protocol, dedup makes less sense, as those messages are going to be processed anyway, so removing for now, we can re-evaluate if bringing it to status-go or not.
* Change the various retrieveX method to a single one:
`RetrieveAll` will be processing those messages that it can process (Currently only `Message`), and return the rest in `RawMessages` (still transit). The format for the response is:
`Chats`: -> The chats updated by receiving the message
`Messages`: -> The messages retrieved (already matched to a chat)
`Contacts`: -> The contacts updated by the messages
`RawMessages` -> Anything else that can't be parsed, eventually as we move everything to status-protocol-go this will go away.
2019-12-05 16:25:34 +00:00
}
// 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 )
2019-07-17 22:25:42 +00:00
}
// 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" ) )
2021-10-29 14:29:28 +00:00
logger . Debug ( "confirming message" , zap . String ( "messageID" , types . EncodeHex ( messageID ) ) )
2019-07-17 22:25:42 +00:00
return p . encryptor . ConfirmMessageProcessed ( messageID )
}
2020-07-31 12:22:05 +00:00
type DecryptMessageResponse struct {
DecryptedMessage [ ] byte
Installations [ ] * multidevice . Installation
SharedSecrets [ ] * sharedsecret . Secret
2020-07-31 09:46:38 +00:00
}
2019-07-17 22:25:42 +00:00
// 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 ,
2020-07-31 12:22:05 +00:00
) ( * DecryptMessageResponse , error ) {
2019-07-17 22:25:42 +00:00
logger := p . logger . With ( zap . String ( "site" , "HandleMessage" ) )
2020-07-31 12:22:05 +00:00
response := & DecryptMessageResponse { }
2019-07-17 22:25:42 +00:00
2020-11-24 12:36:52 +00:00
logger . Debug ( "received a protocol message" ,
zap . String ( "sender-public-key" ,
types . EncodeHex ( crypto . FromECDSAPub ( theirPublicKey ) ) ) ,
zap . String ( "my-installation-id" , p . encryptor . config . InstallationID ) ,
2021-10-29 14:29:28 +00:00
zap . String ( "messageID" , types . EncodeHex ( 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?
2020-07-31 12:22:05 +00:00
newInstallations , err := p . ProcessPublicBundle ( myIdentityKey , bundle )
2019-07-17 22:25:42 +00:00
if err != nil {
return nil , err
}
2020-07-31 12:22:05 +00:00
response . Installations = newInstallations
2019-07-17 22:25:42 +00:00
}
// Check if it's a public message
if publicMessage := protocolMessage . GetPublicMessage ( ) ; publicMessage != nil {
// Nothing to do, as already in cleartext
2020-07-31 12:22:05 +00:00
response . DecryptedMessage = publicMessage
return response , nil
2019-07-17 22:25:42 +00:00
}
// Decrypt message
2021-09-21 15:47:04 +00:00
if encryptedMessage := protocolMessage . GetEncryptedMessage ( ) ; encryptedMessage != nil {
2019-07-17 22:25:42 +00:00
message , err := p . encryptor . DecryptPayload (
myIdentityKey ,
theirPublicKey ,
protocolMessage . GetInstallationId ( ) ,
2021-09-21 15:47:04 +00:00
encryptedMessage ,
2019-07-17 22:25:42 +00:00
messageID ,
)
if err != nil {
return nil , err
}
2021-09-21 15:47:04 +00:00
dmProtocol := encryptedMessage [ p . encryptor . config . InstallationID ]
if dmProtocol == nil {
dmProtocol = encryptedMessage [ noInstallationID ]
}
2021-12-02 11:55:30 +00:00
if dmProtocol != nil {
hrHeader := dmProtocol . HRHeader
if hrHeader != nil && hrHeader . SeqNo == 0 {
// Payload contains hash ratchet key
err = p . encryptor . persistence . SaveHashRatchetKey ( hrHeader . GroupId , hrHeader . KeyId , message )
if err != nil {
return nil , err
}
2021-09-21 15:47:04 +00:00
}
}
2019-09-26 07:01:17 +00:00
bundles := protocolMessage . GetBundles ( )
2019-07-17 22:25:42 +00:00
version := getProtocolVersion ( bundles , protocolMessage . GetInstallationId ( ) )
if version >= sharedSecretNegotiationVersion {
sharedSecret , err := p . secret . Generate ( myIdentityKey , theirPublicKey , protocolMessage . GetInstallationId ( ) )
if err != nil {
return nil , err
}
2020-07-31 12:22:05 +00:00
response . SharedSecrets = [ ] * sharedsecret . Secret { sharedSecret }
2019-07-17 22:25:42 +00:00
}
2020-07-31 12:22:05 +00:00
response . DecryptedMessage = message
return response , nil
2019-07-17 22:25:42 +00:00
}
// 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
}