Skip to content

Commit b2f1d8a

Browse files
committed
feat: add encryption key management
Signed-off-by: chohee <[email protected]>
1 parent 584a612 commit b2f1d8a

File tree

11 files changed

+204
-3
lines changed

11 files changed

+204
-3
lines changed

cmd/dependency/dependency.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import (
5757
"d7y.io/dragonfly/v2/pkg/types"
5858
"d7y.io/dragonfly/v2/pkg/unit"
5959
"d7y.io/dragonfly/v2/version"
60+
manager_cfg "d7y.io/dragonfly/v2/manager/config"
6061
)
6162

6263
// InitCommandAndConfig initializes flags binding and common sub cmds.
@@ -307,7 +308,8 @@ func initDecoderConfig(dc *mapstructure.DecoderConfig) {
307308
reflect.TypeOf(config.URL{}),
308309
reflect.TypeOf(net.IP{}),
309310
reflect.TypeOf(config.CertPool{}),
310-
reflect.TypeOf(config.Regexp{}):
311+
reflect.TypeOf(config.Regexp{}),
312+
reflect.TypeOf(manager_cfg.EncryptionKey{}):
311313

312314
b, _ := yaml.Marshal(v)
313315
p := reflect.New(to)

cmd/manager/cmd/root.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ func init() {
101101
// initDfpath initializes dfpath.
102102
func initDfpath(cfg *config.ServerConfig) (dfpath.Dfpath, error) {
103103
var options []dfpath.Option
104+
if cfg.WorkHome != "" {
105+
options = append(options, dfpath.WithWorkHome(cfg.WorkHome))
106+
}
107+
104108
if cfg.LogDir != "" {
105109
options = append(options, dfpath.WithLogDir(cfg.LogDir))
106110
}
@@ -113,6 +117,10 @@ func initDfpath(cfg *config.ServerConfig) (dfpath.Dfpath, error) {
113117
options = append(options, dfpath.WithPluginDir(cfg.PluginDir))
114118
}
115119

120+
if cfg.DataDir != "" {
121+
options = append(options, dfpath.WithDataDir(cfg.DataDir))
122+
}
123+
116124
return dfpath.New(options...)
117125
}
118126

cmd/scheduler/cmd/root.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ func init() {
100100

101101
func initDfpath(cfg *config.ServerConfig) (dfpath.Dfpath, error) {
102102
var options []dfpath.Option
103+
if cfg.WorkHome != "" {
104+
options = append(options, dfpath.WithWorkHome(cfg.WorkHome))
105+
}
106+
103107
if cfg.LogDir != "" {
104108
options = append(options, dfpath.WithLogDir(cfg.LogDir))
105109
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,6 @@ require (
280280
gorm.io/driver/sqlserver v1.4.1 // indirect
281281
gorm.io/plugin/dbresolver v1.3.0 // indirect
282282
)
283+
284+
// use local api repo
285+
replace d7y.io/api/v2 => ../api_fork

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,6 @@ cloud.google.com/go/storage v1.50.0 h1:3TbVkzTooBvnZsk7WaAQfOsNrdoM8QHusXA1cpk6Q
6363
cloud.google.com/go/storage v1.50.0/go.mod h1:l7XeiD//vx5lfqE3RavfmU9yvk5Pp0Zhcv482poyafY=
6464
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
6565
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
66-
d7y.io/api/v2 v2.1.60 h1:Imk1mw30wmTJNdqKFF5VO/eqI/t1H5PfIPPK4ryKX2U=
67-
d7y.io/api/v2 v2.1.60/go.mod h1:WzakPywEgs27gr/TwrlRarPRRK2o5nN0YrTzHDdFii8=
6866
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
6967
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
7068
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=

manager/config/config.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
package config
1818

1919
import (
20+
"encoding/base64"
2021
"errors"
2122
"fmt"
2223
"net"
2324
"time"
2425

2526
"d7y.io/dragonfly/v2/cmd/dependency/base"
27+
logger "d7y.io/dragonfly/v2/internal/dflog"
2628
"d7y.io/dragonfly/v2/pkg/net/ip"
2729
"d7y.io/dragonfly/v2/pkg/objectstorage"
2830
"d7y.io/dragonfly/v2/pkg/slices"
@@ -56,12 +58,18 @@ type Config struct {
5658

5759
// Network configuration.
5860
Network NetworkConfig `yaml:"network" mapstructure:"network"`
61+
62+
// Encryption configuration
63+
Encryption EncryptionConfig `yaml:"encryption" mapstructure:"encryption"`
5964
}
6065

6166
type ServerConfig struct {
6267
// Server name.
6368
Name string `yaml:"name" mapstructure:"name"`
6469

70+
// Server work home directory.
71+
WorkHome string `yaml:"workHome" mapstructure:"workHome"`
72+
6573
// Server dynamic config cache directory.
6674
CacheDir string `yaml:"cacheDir" mapstructure:"cacheDir"`
6775

@@ -83,6 +91,9 @@ type ServerConfig struct {
8391
// Server plugin directory.
8492
PluginDir string `yaml:"pluginDir" mapstructure:"pluginDir"`
8593

94+
// Server data directory.
95+
DataDir string `yaml:"dataDir" mapstructure:"dataDir"`
96+
8697
// GRPC server configuration.
8798
GRPC GRPCConfig `yaml:"grpc" mapstructure:"grpc"`
8899

@@ -398,6 +409,34 @@ type NetworkConfig struct {
398409
EnableIPv6 bool `mapstructure:"enableIPv6" yaml:"enableIPv6"`
399410
}
400411

412+
type EncryptionKey [32]byte
413+
type EncryptionConfig struct {
414+
Enable bool `mapstructure:"enable" yaml:"enable"`
415+
// AES256 base64, optional
416+
Key *EncryptionKey `mapstructure:"key" yaml:"key"`
417+
}
418+
419+
// UnmarshalText Base64
420+
func (e *EncryptionKey) UnmarshalText(text []byte) error {
421+
logger.Infof("Base64 str: %s", string(text))
422+
keyBytes, err := base64.StdEncoding.DecodeString(string(text))
423+
if err != nil {
424+
return fmt.Errorf("invalid base64 key: %v", err)
425+
}
426+
427+
if len(keyBytes) != 32 {
428+
return fmt.Errorf("key must be 32 bytes, got %d", len(keyBytes))
429+
}
430+
431+
copy(e[:], keyBytes)
432+
return nil
433+
}
434+
435+
// MarshalText Base64
436+
func (e EncryptionKey) MarshalText() ([]byte, error) {
437+
return []byte(base64.StdEncoding.EncodeToString(e[:])), nil
438+
}
439+
401440
// New config instance.
402441
func New() *Config {
403442
return &Config{
@@ -489,6 +528,9 @@ func New() *Config {
489528
Network: NetworkConfig{
490529
EnableIPv6: DefaultNetworkEnableIPv6,
491530
},
531+
Encryption: EncryptionConfig{
532+
Enable: false,
533+
},
492534
}
493535
}
494536

@@ -686,6 +728,8 @@ func (cfg *Config) Validate() error {
686728
}
687729
}
688730

731+
// TODO: validate key
732+
689733
return nil
690734
}
691735

manager/database/database.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ func migrate(db *gorm.DB) error {
115115
&models.Application{},
116116
&models.PersonalAccessToken{},
117117
&models.Peer{},
118+
&models.EncryptionKey{},
118119
)
119120
}
120121

manager/manager.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ package manager
1818

1919
import (
2020
"context"
21+
"crypto/rand"
2122
"embed"
23+
"encoding/base64"
24+
"encoding/hex"
25+
"errors"
2226
"fmt"
2327
"io/fs"
2428
"net"
@@ -29,6 +33,8 @@ import (
2933
"google.golang.org/grpc"
3034
"gorm.io/gorm"
3135

36+
"bytes"
37+
3238
logger "d7y.io/dragonfly/v2/internal/dflog"
3339
"d7y.io/dragonfly/v2/internal/ratelimiter"
3440
"d7y.io/dragonfly/v2/manager/cache"
@@ -37,6 +43,7 @@ import (
3743
managergc "d7y.io/dragonfly/v2/manager/gc"
3844
"d7y.io/dragonfly/v2/manager/job"
3945
"d7y.io/dragonfly/v2/manager/metrics"
46+
"d7y.io/dragonfly/v2/manager/models"
4047
"d7y.io/dragonfly/v2/manager/permission/rbac"
4148
"d7y.io/dragonfly/v2/manager/router"
4249
"d7y.io/dragonfly/v2/manager/rpcserver"
@@ -48,6 +55,7 @@ import (
4855
"d7y.io/dragonfly/v2/pkg/objectstorage"
4956
"d7y.io/dragonfly/v2/pkg/redis"
5057
"d7y.io/dragonfly/v2/pkg/rpc"
58+
"gorm.io/plugin/soft_delete"
5159
)
5260

5361
const (
@@ -122,6 +130,16 @@ func New(cfg *config.Config, d dfpath.Dfpath) (*Server, error) {
122130
return nil, err
123131
}
124132

133+
// Initialize encryption key
134+
if cfg.Encryption.Enable {
135+
logger.Infof("Encryption enabled")
136+
if err := initializeEncryptionKey(cfg, db.DB); err != nil {
137+
return nil, err
138+
}
139+
} else {
140+
logger.Infof("Encryption disabled")
141+
}
142+
125143
// Initialize enforcer.
126144
enforcer, err := rbac.NewEnforcer(db.DB)
127145
if err != nil {
@@ -250,6 +268,104 @@ func registerGCTasks(gc pkggc.GC, db *gorm.DB) error {
250268
return nil
251269
}
252270

271+
// initializeEncryptionKey
272+
func initializeEncryptionKey(cfg *config.Config, db *gorm.DB) error {
273+
// db.Delete(&models.EncryptionKey{}, "1 = 1")
274+
275+
var existingKey models.EncryptionKey
276+
hasDBKey := false
277+
if err := db.First(&existingKey).Error; err == nil {
278+
hasDBKey = true
279+
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
280+
return fmt.Errorf("failed to check encryption key: %v", err)
281+
}
282+
283+
if cfg.Encryption.Key != nil {
284+
configKey := cfg.Encryption.Key
285+
keyBytes := configKey[:]
286+
if hasDBKey {
287+
// compare key in config with key in db
288+
if bytes.Equal(existingKey.Key, keyBytes) {
289+
logger.Infof(
290+
"encryption key in config file is the same as in database, key(hex): %s, key(base64): %s",
291+
hex.EncodeToString(keyBytes),
292+
base64.StdEncoding.EncodeToString(keyBytes),
293+
)
294+
return nil
295+
}
296+
// key in config is different from key in db, overwrite db
297+
oldKeyHex := hex.EncodeToString(existingKey.Key)
298+
oldKeyBase64 := base64.StdEncoding.EncodeToString(existingKey.Key)
299+
newKeyHex := hex.EncodeToString(keyBytes)
300+
newKeyBase64 := base64.StdEncoding.EncodeToString(keyBytes)
301+
302+
if err := db.Model(&existingKey).Update("key", keyBytes).Error; err != nil {
303+
return fmt.Errorf("failed to update encryption key in database: %v", err)
304+
}
305+
306+
logger.Infof(
307+
"encryption key in database is overwritten by config file, old key(hex): %s, old key(base64): %s, new key(hex): %s, new key(base64): %s",
308+
oldKeyHex, oldKeyBase64, newKeyHex, newKeyBase64,
309+
)
310+
return nil
311+
} else {
312+
// config has key, but db has no key, write it into db
313+
// check soft delete
314+
var oldKey models.EncryptionKey
315+
if err := db.Unscoped().Where("`key` = ?", keyBytes).First(&oldKey).Error; err == nil {
316+
if oldKey.IsDel != soft_delete.DeletedAt(soft_delete.FlagActived) {
317+
// restore the key soft deleted
318+
db.Unscoped().Model(&oldKey).Update("is_del", soft_delete.FlagActived)
319+
logger.Infof("Restore the key which was soft deleted before")
320+
} else {
321+
logger.Fatalf("key should be soft deleted in this situation")
322+
}
323+
} else if errors.Is(err, gorm.ErrRecordNotFound) {
324+
// insert new key
325+
if err := db.Create(&models.EncryptionKey{Key: keyBytes}).Error; err != nil {
326+
return fmt.Errorf("failed to save encryption key to database: %v", err)
327+
}
328+
} else {
329+
// return fmt.Errorf("unknow failed when update encryption key in database: %v", err)
330+
logger.Fatalf("unknow failed when update encryption key in database: %v", err)
331+
// panic(err)
332+
}
333+
334+
logger.Infof(
335+
"encryption key from config file is saved to database, key(hex): %s, key(base64): %s",
336+
hex.EncodeToString(keyBytes),
337+
base64.StdEncoding.EncodeToString(keyBytes),
338+
)
339+
return nil
340+
}
341+
}
342+
343+
// config has no key and db has key
344+
if hasDBKey {
345+
logger.Infof(
346+
"encryption key loaded from database, key(hex): %s, key(base64): %s",
347+
hex.EncodeToString(existingKey.Key),
348+
base64.StdEncoding.EncodeToString(existingKey.Key),
349+
)
350+
return nil
351+
}
352+
353+
// config and db both have no key, generate one
354+
keyBytes := make([]byte, 32)
355+
if _, err := rand.Read(keyBytes); err != nil {
356+
return fmt.Errorf("failed to generate random encryption key: %v", err)
357+
}
358+
if err := db.Create(&models.EncryptionKey{Key: keyBytes}).Error; err != nil {
359+
return fmt.Errorf("failed to save random encryption key to database: %v", err)
360+
}
361+
logger.Infof(
362+
"generated random encryption key and saved to database, key(hex): %s, key(base64): %s",
363+
hex.EncodeToString(keyBytes),
364+
base64.StdEncoding.EncodeToString(keyBytes),
365+
)
366+
return nil
367+
}
368+
253369
// Serve starts the manager server.
254370
func (s *Server) Serve() error {
255371
// Started REST server.

manager/models/encryption.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package models
2+
3+
type EncryptionKey struct {
4+
BaseModel
5+
Key []byte `gorm:"type:binary(32);not null;unique" json:"key"`
6+
}

manager/rpcserver/manager_server_v2.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -976,3 +976,19 @@ func (s *managerServerV2) KeepAlive(stream managerv2.Manager_KeepAliveServer) er
976976
}
977977
}
978978
}
979+
980+
// RequestEncryptionKey implements manager.ManagerServer.
981+
func (s *managerServerV2) RequestEncryptionKey(ctx context.Context, req *managerv2.RequestEncryptionKeyRequest) (*managerv2.RequestEncryptionKeyResponse, error) {
982+
log := logger.WithHostnameAndIP(req.Hostname, req.Ip)
983+
984+
// Get key from db
985+
var encKey models.EncryptionKey
986+
if err := s.db.WithContext(ctx).First(&encKey).Error; err != nil {
987+
log.Errorf("failed to get encryption key: %v", err)
988+
return nil, status.Error(codes.Internal, "failed to get encryption key")
989+
}
990+
991+
return &managerv2.RequestEncryptionKeyResponse{
992+
EncryptionKey: encKey.Key,
993+
}, nil
994+
}

0 commit comments

Comments
 (0)