package secretsstore import ( "errors" "fmt" "slices" "strings" bolt "go.etcd.io/bbolt" ) var boltStoreBucketName []byte = []byte("secrets-store") // bboltStore implements the SecretsStore interface on top of a bbolt bucket. type bboltStore struct { db *bolt.DB } // newBboltStore opens a bolt db at path, creates a default bucket, and uses it // as the backing storage for a SecretsStore. Fails if dbpath does not have a // `.db` suffix. func newBboltStore(dbpath string) (*bboltStore, error) { if !strings.HasSuffix(dbpath, ".db") { return nil, errors.New("store path must end in `.db`") } db, err := bolt.Open(dbpath, 0600, nil) if err != nil { return nil, fmt.Errorf("opening db: %w", err) } // Ensure the default bucket. err = db.Update(func(tx *bolt.Tx) error { if _, err := tx.CreateBucketIfNotExists(boltStoreBucketName); err != nil { return err } return nil }) return &bboltStore{db: db}, nil } func (s *bboltStore) Get(key Key) ([]byte, error) { var res []byte err := s.db.View(func(tx *bolt.Tx) error { b := tx.Bucket(boltStoreBucketName) if b == nil { return ErrInvalidStore } val := b.Get([]byte(key)) if val == nil { return ErrKeyDoesNotExist } // Must allocate and copy, memory is owned by bolt, and we should not // assume that its lifetime extends beyond the duration of this tx. res = slices.Clone(val) return nil }) if err != nil { return nil, err } return res, nil } func (s *bboltStore) Put(key Key, val []byte) error { return s.db.Update(func(tx *bolt.Tx) error { b := tx.Bucket(boltStoreBucketName) if b == nil { return ErrInvalidStore } err := b.Put([]byte(key), val) if err != nil { return errors.Join(ErrCouldNotStoreKey, err) } return nil }) } func (s *bboltStore) Delete(key Key) error { return s.db.Update(func(tx *bolt.Tx) error { b := tx.Bucket(boltStoreBucketName) if b == nil { return ErrInvalidStore } err := b.Delete([]byte(key)) if err != nil { return errors.Join(ErrCouldNotDeleteKey, err) } return nil }) }