Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Use base image to build the project avoid npm install every time
FROM jumpserver/luna-base:20250609_105214 AS stage-build
FROM jumpserver/luna-base:20260331_081054 AS stage-build

ARG VERSION
ENV VERSION=$VERSION
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"popper.js": "^1.16.1",
"requirejs": "^2.3.6",
"rxjs": "^7.8.1",
"sm-crypto": "^0.4.0",
"tslib": "^2.6.2",
"xterm-theme": "^1.1.0",
"zone.js": "~0.15.0"
Expand Down
154 changes: 117 additions & 37 deletions src/app/utils/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as CryptoJS from 'crypto-js';
import {getCsrfTokenFromCookie, getCookie} from '@app/utils/common';
import {Buffer} from 'buffer';
import {JSEncrypt} from 'jsencrypt';
import { getCookie } from '@app/utils/common';
import { Buffer } from 'buffer';
import { JSEncrypt } from 'jsencrypt';
import { sm2, sm4 } from 'sm-crypto';


export function fillKey(key: string): Buffer|string {
export function fillKey(key: string): Buffer | string {
const KeyLength = 16;
if (key.length > KeyLength) {
key = key.slice(0, KeyLength);
Expand All @@ -17,58 +17,138 @@
return filledKey;
}


export function aesEncrypt(text: string, originKey: string): string {
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);
}

export function aesDecrypt(cipherText: string, originKey: string): string {
const key = CryptoJS.enc.Utf8.parse(fillKey(originKey));
const bytes = CryptoJS.AES.decrypt(cipherText, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.ZeroPadding
});
return CryptoJS.enc.Utf8.stringify(bytes);
function rsaDecrypt(cipher, pkey) {
const jsEncrypt = new JSEncrypt();
jsEncrypt.setPrivateKey(pkey);
return jsEncrypt.decrypt(cipher);
}

export function aesEncryptByCsrf(text: string): string {
const key = getCsrfTokenFromCookie();
if (!key) { console.log('Not found csrf connectToken'); }
return aesEncrypt(text, key);
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);

Check warning on line 53 in src/app/utils/crypto.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.parseInt` over `parseInt`.

See more on https://sonarcloud.io/project/issues?id=jumpserver_luna&issues=AZ1C8rXfdPLGmyS4VlX6&open=AZ1C8rXfdPLGmyS4VlX6&pullRequest=1513
}
return bytes;
}

export function aesDecryptByCsrf(cipherText: string): string {
const key = getCsrfTokenFromCookie();
if (!key) { console.log('Not found csrf connectToken'); }
return aesDecrypt(cipherText, key);
function bytesToBase64(bytes) {
// Uint8Array -> base64(标准 base64)
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);

Check warning on line 62 in src/app/utils/crypto.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String.fromCodePoint()` over `String.fromCharCode()`.

See more on https://sonarcloud.io/project/issues?id=jumpserver_luna&issues=AZ1C8rXgdPLGmyS4VlX8&open=AZ1C8rXgdPLGmyS4VlX8&pullRequest=1513
}

Check warning on line 63 in src/app/utils/crypto.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected a `for-of` loop instead of a `for` loop with this simple iteration.

See more on https://sonarcloud.io/project/issues?id=jumpserver_luna&issues=AZ1C8rXgdPLGmyS4VlX7&open=AZ1C8rXgdPLGmyS4VlX7&pullRequest=1513
return btoa(binary);
}

export function rsaEncrypt(text, pubKey) {
const jsEncrypt = new JSEncrypt();
jsEncrypt.setPublicKey(pubKey);
return jsEncrypt.encrypt(text);
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.replace(/"/g, '').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 '';
}
const aesKey = (Math.random() + 1).toString(36).substring(2);
// public key 是 base64 存储的
let rsaPublicKeyText = getCookie('jms_public_key');
if (!rsaPublicKeyText) {
let publicKeyText = getCookie('jms_public_key');
if (!publicKeyText) {
console.log('publicKeyText is empty');
return password;
}
rsaPublicKeyText = rsaPublicKeyText
.replace('"', '')
.replace('"', '');
const rsaPublicKey = atob(rsaPublicKeyText);
const keyCipher = rsaEncrypt(aesKey, rsaPublicKey);
const passwordCipher = aesEncrypt(password, aesKey);
return `${keyCipher}:${passwordCipher}`;
publicKeyText = publicKeyText.replace(/"/g, '');
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;
}
9 changes: 8 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6296,7 +6296,7 @@ js-yaml@^4.1.0:
dependencies:
argparse "^2.0.1"

jsbn@1.1.0:
jsbn@1.1.0, jsbn@^1.1.0:
version "1.1.0"
resolved "https://registry.npmmirror.com/jsbn/-/jsbn-1.1.0.tgz"
integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==
Expand Down Expand Up @@ -8418,6 +8418,13 @@ slice-ansi@^7.1.0:
ansi-styles "^6.2.1"
is-fullwidth-code-point "^5.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"
Expand Down