diff --git a/api/client.go b/api/client.go index 2c83ad2af5a5bd43f43b76ebf84e0eedccae57aa..55d41946a23930d0155d60a9912ed43744fc65fc 100644 --- a/api/client.go +++ b/api/client.go @@ -23,6 +23,7 @@ import ( "gitlab.com/elixxir/comms/client" "gitlab.com/elixxir/crypto/cyclic" "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/elixxir/primitives/version" "gitlab.com/xx_network/crypto/csprng" "gitlab.com/xx_network/crypto/large" "gitlab.com/xx_network/crypto/signature/rsa" @@ -79,10 +80,16 @@ func NewClient(ndfJSON, storageDir string, password []byte, registrationCode str protoUser := createNewUser(rngStream, cmixGrp, e2eGrp) + // Get current client version + currentVersion, err := version.ParseVersion(SEMVER) + if err != nil { + return errors.WithMessage(err, "Could not parse version string.") + } + // Create Storage passwordStr := string(password) storageSess, err := storage.New(storageDir, passwordStr, protoUser, - cmixGrp, e2eGrp, rngStreamGen) + currentVersion, cmixGrp, e2eGrp, rngStreamGen) if err != nil { return err } @@ -124,10 +131,16 @@ func NewPrecannedClient(precannedID uint, defJSON, storageDir string, password [ protoUser := createPrecannedUser(precannedID, rngStream, cmixGrp, e2eGrp) + // Get current client version + currentVersion, err := version.ParseVersion(SEMVER) + if err != nil { + return errors.WithMessage(err, "Could not parse version string.") + } + // Create Storage passwordStr := string(password) storageSess, err := storage.New(storageDir, passwordStr, protoUser, - cmixGrp, e2eGrp, rngStreamGen) + currentVersion, cmixGrp, e2eGrp, rngStreamGen) if err != nil { return err } @@ -151,12 +164,18 @@ func NewPrecannedClient(precannedID uint, defJSON, storageDir string, password [ func OpenClient(storageDir string, password []byte, parameters params.Network) (*Client, error) { jww.INFO.Printf("OpenClient()") // Use fastRNG for RNG ops (AES fortuna based RNG using system RNG) - rngStreamGen := fastRNG.NewStreamGenerator(12, 3, - csprng.NewSystemRNG) + rngStreamGen := fastRNG.NewStreamGenerator(12, 3, csprng.NewSystemRNG) + + // Get current client version + currentVersion, err := version.ParseVersion(SEMVER) + if err != nil { + return nil, errors.WithMessage(err, "Could not parse version string.") + } // Load Storage passwordStr := string(password) - storageSess, err := storage.Load(storageDir, passwordStr, rngStreamGen) + storageSess, err := storage.Load(storageDir, passwordStr, currentVersion, + rngStreamGen) if err != nil { return nil, err } diff --git a/storage/clientVersion/store.go b/storage/clientVersion/store.go new file mode 100644 index 0000000000000000000000000000000000000000..344b138bd8cf328bfa17722bce8bec8c6fbfd860 --- /dev/null +++ b/storage/clientVersion/store.go @@ -0,0 +1,117 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package clientVersion + +import ( + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/primitives/version" + "sync" + "time" +) + +const ( + prefix = "clientVersionStore" + storeKey = "clientVersion" + storeVersion = 0 +) + +// Store stores the version of the client's storage. +type Store struct { + version version.Version + kv *versioned.KV + sync.RWMutex +} + +// NewStore returns a new clientVersion store. +func NewStore(newVersion version.Version, kv *versioned.KV) (*Store, error) { + s := &Store{ + version: newVersion, + kv: kv.Prefix(prefix), + } + + return s, s.save() +} + +// LoadStore loads the clientVersion storage object. +func LoadStore(kv *versioned.KV) (*Store, error) { + s := &Store{ + kv: kv.Prefix(prefix), + } + + obj, err := s.kv.Get(storeKey) + if err != nil { + return nil, err + } + + s.version, err = version.ParseVersion(string(obj.Data)) + if err != nil { + return nil, errors.Errorf("failed to parse client version: %+v", err) + } + + return s, nil +} + +// Get returns the stored version. +func (s *Store) Get() version.Version { + s.RLock() + defer s.RUnlock() + + return s.version +} + +// CheckUpdateRequired determines if the storage needs to be upgraded to the new +// client version. It returns true if an update is required (new > stored) and +// false otherwise. The old stored version is returned to be used to determine +// how to upgrade storage. If the new version is older than the stored version, +// an error is returned. +func (s *Store) CheckUpdateRequired(newVersion version.Version) (bool, version.Version, error) { + s.Lock() + defer s.Unlock() + + oldVersion := s.version + diff := version.Cmp(oldVersion, newVersion) + + switch { + case diff < 0: + return true, oldVersion, s.update(newVersion) + case diff > 0: + return false, oldVersion, errors.Errorf("new version (%s) is older "+ + "than stored version (%s).", &newVersion, &oldVersion) + default: + return false, oldVersion, nil + } +} + +// update replaces the current version with the new version if it is newer. Note +// that this function does not take a lock. +func (s *Store) update(newVersion version.Version) error { + jww.DEBUG.Printf("Updating stored client version from %s to %s.", + &s.version, &newVersion) + + // Update version + s.version = newVersion + + // Save new version to storage + return s.save() +} + +// save stores the clientVersion store. Note that this function does not take +// a lock. +func (s *Store) save() error { + timeNow := time.Now() + + obj := versioned.Object{ + Version: storeVersion, + Timestamp: timeNow, + Data: []byte(s.version.String()), + } + + return s.kv.Set(storeKey, &obj) +} diff --git a/storage/clientVersion/store_test.go b/storage/clientVersion/store_test.go new file mode 100644 index 0000000000000000000000000000000000000000..8ff38a27599f2d43b8576e97a58e0843192bb814 --- /dev/null +++ b/storage/clientVersion/store_test.go @@ -0,0 +1,237 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package clientVersion + +import ( + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/ekv" + "gitlab.com/elixxir/primitives/version" + "reflect" + "strings" + "testing" + "time" +) + +// Happy path. +func TestNewStore(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + expected := &Store{ + version: version.New(42, 43, "44"), + kv: kv.Prefix(prefix), + } + + test, err := NewStore(expected.version, kv) + if err != nil { + t.Errorf("NewStore() returned an error: %+v", err) + } + + if !reflect.DeepEqual(expected, test) { + t.Errorf("NewStore() failed to return the expected Store."+ + "\nexpected: %+v\nreceived: %+v", expected, test) + } +} + +// Happy path. +func TestLoadStore(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + ver := version.New(1, 2, "3A") + + expected := &Store{ + version: ver, + kv: kv.Prefix(prefix), + } + err := expected.save() + if err != nil { + t.Fatalf("Failed to save Store: %+v", err) + } + + test, err := LoadStore(kv) + if err != nil { + t.Errorf("LoadStore() returned an error: %+v", err) + } + + if !reflect.DeepEqual(expected, test) { + t.Errorf("LoadStore() failed to return the expected Store."+ + "\nexpected: %+v\nreceived: %+v", expected, test) + } +} + +// Error path: an error is returned when the loaded Store has an invalid version +// that fails to be parsed. +func TestLoadStore_ParseVersionError(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + obj := versioned.Object{ + Version: storeVersion, + Timestamp: time.Now(), + Data: []byte("invalid version"), + } + + err := kv.Prefix(prefix).Set(storeKey, &obj) + if err != nil { + t.Fatalf("Failed to save Store: %+v", err) + } + + _, err = LoadStore(kv) + if err == nil || !strings.Contains(err.Error(), "failed to parse client version") { + t.Errorf("LoadStore() did not return an error when the client version "+ + "is invalid: %+v", err) + } +} + +// Happy path. +func TestStore_Get(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + expected := version.New(1, 2, "3A") + + s := &Store{ + version: expected, + kv: kv.Prefix(prefix), + } + + test := s.Get() + if !reflect.DeepEqual(expected, test) { + t.Errorf("Get() failed to return the expected version."+ + "\nexpected: %s\nreceived: %s", &expected, &test) + } +} + +// Happy path. +func TestStore_CheckUpdateRequired(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + storedVersion := version.New(1, 2, "3") + newVersion := version.New(2, 3, "4") + s, err := NewStore(storedVersion, kv) + if err != nil { + t.Fatalf("Failed to generate a new Store: %+v", err) + } + + updateRequired, oldVersion, err := s.CheckUpdateRequired(newVersion) + if err != nil { + t.Errorf("CheckUpdateRequired() returned an error: %+v", err) + } + + if !updateRequired { + t.Errorf("CheckUpdateRequired() did not indicate that an update is "+ + "required when the new Version (%s) is newer than the stored"+ + "version (%s)", &newVersion, &storedVersion) + } + + if !version.Equal(storedVersion, oldVersion) { + t.Errorf("CheckUpdateRequired() did return the expected old Version."+ + "\nexpected: %s\nreceived: %s", &storedVersion, &oldVersion) + } +} + +// Happy path: the new version is equal to the stored version. +func TestStore_CheckUpdateRequired_EqualVersions(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + storedVersion := version.New(2, 3, "3") + newVersion := version.New(2, 3, "4") + s, err := NewStore(storedVersion, kv) + if err != nil { + t.Fatalf("Failed to generate a new Store: %+v", err) + } + + updateRequired, oldVersion, err := s.CheckUpdateRequired(newVersion) + if err != nil { + t.Errorf("CheckUpdateRequired() returned an error: %+v", err) + } + + if updateRequired { + t.Errorf("CheckUpdateRequired() did not indicate that an update is required "+ + "when the new Version (%s) is equal to the stored version (%s)", + &newVersion, &storedVersion) + } + + if !version.Equal(storedVersion, oldVersion) { + t.Errorf("CheckUpdateRequired() did return the expected old Version."+ + "\nexpected: %s\nreceived: %s", &storedVersion, &oldVersion) + } +} + +// Error path: new version is older than stored version. +func TestStore_CheckUpdateRequired_NewVersionTooOldError(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + storedVersion := version.New(2, 3, "4") + newVersion := version.New(1, 2, "3") + s, err := NewStore(storedVersion, kv) + if err != nil { + t.Fatalf("Failed to generate a new Store: %+v", err) + } + + updateRequired, oldVersion, err := s.CheckUpdateRequired(newVersion) + if err == nil || !strings.Contains(err.Error(), "older than stored version") { + t.Errorf("CheckUpdateRequired() did not return an error when the new version "+ + "is older than the stored version: %+v", err) + } + + if updateRequired { + t.Errorf("CheckUpdateRequired() indicated that an update is required when the "+ + "new Version (%s) is older than the stored version (%s)", + &newVersion, &storedVersion) + } + + if !version.Equal(storedVersion, oldVersion) { + t.Errorf("CheckUpdateRequired() did return the expected old Version."+ + "\nexpected: %s\nreceived: %s", &storedVersion, &oldVersion) + } +} + +// Happy path. +func TestStore_update(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + ver1 := version.New(1, 2, "3A") + ver2 := version.New(1, 5, "patch5") + + s := &Store{ + version: ver1, + kv: kv.Prefix(prefix), + } + + err := s.update(ver2) + if err != nil { + t.Errorf("Update() returned an error: %+v", err) + } + + if !reflect.DeepEqual(ver2, s.version) { + t.Errorf("Update() did not set the correct version."+ + "\nexpected: %s\nreceived: %s", &ver2, &s.version) + } +} + +// Happy path. +func TestStore_save(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + ver := version.New(1, 2, "3A") + + s := &Store{ + version: ver, + kv: kv.Prefix(prefix), + } + + err := s.save() + if err != nil { + t.Errorf("save() returned an error: %+v", err) + } + + obj, err := s.kv.Get(storeKey) + if err != nil { + t.Errorf("Failed to load clientVersion store: %+v", err) + } + + if ver.String() != string(obj.Data) { + t.Errorf("Failed to get correct data from stored object."+ + "\nexpected: %s\nreceived: %s", ver.String(), obj.Data) + } + + if storeVersion != obj.Version { + t.Errorf("Failed to get correct version from stored object."+ + "\nexpected: %d\nreceived: %d", storeVersion, obj.Version) + } + +} diff --git a/storage/session.go b/storage/session.go index de3768d13e941592da584439a3c15b1d38899a8c..698d1e295e78913596935f25dd1adbe3fda3732b 100644 --- a/storage/session.go +++ b/storage/session.go @@ -14,6 +14,7 @@ import ( "gitlab.com/elixxir/client/globals" userInterface "gitlab.com/elixxir/client/interfaces/user" "gitlab.com/elixxir/client/storage/auth" + "gitlab.com/elixxir/client/storage/clientVersion" "gitlab.com/elixxir/client/storage/cmix" "gitlab.com/elixxir/client/storage/conversation" "gitlab.com/elixxir/client/storage/e2e" @@ -25,6 +26,7 @@ import ( "gitlab.com/elixxir/crypto/cyclic" "gitlab.com/elixxir/crypto/fastRNG" "gitlab.com/elixxir/ekv" + "gitlab.com/elixxir/primitives/version" "gitlab.com/xx_network/crypto/csprng" "gitlab.com/xx_network/crypto/large" "gitlab.com/xx_network/crypto/signature/rsa" @@ -57,6 +59,7 @@ type Session struct { criticalRawMessages *utility.CmixMessageBuffer garbledMessages *utility.MeteredCmixMessageBuffer reception *reception.Store + clientVersion *clientVersion.Store } // Initialize a new Session object @@ -76,9 +79,8 @@ func initStore(baseDir, password string) (*Session, error) { } // Creates new UserData in the session - -func New(baseDir, password string, u userInterface.User, cmixGrp, - e2eGrp *cyclic.Group, rng *fastRNG.StreamGenerator) (*Session, error) { +func New(baseDir, password string, u userInterface.User, currentVersion version.Version, + cmixGrp, e2eGrp *cyclic.Group, rng *fastRNG.StreamGenerator) (*Session, error) { s, err := initStore(baseDir, password) if err != nil { @@ -132,11 +134,18 @@ func New(baseDir, password string, u userInterface.User, cmixGrp, s.reception = reception.NewStore(s.kv) + s.clientVersion, err = clientVersion.NewStore(currentVersion, s.kv) + if err != nil { + return nil, errors.WithMessage(err, "Failed to create client version store.") + } + return s, nil } // Loads existing user data into the session -func Load(baseDir, password string, rng *fastRNG.StreamGenerator) (*Session, error) { +func Load(baseDir, password string, currentVersion version.Version, + rng *fastRNG.StreamGenerator) (*Session, error) { + s, err := initStore(baseDir, password) if err != nil { return nil, errors.WithMessage(err, "Failed to load Session") @@ -147,6 +156,17 @@ func Load(baseDir, password string, rng *fastRNG.StreamGenerator) (*Session, err return nil, errors.WithMessage(err, "Failed to load Session") } + s.clientVersion, err = clientVersion.LoadStore(s.kv) + if err != nil { + return nil, errors.WithMessage(err, "Failed to load client version store.") + } + + // Determine if the storage needs to be updated to the current version + _, _, err = s.clientVersion.CheckUpdateRequired(currentVersion) + if err != nil { + return nil, errors.WithMessage(err, "Failed to load client version store.") + } + s.user, err = user.LoadUser(s.kv) if err != nil { return nil, errors.WithMessage(err, "Failed to load Session") @@ -241,6 +261,13 @@ func (s *Session) GetGarbledMessages() *utility.MeteredCmixMessageBuffer { return s.garbledMessages } +// GetClientVersion returns the version of the client storage. +func (s *Session) GetClientVersion() version.Version { + s.mux.RLock() + defer s.mux.RUnlock() + return s.clientVersion.Get() +} + func (s *Session) Conversations() *conversation.Store { s.mux.RLock() defer s.mux.RUnlock() @@ -307,11 +334,11 @@ func InitTestingSession(i interface{}) *Session { "3A10B1C4D203CC76A470A33AFDCBDD92959859ABD8B56E1725252D78EAC66E71"+ "BA9AE3F1DD2487199874393CD4D832186800654760E1E34C09E4D155179F9EC0"+ "DC4473F996BDCE6EED1CABED8B6F116F7AD9CF505DF0F998E34AB27514B0FFE7", 16)) - cmix, err := cmix.NewStore(cmixGrp, kv, cmixGrp.NewInt(2)) + cmixStore, err := cmix.NewStore(cmixGrp, kv, cmixGrp.NewInt(2)) if err != nil { globals.Log.FATAL.Panicf("InitTestingSession failed to create dummy cmix session: %+v", err) } - s.cmix = cmix + s.cmix = cmixStore e2eStore, err := e2e.NewStore(cmixGrp, kv, cmixGrp.NewInt(2), uid, fastRNG.NewStreamGenerator(7, 3, csprng.NewSystemRNG))