diff --git a/session/private.go b/session/private.go index 5a0ca039..8ebce81b 100644 --- a/session/private.go +++ b/session/private.go @@ -11,12 +11,14 @@ import ( type pToken struct { // private session token sessionKey *ecdsa.PrivateKey + // last epoch of the lifetime + validUntil uint64 } -// NewPrivateToken creates PrivateToken instance. +// NewPrivateToken creates PrivateToken instance that expires after passed epoch. // // Returns non-nil error on key generation error. -func NewPrivateToken() (PrivateToken, error) { +func NewPrivateToken(validUntil uint64) (PrivateToken, error) { sk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return nil, err @@ -24,6 +26,7 @@ func NewPrivateToken() (PrivateToken, error) { return &pToken{ sessionKey: sk, + validUntil: validUntil, }, nil } @@ -36,3 +39,7 @@ func (t *pToken) Sign(data []byte) ([]byte, error) { func (t *pToken) PublicKey() []byte { return crypto.MarshalPublicKey(&t.sessionKey.PublicKey) } + +func (t *pToken) Expired(epoch uint64) bool { + return t.validUntil < epoch +} diff --git a/session/private_test.go b/session/private_test.go index f0fb9f4e..7963afb6 100644 --- a/session/private_test.go +++ b/session/private_test.go @@ -10,7 +10,7 @@ import ( func TestPrivateToken(t *testing.T) { // create new private token - pToken, err := NewPrivateToken() + pToken, err := NewPrivateToken(0) require.NoError(t, err) // generate data to sign @@ -31,3 +31,20 @@ func TestPrivateToken(t *testing.T) { ), ) } + +func TestPToken_Expired(t *testing.T) { + e := uint64(10) + + var token PrivateToken = &pToken{ + validUntil: e, + } + + // must not be expired in the epoch before last + require.False(t, token.Expired(e-1)) + + // must not be expired in the last epoch + require.False(t, token.Expired(e)) + + // must be expired in the epoch after last + require.True(t, token.Expired(e+1)) +} diff --git a/session/store.go b/session/store.go index 5cd6314a..79998c7d 100644 --- a/session/store.go +++ b/session/store.go @@ -45,3 +45,20 @@ func (s *mapTokenStore) Fetch(id TokenID) (PrivateToken, error) { return t, nil } + +// RemoveExpired removes all the map elements that are expired in the passed epoch. +// +// Resulting error is always nil. +func (s *mapTokenStore) RemoveExpired(epoch uint64) error { + s.Lock() + + for key, token := range s.tokens { + if token.Expired(epoch) { + delete(s.tokens, key) + } + } + + s.Unlock() + + return nil +} diff --git a/session/store_test.go b/session/store_test.go index 37d742e8..123b1036 100644 --- a/session/store_test.go +++ b/session/store_test.go @@ -9,7 +9,7 @@ import ( func TestMapTokenStore(t *testing.T) { // create new private token - pToken, err := NewPrivateToken() + pToken, err := NewPrivateToken(0) require.NoError(t, err) // create map token store @@ -33,3 +33,64 @@ func TestMapTokenStore(t *testing.T) { // ascertain that returned token equals to initial require.Equal(t, pToken, res) } + +func TestMapTokenStore_RemoveExpired(t *testing.T) { + // create some epoch number + e1 := uint64(1) + + // create private token that expires after e1 + tok1, err := NewPrivateToken(e1) + require.NoError(t, err) + + // create some greater than e1 epoch number + e2 := e1 + 1 + + // create private token that expires after e2 + tok2, err := NewPrivateToken(e2) + require.NoError(t, err) + + // create token store instance + s := NewMapTokenStore() + + // create storage keys for tokens + id1, err := refs.NewUUID() + require.NoError(t, err) + id2, err := refs.NewUUID() + require.NoError(t, err) + + assertPresence := func(ids ...TokenID) { + for i := range ids { + _, err = s.Fetch(ids[i]) + require.NoError(t, err) + } + } + + assertAbsence := func(ids ...TokenID) { + for i := range ids { + _, err = s.Fetch(ids[i]) + require.EqualError(t, err, ErrPrivateTokenNotFound.Error()) + } + } + + // store both tokens + require.NoError(t, s.Store(id1, tok1)) + require.NoError(t, s.Store(id2, tok2)) + + // ascertain that both tokens are available + assertPresence(id1, id2) + + // perform cleaning for epoch in which both tokens are not expired + require.NoError(t, s.RemoveExpired(e1)) + + // ascertain that both tokens are still available + assertPresence(id1, id2) + + // perform cleaning for epoch greater than e1 and not greater than e2 + require.NoError(t, s.RemoveExpired(e1+1)) + + // ascertain that tok1 was removed + assertAbsence(id1) + + // ascertain that tok2 was not removed + assertPresence(id2) +} diff --git a/session/types.go b/session/types.go index 0f209c74..c890aaf5 100644 --- a/session/types.go +++ b/session/types.go @@ -17,6 +17,9 @@ type PrivateToken interface { // Resulting signature must be verified by crypto.Verify function // with the session public key. Sign([]byte) ([]byte, error) + + // Expired must return true if and only if private token is expired in the given epoch number. + Expired(uint64) bool } // PrivateTokenSource is an interface of private token storage with read access. @@ -27,9 +30,16 @@ type PrivateTokenSource interface { Fetch(TokenID) (PrivateToken, error) } +// EpochLifetimeStore is an interface of the storage of elements that lifetime is limited by NeoFS epoch. +type EpochLifetimeStore interface { + // RemoveExpired must remove all elements that are expired in the given epoch. + RemoveExpired(uint64) error +} + // PrivateTokenStore is an interface of the storage of private tokens addressable by TokenID. type PrivateTokenStore interface { PrivateTokenSource + EpochLifetimeStore // Store must save passed private token in the storage under the given key. //