From e3580d93517982dc37ed07ecf06118b1ced47041 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Fri, 24 Oct 2025 15:04:54 +0800 Subject: [PATCH] feat(encryption): add UI and settings for file encryption --- application/dependency/dependency.go | 13 +- application/dependency/options.go | 9 +- assets | 2 +- cmd/masterkey.go | 230 +++++++++++++++++++++++++++ cmd/root.go | 6 +- cmd/server.go | 5 - inventory/setting.go | 8 + inventory/types/types.go | 12 +- pkg/filemanager/encrypt/aes256ctr.go | 6 +- pkg/filemanager/encrypt/encrypt.go | 6 +- pkg/filemanager/encrypt/masterkey.go | 79 ++++++++- pkg/filemanager/fs/dbfs/dbfs.go | 4 +- pkg/filemanager/fs/fs.go | 2 +- pkg/filemanager/manager/entity.go | 8 +- pkg/filemanager/manager/manager.go | 2 +- pkg/filemanager/manager/thumbnail.go | 3 +- pkg/filemanager/manager/upload.go | 4 +- pkg/filemanager/workflows/archive.go | 2 +- pkg/filemanager/workflows/extract.go | 2 +- pkg/setting/provider.go | 18 +++ pkg/setting/types.go | 8 + service/admin/file.go | 4 +- service/admin/site.go | 3 +- service/basic/site.go | 41 ++--- service/explorer/response.go | 8 + service/explorer/upload.go | 2 +- 26 files changed, 415 insertions(+), 72 deletions(-) create mode 100644 cmd/masterkey.go diff --git a/application/dependency/dependency.go b/application/dependency/dependency.go index 445638cc..ae965538 100644 --- a/application/dependency/dependency.go +++ b/application/dependency/dependency.go @@ -131,9 +131,9 @@ type Dep interface { // UAParser Get a singleton uaparser.Parser instance for user agent parsing. UAParser() *uaparser.Parser // MasterEncryptKeyVault Get a singleton encrypt.MasterEncryptKeyVault instance for master encrypt key vault. - MasterEncryptKeyVault() encrypt.MasterEncryptKeyVault + MasterEncryptKeyVault(ctx context.Context) encrypt.MasterEncryptKeyVault // EncryptorFactory Get a new encrypt.CryptorFactory instance. - EncryptorFactory() encrypt.CryptorFactory + EncryptorFactory(ctx context.Context) encrypt.CryptorFactory } type dependency struct { @@ -183,7 +183,6 @@ type dependency struct { configPath string isPro bool requiredDbVersion string - licenseKey string // Protects inner deps that can be reloaded at runtime. mu sync.Mutex @@ -212,17 +211,17 @@ func (d *dependency) RequestClient(opts ...request.Option) request.Client { return request.NewClient(d.ConfigProvider(), opts...) } -func (d *dependency) MasterEncryptKeyVault() encrypt.MasterEncryptKeyVault { +func (d *dependency) MasterEncryptKeyVault(ctx context.Context) encrypt.MasterEncryptKeyVault { if d.masterEncryptKeyVault != nil { return d.masterEncryptKeyVault } - d.masterEncryptKeyVault = encrypt.NewMasterEncryptKeyVault(d.SettingProvider()) + d.masterEncryptKeyVault = encrypt.NewMasterEncryptKeyVault(ctx, d.SettingProvider()) return d.masterEncryptKeyVault } -func (d *dependency) EncryptorFactory() encrypt.CryptorFactory { - return encrypt.NewCryptorFactory(d.MasterEncryptKeyVault()) +func (d *dependency) EncryptorFactory(ctx context.Context) encrypt.CryptorFactory { + return encrypt.NewCryptorFactory(d.MasterEncryptKeyVault(ctx)) } func (d *dependency) WebAuthn(ctx context.Context) (*webauthn.WebAuthn, error) { diff --git a/application/dependency/options.go b/application/dependency/options.go index 9c92319e..7046670e 100644 --- a/application/dependency/options.go +++ b/application/dependency/options.go @@ -1,6 +1,8 @@ package dependency import ( + "io/fs" + "github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/pkg/auth" @@ -11,7 +13,6 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/logging" "github.com/cloudreve/Cloudreve/v4/pkg/setting" "github.com/gin-contrib/static" - "io/fs" ) // Option 发送请求的额外设置 @@ -67,12 +68,6 @@ func WithProFlag(c bool) Option { }) } -func WithLicenseKey(c string) Option { - return optionFunc(func(o *dependency) { - o.licenseKey = c - }) -} - // WithRawEntClient Set the default raw ent client. func WithRawEntClient(c *ent.Client) Option { return optionFunc(func(o *dependency) { diff --git a/assets b/assets index 1c9dd8d9..8b91fca9 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 1c9dd8d9adbb6842b404ecd908a625ce519b754f +Subproject commit 8b91fca9291b58edd100949954039fc71524f97d diff --git a/cmd/masterkey.go b/cmd/masterkey.go new file mode 100644 index 00000000..ede35bce --- /dev/null +++ b/cmd/masterkey.go @@ -0,0 +1,230 @@ +package cmd + +import ( + "context" + "crypto/rand" + "encoding/base64" + "fmt" + "io" + "os" + + "github.com/cloudreve/Cloudreve/v4/application/dependency" + "github.com/cloudreve/Cloudreve/v4/ent/entity" + "github.com/cloudreve/Cloudreve/v4/inventory/types" + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/encrypt" + "github.com/cloudreve/Cloudreve/v4/pkg/setting" + "github.com/spf13/cobra" +) + +var ( + outputToFile string + newMasterKeyFile string +) + +func init() { + rootCmd.AddCommand(masterKeyCmd) + masterKeyCmd.AddCommand(masterKeyGenerateCmd) + masterKeyCmd.AddCommand(masterKeyGetCmd) + masterKeyCmd.AddCommand(masterKeyRotateCmd) + + masterKeyGenerateCmd.Flags().StringVarP(&outputToFile, "output", "o", "", "Output master key to file instead of stdout") + masterKeyRotateCmd.Flags().StringVarP(&newMasterKeyFile, "new-key", "n", "", "Path to file containing the new master key (base64 encoded).") +} + +var masterKeyCmd = &cobra.Command{ + Use: "master-key", + Short: "Master encryption key management", + Long: "Manage master encryption keys for file encryption. Use subcommands to generate, get, or rotate keys.", + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, +} + +var masterKeyGenerateCmd = &cobra.Command{ + Use: "generate", + Short: "Generate a new master encryption key", + Long: "Generate a new random 32-byte (256-bit) master encryption key and output it in base64 format.", + Run: func(cmd *cobra.Command, args []string) { + // Generate 32-byte random key + key := make([]byte, 32) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + fmt.Fprintf(os.Stderr, "Error: Failed to generate random key: %v\n", err) + os.Exit(1) + } + + // Encode to base64 + encodedKey := base64.StdEncoding.EncodeToString(key) + + if outputToFile != "" { + // Write to file + if err := os.WriteFile(outputToFile, []byte(encodedKey), 0600); err != nil { + fmt.Fprintf(os.Stderr, "Error: Failed to write key to file: %v\n", err) + os.Exit(1) + } + fmt.Printf("Master key generated and saved to: %s\n", outputToFile) + } else { + // Output to stdout + fmt.Println(encodedKey) + } + }, +} + +var masterKeyGetCmd = &cobra.Command{ + Use: "get", + Short: "Get the current master encryption key", + Long: "Retrieve and display the current master encryption key from the configured vault (setting, env, or file).", + Run: func(cmd *cobra.Command, args []string) { + ctx := context.Background() + dep := dependency.NewDependency( + dependency.WithConfigPath(confPath), + ) + logger := dep.Logger() + + // Get the master key vault + vault := encrypt.NewMasterEncryptKeyVault(ctx, dep.SettingProvider()) + + // Retrieve the master key + key, err := vault.GetMasterKey(ctx) + if err != nil { + logger.Error("Failed to get master key: %s", err) + os.Exit(1) + } + + // Encode to base64 and display + encodedKey := base64.StdEncoding.EncodeToString(key) + fmt.Println("") + fmt.Println(encodedKey) + }, +} + +var masterKeyRotateCmd = &cobra.Command{ + Use: "rotate", + Short: "Rotate the master encryption key", + Long: `Rotate the master encryption key by re-encrypting all encrypted file keys with a new master key. +This operation: +1. Retrieves the current master key +2. Loads a new master key from file +3. Re-encrypts all file encryption keys with the new master key +4. Updates the master key in the settings database + +Warning: This is a critical operation. Make sure to backup your database before proceeding.`, + Run: func(cmd *cobra.Command, args []string) { + ctx := context.Background() + dep := dependency.NewDependency( + dependency.WithConfigPath(confPath), + ) + logger := dep.Logger() + + logger.Info("Starting master key rotation...") + + // Get the old master key + vault := encrypt.NewMasterEncryptKeyVault(ctx, dep.SettingProvider()) + oldMasterKey, err := vault.GetMasterKey(ctx) + if err != nil { + logger.Error("Failed to get current master key: %s", err) + os.Exit(1) + } + logger.Info("Retrieved current master key") + + // Get or generate the new master key + var newMasterKey []byte + // Load from file + keyData, err := os.ReadFile(newMasterKeyFile) + if err != nil { + logger.Error("Failed to read new master key file: %s", err) + os.Exit(1) + } + newMasterKey, err = base64.StdEncoding.DecodeString(string(keyData)) + if err != nil { + logger.Error("Failed to decode new master key: %s", err) + os.Exit(1) + } + if len(newMasterKey) != 32 { + logger.Error("Invalid new master key: must be 32 bytes (256 bits), got %d bytes", len(newMasterKey)) + os.Exit(1) + } + logger.Info("Loaded new master key from file: %s", newMasterKeyFile) + + // Query all entities with encryption metadata + db := dep.DBClient() + entities, err := db.Entity.Query(). + Where(entity.Not(entity.PropsIsNil())). + All(ctx) + if err != nil { + logger.Error("Failed to query entities: %s", err) + os.Exit(1) + } + + logger.Info("Found %d entities to check for encryption", len(entities)) + + // Re-encrypt each entity's encryption key + encryptedCount := 0 + for _, ent := range entities { + if ent.Props == nil || ent.Props.EncryptMetadata == nil { + continue + } + + encMeta := ent.Props.EncryptMetadata + + // Decrypt the file key with old master key + decryptedFileKey, err := encrypt.DecryptWithMasterKey(oldMasterKey, encMeta.Key) + if err != nil { + logger.Error("Failed to decrypt key for entity %d: %s", ent.ID, err) + os.Exit(1) + } + + // Re-encrypt the file key with new master key + newEncryptedKey, err := encrypt.EncryptWithMasterKey(newMasterKey, decryptedFileKey) + if err != nil { + logger.Error("Failed to re-encrypt key for entity %d: %s", ent.ID, err) + os.Exit(1) + } + + // Update the entity + newProps := *ent.Props + newProps.EncryptMetadata = &types.EncryptMetadata{ + Algorithm: encMeta.Algorithm, + Key: newEncryptedKey, + KeyPlainText: nil, // Don't store plaintext + IV: encMeta.IV, + } + + err = db.Entity.UpdateOne(ent). + SetProps(&newProps). + Exec(ctx) + if err != nil { + logger.Error("Failed to update entity %d: %s", ent.ID, err) + os.Exit(1) + } + + encryptedCount++ + } + + logger.Info("Re-encrypted %d file keys", encryptedCount) + + // Update the master key in settings + keyStore := dep.SettingProvider().MasterEncryptKeyVault(ctx) + if keyStore == setting.MasterEncryptKeyVaultTypeSetting { + encodedNewKey := base64.StdEncoding.EncodeToString(newMasterKey) + err = dep.SettingClient().Set(ctx, map[string]string{ + "encrypt_master_key": encodedNewKey, + }) + if err != nil { + logger.Error("Failed to update master key in settings: %s", err) + logger.Error("WARNING: File keys have been re-encrypted but master key update failed!") + logger.Error("Please manually update the encrypt_master_key setting.") + os.Exit(1) + } + } else { + logger.Info("Current master key is stored in %q", keyStore) + if keyStore == setting.MasterEncryptKeyVaultTypeEnv { + logger.Info("Please update the new master encryption key in your \"CR_ENCRYPT_MASTER_KEY\" environment variable.") + } else if keyStore == setting.MasterEncryptKeyVaultTypeFile { + logger.Info("Please update the new master encryption key in your key file: %q", dep.SettingProvider().MasterEncryptKeyFile(ctx)) + } + logger.Info("Last step: Please manually update the new master encryption key in your ENV or key file.") + } + + logger.Info("Master key rotation completed successfully") + }, +} diff --git a/cmd/root.go b/cmd/root.go index 0b7b2ff9..47acb08a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,14 +2,16 @@ package cmd import ( "fmt" + "os" + "github.com/cloudreve/Cloudreve/v4/pkg/util" "github.com/spf13/cobra" "github.com/spf13/pflag" - "os" ) var ( - confPath string + confPath string + licenseKey string ) func init() { diff --git a/cmd/server.go b/cmd/server.go index 1713a31e..08b51178 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -12,10 +12,6 @@ import ( "github.com/spf13/cobra" ) -var ( - licenseKey string -) - func init() { rootCmd.AddCommand(serverCmd) serverCmd.PersistentFlags().StringVarP(&licenseKey, "license-key", "l", "", "License key of your Cloudreve Pro") @@ -29,7 +25,6 @@ var serverCmd = &cobra.Command{ dependency.WithConfigPath(confPath), dependency.WithProFlag(constants.IsProBool), dependency.WithRequiredDbVersion(constants.BackendVersion), - dependency.WithLicenseKey(licenseKey), ) server := application.NewServer(dep) logger := dep.Logger() diff --git a/inventory/setting.go b/inventory/setting.go index 5b926073..8ba62d24 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -665,6 +665,14 @@ var DefaultSettings = map[string]string{ "headless_bottom_html": "", "sidebar_bottom_html": "", "encrypt_master_key": "", + "encrypt_master_key_vault": "setting", + "encrypt_master_key_file": "", + "show_encryption_status": "1", +} + +var RedactedSettings = map[string]struct{}{ + "encrypt_master_key": {}, + "secret_key": {}, } func init() { diff --git a/inventory/types/types.go b/inventory/types/types.go index 50a8046d..3c7a6ff5 100644 --- a/inventory/types/types.go +++ b/inventory/types/types.go @@ -161,13 +161,13 @@ type ( EncryptMetadata *EncryptMetadata `json:"encrypt_metadata,omitempty"` } - Algorithm string + Cipher string EncryptMetadata struct { - Algorithm Algorithm `json:"algorithm"` - Key []byte `json:"key"` - KeyPlainText []byte `json:"key_plain_text,omitempty"` - IV []byte `json:"iv"` + Algorithm Cipher `json:"algorithm"` + Key []byte `json:"key"` + KeyPlainText []byte `json:"key_plain_text,omitempty"` + IV []byte `json:"iv"` } DavAccountProps struct { @@ -361,5 +361,5 @@ const ( ) const ( - AlgorithmAES256CTR Algorithm = "aes-256-ctr" + CipherAES256CTR Cipher = "aes-256-ctr" ) diff --git a/pkg/filemanager/encrypt/aes256ctr.go b/pkg/filemanager/encrypt/aes256ctr.go index 1b21dd53..ff7dfbf1 100644 --- a/pkg/filemanager/encrypt/aes256ctr.go +++ b/pkg/filemanager/encrypt/aes256ctr.go @@ -62,7 +62,7 @@ // Using the factory pattern: // // factory := NewDecrypterFactory(masterKeyVault) -// decrypter, err := factory(types.AlgorithmAES256CTR) +// decrypter, err := factory(types.CipherAES256CTR) // if err != nil { // return err // } @@ -131,7 +131,7 @@ func (e *AES256CTR) GenerateMetadata(ctx context.Context) (*types.EncryptMetadat } return &types.EncryptMetadata{ - Algorithm: types.AlgorithmAES256CTR, + Algorithm: types.CipherAES256CTR, Key: encryptedKey, KeyPlainText: key, IV: iv, @@ -144,7 +144,7 @@ func (e *AES256CTR) LoadMetadata(ctx context.Context, encryptedMetadata *types.E return fmt.Errorf("encryption metadata is nil") } - if encryptedMetadata.Algorithm != types.AlgorithmAES256CTR { + if encryptedMetadata.Algorithm != types.CipherAES256CTR { return fmt.Errorf("unsupported algorithm: %s", encryptedMetadata.Algorithm) } diff --git a/pkg/filemanager/encrypt/encrypt.go b/pkg/filemanager/encrypt/encrypt.go index 2e03d05f..6f3781c6 100644 --- a/pkg/filemanager/encrypt/encrypt.go +++ b/pkg/filemanager/encrypt/encrypt.go @@ -23,13 +23,13 @@ type ( GenerateMetadata(ctx context.Context) (*types.EncryptMetadata, error) } - CryptorFactory func(algorithm types.Algorithm) (Cryptor, error) + CryptorFactory func(algorithm types.Cipher) (Cryptor, error) ) func NewCryptorFactory(masterKeyVault MasterEncryptKeyVault) CryptorFactory { - return func(algorithm types.Algorithm) (Cryptor, error) { + return func(algorithm types.Cipher) (Cryptor, error) { switch algorithm { - case types.AlgorithmAES256CTR: + case types.CipherAES256CTR: return NewAES256CTR(masterKeyVault), nil default: return nil, fmt.Errorf("unknown algorithm: %s", algorithm) diff --git a/pkg/filemanager/encrypt/masterkey.go b/pkg/filemanager/encrypt/masterkey.go index d339143c..22e4f78c 100644 --- a/pkg/filemanager/encrypt/masterkey.go +++ b/pkg/filemanager/encrypt/masterkey.go @@ -2,18 +2,33 @@ package encrypt import ( "context" + "encoding/base64" "errors" + "fmt" + "os" "github.com/cloudreve/Cloudreve/v4/pkg/setting" ) +const ( + EnvMasterEncryptKey = "CR_ENCRYPT_MASTER_KEY" +) + // MasterEncryptKeyVault is a vault for the master encrypt key. type MasterEncryptKeyVault interface { GetMasterKey(ctx context.Context) ([]byte, error) } -func NewMasterEncryptKeyVault(setting setting.Provider) MasterEncryptKeyVault { - return &settingMasterEncryptKeyVault{setting: setting} +func NewMasterEncryptKeyVault(ctx context.Context, settings setting.Provider) MasterEncryptKeyVault { + vaultType := settings.MasterEncryptKeyVault(ctx) + switch vaultType { + case setting.MasterEncryptKeyVaultTypeEnv: + return NewEnvMasterEncryptKeyVault() + case setting.MasterEncryptKeyVaultTypeFile: + return NewFileMasterEncryptKeyVault(settings.MasterEncryptKeyFile(ctx)) + default: + return NewSettingMasterEncryptKeyVault(settings) + } } // settingMasterEncryptKeyVault is a vault for the master encrypt key that gets the key from the setting KV. @@ -21,6 +36,10 @@ type settingMasterEncryptKeyVault struct { setting setting.Provider } +func NewSettingMasterEncryptKeyVault(setting setting.Provider) MasterEncryptKeyVault { + return &settingMasterEncryptKeyVault{setting: setting} +} + func (v *settingMasterEncryptKeyVault) GetMasterKey(ctx context.Context) ([]byte, error) { key := v.setting.MasterEncryptKey(ctx) if key == nil { @@ -28,3 +47,59 @@ func (v *settingMasterEncryptKeyVault) GetMasterKey(ctx context.Context) ([]byte } return key, nil } + +func NewEnvMasterEncryptKeyVault() MasterEncryptKeyVault { + return &envMasterEncryptKeyVault{} +} + +type envMasterEncryptKeyVault struct { +} + +var envMasterKeyCache = []byte{} + +func (v *envMasterEncryptKeyVault) GetMasterKey(ctx context.Context) ([]byte, error) { + if len(envMasterKeyCache) > 0 { + return envMasterKeyCache, nil + } + + key := os.Getenv(EnvMasterEncryptKey) + if key == "" { + return nil, errors.New("master encrypt key is not set") + } + + decodedKey, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return nil, fmt.Errorf("failed to decode master encrypt key: %w", err) + } + + envMasterKeyCache = decodedKey + return decodedKey, nil +} + +func NewFileMasterEncryptKeyVault(path string) MasterEncryptKeyVault { + return &fileMasterEncryptKeyVault{path: path} +} + +var fileMasterKeyCache = []byte{} + +type fileMasterEncryptKeyVault struct { + path string +} + +func (v *fileMasterEncryptKeyVault) GetMasterKey(ctx context.Context) ([]byte, error) { + if len(fileMasterKeyCache) > 0 { + return fileMasterKeyCache, nil + } + + key, err := os.ReadFile(v.path) + if err != nil { + return nil, fmt.Errorf("invalid master encrypt key file") + } + + decodedKey, err := base64.StdEncoding.DecodeString(string(key)) + if err != nil { + return nil, fmt.Errorf("invalid master encrypt key") + } + fileMasterKeyCache = decodedKey + return fileMasterKeyCache, nil +} diff --git a/pkg/filemanager/fs/dbfs/dbfs.go b/pkg/filemanager/fs/dbfs/dbfs.go index 5292efb9..7f346f53 100644 --- a/pkg/filemanager/fs/dbfs/dbfs.go +++ b/pkg/filemanager/fs/dbfs/dbfs.go @@ -652,8 +652,8 @@ func (f *DBFS) createFile(ctx context.Context, parent *File, name string, fileTy func (f *DBFS) generateEncryptMetadata(ctx context.Context, uploadRequest *fs.UploadRequest, policy *ent.StoragePolicy) (*types.EncryptMetadata, error) { relayEnabled := policy.Settings != nil && policy.Settings.Relay - if (len(uploadRequest.Props.EncryptionSupported) > 0 && uploadRequest.Props.EncryptionSupported[0] == types.AlgorithmAES256CTR) || relayEnabled { - encryptor, err := f.encryptorFactory(types.AlgorithmAES256CTR) + if (len(uploadRequest.Props.EncryptionSupported) > 0 && uploadRequest.Props.EncryptionSupported[0] == types.CipherAES256CTR) || relayEnabled { + encryptor, err := f.encryptorFactory(types.CipherAES256CTR) if err != nil { return nil, fmt.Errorf("failed to get encryptor: %w", err) } diff --git a/pkg/filemanager/fs/fs.go b/pkg/filemanager/fs/fs.go index 80ffa2d4..76a6195e 100644 --- a/pkg/filemanager/fs/fs.go +++ b/pkg/filemanager/fs/fs.go @@ -294,7 +294,7 @@ type ( // with a default version entity. This will be set in update request for existing files. EntityType *types.EntityType ExpireAt time.Time - EncryptionSupported []types.Algorithm + EncryptionSupported []types.Cipher ClientSideEncrypted bool // Whether the file stream is already encrypted by client side. } diff --git a/pkg/filemanager/manager/entity.go b/pkg/filemanager/manager/entity.go index 8384d113..e89504dc 100644 --- a/pkg/filemanager/manager/entity.go +++ b/pkg/filemanager/manager/entity.go @@ -120,7 +120,7 @@ func (m *manager) GetDirectLink(ctx context.Context, urls ...*fs.URI) ([]DirectL } source := entitysource.NewEntitySource(target, d, policy, m.auth, m.settings, m.hasher, m.dep.RequestClient(), - m.l, m.config, m.dep.MimeDetector(ctx), m.dep.EncryptorFactory()) + m.l, m.config, m.dep.MimeDetector(ctx), m.dep.EncryptorFactory(ctx)) sourceUrl, err := source.Url(ctx, entitysource.WithSpeedLimit(int64(m.user.Edges.Group.SpeedLimit)), entitysource.WithDisplayName(file.Name()), @@ -182,7 +182,7 @@ func (m *manager) GetUrlForRedirectedDirectLink(ctx context.Context, dl *ent.Dir } source := entitysource.NewEntitySource(primaryEntity, d, policy, m.auth, m.settings, m.hasher, m.dep.RequestClient(), - m.l, m.config, m.dep.MimeDetector(ctx), m.dep.EncryptorFactory()) + m.l, m.config, m.dep.MimeDetector(ctx), m.dep.EncryptorFactory(ctx)) downloadUrl, err := source.Url(ctx, entitysource.WithExpire(o.Expire), entitysource.WithDownload(o.IsDownload), @@ -282,7 +282,7 @@ func (m *manager) GetEntityUrls(ctx context.Context, args []GetEntityUrlArgs, op // Cache miss, Generate new url source := entitysource.NewEntitySource(target, d, policy, m.auth, m.settings, m.hasher, m.dep.RequestClient(), - m.l, m.config, m.dep.MimeDetector(ctx), m.dep.EncryptorFactory()) + m.l, m.config, m.dep.MimeDetector(ctx), m.dep.EncryptorFactory(ctx)) downloadUrl, err := source.Url(ctx, entitysource.WithExpire(o.Expire), entitysource.WithDownload(o.IsDownload), @@ -349,7 +349,7 @@ func (m *manager) GetEntitySource(ctx context.Context, entityID int, opts ...fs. } return entitysource.NewEntitySource(entity, handler, policy, m.auth, m.settings, m.hasher, m.dep.RequestClient(), m.l, - m.config, m.dep.MimeDetector(ctx), m.dep.EncryptorFactory(), entitysource.WithContext(ctx), entitysource.WithThumb(o.IsThumb)), nil + m.config, m.dep.MimeDetector(ctx), m.dep.EncryptorFactory(ctx), entitysource.WithContext(ctx), entitysource.WithThumb(o.IsThumb)), nil } func (l *manager) SetCurrentVersion(ctx context.Context, path *fs.URI, version int) error { diff --git a/pkg/filemanager/manager/manager.go b/pkg/filemanager/manager/manager.go index bce3208b..4ff5b8b2 100644 --- a/pkg/filemanager/manager/manager.go +++ b/pkg/filemanager/manager/manager.go @@ -148,7 +148,7 @@ func NewFileManager(dep dependency.Dep, u *ent.User) FileManager { settings: dep.SettingProvider(), fs: dbfs.NewDatabaseFS(u, dep.FileClient(), dep.ShareClient(), dep.Logger(), dep.LockSystem(), dep.SettingProvider(), dep.StoragePolicyClient(), dep.HashIDEncoder(), dep.UserClient(), dep.KV(), dep.NavigatorStateKV(), - dep.DirectLinkClient(), dep.EncryptorFactory()), + dep.DirectLinkClient(), dep.EncryptorFactory(context.TODO())), kv: dep.KV(), config: config, auth: dep.GeneralAuth(), diff --git a/pkg/filemanager/manager/thumbnail.go b/pkg/filemanager/manager/thumbnail.go index 605d47ee..3d71afcd 100644 --- a/pkg/filemanager/manager/thumbnail.go +++ b/pkg/filemanager/manager/thumbnail.go @@ -64,7 +64,8 @@ func (m *manager) Thumbnail(ctx context.Context, uri *fs.URI) (entitysource.Enti capabilities := handler.Capabilities() // Check if file extension and size is supported by native policy generator. if capabilities.ThumbSupportAllExts || util.IsInExtensionList(capabilities.ThumbSupportedExts, file.DisplayName()) && - (capabilities.ThumbMaxSize == 0 || latest.Size() <= capabilities.ThumbMaxSize) { + (capabilities.ThumbMaxSize == 0 || latest.Size() <= capabilities.ThumbMaxSize) && + !latest.Encrypted() { thumbSource, err := m.GetEntitySource(ctx, 0, fs.WithEntity(latest), fs.WithUseThumb(true)) if err != nil { return nil, fmt.Errorf("failed to get latest entity source: %w", err) diff --git a/pkg/filemanager/manager/upload.go b/pkg/filemanager/manager/upload.go index 136a8ed0..7cd68fbc 100644 --- a/pkg/filemanager/manager/upload.go +++ b/pkg/filemanager/manager/upload.go @@ -192,7 +192,7 @@ func (m *manager) Upload(ctx context.Context, req *fs.UploadRequest, policy *ent } if session != nil && session.EncryptMetadata != nil && !req.Props.ClientSideEncrypted { - cryptor, err := m.dep.EncryptorFactory()(session.EncryptMetadata.Algorithm) + cryptor, err := m.dep.EncryptorFactory(ctx)(session.EncryptMetadata.Algorithm) if err != nil { return fmt.Errorf("failed to create cryptor: %w", err) } @@ -331,7 +331,7 @@ func (m *manager) Update(ctx context.Context, req *fs.UploadRequest, opts ...fs. req.Props.UploadSessionID = uuid.Must(uuid.NewV4()).String() // Sever side supported encryption algorithms - req.Props.EncryptionSupported = []types.Algorithm{types.AlgorithmAES256CTR} + req.Props.EncryptionSupported = []types.Cipher{types.CipherAES256CTR} if m.stateless { return m.updateStateless(ctx, req, o) diff --git a/pkg/filemanager/workflows/archive.go b/pkg/filemanager/workflows/archive.go index 378d0591..53fe21b1 100644 --- a/pkg/filemanager/workflows/archive.go +++ b/pkg/filemanager/workflows/archive.go @@ -218,7 +218,7 @@ func (m *CreateArchiveTask) listEntitiesAndSendToSlave(ctx context.Context, dep user := inventory.UserFromContext(ctx) fm := manager.NewFileManager(dep, user) storagePolicyClient := dep.StoragePolicyClient() - masterKey, _ := dep.MasterEncryptKeyVault().GetMasterKey(ctx) + masterKey, _ := dep.MasterEncryptKeyVault(ctx).GetMasterKey(ctx) failed, err := fm.CreateArchive(ctx, uris, io.Discard, fs.WithDryRun(func(name string, e fs.Entity) { diff --git a/pkg/filemanager/workflows/extract.go b/pkg/filemanager/workflows/extract.go index 0ca5e6b7..9fd2dce4 100644 --- a/pkg/filemanager/workflows/extract.go +++ b/pkg/filemanager/workflows/extract.go @@ -194,7 +194,7 @@ func (m *ExtractArchiveTask) createSlaveExtractTask(ctx context.Context, dep dep return task.StatusError, fmt.Errorf("failed to get policy: %w", err) } - masterKey, _ := dep.MasterEncryptKeyVault().GetMasterKey(ctx) + masterKey, _ := dep.MasterEncryptKeyVault(ctx).GetMasterKey(ctx) entityModel, err := decryptEntityKeyIfNeeded(masterKey, archiveFile.PrimaryEntity().Model()) if err != nil { return task.StatusError, fmt.Errorf("failed to decrypt entity key for archive file %q: %s", archiveFile.DisplayName(), err) diff --git a/pkg/setting/provider.go b/pkg/setting/provider.go index b94d9880..0cf77f00 100644 --- a/pkg/setting/provider.go +++ b/pkg/setting/provider.go @@ -210,6 +210,12 @@ type ( FFMpegExtraArgs(ctx context.Context) string // MasterEncryptKey returns the master encrypt key. MasterEncryptKey(ctx context.Context) []byte + // MasterEncryptKeyVault returns the master encrypt key vault type. + MasterEncryptKeyVault(ctx context.Context) MasterEncryptKeyVaultType + // MasterEncryptKeyFile returns the master encrypt key file. + MasterEncryptKeyFile(ctx context.Context) string + // ShowEncryptionStatus returns true if encryption status is shown. + ShowEncryptionStatus(ctx context.Context) bool } UseFirstSiteUrlCtxKey = struct{} ) @@ -237,6 +243,18 @@ type ( } ) +func (s *settingProvider) ShowEncryptionStatus(ctx context.Context) bool { + return s.getBoolean(ctx, "show_encryption_status", true) +} + +func (s *settingProvider) MasterEncryptKeyFile(ctx context.Context) string { + return s.getString(ctx, "encrypt_master_key_file", "") +} + +func (s *settingProvider) MasterEncryptKeyVault(ctx context.Context) MasterEncryptKeyVaultType { + return MasterEncryptKeyVaultType(s.getString(ctx, "encrypt_master_key_vault", "setting")) +} + func (s *settingProvider) MasterEncryptKey(ctx context.Context) []byte { encoded := s.getString(ctx, "encrypt_master_key", "") key, err := base64.StdEncoding.DecodeString(encoded) diff --git a/pkg/setting/types.go b/pkg/setting/types.go index 849ee589..9a51273e 100644 --- a/pkg/setting/types.go +++ b/pkg/setting/types.go @@ -223,3 +223,11 @@ type CustomHTML struct { HeadlessBody string `json:"headless_bottom,omitempty"` SidebarBottom string `json:"sidebar_bottom,omitempty"` } + +type MasterEncryptKeyVaultType string + +const ( + MasterEncryptKeyVaultTypeSetting = MasterEncryptKeyVaultType("setting") + MasterEncryptKeyVaultTypeEnv = MasterEncryptKeyVaultType("env") + MasterEncryptKeyVaultTypeFile = MasterEncryptKeyVaultType("file") +) diff --git a/service/admin/file.go b/service/admin/file.go index 5ea36059..e7c690f0 100644 --- a/service/admin/file.go +++ b/service/admin/file.go @@ -347,7 +347,7 @@ func (s *SingleFileService) Url(c *gin.Context) (string, error) { } es := entitysource.NewEntitySource(fs.NewEntity(primaryEntity), driver, policy, dep.GeneralAuth(), - dep.SettingProvider(), dep.HashIDEncoder(), dep.RequestClient(), dep.Logger(), dep.ConfigProvider(), dep.MimeDetector(ctx), dep.EncryptorFactory()) + dep.SettingProvider(), dep.HashIDEncoder(), dep.RequestClient(), dep.Logger(), dep.ConfigProvider(), dep.MimeDetector(ctx), dep.EncryptorFactory(ctx)) expire := time.Now().Add(time.Hour * 1) url, err := es.Url(ctx, entitysource.WithExpire(&expire), entitysource.WithDisplayName(file.Name)) @@ -547,7 +547,7 @@ func (s *SingleEntityService) Url(c *gin.Context) (string, error) { } es := entitysource.NewEntitySource(fs.NewEntity(entity), driver, policy, dep.GeneralAuth(), - dep.SettingProvider(), dep.HashIDEncoder(), dep.RequestClient(), dep.Logger(), dep.ConfigProvider(), dep.MimeDetector(c), dep.EncryptorFactory()) + dep.SettingProvider(), dep.HashIDEncoder(), dep.RequestClient(), dep.Logger(), dep.ConfigProvider(), dep.MimeDetector(c), dep.EncryptorFactory(c)) expire := time.Now().Add(time.Hour * 1) url, err := es.Url(c, entitysource.WithDownload(true), entitysource.WithExpire(&expire), entitysource.WithDisplayName(path.Base(entity.Source))) diff --git a/service/admin/site.go b/service/admin/site.go index 7b5a6332..19693cc6 100644 --- a/service/admin/site.go +++ b/service/admin/site.go @@ -193,7 +193,8 @@ type ( func (s *GetSettingService) GetSetting(c *gin.Context) (map[string]string, error) { dep := dependency.FromContext(c) res, err := dep.SettingClient().Gets(c, lo.Filter(s.Keys, func(item string, index int) bool { - return item != "secret_key" + _, ok := inventory.RedactedSettings[strings.ToLower(item)] + return !ok })) if err != nil { return nil, serializer.NewError(serializer.CodeDBError, "Failed to get settings", err) diff --git a/service/basic/site.go b/service/basic/site.go index 5c34a50a..1a61b469 100644 --- a/service/basic/site.go +++ b/service/basic/site.go @@ -43,16 +43,17 @@ type SiteConfig struct { PrivacyPolicyUrl string `json:"privacy_policy_url,omitempty"` // Explorer section - Icons string `json:"icons,omitempty"` - EmojiPreset string `json:"emoji_preset,omitempty"` - MapProvider setting.MapProvider `json:"map_provider,omitempty"` - GoogleMapTileType setting.MapGoogleTileType `json:"google_map_tile_type,omitempty"` - MapboxAK string `json:"mapbox_ak,omitempty"` - FileViewers []types.ViewerGroup `json:"file_viewers,omitempty"` - MaxBatchSize int `json:"max_batch_size,omitempty"` - ThumbnailWidth int `json:"thumbnail_width,omitempty"` - ThumbnailHeight int `json:"thumbnail_height,omitempty"` - CustomProps []types.CustomProps `json:"custom_props,omitempty"` + Icons string `json:"icons,omitempty"` + EmojiPreset string `json:"emoji_preset,omitempty"` + MapProvider setting.MapProvider `json:"map_provider,omitempty"` + GoogleMapTileType setting.MapGoogleTileType `json:"google_map_tile_type,omitempty"` + MapboxAK string `json:"mapbox_ak,omitempty"` + FileViewers []types.ViewerGroup `json:"file_viewers,omitempty"` + MaxBatchSize int `json:"max_batch_size,omitempty"` + ThumbnailWidth int `json:"thumbnail_width,omitempty"` + ThumbnailHeight int `json:"thumbnail_height,omitempty"` + CustomProps []types.CustomProps `json:"custom_props,omitempty"` + ShowEncryptionStatus bool `json:"show_encryption_status,omitempty"` // Thumbnail section ThumbExts []string `json:"thumb_exts,omitempty"` @@ -100,6 +101,7 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) { fileViewers := settings.FileViewers(c) customProps := settings.CustomProps(c) maxBatchSize := settings.MaxBatchedFile(c) + showEncryptionStatus := settings.ShowEncryptionStatus(c) w, h := settings.ThumbSize(c) for i := range fileViewers { for j := range fileViewers[i].Viewers { @@ -107,15 +109,16 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) { } } return &SiteConfig{ - MaxBatchSize: maxBatchSize, - FileViewers: fileViewers, - Icons: explorerSettings.Icons, - MapProvider: mapSettings.Provider, - GoogleMapTileType: mapSettings.GoogleTileType, - MapboxAK: mapSettings.MapboxAK, - ThumbnailWidth: w, - ThumbnailHeight: h, - CustomProps: customProps, + MaxBatchSize: maxBatchSize, + FileViewers: fileViewers, + Icons: explorerSettings.Icons, + MapProvider: mapSettings.Provider, + GoogleMapTileType: mapSettings.GoogleTileType, + MapboxAK: mapSettings.MapboxAK, + ThumbnailWidth: w, + ThumbnailHeight: h, + CustomProps: customProps, + ShowEncryptionStatus: showEncryptionStatus, }, nil case "emojis": emojis := settings.EmojiPresets(c) diff --git a/service/explorer/response.go b/service/explorer/response.go index c4e76450..7049d4c8 100644 --- a/service/explorer/response.go +++ b/service/explorer/response.go @@ -292,6 +292,7 @@ type Entity struct { CreatedAt time.Time `json:"created_at"` StoragePolicy *StoragePolicy `json:"storage_policy,omitempty"` CreatedBy *user.User `json:"created_by,omitempty"` + EncryptedWith types.Cipher `json:"encrypted_with,omitempty"` } type Share struct { @@ -452,6 +453,12 @@ func BuildEntity(extendedInfo *fs.FileExtendedInfo, e fs.Entity, hasher hashid.E userRedacted := user.BuildUserRedacted(e.CreatedBy(), user.RedactLevelAnonymous, hasher) u = &userRedacted } + + encryptedWith := types.Cipher("") + if e.Encrypted() { + encryptedWith = e.Props().EncryptMetadata.Algorithm + } + return Entity{ ID: hashid.EncodeEntityID(hasher, e.ID()), Type: e.Type(), @@ -459,6 +466,7 @@ func BuildEntity(extendedInfo *fs.FileExtendedInfo, e fs.Entity, hasher hashid.E StoragePolicy: BuildStoragePolicy(extendedInfo.EntityStoragePolicies[e.PolicyID()], hasher), Size: e.Size(), CreatedBy: u, + EncryptedWith: encryptedWith, } } diff --git a/service/explorer/upload.go b/service/explorer/upload.go index 73e24646..d0046fa7 100644 --- a/service/explorer/upload.go +++ b/service/explorer/upload.go @@ -29,7 +29,7 @@ type ( PolicyID string `json:"policy_id"` Metadata map[string]string `json:"metadata" binding:"max=256"` EntityType string `json:"entity_type" binding:"eq=|eq=live_photo|eq=version"` - EncryptionSupported []types.Algorithm `json:"encryption_supported"` + EncryptionSupported []types.Cipher `json:"encryption_supported"` } )