1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-31 14:08:06 +08:00

feat: Added password encryption for login functionality (#7764)

This commit is contained in:
zhengkunwang 2025-01-23 18:09:12 +08:00 committed by GitHub
parent 4f57dfc76e
commit aaaa5980b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 277 additions and 35 deletions

View File

@ -15,6 +15,7 @@ type DBOption func(*gorm.DB) *gorm.DB
type ICommonRepo interface {
WithByID(id uint) DBOption
WithByName(name string) DBOption
WithByLowerName(name string) DBOption
WithByType(tp string) DBOption
WithOrderBy(orderStr string) DBOption
WithOrderRuleBy(orderBy, order string) DBOption
@ -45,6 +46,12 @@ func (c *CommonRepo) WithByName(name string) DBOption {
}
}
func (c *CommonRepo) WithByLowerName(name string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("LOWER(name) = LOWER(?)", name)
}
}
func (c *CommonRepo) WithByDate(startTime, endTime time.Time) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("start_time > ? AND start_time < ?", startTime, endTime)

View File

@ -16,6 +16,7 @@ type ISettingRepo interface {
Create(key, value string) error
Update(key, value string) error
WithByKey(key string) DBOption
UpdateOrCreate(key, value string) error
CreateMonitorBase(model model.MonitorBase) error
BatchCreateMonitorIO(ioList []model.MonitorIO) error
@ -85,3 +86,7 @@ func (u *SettingRepo) DelMonitorIO(timeForDelete time.Time) error {
func (u *SettingRepo) DelMonitorNet(timeForDelete time.Time) error {
return global.MonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorNetwork{}).Error
}
func (u *SettingRepo) UpdateOrCreate(key, value string) error {
return global.DB.Model(&model.Setting{}).Where("key = ?", key).Assign(model.Setting{Key: key, Value: value}).FirstOrCreate(&model.Setting{}).Error
}

View File

@ -326,7 +326,8 @@ func (a AppService) Install(ctx context.Context, req request.AppInstallCreate) (
err = buserr.WithDetail(constant.Err1PanelNetworkFailed, err.Error(), nil)
return
}
if list, _ := appInstallRepo.ListBy(commonRepo.WithByName(req.Name)); len(list) > 0 {
if list, _ := appInstallRepo.ListBy(commonRepo.WithByLowerName(req.Name)); len(list) > 0 {
err = buserr.New(constant.ErrAppNameExist)
return
}

View File

@ -3,8 +3,6 @@ package service
import (
"crypto/hmac"
"encoding/base64"
"strconv"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
@ -15,6 +13,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/pkg/errors"
"strconv"
)
type AuthService struct{}
@ -38,16 +37,11 @@ func (u *AuthService) Login(c *gin.Context, info dto.Login, entrance string) (*d
if err != nil {
return nil, errors.WithMessage(constant.ErrRecordNotFound, err.Error())
}
passwordSetting, err := settingRepo.Get(settingRepo.WithByKey("Password"))
if err != nil {
return nil, errors.WithMessage(constant.ErrRecordNotFound, err.Error())
}
pass, err := encrypt.StringDecrypt(passwordSetting.Value)
if err != nil {
if nameSetting.Value != info.Name {
return nil, constant.ErrAuth
}
if !hmac.Equal([]byte(info.Password), []byte(pass)) || nameSetting.Value != info.Name {
return nil, constant.ErrAuth
if err = checkPassword(info.Password); err != nil {
return nil, err
}
entranceSetting, err := settingRepo.Get(settingRepo.WithByKey("SecurityEntrance"))
if err != nil {
@ -83,17 +77,12 @@ func (u *AuthService) MFALogin(c *gin.Context, info dto.MFALogin, entrance strin
if err != nil {
return nil, errors.WithMessage(constant.ErrRecordNotFound, err.Error())
}
passwordSetting, err := settingRepo.Get(settingRepo.WithByKey("Password"))
if err != nil {
return nil, errors.WithMessage(constant.ErrRecordNotFound, err.Error())
}
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, constant.ErrAuth
}
if err = checkPassword(info.Password); err != nil {
return nil, err
}
entranceSetting, err := settingRepo.Get(settingRepo.WithByKey("SecurityEntrance"))
if err != nil {
return nil, err
@ -219,3 +208,28 @@ func (u *AuthService) IsLogin(c *gin.Context) bool {
}
return true
}
func checkPassword(password string) error {
priKey, _ := settingRepo.Get(settingRepo.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(settingRepo.WithByKey("Password"))
if err != nil {
return errors.WithMessage(constant.ErrRecordNotFound, err.Error())
}
existPassword, err := encrypt.StringDecrypt(passwordSetting.Value)
if err != nil {
return err
}
if !hmac.Equal([]byte(loginPassword), []byte(existPassword)) {
return constant.ErrAuth
}
return nil
}

View File

@ -1,6 +1,8 @@
package service
import (
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/json"
@ -42,6 +44,7 @@ type ISettingService interface {
HandlePasswordExpired(c *gin.Context, old, new string) error
GenerateApiKey() (string, error)
UpdateApiConfig(req dto.ApiInterfaceConfig) error
GenerateRSAKey() error
}
func NewISettingService() ISettingService {
@ -516,3 +519,47 @@ func (u *SettingService) UpdateApiConfig(req dto.ApiInterfaceConfig) error {
global.CONF.System.ApiKeyValidityTime = req.ApiKeyValidityTime
return 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
}
func (u *SettingService) GenerateRSAKey() error {
priKey, _ := settingRepo.Get(settingRepo.WithByKey("PASSWORD_PRIVATE_KEY"))
pubKey, _ := settingRepo.Get(settingRepo.WithByKey("PASSWORD_PUBLIC_KEY"))
if priKey.Value != "" && pubKey.Value != "" {
return nil
}
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return err
}
privateKeyPEM := exportPrivateKeyToPEM(privateKey)
publicKeyPEM, err := exportPublicKeyToPEM(&privateKey.PublicKey)
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
}

View File

@ -11,6 +11,7 @@ func Init() {
go syncInstalledApp()
go syncRuntime()
go syncSSL()
generateKey()
}
func syncApp() {
@ -38,3 +39,9 @@ func syncSSL() {
global.LOG.Errorf("sync ssl status error : %s", err.Error())
}
}
func generateKey() {
if err := service.NewISettingService().GenerateRSAKey(); err != nil {
global.LOG.Errorf("generate rsa key error : %s", err.Error())
}
}

View File

@ -113,9 +113,7 @@ func setWebStatic(rootRouter *gin.RouterGroup) {
rootRouter.StaticFS("/public", http.FS(web.Favicon))
rootRouter.StaticFS("/favicon.ico", http.FS(web.Favicon))
rootRouter.Static("/api/v1/images", "./uploads")
rootRouter.Use(func(c *gin.Context) {
c.Next()
})
rootRouter.GET("/assets/*filepath", func(c *gin.Context) {
c.Writer.Header().Set("Cache-Control", fmt.Sprintf("private, max-age=%d", 3600))
staticServer := http.FileServer(http.FS(web.Assets))
@ -158,6 +156,7 @@ func Routers() *gin.Engine {
Router.Use(middleware.WhiteAllow())
Router.Use(middleware.BindDomain())
Router.Use(middleware.SetPasswordPublicKey())
Router.NoRoute(func(c *gin.Context) {
if checkFrontendPath(c) {

View File

@ -0,0 +1,22 @@
package middleware
import (
"encoding/base64"
"github.com/1Panel-dev/1Panel/backend/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(settingRepo.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()
}
}

View File

@ -5,9 +5,14 @@ import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"io"
"strings"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/global"
@ -102,3 +107,75 @@ 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
}

View File

@ -21,9 +21,9 @@
},
"dependencies": {
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-php": "^6.0.1",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.10.2",
"@codemirror/legacy-modes": "^6.4.0",
"@codemirror/state": "^6.4.1",
@ -37,11 +37,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",
@ -54,11 +56,12 @@
"vue-clipboard3": "^2.0.0",
"vue-codemirror": "^6.1.1",
"vue-demi": "^0.14.6",
"vue-i18n": "^10.0.5",
"vue-router": "^4.3.3"
"vue-i18n": "^9.13.1",
"vue-router": "^4.3.3",
"vue-virtual-scroller": "^2.0.0-beta.8"
},
"devDependencies": {
"@types/node": "^20.14.2",
"@types/node": "^20.15.0",
"@typescript-eslint/eslint-plugin": "^5.22.0",
"@typescript-eslint/parser": "^5.22.0",
"@vitejs/plugin-vue": "^5.0.5",
@ -73,7 +76,7 @@
"postcss": "^8.4.31",
"postcss-html": "^1.4.1",
"prettier": "^2.6.2",
"rollup-plugin-visualizer": "^5.5.4",
"rollup-plugin-visualizer": "^5.14.0",
"sass": "^1.77.8",
"standard-version": "^9.5.0",
"stylelint": "^15.10.1",
@ -81,7 +84,7 @@
"typescript": "^4.5.4",
"unplugin-auto-import": "^0.16.4",
"unplugin-vue-components": "^0.25.0",
"vite": "^5.2.13",
"vite": "^6.0.7",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-html": "^3.2.2",
@ -90,7 +93,7 @@
"vue-tsc": "^0.29.8"
},
"overrides": {
"esbuild": "npm:esbuild-wasm@latest"
"esbuild": "npm:esbuild-wasm@0.24.2"
},
"config": {
"commitizen": {

View File

@ -289,7 +289,7 @@ const checkAppName = (rule: any, value: any, callback: any) => {
if (value === '' || typeof value === 'undefined' || value == null) {
callback(new Error(i18n.global.t('commons.rule.appName')));
} else {
const reg = /^(?![_-])[a-z0-9_-]{1,29}[a-zA-Z0-9]$/;
const reg = /^(?![_-])[a-zA-Z0-9_-]{1,29}[a-zA-Z0-9]$/;
if (!reg.test(value) && value !== '') {
callback(new Error(i18n.global.t('commons.rule.appName')));
} else {

View File

@ -1451,8 +1451,7 @@ const message = {
confDockerProxy: 'Configurar proxy do Docker',
restartNowHelper: 'Configurar o proxy do Docker exige reiniciar o serviço Docker.',
restartNow: 'Reiniciar imediatamente',
systemIPWarning:
'O endereço do sistema não está definido no momento. Defina-o primeiro no painel de controle.',
systemIPWarning: 'O endereço do sistema não está definido no momento. Defina-o primeiro no painel de controle.',
systemIPWarning1:
'O endereço do sistema atual está definido como {0}, e o redirecionamento rápido não é possível!',
defaultNetwork: 'Placa de rede',

View File

@ -3,6 +3,8 @@ import i18n from '@/lang';
import useClipboard from 'vue-clipboard3';
const { toClipboard } = useClipboard();
import { MsgError, MsgSuccess } from '@/utils/message';
import JSEncrypt from 'jsencrypt';
import CryptoJS from 'crypto-js';
export function deepCopy<T>(obj: any): T {
let newObj: any;
@ -615,3 +617,61 @@ export const escapeProxyURL = (url: string): string => {
return url.replace(/[\/:?#[\]@!$&'()*+,;=%~]/g, (match) => encodeMap[match] || match);
};
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}`;
};

View File

@ -180,6 +180,7 @@ import { MsgSuccess } from '@/utils/message';
import { useI18n } from 'vue-i18n';
import { getSettingInfo } from '@/api/modules/setting';
import { Rules } from '@/global/form-rules';
import { encryptPassword } from '@/utils/util';
const i18n = useI18n();
const themeConfig = computed(() => globalStore.themeConfig);
@ -314,7 +315,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,
@ -370,7 +371,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);
const res = await mfaLoginApi(mfaLoginForm);
if (res.code === 406) {
errMfaInfo.value = true;