From 9e6e0205fc3722c9e76795f9fa99bd04c1e5e2a8 Mon Sep 17 00:00:00 2001 From: Patrick Deziel Date: Wed, 25 Oct 2023 13:17:44 -0500 Subject: [PATCH 1/2] Local storage implementation --- pkg/store/errors.go | 7 ++ pkg/store/local/store.go | 141 ++++++++++++++++++++++++++++++++++ pkg/store/local/store_test.go | 74 ++++++++++++++++++ pkg/store/store.go | 20 +++++ 4 files changed, 242 insertions(+) create mode 100644 pkg/store/errors.go create mode 100644 pkg/store/local/store.go create mode 100644 pkg/store/local/store_test.go create mode 100644 pkg/store/store.go diff --git a/pkg/store/errors.go b/pkg/store/errors.go new file mode 100644 index 0000000..ada26b8 --- /dev/null +++ b/pkg/store/errors.go @@ -0,0 +1,7 @@ +package store + +import "errors" + +var ( + ErrNotFound = errors.New("resource not found in store") +) diff --git a/pkg/store/local/store.go b/pkg/store/local/store.go new file mode 100644 index 0000000..91091a0 --- /dev/null +++ b/pkg/store/local/store.go @@ -0,0 +1,141 @@ +package local + +import ( + "archive/zip" + "io" + "os" + "path/filepath" + "sync" + + "github.com/trisacrypto/courier/pkg/config" + "github.com/trisacrypto/courier/pkg/store" +) + +const ( + passwordPrefix = "password" + passwordFile = "password.txt" + certificatePrefix = "certificate" + certificateFile = "certificate.pem" +) + +// Open the local storage backend. +func Open(conf config.LocalStorageConfig) (store *Store, err error) { + store = &Store{ + path: conf.Path, + } + + // Ensure the path exists + if err = os.MkdirAll(conf.Path, 0755); err != nil { + return nil, err + } + + return store, nil +} + +// Store implements the store.Store interface for local storage. +type Store struct { + sync.RWMutex + path string +} + +var _ store.Store = &Store{} + +// Close the local storage backend. +func (s *Store) Close() error { + return nil +} + +//=========================================================================== +// Password Methods +//=========================================================================== + +// GetPassword retrieves a password by id from the local storage backend. +func (s *Store) GetPassword(id string) (password []byte, err error) { + s.RLock() + defer s.RUnlock() + return s.load(s.fullPath(passwordPrefix, id)) +} + +// UpdatePassword updates a password by id in the local storage backend. If the +// password does not exist, it is created. Otherwise, it is overwritten. +func (s *Store) UpdatePassword(id string, password []byte) (err error) { + s.Lock() + defer s.Unlock() + return s.store(s.fullPath(passwordPrefix, id), passwordFile, password) +} + +//=========================================================================== +// Certificate Methods +//=========================================================================== + +// GetCertificate retrieves a certificate by id from the local storage backend. +func (s *Store) GetCertificate(name string) (cert []byte, err error) { + s.RLock() + defer s.RUnlock() + return s.load(s.fullPath(certificatePrefix, name)) +} + +// UpdateCertificate updates a certificate in the local storage backend. +func (s *Store) UpdateCertificate(name string, cert []byte) (err error) { + s.Lock() + defer s.Unlock() + return s.store(s.fullPath(certificatePrefix, name), certificateFile, cert) +} + +//=========================================================================== +// Helper methods +//=========================================================================== + +// fullPath returns the full path to an archive file in the local storage backend. +func (s *Store) fullPath(prefix, name string) string { + return filepath.Join(s.path, prefix+"-"+name+".zip") +} + +// load returns file data by archive path from the local storage +func (s *Store) load(path string) (data []byte, err error) { + var archive *zip.ReadCloser + if archive, err = zip.OpenReader(path); err != nil { + if os.IsNotExist(err) { + return nil, store.ErrNotFound + } + return nil, err + } + defer archive.Close() + + // Load the file from the archive + if len(archive.File) == 0 { + return nil, store.ErrNotFound + } + + var reader io.ReadCloser + if reader, err = archive.File[0].Open(); err != nil { + return nil, err + } + defer reader.Close() + + return io.ReadAll(reader) +} + +// store saves file data to an archive and file name in the local storage +func (s *Store) store(path, name string, data []byte) (err error) { + var archive *os.File + if archive, err = os.Create(path); err != nil { + return err + } + defer archive.Close() + + // Write the file to the archive + zipWriter := zip.NewWriter(archive) + defer zipWriter.Close() + + var writer io.Writer + if writer, err = zipWriter.Create(name); err != nil { + return err + } + + if _, err = writer.Write(data); err != nil { + return err + } + + return nil +} diff --git a/pkg/store/local/store_test.go b/pkg/store/local/store_test.go new file mode 100644 index 0000000..544c855 --- /dev/null +++ b/pkg/store/local/store_test.go @@ -0,0 +1,74 @@ +package local_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/trisacrypto/courier/pkg/config" + "github.com/trisacrypto/courier/pkg/store" + "github.com/trisacrypto/courier/pkg/store/local" +) + +type localStoreTestSuite struct { + suite.Suite + conf config.LocalStorageConfig + store *local.Store +} + +func (s *localStoreTestSuite) SetupSuite() { + // Open the storage backend in a temporary directory + var err error + path := s.T().TempDir() + s.store, err = local.Open(config.LocalStorageConfig{ + Enabled: true, + Path: path, + }) + s.NoError(err, "could not open local storage backend") +} + +func (s *localStoreTestSuite) TearDownSuite() { + // Remove the temporary directory + s.NoError(s.store.Close(), "could not close local storage backend") + s.NoError(os.RemoveAll(s.conf.Path), "could not remove temporary directory") +} + +func TestLocalStore(t *testing.T) { + suite.Run(t, new(localStoreTestSuite)) +} + +func (s *localStoreTestSuite) TestPasswordStore() { + require := s.Require() + + // Try to get a password that does not exist + _, err := s.store.GetPassword("does-not-exist") + require.ErrorIs(err, store.ErrNotFound, "should return error if password does not exist") + + // Create a password + password := []byte("password") + err = s.store.UpdatePassword("password_id", password) + require.NoError(err, "should be able to create a password") + + // Get the password + actual, err := s.store.GetPassword("password_id") + require.NoError(err, "should be able to get a password") + require.Equal(password, actual, "wrong password returned") +} + +func (s *localStoreTestSuite) TestCertificateStore() { + require := s.Require() + + // Try to get a certificate that does not exist + _, err := s.store.GetCertificate("does-not-exist") + require.ErrorIs(err, store.ErrNotFound, "should return error if certificate does not exist") + + // Create a certificate + cert := []byte("certificate") + err = s.store.UpdateCertificate("certificate_id", cert) + require.NoError(err, "should be able to create a certificate") + + // Get the certificate + actual, err := s.store.GetCertificate("certificate_id") + require.NoError(err, "should be able to get a certificate") + require.Equal(cert, actual, "wrong certificate returned") +} diff --git a/pkg/store/store.go b/pkg/store/store.go new file mode 100644 index 0000000..b75bf4a --- /dev/null +++ b/pkg/store/store.go @@ -0,0 +1,20 @@ +package store + +// Store is a generic interface for storing and retrieving data. +type Store interface { + Close() error + PasswordStore + CertificateStore +} + +// PasswordStore is a generic interface for storing and retrieving passwords. +type PasswordStore interface { + GetPassword(name string) ([]byte, error) + UpdatePassword(name string, password []byte) error +} + +// CertificateStore is a generic interface for storing and retrieving certificates. +type CertificateStore interface { + GetCertificate(name string) ([]byte, error) + UpdateCertificate(name string, cert []byte) error +} From 3439b66e6e93385e46219d75ee47f6fd033b08cd Mon Sep 17 00:00:00 2001 From: Patrick Deziel Date: Wed, 25 Oct 2023 13:26:39 -0500 Subject: [PATCH 2/2] Update file names --- pkg/store/local/store.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/store/local/store.go b/pkg/store/local/store.go index 91091a0..33f8020 100644 --- a/pkg/store/local/store.go +++ b/pkg/store/local/store.go @@ -12,10 +12,10 @@ import ( ) const ( - passwordPrefix = "password" - passwordFile = "password.txt" + passwordPrefix = "pkcs12" + passwordFile = "pkcs12.password" certificatePrefix = "certificate" - certificateFile = "certificate.pem" + certificateFile = "certificate" ) // Open the local storage backend.