diff --git a/README.md b/README.md index 155b27e..d0cc84b 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Next Terminal基于 [Apache Guacamole](https://guacamole.apache.org/) 开发, - 批量执行命令 - 在线会话管理(监控、强制断开) - 离线会话管理(查看录屏) -- 双因素认证 感谢 [naiba](https://github.com/naiba) 贡献 +- 双因素认证 - 资产标签 - 资产授权 - 多用户&用户分组 diff --git a/docs/faq.md b/docs/faq.md index fda1bc1..934d932 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -18,7 +18,8 @@ location / {
访问realvnc提示验证失败? -把加密类型修改为 Prefer On +1. 把密码类型修改为VNC +2. 把加密类型修改为 Prefer On
@@ -39,7 +40,7 @@ docker pull dushixiang/next-terminal:latest ```shell docker rm -f ``` -再重新执行一次 [docker方式安装命令](install-naive.md) +再重新执行一次 [docker方式安装命令](install-docker.md) @@ -88,3 +89,60 @@ Mar 5 20:00:16.923 [DEBU] 用户「admin」密码初始化为: next-terminal +
+ TOTP客户端丢了怎么办? +首先需要进入程序所在目录,使用docker安装的程序目录为:/usr/local/next-terminal + +执行命令 + +```shell +./next-terminal --reset-totp admin +``` + +其中 admin 为用户登录账号,成功之后会输出 + +``` shell + + _______ __ ___________ .__ .__ + \ \ ____ ___ ____/ |_ \__ ___/__________ _____ |__| ____ _____ | | + / | \_/ __ \\ \/ /\ __\ | |_/ __ \_ __ \/ \| |/ \\__ \ | | +/ | \ ___/ > < | | | |\ ___/| | \/ Y Y \ | | \/ __ \| |__ +\____|__ /\___ >__/\_ \ |__| |____| \___ >__| |__|_| /__|___| (____ /____/ + \/ \/ \/ \/ \/ \/ \/ v0.4.0 + +当前数据库模式为:mysql +Mar 5 20:00:16.923 [DEBU] 用户「admin」已重置TOTP + +``` + +
+ +
+ 想要修改数据库敏感信息加密的key怎么办? +首先需要进入程序所在目录,使用docker安装的程序目录为:/usr/local/next-terminal + +执行命令 + +```shell +./next-terminal --encryption-key 旧的加密key new-encryption-key 新的的加密key +``` + +成功之后会输出 + +``` shell + + _______ __ ___________ .__ .__ + \ \ ____ ___ ____/ |_ \__ ___/__________ _____ |__| ____ _____ | | + / | \_/ __ \\ \/ /\ __\ | |_/ __ \_ __ \/ \| |/ \\__ \ | | +/ | \ ___/ > < | | | |\ ___/| | \/ Y Y \ | | \/ __ \| |__ +\____|__ /\___ >__/\_ \ |__| |____| \___ >__| |__|_| /__|___| (____ /____/ + \/ \/ \/ \/ \/ \/ \/ v0.4.0 + +当前数据库模式为:mysql +Mar 5 20:00:16.923 [DEBU] encryption key has being changed. + +``` + +最后重新启动程序,并且把加密key修改为新的。 +
+ diff --git a/docs/install-docker.md b/docs/install-docker.md index 89ad9a9..77eb4c5 100644 --- a/docs/install-docker.md +++ b/docs/install-docker.md @@ -104,6 +104,7 @@ docker run -d \ | MYSQL_PASSWORD | `mysql`数据库密码 | | MYSQL_DATABASE | `mysql`数据库名称 | | SERVER_ADDR | 服务器监听地址,默认`0.0.0.0:8088` | +| ENCRYPTION_KEY | 授权凭证和资产的密码,密钥等敏感信息加密的key,默认`next-terminal` | ## 其他 diff --git a/docs/install-naive.md b/docs/install-naive.md index 6baa096..43605ad 100644 --- a/docs/install-naive.md +++ b/docs/install-naive.md @@ -119,10 +119,7 @@ mkfontdir fc-cache ``` ### 安装 Next Terminal -建立next-terminal目录 -```shell -mkdir ~/next-terminal && cd ~/next-terminal -``` +> 示例步骤安装在 `/usr/local/next-terminal`,你可以自由选择安装目录。 下载 ```shell @@ -131,11 +128,10 @@ wget https://github.com/dushixiang/next-terminal/releases/latest/download/next-t 解压 ```shell -tar -xvf next-terminal.tgz -cd next-terminal +tar -zxvf next-terminal.tgz -C /usr/local/ ``` -在当前目录下创建或修改配置文件`config.yml` +在`/usr/local/next-terminal`或`/etc/next-terminal`下创建或修改配置文件`config.yml` ```shell db: sqlite # 当db为sqlite时mysql的配置无效 @@ -151,12 +147,41 @@ sqlite: file: 'next-terminal.db' server: addr: 0.0.0.0:8088 -# 当设置下面两个参数时会自动开启https模式 +# 当设置下面两个参数时会自动开启https模式(前提是证书文件存在) # cert: /root/next-terminal/cert.pem # key: /root/next-terminal/key.pem + +# 授权凭证和资产的密码,密钥等敏感信息加密的key,默认`next-terminal` +#encryption-key: next-terminal ``` 启动 ```shell ./next-terminal ``` + +使用系统服务方式启动 + +在 `/etc/systemd/system/` 目录创建 `next-terminal.service` 文件并写入以下内容 +```shell +[Unit] +Description=next-terminal service +After=network.target + +[Service] +User=root +WorkingDirectory=/usr/local/next-terminal +ExecStart=/usr/local/next-terminal/next-terminal +Restart=on-failure + +[Install] +WantedBy=multi-user.target +``` + +重载系统服务&&设置开机启动&&启动服务&&查看状态 +```shell +systemctl daemon-reload +systemctl enable next-terminal +systemctl start next-terminal +systemctl status next-terminal +``` \ No newline at end of file diff --git a/main.go b/main.go index 597d7d6..d5e953b 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "crypto/md5" "fmt" "next-terminal/pkg/config" @@ -12,7 +13,7 @@ import ( "github.com/labstack/gommon/log" ) -const Version = "v0.3.4" +const Version = "v0.4.0" func main() { err := Run() @@ -34,13 +35,27 @@ func Run() error { // 为了兼容之前调用global包的代码 后期预期会改为调用pgk/config global.Config = config.GlobalCfg + if global.Config.EncryptionKey == "" { + global.Config.EncryptionKey = "next-terminal" + } + md5Sum := fmt.Sprintf("%x", md5.Sum([]byte(global.Config.EncryptionKey))) + global.Config.EncryptionPassword = []byte(md5Sum) + global.Cache = api.SetupCache() db := api.SetupDB() e := api.SetupRoutes(db) if global.Config.ResetPassword != "" { - return api.ResetPassword() + return api.ResetPassword(global.Config.ResetPassword) } + if global.Config.ResetTotp != "" { + return api.ResetTotp(global.Config.ResetTotp) + } + + if global.Config.NewEncryptionKey != "" { + return api.ChangeEncryptionKey(global.Config.EncryptionKey, global.Config.NewEncryptionKey) + } + sessionRepo := repository.NewSessionRepository(db) propertyRepo := repository.NewPropertyRepository(db) ticker := task.NewTicker(sessionRepo, propertyRepo) diff --git a/pkg/config/config.go b/pkg/config/config.go index 5fe0868..f7b31a5 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -11,13 +11,17 @@ import ( var GlobalCfg *Config type Config struct { - Debug bool - Demo bool - DB string - Server *Server - Mysql *Mysql - Sqlite *Sqlite - ResetPassword string + Debug bool + Demo bool + DB string + Server *Server + Mysql *Mysql + Sqlite *Sqlite + ResetPassword string + ResetTotp string + EncryptionKey string + EncryptionPassword []byte + NewEncryptionKey string } type Mysql struct { @@ -82,9 +86,12 @@ func SetupConfig() *Config { Cert: viper.GetString("server.cert"), Key: viper.GetString("server.key"), }, - ResetPassword: viper.GetString("reset-password"), - Debug: viper.GetBool("debug"), - Demo: viper.GetBool("demo"), + ResetPassword: viper.GetString("reset-password"), + ResetTotp: viper.GetString("reset-totp"), + Debug: viper.GetBool("debug"), + Demo: viper.GetBool("demo"), + EncryptionKey: viper.GetString("encryption-key"), + NewEncryptionKey: viper.GetString("new-encryption-key"), } GlobalCfg = config return config diff --git a/pkg/service/asset.go b/pkg/service/asset.go new file mode 100644 index 0000000..d431436 --- /dev/null +++ b/pkg/service/asset.go @@ -0,0 +1,34 @@ +package service + +import ( + "next-terminal/pkg/global" + "next-terminal/server/repository" +) + +type AssetService struct { + assetRepository *repository.AssetRepository +} + +func NewAssetService(assetRepository *repository.AssetRepository) *AssetService { + return &AssetService{assetRepository: assetRepository} +} + +func (r AssetService) Encrypt() error { + items, err := r.assetRepository.FindAll() + if err != nil { + return err + } + for i := range items { + item := items[i] + if item.Encrypted { + continue + } + if err := r.assetRepository.Encrypt(&item, global.Config.EncryptionPassword); err != nil { + return err + } + if err := r.assetRepository.UpdateById(&item, item.ID); err != nil { + return err + } + } + return nil +} diff --git a/pkg/service/credential.go b/pkg/service/credential.go new file mode 100644 index 0000000..b860f95 --- /dev/null +++ b/pkg/service/credential.go @@ -0,0 +1,34 @@ +package service + +import ( + "next-terminal/pkg/global" + "next-terminal/server/repository" +) + +type CredentialService struct { + credentialRepository *repository.CredentialRepository +} + +func NewCredentialService(credentialRepository *repository.CredentialRepository) *CredentialService { + return &CredentialService{credentialRepository: credentialRepository} +} + +func (r CredentialService) Encrypt() error { + items, err := r.credentialRepository.FindAll() + if err != nil { + return err + } + for i := range items { + item := items[i] + if item.Encrypted { + continue + } + if err := r.credentialRepository.Encrypt(&item, global.Config.EncryptionPassword); err != nil { + return err + } + if err := r.credentialRepository.UpdateById(&item, item.ID); err != nil { + return err + } + } + return nil +} diff --git a/pkg/service/job.go b/pkg/service/job.go index d923ebc..9c592f6 100644 --- a/pkg/service/job.go +++ b/pkg/service/job.go @@ -160,7 +160,7 @@ func (r ShellJob) Run() { msgChan := make(chan string) for i := range assets { - asset, err := r.jobService.assetRepository.FindById(assets[i].ID) + asset, err := r.jobService.assetRepository.FindByIdAndDecrypt(assets[i].ID) if err != nil { msgChan <- fmt.Sprintf("资产「%v」Shell执行失败,查询数据异常「%v」", assets[i].Name, err.Error()) return @@ -176,7 +176,7 @@ func (r ShellJob) Run() { ) if asset.AccountType == "credential" { - credential, err := r.jobService.credentialRepository.FindById(asset.CredentialId) + credential, err := r.jobService.credentialRepository.FindByIdAndDecrypt(asset.CredentialId) if err != nil { msgChan <- fmt.Sprintf("资产「%v」Shell执行失败,查询授权凭证数据异常「%v」", assets[i].Name, err.Error()) return diff --git a/pkg/service/session.go b/pkg/service/session.go index ae6b267..6a8be9f 100644 --- a/pkg/service/session.go +++ b/pkg/service/session.go @@ -33,3 +33,7 @@ func (r SessionService) FixSessionState() error { } return nil } + +func (r SessionService) EmptyPassword() error { + return r.sessionRepository.EmptyPassword() +} diff --git a/pkg/service/user.go b/pkg/service/user.go index b57f67c..9f0bcd5 100644 --- a/pkg/service/user.go +++ b/pkg/service/user.go @@ -58,7 +58,7 @@ func (r UserService) InitUser() (err error) { return nil } -func (r UserService) FixedUserOnlineState() error { +func (r UserService) FixUserOnlineState() error { // 修正用户登录状态 onlineUsers, err := r.userRepository.FindOnlineUsers() if err != nil { diff --git a/server/api/asset.go b/server/api/asset.go index 2e4e470..3a22fed 100644 --- a/server/api/asset.go +++ b/server/api/asset.go @@ -9,6 +9,7 @@ import ( "strings" "next-terminal/pkg/constant" + "next-terminal/pkg/global" "next-terminal/server/model" "next-terminal/server/utils" @@ -199,6 +200,9 @@ func AssetUpdateEndpoint(c echo.Context) error { item.Description = "-" } + if err := assetRepository.Encrypt(&item, global.Config.EncryptionPassword); err != nil { + return err + } if err := assetRepository.UpdateById(&item, id); err != nil { return err } @@ -264,7 +268,7 @@ func AssetGetEndpoint(c echo.Context) (err error) { } var item model.Asset - if item, err = assetRepository.FindById(id); err != nil { + if item, err = assetRepository.FindByIdAndDecrypt(id); err != nil { return err } attributeMap, err := assetRepository.FindAssetAttrMapByAssetId(id) @@ -289,9 +293,12 @@ func AssetTcpingEndpoint(c echo.Context) (err error) { active := utils.Tcping(item.IP, item.Port) - if err := assetRepository.UpdateActiveById(active, item.ID); err != nil { - return err + if item.Active != active { + if err := assetRepository.UpdateActiveById(active, item.ID); err != nil { + return err + } } + return Success(c, active) } diff --git a/server/api/credential.go b/server/api/credential.go index 25caf1a..49a4643 100644 --- a/server/api/credential.go +++ b/server/api/credential.go @@ -1,11 +1,13 @@ package api import ( + "encoding/base64" "errors" "strconv" "strings" "next-terminal/pkg/constant" + "next-terminal/pkg/global" "next-terminal/server/model" "next-terminal/server/utils" @@ -32,27 +34,28 @@ func CredentialCreateEndpoint(c echo.Context) error { case constant.Custom: item.PrivateKey = "-" item.Passphrase = "-" - if len(item.Username) == 0 { + if item.Username == "" { item.Username = "-" } - if len(item.Password) == 0 { + if item.Password == "" { item.Password = "-" } case constant.PrivateKey: item.Password = "-" - if len(item.Username) == 0 { + if item.Username == "" { item.Username = "-" } - if len(item.PrivateKey) == 0 { + if item.PrivateKey == "" { item.PrivateKey = "-" } - if len(item.Passphrase) == 0 { + if item.Passphrase == "" { item.Passphrase = "-" } default: return Fail(c, -1, "类型错误") } + item.Encrypted = true if err := credentialRepository.Create(&item); err != nil { return err } @@ -96,26 +99,48 @@ func CredentialUpdateEndpoint(c echo.Context) error { case constant.Custom: item.PrivateKey = "-" item.Passphrase = "-" - if len(item.Username) == 0 { + if item.Username == "" { item.Username = "-" } - if len(item.Password) == 0 { + if item.Password == "" { item.Password = "-" } + if item.Password != "-" { + encryptedCBC, err := utils.AesEncryptCBC([]byte(item.Password), global.Config.EncryptionPassword) + if err != nil { + return err + } + item.Password = base64.StdEncoding.EncodeToString(encryptedCBC) + } case constant.PrivateKey: item.Password = "-" - if len(item.Username) == 0 { + if item.Username == "" { item.Username = "-" } - if len(item.PrivateKey) == 0 { + if item.PrivateKey == "" { item.PrivateKey = "-" } - if len(item.Passphrase) == 0 { + if item.PrivateKey != "-" { + encryptedCBC, err := utils.AesEncryptCBC([]byte(item.PrivateKey), global.Config.EncryptionPassword) + if err != nil { + return err + } + item.PrivateKey = base64.StdEncoding.EncodeToString(encryptedCBC) + } + if item.Passphrase == "" { item.Passphrase = "-" } + if item.Passphrase != "-" { + encryptedCBC, err := utils.AesEncryptCBC([]byte(item.Passphrase), global.Config.EncryptionPassword) + if err != nil { + return err + } + item.Passphrase = base64.StdEncoding.EncodeToString(encryptedCBC) + } default: return Fail(c, -1, "类型错误") } + item.Encrypted = true if err := credentialRepository.UpdateById(&item, id); err != nil { return err @@ -149,7 +174,7 @@ func CredentialGetEndpoint(c echo.Context) error { return err } - item, err := credentialRepository.FindById(id) + item, err := credentialRepository.FindByIdAndDecrypt(id) if err != nil { return err } diff --git a/server/api/routes.go b/server/api/routes.go index 0a67e1b..efe77e7 100644 --- a/server/api/routes.go +++ b/server/api/routes.go @@ -1,8 +1,10 @@ package api import ( + "crypto/md5" "fmt" "net/http" + "os" "strings" "time" @@ -39,12 +41,14 @@ var ( jobLogRepository *repository.JobLogRepository loginLogRepository *repository.LoginLogRepository - jobService *service.JobService - propertyService *service.PropertyService - userService *service.UserService - sessionService *service.SessionService - mailService *service.MailService - numService *service.NumService + jobService *service.JobService + propertyService *service.PropertyService + userService *service.UserService + sessionService *service.SessionService + mailService *service.MailService + numService *service.NumService + assetService *service.AssetService + credentialService *service.CredentialService ) func SetupRoutes(db *gorm.DB) *echo.Echo { @@ -54,6 +58,7 @@ func SetupRoutes(db *gorm.DB) *echo.Echo { if err := InitDBData(); err != nil { log.WithError(err).Error("初始化数据异常") + os.Exit(0) } if err := ReloadData(); err != nil { @@ -251,6 +256,8 @@ func InitService() { sessionService = service.NewSessionService(sessionRepository) mailService = service.NewMailService(propertyRepository) numService = service.NewNumService(numRepository) + assetService = service.NewAssetService(assetRepository) + credentialService = service.NewCredentialService(credentialRepository) } func InitDBData() (err error) { @@ -266,17 +273,26 @@ func InitDBData() (err error) { if err := jobService.InitJob(); err != nil { return err } - if err := userService.FixedUserOnlineState(); err != nil { + if err := userService.FixUserOnlineState(); err != nil { return err } if err := sessionService.FixSessionState(); err != nil { return err } + if err := sessionService.EmptyPassword(); err != nil { + return err + } + if err := credentialService.Encrypt(); err != nil { + return err + } + if err := assetService.Encrypt(); err != nil { + return err + } return nil } -func ResetPassword() error { - user, err := userRepository.FindByUsername(global.Config.ResetPassword) +func ResetPassword(username string) error { + user, err := userRepository.FindByUsername(username) if err != nil { return err } @@ -296,6 +312,63 @@ func ResetPassword() error { return nil } +func ResetTotp(username string) error { + user, err := userRepository.FindByUsername(username) + if err != nil { + return err + } + u := &model.User{ + TOTPSecret: "-", + ID: user.ID, + } + if err := userRepository.Update(u); err != nil { + return err + } + log.Debugf("用户「%v」已重置TOTP", user.Username) + return nil +} + +func ChangeEncryptionKey(oldEncryptionKey, newEncryptionKey string) error { + + oldPassword := []byte(fmt.Sprintf("%x", md5.Sum([]byte(oldEncryptionKey)))) + newPassword := []byte(fmt.Sprintf("%x", md5.Sum([]byte(newEncryptionKey)))) + + credentials, err := credentialRepository.FindAll() + if err != nil { + return err + } + for i := range credentials { + credential := credentials[i] + if err := credentialRepository.Decrypt(&credential, oldPassword); err != nil { + return err + } + if err := credentialRepository.Encrypt(&credential, newPassword); err != nil { + return err + } + if err := credentialRepository.UpdateById(&credential, credential.ID); err != nil { + return err + } + } + assets, err := assetRepository.FindAll() + if err != nil { + return err + } + for i := range assets { + asset := assets[i] + if err := assetRepository.Decrypt(&asset, oldPassword); err != nil { + return err + } + if err := assetRepository.Encrypt(&asset, newPassword); err != nil { + return err + } + if err := assetRepository.UpdateById(&asset, asset.ID); err != nil { + return err + } + } + log.Infof("encryption key has being changed.") + return nil +} + func SetupCache() *cache.Cache { // 配置缓存器 mCache := cache.New(5*time.Minute, 10*time.Minute) diff --git a/server/api/session.go b/server/api/session.go index 9a91d83..00e3a9f 100644 --- a/server/api/session.go +++ b/server/api/session.go @@ -137,6 +137,9 @@ func CloseSessionById(sessionId string, code int, reason string) { session.DisconnectedTime = utils.NowJsonTime() session.Code = code session.Message = reason + session.Password = "-" + session.PrivateKey = "-" + session.Passphrase = "-" _ = sessionRepository.UpdateById(&session, sessionId) } @@ -359,7 +362,7 @@ type File struct { func SessionLsEndpoint(c echo.Context) error { sessionId := c.Param("id") - session, err := sessionRepository.FindById(sessionId) + session, err := sessionRepository.FindByIdAndDecrypt(sessionId) if err != nil { return err } diff --git a/server/api/ssh.go b/server/api/ssh.go index a76be4b..1827369 100644 --- a/server/api/ssh.go +++ b/server/api/ssh.go @@ -54,7 +54,7 @@ func SSHEndpoint(c echo.Context) (err error) { cols, _ := strconv.Atoi(c.QueryParam("cols")) rows, _ := strconv.Atoi(c.QueryParam("rows")) - session, err := sessionRepository.FindById(sessionId) + session, err := sessionRepository.FindByIdAndDecrypt(sessionId) if err != nil { msg := Message{ Type: Closed, diff --git a/server/api/tunnel.go b/server/api/tunnel.go index a1330cc..e8a3bc7 100644 --- a/server/api/tunnel.go +++ b/server/api/tunnel.go @@ -65,7 +65,7 @@ func TunEndpoint(c echo.Context) error { configuration.SetParameter("width", width) configuration.SetParameter("height", height) configuration.SetParameter("dpi", dpi) - session, err = sessionRepository.FindById(sessionId) + session, err = sessionRepository.FindByIdAndDecrypt(sessionId) if err != nil { CloseSessionById(sessionId, NotFoundSession, "会话不存在") return err diff --git a/server/model/asset.go b/server/model/asset.go index 0a69c2f..ed6f344 100644 --- a/server/model/asset.go +++ b/server/model/asset.go @@ -21,6 +21,7 @@ type Asset struct { Created utils.JsonTime `json:"created"` Tags string `json:"tags"` Owner string `gorm:"index" json:"owner"` + Encrypted bool `json:"encrypted"` } type AssetForPage struct { diff --git a/server/model/credential.go b/server/model/credential.go index a2fb006..cbf7f32 100644 --- a/server/model/credential.go +++ b/server/model/credential.go @@ -14,6 +14,7 @@ type Credential struct { Passphrase string `json:"passphrase"` Created utils.JsonTime `json:"created"` Owner string `gorm:"index" json:"owner"` + Encrypted bool `json:"encrypted"` } func (r *Credential) TableName() string { diff --git a/server/repository/asset.go b/server/repository/asset.go index 8c9b684..dfa73e1 100644 --- a/server/repository/asset.go +++ b/server/repository/asset.go @@ -1,6 +1,7 @@ package repository import ( + "encoding/base64" "fmt" "strings" @@ -145,7 +146,36 @@ func (r AssetRepository) Find(pageIndex, pageSize int, name, protocol, tags stri return } +func (r AssetRepository) Encrypt(item *model.Asset, password []byte) error { + if item.Password != "" && item.Password != "-" { + encryptedCBC, err := utils.AesEncryptCBC([]byte(item.Password), password) + if err != nil { + return err + } + item.Password = base64.StdEncoding.EncodeToString(encryptedCBC) + } + if item.PrivateKey != "" && item.PrivateKey != "-" { + encryptedCBC, err := utils.AesEncryptCBC([]byte(item.PrivateKey), password) + if err != nil { + return err + } + item.PrivateKey = base64.StdEncoding.EncodeToString(encryptedCBC) + } + if item.Passphrase != "" && item.Passphrase != "-" { + encryptedCBC, err := utils.AesEncryptCBC([]byte(item.Passphrase), password) + if err != nil { + return err + } + item.Passphrase = base64.StdEncoding.EncodeToString(encryptedCBC) + } + item.Encrypted = true + return nil +} + func (r AssetRepository) Create(o *model.Asset) (err error) { + if err := r.Encrypt(o, global.Config.EncryptionPassword); err != nil { + return err + } if err = r.DB.Create(o).Error; err != nil { return err } @@ -157,8 +187,54 @@ func (r AssetRepository) FindById(id string) (o model.Asset, err error) { return } +func (r AssetRepository) Decrypt(item *model.Asset, password []byte) error { + if item.Encrypted { + if item.Password != "" && item.Password != "-" { + origData, err := base64.StdEncoding.DecodeString(item.Password) + if err != nil { + return err + } + decryptedCBC, err := utils.AesDecryptCBC(origData, password) + if err != nil { + return err + } + item.Password = string(decryptedCBC) + } + if item.PrivateKey != "" && item.PrivateKey != "-" { + origData, err := base64.StdEncoding.DecodeString(item.PrivateKey) + if err != nil { + return err + } + decryptedCBC, err := utils.AesDecryptCBC(origData, password) + if err != nil { + return err + } + item.PrivateKey = string(decryptedCBC) + } + if item.Passphrase != "" && item.Passphrase != "-" { + origData, err := base64.StdEncoding.DecodeString(item.Passphrase) + if err != nil { + return err + } + decryptedCBC, err := utils.AesDecryptCBC(origData, password) + if err != nil { + return err + } + item.Passphrase = string(decryptedCBC) + } + } + return nil +} + +func (r AssetRepository) FindByIdAndDecrypt(id string) (o model.Asset, err error) { + err = r.DB.Where("id = ?", id).First(&o).Error + if err == nil { + err = r.Decrypt(&o, global.Config.EncryptionPassword) + } + return +} + func (r AssetRepository) UpdateById(o *model.Asset, id string) error { - o.ID = id return r.DB.Updates(o).Error } diff --git a/server/repository/credential.go b/server/repository/credential.go index 0a29b2d..f0f9d27 100644 --- a/server/repository/credential.go +++ b/server/repository/credential.go @@ -1,8 +1,12 @@ package repository import ( + "encoding/base64" + "next-terminal/pkg/constant" + "next-terminal/pkg/global" "next-terminal/server/model" + "next-terminal/server/utils" "gorm.io/gorm" ) @@ -65,6 +69,9 @@ func (r CredentialRepository) Find(pageIndex, pageSize int, name, order, field s } func (r CredentialRepository) Create(o *model.Credential) (err error) { + if err := r.Encrypt(o, global.Config.EncryptionPassword); err != nil { + return err + } if err = r.DB.Create(o).Error; err != nil { return err } @@ -76,6 +83,79 @@ func (r CredentialRepository) FindById(id string) (o model.Credential, err error return } +func (r CredentialRepository) Encrypt(item *model.Credential, password []byte) error { + if item.Password != "-" { + encryptedCBC, err := utils.AesEncryptCBC([]byte(item.Password), password) + if err != nil { + return err + } + item.Password = base64.StdEncoding.EncodeToString(encryptedCBC) + } + if item.PrivateKey != "-" { + encryptedCBC, err := utils.AesEncryptCBC([]byte(item.PrivateKey), password) + if err != nil { + return err + } + item.PrivateKey = base64.StdEncoding.EncodeToString(encryptedCBC) + } + if item.Passphrase != "-" { + encryptedCBC, err := utils.AesEncryptCBC([]byte(item.Passphrase), password) + if err != nil { + return err + } + item.Passphrase = base64.StdEncoding.EncodeToString(encryptedCBC) + } + item.Encrypted = true + return nil +} + +func (r CredentialRepository) Decrypt(item *model.Credential, password []byte) error { + if item.Encrypted { + if item.Password != "" && item.Password != "-" { + origData, err := base64.StdEncoding.DecodeString(item.Password) + if err != nil { + return err + } + decryptedCBC, err := utils.AesDecryptCBC(origData, password) + if err != nil { + return err + } + item.Password = string(decryptedCBC) + } + if item.PrivateKey != "" && item.PrivateKey != "-" { + origData, err := base64.StdEncoding.DecodeString(item.PrivateKey) + if err != nil { + return err + } + decryptedCBC, err := utils.AesDecryptCBC(origData, password) + if err != nil { + return err + } + item.PrivateKey = string(decryptedCBC) + } + if item.Passphrase != "" && item.Passphrase != "-" { + origData, err := base64.StdEncoding.DecodeString(item.Passphrase) + if err != nil { + return err + } + decryptedCBC, err := utils.AesDecryptCBC(origData, password) + if err != nil { + return err + } + item.Passphrase = string(decryptedCBC) + } + } + return nil +} + +func (r CredentialRepository) FindByIdAndDecrypt(id string) (o model.Credential, err error) { + err = r.DB.Where("id = ?", id).First(&o).Error + if err == nil { + err = r.Decrypt(&o, global.Config.EncryptionPassword) + } + return +} + func (r CredentialRepository) UpdateById(o *model.Credential, id string) error { o.ID = id return r.DB.Updates(o).Error @@ -107,3 +187,8 @@ func (r CredentialRepository) CountByUserId(userId string) (total int64, err err err = db.Find(&model.Credential{}).Count(&total).Error return } + +func (r CredentialRepository) FindAll() (o []model.Credential, err error) { + err = r.DB.Find(&o).Error + return +} diff --git a/server/repository/session.go b/server/repository/session.go index 66f082f..284dfac 100644 --- a/server/repository/session.go +++ b/server/repository/session.go @@ -1,12 +1,15 @@ package repository import ( + "encoding/base64" "os" "path" "time" "next-terminal/pkg/constant" + "next-terminal/pkg/global" "next-terminal/server/model" + "next-terminal/server/utils" "gorm.io/gorm" ) @@ -93,6 +96,51 @@ func (r SessionRepository) FindById(id string) (o model.Session, err error) { return } +func (r SessionRepository) FindByIdAndDecrypt(id string) (o model.Session, err error) { + err = r.DB.Where("id = ?", id).First(&o).Error + if err == nil { + err = r.Decrypt(&o) + } + return +} + +func (r SessionRepository) Decrypt(item *model.Session) error { + if item.Password != "" && item.Password != "-" { + origData, err := base64.StdEncoding.DecodeString(item.Password) + if err != nil { + return err + } + decryptedCBC, err := utils.AesDecryptCBC(origData, global.Config.EncryptionPassword) + if err != nil { + return err + } + item.Password = string(decryptedCBC) + } + if item.PrivateKey != "" && item.PrivateKey != "-" { + origData, err := base64.StdEncoding.DecodeString(item.PrivateKey) + if err != nil { + return err + } + decryptedCBC, err := utils.AesDecryptCBC(origData, global.Config.EncryptionPassword) + if err != nil { + return err + } + item.PrivateKey = string(decryptedCBC) + } + if item.Passphrase != "" && item.Passphrase != "-" { + origData, err := base64.StdEncoding.DecodeString(item.Passphrase) + if err != nil { + return err + } + decryptedCBC, err := utils.AesDecryptCBC(origData, global.Config.EncryptionPassword) + if err != nil { + return err + } + item.Passphrase = string(decryptedCBC) + } + return nil +} + func (r SessionRepository) FindByConnectionId(connectionId string) (o model.Session, err error) { err = r.DB.Where("connection_id = ?", connectionId).First(&o).Error return @@ -167,3 +215,8 @@ func (r SessionRepository) CountSessionByDay(day int) (results []D, err error) { return } + +func (r SessionRepository) EmptyPassword() error { + sql := "update sessions set password = '-',private_key = '-', passphrase = '-' where 1=1" + return r.DB.Exec(sql).Error +} diff --git a/server/utils/util_test.go b/server/utils/util_test.go index 2f9d0f3..b8d4fba 100644 --- a/server/utils/util_test.go +++ b/server/utils/util_test.go @@ -1,6 +1,10 @@ package utils_test import ( + "crypto/md5" + "encoding/base64" + "encoding/hex" + "fmt" "net" "testing" @@ -33,3 +37,43 @@ func TestTcping(t *testing.T) { _ = conn.Close() }() } + +func TestAesEncryptCBC(t *testing.T) { + origData := []byte("Hello Next Terminal") // 待加密的数据 + key := []byte("qwertyuiopasdfgh") // 加密的密钥 + encryptedCBC, err := utils.AesEncryptCBC(origData, key) + assert.NoError(t, err) + assert.Equal(t, "s2xvMRPfZjmttpt+x0MzG9dsWcf1X+h9nt7waLvXpNM=", base64.StdEncoding.EncodeToString(encryptedCBC)) +} + +func TestAesDecryptCBC(t *testing.T) { + origData, err := base64.StdEncoding.DecodeString("s2xvMRPfZjmttpt+x0MzG9dsWcf1X+h9nt7waLvXpNM=") // 待解密的数据 + assert.NoError(t, err) + key := []byte("qwertyuiopasdfgh") // 解密的密钥 + decryptCBC, err := utils.AesDecryptCBC(origData, key) + assert.NoError(t, err) + assert.Equal(t, "Hello Next Terminal", string(decryptCBC)) +} + +func TestPbkdf2(t *testing.T) { + pbkdf2, err := utils.Pbkdf2("1234") + assert.NoError(t, err) + println(hex.EncodeToString(pbkdf2)) +} + +func TestAesEncryptCBCWithAnyKey(t *testing.T) { + origData := []byte("admin") // 待加密的数据 + key := []byte(fmt.Sprintf("%x", md5.Sum([]byte("next-terminal")))) // 加密的密钥 + encryptedCBC, err := utils.AesEncryptCBC(origData, key) + assert.NoError(t, err) + assert.Equal(t, "3qwawlPxghyiLS5hdr/p0g==", base64.StdEncoding.EncodeToString(encryptedCBC)) +} + +func TestAesDecryptCBCWithAnyKey(t *testing.T) { + origData, err := base64.StdEncoding.DecodeString("3qwawlPxghyiLS5hdr/p0g==") // 待解密的数据 + assert.NoError(t, err) + key := []byte(fmt.Sprintf("%x", md5.Sum([]byte("next-terminal")))) // 加密的密钥 + decryptCBC, err := utils.AesDecryptCBC(origData, key) + assert.NoError(t, err) + assert.Equal(t, "admin", string(decryptCBC)) +} diff --git a/server/utils/utils.go b/server/utils/utils.go index d586471..0d5333c 100644 --- a/server/utils/utils.go +++ b/server/utils/utils.go @@ -2,7 +2,11 @@ package utils import ( "bytes" + "crypto/aes" + "crypto/cipher" "crypto/md5" + "crypto/rand" + "crypto/sha256" "database/sql/driver" "encoding/base64" "fmt" @@ -17,6 +21,8 @@ import ( "strings" "time" + "golang.org/x/crypto/pbkdf2" + "github.com/gofrs/uuid" "github.com/sirupsen/logrus" "golang.org/x/crypto/bcrypt" @@ -224,3 +230,56 @@ func Check(f func() error) { logrus.Error("Received error:", err) } } + +func PKCS5Padding(ciphertext []byte, blockSize int) []byte { + padding := blockSize - len(ciphertext)%blockSize + padText := bytes.Repeat([]byte{byte(padding)}, padding) + return append(ciphertext, padText...) +} + +func PKCS5UnPadding(origData []byte) []byte { + length := len(origData) + unPadding := int(origData[length-1]) + return origData[:(length - unPadding)] +} + +// AesEncryptCBC /* +func AesEncryptCBC(origData, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + blockSize := block.BlockSize() + origData = PKCS5Padding(origData, blockSize) + blockMode := cipher.NewCBCEncrypter(block, key[:blockSize]) + encrypted := make([]byte, len(origData)) + blockMode.CryptBlocks(encrypted, origData) + return encrypted, nil +} + +func AesDecryptCBC(encrypted, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + blockSize := block.BlockSize() + blockMode := cipher.NewCBCDecrypter(block, key[:blockSize]) + origData := make([]byte, len(encrypted)) + blockMode.CryptBlocks(origData, encrypted) + origData = PKCS5UnPadding(origData) + return origData, nil +} + +func Pbkdf2(password string) ([]byte, error) { + //生成随机盐 + salt := make([]byte, 32) + _, err := rand.Read(salt) + if err != nil { + return nil, err + } + //生成密文 + dk := pbkdf2.Key([]byte(password), salt, 1, 32, sha256.New) + return dk, nil +} diff --git a/web/package.json b/web/package.json index ae3f55c..5390912 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "next-terminal", - "version": "0.3.4", + "version": "0.4.0", "private": true, "dependencies": { "@ant-design/icons": "^4.3.0", diff --git a/web/src/components/Login.js b/web/src/components/Login.js index 351b442..04735c8 100644 --- a/web/src/components/Login.js +++ b/web/src/components/Login.js @@ -132,7 +132,7 @@ class LoginForm extends Component { { this.formRef.current diff --git a/web/src/components/access/Term.js b/web/src/components/access/Term.js index 786cc9d..68f098d 100644 --- a/web/src/components/access/Term.js +++ b/web/src/components/access/Term.js @@ -20,7 +20,8 @@ class Term extends Component { term: undefined, webSocket: undefined, fitAddon: undefined, - sessionId: undefined + sessionId: undefined, + enterBtnIndex: 1001 }; componentDidMount = async () => { @@ -215,17 +216,17 @@ class Term extends Component { }}/> - + + + + + + + + + + + + + + + + + + `总计 ${total} 条` + }} + loading={this.state.loading} + /> + + + ); + } +} + +export default ChooseAsset; diff --git a/web/src/components/command/DynamicCommand.js b/web/src/components/command/DynamicCommand.js index 08eea4d..c7161bc 100644 --- a/web/src/components/command/DynamicCommand.js +++ b/web/src/components/command/DynamicCommand.js @@ -32,10 +32,10 @@ import { SyncOutlined, UndoOutlined } from '@ant-design/icons'; -import {compare} from "../../utils/utils"; import {hasPermission, isAdmin} from "../../service/permission"; import dayjs from "dayjs"; +import ChooseAsset from "./ChooseAsset"; const confirm = Modal.confirm; const {Content} = Layout; @@ -191,6 +191,12 @@ class DynamicCommand extends Component { }); }; + setCheckedAssets = (checkedAssets) => { + this.setState({ + checkedAssets: checkedAssets + }) + } + executeCommand = e => { let checkedAssets = this.state.checkedAssets; if (checkedAssets.length === 0) { @@ -198,18 +204,10 @@ class DynamicCommand extends Component { return; } - let assets = this.state.assets; let cAssets = checkedAssets.map(item => { - let name = ''; - for (let i = 0; i < assets.length; i++) { - if (assets[i]['id'] === item) { - name = assets[i]['name']; - break; - } - } return { - id: item, - name: name + id: item['id'], + name: item['name'] } }); @@ -474,17 +472,6 @@ class DynamicCommand extends Component { assetsVisible: true, commandId: record['id'] }); - - let result = await request.get('/assets?protocol=ssh'); - if (result.code === 1) { - let assets = result.data; - assets.sort(compare('name')); - this.setState({ - assets: assets - }); - } else { - message.error(result.message); - } }}>执行 @@ -640,7 +627,8 @@ class DynamicCommand extends Component { { this.setState({ @@ -648,19 +636,11 @@ class DynamicCommand extends Component { }); }} > - - 全选 - - + - { - return { - label: item.name, - value: item.id, - key: item.id, - } - })} value={this.state.checkedAssets} onChange={this.onChange}/> + diff --git a/web/src/components/command/DynamicCommandModal.js b/web/src/components/command/DynamicCommandModal.js index deb776c..7855292 100644 --- a/web/src/components/command/DynamicCommandModal.js +++ b/web/src/components/command/DynamicCommandModal.js @@ -21,6 +21,7 @@ const DynamicCommandModal = ({title, visible, handleOk, handleCancel, confirmLoa title={title} visible={visible} maskClosable={false} + onOk={() => { form .validateFields() diff --git a/web/src/components/credential/CredentialModal.js b/web/src/components/credential/CredentialModal.js index 874e722..32f06f8 100644 --- a/web/src/components/credential/CredentialModal.js +++ b/web/src/components/credential/CredentialModal.js @@ -47,6 +47,7 @@ const CredentialModal = ({title, visible, handleOk, handleCancel, confirmLoading title={title} visible={visible} maskClosable={false} + onOk={() => { form .validateFields() diff --git a/web/src/components/devops/JobModal.js b/web/src/components/devops/JobModal.js index 1246852..135baba 100644 --- a/web/src/components/devops/JobModal.js +++ b/web/src/components/devops/JobModal.js @@ -47,6 +47,7 @@ const JobModal = ({title, visible, handleOk, handleCancel, confirmLoading, model title={title} visible={visible} maskClosable={false} + onOk={() => { form .validateFields() diff --git a/web/src/components/devops/SecurityModal.js b/web/src/components/devops/SecurityModal.js index f37f54d..6d54a3f 100644 --- a/web/src/components/devops/SecurityModal.js +++ b/web/src/components/devops/SecurityModal.js @@ -1,6 +1,5 @@ import React from 'react'; -import {Form, Input, InputNumber, Modal, Radio, Tooltip} from "antd/lib/index"; -import {ExclamationCircleOutlined} from "@ant-design/icons"; +import {Form, Input, InputNumber, Modal, Radio} from "antd/lib/index"; const formItemLayout = { labelCol: {span: 6}, @@ -20,6 +19,7 @@ const SecurityModal = ({title, visible, handleOk, handleCancel, confirmLoading, title={title} visible={visible} maskClosable={false} + onOk={() => { form .validateFields() @@ -54,9 +54,7 @@ const SecurityModal = ({title, visible, handleOk, handleCancel, confirmLoading, - 优先级  - } name='priority' rules={[{required: true, message: '请输入优先级'}]}> + diff --git a/web/src/components/user/User.js b/web/src/components/user/User.js index 402bac1..44a9ef3 100644 --- a/web/src/components/user/User.js +++ b/web/src/components/user/User.js @@ -628,7 +628,6 @@ class User extends Component { width={window.innerWidth * 0.8} title='已授权资产' visible={this.state.assetVisible} - maskClosable={false} destroyOnClose={true} onOk={() => { @@ -649,7 +648,6 @@ class User extends Component { { this.changePasswordFormRef.current .validateFields() diff --git a/web/src/components/user/UserGroup.js b/web/src/components/user/UserGroup.js index 5bc5af2..fef32e8 100644 --- a/web/src/components/user/UserGroup.js +++ b/web/src/components/user/UserGroup.js @@ -429,7 +429,6 @@ class UserGroup extends Component { title='已授权资产' visible={this.state.assetVisible} maskClosable={false} - destroyOnClose={true} onOk={() => {