2019-07-08 09:21:21 +00:00
|
|
|
package encryption
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/ecdsa"
|
|
|
|
"fmt"
|
2019-07-16 10:43:07 +00:00
|
|
|
"io/ioutil"
|
2019-07-08 09:21:21 +00:00
|
|
|
"testing"
|
|
|
|
|
2019-07-30 18:36:42 +00:00
|
|
|
"github.com/status-im/status-protocol-go/tt"
|
|
|
|
|
2019-07-27 12:27:31 +00:00
|
|
|
"github.com/status-im/status-protocol-go/encryption/migrations"
|
|
|
|
"github.com/status-im/status-protocol-go/sqlite"
|
|
|
|
|
2019-07-08 09:21:21 +00:00
|
|
|
"github.com/ethereum/go-ethereum/crypto"
|
|
|
|
"github.com/stretchr/testify/suite"
|
2019-07-18 13:41:48 +00:00
|
|
|
"go.uber.org/zap"
|
2019-07-08 09:21:21 +00:00
|
|
|
|
|
|
|
"github.com/status-im/status-protocol-go/encryption/multidevice"
|
|
|
|
"github.com/status-im/status-protocol-go/encryption/sharedsecret"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
aliceUser = "alice"
|
|
|
|
bobUser = "bob"
|
|
|
|
)
|
|
|
|
|
|
|
|
func TestEncryptionServiceMultiDeviceSuite(t *testing.T) {
|
|
|
|
suite.Run(t, new(EncryptionServiceMultiDeviceSuite))
|
|
|
|
}
|
|
|
|
|
|
|
|
type serviceAndKey struct {
|
2019-07-16 10:43:07 +00:00
|
|
|
services []*Protocol
|
2019-07-08 09:21:21 +00:00
|
|
|
key *ecdsa.PrivateKey
|
|
|
|
}
|
|
|
|
|
|
|
|
type EncryptionServiceMultiDeviceSuite struct {
|
|
|
|
suite.Suite
|
|
|
|
services map[string]*serviceAndKey
|
2019-07-18 13:41:48 +00:00
|
|
|
logger *zap.Logger
|
2019-07-08 09:21:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func setupUser(user string, s *EncryptionServiceMultiDeviceSuite, n int) error {
|
|
|
|
key, err := crypto.GenerateKey()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
s.services[user] = &serviceAndKey{
|
|
|
|
key: key,
|
2019-07-16 10:43:07 +00:00
|
|
|
services: make([]*Protocol, n),
|
2019-07-08 09:21:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for i := 0; i < n; i++ {
|
|
|
|
installationID := fmt.Sprintf("%s%d", user, i+1)
|
2019-07-23 08:33:57 +00:00
|
|
|
dbFileName := fmt.Sprintf("sqlite-persistence-test-%s.sql", installationID)
|
|
|
|
dbPath, err := ioutil.TempFile("", dbFileName)
|
2019-07-08 09:21:21 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-07-27 12:27:31 +00:00
|
|
|
db, err := sqlite.Open(dbPath.Name(), "some-key", sqlite.MigrationConfig{
|
|
|
|
AssetNames: migrations.AssetNames(),
|
|
|
|
AssetGetter: migrations.Asset,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-07-08 09:21:21 +00:00
|
|
|
|
2019-07-27 12:27:31 +00:00
|
|
|
protocol := New(
|
|
|
|
db,
|
2019-07-16 10:43:07 +00:00
|
|
|
installationID,
|
2019-07-08 09:21:21 +00:00
|
|
|
func(s []*multidevice.Installation) {},
|
|
|
|
func(s []*sharedsecret.Secret) {},
|
2019-07-16 10:43:07 +00:00
|
|
|
func(*ProtocolMessageSpec) {},
|
2019-07-18 13:41:48 +00:00
|
|
|
s.logger.With(zap.String("user", user)),
|
2019-07-08 09:21:21 +00:00
|
|
|
)
|
|
|
|
s.services[user].services[i] = protocol
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *EncryptionServiceMultiDeviceSuite) SetupTest() {
|
2019-07-30 18:36:42 +00:00
|
|
|
s.logger = tt.MustCreateTestLogger()
|
2019-07-18 13:41:48 +00:00
|
|
|
|
2019-07-08 09:21:21 +00:00
|
|
|
s.services = make(map[string]*serviceAndKey)
|
2019-07-30 18:36:42 +00:00
|
|
|
err := setupUser(aliceUser, s, 4)
|
2019-07-08 09:21:21 +00:00
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
err = setupUser(bobUser, s, 4)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
}
|
|
|
|
|
2019-07-18 13:41:48 +00:00
|
|
|
func (s *EncryptionServiceMultiDeviceSuite) TearDownTest() {
|
2019-07-21 05:40:12 +00:00
|
|
|
_ = s.logger.Sync()
|
2019-07-18 13:41:48 +00:00
|
|
|
}
|
|
|
|
|
2019-07-08 09:21:21 +00:00
|
|
|
func (s *EncryptionServiceMultiDeviceSuite) TestProcessPublicBundle() {
|
|
|
|
aliceKey := s.services[aliceUser].key
|
|
|
|
|
|
|
|
alice2Bundle, err := s.services[aliceUser].services[1].GetBundle(aliceKey)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
alice2IdentityPK, err := ExtractIdentity(alice2Bundle)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
alice2Identity := fmt.Sprintf("0x%x", crypto.FromECDSAPub(alice2IdentityPK))
|
|
|
|
|
|
|
|
alice3Bundle, err := s.services[aliceUser].services[2].GetBundle(aliceKey)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
alice3IdentityPK, err := ExtractIdentity(alice2Bundle)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
alice3Identity := fmt.Sprintf("0x%x", crypto.FromECDSAPub(alice3IdentityPK))
|
|
|
|
|
|
|
|
// Add alice2 bundle
|
|
|
|
response, err := s.services[aliceUser].services[0].ProcessPublicBundle(aliceKey, alice2Bundle)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
s.Require().Equal(multidevice.Installation{
|
|
|
|
Identity: alice2Identity,
|
|
|
|
Version: 1,
|
|
|
|
ID: "alice2",
|
|
|
|
}, *response[0])
|
|
|
|
|
|
|
|
// Add alice3 bundle
|
|
|
|
response, err = s.services[aliceUser].services[0].ProcessPublicBundle(aliceKey, alice3Bundle)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
s.Require().Equal(multidevice.Installation{
|
|
|
|
Identity: alice3Identity,
|
|
|
|
Version: 1,
|
|
|
|
ID: "alice3",
|
|
|
|
}, *response[0])
|
|
|
|
|
|
|
|
// No installation is enabled
|
|
|
|
alice1MergedBundle1, err := s.services[aliceUser].services[0].GetBundle(aliceKey)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
s.Require().Equal(1, len(alice1MergedBundle1.GetSignedPreKeys()))
|
|
|
|
s.Require().NotNil(alice1MergedBundle1.GetSignedPreKeys()["alice1"])
|
|
|
|
|
|
|
|
// We enable the installations
|
|
|
|
err = s.services[aliceUser].services[0].EnableInstallation(&aliceKey.PublicKey, "alice2")
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
err = s.services[aliceUser].services[0].EnableInstallation(&aliceKey.PublicKey, "alice3")
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
alice1MergedBundle2, err := s.services[aliceUser].services[0].GetBundle(aliceKey)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
// We get back a bundle with all the installations
|
|
|
|
s.Require().Equal(3, len(alice1MergedBundle2.GetSignedPreKeys()))
|
|
|
|
s.Require().NotNil(alice1MergedBundle2.GetSignedPreKeys()["alice1"])
|
|
|
|
s.Require().NotNil(alice1MergedBundle2.GetSignedPreKeys()["alice2"])
|
|
|
|
s.Require().NotNil(alice1MergedBundle2.GetSignedPreKeys()["alice3"])
|
|
|
|
|
|
|
|
// We disable the installations
|
|
|
|
err = s.services[aliceUser].services[0].DisableInstallation(&aliceKey.PublicKey, "alice2")
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
alice1MergedBundle3, err := s.services[aliceUser].services[0].GetBundle(aliceKey)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
// We get back a bundle with all the installations
|
|
|
|
s.Require().Equal(2, len(alice1MergedBundle3.GetSignedPreKeys()))
|
|
|
|
s.Require().NotNil(alice1MergedBundle3.GetSignedPreKeys()["alice1"])
|
|
|
|
s.Require().NotNil(alice1MergedBundle3.GetSignedPreKeys()["alice3"])
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *EncryptionServiceMultiDeviceSuite) TestProcessPublicBundleOutOfOrder() {
|
|
|
|
aliceKey, err := crypto.GenerateKey()
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
// Alice1 creates a bundle
|
|
|
|
alice1Bundle, err := s.services[aliceUser].services[0].GetBundle(aliceKey)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
// Alice2 Receives the bundle
|
|
|
|
_, err = s.services[aliceUser].services[1].ProcessPublicBundle(aliceKey, alice1Bundle)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
// Alice2 Creates a Bundle
|
|
|
|
_, err = s.services[aliceUser].services[1].GetBundle(aliceKey)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
// We enable the installation
|
|
|
|
err = s.services[aliceUser].services[1].EnableInstallation(&aliceKey.PublicKey, "alice1")
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
// It should contain both bundles
|
|
|
|
alice2MergedBundle1, err := s.services[aliceUser].services[1].GetBundle(aliceKey)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
s.Require().NotNil(alice2MergedBundle1.GetSignedPreKeys()["alice1"])
|
|
|
|
s.Require().NotNil(alice2MergedBundle1.GetSignedPreKeys()["alice2"])
|
|
|
|
}
|
|
|
|
|
|
|
|
func pairDevices(s *serviceAndKey, target int) error {
|
|
|
|
device := s.services[target]
|
|
|
|
for i := 0; i < len(s.services); i++ {
|
|
|
|
b, err := s.services[i].GetBundle(s.key)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = device.ProcessPublicBundle(s.key, b)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-07-16 10:43:07 +00:00
|
|
|
err = device.EnableInstallation(&s.key.PublicKey, s.services[i].encryptor.config.InstallationID)
|
2019-07-08 09:21:21 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *EncryptionServiceMultiDeviceSuite) TestMaxDevices() {
|
|
|
|
err := pairDevices(s.services[aliceUser], 0)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
alice1 := s.services[aliceUser].services[0]
|
|
|
|
bob1 := s.services[bobUser].services[0]
|
|
|
|
aliceKey := s.services[aliceUser].key
|
|
|
|
bobKey := s.services[bobUser].key
|
|
|
|
|
|
|
|
// Check bundle is ok
|
|
|
|
// No installation is enabled
|
|
|
|
aliceBundle, err := alice1.GetBundle(aliceKey)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
// Check all installations are correctly working, and that the oldest device is not there
|
|
|
|
preKeys := aliceBundle.GetSignedPreKeys()
|
|
|
|
s.Require().Equal(3, len(preKeys))
|
|
|
|
s.Require().NotNil(preKeys["alice1"])
|
|
|
|
// alice2 being the oldest device is rotated out, as we reached the maximum
|
|
|
|
s.Require().Nil(preKeys["alice2"])
|
|
|
|
s.Require().NotNil(preKeys["alice3"])
|
|
|
|
s.Require().NotNil(preKeys["alice4"])
|
|
|
|
|
|
|
|
// We propagate this to bob
|
|
|
|
_, err = bob1.ProcessPublicBundle(bobKey, aliceBundle)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
// Bob sends a message to alice
|
|
|
|
msg, err := bob1.BuildDirectMessage(bobKey, &aliceKey.PublicKey, []byte("test"))
|
|
|
|
s.Require().NoError(err)
|
|
|
|
payload := msg.Message.GetDirectMessage()
|
|
|
|
s.Require().Equal(3, len(payload))
|
|
|
|
s.Require().NotNil(payload["alice1"])
|
|
|
|
s.Require().NotNil(payload["alice3"])
|
|
|
|
s.Require().NotNil(payload["alice4"])
|
|
|
|
|
|
|
|
// We disable the last installation
|
|
|
|
err = s.services[aliceUser].services[0].DisableInstallation(&aliceKey.PublicKey, "alice4")
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
// We check the bundle is updated
|
|
|
|
aliceBundle, err = alice1.GetBundle(aliceKey)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
// Check all installations are there
|
|
|
|
preKeys = aliceBundle.GetSignedPreKeys()
|
|
|
|
s.Require().Equal(3, len(preKeys))
|
|
|
|
s.Require().NotNil(preKeys["alice1"])
|
|
|
|
s.Require().NotNil(preKeys["alice2"])
|
|
|
|
s.Require().NotNil(preKeys["alice3"])
|
|
|
|
// alice4 is disabled at this point, alice2 is back in
|
|
|
|
s.Require().Nil(preKeys["alice4"])
|
|
|
|
|
|
|
|
// We propagate this to bob
|
|
|
|
_, err = bob1.ProcessPublicBundle(bobKey, aliceBundle)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
// Bob sends a message to alice
|
|
|
|
msg, err = bob1.BuildDirectMessage(bobKey, &aliceKey.PublicKey, []byte("test"))
|
|
|
|
s.Require().NoError(err)
|
|
|
|
payload = msg.Message.GetDirectMessage()
|
|
|
|
s.Require().Equal(3, len(payload))
|
|
|
|
s.Require().NotNil(payload["alice1"])
|
|
|
|
s.Require().NotNil(payload["alice2"])
|
|
|
|
s.Require().NotNil(payload["alice3"])
|
|
|
|
}
|
[Fixes #9] Increase multidevice response (#16)
We only generate the bundle once every 6 hours (or if pairing
information changes).
This means that in some cases where many devices appear/disappear,
new devices are not picked up until the bundle is refreshed.
For example:
Alice creates a device with timestamp t1 A1
Alice creates a device with timestamp t2 A2
Alice creates a device with timestamp t3 A3
Alice creates a device with timestamp t4 A4
Alice propagates this to bob, bob's list of active devices for alice is now: t2 A2,
t3 A3, t4 A4 as we only keep maximum 3.
If Alice sends a message to Bob from A1, previously it would be still
using t1 (as long as less than 6 hours have passed).
Now instead the new bundle will have t5 instead, so upon receiving the
message bob will have: t3 A3, t4 A4, t5 A1.
This commit changes the behavior to always update the timestamp before
publishing the bundle, so that as soon as a message is received from a
new device, it will increase the timestamp for that device.
2019-07-17 06:42:27 +00:00
|
|
|
|
|
|
|
func (s *EncryptionServiceMultiDeviceSuite) TestMaxDevicesRefreshedBundle() {
|
|
|
|
alice1 := s.services[aliceUser].services[0]
|
|
|
|
alice2 := s.services[aliceUser].services[1]
|
|
|
|
alice3 := s.services[aliceUser].services[2]
|
|
|
|
alice4 := s.services[aliceUser].services[3]
|
|
|
|
bob1 := s.services[bobUser].services[0]
|
|
|
|
bobKey := s.services[bobUser].key
|
|
|
|
aliceKey := s.services[aliceUser].key
|
|
|
|
|
|
|
|
// We create alice bundles, in order
|
|
|
|
alice1Bundle, err := alice1.GetBundle(aliceKey)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
alice2Bundle, err := alice2.GetBundle(aliceKey)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
alice3Bundle, err := alice3.GetBundle(aliceKey)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
alice4Bundle, err := alice4.GetBundle(aliceKey)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
// We send all the bundles to bob
|
|
|
|
_, err = bob1.ProcessPublicBundle(bobKey, alice1Bundle)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
_, err = bob1.ProcessPublicBundle(bobKey, alice2Bundle)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
_, err = bob1.ProcessPublicBundle(bobKey, alice3Bundle)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
_, err = bob1.ProcessPublicBundle(bobKey, alice4Bundle)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
// Bob sends a message to alice
|
|
|
|
msg1, err := bob1.BuildDirectMessage(bobKey, &aliceKey.PublicKey, []byte("test"))
|
|
|
|
s.Require().NoError(err)
|
|
|
|
payload := msg1.Message.GetDirectMessage()
|
|
|
|
s.Require().Equal(3, len(payload))
|
|
|
|
// Alice1 is the oldest bundle and is rotated out
|
|
|
|
// as we send maximum to 3 devices
|
|
|
|
s.Require().Nil(payload["alice1"])
|
|
|
|
s.Require().NotNil(payload["alice2"])
|
|
|
|
s.Require().NotNil(payload["alice3"])
|
|
|
|
s.Require().NotNil(payload["alice4"])
|
|
|
|
|
|
|
|
// We send a message to bob from alice1, the timestamp should be refreshed
|
|
|
|
msg2, err := alice1.BuildDirectMessage(aliceKey, &bobKey.PublicKey, []byte("test"))
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
alice1Bundle = msg2.Message.GetBundle()
|
|
|
|
|
|
|
|
// Bob processes the bundle
|
|
|
|
_, err = bob1.ProcessPublicBundle(bobKey, alice1Bundle)
|
|
|
|
s.Require().NoError(err)
|
|
|
|
|
|
|
|
// Bob sends a message to alice
|
|
|
|
msg3, err := bob1.BuildDirectMessage(bobKey, &aliceKey.PublicKey, []byte("test"))
|
|
|
|
s.Require().NoError(err)
|
|
|
|
payload = msg3.Message.GetDirectMessage()
|
|
|
|
s.Require().Equal(3, len(payload))
|
|
|
|
// Alice 1 is added back to the list of active devices
|
|
|
|
s.Require().NotNil(payload["alice1"])
|
|
|
|
// Alice 2 is rotated out as the oldest device in terms of activity
|
|
|
|
s.Require().Nil(payload["alice2"])
|
|
|
|
// Alice 3, 4 are still in
|
|
|
|
s.Require().NotNil(payload["alice3"])
|
|
|
|
s.Require().NotNil(payload["alice4"])
|
|
|
|
}
|