From 4fd0832df953d1dc3139baa69f9a06a42b029a8f Mon Sep 17 00:00:00 2001 From: Thomas Miceli <27960254+thomiceli@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:50:13 +0100 Subject: [PATCH] Allow to define secret key & move the secret key file to parent directory (#358) --- config.yml | 3 +++ docs/configuration/cheat-sheet.md | 1 + internal/cli/main.go | 2 ++ internal/config/config.go | 17 ++++++++++++- internal/config/migrate.go | 42 +++++++++++++++++++++++++++++++ internal/db/totp.go | 5 ++-- internal/utils/session.go | 8 +++--- internal/web/server.go | 6 ++--- internal/web/test/auth_test.go | 24 ++++++------------ internal/web/test/gist_test.go | 24 ++++++------------ internal/web/test/server.go | 9 ++++++- 11 files changed, 98 insertions(+), 43 deletions(-) create mode 100644 internal/config/migrate.go diff --git a/config.yml b/config.yml index 1230d2c..df0b400 100644 --- a/config.yml +++ b/config.yml @@ -14,6 +14,9 @@ external-url: # Directory where Opengist will store its data. Default: ~/.opengist/ opengist-home: +# Secret key used for session store & encrypt MFA data on database. Default: +secret-key: + # URI of the database. Default: opengist.db (SQLite) # SQLite: file name # PostgreSQL: postgres://user:password@host:port/database diff --git a/docs/configuration/cheat-sheet.md b/docs/configuration/cheat-sheet.md index d8880a9..da1dddf 100644 --- a/docs/configuration/cheat-sheet.md +++ b/docs/configuration/cheat-sheet.md @@ -10,6 +10,7 @@ aside: false | log-output | OG_LOG_OUTPUT | `stdout,file` | Set the log output to one or more of the following: `stdout`, `file`. | | external-url | OG_EXTERNAL_URL | none | Public URL to access to Opengist. | | opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. | +| secret-key | OG_SECRET_KEY | randomized 32 bytes | Secret key used for session store & encrypt MFA data on database. | | db-filename | OG_DB_FILENAME | `opengist.db` | Name of the SQLite database file. | | index.enabled | OG_INDEX_ENABLED | `true` | Enable or disable the code search index (`true` or `false`) | | index.dirname | OG_INDEX_DIRNAME | `opengist.index` | Name of the directory where the code search index is stored. | diff --git a/internal/cli/main.go b/internal/cli/main.go index 43aa5cc..9a403c3 100644 --- a/internal/cli/main.go +++ b/internal/cli/main.go @@ -76,6 +76,8 @@ func Initialize(ctx *cli.Context) { panic(err) } + config.SetupSecretKey() + config.InitLog() gitVersion, err := git.GetGitVersion() diff --git a/internal/config/config.go b/internal/config/config.go index 1d4e1f4..06e99d2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -27,6 +27,8 @@ var SecretKey []byte // Not using nested structs because the library // doesn't support dot notation in this case sadly type config struct { + SecretKey string `yaml:"secret-key" env:"OG_SECRET_KEY"` + LogLevel string `yaml:"log-level" env:"OG_LOG_LEVEL"` LogOutput string `yaml:"log-output" env:"OG_LOG_OUTPUT"` ExternalUrl string `yaml:"external-url" env:"OG_EXTERNAL_URL"` @@ -82,6 +84,8 @@ type StaticLink struct { func configWithDefaults() (*config, error) { c := &config{} + c.SecretKey = "" + c.LogLevel = "warn" c.LogOutput = "stdout,file" c.OpengistHome = "" @@ -138,7 +142,9 @@ func InitConfig(configPath string, out io.Writer) error { C = c - // SecretKey = utils.GenerateSecretKey(filepath.Join(GetHomeDir(), "opengist-secret.key")) + if err = migrateConfig(); err != nil { + return err + } if err = os.Setenv("OG_OPENGIST_HOME_INTERNAL", GetHomeDir()); err != nil { return err @@ -235,6 +241,15 @@ func GetHomeDir() string { return filepath.Clean(absolutePath) } +func SetupSecretKey() { + if C.SecretKey == "" { + path := filepath.Join(GetHomeDir(), "opengist-secret.key") + SecretKey, _ = utils.GenerateSecretKey(path) + } else { + SecretKey = []byte(C.SecretKey) + } +} + func loadConfigFromYaml(c *config, configPath string, out io.Writer) error { if configPath != "" { absolutePath, _ := filepath.Abs(configPath) diff --git a/internal/config/migrate.go b/internal/config/migrate.go new file mode 100644 index 0000000..54c7cdf --- /dev/null +++ b/internal/config/migrate.go @@ -0,0 +1,42 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" +) + +// auto migration for newer versions of Opengist +func migrateConfig() error { + configMigrations := []struct { + Version string + Func func() error + }{ + {"1.8.0", v1_8_0}, + } + + for _, fn := range configMigrations { + err := fn.Func() + if err != nil { + return err + } + } + + return nil +} + +func v1_8_0() error { + homeDir := GetHomeDir() + moveFile(filepath.Join(filepath.Join(homeDir, "sessions"), "session-auth.key"), filepath.Join(homeDir, "opengist-secret.key")) + return nil +} + +func moveFile(oldPath, newPath string) { + if _, err := os.Stat(oldPath); err != nil { + return + } + + if err := os.Rename(oldPath, newPath); err == nil { + fmt.Printf("Automatically moved %s to %s\n", oldPath, newPath) + } +} diff --git a/internal/db/totp.go b/internal/db/totp.go index 60e30f4..7ae7dce 100644 --- a/internal/db/totp.go +++ b/internal/db/totp.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" ogtotp "github.com/thomiceli/opengist/internal/auth/totp" + "github.com/thomiceli/opengist/internal/config" "github.com/thomiceli/opengist/internal/utils" "slices" ) @@ -29,7 +30,7 @@ func GetTOTPByUserID(userID uint) (*TOTP, error) { func (totp *TOTP) StoreSecret(secret string) error { secretBytes := []byte(secret) - encrypted, err := utils.AESEncrypt([]byte("tmp"), secretBytes) + encrypted, err := utils.AESEncrypt(config.SecretKey, secretBytes) if err != nil { return err } @@ -44,7 +45,7 @@ func (totp *TOTP) ValidateCode(code string) (bool, error) { return false, err } - secretBytes, err := utils.AESDecrypt([]byte("tmp"), ciphertext) + secretBytes, err := utils.AESDecrypt(config.SecretKey, ciphertext) if err != nil { return false, err } diff --git a/internal/utils/session.go b/internal/utils/session.go index 74e0892..fec5fac 100644 --- a/internal/utils/session.go +++ b/internal/utils/session.go @@ -6,10 +6,12 @@ import ( "os" ) -func GenerateSecretKey(filePath string) []byte { +// GenerateSecretKey generates a new secret key for sessions +// Returns the key and a boolean indicating if the key was generated +func GenerateSecretKey(filePath string) ([]byte, bool) { key, err := os.ReadFile(filePath) if err == nil { - return key + return key, false } key = securecookie.GenerateRandomKey(32) @@ -22,5 +24,5 @@ func GenerateSecretKey(filePath string) []byte { log.Fatal().Err(err).Msgf("Failed to save the key to %s", filePath) } - return key + return key, true } diff --git a/internal/web/server.go b/internal/web/server.go index 74530a5..03010bf 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -167,10 +167,8 @@ type Server struct { func NewServer(isDev bool, sessionsPath string) *Server { dev = isDev flashStore = sessions.NewCookieStore([]byte("opengist")) - userStore = sessions.NewFilesystemStore(sessionsPath, - utils.GenerateSecretKey(path.Join(sessionsPath, "session-auth.key")), - utils.GenerateSecretKey(path.Join(sessionsPath, "session-encrypt.key")), - ) + encryptKey, _ := utils.GenerateSecretKey(filepath.Join(sessionsPath, "session-encrypt.key")) + userStore = sessions.NewFilesystemStore(sessionsPath, config.SecretKey, encryptKey) userStore.MaxLength(10 * 1024) gothic.Store = userStore diff --git a/internal/web/test/auth_test.go b/internal/web/test/auth_test.go index cc13dc6..16bce8e 100644 --- a/internal/web/test/auth_test.go +++ b/internal/web/test/auth_test.go @@ -12,12 +12,10 @@ import ( ) func TestRegister(t *testing.T) { - setup(t) - s, err := newTestServer() - require.NoError(t, err, "Failed to create test server") + s := setup(t) defer teardown(t, s) - err = s.request("GET", "/", nil, 302) + err := s.request("GET", "/", nil, 302) require.NoError(t, err) err = s.request("GET", "/register", nil, 200) @@ -55,12 +53,10 @@ func TestRegister(t *testing.T) { } func TestLogin(t *testing.T) { - setup(t) - s, err := newTestServer() - require.NoError(t, err, "Failed to create test server") + s := setup(t) defer teardown(t, s) - err = s.request("GET", "/login", nil, 200) + err := s.request("GET", "/login", nil, 200) require.NoError(t, err) user1 := db.UserDTO{Username: "thomas", Password: "thomas"} @@ -101,15 +97,13 @@ type settingSet struct { } func TestAnonymous(t *testing.T) { - setup(t) - s, err := newTestServer() - require.NoError(t, err, "Failed to create test server") + s := setup(t) defer teardown(t, s) user := db.UserDTO{Username: "thomas", Password: "azeaze"} register(t, s, user) - err = s.request("PUT", "/admin-panel/set-config", settingSet{"require-login", "1"}, 200) + err := s.request("PUT", "/admin-panel/set-config", settingSet{"require-login", "1"}, 200) require.NoError(t, err) gist1 := db.GistDTO{ @@ -154,9 +148,7 @@ func TestAnonymous(t *testing.T) { } func TestGitOperations(t *testing.T) { - setup(t) - s, err := newTestServer() - require.NoError(t, err, "Failed to create test server") + s := setup(t) defer teardown(t, s) admin := db.UserDTO{Username: "thomas", Password: "thomas"} @@ -178,7 +170,7 @@ func TestGitOperations(t *testing.T) { "yeah", }, } - err = s.request("POST", "/", gist1, 302) + err := s.request("POST", "/", gist1, 302) require.NoError(t, err) gist2 := db.GistDTO{ diff --git a/internal/web/test/gist_test.go b/internal/web/test/gist_test.go index 840753b..310cc66 100644 --- a/internal/web/test/gist_test.go +++ b/internal/web/test/gist_test.go @@ -9,12 +9,10 @@ import ( ) func TestGists(t *testing.T) { - setup(t) - s, err := newTestServer() - require.NoError(t, err, "Failed to create test server") + s := setup(t) defer teardown(t, s) - err = s.request("GET", "/", nil, 302) + err := s.request("GET", "/", nil, 302) require.NoError(t, err) user1 := db.UserDTO{Username: "thomas", Password: "thomas"} @@ -106,9 +104,7 @@ func TestGists(t *testing.T) { } func TestVisibility(t *testing.T) { - setup(t) - s, err := newTestServer() - require.NoError(t, err, "Failed to create test server") + s := setup(t) defer teardown(t, s) user1 := db.UserDTO{Username: "thomas", Password: "thomas"} @@ -123,7 +119,7 @@ func TestVisibility(t *testing.T) { Name: []string{""}, Content: []string{"yeah"}, } - err = s.request("POST", "/", gist1, 302) + err := s.request("POST", "/", gist1, 302) require.NoError(t, err) gist1db, err := db.GetGistByID("1") @@ -150,9 +146,7 @@ func TestVisibility(t *testing.T) { } func TestLikeFork(t *testing.T) { - setup(t) - s, err := newTestServer() - require.NoError(t, err, "Failed to create test server") + s := setup(t) defer teardown(t, s) user1 := db.UserDTO{Username: "thomas", Password: "thomas"} @@ -167,7 +161,7 @@ func TestLikeFork(t *testing.T) { Name: []string{""}, Content: []string{"yeah"}, } - err = s.request("POST", "/", gist1, 302) + err := s.request("POST", "/", gist1, 302) require.NoError(t, err) s.sessionCookie = "" @@ -211,9 +205,7 @@ func TestLikeFork(t *testing.T) { } func TestCustomUrl(t *testing.T) { - setup(t) - s, err := newTestServer() - require.NoError(t, err, "Failed to create test server") + s := setup(t) defer teardown(t, s) user1 := db.UserDTO{Username: "thomas", Password: "thomas"} @@ -229,7 +221,7 @@ func TestCustomUrl(t *testing.T) { Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"}, Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"}, } - err = s.request("POST", "/", gist1, 302) + err := s.request("POST", "/", gist1, 302) require.NoError(t, err) gist1db, err := db.GetGistByID("1") diff --git a/internal/web/test/server.go b/internal/web/test/server.go index e6b5bd9..6463e41 100644 --- a/internal/web/test/server.go +++ b/internal/web/test/server.go @@ -133,7 +133,7 @@ func structToURLValues(s interface{}) url.Values { return v } -func setup(t *testing.T) { +func setup(t *testing.T) *testServer { var databaseDsn string databaseType = os.Getenv("OPENGIST_TEST_DB") switch databaseType { @@ -153,6 +153,8 @@ func setup(t *testing.T) { err = os.MkdirAll(filepath.Join(config.GetHomeDir()), 0755) require.NoError(t, err, "Could not create Opengist home directory") + config.SetupSecretKey() + git.ReposDirectory = path.Join("tests") config.C.IndexEnabled = false @@ -180,6 +182,11 @@ func setup(t *testing.T) { // err = index.Open(filepath.Join(homePath, "testsindex", "opengist.index")) // require.NoError(t, err, "Could not open index") + + s, err := newTestServer() + require.NoError(t, err, "Failed to create test server") + + return s } func teardown(t *testing.T, s *testServer) {