From 30f50f67c3af843d304ea9ca2da8b149a26dc763 Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 31 Mar 2026 16:11:08 +0800 Subject: [PATCH] perf: update encrypt support gm --- package.json | 1 + .../Apps/AccountCreateUpdateForm/index.vue | 31 ++-- .../AccountBulkUpdateDialog.vue | 10 +- .../AccountListTable/UpdateSecretInfo.vue | 37 ++-- .../Apps/AccountListTable/ViewSecret.vue | 33 ++-- .../Apps/UserConfirmDialog/index.vue | 4 +- .../ListTable/TableAction/ImportTable.vue | 52 +++--- .../GenericCreateUpdateForm/index.vue | 57 ++++--- src/utils/secure.js | 62 +------ src/utils/session-encrypt.js | 160 ++++++++++++++++++ .../AccountBackupCreateUpdate.vue | 9 +- .../AccountTemplateCreateUpdate.vue | 9 +- .../BaseAssetCreateUpdate.vue | 9 +- src/views/assets/Cloud/const.js | 16 +- .../Storage/ObjectStorageCreateUpdate.vue | 14 +- yarn.lock | 12 ++ 16 files changed, 328 insertions(+), 188 deletions(-) create mode 100644 src/utils/session-encrypt.js diff --git a/package.json b/package.json index c60995f40..ac3a747bb 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "npm": "^7.8.0", "nprogress": "0.2.0", "path-to-regexp": "3.3.0", + "sm-crypto": "^0.4.0", "sortablejs": "^1.15.6", "v-sanitize": "^0.0.13", "vue": "2.7.16", diff --git a/src/components/Apps/AccountCreateUpdateForm/index.vue b/src/components/Apps/AccountCreateUpdateForm/index.vue index c31abc1c9..12c672ecd 100644 --- a/src/components/Apps/AccountCreateUpdateForm/index.vue +++ b/src/components/Apps/AccountCreateUpdateForm/index.vue @@ -2,7 +2,7 @@ import AutoDataForm from '@/components/Form/AutoDataForm/index.vue' -import { encryptPassword } from '@/utils/secure' +import { encryptPassword } from '@/utils/session-encrypt' import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const' export default { @@ -59,16 +59,27 @@ export default { ] }, url: '/api/v1/accounts/accounts/', - form: Object.assign({ 'on_invalid': 'error' }, this.account || {}), + form: Object.assign({ on_invalid: 'error' }, this.account || {}), encryptedFields: ['secret'], fields: [ - [this.$t('Basic'), ['name', 'username', 'privileged', 'su_from', 'su_from_username', 'template']], + [ + this.$t('Basic'), + ['name', 'username', 'privileged', 'su_from', 'su_from_username', 'template'] + ], [this.$t('Asset'), ['nodes', 'assets']], - [this.$t('Secret'), [ - 'secret_type', 'password', 'ssh_key', 'token', - 'access_key', 'passphrase', 'api_key', - 'secret_reset' - ]], + [ + this.$t('Secret'), + [ + 'secret_type', + 'password', + 'ssh_key', + 'token', + 'access_key', + 'passphrase', + 'api_key', + 'secret_reset' + ] + ], [this.$t('Other'), ['push_now', 'params', 'on_invalid', 'is_active', 'comment']] ], fieldsMeta: accountFieldsMeta(this), @@ -170,7 +181,7 @@ export default { } - + diff --git a/src/components/Apps/AccountListTable/UpdateSecretInfo.vue b/src/components/Apps/AccountListTable/UpdateSecretInfo.vue index 2d370139a..43c146cf3 100644 --- a/src/components/Apps/AccountListTable/UpdateSecretInfo.vue +++ b/src/components/Apps/AccountListTable/UpdateSecretInfo.vue @@ -23,7 +23,7 @@ + diff --git a/src/utils/secure.js b/src/utils/secure.js index ac6f31f30..575e6a41d 100644 --- a/src/utils/secure.js +++ b/src/utils/secure.js @@ -1,6 +1,4 @@ -/** - * Created by PanJiaChen on 16/11/18. - */ +import { encryptPassword } from './session-encrypt' /** * @param {string} path @@ -43,60 +41,6 @@ const options = { } } const filter = new xss.FilterXSS(options) - -import JSEncrypt from 'jsencrypt/bin/jsencrypt.min' -import CryptoJS from 'crypto-js' -import { vueCookie as VueCookie } from '@/utils/storage' - -export function fillKey(key) { - const KeyLength = 16 - if (key.length > KeyLength) { - key = key.slice(0, KeyLength) - } - const filledKey = Buffer.alloc(KeyLength) - const keys = Buffer.from(key) - for (let i = 0; i < keys.length; i++) { - filledKey[i] = keys[i] - } - return filledKey -} - -export function aesEncrypt(text, originKey) { - const key = CryptoJS.enc.Utf8.parse(fillKey(originKey)) - return CryptoJS.AES.encrypt(text, key, { - mode: CryptoJS.mode.ECB, - padding: CryptoJS.pad.ZeroPadding - }).toString() -} - -export function rsaEncrypt(text, pubKey) { - const jsEncrypt = new JSEncrypt() - jsEncrypt.setPublicKey(pubKey) - return jsEncrypt.encrypt(text) -} - -export function getCookie(name) { - return VueCookie.get(name) -} - -export function encryptPassword(password) { - if (!password) { - return '' - } - let rsaPublicKeyText = getCookie('jms_public_key') - if (!rsaPublicKeyText) { - return password - } - const aesKey = (Math.random() + 1).toString(36).substring(2) - // public key 是 base64 存储的 - rsaPublicKeyText = rsaPublicKeyText.replaceAll('"', '') - const rsaPublicKey = atob(rsaPublicKeyText) - const keyCipher = rsaEncrypt(aesKey, rsaPublicKey) - const passwordCipher = aesEncrypt(String(password), aesKey) - return `${keyCipher}:${passwordCipher}` -} - -window.aesEncrypt = aesEncrypt -window.fillKey = fillKey - export default filter + +window.encryptPassword = encryptPassword diff --git a/src/utils/session-encrypt.js b/src/utils/session-encrypt.js new file mode 100644 index 000000000..a557c713e --- /dev/null +++ b/src/utils/session-encrypt.js @@ -0,0 +1,160 @@ +import JSEncrypt from 'jsencrypt/bin/jsencrypt.min' +import CryptoJS from 'crypto-js' +import { vueCookie as VueCookie } from '@/utils/storage' +import { sm2, sm4 } from 'sm-crypto' + +export function getCookie(name) { + return VueCookie.get(name) +} + +export function fillKey(key) { + const KeyLength = 16 + if (key.length > KeyLength) { + key = key.slice(0, KeyLength) + } + const filledKey = Buffer.alloc(KeyLength) + const keys = Buffer.from(key) + for (let i = 0; i < keys.length; i++) { + filledKey[i] = keys[i] + } + return filledKey +} + +function aesEncrypt(text, originKey) { + const key = CryptoJS.enc.Utf8.parse(fillKey(originKey)) + return CryptoJS.AES.encrypt(text, key, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.ZeroPadding + }).toString() +} + +function rsaEncrypt(text, pubKey) { + if (!text) { + return text + } + const jsEncrypt = new JSEncrypt() + jsEncrypt.setPublicKey(pubKey) + return jsEncrypt.encrypt(text) +} + +function rsaDecrypt(cipher, pkey) { + const jsEncrypt = new JSEncrypt() + jsEncrypt.setPrivateKey(pkey) + return jsEncrypt.decrypt(cipher) +} + +window.rsaEncrypt = rsaEncrypt +window.rsaDecrypt = rsaDecrypt + +function hexToBytes(hex) { + if (!hex) return new Uint8Array([]) + hex = hex.toString().trim().toLowerCase() + if (hex.startsWith('0x')) { + hex = hex.slice(2) + } + // 确保是偶数长度 + const len = Math.floor(hex.length / 2) + const bytes = new Uint8Array(len) + for (let i = 0; i < len; i++) { + bytes[i] = parseInt(hex.substr(i * 2, 2), 16) + } + return bytes +} + +function bytesToBase64(bytes) { + // Uint8Array -> base64(标准 base64) + let binary = '' + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]) + } + return btoa(binary) +} + +function rsaEncryptPassword(password, rsaPublicKey) { + const aesKey = (Math.random() + 1).toString(36).substring(2) + // public key 是 base64 存储的 + const keyCipher = rsaEncrypt(aesKey, rsaPublicKey) + const passwordCipher = aesEncrypt(password, aesKey) + return `${keyCipher}:${passwordCipher}` +} + +function ensureSm2PublicKey(sm2PublicKey) { + // sm2.min.js 的 doEncrypt 需要能被 decodePointHex 解析的公钥: + // 通常为非压缩点 hex,格式 `04||x||y`(总长度 130)。 + // 但后端生成/下发的公钥有时是 `x||y`(长度 128),这里做归一化补齐 `04` 前缀。 + if (typeof sm2PublicKey === 'string') { + sm2PublicKey = sm2PublicKey.replaceAll('"', '').trim() + if (sm2PublicKey.startsWith('0x')) { + sm2PublicKey = sm2PublicKey.slice(2) + } + // 后端下发的 SM2 公钥常见是 x||y(128 hex),sm-crypto 需要 04||x||y(130 hex) + if (sm2PublicKey.length === 128 && !sm2PublicKey.startsWith('04')) { + sm2PublicKey = '04' + sm2PublicKey + } + } + return sm2PublicKey +} + +function gmEncryptPassword(password, sm2PublicKey) { + sm2PublicKey = ensureSm2PublicKey(sm2PublicKey) + // 只适配前端,不改后端: + // 直接生成 16 字符 key(后端 padding_key 会保持原样,不再补齐) + const sm4KeyRaw = randomString(16) + const sm4KeyHex = Buffer.from(sm4KeyRaw).toString('hex') + + let keyCipher = '' + try { + // 与后端 gmssl.sm2.CryptSM2 默认 decrypt 的 mode 对齐: + // gmssl 解析的格式是 C1C2C3(mode=0),前端这里输出也用 mode=0。 + keyCipher = sm2.doEncrypt(sm4KeyRaw, sm2PublicKey, 0) + } catch (e) { + console.error('gmEncryptPassword sm2.doEncrypt failed:', e) + // 避免前端崩溃:失败时返回明文,由后端按原值流程处理(至少可继续登录/看报错) + return password + } + + const passwordCipher = sm4.encrypt(password, sm4KeyHex) + // sm2/sm4 默认输出是 hex,但后端 gm.py/session.py 需要 base64: + // - sm2_decrypt: base64.b64decode + // - sm4 decrypt: base64.urlsafe_b64decode + const keyCipherB64 = bytesToBase64(hexToBytes(keyCipher)) + const passwordCipherB64 = bytesToBase64(hexToBytes(passwordCipher)) + return `${keyCipherB64}:${passwordCipherB64}` +} + +export function encryptPassword(password) { + if (!password) { + console.log('password is empty') + return '' + } + let publicKeyText = getCookie('jms_public_key') + if (!publicKeyText) { + console.log('publicKeyText is empty') + return password + } + publicKeyText = publicKeyText.replaceAll('"', '') + publicKeyText = atob(publicKeyText) + let cipher = '' + let jmsGMSSL = getCookie('jms_gm_ssl') + if (publicKeyText.includes('PUBLIC KEY')) { + jmsGMSSL = '0' + } + if (jmsGMSSL === '1') { + cipher = gmEncryptPassword(password, publicKeyText) + } else { + cipher = rsaEncryptPassword(password, publicKeyText) + } + + return cipher +} + +export function randomString(length) { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + let result = '' + const charactersLength = characters.length + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)) + } + + return result +} diff --git a/src/views/accounts/AccountBackup/AccountBackupCreateUpdate.vue b/src/views/accounts/AccountBackup/AccountBackupCreateUpdate.vue index 2fa0e8c08..e43b9bd5c 100644 --- a/src/views/accounts/AccountBackup/AccountBackupCreateUpdate.vue +++ b/src/views/accounts/AccountBackup/AccountBackupCreateUpdate.vue @@ -5,7 +5,7 @@ - + diff --git a/src/views/accounts/AccountTemplate/AccountTemplateCreateUpdate.vue b/src/views/accounts/AccountTemplate/AccountTemplateCreateUpdate.vue index 792b28416..805725578 100644 --- a/src/views/accounts/AccountTemplate/AccountTemplateCreateUpdate.vue +++ b/src/views/accounts/AccountTemplate/AccountTemplateCreateUpdate.vue @@ -1,14 +1,11 @@ - + diff --git a/src/views/assets/Cloud/const.js b/src/views/assets/Cloud/const.js index fc8bd621e..9dc090b7f 100644 --- a/src/views/assets/Cloud/const.js +++ b/src/views/assets/Cloud/const.js @@ -1,5 +1,5 @@ import i18n from '@/i18n/i18n' -import { encryptPassword } from '@/utils/secure' +import { encryptPassword } from '@/utils/session-encrypt' export const gcp = 'gcp' export const aliyun = 'aliyun' @@ -55,8 +55,18 @@ export const publicHostProviders = [ export const publicDBProviders = [aliyun] export const privateCloudProviders = [ - vmware, qingcloud_private, huaweicloud_private, ctyun_private, - openstack, zstack, nutanix, fc, scp, apsara_stack, smartx, proxmox + vmware, + qingcloud_private, + huaweicloud_private, + ctyun_private, + openstack, + zstack, + nutanix, + fc, + scp, + apsara_stack, + smartx, + proxmox ] export const ACCOUNT_PROVIDER_ATTRS_MAP = { diff --git a/src/views/settings/Storage/ObjectStorageCreateUpdate.vue b/src/views/settings/Storage/ObjectStorageCreateUpdate.vue index d49d308b9..fb3418d8e 100644 --- a/src/views/settings/Storage/ObjectStorageCreateUpdate.vue +++ b/src/views/settings/Storage/ObjectStorageCreateUpdate.vue @@ -11,7 +11,7 @@ import { GenericCreateUpdatePage } from '@/layout/components' import { STORAGE_TYPE_META_MAP } from '@/views/sessions/const' import { UploadSecret } from '@/components/Form/FormFields' -import { encryptPassword } from '@/utils/secure' +import { encryptPassword } from '@/utils/session-encrypt' export default { name: 'ReplayStorageUpdate', @@ -51,19 +51,19 @@ export default { fields: storageTypeMeta.meta, fieldsMeta: { SFTP_PASSWORD: { - hidden: (formValue) => formValue.STP_SECRET_TYPE !== 'password' + hidden: formValue => formValue.STP_SECRET_TYPE !== 'password' }, STP_PRIVATE_KEY: { component: UploadSecret, - hidden: (formValue) => formValue.STP_SECRET_TYPE !== 'ssh_key' + hidden: formValue => formValue.STP_SECRET_TYPE !== 'ssh_key' }, STP_PASSPHRASE: { - hidden: (formValue) => formValue.STP_SECRET_TYPE !== 'ssh_key' + hidden: formValue => formValue.STP_SECRET_TYPE !== 'ssh_key' } } }, is_default: { - hidden: (formValue) => formValue.type === 'sftp' + hidden: formValue => formValue.type === 'sftp' }, comment: { component: 'el-input', @@ -102,6 +102,4 @@ export default { } - + diff --git a/yarn.lock b/yarn.lock index 97fe06133..7cadc5d73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8577,6 +8577,11 @@ js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@^3.7.0, js-yaml@^3.9. argparse "^1.0.7" esprima "^4.0.0" +jsbn@^1.1.0: + version "1.1.0" + resolved "https://registry.npmmirror.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.npmmirror.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -12849,6 +12854,13 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" +sm-crypto@^0.4.0: + version "0.4.0" + resolved "https://registry.npmmirror.com/sm-crypto/-/sm-crypto-0.4.0.tgz#13917f5b41e8428d764e9e6934840fdede6a4022" + integrity sha512-OexH2V1EqmhXuOIPGoCl55OjMF0wwPUM/zhUjT0Q6vHBeopSRvTNRy76/1eRoFs3VBKt39hdFnxwpFmooHYa2A== + dependencies: + jsbn "^1.1.0" + smart-buffer@^4.2.0: version "4.2.0" resolved "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"