diff --git a/cmix/identity/receptionID/store.go b/cmix/identity/receptionID/store.go index e13eddfac252e4fd9daf2032f920f0e110a75dfe..312ebaaf7d65de7a409993801e121f0bc2ba534c 100644 --- a/cmix/identity/receptionID/store.go +++ b/cmix/identity/receptionID/store.go @@ -5,6 +5,7 @@ import ( "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/crypto/shuffle" "gitlab.com/xx_network/crypto/large" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/id/ephemeral" @@ -21,6 +22,8 @@ const ( receptionStoreStorageVersion = 0 ) +var InvalidRequestedNumIdentities = errors.New("Cannot get less than 1 identities") + type Store struct { // Identities which are being actively checked active []*registration @@ -150,6 +153,8 @@ func (s *Store) makeStoredReferences() []storedReference { return identities[:i] } +// GetIdentity will return a single identity. If none are available, it will +// return a fake one func (s *Store) GetIdentity(rng io.Reader, addressSize uint8) (IdentityUse, error) { s.mux.Lock() defer s.mux.Unlock() @@ -181,6 +186,47 @@ func (s *Store) GetIdentity(rng io.Reader, addressSize uint8) (IdentityUse, erro return identity, nil } +// GetIdentities will return up to 'num' identities randomly in a random order. +// if no identities exist, it will return a single fake identity +func (s *Store) GetIdentities(num int, rng io.Reader, + addressSize uint8) ([]IdentityUse, error) { + + if num < 1 { + return nil, errors.New("Cannot get less than 1 identities") + } + + s.mux.Lock() + defer s.mux.Unlock() + + now := netTime.Now() + + // Remove any now expired identities + s.prune(now) + + var identities []IdentityUse + var err error + + // If the list is empty, then return a randomly generated identity to poll + // with so that we can continue tracking the network and to further + // obfuscate network identities. + if len(s.active) == 0 { + fakeIdentity, err := generateFakeIdentity(rng, addressSize, now) + if err != nil { + jww.FATAL.Panicf( + "Failed to generate a new ID when none available: %+v", err) + } + identities = append(identities, fakeIdentity) + + } else { + identities, err = s.selectIdentities(num, rng, now) + if err != nil { + jww.FATAL.Panicf("Failed to select a list of IDs: %+v", err) + } + } + + return identities, nil +} + func (s *Store) AddIdentity(identity Identity) error { idH := makeIdHash(identity.EphId, identity.Source) s.mux.Lock() @@ -353,3 +399,52 @@ func (s *Store) selectIdentity(rng io.Reader, now time.Time) (IdentityUse, error CR: selected.CR, }, nil } + +func (s *Store) selectIdentities(num int, rng io.Reader, now time.Time) ([]IdentityUse, error) { + // Choose a member from the list + selected := make([]IdentityUse, 0, num) + + if len(s.active) == 1 { + selected = append(selected, makeIdentityUse(s.active[0], now)) + } else { + + // make the seed + seed := make([]byte, 32) + if _, err := rng.Read(seed); err != nil { + return nil, errors.WithMessage(err, "Failed to choose "+ + "ID due to RNG failure") + } + + // make the list to shuffle + registered := make([]*registration, 0, len(s.active)) + for i := 0; i < len(s.active); i++ { + registered = append(registered, s.active[i]) + } + + //shuffle the list via fisher-yates + shuffle.ShuffleSwap(seed, len(s.active), func(i int, j int) { + registered[i], registered[j] = registered[j], registered[i] + }) + + //convert the list to identity use + for i := 0; i < len(registered) && (i < num); i++ { + selected = append(selected, makeIdentityUse(registered[i], now)) + } + + } + + return selected, nil +} + +func makeIdentityUse(selected *registration, now time.Time) IdentityUse { + if now.After(selected.End) { + selected.ExtraChecks-- + } + return IdentityUse{ + Identity: selected.Identity, + Fake: false, + UR: selected.UR, + ER: selected.ER, + CR: selected.CR, + } +} diff --git a/cmix/identity/receptionID/store_test.go b/cmix/identity/receptionID/store_test.go index 19a53f97b11a9e7abaef0ecf3c2c0c1698602105..3fa162c605b7696772199a53be689b60e8b055e2 100644 --- a/cmix/identity/receptionID/store_test.go +++ b/cmix/identity/receptionID/store_test.go @@ -2,10 +2,13 @@ package receptionID import ( "bytes" + "encoding/binary" "encoding/json" "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/crypto/hash" "gitlab.com/elixxir/ekv" "gitlab.com/xx_network/primitives/netTime" + "math" "math/rand" "reflect" "testing" @@ -164,6 +167,222 @@ func TestStore_GetIdentity(t *testing.T) { } } +func TestStore_GetIdentity_NoIdentities(t *testing.T) { + kv := versioned.NewKV(ekv.MakeMemstore()) + s := NewOrLoadStore(kv) + prng := rand.New(rand.NewSource(42)) + + idu, err := s.GetIdentity(prng, 15) + if err != nil { + t.Errorf("GetIdentity() produced an error: %+v", err) + } + + if !idu.Fake { + t.Errorf("GetIdentity() did not return a fake identity") + } +} + +func TestStore_GetIdentities(t *testing.T) { + kv := versioned.NewKV(ekv.MakeMemstore()) + s := NewOrLoadStore(kv) + prng := rand.New(rand.NewSource(42)) + + numToTest := 100 + + idsGenerated := make(map[uint64]interface{}) + + for i := 0; i < numToTest; i++ { + testID, err := generateFakeIdentity(prng, 15, netTime.Now()) + if err != nil { + t.Fatalf("Failed to generate fake ID: %+v", err) + } + testID.Fake = false + if s.AddIdentity(testID.Identity) != nil { + t.Errorf("AddIdentity() produced an error: %+v", err) + } + + idsGenerated[getIDFp(testID.EphemeralIdentity)] = nil + + } + + //get one + idu, err := s.GetIdentities(1, prng, 15) + if err != nil { + t.Errorf("GetIdentity() produced an error: %+v", err) + } + + if _, exists := idsGenerated[getIDFp(idu[0].EphemeralIdentity)]; !exists || + idu[0].Fake { + t.Errorf("An unknown or fake identity was returned") + } + + //get three + idu, err = s.GetIdentities(3, prng, 15) + if err != nil { + t.Errorf("GetIdentity() produced an error: %+v", err) + } + + if len(idu) != 3 { + t.Errorf("the wrong number of identities was returned") + } + + for i := 0; i < len(idu); i++ { + if _, exists := idsGenerated[getIDFp(idu[i].EphemeralIdentity)]; !exists || + idu[i].Fake { + t.Errorf("An unknown or fake identity was returned") + } + } + + //get ten + idu, err = s.GetIdentities(10, prng, 15) + if err != nil { + t.Errorf("GetIdentity() produced an error: %+v", err) + } + + if len(idu) != 10 { + t.Errorf("the wrong number of identities was returned") + } + + for i := 0; i < len(idu); i++ { + if _, exists := idsGenerated[getIDFp(idu[i].EphemeralIdentity)]; !exists || + idu[i].Fake { + t.Errorf("An unknown or fake identity was returned") + } + } + + //get fifty + idu, err = s.GetIdentities(50, prng, 15) + if err != nil { + t.Errorf("GetIdentity() produced an error: %+v", err) + } + + if len(idu) != 50 { + t.Errorf("the wrong number of identities was returned") + } + + for i := 0; i < len(idu); i++ { + if _, exists := idsGenerated[getIDFp(idu[i].EphemeralIdentity)]; !exists || + idu[i].Fake { + t.Errorf("An unknown or fake identity was returned") + } + } + + //get 100 + idu, err = s.GetIdentities(100, prng, 15) + if err != nil { + t.Errorf("GetIdentity() produced an error: %+v", err) + } + + if len(idu) != 100 { + t.Errorf("the wrong number of identities was returned") + } + + for i := 0; i < len(idu); i++ { + if _, exists := idsGenerated[getIDFp(idu[i].EphemeralIdentity)]; !exists || + idu[i].Fake { + t.Errorf("An unknown or fake identity was returned") + } + } + + //get 1000, should only return 100 + idu, err = s.GetIdentities(1000, prng, 15) + if err != nil { + t.Errorf("GetIdentity() produced an error: %+v", err) + } + + if len(idu) != 100 { + t.Errorf("the wrong number of identities was returned") + } + + for i := 0; i < len(idu); i++ { + if _, exists := idsGenerated[getIDFp(idu[i].EphemeralIdentity)]; !exists || + idu[i].Fake { + t.Errorf("An unknown or fake identity was returned") + } + } + + // get 100 a second time and make sure the order is not the same as a + // smoke test that the shuffle is working + idu2, err := s.GetIdentities(1000, prng, 15) + if err != nil { + t.Errorf("GetIdentity() produced an error: %+v", err) + } + + diferent := false + for i := 0; i < len(idu); i++ { + if !idu[i].Source.Cmp(idu2[i].Source) { + diferent = true + break + } + } + + if !diferent { + t.Errorf("The 2 100 shuffels retruned the same result, shuffling" + + " is likley not occuring") + } + +} + +func TestStore_GetIdentities_NoIdentities(t *testing.T) { + kv := versioned.NewKV(ekv.MakeMemstore()) + s := NewOrLoadStore(kv) + prng := rand.New(rand.NewSource(42)) + + idu, err := s.GetIdentities(5, prng, 15) + if err != nil { + t.Errorf("GetIdentities() produced an error: %+v", err) + } + + if len(idu) != 1 { + t.Errorf("GetIdenties() did not return only one identity " + + "when looking for a fake") + } + + if !idu[0].Fake { + t.Errorf("GetIdenties() did not return a fake identity " + + "when only one is avalible") + } +} + +func TestStore_GetIdentities_BadNum(t *testing.T) { + kv := versioned.NewKV(ekv.MakeMemstore()) + s := NewOrLoadStore(kv) + prng := rand.New(rand.NewSource(42)) + + _, err := s.GetIdentities(0, prng, 15) + if err == nil { + t.Errorf("GetIdentities() shoud error with bad num value") + } + + _, err = s.GetIdentities(-1, prng, 15) + if err == nil { + t.Errorf("GetIdentities() shoud error with bad num value") + } + + _, err = s.GetIdentities(-100, prng, 15) + if err == nil { + t.Errorf("GetIdentities() shoud error with bad num value") + } + + _, err = s.GetIdentities(-1000000, prng, 15) + if err == nil { + t.Errorf("GetIdentities() shoud error with bad num value") + } + + _, err = s.GetIdentities(math.MinInt64, prng, 15) + if err == nil { + t.Errorf("GetIdentities() shoud error with bad num value") + } +} + +func getIDFp(identity EphemeralIdentity) uint64 { + h, _ := hash.NewCMixHash() + h.Write(identity.EphId[:]) + h.Write(identity.Source.Bytes()) + r := h.Sum(nil) + return binary.BigEndian.Uint64(r) +} + func TestStore_AddIdentity(t *testing.T) { kv := versioned.NewKV(ekv.MakeMemstore()) s := NewOrLoadStore(kv) diff --git a/cmix/identity/tracker.go b/cmix/identity/tracker.go index 8d9f8a6d2a168d27147f9fe280f2d777e0b9935c..2a2cfbd18dcb9f7a1f8d1e08ba81a51145fe9a72 100644 --- a/cmix/identity/tracker.go +++ b/cmix/identity/tracker.go @@ -144,11 +144,21 @@ func (t *manager) RemoveIdentity(id *id.ID) { } // GetEphemeralIdentity returns an ephemeral Identity to poll the network with. +// It will return a fake identity if none are available. func (t *manager) GetEphemeralIdentity(rng io.Reader, addressSize uint8) ( receptionID.IdentityUse, error) { return t.ephemeral.GetIdentity(rng, addressSize) } +// GetEphemeralIdentities returns a fisher-yates shuffled list of up to 'num' +// ephemeral identities. It will return a fake identity if none are available +// and less than 'num' if less than 'num' are available. +// 'num' must be positive non-zero +func (t *manager) GetEphemeralIdentities(num int, rng io.Reader, addressSize uint8) ( + []receptionID.IdentityUse, error) { + return t.ephemeral.GetIdentities(num, rng, addressSize) +} + // GetIdentity returns a currently tracked identity func (t *manager) GetIdentity(get *id.ID) (TrackedID, error) { t.mux.Lock() diff --git a/go.mod b/go.mod index 7b732085c5ae630d5ef1cca6c40e3af43f7b5d70..bba8ff464ab07d868e0f0f3f7af973e74677605c 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/golang/protobuf v1.5.2 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/pkg/errors v0.9.1 + github.com/pkg/profile v1.6.0 github.com/spf13/cobra v1.5.0 github.com/spf13/jwalterweatherman v1.1.0 github.com/spf13/viper v1.12.0 @@ -39,7 +40,6 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.2 // indirect - github.com/pkg/profile v1.6.0 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/spf13/afero v1.9.2 // indirect github.com/spf13/cast v1.5.0 // indirect