From 8b30308a4b39d79c6eccbf45c4b292d530c3a8dc Mon Sep 17 00:00:00 2001 From: zhengkunwang <31820853+zhengkunwang223@users.noreply.github.com> Date: Sat, 8 Feb 2025 16:43:55 +0800 Subject: [PATCH] feat(system): Password encryption for login API. (#7825) --- core/app/service/auth.go | 49 +++-- core/app/service/setting.go | 30 +++ core/init/hook/hook.go | 9 + core/init/router/router.go | 1 + core/middleware/password_rsa.go | 22 +++ core/utils/encrypt/encrypt.go | 109 +++++++++-- frontend/package.json | 4 +- frontend/src/utils/util.ts | 60 ++++++ .../src/views/login/components/login-form.vue | 176 +----------------- frontend/src/views/login/index.vue | 105 +---------- 10 files changed, 258 insertions(+), 307 deletions(-) create mode 100644 core/middleware/password_rsa.go diff --git a/core/app/service/auth.go b/core/app/service/auth.go index f65e16b84..f91b5bb2f 100644 --- a/core/app/service/auth.go +++ b/core/app/service/auth.go @@ -37,16 +37,11 @@ func (u *AuthService) Login(c *gin.Context, info dto.Login, entrance string) (*d if err != nil { return nil, "", buserr.New("ErrRecordNotFound") } - passwordSetting, err := settingRepo.Get(repo.WithByKey("Password")) - if err != nil { - return nil, "", buserr.New("ErrRecordNotFound") - } - pass, err := encrypt.StringDecrypt(passwordSetting.Value) - if err != nil { + if nameSetting.Value != info.Name { return nil, "ErrAuth", nil } - if !hmac.Equal([]byte(info.Password), []byte(pass)) || nameSetting.Value != info.Name { - return nil, "ErrAuth", nil + if err = checkPassword(info.Password); err != nil { + return nil, "ErrAuth", err } entranceSetting, err := settingRepo.Get(repo.WithByKey("SecurityEntrance")) if err != nil { @@ -77,17 +72,12 @@ func (u *AuthService) MFALogin(c *gin.Context, info dto.MFALogin, entrance strin if err != nil { return nil, "", buserr.New("ErrRecordNotFound") } - passwordSetting, err := settingRepo.Get(repo.WithByKey("Password")) - if err != nil { - return nil, "", buserr.New("ErrRecordNotFound") - } - pass, err := encrypt.StringDecrypt(passwordSetting.Value) - if err != nil { - return nil, "", err - } - if !hmac.Equal([]byte(info.Password), []byte(pass)) || nameSetting.Value != info.Name { + if nameSetting.Value != info.Name { return nil, "ErrAuth", nil } + if err = checkPassword(info.Password); err != nil { + return nil, "ErrAuth", err + } entranceSetting, err := settingRepo.Get(repo.WithByKey("SecurityEntrance")) if err != nil { return nil, "", err @@ -216,3 +206,28 @@ func (u *AuthService) IsLogin(c *gin.Context) bool { _, err := global.SESSION.Get(c) return err == nil } + +func checkPassword(password string) error { + priKey, _ := settingRepo.Get(repo.WithByKey("PASSWORD_PRIVATE_KEY")) + + privateKey, err := encrypt.ParseRSAPrivateKey(priKey.Value) + if err != nil { + return err + } + loginPassword, err := encrypt.DecryptPassword(password, privateKey) + if err != nil { + return err + } + passwordSetting, err := settingRepo.Get(repo.WithByKey("Password")) + if err != nil { + return err + } + existPassword, err := encrypt.StringDecrypt(passwordSetting.Value) + if err != nil { + return err + } + if !hmac.Equal([]byte(loginPassword), []byte(existPassword)) { + return buserr.New("ErrAuth") + } + return nil +} diff --git a/core/app/service/setting.go b/core/app/service/setting.go index cf904a4a7..cef08c488 100644 --- a/core/app/service/setting.go +++ b/core/app/service/setting.go @@ -1,6 +1,8 @@ package service import ( + "crypto/rand" + "crypto/rsa" "crypto/tls" "crypto/x509" "encoding/json" @@ -45,6 +47,8 @@ type ISettingService interface { UpdateTerminal(req dto.TerminalInfo) error UpdateSystemSSL() error + + GenerateRSAKey() error } func NewISettingService() ISettingService { @@ -497,3 +501,29 @@ func checkCertValid() error { return nil } + +func (u *SettingService) GenerateRSAKey() error { + priKey, _ := settingRepo.Get(repo.WithByKey("PASSWORD_PRIVATE_KEY")) + pubKey, _ := settingRepo.Get(repo.WithByKey("PASSWORD_PUBLIC_KEY")) + if priKey.Value != "" && pubKey.Value != "" { + return nil + } + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return err + } + privateKeyPEM := encrypt.ExportPrivateKeyToPEM(privateKey) + publicKeyPEM, err := encrypt.ExportPublicKeyToPEM(&privateKey.PublicKey) + if err != nil { + return err + } + err = settingRepo.UpdateOrCreate("PASSWORD_PRIVATE_KEY", privateKeyPEM) + if err != nil { + return err + } + err = settingRepo.UpdateOrCreate("PASSWORD_PUBLIC_KEY", publicKeyPEM) + if err != nil { + return err + } + return nil +} diff --git a/core/init/hook/hook.go b/core/init/hook/hook.go index 1a3ce114c..c38a1ce89 100644 --- a/core/init/hook/hook.go +++ b/core/init/hook/hook.go @@ -1,6 +1,7 @@ package hook import ( + "github.com/1Panel-dev/1Panel/core/app/service" "strings" "github.com/1Panel-dev/1Panel/core/app/repo" @@ -29,6 +30,8 @@ func Init() { } handleUserInfo(global.CONF.Base.ChangeUserInfo, settingRepo) + + generateKey() } func handleUserInfo(tags string, settingRepo repo.ISettingRepo) { @@ -68,3 +71,9 @@ func handleUserInfo(tags string, settingRepo repo.ISettingRepo) { sudo := cmd.SudoHandleCmd() _, _ = cmd.Execf("%s sed -i '/CHANGE_USER_INFO=%v/d' /usr/local/bin/1pctl", sudo, global.CONF.Base.ChangeUserInfo) } + +func generateKey() { + if err := service.NewISettingService().GenerateRSAKey(); err != nil { + global.LOG.Errorf("generate rsa key error : %s", err.Error()) + } +} diff --git a/core/init/router/router.go b/core/init/router/router.go index a3461f067..22e6568dc 100644 --- a/core/init/router/router.go +++ b/core/init/router/router.go @@ -167,6 +167,7 @@ func Routers() *gin.Engine { PrivateGroup.Use(middleware.WhiteAllow()) PrivateGroup.Use(middleware.BindDomain()) PrivateGroup.Use(middleware.GlobalLoading()) + PrivateGroup.Use(middleware.SetPasswordPublicKey()) for _, router := range rou.RouterGroupApp { router.InitRouter(PrivateGroup) } diff --git a/core/middleware/password_rsa.go b/core/middleware/password_rsa.go new file mode 100644 index 000000000..654856210 --- /dev/null +++ b/core/middleware/password_rsa.go @@ -0,0 +1,22 @@ +package middleware + +import ( + "encoding/base64" + "github.com/1Panel-dev/1Panel/core/app/repo" + "github.com/gin-gonic/gin" +) + +func SetPasswordPublicKey() gin.HandlerFunc { + return func(c *gin.Context) { + cookieKey, _ := c.Cookie("panel_public_key") + settingRepo := repo.NewISettingRepo() + key, _ := settingRepo.Get(repo.WithByKey("PASSWORD_PUBLIC_KEY")) + base64Key := base64.StdEncoding.EncodeToString([]byte(key.Value)) + if base64Key == cookieKey { + c.Next() + return + } + c.SetCookie("panel_public_key", base64Key, 7*24*60*60, "/", "", false, false) + c.Next() + } +} diff --git a/core/utils/encrypt/encrypt.go b/core/utils/encrypt/encrypt.go index 82e92e0c8..f42269ac0 100644 --- a/core/utils/encrypt/encrypt.go +++ b/core/utils/encrypt/encrypt.go @@ -5,26 +5,19 @@ import ( "crypto/aes" "crypto/cipher" "crypto/rand" + "crypto/rsa" + "crypto/x509" "encoding/base64" + "encoding/pem" + "errors" "fmt" "io" + "strings" "github.com/1Panel-dev/1Panel/core/app/model" "github.com/1Panel-dev/1Panel/core/global" ) -func StringEncryptWithBase64(text string) (string, error) { - base64Item, err := base64.StdEncoding.DecodeString(text) - if err != nil { - return "", err - } - encryptItem, err := StringEncrypt(string(base64Item)) - if err != nil { - return "", err - } - return encryptItem, nil -} - func StringDecryptWithBase64(text string) (string, error) { decryptItem, err := StringDecrypt(text) if err != nil { @@ -136,3 +129,95 @@ func aesDecryptWithSalt(key, ciphertext []byte) ([]byte, error) { ciphertext = unPadding(ciphertext) return ciphertext, nil } + +func ParseRSAPrivateKey(privateKeyPEM string) (*rsa.PrivateKey, error) { + block, _ := pem.Decode([]byte(privateKeyPEM)) + if block == nil || block.Type != "RSA PRIVATE KEY" { + return nil, errors.New("failed to decode PEM block containing the private key") + } + privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + return privateKey, nil +} + +func aesDecrypt(ciphertext, key, iv []byte) ([]byte, error) { + if len(key) != 16 && len(key) != 24 && len(key) != 32 { + return nil, errors.New("invalid AES key length: must be 16, 24, or 32 bytes") + } + if len(iv) != aes.BlockSize { + return nil, errors.New("invalid IV length: must be 16 bytes") + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(ciphertext, ciphertext) + ciphertext = pkcs7Unpad(ciphertext) + return ciphertext, nil +} + +func pkcs7Unpad(data []byte) []byte { + length := len(data) + padLength := int(data[length-1]) + return data[:length-padLength] +} + +func DecryptPassword(encryptedData string, privateKey *rsa.PrivateKey) (string, error) { + parts := strings.Split(encryptedData, ":") + if len(parts) != 3 { + return "", errors.New("encrypted data format error") + } + keyCipher := parts[0] + ivBase64 := parts[1] + ciphertextBase64 := parts[2] + + encryptedAESKey, err := base64.StdEncoding.DecodeString(keyCipher) + if err != nil { + return "", errors.New("failed to decode keyCipher") + } + + aesKey, err := rsa.DecryptPKCS1v15(rand.Reader, privateKey, encryptedAESKey) + if err != nil { + return "", errors.New("failed to decode AES Key") + } + + ciphertext, err := base64.StdEncoding.DecodeString(ciphertextBase64) + if err != nil { + return "", errors.New("failed to decrypt the encrypted data") + } + iv, err := base64.StdEncoding.DecodeString(ivBase64) + if err != nil { + return "", errors.New("failed to decode the IV") + } + + password, err := aesDecrypt(ciphertext, aesKey, iv) + if err != nil { + return "", err + } + return string(password), nil +} + +func ExportPrivateKeyToPEM(privateKey *rsa.PrivateKey) string { + privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) + privateKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privateKeyBytes, + }) + return string(privateKeyPEM) +} + +func ExportPublicKeyToPEM(publicKey *rsa.PublicKey) (string, error) { + publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey) + if err != nil { + return "", err + } + publicKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: publicKeyBytes, + }) + return string(publicKeyPEM), nil +} diff --git a/frontend/package.json b/frontend/package.json index 3ca9c181d..b8f4b19fb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,8 +20,8 @@ "prettier": "prettier --write ." }, "dependencies": { - "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-html": "^6.4.9", + "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-php": "^6.0.1", "@codemirror/language": "^6.10.2", "@codemirror/legacy-modes": "^6.4.0", @@ -35,11 +35,13 @@ "@xterm/xterm": "^5.5.0", "axios": "^1.7.2", "codemirror": "^6.0.1", + "crypto-js": "^4.2.0", "echarts": "^5.5.0", "element-plus": "^2.7.5", "fit2cloud-ui-plus": "^1.2.0", "highlight.js": "^11.9.0", "js-base64": "^3.7.7", + "jsencrypt": "^3.3.2", "md-editor-v3": "^2.11.3", "monaco-editor": "^0.50.0", "nprogress": "^0.2.0", diff --git a/frontend/src/utils/util.ts b/frontend/src/utils/util.ts index 46b5cb28d..e3028b19e 100644 --- a/frontend/src/utils/util.ts +++ b/frontend/src/utils/util.ts @@ -4,6 +4,8 @@ import useClipboard from 'vue-clipboard3'; const { toClipboard } = useClipboard(); import { MsgError, MsgSuccess } from '@/utils/message'; import { v4 as uuidv4 } from 'uuid'; +import JSEncrypt from 'jsencrypt'; +import CryptoJS from 'crypto-js'; export function deepCopy(obj: any): T { let newObj: any; @@ -695,3 +697,61 @@ export function getRuntimeLabel(type: string) { } return ''; } + +function getCookie(name: string) { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(';').shift(); +} + +function rsaEncrypt(data: string, publicKey: string) { + if (!data) { + return data; + } + const jsEncrypt = new JSEncrypt(); + jsEncrypt.setPublicKey(publicKey); + return jsEncrypt.encrypt(data); +} + +function aesEncrypt(data: string, key: string) { + const keyBytes = CryptoJS.enc.Utf8.parse(key); + const iv = CryptoJS.lib.WordArray.random(16); + const encrypted = CryptoJS.AES.encrypt(data, keyBytes, { + iv: iv, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7, + }); + return iv.toString(CryptoJS.enc.Base64) + ':' + encrypted.toString(); +} + +function urlDecode(value: string): string { + return decodeURIComponent(value.replace(/\+/g, ' ')); +} + +function generateAESKey(): string { + const keyLength = 16; + const randomBytes = new Uint8Array(keyLength); + crypto.getRandomValues(randomBytes); + return Array.from(randomBytes) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); +} + +export const encryptPassword = (password: string) => { + if (!password) { + return ''; + } + let rsaPublicKeyText = getCookie('panel_public_key'); + if (!rsaPublicKeyText) { + console.log('RSA public key not found'); + return password; + } + rsaPublicKeyText = urlDecode(rsaPublicKeyText); + + const aesKey = generateAESKey(); + rsaPublicKeyText = rsaPublicKeyText.replaceAll('"', ''); + const rsaPublicKey = atob(rsaPublicKeyText); + const keyCipher = rsaEncrypt(aesKey, rsaPublicKey); + const passwordCipher = aesEncrypt(password, aesKey); + return `${keyCipher}:${passwordCipher}`; +}; diff --git a/frontend/src/views/login/components/login-form.vue b/frontend/src/views/login/components/login-form.vue index 55c91f6d7..497421b02 100644 --- a/frontend/src/views/login/components/login-form.vue +++ b/frontend/src/views/login/components/login-form.vue @@ -189,6 +189,7 @@ import { GlobalStore, MenuStore, TabsStore } from '@/store'; import { MsgSuccess } from '@/utils/message'; import { useI18n } from 'vue-i18n'; import { getSettingInfo } from '@/api/modules/setting'; +import { encryptPassword } from '@/utils/util'; const i18n = useI18n(); const themeConfig = computed(() => globalStore.themeConfig); @@ -313,7 +314,7 @@ const login = (formEl: FormInstance | undefined) => { } let requestLoginForm = { name: loginForm.name, - password: loginForm.password, + password: encryptPassword(loginForm.password), ignoreCaptcha: globalStore.ignoreCaptcha, captcha: loginForm.captcha, captchaID: captcha.captchaID, @@ -352,7 +353,6 @@ const login = (formEl: FormInstance | undefined) => { globalStore.ignoreCaptcha = false; errCaptcha.value = false; errAuthInfo.value = true; - console.log('11111'); } } loginVerify(); @@ -368,7 +368,7 @@ const mfaLogin = async (auto: boolean) => { if ((!auto && mfaLoginForm.code) || (auto && mfaLoginForm.code.length === 6)) { isLoggingIn = true; mfaLoginForm.name = loginForm.name; - mfaLoginForm.password = loginForm.password; + mfaLoginForm.password = encryptPassword(loginForm.password); try { await mfaLoginApi(mfaLoginForm); globalStore.setLogStatus(true); @@ -464,173 +464,3 @@ onMounted(() => { padding: 0 !important; } - diff --git a/frontend/src/views/login/index.vue b/frontend/src/views/login/index.vue index e8d1b943a..37e48d5c7 100644 --- a/frontend/src/views/login/index.vue +++ b/frontend/src/views/login/index.vue @@ -19,23 +19,6 @@ - - -