From 7f8467cd100c74d6354083006fa718d662924cff Mon Sep 17 00:00:00 2001 From: KhashayarKhm Date: Mon, 28 Apr 2025 22:45:57 +0330 Subject: [PATCH] feat: add TOTP dependencies and encryption utilities - add pquerna/otp package - add TOTP fields to User and Server structs - add TOTP common error - add symmetric (de)encryption and TOTP code validator function --- cmd/root.go | 14 ++++++++ errors/errors.go | 1 + go.mod | 2 ++ go.sum | 5 +++ settings/settings.go | 30 ++++++++-------- users/password.go | 85 ++++++++++++++++++++++++++++++++++++++++++++ users/users.go | 2 ++ 7 files changed, 125 insertions(+), 14 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 59329c5c..d9a81d9c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,6 +2,7 @@ package cmd import ( "crypto/tls" + "encoding/base64" "errors" "io" "io/fs" @@ -23,6 +24,7 @@ import ( "github.com/filebrowser/filebrowser/v2/auth" "github.com/filebrowser/filebrowser/v2/diskcache" + fbErrors "github.com/filebrowser/filebrowser/v2/errors" "github.com/filebrowser/filebrowser/v2/frontend" fbhttp "github.com/filebrowser/filebrowser/v2/http" "github.com/filebrowser/filebrowser/v2/img" @@ -65,6 +67,7 @@ func addServerFlags(flags *pflag.FlagSet) { flags.StringP("baseurl", "b", "", "base url") flags.String("cache-dir", "", "file cache directory (disabled if empty)") flags.String("token-expiration-time", "2h", "user session timeout") + flags.String("totp-token-exiration-time", "2m", "user totp sesstion timeout to login") flags.Int("img-processors", 4, "image processors count") //nolint:gomnd flags.Bool("disable-thumbnails", false, "disable image thumbnails") flags.Bool("disable-preview-resize", false, "disable resize of image previews") @@ -142,6 +145,8 @@ user created with the credentials from options "username" and "password".`, checkErr(err) server.Root = root + setTOTPEncryptionKey(server) + adr := server.Address + ":" + server.Port var listener net.Listener @@ -425,3 +430,12 @@ func initConfig() { cfgFile = "Using config file: " + v.ConfigFileUsed() } } + +func setTOTPEncryptionKey(server *settings.Server) { + totpEK, err := base64.StdEncoding.DecodeString(v.GetString("totp.encryption.key")) + checkErr(err) + if len(totpEK) != 32 { + checkErr(fbErrors.ErrInvalidEncryptionKey) + } + server.TOTPEncryptionKey = totpEK +} diff --git a/errors/errors.go b/errors/errors.go index 5ec364c0..7743ed92 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -3,6 +3,7 @@ package errors import "errors" var ( + ErrInvalidEncryptionKey = errors.New("The TOTP encryption key should be a 32-byte string encoded in Base64") ErrEmptyKey = errors.New("empty key") ErrExist = errors.New("the resource already exists") ErrNotExist = errors.New("the resource does not exist") diff --git a/go.mod b/go.mod index a58d5c24..60f97db0 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/mholt/archiver/v3 v3.5.1 github.com/mitchellh/go-homedir v1.1.0 github.com/pelletier/go-toml/v2 v2.2.3 + github.com/pquerna/otp v1.4.0 github.com/shirou/gopsutil/v3 v3.24.5 github.com/spf13/afero v1.11.0 github.com/spf13/cobra v1.8.1 @@ -35,6 +36,7 @@ require ( github.com/andybalholm/brotli v1.1.0 // indirect github.com/asticode/go-astikit v0.42.0 // indirect github.com/asticode/go-astits v1.13.0 // indirect + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect diff --git a/go.sum b/go.sum index 4e451c05..00c41e3d 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/asticode/go-astisub v0.26.2/go.mod h1:WTkuSzFB+Bp7wezuSf2Oxulj5A8zu2z github.com/asticode/go-astits v1.8.0/go.mod h1:DkOWmBNQpnr9mv24KfZjq4JawCFX1FCqjLVGvO0DygQ= github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c= github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -127,6 +129,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= +github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -150,6 +154,7 @@ github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= diff --git a/settings/settings.go b/settings/settings.go index 22908396..43e5ea85 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -36,20 +36,22 @@ func (s *Settings) GetRules() []rules.Rule { // Server specific settings. type Server struct { - Root string `json:"root"` - BaseURL string `json:"baseURL"` - Socket string `json:"socket"` - TLSKey string `json:"tlsKey"` - TLSCert string `json:"tlsCert"` - Port string `json:"port"` - Address string `json:"address"` - Log string `json:"log"` - EnableThumbnails bool `json:"enableThumbnails"` - ResizePreview bool `json:"resizePreview"` - EnableExec bool `json:"enableExec"` - TypeDetectionByHeader bool `json:"typeDetectionByHeader"` - AuthHook string `json:"authHook"` - TokenExpirationTime string `json:"tokenExpirationTime"` + Root string `json:"root"` + BaseURL string `json:"baseURL"` + Socket string `json:"socket"` + TLSKey string `json:"tlsKey"` + TLSCert string `json:"tlsCert"` + Port string `json:"port"` + Address string `json:"address"` + Log string `json:"log"` + EnableThumbnails bool `json:"enableThumbnails"` + ResizePreview bool `json:"resizePreview"` + EnableExec bool `json:"enableExec"` + TypeDetectionByHeader bool `json:"typeDetectionByHeader"` + AuthHook string `json:"authHook"` + TokenExpirationTime string `json:"tokenExpirationTime"` + TOTPTokenExpirationTime string `json:"totpTokenExpirationTime"` + TOTPEncryptionKey []byte `json:"totpEncryptionKey"` } // Clean cleans any variables that might need cleaning. diff --git a/users/password.go b/users/password.go index d7ef250a..24468bb6 100644 --- a/users/password.go +++ b/users/password.go @@ -1,6 +1,15 @@ package users import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "io" + "log" + + fbErrors "github.com/filebrowser/filebrowser/v2/errors" + "github.com/pquerna/otp/totp" "golang.org/x/crypto/bcrypt" ) @@ -15,3 +24,79 @@ func CheckPwd(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil } + +// returns cipher text and nonce in base64 +func EncryptSymmetric(encryptionKey, secret []byte) (string, string, error) { + if len(encryptionKey) != 32 { + log.Printf("%s (key=\"%s\")", fbErrors.ErrInvalidEncryptionKey.Error(), string(encryptionKey)) + return "", "", fbErrors.ErrInvalidEncryptionKey + } + + block, err := aes.NewCipher(encryptionKey) + if err != nil { + return "", "", err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", "", err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return "", "", err + } + + cipherText := gcm.Seal(nil, nonce, secret, nil) + + return base64.StdEncoding.EncodeToString(cipherText), base64.StdEncoding.EncodeToString(nonce), nil +} + +func DecryptSymmetric(encryptionKey []byte, cipherTextB64, nonceB64 string) (string, error) { + if len(encryptionKey) != 32 { + log.Printf("%s (key=\"%s\")", fbErrors.ErrInvalidEncryptionKey.Error(), string(encryptionKey)) + return "", fbErrors.ErrInvalidEncryptionKey + } + + cipherText, err := base64.StdEncoding.DecodeString(cipherTextB64) + if err != nil { + return "", err + } + + nonce, err := base64.StdEncoding.DecodeString(nonceB64) + if err != nil { + return "", err + } + + block, err := aes.NewCipher(encryptionKey) + if err != nil { + return "", err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + secret, err := gcm.Open(nil, nonce, cipherText, nil) + if err != nil { + return "", err + } + + return string(secret), nil +} + +// Decrypt the secret and validate the code +func CheckTOTP(totpEncryptionKey []byte, encryptedSecretB64, nonceB64, code string) (bool, error) { + if len(totpEncryptionKey) != 32 { + log.Printf("%s (key=\"%s\")", fbErrors.ErrInvalidEncryptionKey.Error(), string(totpEncryptionKey)) + return false, fbErrors.ErrInvalidEncryptionKey + } + + secret, err := DecryptSymmetric(totpEncryptionKey, encryptedSecretB64, nonceB64) + if err != nil { + return false, err + } + + return totp.Validate(code, secret), nil +} diff --git a/users/users.go b/users/users.go index ec613856..29eebcf6 100644 --- a/users/users.go +++ b/users/users.go @@ -23,6 +23,8 @@ const ( type User struct { ID uint `storm:"id,increment" json:"id"` Username string `storm:"unique" json:"username"` + TOTPSecret string `json:"totpSecret"` + TOTPNonce string `json:"totpNonce"` Password string `json:"password"` Scope string `json:"scope"` Locale string `json:"locale"`