Skip to content

Commit

Permalink
Encryption At Rest (#284)
Browse files Browse the repository at this point in the history
  • Loading branch information
AnalogJ authored Nov 7, 2023
1 parent d657ec0 commit 77cb814
Show file tree
Hide file tree
Showing 13 changed files with 466 additions and 127 deletions.
25 changes: 25 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,28 @@ curl -H "Authorization: Bearer ${JWT_TOKEN_HERE}" http://localhost:5984/_session
```bash
ng run fastenhealth:storybook
```


# Access Encrypted SQLite Database with IntelliJ

- Download the latest `sqlite-jdbc-crypt` jar from https://github.com/Willena/sqlite-jdbc-crypt/releases
- Open IntelliJ -> Data Source Properties -> Driver Tab
- Find & Select `Sqlite` -> Right Click -> Duplicate
- Rename to `Sqlite (Encrypted)`
- Find `Driver Files` -> Select `sqlite-jdbc-crypt` jar that you downloaded previously
- Remove `Xerial Sqlite JDBC` jar
- Click `Apply` -> Click `OK`
- Create New Data Source -> Select `Sqlite (Encrypted)` -> Change Connection Type to `Url only`
- Specify the following connection url: `jdbc:sqlite:fasten.db?cipher=sqlcipher&legacy=3&hmac_use=0&kdf_iter=4000&legacy_page_size=1024&key=123456789012345678901234567890`
- Replace `key` with the encryption key specified in your config file (`database.encryption_key`)
- Click `Test Connection` -> Should be successful
- Click `Apply` -> Click `OK`

# Flush SQLite Write-Ahead-Log (WAL) to Database

```sqlite
PRAGMA wal_checkpoint(TRUNCATE);
```

See: https://sqlite.org/forum/info/fefd56014e2135589ea57825b0e2aa3e2df5daf53b5e41aa6a9d8f0c29d0b8e5
TODO: check if https://www.sqlite.org/pragma.html#pragma_wal_checkpoint can be used to do this automatically.
11 changes: 10 additions & 1 deletion backend/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func (c *configuration) Init() error {
c.SetDefault("web.src.frontend.path", "/opt/fasten/web")
c.SetDefault("database.type", "sqlite")
c.SetDefault("database.location", "/opt/fasten/db/fasten.db")
//c.SetDefault("database.encryption.key", "") //encryption key must be set by the user.
c.SetDefault("cache.location", "/opt/fasten/cache/")

c.SetDefault("jwt.issuer.key", "thisismysupersecuressessionsecretlength")
Expand Down Expand Up @@ -80,6 +81,14 @@ func (c *configuration) ReadConfig(configFilePath string) error {

// This function ensures that required configuration keys (that must be manually set) are present
func (c *configuration) ValidateConfig() error {

if c.IsSet("database.encryption.key") {
key := c.GetString("database.encryption.key")
if key == "" {
return errors.ConfigValidationError("database.encryption.key cannot be empty")
}
if len(key) < 10 {
return errors.ConfigValidationError("database.encryption.key must be at least 10 characters")
}
}
return nil
}
25 changes: 25 additions & 0 deletions backend/pkg/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package config

import (
"github.com/fastenhealth/fasten-onprem/backend/pkg/errors"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
"testing"
)

func Test_ValidateConfig(t *testing.T) {
//setup
testConfig := configuration{
Viper: viper.New(),
}

//test & verify
testConfig.Set("database.encryption.key", "tooshort")
err := testConfig.ValidateConfig()
require.ErrorIs(t, err, errors.ConfigValidationError("database.encryption.key must be at least 10 characters"))

testConfig.Set("database.encryption.key", "")
err = testConfig.ValidateConfig()
require.ErrorIs(t, err, errors.ConfigValidationError("database.encryption.key cannot be empty"))

}
21 changes: 5 additions & 16 deletions backend/pkg/database/gorm_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"time"

Expand Down Expand Up @@ -485,7 +484,7 @@ func (gr *GormRepository) AddResourceAssociation(ctx context.Context, source *mo
"related_resource_source_resource_type": relatedResourceType,
"related_resource_source_resource_id": relatedResourceId,
}).Error
uniqueConstraintError := errors.New("constraint failed: UNIQUE constraint failed")
uniqueConstraintError := errors.New("UNIQUE constraint failed")
if err != nil {
if strings.HasPrefix(err.Error(), uniqueConstraintError.Error()) {
gr.Logger.Warnf("Ignoring an error when creating a related_resource association for %s/%s: %v", resourceType, resourceId, err)
Expand Down Expand Up @@ -1002,8 +1001,11 @@ func (gr *GormRepository) ListBackgroundJobs(ctx context.Context, queryOptions m
var backgroundJobs []models.BackgroundJob
query := gr.GormClient.WithContext(ctx).
//Group("source_id"). //broken in Postgres.
Where(queryParam).Limit(queryOptions.Limit).Order("locked_time DESC")
Where(queryParam).Order("locked_time DESC")

if queryOptions.Limit > 0 {
query = query.Limit(queryOptions.Limit)
}
if queryOptions.Offset > 0 {
query = query.Offset(queryOptions.Offset)
}
Expand Down Expand Up @@ -1119,19 +1121,6 @@ func (gr *GormRepository) CancelAllLockedBackgroundJobsAndFail() error {
// Utilities
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

func sqlitePragmaString(pragmas map[string]string) string {
q := url.Values{}
for key, val := range pragmas {
q.Add("_pragma", fmt.Sprintf("%s=%s", key, val))
}

queryStr := q.Encode()
if len(queryStr) > 0 {
return "?" + queryStr
}
return ""
}

// Internal function
// This function will return a list of resources from all FHIR tables in the database
// The query allows us to set the source id, source resource id, source resource type
Expand Down
3 changes: 3 additions & 0 deletions backend/pkg/database/gorm_repository_query_sql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func (suite *RepositorySqlTestSuite) BeforeTest(suiteName, testName string) {
fakeConfig := mock_config.NewMockInterface(suite.MockCtrl)
fakeConfig.EXPECT().GetString("database.location").Return(suite.TestDatabase.Name()).AnyTimes()
fakeConfig.EXPECT().GetString("database.type").Return("sqlite").AnyTimes()
fakeConfig.EXPECT().IsSet("database.encryption.key").Return(false).AnyTimes()
fakeConfig.EXPECT().GetString("log.level").Return("INFO").AnyTimes()
dbRepo, err := NewRepository(fakeConfig, logrus.WithField("test", suite.T().Name()), event_bus.NewNoopEventBusServer())
require.NoError(suite.T(), err)
Expand All @@ -62,6 +63,8 @@ func (suite *RepositorySqlTestSuite) BeforeTest(suiteName, testName string) {
func (suite *RepositorySqlTestSuite) AfterTest(suiteName, testName string) {
suite.MockCtrl.Finish()
os.Remove(suite.TestDatabase.Name())
os.Remove(suite.TestDatabase.Name() + "-shm")
os.Remove(suite.TestDatabase.Name() + "-wal")
}

// In order for 'go test' to run this suite, we need to create
Expand Down
1 change: 1 addition & 0 deletions backend/pkg/database/gorm_repository_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ func (suite *RepositoryTestSuite) TestQueryResources_SQL() {
fakeConfig := mock_config.NewMockInterface(suite.MockCtrl)
fakeConfig.EXPECT().GetString("database.location").Return(suite.TestDatabase.Name()).AnyTimes()
fakeConfig.EXPECT().GetString("database.type").Return("sqlite").AnyTimes()
fakeConfig.EXPECT().IsSet("database.encryption.key").Return(false).AnyTimes()
fakeConfig.EXPECT().GetString("log.level").Return("INFO").AnyTimes()
dbRepo, err := NewRepository(fakeConfig, logrus.WithField("test", suite.T().Name()), event_bus.NewNoopEventBusServer())
require.NoError(suite.T(), err)
Expand Down
Loading

0 comments on commit 77cb814

Please sign in to comment.