@liujitcn/kratos-admin-core 0.0.66 → 0.0.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package/declarations/src/api/base/v1/login.d.ts +1 -3
- package/dist/package/declarations/src/api/base/v1/secret_crypto.d.ts +9 -0
- package/dist/package/declarations/src/components/Dialog/PasswordVerifyDialog.vue.d.ts +3 -7
- package/dist/package/declarations/src/components/ProForm/components/ProFormItem.vue.d.ts +6 -6
- package/dist/package/declarations/src/components/ProForm/components/SecretInput.vue.d.ts +23 -0
- package/dist/package/declarations/src/components/ProForm/interface/index.d.ts +14 -1
- package/dist/package/declarations/src/locales/generated.d.ts +32 -0
- package/dist/package/declarations/src/rpc/base/v1/login.d.ts +2 -39
- package/dist/package/declarations/src/rpc/base/v1/mfa.d.ts +3 -3
- package/dist/package/declarations/src/rpc/base/v1/oauth.d.ts +2 -2
- package/dist/package/declarations/src/rpc/base/v1/secret_crypto.d.ts +37 -0
- package/dist/package/declarations/src/rpc/common/v1/types.d.ts +4 -4
- package/dist/package/declarations/src/rpc/system/admin/v1/auth.d.ts +3 -3
- package/dist/package/declarations/src/security.d.ts +1 -1
- package/dist/package/declarations/src/utils/secretCrypto.d.ts +19 -0
- package/dist/package/src/api/base/v1/login.ts +0 -12
- package/dist/package/src/api/base/v1/secret_crypto.ts +35 -0
- package/dist/package/src/components/Dialog/PasswordVerifyDialog.vue +4 -8
- package/dist/package/src/components/ProForm/components/ProFormItem.vue +24 -1
- package/dist/package/src/components/ProForm/components/SecretInput.vue +117 -0
- package/dist/package/src/components/ProForm/index.vue +1 -0
- package/dist/package/src/components/ProForm/interface/index.ts +15 -0
- package/dist/package/src/components/ProTable/index.vue +8 -1
- package/dist/package/src/locales/README.md +1 -1
- package/dist/package/src/locales/en-US.json +9 -1
- package/dist/package/src/locales/ja-JP.json +9 -1
- package/dist/package/src/locales/zh-CN.json +9 -1
- package/dist/package/src/locales/zh-TW.json +9 -1
- package/dist/package/src/rpc/base/v1/login.ts +2 -42
- package/dist/package/src/rpc/base/v1/mfa.ts +3 -3
- package/dist/package/src/rpc/base/v1/oauth.ts +2 -2
- package/dist/package/src/rpc/base/v1/secret_crypto.ts +49 -0
- package/dist/package/src/rpc/common/v1/types.ts +4 -4
- package/dist/package/src/rpc/system/admin/v1/auth.ts +3 -3
- package/dist/package/src/security.ts +1 -1
- package/dist/package/src/utils/request.ts +3 -3
- package/dist/package/src/utils/{passwordCrypto.ts → secretCrypto.ts} +112 -50
- package/dist/package/src/views/demo/components/DemoFormDialog.vue +3 -3
- package/dist/package/src/views/demo/table/index.vue +2 -2
- package/dist/package/src/views/demo/tree-grid/index.vue +1 -1
- package/dist/package/src/views/login/components/LoginForm.vue +2 -2
- package/package.json +6 -2
- package/dist/package/declarations/src/utils/passwordCrypto.d.ts +0 -6
|
@@ -1,13 +1,112 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
1
|
+
import { defSecretCryptoService } from "../api/base/v1/secret_crypto";
|
|
2
|
+
import type { GetSecretPublicKeyResponse } from "../rpc/base/v1/secret_crypto";
|
|
3
|
+
import type { SecretCrypto } from "../rpc/common/v1/types";
|
|
4
|
+
import type { ProFormField } from "../components/ProForm/interface";
|
|
4
5
|
import { t } from "../locales";
|
|
5
6
|
import * as portableCrypto from "asmcrypto.js";
|
|
6
7
|
|
|
7
8
|
type PortableCrypto = typeof import("asmcrypto.js");
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
export
|
|
10
|
+
/** 获取敏感字段一次性临时公钥,同一次提交的多个密文字段共用该公钥。 */
|
|
11
|
+
export async function fetchSecretPublicKey(): Promise<GetSecretPublicKeyResponse> {
|
|
12
|
+
return defSecretCryptoService.GetSecretPublicKey({});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** encodeSecretFields 在提交前把声明 secret 的字段统一加密为 SecretCrypto,返回模型浅克隆且不改动原表单。 */
|
|
16
|
+
export async function encodeSecretFields(
|
|
17
|
+
fields: ProFormField[],
|
|
18
|
+
model: Record<string, any>
|
|
19
|
+
): Promise<Record<string, any>> {
|
|
20
|
+
const secretFields = fields.filter(field => field.secret);
|
|
21
|
+
const filled = secretFields
|
|
22
|
+
.map(field => ({ field, value: getByPath(model, field.prop) }))
|
|
23
|
+
.filter((item): item is { field: ProFormField; value: string } => typeof item.value === "string" && item.value.trim() !== "");
|
|
24
|
+
if (secretFields.length === 0) {
|
|
25
|
+
return { ...model };
|
|
26
|
+
}
|
|
27
|
+
const publicKey = filled.length > 0 ? await fetchSecretPublicKey() : null;
|
|
28
|
+
const payload = { ...model };
|
|
29
|
+
for (const item of secretFields) {
|
|
30
|
+
const value = getByPath(model, item.prop);
|
|
31
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
32
|
+
payload[item.prop] = await encryptWithPublicKey(publicKey!, value);
|
|
33
|
+
} else {
|
|
34
|
+
payload[item.prop] = undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return payload;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 按点路径读取对象字段值。 */
|
|
41
|
+
function getByPath(model: Record<string, any>, path: string) {
|
|
42
|
+
return path.split(".").reduce((current, key) => (current == null ? current : current[key]), model as any);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 使用已获取的临时公钥加密单个敏感值,供一次取钥加密多个字段的场景复用。 */
|
|
46
|
+
export async function encryptWithPublicKey(publicKey: GetSecretPublicKeyResponse, value: string): Promise<SecretCrypto> {
|
|
47
|
+
if (!value) {
|
|
48
|
+
throw new Error(t("core.password.required"));
|
|
49
|
+
}
|
|
50
|
+
const cryptoApi = globalThis.crypto;
|
|
51
|
+
const isSecureContext = typeof window === "undefined" || window.isSecureContext;
|
|
52
|
+
if (!cryptoApi?.subtle || !isSecureContext) {
|
|
53
|
+
return encryptPortableSecret(portableCrypto, value, publicKey);
|
|
54
|
+
}
|
|
55
|
+
const subtleCrypto = getSubtleCrypto();
|
|
56
|
+
const publicKeyKey = await subtleCrypto.subtle.importKey(
|
|
57
|
+
"spki",
|
|
58
|
+
pemToArrayBuffer(publicKey.public_key),
|
|
59
|
+
{ name: "RSA-OAEP", hash: "SHA-256" },
|
|
60
|
+
false,
|
|
61
|
+
["encrypt"]
|
|
62
|
+
);
|
|
63
|
+
const aesKey = await subtleCrypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt"]);
|
|
64
|
+
const rawAesKey = await subtleCrypto.subtle.exportKey("raw", aesKey);
|
|
65
|
+
const iv = subtleCrypto.getRandomValues(new Uint8Array(12));
|
|
66
|
+
const ciphertext = await subtleCrypto.subtle.encrypt({ name: "AES-GCM", iv }, aesKey, new TextEncoder().encode(value));
|
|
67
|
+
const encryptedKey = await subtleCrypto.subtle.encrypt({ name: "RSA-OAEP" }, publicKeyKey, rawAesKey);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
key_id: publicKey.key_id,
|
|
71
|
+
nonce: publicKey.nonce,
|
|
72
|
+
algorithm: publicKey.algorithm,
|
|
73
|
+
encrypted_key: arrayBufferToBase64(encryptedKey),
|
|
74
|
+
iv: arrayBufferToBase64(iv.buffer),
|
|
75
|
+
text: arrayBufferToBase64(ciphertext)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 加密单个敏感字段值,自动获取一次性临时公钥。 */
|
|
80
|
+
export async function encryptSecret(value: string): Promise<SecretCrypto> {
|
|
81
|
+
return encryptWithPublicKey(await fetchSecretPublicKey(), value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** SecretRevealHandler 按资源标识与字段名查看密钥明文的实现。 */
|
|
85
|
+
export type SecretRevealHandler = (resource: string, id: number, field: string) => Promise<string>;
|
|
86
|
+
|
|
87
|
+
let secretRevealHandler: SecretRevealHandler | null = null;
|
|
88
|
+
|
|
89
|
+
/** registerSecretRevealHandler 注册密钥明文查看实现,由业务模块在启动时注入。 */
|
|
90
|
+
export function registerSecretRevealHandler(handler: SecretRevealHandler) {
|
|
91
|
+
secretRevealHandler = handler;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** revealSecret 查看注册过的密钥字段明文。 */
|
|
95
|
+
export async function revealSecret(resource: string, id: number, field: string): Promise<string> {
|
|
96
|
+
if (!secretRevealHandler) {
|
|
97
|
+
throw new Error(t("core.password.reveal_unsupported"));
|
|
98
|
+
}
|
|
99
|
+
return secretRevealHandler(resource, id, field);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 加密密码字段:去除首尾空白且不允许为空。 */
|
|
103
|
+
export async function encryptPassword(value: string): Promise<SecretCrypto> {
|
|
104
|
+
const plainPassword = value.trim();
|
|
105
|
+
if (!plainPassword) {
|
|
106
|
+
throw new Error(t("core.password.required"));
|
|
107
|
+
}
|
|
108
|
+
return encryptSecret(plainPassword);
|
|
109
|
+
}
|
|
11
110
|
|
|
12
111
|
/** 将 PEM 公钥转换为二进制 DER 数据。 */
|
|
13
112
|
function pemToArrayBuffer(pem: string) {
|
|
@@ -129,7 +228,7 @@ function generateMgf1Mask(cryptoApi: PortableCrypto, seed: Uint8Array, length: n
|
|
|
129
228
|
return mask;
|
|
130
229
|
}
|
|
131
230
|
|
|
132
|
-
/** 生成纯 JavaScript
|
|
231
|
+
/** 生成纯 JavaScript 加密所需的随机字节。 */
|
|
133
232
|
function getPortableRandomBytes(length: number) {
|
|
134
233
|
const cryptoApi = globalThis.crypto;
|
|
135
234
|
if (!cryptoApi?.getRandomValues) {
|
|
@@ -138,15 +237,15 @@ function getPortableRandomBytes(length: number) {
|
|
|
138
237
|
return cryptoApi.getRandomValues(new Uint8Array(length));
|
|
139
238
|
}
|
|
140
239
|
|
|
141
|
-
/** 使用纯 JavaScript 实现兼容缺少 WebCrypto
|
|
142
|
-
async function
|
|
240
|
+
/** 使用纯 JavaScript 实现兼容缺少 WebCrypto 环境的敏感值加密协议。 */
|
|
241
|
+
async function encryptPortableSecret(
|
|
143
242
|
cryptoApi: PortableCrypto,
|
|
144
|
-
|
|
145
|
-
publicKeyResponse:
|
|
146
|
-
): Promise<
|
|
243
|
+
value: string,
|
|
244
|
+
publicKeyResponse: GetSecretPublicKeyResponse
|
|
245
|
+
): Promise<SecretCrypto> {
|
|
147
246
|
const aesKey = getPortableRandomBytes(32);
|
|
148
247
|
const iv = getPortableRandomBytes(12);
|
|
149
|
-
const plaintext = cryptoApi.string_to_bytes(
|
|
248
|
+
const plaintext = cryptoApi.string_to_bytes(value, true);
|
|
150
249
|
const [modulus, exponent] = parsePortableRsaPublicKey(publicKeyResponse.public_key);
|
|
151
250
|
const hash = new cryptoApi.Sha256();
|
|
152
251
|
const keySize = Math.ceil(new cryptoApi.BigNumber(modulus).bitLength / 8);
|
|
@@ -187,43 +286,6 @@ async function encryptPortablePassword(
|
|
|
187
286
|
algorithm: publicKeyResponse.algorithm,
|
|
188
287
|
encrypted_key: arrayBufferToBase64(encryptedKey.slice().buffer),
|
|
189
288
|
iv: arrayBufferToBase64(iv.buffer),
|
|
190
|
-
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/** 加密单个密码字段,返回后端可解析的密码密文。 */
|
|
195
|
-
export async function encryptPassword(password: string, scene: PasswordCryptoScene): Promise<PasswordCrypto> {
|
|
196
|
-
const plainPassword = password.trim();
|
|
197
|
-
if (!plainPassword) {
|
|
198
|
-
throw new Error(t("core.password.required"));
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const publicKeyResponse = await defLoginService.PasswordPublicKey({ scene });
|
|
202
|
-
const cryptoApi = globalThis.crypto;
|
|
203
|
-
const isSecureContext = typeof window === "undefined" || window.isSecureContext;
|
|
204
|
-
if (!cryptoApi?.subtle || !isSecureContext) {
|
|
205
|
-
return encryptPortablePassword(portableCrypto, plainPassword, publicKeyResponse);
|
|
206
|
-
}
|
|
207
|
-
const subtleCrypto = getSubtleCrypto();
|
|
208
|
-
const publicKey = await subtleCrypto.subtle.importKey(
|
|
209
|
-
"spki",
|
|
210
|
-
pemToArrayBuffer(publicKeyResponse.public_key),
|
|
211
|
-
{ name: "RSA-OAEP", hash: "SHA-256" },
|
|
212
|
-
false,
|
|
213
|
-
["encrypt"]
|
|
214
|
-
);
|
|
215
|
-
const aesKey = await subtleCrypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt"]);
|
|
216
|
-
const rawAesKey = await subtleCrypto.subtle.exportKey("raw", aesKey);
|
|
217
|
-
const iv = subtleCrypto.getRandomValues(new Uint8Array(12));
|
|
218
|
-
const ciphertext = await subtleCrypto.subtle.encrypt({ name: "AES-GCM", iv }, aesKey, new TextEncoder().encode(plainPassword));
|
|
219
|
-
const encryptedKey = await subtleCrypto.subtle.encrypt({ name: "RSA-OAEP" }, publicKey, rawAesKey);
|
|
220
|
-
|
|
221
|
-
return {
|
|
222
|
-
key_id: publicKeyResponse.key_id,
|
|
223
|
-
nonce: publicKeyResponse.nonce,
|
|
224
|
-
algorithm: publicKeyResponse.algorithm,
|
|
225
|
-
encrypted_key: arrayBufferToBase64(encryptedKey),
|
|
226
|
-
iv: arrayBufferToBase64(iv.buffer),
|
|
227
|
-
ciphertext: arrayBufferToBase64(ciphertext)
|
|
289
|
+
text: arrayBufferToBase64(ciphertext.slice().buffer)
|
|
228
290
|
};
|
|
229
291
|
}
|
|
@@ -123,13 +123,13 @@ const deptTreeOptions: ProFormOption[] = demoDeptTree.map(dept => ({
|
|
|
123
123
|
|
|
124
124
|
/** 租户静态选项:tenant-select 传入 options 后不再请求租户接口,纯静态可渲染。 */
|
|
125
125
|
const tenantOptions: ProFormOption[] = [
|
|
126
|
-
{ label: "
|
|
127
|
-
{ label: "
|
|
126
|
+
{ label: t("core.demo.form.tenant.main"), value: 1 },
|
|
127
|
+
{ label: t("core.demo.form.tenant.sub"), value: 2 }
|
|
128
128
|
];
|
|
129
129
|
|
|
130
130
|
/** 协作成员穿梭框数据源:value 即选中后的 key。 */
|
|
131
131
|
const memberOptions: ProFormOption[] = demoDeptTree[0].children!.map((dept, index) => ({
|
|
132
|
-
label:
|
|
132
|
+
label: t("core.demo.form.leader_title", { name: dept.name }),
|
|
133
133
|
value: index + 1
|
|
134
134
|
}));
|
|
135
135
|
|
|
@@ -68,7 +68,7 @@ const statusTagOptions = [
|
|
|
68
68
|
];
|
|
69
69
|
|
|
70
70
|
/** select-v2 大数据下拉的静态选项(isShow: false,仅出现在搜索区)。 */
|
|
71
|
-
const tagSearchOptions = Array.from({ length: 50 }, (_, index) => ({ label:
|
|
71
|
+
const tagSearchOptions = Array.from({ length: 50 }, (_, index) => ({ label: t("core.demo.table.tag_item", { index: index + 1 }), value: `tag-${index + 1}` }));
|
|
72
72
|
|
|
73
73
|
/** tree-select 搜索项的静态团队树(isShow: false,仅出现在搜索区)。 */
|
|
74
74
|
const ownerGroupOptions = demoRegionTree;
|
|
@@ -121,7 +121,7 @@ const columns = computed<ColumnProps[]>(() => [
|
|
|
121
121
|
minWidth: 130,
|
|
122
122
|
align: "right",
|
|
123
123
|
cellType: "money",
|
|
124
|
-
moneyProps: { prefix: "¥", suffix: "
|
|
124
|
+
moneyProps: { prefix: "¥", suffix: t("core.demo.table.money_unit") }
|
|
125
125
|
},
|
|
126
126
|
{
|
|
127
127
|
// 多级表头:_children 嵌套的列渲染在“任务情况”分组下
|
|
@@ -180,7 +180,7 @@ function handleCreateChild(parentId?: number) {
|
|
|
180
180
|
function handleFormSubmit(data: Record<string, any>) {
|
|
181
181
|
const node: DemoCatalog = {
|
|
182
182
|
id: ++nextId,
|
|
183
|
-
name: String(data.name ||
|
|
183
|
+
name: String(data.name || t("core.demo.tree.new_dir", { id: nextId })),
|
|
184
184
|
code: `demo-${nextId}`,
|
|
185
185
|
owner: data.visibility === "private" ? "--" : "Demo",
|
|
186
186
|
status: data.enabled ? STATUS_ENABLE : STATUS_DISABLE,
|
|
@@ -217,7 +217,7 @@ import { useKeepAliveStore } from "../../../stores/modules/keepAlive";
|
|
|
217
217
|
import { initDynamicRouter } from "../../../routers/modules/dynamicRouter";
|
|
218
218
|
import { isUnmatchedRoute, navigateTo, resolveFrontendRouteURL } from "../../../utils/router";
|
|
219
219
|
import type { ElForm, FormRules } from "element-plus";
|
|
220
|
-
import {
|
|
220
|
+
import { encryptPassword } from "../../../utils/secretCrypto";
|
|
221
221
|
import { useConfigStore } from "../../../stores/modules/config";
|
|
222
222
|
import { Click as GoCaptchaClick, Rotate as GoCaptchaRotate, Slide as GoCaptchaSlide } from "go-captcha-vue";
|
|
223
223
|
import "go-captcha-vue/dist/style.css";
|
|
@@ -797,7 +797,7 @@ const verifyCaptchaToken = async (captchaCode: string) => {
|
|
|
797
797
|
const submitLogin = async (captchaToken: string) => {
|
|
798
798
|
loading.value = true;
|
|
799
799
|
try {
|
|
800
|
-
const password = await encryptPassword(loginForm.password
|
|
800
|
+
const password = await encryptPassword(loginForm.password);
|
|
801
801
|
const loginRequest: LoginRequest = {
|
|
802
802
|
tenant_code: configStore.showTenantCode ? loginForm.tenant_code : "0000",
|
|
803
803
|
user_name: loginForm.user_name,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liujitcn/kratos-admin-core",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.68",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "kratos-admin frontend runtime and base modules",
|
|
7
7
|
"author": {
|
|
@@ -232,7 +232,11 @@
|
|
|
232
232
|
"types": "./dist/package/declarations/vite.config.d.ts",
|
|
233
233
|
"default": "./vite.config.ts"
|
|
234
234
|
},
|
|
235
|
-
"./package.json": "./package.json"
|
|
235
|
+
"./package.json": "./package.json",
|
|
236
|
+
"./utils/secretCrypto": {
|
|
237
|
+
"types": "./dist/package/declarations/src/utils/secretCrypto.d.ts",
|
|
238
|
+
"default": "./dist/package/src/utils/secretCrypto.ts"
|
|
239
|
+
}
|
|
236
240
|
},
|
|
237
241
|
"files": [
|
|
238
242
|
"dist/package",
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { PasswordCryptoScene } from "../rpc/base/v1/login";
|
|
2
|
-
import type { PasswordCrypto } from "../rpc/common/v1/types";
|
|
3
|
-
export declare const PASSWORD_CRYPTO_SCENE: typeof PasswordCryptoScene;
|
|
4
|
-
export type { PasswordCryptoScene };
|
|
5
|
-
/** 加密单个密码字段,返回后端可解析的密码密文。 */
|
|
6
|
-
export declare function encryptPassword(password: string, scene: PasswordCryptoScene): Promise<PasswordCrypto>;
|