@l1yp/file-viewer 0.1.4 → 0.1.6
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/README.md +57 -6
- package/dist/assets/step.worker-CfDTYOf_.js.map +1 -0
- package/dist/components/AnsiText.d.ts +47 -0
- package/dist/components/AnsiText.js +41 -0
- package/dist/components/AnsiText.js.map +1 -0
- package/dist/components/CertViewer.js +1 -1
- package/dist/components/CertViewer.js.map +1 -1
- package/dist/components/CertViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/CodeView.js +1 -1
- package/dist/components/CodeView.js.map +1 -1
- package/dist/components/CodeView.vue_vue_type_script_setup_true_lang.js +80 -71
- package/dist/components/CodeView.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/DocxEmbeddedFiles.js +9 -0
- package/dist/components/DocxEmbeddedFiles.js.map +1 -0
- package/dist/components/DocxEmbeddedFiles.vue.d.ts +15 -0
- package/dist/components/DocxEmbeddedFiles.vue_vue_type_script_setup_true_lang.js +69 -0
- package/dist/components/DocxEmbeddedFiles.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/DocxNavigationPane.js +1 -1
- package/dist/components/DocxNavigationPane.js.map +1 -1
- package/dist/components/DocxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/DocxViewer.js +1 -1
- package/dist/components/DocxViewer.js.map +1 -1
- package/dist/components/DocxViewer.vue.d.ts +1 -1
- package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js +182 -50
- package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/EplViewer.js +1 -1
- package/dist/components/EplViewer.js.map +1 -1
- package/dist/components/EplViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/FileViewerBody.js +1 -1
- package/dist/components/FileViewerBody.js.map +1 -1
- package/dist/components/FileViewerBody.vue.d.ts +1 -0
- package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js +292 -281
- package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/FileViewerPanel.js +1 -1
- package/dist/components/FileViewerPanel.js.map +1 -1
- package/dist/components/FileViewerPanel.vue.d.ts +6 -1
- package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js +53 -51
- package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/HarViewer.js +1 -1
- package/dist/components/HarViewer.js.map +1 -1
- package/dist/components/HarViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/JksViewer.js +1 -1
- package/dist/components/JksViewer.js.map +1 -1
- package/dist/components/JksViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/LogViewer.js +1 -1
- package/dist/components/LogViewer.js.map +1 -1
- package/dist/components/LogViewer.vue.d.ts +3 -1
- package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js +284 -280
- package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/ModelViewModes.js +9 -0
- package/dist/components/ModelViewModes.js.map +1 -0
- package/dist/components/ModelViewModes.vue.d.ts +11 -0
- package/dist/components/ModelViewModes.vue_vue_type_script_setup_true_lang.js +49 -0
- package/dist/components/ModelViewModes.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/ModelViewer.js +1 -1
- package/dist/components/ModelViewer.js.map +1 -1
- package/dist/components/ModelViewer.vue.d.ts +2 -1
- package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js +183 -96
- package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/PdfNavigationPane.js +1 -1
- package/dist/components/PdfNavigationPane.js.map +1 -1
- package/dist/components/PdfNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/PdfViewer.js +1 -1
- package/dist/components/PdfViewer.js.map +1 -1
- package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/PptxNavigationPane.js +1 -1
- package/dist/components/PptxNavigationPane.js.map +1 -1
- package/dist/components/PptxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/PsdViewer.js +1 -1
- package/dist/components/PsdViewer.js.map +1 -1
- package/dist/components/PsdViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/SqliteViewer.js +1 -1
- package/dist/components/SqliteViewer.js.map +1 -1
- package/dist/components/SqliteViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/ViewerLoadError.js +9 -0
- package/dist/components/ViewerLoadError.js.map +1 -0
- package/dist/components/ViewerLoadError.vue.d.ts +17 -0
- package/dist/components/ViewerLoadError.vue_vue_type_script_setup_true_lang.js +58 -0
- package/dist/components/ViewerLoadError.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/XlsxChart.js +1 -1
- package/dist/components/XlsxChart.js.map +1 -1
- package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js +9 -9
- package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/XlsxImageLayer.js +9 -0
- package/dist/components/XlsxImageLayer.js.map +1 -0
- package/dist/components/XlsxImageLayer.vue.d.ts +9 -0
- package/dist/components/XlsxImageLayer.vue_vue_type_script_setup_true_lang.js +46 -0
- package/dist/components/XlsxImageLayer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/XlsxViewer.js +1 -1
- package/dist/components/XlsxViewer.js.map +1 -1
- package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js +230 -216
- package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/epl/EplResource.js +1 -1
- package/dist/components/epl/EplResource.js.map +1 -1
- package/dist/components/epl/EplResource.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/optionalViewer.d.ts +3 -2
- package/dist/components/optionalViewer.js +6 -3
- package/dist/components/optionalViewer.js.map +1 -1
- package/dist/composables/useFileViewer.d.ts +11 -3
- package/dist/composables/useFileViewer.js.map +1 -1
- package/dist/composables/useTeleportTheme.d.ts +7 -0
- package/dist/composables/useTeleportTheme.js +42 -0
- package/dist/composables/useTeleportTheme.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +29 -28
- package/dist/lib/ansi.d.ts +44 -0
- package/dist/lib/ansi.js +224 -0
- package/dist/lib/ansi.js.map +1 -0
- package/dist/lib/cfb.d.ts +37 -0
- package/dist/lib/cfb.js +112 -0
- package/dist/lib/cfb.js.map +1 -0
- package/dist/lib/docxEmbeddedDom.d.ts +22 -0
- package/dist/lib/docxEmbeddedDom.js +128 -0
- package/dist/lib/docxEmbeddedDom.js.map +1 -0
- package/dist/lib/docxEmbeddings.d.ts +41 -0
- package/dist/lib/docxEmbeddings.js +226 -0
- package/dist/lib/docxEmbeddings.js.map +1 -0
- package/dist/lib/docxTheme.d.ts +6 -0
- package/dist/lib/docxTheme.js +64 -0
- package/dist/lib/docxTheme.js.map +1 -0
- package/dist/lib/epl/code/structures.js +82 -0
- package/dist/lib/epl/code/structures.js.map +1 -0
- package/dist/lib/epl/code/variable.js +16 -0
- package/dist/lib/epl/code/variable.js.map +1 -0
- package/dist/lib/epl/eplDocument.js +28 -22
- package/dist/lib/epl/eplDocument.js.map +1 -1
- package/dist/lib/epl/index.d.ts +2 -1
- package/dist/lib/epl/index.js.map +1 -1
- package/dist/lib/epl/sections/code.js +37 -0
- package/dist/lib/epl/sections/code.js.map +1 -0
- package/dist/lib/epl/sections/esystemInfo.js +29 -0
- package/dist/lib/epl/sections/esystemInfo.js.map +1 -0
- package/dist/lib/epl/sections/losable.js +33 -0
- package/dist/lib/epl/sections/losable.js.map +1 -0
- package/dist/lib/epl/sections/projectConfig.js +39 -0
- package/dist/lib/epl/sections/projectConfig.js.map +1 -0
- package/dist/lib/epl/sections/resource.js +84 -0
- package/dist/lib/epl/sections/resource.js.map +1 -0
- package/dist/lib/fileType.d.ts +3 -1
- package/dist/lib/fileType.js +150 -137
- package/dist/lib/fileType.js.map +1 -1
- package/dist/lib/modelAppearance.d.ts +7 -0
- package/dist/lib/modelAppearance.js +41 -0
- package/dist/lib/modelAppearance.js.map +1 -0
- package/dist/lib/msi.js +62 -154
- package/dist/lib/msi.js.map +1 -1
- package/dist/lib/olePackage.d.ts +8 -0
- package/dist/lib/olePackage.js +37 -0
- package/dist/lib/olePackage.js.map +1 -0
- package/dist/lib/pptxFirstSlide.d.ts +11 -0
- package/dist/lib/pptxFirstSlide.js +34 -0
- package/dist/lib/pptxFirstSlide.js.map +1 -0
- package/dist/lib/step.d.ts +10 -0
- package/dist/lib/step.js +35 -0
- package/dist/lib/step.js.map +1 -0
- package/dist/lib/step.worker.d.ts +1 -0
- package/dist/lib/step.worker.js +18 -0
- package/dist/lib/step.worker.js.map +1 -0
- package/dist/lib/stepObject.d.ts +4 -0
- package/dist/lib/stepObject.js +52 -0
- package/dist/lib/stepObject.js.map +1 -0
- package/dist/lib/stepTypes.d.ts +60 -0
- package/dist/lib/viewerKind.js +10 -5
- package/dist/lib/viewerKind.js.map +1 -1
- package/dist/lib/xlsx.d.ts +1 -1
- package/dist/lib/xlsx.js +106 -89
- package/dist/lib/xlsx.js.map +1 -1
- package/dist/lib/xlsxCharts.js +71 -119
- package/dist/lib/xlsxCharts.js.map +1 -1
- package/dist/lib/xlsxImages.d.ts +43 -0
- package/dist/lib/xlsxImages.js +114 -0
- package/dist/lib/xlsxImages.js.map +1 -0
- package/dist/lib/xlsxParts.d.ts +17 -0
- package/dist/lib/xlsxParts.js +86 -0
- package/dist/lib/xlsxParts.js.map +1 -0
- package/dist/style.css +1 -1
- package/package.json +8 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HarViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/HarViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport {\n type DecodedBody,\n type HarEntry,\n type HarNV,\n decodeContent,\n decodePostData,\n fmtMs,\n fmtSize,\n parseHar,\n parseQuery,\n prettyBody,\n statusClass,\n toCurl,\n} from '@/lib/har'\nimport { highlightCode } from '@/lib/highlight'\nimport { formatBytes } from '@/lib/format'\n\n/**\n * Side-viewer HAR (`.har`) renderer. Fetches the archive text and parses the HTTP Archive entirely in-browser\n * (hand-rolled, see lib/har.ts) — no third-party. Lays out a master-detail vertical split tuned for the narrow\n * side pane: a filterable request list on top, and the selected request's detail (general / headers / query /\n * cookies / request + response body) below. Bodies are decoded (standard base64 + the «base64 but undeclared»\n * case some tools emit); images preview from a data URI, binaries show a hex dump.\n *\n * Safety: a HAR body is untrusted upstream content. We never execute it — text bodies render as escaped,\n * optionally syntax-highlighted source; image bodies render through a data-URI `<img>` (an `<img>` never runs\n * scripts, so even an SVG body is inert). No body is fetched as a live resource.\n */\nconst props = defineProps<{\n /** Download URL of the .har bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\n\ninterface HarState {\n entries: HarEntry[]\n creator: string\n version: string\n}\nconst doc = ref<HarState | null>(null)\nconst selected = ref<HarEntry | null>(null)\nconst query = ref('')\n\ntype DetailTab =\n | 'general'\n | 'reqHeaders'\n | 'resHeaders'\n | 'query'\n | 'cookies'\n | 'reqBody'\n | 'resBody'\nconst tab = ref<DetailTab>('general')\n\n// A HAR can be large (megabytes of inlined bodies); gate very big ones behind a confirm before parsing.\nconst SIZE_LIMIT = 25 * 1024 * 1024\n\nconst CATEGORY_LABELS: Record<string, string> = {\n doc: '文档',\n xhr: 'XHR',\n js: 'JS',\n css: 'CSS',\n img: '图片',\n font: '字体',\n media: '媒体',\n ws: 'WS',\n other: '其它',\n}\n\n// Category quick-filter: 'all' or a HarCategory; only categories present in the doc are offered.\nconst catFilter = ref<string>('all')\nconst presentCategories = computed(() => {\n const seen = new Set<string>()\n for (const e of doc.value?.entries ?? []) seen.add(e.category)\n return Object.keys(CATEGORY_LABELS).filter((c) => seen.has(c))\n})\n\nconst filtered = computed(() => {\n const list = doc.value?.entries ?? []\n const q = query.value.trim().toLowerCase()\n const cat = catFilter.value\n return list.filter((e) => {\n if (cat !== 'all' && e.category !== cat) return false\n if (!q) return true\n return (\n e.url.toLowerCase().includes(q) ||\n e.method.toLowerCase().includes(q) ||\n String(e.status).includes(q) ||\n e.type.toLowerCase().includes(q)\n )\n })\n})\n\n// Last path segment (or host) as the row's headline; the host shows as the dim sub-line.\nfunction entryName(e: HarEntry): string {\n const seg = e.pathname.split('/').filter(Boolean).pop()\n return seg || e.host || e.pathname || e.url\n}\n\nfunction pick(e: HarEntry): void {\n selected.value = e\n // Reset to a tab that has content: prefer general; it's always present.\n tab.value = 'general'\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n selected.value = null\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const text = await res.text()\n const parsed = parseHar(text)\n doc.value = { entries: parsed.entries, creator: parsed.creator, version: parsed.version }\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nwatch(() => props.url, load, { immediate: true })\n\n/* ===== detail derivations ===== */\n\nconst reqHeaders = computed<HarNV[]>(() => selected.value?.raw.request.headers ?? [])\nconst resHeaders = computed<HarNV[]>(() => selected.value?.raw.response.headers ?? [])\nconst queryParams = computed<HarNV[]>(() => {\n const e = selected.value\n if (!e) return []\n // Prefer the explicit queryString; fall back to parsing the URL's search.\n const qs = e.raw.request.queryString\n if (qs.length) return qs\n const i = e.url.indexOf('?')\n return i >= 0 ? parseQuery(e.url.slice(i)) : []\n})\nconst cookies = computed(() => {\n const e = selected.value\n if (!e) return { req: [], res: [] }\n return { req: e.raw.request.cookies, res: e.raw.response.cookies }\n})\nconst hasCookies = computed(() => cookies.value.req.length + cookies.value.res.length > 0)\n\nconst reqBody = computed<DecodedBody | null>(() => {\n const e = selected.value\n if (!e) return null\n const b = decodePostData(e.raw.request.postData, 'auto')\n return b.kind === 'empty' ? null : b\n})\nconst resBody = computed<DecodedBody | null>(() => {\n const e = selected.value\n if (!e) return null\n const b = decodeContent(e.raw.response.content, 'auto')\n return b.kind === 'empty' ? null : b\n})\n\n// General info rows for the selected entry.\nconst generalRows = computed<HarNV[]>(() => {\n const e = selected.value\n if (!e) return []\n const r = e.raw\n const rows: HarNV[] = [\n { name: '请求 URL', value: e.url },\n { name: '方法', value: e.method },\n { name: '状态', value: e.status ? `${e.status} ${e.statusText}`.trim() : '(无响应)' },\n { name: '协议', value: r.response.httpVersion || r.request.httpVersion || '' },\n { name: '远端 IP', value: e.serverIP },\n { name: '响应类型', value: e.mime || '' },\n { name: '响应大小', value: fmtSize(e.size) },\n { name: '请求体大小', value: e.reqSize ? fmtSize(e.reqSize) : '' },\n { name: '耗时', value: fmtMs(e.time) },\n { name: '开始时间', value: e.startedISO },\n ]\n if (r.response.redirectURL) rows.push({ name: '重定向', value: r.response.redirectURL })\n return rows.filter((row) => row.value)\n})\n\n/** Render a decoded body to HTML for the <pre>: pretty-print JSON, syntax-highlight when we have a language. */\nfunction bodyHtml(body: DecodedBody): string {\n const { text, pretty } = prettyBody(body.text, body.mime)\n const lang = pretty ? 'json' : bodyLangFor(body.mime)\n return lang ? highlightCode(text, lang) : escapeHtml(text)\n}\nfunction bodyLangFor(mime: string): string {\n const m = mime.toLowerCase()\n if (m.includes('javascript') || m.includes('ecmascript')) return 'javascript'\n if (m === 'text/css') return 'css'\n if (m === 'text/html' || m.includes('xhtml')) return 'xml'\n if (m.includes('json')) return 'json'\n if (m.includes('svg') || m.includes('xml')) return 'xml'\n return ''\n}\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\n}\n\n/* ===== copy helpers ===== */\n\nconst copiedKey = ref('')\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyText(text: string, key: string): Promise<void> {\n try {\n await navigator.clipboard?.writeText(text)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copiedKey.value = key\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copiedKey.value = ''), 1400)\n}\nfunction copyCurl(): void {\n if (selected.value) void copyText(toCurl(selected.value), 'curl')\n}\nonBeforeUnmount(() => clearTimeout(copyTimer))\n</script>\n\n<template>\n <div class=\"har\">\n <p v-if=\"status === 'loading'\" class=\"har__msg\">正在解析 HAR…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"har__msg har__gate\">\n <p>该 HAR 较大({{ formatBytes(props.size) }}),将在浏览器内解析。是否继续?</p>\n <button type=\"button\" class=\"har__btn\" @click=\"doLoad\">解析并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"har__msg har__msg--error\">\n <p>解析失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"har__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <template v-else-if=\"status === 'ready' && doc\">\n <!-- toolbar: filter + category chips + count -->\n <div class=\"har__bar\">\n <input\n v-model=\"query\"\n class=\"har__filter\"\n type=\"text\"\n placeholder=\"筛选 URL / 方法 / 状态…\"\n />\n <div class=\"har__cats\">\n <button\n type=\"button\"\n class=\"har__cat\"\n :class=\"{ 'har__cat--on': catFilter === 'all' }\"\n @click=\"catFilter = 'all'\"\n >\n 全部 {{ doc.entries.length }}\n </button>\n <button\n v-for=\"c in presentCategories\"\n :key=\"c\"\n type=\"button\"\n class=\"har__cat\"\n :class=\"[`har__cat--${c}`, { 'har__cat--on': catFilter === c }]\"\n @click=\"catFilter = c\"\n >\n {{ CATEGORY_LABELS[c] }}\n </button>\n </div>\n </div>\n\n <!-- master: request list -->\n <div class=\"har__list\" :class=\"{ 'har__list--split': selected }\">\n <p v-if=\"!filtered.length\" class=\"har__empty\">没有匹配的请求。</p>\n <button\n v-for=\"e in filtered\"\n :key=\"e.index\"\n type=\"button\"\n class=\"har__row\"\n :class=\"{ 'har__row--on': selected?.index === e.index }\"\n :title=\"e.url\"\n @click=\"pick(e)\"\n >\n <span class=\"har__method\" :class=\"`m-${e.method.toLowerCase()}`\">{{ e.method }}</span>\n <span class=\"har__status\" :class=\"statusClass(e.status)\">{{ e.status || '—' }}</span>\n <span class=\"har__name\">\n <span class=\"har__name-main\">{{ entryName(e) }}</span>\n <span class=\"har__name-sub\">{{ e.host }}</span>\n </span>\n <span class=\"har__type\">{{ e.type }}</span>\n <span class=\"har__size\">{{ fmtSize(e.size) }}</span>\n <span class=\"har__time\">{{ fmtMs(e.time) }}</span>\n </button>\n </div>\n\n <!-- detail: tabs + content -->\n <section v-if=\"selected\" class=\"har__detail\">\n <div class=\"har__tabs\">\n <button\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'general' }\"\n @click=\"tab = 'general'\"\n >\n 概览\n </button>\n <button\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'reqHeaders' }\"\n @click=\"tab = 'reqHeaders'\"\n >\n 请求头 <span class=\"har__tab-n\">{{ reqHeaders.length }}</span>\n </button>\n <button\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'resHeaders' }\"\n @click=\"tab = 'resHeaders'\"\n >\n 响应头 <span class=\"har__tab-n\">{{ resHeaders.length }}</span>\n </button>\n <button\n v-if=\"queryParams.length\"\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'query' }\"\n @click=\"tab = 'query'\"\n >\n 查询 <span class=\"har__tab-n\">{{ queryParams.length }}</span>\n </button>\n <button\n v-if=\"hasCookies\"\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'cookies' }\"\n @click=\"tab = 'cookies'\"\n >\n Cookie\n </button>\n <button\n v-if=\"reqBody\"\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'reqBody' }\"\n @click=\"tab = 'reqBody'\"\n >\n 请求体\n </button>\n <button\n v-if=\"resBody\"\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'resBody' }\"\n @click=\"tab = 'resBody'\"\n >\n 响应体\n </button>\n </div>\n\n <div class=\"har__pane\">\n <!-- general -->\n <template v-if=\"tab === 'general'\">\n <dl class=\"har__kv\">\n <div v-for=\"row in generalRows\" :key=\"row.name\" class=\"har__kv-row\">\n <dt>{{ row.name }}</dt>\n <dd>{{ row.value }}</dd>\n </div>\n </dl>\n <button type=\"button\" class=\"har__btn har__btn--sm\" @click=\"copyCurl\">\n {{ copiedKey === 'curl' ? '已复制' : '复制为 cURL' }}\n </button>\n </template>\n\n <!-- request headers -->\n <dl v-else-if=\"tab === 'reqHeaders'\" class=\"har__kv har__kv--mono\">\n <div v-for=\"(h, i) in reqHeaders\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ h.name }}</dt>\n <dd>{{ h.value }}</dd>\n </div>\n </dl>\n\n <!-- response headers -->\n <dl v-else-if=\"tab === 'resHeaders'\" class=\"har__kv har__kv--mono\">\n <div v-for=\"(h, i) in resHeaders\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ h.name }}</dt>\n <dd>{{ h.value }}</dd>\n </div>\n </dl>\n\n <!-- query string -->\n <dl v-else-if=\"tab === 'query'\" class=\"har__kv har__kv--mono\">\n <div v-for=\"(p, i) in queryParams\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ p.name }}</dt>\n <dd>{{ p.value }}</dd>\n </div>\n </dl>\n\n <!-- cookies -->\n <template v-else-if=\"tab === 'cookies'\">\n <template v-if=\"cookies.req.length\">\n <h4 class=\"har__sub\">请求 Cookie</h4>\n <dl class=\"har__kv har__kv--mono\">\n <div v-for=\"(c, i) in cookies.req\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ c.name }}</dt>\n <dd>{{ c.value }}</dd>\n </div>\n </dl>\n </template>\n <template v-if=\"cookies.res.length\">\n <h4 class=\"har__sub\">响应 Set-Cookie</h4>\n <dl class=\"har__kv har__kv--mono\">\n <div v-for=\"(c, i) in cookies.res\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ c.name }}</dt>\n <dd>{{ c.value }}</dd>\n </div>\n </dl>\n </template>\n </template>\n\n <!-- request body -->\n <template v-else-if=\"tab === 'reqBody' && reqBody\">\n <dl v-if=\"reqBody.kind === 'form' && reqBody.form\" class=\"har__kv har__kv--mono\">\n <div v-for=\"(p, i) in reqBody.form\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ p.name }}</dt>\n <dd>{{ p.value }}</dd>\n </div>\n </dl>\n <template v-else>\n <div class=\"har__bodybar\">\n <span class=\"har__bodymime\"\n >{{ reqBody.mime || '文本' }} · {{ fmtSize(reqBody.size) }}</span\n >\n <button\n type=\"button\"\n class=\"har__btn har__btn--sm\"\n @click=\"copyText(reqBody.text, 'reqBody')\"\n >\n {{ copiedKey === 'reqBody' ? '已复制' : '复制' }}\n </button>\n </div>\n <pre class=\"har__body\"><code class=\"hljs\" v-html=\"bodyHtml(reqBody)\" /></pre>\n </template>\n </template>\n\n <!-- response body -->\n <template v-else-if=\"tab === 'resBody' && resBody\">\n <div class=\"har__bodybar\">\n <span class=\"har__bodymime\">\n {{ resBody.mime || '文本' }} · {{ fmtSize(resBody.size) }}\n <span v-if=\"resBody.wasBase64\" class=\"har__b64\">base64 已解码</span>\n </span>\n <button\n v-if=\"!resBody.isImage\"\n type=\"button\"\n class=\"har__btn har__btn--sm\"\n @click=\"copyText(resBody.text, 'resBody')\"\n >\n {{ copiedKey === 'resBody' ? '已复制' : '复制' }}\n </button>\n </div>\n <div v-if=\"resBody.isImage\" class=\"har__img\">\n <img :src=\"resBody.dataUri\" :alt=\"selected.url\" />\n </div>\n <pre v-else class=\"har__body\"><code class=\"hljs\" v-html=\"bodyHtml(resBody)\" /></pre>\n </template>\n </div>\n </section>\n </template>\n </div>\n</template>\n\n<style scoped>\n.har {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.har__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.har__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.har__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.har__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.har__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.har__btn--sm {\n padding: 3px 10px;\n font-size: 11.5px;\n}\n\n/* toolbar */\n.har__bar {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px 12px;\n border-bottom: 1px solid var(--border);\n}\n.har__filter {\n width: 100%;\n padding: 6px 10px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 7px;\n}\n.har__filter:focus {\n outline: none;\n border-color: var(--accent);\n}\n.har__cats {\n display: flex;\n flex-wrap: wrap;\n gap: 5px;\n}\n.har__cat {\n font-family: inherit;\n font-size: 11px;\n color: var(--muted);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 999px;\n padding: 2px 9px;\n cursor: pointer;\n transition:\n border-color 0.15s,\n color 0.15s;\n}\n.har__cat:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.har__cat--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n\n/* master list */\n.har__list {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n.har__list--split {\n flex: 0 0 42%;\n border-bottom: 1px solid var(--border);\n}\n.har__empty {\n padding: 18px 14px;\n font-size: 12.5px;\n color: var(--faint);\n}\n.har__row {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 6px 12px;\n background: transparent;\n border: none;\n border-bottom: 1px solid var(--border);\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n}\n.har__row:hover {\n background: var(--surface-2);\n}\n.har__row--on {\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n}\n.har__method {\n flex-shrink: 0;\n width: 42px;\n font-family: var(--font-mono);\n font-size: 10.5px;\n font-weight: 700;\n letter-spacing: 0.02em;\n color: var(--muted);\n}\n.har__method.m-get {\n color: #2f9e44;\n}\n.har__method.m-post {\n color: #1c7ed6;\n}\n.har__method.m-put,\n.har__method.m-patch {\n color: #f08c00;\n}\n.har__method.m-delete {\n color: #e03131;\n}\n.har__status {\n flex-shrink: 0;\n width: 30px;\n font-family: var(--font-mono);\n font-size: 11px;\n font-weight: 600;\n}\n.har__name {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n}\n.har__name-main {\n font-size: 12px;\n color: var(--text-strong);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.har__name-sub {\n font-size: 10.5px;\n color: var(--faint);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.har__type {\n flex-shrink: 0;\n font-size: 10.5px;\n color: var(--muted);\n}\n.har__size,\n.har__time {\n flex-shrink: 0;\n width: 52px;\n text-align: right;\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--faint);\n}\n\n/* status colors (shared) */\n.s-2xx {\n color: #2f9e44;\n}\n.s-3xx {\n color: #1c7ed6;\n}\n.s-4xx {\n color: #f08c00;\n}\n.s-5xx {\n color: #e03131;\n}\n.s-pending {\n color: var(--faint);\n}\n\n/* detail */\n.har__detail {\n flex: 1 1 58%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.har__tabs {\n flex-shrink: 0;\n display: flex;\n gap: 2px;\n padding: 6px 10px 0;\n overflow-x: auto;\n border-bottom: 1px solid var(--border);\n white-space: nowrap;\n}\n.har__tab {\n flex-shrink: 0;\n font-family: inherit;\n font-size: 12px;\n color: var(--muted);\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n padding: 6px 9px;\n cursor: pointer;\n}\n.har__tab:hover {\n color: var(--text);\n}\n.har__tab--on {\n color: var(--accent);\n border-bottom-color: var(--accent);\n}\n.har__tab-n {\n font-size: 10.5px;\n color: var(--faint);\n}\n.har__pane {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n overscroll-behavior: contain;\n padding: 12px 14px;\n}\n\n.har__kv {\n margin: 0 0 12px;\n display: flex;\n flex-direction: column;\n}\n.har__kv-row {\n display: flex;\n gap: 10px;\n padding: 4px 0;\n border-bottom: 1px solid var(--border);\n font-size: 12px;\n line-height: 1.5;\n}\n.har__kv-row dt {\n flex-shrink: 0;\n width: 38%;\n max-width: 180px;\n color: var(--muted);\n word-break: break-word;\n}\n.har__kv-row dd {\n flex: 1 1 auto;\n min-width: 0;\n margin: 0;\n color: var(--text);\n word-break: break-word;\n}\n.har__kv--mono dt,\n.har__kv--mono dd {\n font-family: var(--font-mono);\n font-size: 11.5px;\n}\n.har__sub {\n margin: 4px 0 8px;\n font-size: 12px;\n font-weight: 600;\n color: var(--text-strong);\n}\n\n.har__bodybar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n margin-bottom: 8px;\n}\n.har__bodymime {\n font-size: 11px;\n color: var(--faint);\n}\n.har__b64 {\n margin-left: 6px;\n padding: 1px 6px;\n border-radius: 4px;\n background: var(--surface-2);\n color: var(--muted);\n}\n.har__body {\n margin: 0;\n padding: 10px 12px;\n background: var(--code-bg);\n border: 1px solid var(--code-border);\n border-radius: 8px;\n font-family: var(--font-mono);\n font-size: 11.5px;\n line-height: 1.55;\n color: var(--text);\n white-space: pre-wrap;\n word-break: break-word;\n overflow-x: auto;\n}\n.har__body code {\n font-family: inherit;\n background: transparent;\n border: none;\n padding: 0;\n}\n.har__img {\n display: flex;\n justify-content: center;\n padding: 12px;\n background: var(--surface-2);\n border-radius: 8px;\n}\n.har__img img {\n max-width: 100%;\n height: auto;\n object-fit: contain;\n}\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"HarViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/HarViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport {\n type DecodedBody,\n type HarEntry,\n type HarNV,\n decodeContent,\n decodePostData,\n fmtMs,\n fmtSize,\n parseHar,\n parseQuery,\n prettyBody,\n statusClass,\n toCurl,\n} from '@/lib/har'\nimport { highlightCode } from '@/lib/highlight'\nimport { formatBytes } from '@/lib/format'\n\n/**\n * Side-viewer HAR (`.har`) renderer. Fetches the archive text and parses the HTTP Archive entirely in-browser\n * (hand-rolled, see lib/har.ts) — no third-party. Lays out a master-detail vertical split tuned for the narrow\n * side pane: a filterable request list on top, and the selected request's detail (general / headers / query /\n * cookies / request + response body) below. Bodies are decoded (standard base64 + the «base64 but undeclared»\n * case some tools emit); images preview from a data URI, binaries show a hex dump.\n *\n * Safety: a HAR body is untrusted upstream content. We never execute it — text bodies render as escaped,\n * optionally syntax-highlighted source; image bodies render through a data-URI `<img>` (an `<img>` never runs\n * scripts, so even an SVG body is inert). No body is fetched as a live resource.\n */\nconst props = defineProps<{\n /** Download URL of the .har bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\n\ninterface HarState {\n entries: HarEntry[]\n creator: string\n version: string\n}\nconst doc = ref<HarState | null>(null)\nconst selected = ref<HarEntry | null>(null)\nconst query = ref('')\n\ntype DetailTab =\n | 'general'\n | 'reqHeaders'\n | 'resHeaders'\n | 'query'\n | 'cookies'\n | 'reqBody'\n | 'resBody'\nconst tab = ref<DetailTab>('general')\n\n// A HAR can be large (megabytes of inlined bodies); gate very big ones behind a confirm before parsing.\nconst SIZE_LIMIT = 25 * 1024 * 1024\n\nconst CATEGORY_LABELS: Record<string, string> = {\n doc: '文档',\n xhr: 'XHR',\n js: 'JS',\n css: 'CSS',\n img: '图片',\n font: '字体',\n media: '媒体',\n ws: 'WS',\n other: '其它',\n}\n\n// Category quick-filter: 'all' or a HarCategory; only categories present in the doc are offered.\nconst catFilter = ref<string>('all')\nconst presentCategories = computed(() => {\n const seen = new Set<string>()\n for (const e of doc.value?.entries ?? []) seen.add(e.category)\n return Object.keys(CATEGORY_LABELS).filter((c) => seen.has(c))\n})\n\nconst filtered = computed(() => {\n const list = doc.value?.entries ?? []\n const q = query.value.trim().toLowerCase()\n const cat = catFilter.value\n return list.filter((e) => {\n if (cat !== 'all' && e.category !== cat) return false\n if (!q) return true\n return (\n e.url.toLowerCase().includes(q) ||\n e.method.toLowerCase().includes(q) ||\n String(e.status).includes(q) ||\n e.type.toLowerCase().includes(q)\n )\n })\n})\n\n// Last path segment (or host) as the row's headline; the host shows as the dim sub-line.\nfunction entryName(e: HarEntry): string {\n const seg = e.pathname.split('/').filter(Boolean).pop()\n return seg || e.host || e.pathname || e.url\n}\n\nfunction pick(e: HarEntry): void {\n selected.value = e\n // Reset to a tab that has content: prefer general; it's always present.\n tab.value = 'general'\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n selected.value = null\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const text = await res.text()\n const parsed = parseHar(text)\n doc.value = { entries: parsed.entries, creator: parsed.creator, version: parsed.version }\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nwatch(() => props.url, load, { immediate: true })\n\n/* ===== detail derivations ===== */\n\nconst reqHeaders = computed<HarNV[]>(() => selected.value?.raw.request.headers ?? [])\nconst resHeaders = computed<HarNV[]>(() => selected.value?.raw.response.headers ?? [])\nconst queryParams = computed<HarNV[]>(() => {\n const e = selected.value\n if (!e) return []\n // Prefer the explicit queryString; fall back to parsing the URL's search.\n const qs = e.raw.request.queryString\n if (qs.length) return qs\n const i = e.url.indexOf('?')\n return i >= 0 ? parseQuery(e.url.slice(i)) : []\n})\nconst cookies = computed(() => {\n const e = selected.value\n if (!e) return { req: [], res: [] }\n return { req: e.raw.request.cookies, res: e.raw.response.cookies }\n})\nconst hasCookies = computed(() => cookies.value.req.length + cookies.value.res.length > 0)\n\nconst reqBody = computed<DecodedBody | null>(() => {\n const e = selected.value\n if (!e) return null\n const b = decodePostData(e.raw.request.postData, 'auto')\n return b.kind === 'empty' ? null : b\n})\nconst resBody = computed<DecodedBody | null>(() => {\n const e = selected.value\n if (!e) return null\n const b = decodeContent(e.raw.response.content, 'auto')\n return b.kind === 'empty' ? null : b\n})\n\n// General info rows for the selected entry.\nconst generalRows = computed<HarNV[]>(() => {\n const e = selected.value\n if (!e) return []\n const r = e.raw\n const rows: HarNV[] = [\n { name: '请求 URL', value: e.url },\n { name: '方法', value: e.method },\n { name: '状态', value: e.status ? `${e.status} ${e.statusText}`.trim() : '(无响应)' },\n { name: '协议', value: r.response.httpVersion || r.request.httpVersion || '' },\n { name: '远端 IP', value: e.serverIP },\n { name: '响应类型', value: e.mime || '' },\n { name: '响应大小', value: fmtSize(e.size) },\n { name: '请求体大小', value: e.reqSize ? fmtSize(e.reqSize) : '' },\n { name: '耗时', value: fmtMs(e.time) },\n { name: '开始时间', value: e.startedISO },\n ]\n if (r.response.redirectURL) rows.push({ name: '重定向', value: r.response.redirectURL })\n return rows.filter((row) => row.value)\n})\n\n/** Render a decoded body to HTML for the <pre>: pretty-print JSON, syntax-highlight when we have a language. */\nfunction bodyHtml(body: DecodedBody): string {\n const { text, pretty } = prettyBody(body.text, body.mime)\n const lang = pretty ? 'json' : bodyLangFor(body.mime)\n return lang ? highlightCode(text, lang) : escapeHtml(text)\n}\nfunction bodyLangFor(mime: string): string {\n const m = mime.toLowerCase()\n if (m.includes('javascript') || m.includes('ecmascript')) return 'javascript'\n if (m === 'text/css') return 'css'\n if (m === 'text/html' || m.includes('xhtml')) return 'xml'\n if (m.includes('json')) return 'json'\n if (m.includes('svg') || m.includes('xml')) return 'xml'\n return ''\n}\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\n}\n\n/* ===== copy helpers ===== */\n\nconst copiedKey = ref('')\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyText(text: string, key: string): Promise<void> {\n try {\n await navigator.clipboard?.writeText(text)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copiedKey.value = key\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copiedKey.value = ''), 1400)\n}\nfunction copyCurl(): void {\n if (selected.value) void copyText(toCurl(selected.value), 'curl')\n}\nonBeforeUnmount(() => clearTimeout(copyTimer))\n</script>\n\n<template>\n <div class=\"har\">\n <p v-if=\"status === 'loading'\" class=\"har__msg\">正在解析 HAR…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"har__msg har__gate\">\n <p>该 HAR 较大({{ formatBytes(props.size) }}),将在浏览器内解析。是否继续?</p>\n <button type=\"button\" class=\"har__btn\" @click=\"doLoad\">解析并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"har__msg har__msg--error\">\n <p>解析失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"har__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <template v-else-if=\"status === 'ready' && doc\">\n <!-- toolbar: filter + category chips + count -->\n <div class=\"har__bar\">\n <input\n v-model=\"query\"\n class=\"har__filter\"\n type=\"text\"\n placeholder=\"筛选 URL / 方法 / 状态…\"\n />\n <div class=\"har__cats\">\n <button\n type=\"button\"\n class=\"har__cat\"\n :class=\"{ 'har__cat--on': catFilter === 'all' }\"\n @click=\"catFilter = 'all'\"\n >\n 全部 {{ doc.entries.length }}\n </button>\n <button\n v-for=\"c in presentCategories\"\n :key=\"c\"\n type=\"button\"\n class=\"har__cat\"\n :class=\"[`har__cat--${c}`, { 'har__cat--on': catFilter === c }]\"\n @click=\"catFilter = c\"\n >\n {{ CATEGORY_LABELS[c] }}\n </button>\n </div>\n </div>\n\n <!-- master: request list -->\n <div class=\"har__list\" :class=\"{ 'har__list--split': selected }\">\n <p v-if=\"!filtered.length\" class=\"har__empty\">没有匹配的请求。</p>\n <button\n v-for=\"e in filtered\"\n :key=\"e.index\"\n type=\"button\"\n class=\"har__row\"\n :class=\"{ 'har__row--on': selected?.index === e.index }\"\n :title=\"e.url\"\n @click=\"pick(e)\"\n >\n <span class=\"har__method\" :class=\"`m-${e.method.toLowerCase()}`\">{{ e.method }}</span>\n <span class=\"har__status\" :class=\"statusClass(e.status)\">{{ e.status || '—' }}</span>\n <span class=\"har__name\">\n <span class=\"har__name-main\">{{ entryName(e) }}</span>\n <span class=\"har__name-sub\">{{ e.host }}</span>\n </span>\n <span class=\"har__type\">{{ e.type }}</span>\n <span class=\"har__size\">{{ fmtSize(e.size) }}</span>\n <span class=\"har__time\">{{ fmtMs(e.time) }}</span>\n </button>\n </div>\n\n <!-- detail: tabs + content -->\n <section v-if=\"selected\" class=\"har__detail\">\n <div class=\"har__tabs\">\n <button\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'general' }\"\n @click=\"tab = 'general'\"\n >\n 概览\n </button>\n <button\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'reqHeaders' }\"\n @click=\"tab = 'reqHeaders'\"\n >\n 请求头 <span class=\"har__tab-n\">{{ reqHeaders.length }}</span>\n </button>\n <button\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'resHeaders' }\"\n @click=\"tab = 'resHeaders'\"\n >\n 响应头 <span class=\"har__tab-n\">{{ resHeaders.length }}</span>\n </button>\n <button\n v-if=\"queryParams.length\"\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'query' }\"\n @click=\"tab = 'query'\"\n >\n 查询 <span class=\"har__tab-n\">{{ queryParams.length }}</span>\n </button>\n <button\n v-if=\"hasCookies\"\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'cookies' }\"\n @click=\"tab = 'cookies'\"\n >\n Cookie\n </button>\n <button\n v-if=\"reqBody\"\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'reqBody' }\"\n @click=\"tab = 'reqBody'\"\n >\n 请求体\n </button>\n <button\n v-if=\"resBody\"\n type=\"button\"\n class=\"har__tab\"\n :class=\"{ 'har__tab--on': tab === 'resBody' }\"\n @click=\"tab = 'resBody'\"\n >\n 响应体\n </button>\n </div>\n\n <div class=\"har__pane\">\n <!-- general -->\n <template v-if=\"tab === 'general'\">\n <dl class=\"har__kv\">\n <div v-for=\"row in generalRows\" :key=\"row.name\" class=\"har__kv-row\">\n <dt>{{ row.name }}</dt>\n <dd>{{ row.value }}</dd>\n </div>\n </dl>\n <button type=\"button\" class=\"har__btn har__btn--sm\" @click=\"copyCurl\">\n {{ copiedKey === 'curl' ? '已复制' : '复制为 cURL' }}\n </button>\n </template>\n\n <!-- request headers -->\n <dl v-else-if=\"tab === 'reqHeaders'\" class=\"har__kv har__kv--mono\">\n <div v-for=\"(h, i) in reqHeaders\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ h.name }}</dt>\n <dd>{{ h.value }}</dd>\n </div>\n </dl>\n\n <!-- response headers -->\n <dl v-else-if=\"tab === 'resHeaders'\" class=\"har__kv har__kv--mono\">\n <div v-for=\"(h, i) in resHeaders\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ h.name }}</dt>\n <dd>{{ h.value }}</dd>\n </div>\n </dl>\n\n <!-- query string -->\n <dl v-else-if=\"tab === 'query'\" class=\"har__kv har__kv--mono\">\n <div v-for=\"(p, i) in queryParams\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ p.name }}</dt>\n <dd>{{ p.value }}</dd>\n </div>\n </dl>\n\n <!-- cookies -->\n <template v-else-if=\"tab === 'cookies'\">\n <template v-if=\"cookies.req.length\">\n <h4 class=\"har__sub\">请求 Cookie</h4>\n <dl class=\"har__kv har__kv--mono\">\n <div v-for=\"(c, i) in cookies.req\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ c.name }}</dt>\n <dd>{{ c.value }}</dd>\n </div>\n </dl>\n </template>\n <template v-if=\"cookies.res.length\">\n <h4 class=\"har__sub\">响应 Set-Cookie</h4>\n <dl class=\"har__kv har__kv--mono\">\n <div v-for=\"(c, i) in cookies.res\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ c.name }}</dt>\n <dd>{{ c.value }}</dd>\n </div>\n </dl>\n </template>\n </template>\n\n <!-- request body -->\n <template v-else-if=\"tab === 'reqBody' && reqBody\">\n <dl v-if=\"reqBody.kind === 'form' && reqBody.form\" class=\"har__kv har__kv--mono\">\n <div v-for=\"(p, i) in reqBody.form\" :key=\"i\" class=\"har__kv-row\">\n <dt>{{ p.name }}</dt>\n <dd>{{ p.value }}</dd>\n </div>\n </dl>\n <template v-else>\n <div class=\"har__bodybar\">\n <span class=\"har__bodymime\"\n >{{ reqBody.mime || '文本' }} · {{ fmtSize(reqBody.size) }}</span\n >\n <button\n type=\"button\"\n class=\"har__btn har__btn--sm\"\n @click=\"copyText(reqBody.text, 'reqBody')\"\n >\n {{ copiedKey === 'reqBody' ? '已复制' : '复制' }}\n </button>\n </div>\n <pre class=\"har__body\"><code class=\"hljs\" v-html=\"bodyHtml(reqBody)\" /></pre>\n </template>\n </template>\n\n <!-- response body -->\n <template v-else-if=\"tab === 'resBody' && resBody\">\n <div class=\"har__bodybar\">\n <span class=\"har__bodymime\">\n {{ resBody.mime || '文本' }} · {{ fmtSize(resBody.size) }}\n <span v-if=\"resBody.wasBase64\" class=\"har__b64\">base64 已解码</span>\n </span>\n <button\n v-if=\"!resBody.isImage\"\n type=\"button\"\n class=\"har__btn har__btn--sm\"\n @click=\"copyText(resBody.text, 'resBody')\"\n >\n {{ copiedKey === 'resBody' ? '已复制' : '复制' }}\n </button>\n </div>\n <div v-if=\"resBody.isImage\" class=\"har__img\">\n <img :src=\"resBody.dataUri\" :alt=\"selected.url\" />\n </div>\n <pre v-else class=\"har__body\"><code class=\"hljs\" v-html=\"bodyHtml(resBody)\" /></pre>\n </template>\n </div>\n </section>\n </template>\n </div>\n</template>\n\n<style scoped>\n.har {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.har__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.har__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.har__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.har__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.har__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.har__btn--sm {\n padding: 3px 10px;\n font-size: 11.5px;\n}\n\n/* toolbar */\n.har__bar {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px 12px;\n border-bottom: 1px solid var(--border);\n}\n.har__filter {\n width: 100%;\n padding: 6px 10px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 7px;\n}\n.har__filter:focus {\n outline: none;\n border-color: var(--accent);\n}\n.har__cats {\n display: flex;\n flex-wrap: wrap;\n gap: 5px;\n}\n.har__cat {\n font-family: inherit;\n font-size: 11px;\n color: var(--muted);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 999px;\n padding: 2px 9px;\n cursor: pointer;\n transition:\n border-color 0.15s,\n color 0.15s;\n}\n.har__cat:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.har__cat--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n\n/* master list */\n.har__list {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n.har__list--split {\n flex: 0 0 42%;\n border-bottom: 1px solid var(--border);\n}\n.har__empty {\n padding: 18px 14px;\n font-size: 12.5px;\n color: var(--faint);\n}\n.har__row {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 6px 12px;\n background: transparent;\n border: none;\n border-bottom: 1px solid var(--border);\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n}\n.har__row:hover {\n background: var(--surface-2);\n}\n.har__row--on {\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n}\n.har__method {\n flex-shrink: 0;\n width: 42px;\n font-family: var(--font-mono);\n font-size: 10.5px;\n font-weight: 700;\n letter-spacing: 0.02em;\n color: var(--muted);\n}\n.har__method.m-get {\n color: var(--hl-name);\n}\n.har__method.m-post {\n color: var(--hl-attr);\n}\n.har__method.m-put,\n.har__method.m-patch {\n color: var(--ansi-yellow);\n}\n.har__method.m-delete {\n color: var(--danger);\n}\n.har__status {\n flex-shrink: 0;\n width: 30px;\n font-family: var(--font-mono);\n font-size: 11px;\n font-weight: 600;\n}\n.har__name {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n}\n.har__name-main {\n font-size: 12px;\n color: var(--text-strong);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.har__name-sub {\n font-size: 10.5px;\n color: var(--faint);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.har__type {\n flex-shrink: 0;\n font-size: 10.5px;\n color: var(--muted);\n}\n.har__size,\n.har__time {\n flex-shrink: 0;\n width: 52px;\n text-align: right;\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--faint);\n}\n\n/* status colors (shared) */\n.s-2xx {\n color: var(--hl-name);\n}\n.s-3xx {\n color: var(--hl-attr);\n}\n.s-4xx {\n color: var(--ansi-yellow);\n}\n.s-5xx {\n color: var(--danger);\n}\n.s-pending {\n color: var(--faint);\n}\n\n/* detail */\n.har__detail {\n flex: 1 1 58%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.har__tabs {\n flex-shrink: 0;\n display: flex;\n gap: 2px;\n padding: 6px 10px 0;\n overflow-x: auto;\n border-bottom: 1px solid var(--border);\n white-space: nowrap;\n}\n.har__tab {\n flex-shrink: 0;\n font-family: inherit;\n font-size: 12px;\n color: var(--muted);\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n padding: 6px 9px;\n cursor: pointer;\n}\n.har__tab:hover {\n color: var(--text);\n}\n.har__tab--on {\n color: var(--accent);\n border-bottom-color: var(--accent);\n}\n.har__tab-n {\n font-size: 10.5px;\n color: var(--faint);\n}\n.har__pane {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n overscroll-behavior: contain;\n padding: 12px 14px;\n}\n\n.har__kv {\n margin: 0 0 12px;\n display: flex;\n flex-direction: column;\n}\n.har__kv-row {\n display: flex;\n gap: 10px;\n padding: 4px 0;\n border-bottom: 1px solid var(--border);\n font-size: 12px;\n line-height: 1.5;\n}\n.har__kv-row dt {\n flex-shrink: 0;\n width: 38%;\n max-width: 180px;\n color: var(--muted);\n word-break: break-word;\n}\n.har__kv-row dd {\n flex: 1 1 auto;\n min-width: 0;\n margin: 0;\n color: var(--text);\n word-break: break-word;\n}\n.har__kv--mono dt,\n.har__kv--mono dd {\n font-family: var(--font-mono);\n font-size: 11.5px;\n}\n.har__sub {\n margin: 4px 0 8px;\n font-size: 12px;\n font-weight: 600;\n color: var(--text-strong);\n}\n\n.har__bodybar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n margin-bottom: 8px;\n}\n.har__bodymime {\n font-size: 11px;\n color: var(--faint);\n}\n.har__b64 {\n margin-left: 6px;\n padding: 1px 6px;\n border-radius: 4px;\n background: var(--surface-2);\n color: var(--muted);\n}\n.har__body {\n margin: 0;\n padding: 10px 12px;\n background: var(--code-bg);\n border: 1px solid var(--code-border);\n border-radius: 8px;\n font-family: var(--font-mono);\n font-size: 11.5px;\n line-height: 1.55;\n color: var(--text);\n white-space: pre-wrap;\n word-break: break-word;\n overflow-x: auto;\n}\n.har__body code {\n font-family: inherit;\n background: transparent;\n border: none;\n padding: 0;\n}\n.har__img {\n display: flex;\n justify-content: center;\n padding: 12px;\n background: var(--surface-2);\n border-radius: 8px;\n}\n.har__img img {\n max-width: 100%;\n height: auto;\n object-fit: contain;\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import e from "../_virtual/_plugin-vue_export-helper.js";
|
|
|
2
2
|
import t from "./JksViewer.vue_vue_type_script_setup_true_lang.js";
|
|
3
3
|
/* empty css */
|
|
4
4
|
//#region src/components/JksViewer.vue
|
|
5
|
-
var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-
|
|
5
|
+
var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-667f823e"]]);
|
|
6
6
|
//#endregion
|
|
7
7
|
export { n as default };
|
|
8
8
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JksViewer.js","names":[],"sources":["../../src/components/JksViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, reactive, ref, shallowRef, watch } from 'vue'\nimport {\n decryptPrivateKey,\n JCEKS_MAGIC,\n JKS_MAGIC,\n JksError,\n readJks,\n verifyStorePassword,\n type JksEntry,\n type JksStore,\n type KeyDecryptResult,\n} from '@/lib/jks'\nimport { looksLikePkcs12, readPkcs12, type Pkcs12Result } from '@/lib/pkcs12'\nimport { parseCertificate, sha256Hex, type CertSummary } from '@/lib/x509'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * Side-viewer for keystores. Fetches the BYTES itself (binary-safe) and sniffs the format:\n * - JKS / JCEKS (magic 0xFEEDFEED / 0xCECECECE): the Sun binary format (see lib/jks.ts). Certificates are\n * plaintext, so the whole inventory shows immediately; a password verifies the store MAC and decrypts JKS\n * private keys.\n * - PKCS#12 / PFX (`.p12`/`.pfx`, and the very common `.jks`-that-is-actually-PKCS#12 — keytool defaults to\n * PKCS12 since JDK 9): everything is encrypted (see lib/pkcs12.ts), so nothing shows until the password\n * unlocks it; then certs + private keys appear and the integrity MAC is verified.\n *\n * Both render the same cards: per entry the alias / kind / (JKS) creation date, the private-key algorithm once\n * decrypted, and each certificate's X.509 fields (subject/issuer, validity + expiry badge, key + signature\n * algorithm, SANs, SHA-256 fingerprint — reusing lib/x509.ts). All parsing/decryption is in-browser; the\n * password and key material never leave the page.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Format = 'jks' | 'jceks' | 'pkcs12'\n\ninterface CertView {\n summary: CertSummary\n id: string\n}\ninterface EntryView {\n entry: JksEntry\n index: number\n certs: CertView[]\n}\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst format = ref<Format | null>(null)\nconst rawBuf = shallowRef<ArrayBuffer | null>(null) // kept for PKCS#12 (decrypted on password submit)\nconst jksStore = shallowRef<JksStore | null>(null)\nconst entryViews = shallowRef<EntryView[]>([])\nconst fingerprints = reactive<Record<string, string>>({})\n\n// Password state: one field drives store-MAC verification and per-key decryption (keytool defaults the store\n// password equal to each key password — the common case).\nconst password = ref('')\nconst pwTried = ref(false)\nconst integrity = ref<'unknown' | 'ok' | 'bad'>('unknown')\nconst unlocked = ref(false) // PKCS#12: bags decrypted\nconst keyResults = reactive<Record<number, KeyDecryptResult>>({})\nconst p12meta = shallowRef<Pkcs12Result | null>(null)\nconst p12error = ref('') // unsupported / malformed (distinct from wrong-password)\n\nconst SIZE_LIMIT = 8 * 1024 * 1024\n\nconst isPkcs12 = computed(() => format.value === 'pkcs12')\nconst storeTypeLabel = computed(() =>\n format.value === 'pkcs12' ? 'PKCS#12' : format.value === 'jceks' ? 'JCEKS' : 'JKS',\n)\nconst hasPrivateKeys = computed(() => entryViews.value.some((e) => e.entry.type === 'privateKey'))\nconst storeMeta = computed(() => {\n const s = jksStore.value\n if (format.value === 'pkcs12') {\n const m = p12meta.value\n if (m) return [...m.encryptionAlgos, m.macAlgorithm].filter(Boolean).join(' · ')\n return '证书与私钥均加密,输入密码解密'\n }\n return s ? `版本 ${s.version} · ${s.entries.length} 个条目` : ''\n})\n\nfunction fingerprintAll(): void {\n for (const ev of entryViews.value) {\n for (const cv of ev.certs) {\n if (cv.summary.ok) {\n void sha256Hex(cv.summary.der)\n .then((hex) => {\n fingerprints[cv.id] = hex\n })\n .catch(() => {})\n }\n }\n }\n}\n\nfunction buildJksEntryViews(store: JksStore): void {\n entryViews.value = store.entries.map((entry, index) => ({\n entry,\n index,\n certs: entry.certs.map((c, ci) => ({ summary: parseCertificate(c.der), id: `${index}.${ci}` })),\n }))\n}\n\nfunction processBuffer(buf: ArrayBuffer): void {\n const bytes = new Uint8Array(buf)\n const magic =\n bytes.length >= 4 ? ((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]) >>> 0 : 0\n if (magic === JKS_MAGIC || magic === JCEKS_MAGIC) {\n const store = readJks(buf)\n format.value = store.storeType\n jksStore.value = store\n buildJksEntryViews(store)\n fingerprintAll()\n } else if (looksLikePkcs12(bytes)) {\n format.value = 'pkcs12'\n rawBuf.value = buf // wait for the password before decrypting\n } else {\n throw new JksError(\n `文件头不匹配(magic=0x${magic.toString(16).padStart(8, '0')})—— 不是 JKS/JCEKS/PKCS#12 密钥库`,\n )\n }\n status.value = 'ready'\n}\n\nfunction resetState(): void {\n errorMsg.value = ''\n format.value = null\n rawBuf.value = null\n jksStore.value = null\n entryViews.value = []\n password.value = ''\n pwTried.value = false\n integrity.value = 'unknown'\n unlocked.value = false\n p12meta.value = null\n p12error.value = ''\n for (const k of Object.keys(fingerprints)) delete fingerprints[k]\n for (const k of Object.keys(keyResults)) delete keyResults[Number(k)]\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n resetState()\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n processBuffer(await res.arrayBuffer())\n } catch (e) {\n errorMsg.value = e instanceof JksError || e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nfunction submitJksPassword(store: JksStore): void {\n integrity.value = verifyStorePassword(store, password.value) ? 'ok' : 'bad'\n for (const ev of entryViews.value) {\n if (ev.entry.type === 'privateKey' && ev.entry.encryptedKey) {\n keyResults[ev.index] = decryptPrivateKey(ev.entry.encryptedKey, password.value)\n }\n }\n}\n\nfunction buildPkcs12EntryViews(result: Pkcs12Result): void {\n const views: EntryView[] = []\n const usedCerts = new Set<number>()\n let idx = 0\n for (const k of result.keys) {\n const chain = result.certs.filter((c, ci) => {\n const match = !!k.localKeyId && c.localKeyId === k.localKeyId\n if (match) usedCerts.add(ci)\n return match\n })\n const entry: JksEntry = {\n type: 'privateKey',\n alias: k.friendlyName || k.localKeyId || `私钥 ${idx + 1}`,\n createdMs: 0,\n certs: chain.map((c) => ({ type: 'X.509', der: c.der })),\n }\n views.push({\n entry,\n index: idx,\n certs: entry.certs.map((c, ci) => ({ summary: parseCertificate(c.der), id: `${idx}.${ci}` })),\n })\n keyResults[idx] = {\n ok: true,\n key: { pkcs8: k.pkcs8, algorithm: k.algorithm, bits: k.bits, curve: k.curve },\n }\n idx++\n }\n result.certs.forEach((c, ci) => {\n if (usedCerts.has(ci)) return\n const entry: JksEntry = {\n type: 'trustedCert',\n alias: c.friendlyName || `证书 ${idx + 1}`,\n createdMs: 0,\n certs: [{ type: 'X.509', der: c.der }],\n }\n views.push({ entry, index: idx, certs: [{ summary: parseCertificate(c.der), id: `${idx}.0` }] })\n idx++\n })\n entryViews.value = views\n}\n\nfunction submitPkcs12Password(buf: ArrayBuffer): void {\n const r = readPkcs12(buf, password.value)\n if (!r.ok) {\n unlocked.value = false\n entryViews.value = []\n for (const k of Object.keys(keyResults)) delete keyResults[Number(k)]\n if (r.reason === 'wrong-password') {\n integrity.value = 'bad'\n p12error.value = ''\n } else {\n integrity.value = 'unknown'\n p12error.value =\n (r.reason === 'unsupported' ? '暂不支持的加密方式:' : '解析失败:') + (r.detail ?? '')\n }\n return\n }\n p12error.value = ''\n p12meta.value = r.result\n buildPkcs12EntryViews(r.result)\n integrity.value = r.result.macVerified === true ? 'ok' : 'unknown'\n unlocked.value = true\n fingerprintAll()\n}\n\n// Apply the password to whichever format this keystore is.\nfunction submitPassword(): void {\n if (!password.value) return\n pwTried.value = true\n if (format.value === 'pkcs12') {\n if (rawBuf.value) submitPkcs12Password(rawBuf.value)\n } else if (jksStore.value) {\n submitJksPassword(jksStore.value)\n }\n}\n\n// ── friendly-field formatting (shared with the cert cards) ─────────────────────────────────────────\nfunction formatDn(rdns: CertSummary['subject']): string {\n return rdns.map((r) => `${r.name}=${r.value}`).join(', ')\n}\n\nconst ENTRY_LABEL: Record<JksEntry['type'], string> = {\n privateKey: '私钥条目',\n trustedCert: '受信任证书',\n secretKey: '密钥条目',\n}\nconst ENTRY_CLASS: Record<JksEntry['type'], string> = {\n privateKey: 'jks__badge--key',\n trustedCert: 'jks__badge--cert',\n secretKey: 'jks__badge--secret',\n}\n\nfunction formatDate(ms: number): string {\n if (!ms) return ''\n try {\n return new Date(ms).toLocaleString('zh-CN', { hour12: false })\n } catch {\n return ''\n }\n}\n\nfunction publicKeyText(s: CertSummary): string {\n const parts: string[] = []\n if (s.pubKeyAlg) parts.push(s.pubKeyAlg)\n if (s.pubKeyBits) parts.push(`${s.pubKeyBits} 位`)\n if (s.pubKeyCurve && s.pubKeyCurve !== s.pubKeyAlg) parts.push(s.pubKeyCurve)\n return parts.join(' · ')\n}\n\n// Validity badge: future / expired / soon (≤30 days) / valid, from notBefore/notAfter vs now.\nfunction expiry(s: CertSummary): { text: string; cls: string } | null {\n if (s.notAfterMs == null) return null\n const now = Date.now()\n if (s.notBeforeMs != null && now < s.notBeforeMs)\n return { text: '尚未生效', cls: 'jks__cbadge--warn' }\n if (now > s.notAfterMs) return { text: '已过期', cls: 'jks__cbadge--bad' }\n const days = Math.floor((s.notAfterMs - now) / 86_400_000)\n if (days <= 30) return { text: `${days} 天后过期`, cls: 'jks__cbadge--warn' }\n return { text: `有效(剩 ${days} 天)`, cls: 'jks__cbadge--ok' }\n}\n\n// The private-key row's decrypted/locked/error state for one entry.\ntype KeyState =\n | { kind: 'locked' }\n | { kind: 'ok'; text: string }\n | { kind: 'wrong' }\n | { kind: 'unsupported'; detail?: string }\n | { kind: 'malformed' }\n\nfunction keyState(index: number): KeyState {\n const r = keyResults[index]\n if (!r) return { kind: 'locked' }\n if (r.ok) {\n const parts = [r.key.algorithm]\n if (r.key.bits) parts.push(`${r.key.bits} 位`)\n if (r.key.curve && r.key.curve !== r.key.algorithm) parts.push(r.key.curve)\n return { kind: 'ok', text: parts.join(' · ') }\n }\n if (r.reason === 'wrong-password') return { kind: 'wrong' }\n if (r.reason === 'unsupported') return { kind: 'unsupported', detail: r.detail }\n return { kind: 'malformed' }\n}\n\nconst pwPlaceholder = computed(() => {\n if (isPkcs12.value)\n return unlocked.value ? '已解锁(可重新输入)' : '输入 PKCS#12 密码解密证书与私钥'\n return hasPrivateKeys.value ? '输入密码:解密私钥并校验完整性' : '输入库密码校验完整性(可选)'\n})\n\n// Integrity/unlock status line after a password attempt (format-aware).\nconst integrityText = computed(() => {\n if (integrity.value === 'bad') {\n return isPkcs12.value ? '密码错误 —— 无法解密' : '完整性校验未通过 —— 密码错误或文件已被修改'\n }\n if (integrity.value === 'ok') {\n if (isPkcs12.value) return `密码正确,完整性校验通过(${p12meta.value?.macAlgorithm ?? 'MAC'})`\n return '完整性校验通过 —— 密码正确,keystore 未被篡改'\n }\n // PKCS#12 unlocked but MAC absent/unsupported: still a successful decrypt.\n if (isPkcs12.value && unlocked.value) return '已解密(该文件无 MAC 或摘要不支持,密码经解密验证)'\n return ''\n})\nconst integrityOk = computed(() => integrity.value === 'ok' || (isPkcs12.value && unlocked.value))\n\nwatch(() => [props.url, props.name], load, { immediate: true })\nonBeforeUnmount(() => {\n // nothing to revoke; bytes are not retained as object URLs\n})\n</script>\n\n<template>\n <div class=\"jks\">\n <p v-if=\"status === 'loading'\" class=\"jks__msg\">正在解析 keystore…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"jks__msg jks__gate\">\n <p>该文件较大({{ formatBytes(props.size) }}),将在浏览器内解析。是否继续?</p>\n <button type=\"button\" class=\"jks__btn\" @click=\"doLoad\">解析并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"jks__msg jks__msg--error\">\n <p>无法解析:{{ errorMsg }}</p>\n <button type=\"button\" class=\"jks__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <div v-else-if=\"format\" class=\"jks__scroll\">\n <!-- store header + password -->\n <section class=\"jks__store\">\n <header class=\"jks__storehead\">\n <span class=\"jks__badge jks__badge--type\">{{ storeTypeLabel }}</span>\n <span class=\"jks__storetitle\">{{ isPkcs12 ? 'PKCS#12 密钥库' : 'Java KeyStore' }}</span>\n <span class=\"jks__storemeta\">{{ storeMeta }}</span>\n </header>\n\n <div class=\"jks__pw\" :class=\"{ 'jks__pw--ok': integrityOk }\">\n <Icon name=\"key\" :size=\"14\" />\n <input\n v-model=\"password\"\n type=\"password\"\n class=\"jks__pwinput\"\n :placeholder=\"pwPlaceholder\"\n autocomplete=\"off\"\n @keyup.enter=\"submitPassword\"\n />\n <button type=\"button\" class=\"jks__pwbtn\" @click=\"submitPassword\">\n {{ isPkcs12 && unlocked ? '重新解锁' : '解锁' }}\n </button>\n </div>\n\n <p\n v-if=\"pwTried && integrityText\"\n class=\"jks__integrity\"\n :class=\"`jks__integrity--${integrity}`\"\n >\n <Icon :name=\"integrityOk ? 'check' : 'close'\" :size=\"13\" />\n <span>{{ integrityText }}</span>\n </p>\n\n <p class=\"jks__hint\">全部解析与解密均在浏览器本地完成,密码与密钥不会上传。</p>\n <p v-if=\"jksStore?.truncatedNote\" class=\"jks__warn\">{{ jksStore.truncatedNote }}</p>\n <p v-if=\"p12error\" class=\"jks__warn\">{{ p12error }}</p>\n <p v-for=\"(w, i) in p12meta?.warnings ?? []\" :key=\"i\" class=\"jks__warn\">{{ w }}</p>\n </section>\n\n <!-- PKCS#12 pre-unlock: nothing is visible until decrypted -->\n <div v-if=\"isPkcs12 && !unlocked\" class=\"jks__locked\">\n <Icon name=\"key\" :size=\"26\" class=\"jks__lockicon\" />\n <p>PKCS#12 密钥库的证书与私钥均已加密,输入上方密码后在此显示内容。</p>\n </div>\n\n <!-- entries -->\n <section v-for=\"ev in entryViews\" :key=\"ev.index\" class=\"jks__entry\">\n <header class=\"jks__entryhead\">\n <span class=\"jks__badge\" :class=\"ENTRY_CLASS[ev.entry.type]\">{{\n ENTRY_LABEL[ev.entry.type]\n }}</span>\n <span class=\"jks__alias\" :title=\"ev.entry.alias\">{{ ev.entry.alias }}</span>\n <span v-if=\"ev.entry.createdMs\" class=\"jks__date\">{{\n formatDate(ev.entry.createdMs)\n }}</span>\n </header>\n\n <!-- private key material -->\n <div v-if=\"ev.entry.type === 'privateKey'\" class=\"jks__key\">\n <template v-if=\"keyState(ev.index).kind === 'locked'\">\n <Icon name=\"key\" :size=\"13\" class=\"jks__keyicon\" />\n <span class=\"jks__keytext\">私钥已加密 —— 输入密码后在此显示密钥算法</span>\n </template>\n <template v-else-if=\"keyState(ev.index).kind === 'ok'\">\n <Icon name=\"check\" :size=\"13\" class=\"jks__keyicon jks__keyicon--ok\" />\n <span class=\"jks__keytext jks__keytext--ok\"\n >私钥已解密 · {{ (keyState(ev.index) as { text: string }).text }}</span\n >\n </template>\n <template v-else-if=\"keyState(ev.index).kind === 'wrong'\">\n <Icon name=\"close\" :size=\"13\" class=\"jks__keyicon jks__keyicon--bad\" />\n <span class=\"jks__keytext jks__keytext--bad\">密码错误 —— 无法解密该私钥</span>\n </template>\n <template v-else-if=\"keyState(ev.index).kind === 'unsupported'\">\n <Icon name=\"key\" :size=\"13\" class=\"jks__keyicon\" />\n <span class=\"jks__keytext\"\n >该私钥加密方式暂不支持浏览器内解密({{\n (keyState(ev.index) as { detail?: string }).detail\n }})—— 证书链仍可查看</span\n >\n </template>\n <template v-else>\n <Icon name=\"close\" :size=\"13\" class=\"jks__keyicon jks__keyicon--bad\" />\n <span class=\"jks__keytext jks__keytext--bad\">私钥数据无法解析</span>\n </template>\n </div>\n\n <p v-else-if=\"ev.entry.type === 'secretKey'\" class=\"jks__note\">\n JCEKS 对称密钥条目(SecretKey)—— 以 Java 序列化对象存储,暂不解析。\n </p>\n\n <!-- certificate chain -->\n <div v-if=\"ev.certs.length\" class=\"jks__chain\">\n <div v-if=\"ev.entry.type === 'privateKey'\" class=\"jks__chainlabel\">\n 证书链({{ ev.certs.length }})\n </div>\n <article v-for=\"cv in ev.certs\" :key=\"cv.id\" class=\"jks__cert\">\n <div class=\"jks__certhead\">\n <span class=\"jks__certcn\">{{ cv.summary.subjectCN || cv.summary.label }}</span>\n <span v-if=\"expiry(cv.summary)\" class=\"jks__cbadge\" :class=\"expiry(cv.summary)?.cls\">\n {{ expiry(cv.summary)?.text }}\n </span>\n </div>\n <p v-if=\"cv.summary.note && !cv.summary.ok\" class=\"jks__note\">{{ cv.summary.note }}</p>\n <dl class=\"jks__dl\">\n <template v-if=\"cv.summary.subject.length\">\n <dt>主题</dt>\n <dd>{{ formatDn(cv.summary.subject) }}</dd>\n </template>\n <template v-if=\"cv.summary.issuer.length\">\n <dt>颁发者</dt>\n <dd>{{ formatDn(cv.summary.issuer) }}</dd>\n </template>\n <template v-if=\"cv.summary.notBefore || cv.summary.notAfter\">\n <dt>有效期</dt>\n <dd>{{ cv.summary.notBefore ?? '?' }} → {{ cv.summary.notAfter ?? '?' }}</dd>\n </template>\n <template v-if=\"publicKeyText(cv.summary)\">\n <dt>公钥</dt>\n <dd>{{ publicKeyText(cv.summary) }}</dd>\n </template>\n <template v-if=\"cv.summary.sigAlg\">\n <dt>签名算法</dt>\n <dd>{{ cv.summary.sigAlg }}</dd>\n </template>\n <template v-if=\"cv.summary.serialHex\">\n <dt>序列号</dt>\n <dd class=\"jks__mono\">{{ cv.summary.serialHex }}</dd>\n </template>\n <template v-if=\"cv.summary.sans.length\">\n <dt>备用名 (SAN)</dt>\n <dd>{{ cv.summary.sans.join(',') }}</dd>\n </template>\n <template v-if=\"fingerprints[cv.id]\">\n <dt>SHA-256</dt>\n <dd class=\"jks__mono jks__fp\">{{ fingerprints[cv.id] }}</dd>\n </template>\n </dl>\n </article>\n </div>\n </section>\n </div>\n </div>\n</template>\n\n<style scoped>\n.jks {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.jks__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.jks__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.jks__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.jks__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.jks__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.jks__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: 14px 14px 60px;\n}\n\n/* Store header + password ---------------------------------------------------------------------- */\n.jks__store {\n margin-bottom: 16px;\n border: 1px solid var(--border);\n border-radius: 10px;\n background: var(--surface);\n overflow: hidden;\n}\n.jks__storehead {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 12px;\n background: var(--surface-2);\n border-bottom: 1px solid var(--border);\n}\n.jks__storetitle {\n flex-shrink: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.jks__storemeta {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n text-align: right;\n font-size: 11.5px;\n color: var(--faint);\n}\n\n.jks__pw {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 12px;\n color: var(--muted);\n}\n.jks__pw--ok {\n color: var(--accent);\n}\n.jks__pwinput {\n flex: 1 1 auto;\n min-width: 0;\n height: 28px;\n padding: 0 9px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n}\n.jks__pwinput:focus {\n outline: none;\n border-color: var(--accent);\n}\n.jks__pwbtn {\n flex-shrink: 0;\n height: 28px;\n padding: 0 14px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n}\n.jks__pwbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.jks__integrity {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0;\n padding: 0 12px 8px;\n font-size: 12px;\n}\n.jks__integrity--ok {\n color: #15803d;\n}\n.jks__integrity--bad {\n color: #b91c1c;\n}\n.jks__integrity--unknown {\n color: var(--accent);\n}\n.jks__hint {\n margin: 0;\n padding: 0 12px 10px;\n font-size: 11.5px;\n line-height: 1.5;\n color: var(--faint);\n}\n.jks__warn {\n margin: 0;\n padding: 9px 12px;\n font-size: 12px;\n line-height: 1.5;\n color: #b45309;\n background: color-mix(in srgb, #f59e0b 12%, transparent);\n border-top: 1px solid var(--border);\n}\n\n.jks__locked {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 10px;\n padding: 40px 24px;\n text-align: center;\n color: var(--muted);\n font-size: 12.5px;\n}\n.jks__lockicon {\n color: var(--faint);\n}\n\n/* Badges ------------------------------------------------------------------------------------- */\n.jks__badge {\n flex-shrink: 0;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.02em;\n border-radius: 5px;\n padding: 2px 7px;\n}\n.jks__badge--type {\n color: var(--accent);\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n}\n.jks__badge--key {\n color: #b45309;\n background: color-mix(in srgb, #f59e0b 16%, transparent);\n}\n.jks__badge--cert {\n color: #15803d;\n background: color-mix(in srgb, #22c55e 16%, transparent);\n}\n.jks__badge--secret {\n color: var(--muted);\n background: var(--surface-2);\n}\n\n/* Entry cards -------------------------------------------------------------------------------- */\n.jks__entry {\n margin-bottom: 14px;\n border: 1px solid var(--border);\n border-radius: 10px;\n background: var(--surface);\n overflow: hidden;\n}\n.jks__entryhead {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 12px;\n border-bottom: 1px solid var(--border);\n}\n.jks__alias {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.jks__date {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n\n.jks__key {\n display: flex;\n align-items: center;\n gap: 7px;\n padding: 9px 12px;\n font-size: 12px;\n background: color-mix(in srgb, var(--accent) 4%, transparent);\n border-bottom: 1px solid var(--border);\n}\n.jks__keyicon {\n flex-shrink: 0;\n color: var(--faint);\n}\n.jks__keyicon--ok {\n color: #15803d;\n}\n.jks__keyicon--bad {\n color: #b91c1c;\n}\n.jks__keytext {\n min-width: 0;\n color: var(--muted);\n}\n.jks__keytext--ok {\n color: #15803d;\n font-weight: 500;\n}\n.jks__keytext--bad {\n color: #b91c1c;\n}\n\n.jks__note {\n margin: 0;\n padding: 9px 12px;\n font-size: 12px;\n line-height: 1.5;\n color: var(--muted);\n}\n\n/* Certificate chain -------------------------------------------------------------------------- */\n.jks__chain {\n padding: 10px 12px 12px;\n}\n.jks__chainlabel {\n margin-bottom: 8px;\n font-size: 11.5px;\n color: var(--faint);\n}\n.jks__cert {\n margin-bottom: 10px;\n border: 1px solid var(--border);\n border-radius: 8px;\n overflow: hidden;\n}\n.jks__cert:last-child {\n margin-bottom: 0;\n}\n.jks__certhead {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 10px;\n background: var(--surface-2);\n border-bottom: 1px solid var(--border);\n}\n.jks__certcn {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 12.5px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.jks__cbadge {\n flex-shrink: 0;\n font-size: 11px;\n font-weight: 600;\n border-radius: 5px;\n padding: 2px 8px;\n}\n.jks__cbadge--ok {\n color: #15803d;\n background: color-mix(in srgb, #22c55e 16%, transparent);\n}\n.jks__cbadge--warn {\n color: #b45309;\n background: color-mix(in srgb, #f59e0b 18%, transparent);\n}\n.jks__cbadge--bad {\n color: #b91c1c;\n background: color-mix(in srgb, #ef4444 16%, transparent);\n}\n.jks__dl {\n display: grid;\n grid-template-columns: max-content 1fr;\n gap: 6px 14px;\n margin: 0;\n padding: 10px;\n}\n.jks__dl dt {\n font-size: 12px;\n color: var(--faint);\n white-space: nowrap;\n}\n.jks__dl dd {\n margin: 0;\n font-size: 12.5px;\n color: var(--text);\n word-break: break-word;\n}\n.jks__mono {\n font-family: var(--font-mono);\n font-size: 11.5px;\n}\n.jks__fp {\n word-break: break-all;\n}\n</style>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"JksViewer.js","names":[],"sources":["../../src/components/JksViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, reactive, ref, shallowRef, watch } from 'vue'\nimport {\n decryptPrivateKey,\n JCEKS_MAGIC,\n JKS_MAGIC,\n JksError,\n readJks,\n verifyStorePassword,\n type JksEntry,\n type JksStore,\n type KeyDecryptResult,\n} from '@/lib/jks'\nimport { looksLikePkcs12, readPkcs12, type Pkcs12Result } from '@/lib/pkcs12'\nimport { parseCertificate, sha256Hex, type CertSummary } from '@/lib/x509'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * Side-viewer for keystores. Fetches the BYTES itself (binary-safe) and sniffs the format:\n * - JKS / JCEKS (magic 0xFEEDFEED / 0xCECECECE): the Sun binary format (see lib/jks.ts). Certificates are\n * plaintext, so the whole inventory shows immediately; a password verifies the store MAC and decrypts JKS\n * private keys.\n * - PKCS#12 / PFX (`.p12`/`.pfx`, and the very common `.jks`-that-is-actually-PKCS#12 — keytool defaults to\n * PKCS12 since JDK 9): everything is encrypted (see lib/pkcs12.ts), so nothing shows until the password\n * unlocks it; then certs + private keys appear and the integrity MAC is verified.\n *\n * Both render the same cards: per entry the alias / kind / (JKS) creation date, the private-key algorithm once\n * decrypted, and each certificate's X.509 fields (subject/issuer, validity + expiry badge, key + signature\n * algorithm, SANs, SHA-256 fingerprint — reusing lib/x509.ts). All parsing/decryption is in-browser; the\n * password and key material never leave the page.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Format = 'jks' | 'jceks' | 'pkcs12'\n\ninterface CertView {\n summary: CertSummary\n id: string\n}\ninterface EntryView {\n entry: JksEntry\n index: number\n certs: CertView[]\n}\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst format = ref<Format | null>(null)\nconst rawBuf = shallowRef<ArrayBuffer | null>(null) // kept for PKCS#12 (decrypted on password submit)\nconst jksStore = shallowRef<JksStore | null>(null)\nconst entryViews = shallowRef<EntryView[]>([])\nconst fingerprints = reactive<Record<string, string>>({})\n\n// Password state: one field drives store-MAC verification and per-key decryption (keytool defaults the store\n// password equal to each key password — the common case).\nconst password = ref('')\nconst pwTried = ref(false)\nconst integrity = ref<'unknown' | 'ok' | 'bad'>('unknown')\nconst unlocked = ref(false) // PKCS#12: bags decrypted\nconst keyResults = reactive<Record<number, KeyDecryptResult>>({})\nconst p12meta = shallowRef<Pkcs12Result | null>(null)\nconst p12error = ref('') // unsupported / malformed (distinct from wrong-password)\n\nconst SIZE_LIMIT = 8 * 1024 * 1024\n\nconst isPkcs12 = computed(() => format.value === 'pkcs12')\nconst storeTypeLabel = computed(() =>\n format.value === 'pkcs12' ? 'PKCS#12' : format.value === 'jceks' ? 'JCEKS' : 'JKS',\n)\nconst hasPrivateKeys = computed(() => entryViews.value.some((e) => e.entry.type === 'privateKey'))\nconst storeMeta = computed(() => {\n const s = jksStore.value\n if (format.value === 'pkcs12') {\n const m = p12meta.value\n if (m) return [...m.encryptionAlgos, m.macAlgorithm].filter(Boolean).join(' · ')\n return '证书与私钥均加密,输入密码解密'\n }\n return s ? `版本 ${s.version} · ${s.entries.length} 个条目` : ''\n})\n\nfunction fingerprintAll(): void {\n for (const ev of entryViews.value) {\n for (const cv of ev.certs) {\n if (cv.summary.ok) {\n void sha256Hex(cv.summary.der)\n .then((hex) => {\n fingerprints[cv.id] = hex\n })\n .catch(() => {})\n }\n }\n }\n}\n\nfunction buildJksEntryViews(store: JksStore): void {\n entryViews.value = store.entries.map((entry, index) => ({\n entry,\n index,\n certs: entry.certs.map((c, ci) => ({ summary: parseCertificate(c.der), id: `${index}.${ci}` })),\n }))\n}\n\nfunction processBuffer(buf: ArrayBuffer): void {\n const bytes = new Uint8Array(buf)\n const magic =\n bytes.length >= 4 ? ((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]) >>> 0 : 0\n if (magic === JKS_MAGIC || magic === JCEKS_MAGIC) {\n const store = readJks(buf)\n format.value = store.storeType\n jksStore.value = store\n buildJksEntryViews(store)\n fingerprintAll()\n } else if (looksLikePkcs12(bytes)) {\n format.value = 'pkcs12'\n rawBuf.value = buf // wait for the password before decrypting\n } else {\n throw new JksError(\n `文件头不匹配(magic=0x${magic.toString(16).padStart(8, '0')})—— 不是 JKS/JCEKS/PKCS#12 密钥库`,\n )\n }\n status.value = 'ready'\n}\n\nfunction resetState(): void {\n errorMsg.value = ''\n format.value = null\n rawBuf.value = null\n jksStore.value = null\n entryViews.value = []\n password.value = ''\n pwTried.value = false\n integrity.value = 'unknown'\n unlocked.value = false\n p12meta.value = null\n p12error.value = ''\n for (const k of Object.keys(fingerprints)) delete fingerprints[k]\n for (const k of Object.keys(keyResults)) delete keyResults[Number(k)]\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n resetState()\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n processBuffer(await res.arrayBuffer())\n } catch (e) {\n errorMsg.value = e instanceof JksError || e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nfunction submitJksPassword(store: JksStore): void {\n integrity.value = verifyStorePassword(store, password.value) ? 'ok' : 'bad'\n for (const ev of entryViews.value) {\n if (ev.entry.type === 'privateKey' && ev.entry.encryptedKey) {\n keyResults[ev.index] = decryptPrivateKey(ev.entry.encryptedKey, password.value)\n }\n }\n}\n\nfunction buildPkcs12EntryViews(result: Pkcs12Result): void {\n const views: EntryView[] = []\n const usedCerts = new Set<number>()\n let idx = 0\n for (const k of result.keys) {\n const chain = result.certs.filter((c, ci) => {\n const match = !!k.localKeyId && c.localKeyId === k.localKeyId\n if (match) usedCerts.add(ci)\n return match\n })\n const entry: JksEntry = {\n type: 'privateKey',\n alias: k.friendlyName || k.localKeyId || `私钥 ${idx + 1}`,\n createdMs: 0,\n certs: chain.map((c) => ({ type: 'X.509', der: c.der })),\n }\n views.push({\n entry,\n index: idx,\n certs: entry.certs.map((c, ci) => ({ summary: parseCertificate(c.der), id: `${idx}.${ci}` })),\n })\n keyResults[idx] = {\n ok: true,\n key: { pkcs8: k.pkcs8, algorithm: k.algorithm, bits: k.bits, curve: k.curve },\n }\n idx++\n }\n result.certs.forEach((c, ci) => {\n if (usedCerts.has(ci)) return\n const entry: JksEntry = {\n type: 'trustedCert',\n alias: c.friendlyName || `证书 ${idx + 1}`,\n createdMs: 0,\n certs: [{ type: 'X.509', der: c.der }],\n }\n views.push({ entry, index: idx, certs: [{ summary: parseCertificate(c.der), id: `${idx}.0` }] })\n idx++\n })\n entryViews.value = views\n}\n\nfunction submitPkcs12Password(buf: ArrayBuffer): void {\n const r = readPkcs12(buf, password.value)\n if (!r.ok) {\n unlocked.value = false\n entryViews.value = []\n for (const k of Object.keys(keyResults)) delete keyResults[Number(k)]\n if (r.reason === 'wrong-password') {\n integrity.value = 'bad'\n p12error.value = ''\n } else {\n integrity.value = 'unknown'\n p12error.value =\n (r.reason === 'unsupported' ? '暂不支持的加密方式:' : '解析失败:') + (r.detail ?? '')\n }\n return\n }\n p12error.value = ''\n p12meta.value = r.result\n buildPkcs12EntryViews(r.result)\n integrity.value = r.result.macVerified === true ? 'ok' : 'unknown'\n unlocked.value = true\n fingerprintAll()\n}\n\n// Apply the password to whichever format this keystore is.\nfunction submitPassword(): void {\n if (!password.value) return\n pwTried.value = true\n if (format.value === 'pkcs12') {\n if (rawBuf.value) submitPkcs12Password(rawBuf.value)\n } else if (jksStore.value) {\n submitJksPassword(jksStore.value)\n }\n}\n\n// ── friendly-field formatting (shared with the cert cards) ─────────────────────────────────────────\nfunction formatDn(rdns: CertSummary['subject']): string {\n return rdns.map((r) => `${r.name}=${r.value}`).join(', ')\n}\n\nconst ENTRY_LABEL: Record<JksEntry['type'], string> = {\n privateKey: '私钥条目',\n trustedCert: '受信任证书',\n secretKey: '密钥条目',\n}\nconst ENTRY_CLASS: Record<JksEntry['type'], string> = {\n privateKey: 'jks__badge--key',\n trustedCert: 'jks__badge--cert',\n secretKey: 'jks__badge--secret',\n}\n\nfunction formatDate(ms: number): string {\n if (!ms) return ''\n try {\n return new Date(ms).toLocaleString('zh-CN', { hour12: false })\n } catch {\n return ''\n }\n}\n\nfunction publicKeyText(s: CertSummary): string {\n const parts: string[] = []\n if (s.pubKeyAlg) parts.push(s.pubKeyAlg)\n if (s.pubKeyBits) parts.push(`${s.pubKeyBits} 位`)\n if (s.pubKeyCurve && s.pubKeyCurve !== s.pubKeyAlg) parts.push(s.pubKeyCurve)\n return parts.join(' · ')\n}\n\n// Validity badge: future / expired / soon (≤30 days) / valid, from notBefore/notAfter vs now.\nfunction expiry(s: CertSummary): { text: string; cls: string } | null {\n if (s.notAfterMs == null) return null\n const now = Date.now()\n if (s.notBeforeMs != null && now < s.notBeforeMs)\n return { text: '尚未生效', cls: 'jks__cbadge--warn' }\n if (now > s.notAfterMs) return { text: '已过期', cls: 'jks__cbadge--bad' }\n const days = Math.floor((s.notAfterMs - now) / 86_400_000)\n if (days <= 30) return { text: `${days} 天后过期`, cls: 'jks__cbadge--warn' }\n return { text: `有效(剩 ${days} 天)`, cls: 'jks__cbadge--ok' }\n}\n\n// The private-key row's decrypted/locked/error state for one entry.\ntype KeyState =\n | { kind: 'locked' }\n | { kind: 'ok'; text: string }\n | { kind: 'wrong' }\n | { kind: 'unsupported'; detail?: string }\n | { kind: 'malformed' }\n\nfunction keyState(index: number): KeyState {\n const r = keyResults[index]\n if (!r) return { kind: 'locked' }\n if (r.ok) {\n const parts = [r.key.algorithm]\n if (r.key.bits) parts.push(`${r.key.bits} 位`)\n if (r.key.curve && r.key.curve !== r.key.algorithm) parts.push(r.key.curve)\n return { kind: 'ok', text: parts.join(' · ') }\n }\n if (r.reason === 'wrong-password') return { kind: 'wrong' }\n if (r.reason === 'unsupported') return { kind: 'unsupported', detail: r.detail }\n return { kind: 'malformed' }\n}\n\nconst pwPlaceholder = computed(() => {\n if (isPkcs12.value)\n return unlocked.value ? '已解锁(可重新输入)' : '输入 PKCS#12 密码解密证书与私钥'\n return hasPrivateKeys.value ? '输入密码:解密私钥并校验完整性' : '输入库密码校验完整性(可选)'\n})\n\n// Integrity/unlock status line after a password attempt (format-aware).\nconst integrityText = computed(() => {\n if (integrity.value === 'bad') {\n return isPkcs12.value ? '密码错误 —— 无法解密' : '完整性校验未通过 —— 密码错误或文件已被修改'\n }\n if (integrity.value === 'ok') {\n if (isPkcs12.value) return `密码正确,完整性校验通过(${p12meta.value?.macAlgorithm ?? 'MAC'})`\n return '完整性校验通过 —— 密码正确,keystore 未被篡改'\n }\n // PKCS#12 unlocked but MAC absent/unsupported: still a successful decrypt.\n if (isPkcs12.value && unlocked.value) return '已解密(该文件无 MAC 或摘要不支持,密码经解密验证)'\n return ''\n})\nconst integrityOk = computed(() => integrity.value === 'ok' || (isPkcs12.value && unlocked.value))\n\nwatch(() => [props.url, props.name], load, { immediate: true })\nonBeforeUnmount(() => {\n // nothing to revoke; bytes are not retained as object URLs\n})\n</script>\n\n<template>\n <div class=\"jks\">\n <p v-if=\"status === 'loading'\" class=\"jks__msg\">正在解析 keystore…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"jks__msg jks__gate\">\n <p>该文件较大({{ formatBytes(props.size) }}),将在浏览器内解析。是否继续?</p>\n <button type=\"button\" class=\"jks__btn\" @click=\"doLoad\">解析并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"jks__msg jks__msg--error\">\n <p>无法解析:{{ errorMsg }}</p>\n <button type=\"button\" class=\"jks__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <div v-else-if=\"format\" class=\"jks__scroll\">\n <!-- store header + password -->\n <section class=\"jks__store\">\n <header class=\"jks__storehead\">\n <span class=\"jks__badge jks__badge--type\">{{ storeTypeLabel }}</span>\n <span class=\"jks__storetitle\">{{ isPkcs12 ? 'PKCS#12 密钥库' : 'Java KeyStore' }}</span>\n <span class=\"jks__storemeta\">{{ storeMeta }}</span>\n </header>\n\n <div class=\"jks__pw\" :class=\"{ 'jks__pw--ok': integrityOk }\">\n <Icon name=\"key\" :size=\"14\" />\n <input\n v-model=\"password\"\n type=\"password\"\n class=\"jks__pwinput\"\n :placeholder=\"pwPlaceholder\"\n autocomplete=\"off\"\n @keyup.enter=\"submitPassword\"\n />\n <button type=\"button\" class=\"jks__pwbtn\" @click=\"submitPassword\">\n {{ isPkcs12 && unlocked ? '重新解锁' : '解锁' }}\n </button>\n </div>\n\n <p\n v-if=\"pwTried && integrityText\"\n class=\"jks__integrity\"\n :class=\"`jks__integrity--${integrity}`\"\n >\n <Icon :name=\"integrityOk ? 'check' : 'close'\" :size=\"13\" />\n <span>{{ integrityText }}</span>\n </p>\n\n <p class=\"jks__hint\">全部解析与解密均在浏览器本地完成,密码与密钥不会上传。</p>\n <p v-if=\"jksStore?.truncatedNote\" class=\"jks__warn\">{{ jksStore.truncatedNote }}</p>\n <p v-if=\"p12error\" class=\"jks__warn\">{{ p12error }}</p>\n <p v-for=\"(w, i) in p12meta?.warnings ?? []\" :key=\"i\" class=\"jks__warn\">{{ w }}</p>\n </section>\n\n <!-- PKCS#12 pre-unlock: nothing is visible until decrypted -->\n <div v-if=\"isPkcs12 && !unlocked\" class=\"jks__locked\">\n <Icon name=\"key\" :size=\"26\" class=\"jks__lockicon\" />\n <p>PKCS#12 密钥库的证书与私钥均已加密,输入上方密码后在此显示内容。</p>\n </div>\n\n <!-- entries -->\n <section v-for=\"ev in entryViews\" :key=\"ev.index\" class=\"jks__entry\">\n <header class=\"jks__entryhead\">\n <span class=\"jks__badge\" :class=\"ENTRY_CLASS[ev.entry.type]\">{{\n ENTRY_LABEL[ev.entry.type]\n }}</span>\n <span class=\"jks__alias\" :title=\"ev.entry.alias\">{{ ev.entry.alias }}</span>\n <span v-if=\"ev.entry.createdMs\" class=\"jks__date\">{{\n formatDate(ev.entry.createdMs)\n }}</span>\n </header>\n\n <!-- private key material -->\n <div v-if=\"ev.entry.type === 'privateKey'\" class=\"jks__key\">\n <template v-if=\"keyState(ev.index).kind === 'locked'\">\n <Icon name=\"key\" :size=\"13\" class=\"jks__keyicon\" />\n <span class=\"jks__keytext\">私钥已加密 —— 输入密码后在此显示密钥算法</span>\n </template>\n <template v-else-if=\"keyState(ev.index).kind === 'ok'\">\n <Icon name=\"check\" :size=\"13\" class=\"jks__keyicon jks__keyicon--ok\" />\n <span class=\"jks__keytext jks__keytext--ok\"\n >私钥已解密 · {{ (keyState(ev.index) as { text: string }).text }}</span\n >\n </template>\n <template v-else-if=\"keyState(ev.index).kind === 'wrong'\">\n <Icon name=\"close\" :size=\"13\" class=\"jks__keyicon jks__keyicon--bad\" />\n <span class=\"jks__keytext jks__keytext--bad\">密码错误 —— 无法解密该私钥</span>\n </template>\n <template v-else-if=\"keyState(ev.index).kind === 'unsupported'\">\n <Icon name=\"key\" :size=\"13\" class=\"jks__keyicon\" />\n <span class=\"jks__keytext\"\n >该私钥加密方式暂不支持浏览器内解密({{\n (keyState(ev.index) as { detail?: string }).detail\n }})—— 证书链仍可查看</span\n >\n </template>\n <template v-else>\n <Icon name=\"close\" :size=\"13\" class=\"jks__keyicon jks__keyicon--bad\" />\n <span class=\"jks__keytext jks__keytext--bad\">私钥数据无法解析</span>\n </template>\n </div>\n\n <p v-else-if=\"ev.entry.type === 'secretKey'\" class=\"jks__note\">\n JCEKS 对称密钥条目(SecretKey)—— 以 Java 序列化对象存储,暂不解析。\n </p>\n\n <!-- certificate chain -->\n <div v-if=\"ev.certs.length\" class=\"jks__chain\">\n <div v-if=\"ev.entry.type === 'privateKey'\" class=\"jks__chainlabel\">\n 证书链({{ ev.certs.length }})\n </div>\n <article v-for=\"cv in ev.certs\" :key=\"cv.id\" class=\"jks__cert\">\n <div class=\"jks__certhead\">\n <span class=\"jks__certcn\">{{ cv.summary.subjectCN || cv.summary.label }}</span>\n <span v-if=\"expiry(cv.summary)\" class=\"jks__cbadge\" :class=\"expiry(cv.summary)?.cls\">\n {{ expiry(cv.summary)?.text }}\n </span>\n </div>\n <p v-if=\"cv.summary.note && !cv.summary.ok\" class=\"jks__note\">{{ cv.summary.note }}</p>\n <dl class=\"jks__dl\">\n <template v-if=\"cv.summary.subject.length\">\n <dt>主题</dt>\n <dd>{{ formatDn(cv.summary.subject) }}</dd>\n </template>\n <template v-if=\"cv.summary.issuer.length\">\n <dt>颁发者</dt>\n <dd>{{ formatDn(cv.summary.issuer) }}</dd>\n </template>\n <template v-if=\"cv.summary.notBefore || cv.summary.notAfter\">\n <dt>有效期</dt>\n <dd>{{ cv.summary.notBefore ?? '?' }} → {{ cv.summary.notAfter ?? '?' }}</dd>\n </template>\n <template v-if=\"publicKeyText(cv.summary)\">\n <dt>公钥</dt>\n <dd>{{ publicKeyText(cv.summary) }}</dd>\n </template>\n <template v-if=\"cv.summary.sigAlg\">\n <dt>签名算法</dt>\n <dd>{{ cv.summary.sigAlg }}</dd>\n </template>\n <template v-if=\"cv.summary.serialHex\">\n <dt>序列号</dt>\n <dd class=\"jks__mono\">{{ cv.summary.serialHex }}</dd>\n </template>\n <template v-if=\"cv.summary.sans.length\">\n <dt>备用名 (SAN)</dt>\n <dd>{{ cv.summary.sans.join(',') }}</dd>\n </template>\n <template v-if=\"fingerprints[cv.id]\">\n <dt>SHA-256</dt>\n <dd class=\"jks__mono jks__fp\">{{ fingerprints[cv.id] }}</dd>\n </template>\n </dl>\n </article>\n </div>\n </section>\n </div>\n </div>\n</template>\n\n<style scoped>\n.jks {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.jks__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.jks__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.jks__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.jks__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.jks__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.jks__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: 14px 14px 60px;\n}\n\n/* Store header + password ---------------------------------------------------------------------- */\n.jks__store {\n margin-bottom: 16px;\n border: 1px solid var(--border);\n border-radius: 10px;\n background: var(--surface);\n overflow: hidden;\n}\n.jks__storehead {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 12px;\n background: var(--surface-2);\n border-bottom: 1px solid var(--border);\n}\n.jks__storetitle {\n flex-shrink: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.jks__storemeta {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n text-align: right;\n font-size: 11.5px;\n color: var(--faint);\n}\n\n.jks__pw {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 12px;\n color: var(--muted);\n}\n.jks__pw--ok {\n color: var(--accent);\n}\n.jks__pwinput {\n flex: 1 1 auto;\n min-width: 0;\n height: 28px;\n padding: 0 9px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n}\n.jks__pwinput:focus {\n outline: none;\n border-color: var(--accent);\n}\n.jks__pwbtn {\n flex-shrink: 0;\n height: 28px;\n padding: 0 14px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n}\n.jks__pwbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.jks__integrity {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0;\n padding: 0 12px 8px;\n font-size: 12px;\n}\n.jks__integrity--ok {\n color: var(--hl-name);\n}\n.jks__integrity--bad {\n color: var(--danger);\n}\n.jks__integrity--unknown {\n color: var(--accent);\n}\n.jks__hint {\n margin: 0;\n padding: 0 12px 10px;\n font-size: 11.5px;\n line-height: 1.5;\n color: var(--faint);\n}\n.jks__warn {\n margin: 0;\n padding: 9px 12px;\n font-size: 12px;\n line-height: 1.5;\n color: var(--ansi-yellow);\n background: color-mix(in srgb, var(--ansi-yellow) 12%, transparent);\n border-top: 1px solid var(--border);\n}\n\n.jks__locked {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 10px;\n padding: 40px 24px;\n text-align: center;\n color: var(--muted);\n font-size: 12.5px;\n}\n.jks__lockicon {\n color: var(--faint);\n}\n\n/* Badges ------------------------------------------------------------------------------------- */\n.jks__badge {\n flex-shrink: 0;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.02em;\n border-radius: 5px;\n padding: 2px 7px;\n}\n.jks__badge--type {\n color: var(--accent);\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n}\n.jks__badge--key {\n color: var(--ansi-yellow);\n background: color-mix(in srgb, var(--ansi-yellow) 16%, transparent);\n}\n.jks__badge--cert {\n color: var(--hl-name);\n background: color-mix(in srgb, var(--hl-name) 16%, transparent);\n}\n.jks__badge--secret {\n color: var(--muted);\n background: var(--surface-2);\n}\n\n/* Entry cards -------------------------------------------------------------------------------- */\n.jks__entry {\n margin-bottom: 14px;\n border: 1px solid var(--border);\n border-radius: 10px;\n background: var(--surface);\n overflow: hidden;\n}\n.jks__entryhead {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 12px;\n border-bottom: 1px solid var(--border);\n}\n.jks__alias {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.jks__date {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n\n.jks__key {\n display: flex;\n align-items: center;\n gap: 7px;\n padding: 9px 12px;\n font-size: 12px;\n background: color-mix(in srgb, var(--accent) 4%, transparent);\n border-bottom: 1px solid var(--border);\n}\n.jks__keyicon {\n flex-shrink: 0;\n color: var(--faint);\n}\n.jks__keyicon--ok {\n color: var(--hl-name);\n}\n.jks__keyicon--bad {\n color: var(--danger);\n}\n.jks__keytext {\n min-width: 0;\n color: var(--muted);\n}\n.jks__keytext--ok {\n color: var(--hl-name);\n font-weight: 500;\n}\n.jks__keytext--bad {\n color: var(--danger);\n}\n\n.jks__note {\n margin: 0;\n padding: 9px 12px;\n font-size: 12px;\n line-height: 1.5;\n color: var(--muted);\n}\n\n/* Certificate chain -------------------------------------------------------------------------- */\n.jks__chain {\n padding: 10px 12px 12px;\n}\n.jks__chainlabel {\n margin-bottom: 8px;\n font-size: 11.5px;\n color: var(--faint);\n}\n.jks__cert {\n margin-bottom: 10px;\n border: 1px solid var(--border);\n border-radius: 8px;\n overflow: hidden;\n}\n.jks__cert:last-child {\n margin-bottom: 0;\n}\n.jks__certhead {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 10px;\n background: var(--surface-2);\n border-bottom: 1px solid var(--border);\n}\n.jks__certcn {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 12.5px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.jks__cbadge {\n flex-shrink: 0;\n font-size: 11px;\n font-weight: 600;\n border-radius: 5px;\n padding: 2px 8px;\n}\n.jks__cbadge--ok {\n color: var(--hl-name);\n background: color-mix(in srgb, var(--hl-name) 16%, transparent);\n}\n.jks__cbadge--warn {\n color: var(--ansi-yellow);\n background: color-mix(in srgb, var(--ansi-yellow) 18%, transparent);\n}\n.jks__cbadge--bad {\n color: var(--danger);\n background: color-mix(in srgb, var(--danger) 16%, transparent);\n}\n.jks__dl {\n display: grid;\n grid-template-columns: max-content 1fr;\n gap: 6px 14px;\n margin: 0;\n padding: 10px;\n}\n.jks__dl dt {\n font-size: 12px;\n color: var(--faint);\n white-space: nowrap;\n}\n.jks__dl dd {\n margin: 0;\n font-size: 12.5px;\n color: var(--text);\n word-break: break-word;\n}\n.jks__mono {\n font-family: var(--font-mono);\n font-size: 11.5px;\n}\n.jks__fp {\n word-break: break-all;\n}\n</style>\n"],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JksViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/JksViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, reactive, ref, shallowRef, watch } from 'vue'\nimport {\n decryptPrivateKey,\n JCEKS_MAGIC,\n JKS_MAGIC,\n JksError,\n readJks,\n verifyStorePassword,\n type JksEntry,\n type JksStore,\n type KeyDecryptResult,\n} from '@/lib/jks'\nimport { looksLikePkcs12, readPkcs12, type Pkcs12Result } from '@/lib/pkcs12'\nimport { parseCertificate, sha256Hex, type CertSummary } from '@/lib/x509'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * Side-viewer for keystores. Fetches the BYTES itself (binary-safe) and sniffs the format:\n * - JKS / JCEKS (magic 0xFEEDFEED / 0xCECECECE): the Sun binary format (see lib/jks.ts). Certificates are\n * plaintext, so the whole inventory shows immediately; a password verifies the store MAC and decrypts JKS\n * private keys.\n * - PKCS#12 / PFX (`.p12`/`.pfx`, and the very common `.jks`-that-is-actually-PKCS#12 — keytool defaults to\n * PKCS12 since JDK 9): everything is encrypted (see lib/pkcs12.ts), so nothing shows until the password\n * unlocks it; then certs + private keys appear and the integrity MAC is verified.\n *\n * Both render the same cards: per entry the alias / kind / (JKS) creation date, the private-key algorithm once\n * decrypted, and each certificate's X.509 fields (subject/issuer, validity + expiry badge, key + signature\n * algorithm, SANs, SHA-256 fingerprint — reusing lib/x509.ts). All parsing/decryption is in-browser; the\n * password and key material never leave the page.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Format = 'jks' | 'jceks' | 'pkcs12'\n\ninterface CertView {\n summary: CertSummary\n id: string\n}\ninterface EntryView {\n entry: JksEntry\n index: number\n certs: CertView[]\n}\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst format = ref<Format | null>(null)\nconst rawBuf = shallowRef<ArrayBuffer | null>(null) // kept for PKCS#12 (decrypted on password submit)\nconst jksStore = shallowRef<JksStore | null>(null)\nconst entryViews = shallowRef<EntryView[]>([])\nconst fingerprints = reactive<Record<string, string>>({})\n\n// Password state: one field drives store-MAC verification and per-key decryption (keytool defaults the store\n// password equal to each key password — the common case).\nconst password = ref('')\nconst pwTried = ref(false)\nconst integrity = ref<'unknown' | 'ok' | 'bad'>('unknown')\nconst unlocked = ref(false) // PKCS#12: bags decrypted\nconst keyResults = reactive<Record<number, KeyDecryptResult>>({})\nconst p12meta = shallowRef<Pkcs12Result | null>(null)\nconst p12error = ref('') // unsupported / malformed (distinct from wrong-password)\n\nconst SIZE_LIMIT = 8 * 1024 * 1024\n\nconst isPkcs12 = computed(() => format.value === 'pkcs12')\nconst storeTypeLabel = computed(() =>\n format.value === 'pkcs12' ? 'PKCS#12' : format.value === 'jceks' ? 'JCEKS' : 'JKS',\n)\nconst hasPrivateKeys = computed(() => entryViews.value.some((e) => e.entry.type === 'privateKey'))\nconst storeMeta = computed(() => {\n const s = jksStore.value\n if (format.value === 'pkcs12') {\n const m = p12meta.value\n if (m) return [...m.encryptionAlgos, m.macAlgorithm].filter(Boolean).join(' · ')\n return '证书与私钥均加密,输入密码解密'\n }\n return s ? `版本 ${s.version} · ${s.entries.length} 个条目` : ''\n})\n\nfunction fingerprintAll(): void {\n for (const ev of entryViews.value) {\n for (const cv of ev.certs) {\n if (cv.summary.ok) {\n void sha256Hex(cv.summary.der)\n .then((hex) => {\n fingerprints[cv.id] = hex\n })\n .catch(() => {})\n }\n }\n }\n}\n\nfunction buildJksEntryViews(store: JksStore): void {\n entryViews.value = store.entries.map((entry, index) => ({\n entry,\n index,\n certs: entry.certs.map((c, ci) => ({ summary: parseCertificate(c.der), id: `${index}.${ci}` })),\n }))\n}\n\nfunction processBuffer(buf: ArrayBuffer): void {\n const bytes = new Uint8Array(buf)\n const magic =\n bytes.length >= 4 ? ((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]) >>> 0 : 0\n if (magic === JKS_MAGIC || magic === JCEKS_MAGIC) {\n const store = readJks(buf)\n format.value = store.storeType\n jksStore.value = store\n buildJksEntryViews(store)\n fingerprintAll()\n } else if (looksLikePkcs12(bytes)) {\n format.value = 'pkcs12'\n rawBuf.value = buf // wait for the password before decrypting\n } else {\n throw new JksError(\n `文件头不匹配(magic=0x${magic.toString(16).padStart(8, '0')})—— 不是 JKS/JCEKS/PKCS#12 密钥库`,\n )\n }\n status.value = 'ready'\n}\n\nfunction resetState(): void {\n errorMsg.value = ''\n format.value = null\n rawBuf.value = null\n jksStore.value = null\n entryViews.value = []\n password.value = ''\n pwTried.value = false\n integrity.value = 'unknown'\n unlocked.value = false\n p12meta.value = null\n p12error.value = ''\n for (const k of Object.keys(fingerprints)) delete fingerprints[k]\n for (const k of Object.keys(keyResults)) delete keyResults[Number(k)]\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n resetState()\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n processBuffer(await res.arrayBuffer())\n } catch (e) {\n errorMsg.value = e instanceof JksError || e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nfunction submitJksPassword(store: JksStore): void {\n integrity.value = verifyStorePassword(store, password.value) ? 'ok' : 'bad'\n for (const ev of entryViews.value) {\n if (ev.entry.type === 'privateKey' && ev.entry.encryptedKey) {\n keyResults[ev.index] = decryptPrivateKey(ev.entry.encryptedKey, password.value)\n }\n }\n}\n\nfunction buildPkcs12EntryViews(result: Pkcs12Result): void {\n const views: EntryView[] = []\n const usedCerts = new Set<number>()\n let idx = 0\n for (const k of result.keys) {\n const chain = result.certs.filter((c, ci) => {\n const match = !!k.localKeyId && c.localKeyId === k.localKeyId\n if (match) usedCerts.add(ci)\n return match\n })\n const entry: JksEntry = {\n type: 'privateKey',\n alias: k.friendlyName || k.localKeyId || `私钥 ${idx + 1}`,\n createdMs: 0,\n certs: chain.map((c) => ({ type: 'X.509', der: c.der })),\n }\n views.push({\n entry,\n index: idx,\n certs: entry.certs.map((c, ci) => ({ summary: parseCertificate(c.der), id: `${idx}.${ci}` })),\n })\n keyResults[idx] = {\n ok: true,\n key: { pkcs8: k.pkcs8, algorithm: k.algorithm, bits: k.bits, curve: k.curve },\n }\n idx++\n }\n result.certs.forEach((c, ci) => {\n if (usedCerts.has(ci)) return\n const entry: JksEntry = {\n type: 'trustedCert',\n alias: c.friendlyName || `证书 ${idx + 1}`,\n createdMs: 0,\n certs: [{ type: 'X.509', der: c.der }],\n }\n views.push({ entry, index: idx, certs: [{ summary: parseCertificate(c.der), id: `${idx}.0` }] })\n idx++\n })\n entryViews.value = views\n}\n\nfunction submitPkcs12Password(buf: ArrayBuffer): void {\n const r = readPkcs12(buf, password.value)\n if (!r.ok) {\n unlocked.value = false\n entryViews.value = []\n for (const k of Object.keys(keyResults)) delete keyResults[Number(k)]\n if (r.reason === 'wrong-password') {\n integrity.value = 'bad'\n p12error.value = ''\n } else {\n integrity.value = 'unknown'\n p12error.value =\n (r.reason === 'unsupported' ? '暂不支持的加密方式:' : '解析失败:') + (r.detail ?? '')\n }\n return\n }\n p12error.value = ''\n p12meta.value = r.result\n buildPkcs12EntryViews(r.result)\n integrity.value = r.result.macVerified === true ? 'ok' : 'unknown'\n unlocked.value = true\n fingerprintAll()\n}\n\n// Apply the password to whichever format this keystore is.\nfunction submitPassword(): void {\n if (!password.value) return\n pwTried.value = true\n if (format.value === 'pkcs12') {\n if (rawBuf.value) submitPkcs12Password(rawBuf.value)\n } else if (jksStore.value) {\n submitJksPassword(jksStore.value)\n }\n}\n\n// ── friendly-field formatting (shared with the cert cards) ─────────────────────────────────────────\nfunction formatDn(rdns: CertSummary['subject']): string {\n return rdns.map((r) => `${r.name}=${r.value}`).join(', ')\n}\n\nconst ENTRY_LABEL: Record<JksEntry['type'], string> = {\n privateKey: '私钥条目',\n trustedCert: '受信任证书',\n secretKey: '密钥条目',\n}\nconst ENTRY_CLASS: Record<JksEntry['type'], string> = {\n privateKey: 'jks__badge--key',\n trustedCert: 'jks__badge--cert',\n secretKey: 'jks__badge--secret',\n}\n\nfunction formatDate(ms: number): string {\n if (!ms) return ''\n try {\n return new Date(ms).toLocaleString('zh-CN', { hour12: false })\n } catch {\n return ''\n }\n}\n\nfunction publicKeyText(s: CertSummary): string {\n const parts: string[] = []\n if (s.pubKeyAlg) parts.push(s.pubKeyAlg)\n if (s.pubKeyBits) parts.push(`${s.pubKeyBits} 位`)\n if (s.pubKeyCurve && s.pubKeyCurve !== s.pubKeyAlg) parts.push(s.pubKeyCurve)\n return parts.join(' · ')\n}\n\n// Validity badge: future / expired / soon (≤30 days) / valid, from notBefore/notAfter vs now.\nfunction expiry(s: CertSummary): { text: string; cls: string } | null {\n if (s.notAfterMs == null) return null\n const now = Date.now()\n if (s.notBeforeMs != null && now < s.notBeforeMs)\n return { text: '尚未生效', cls: 'jks__cbadge--warn' }\n if (now > s.notAfterMs) return { text: '已过期', cls: 'jks__cbadge--bad' }\n const days = Math.floor((s.notAfterMs - now) / 86_400_000)\n if (days <= 30) return { text: `${days} 天后过期`, cls: 'jks__cbadge--warn' }\n return { text: `有效(剩 ${days} 天)`, cls: 'jks__cbadge--ok' }\n}\n\n// The private-key row's decrypted/locked/error state for one entry.\ntype KeyState =\n | { kind: 'locked' }\n | { kind: 'ok'; text: string }\n | { kind: 'wrong' }\n | { kind: 'unsupported'; detail?: string }\n | { kind: 'malformed' }\n\nfunction keyState(index: number): KeyState {\n const r = keyResults[index]\n if (!r) return { kind: 'locked' }\n if (r.ok) {\n const parts = [r.key.algorithm]\n if (r.key.bits) parts.push(`${r.key.bits} 位`)\n if (r.key.curve && r.key.curve !== r.key.algorithm) parts.push(r.key.curve)\n return { kind: 'ok', text: parts.join(' · ') }\n }\n if (r.reason === 'wrong-password') return { kind: 'wrong' }\n if (r.reason === 'unsupported') return { kind: 'unsupported', detail: r.detail }\n return { kind: 'malformed' }\n}\n\nconst pwPlaceholder = computed(() => {\n if (isPkcs12.value)\n return unlocked.value ? '已解锁(可重新输入)' : '输入 PKCS#12 密码解密证书与私钥'\n return hasPrivateKeys.value ? '输入密码:解密私钥并校验完整性' : '输入库密码校验完整性(可选)'\n})\n\n// Integrity/unlock status line after a password attempt (format-aware).\nconst integrityText = computed(() => {\n if (integrity.value === 'bad') {\n return isPkcs12.value ? '密码错误 —— 无法解密' : '完整性校验未通过 —— 密码错误或文件已被修改'\n }\n if (integrity.value === 'ok') {\n if (isPkcs12.value) return `密码正确,完整性校验通过(${p12meta.value?.macAlgorithm ?? 'MAC'})`\n return '完整性校验通过 —— 密码正确,keystore 未被篡改'\n }\n // PKCS#12 unlocked but MAC absent/unsupported: still a successful decrypt.\n if (isPkcs12.value && unlocked.value) return '已解密(该文件无 MAC 或摘要不支持,密码经解密验证)'\n return ''\n})\nconst integrityOk = computed(() => integrity.value === 'ok' || (isPkcs12.value && unlocked.value))\n\nwatch(() => [props.url, props.name], load, { immediate: true })\nonBeforeUnmount(() => {\n // nothing to revoke; bytes are not retained as object URLs\n})\n</script>\n\n<template>\n <div class=\"jks\">\n <p v-if=\"status === 'loading'\" class=\"jks__msg\">正在解析 keystore…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"jks__msg jks__gate\">\n <p>该文件较大({{ formatBytes(props.size) }}),将在浏览器内解析。是否继续?</p>\n <button type=\"button\" class=\"jks__btn\" @click=\"doLoad\">解析并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"jks__msg jks__msg--error\">\n <p>无法解析:{{ errorMsg }}</p>\n <button type=\"button\" class=\"jks__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <div v-else-if=\"format\" class=\"jks__scroll\">\n <!-- store header + password -->\n <section class=\"jks__store\">\n <header class=\"jks__storehead\">\n <span class=\"jks__badge jks__badge--type\">{{ storeTypeLabel }}</span>\n <span class=\"jks__storetitle\">{{ isPkcs12 ? 'PKCS#12 密钥库' : 'Java KeyStore' }}</span>\n <span class=\"jks__storemeta\">{{ storeMeta }}</span>\n </header>\n\n <div class=\"jks__pw\" :class=\"{ 'jks__pw--ok': integrityOk }\">\n <Icon name=\"key\" :size=\"14\" />\n <input\n v-model=\"password\"\n type=\"password\"\n class=\"jks__pwinput\"\n :placeholder=\"pwPlaceholder\"\n autocomplete=\"off\"\n @keyup.enter=\"submitPassword\"\n />\n <button type=\"button\" class=\"jks__pwbtn\" @click=\"submitPassword\">\n {{ isPkcs12 && unlocked ? '重新解锁' : '解锁' }}\n </button>\n </div>\n\n <p\n v-if=\"pwTried && integrityText\"\n class=\"jks__integrity\"\n :class=\"`jks__integrity--${integrity}`\"\n >\n <Icon :name=\"integrityOk ? 'check' : 'close'\" :size=\"13\" />\n <span>{{ integrityText }}</span>\n </p>\n\n <p class=\"jks__hint\">全部解析与解密均在浏览器本地完成,密码与密钥不会上传。</p>\n <p v-if=\"jksStore?.truncatedNote\" class=\"jks__warn\">{{ jksStore.truncatedNote }}</p>\n <p v-if=\"p12error\" class=\"jks__warn\">{{ p12error }}</p>\n <p v-for=\"(w, i) in p12meta?.warnings ?? []\" :key=\"i\" class=\"jks__warn\">{{ w }}</p>\n </section>\n\n <!-- PKCS#12 pre-unlock: nothing is visible until decrypted -->\n <div v-if=\"isPkcs12 && !unlocked\" class=\"jks__locked\">\n <Icon name=\"key\" :size=\"26\" class=\"jks__lockicon\" />\n <p>PKCS#12 密钥库的证书与私钥均已加密,输入上方密码后在此显示内容。</p>\n </div>\n\n <!-- entries -->\n <section v-for=\"ev in entryViews\" :key=\"ev.index\" class=\"jks__entry\">\n <header class=\"jks__entryhead\">\n <span class=\"jks__badge\" :class=\"ENTRY_CLASS[ev.entry.type]\">{{\n ENTRY_LABEL[ev.entry.type]\n }}</span>\n <span class=\"jks__alias\" :title=\"ev.entry.alias\">{{ ev.entry.alias }}</span>\n <span v-if=\"ev.entry.createdMs\" class=\"jks__date\">{{\n formatDate(ev.entry.createdMs)\n }}</span>\n </header>\n\n <!-- private key material -->\n <div v-if=\"ev.entry.type === 'privateKey'\" class=\"jks__key\">\n <template v-if=\"keyState(ev.index).kind === 'locked'\">\n <Icon name=\"key\" :size=\"13\" class=\"jks__keyicon\" />\n <span class=\"jks__keytext\">私钥已加密 —— 输入密码后在此显示密钥算法</span>\n </template>\n <template v-else-if=\"keyState(ev.index).kind === 'ok'\">\n <Icon name=\"check\" :size=\"13\" class=\"jks__keyicon jks__keyicon--ok\" />\n <span class=\"jks__keytext jks__keytext--ok\"\n >私钥已解密 · {{ (keyState(ev.index) as { text: string }).text }}</span\n >\n </template>\n <template v-else-if=\"keyState(ev.index).kind === 'wrong'\">\n <Icon name=\"close\" :size=\"13\" class=\"jks__keyicon jks__keyicon--bad\" />\n <span class=\"jks__keytext jks__keytext--bad\">密码错误 —— 无法解密该私钥</span>\n </template>\n <template v-else-if=\"keyState(ev.index).kind === 'unsupported'\">\n <Icon name=\"key\" :size=\"13\" class=\"jks__keyicon\" />\n <span class=\"jks__keytext\"\n >该私钥加密方式暂不支持浏览器内解密({{\n (keyState(ev.index) as { detail?: string }).detail\n }})—— 证书链仍可查看</span\n >\n </template>\n <template v-else>\n <Icon name=\"close\" :size=\"13\" class=\"jks__keyicon jks__keyicon--bad\" />\n <span class=\"jks__keytext jks__keytext--bad\">私钥数据无法解析</span>\n </template>\n </div>\n\n <p v-else-if=\"ev.entry.type === 'secretKey'\" class=\"jks__note\">\n JCEKS 对称密钥条目(SecretKey)—— 以 Java 序列化对象存储,暂不解析。\n </p>\n\n <!-- certificate chain -->\n <div v-if=\"ev.certs.length\" class=\"jks__chain\">\n <div v-if=\"ev.entry.type === 'privateKey'\" class=\"jks__chainlabel\">\n 证书链({{ ev.certs.length }})\n </div>\n <article v-for=\"cv in ev.certs\" :key=\"cv.id\" class=\"jks__cert\">\n <div class=\"jks__certhead\">\n <span class=\"jks__certcn\">{{ cv.summary.subjectCN || cv.summary.label }}</span>\n <span v-if=\"expiry(cv.summary)\" class=\"jks__cbadge\" :class=\"expiry(cv.summary)?.cls\">\n {{ expiry(cv.summary)?.text }}\n </span>\n </div>\n <p v-if=\"cv.summary.note && !cv.summary.ok\" class=\"jks__note\">{{ cv.summary.note }}</p>\n <dl class=\"jks__dl\">\n <template v-if=\"cv.summary.subject.length\">\n <dt>主题</dt>\n <dd>{{ formatDn(cv.summary.subject) }}</dd>\n </template>\n <template v-if=\"cv.summary.issuer.length\">\n <dt>颁发者</dt>\n <dd>{{ formatDn(cv.summary.issuer) }}</dd>\n </template>\n <template v-if=\"cv.summary.notBefore || cv.summary.notAfter\">\n <dt>有效期</dt>\n <dd>{{ cv.summary.notBefore ?? '?' }} → {{ cv.summary.notAfter ?? '?' }}</dd>\n </template>\n <template v-if=\"publicKeyText(cv.summary)\">\n <dt>公钥</dt>\n <dd>{{ publicKeyText(cv.summary) }}</dd>\n </template>\n <template v-if=\"cv.summary.sigAlg\">\n <dt>签名算法</dt>\n <dd>{{ cv.summary.sigAlg }}</dd>\n </template>\n <template v-if=\"cv.summary.serialHex\">\n <dt>序列号</dt>\n <dd class=\"jks__mono\">{{ cv.summary.serialHex }}</dd>\n </template>\n <template v-if=\"cv.summary.sans.length\">\n <dt>备用名 (SAN)</dt>\n <dd>{{ cv.summary.sans.join(',') }}</dd>\n </template>\n <template v-if=\"fingerprints[cv.id]\">\n <dt>SHA-256</dt>\n <dd class=\"jks__mono jks__fp\">{{ fingerprints[cv.id] }}</dd>\n </template>\n </dl>\n </article>\n </div>\n </section>\n </div>\n </div>\n</template>\n\n<style scoped>\n.jks {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.jks__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.jks__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.jks__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.jks__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.jks__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.jks__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: 14px 14px 60px;\n}\n\n/* Store header + password ---------------------------------------------------------------------- */\n.jks__store {\n margin-bottom: 16px;\n border: 1px solid var(--border);\n border-radius: 10px;\n background: var(--surface);\n overflow: hidden;\n}\n.jks__storehead {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 12px;\n background: var(--surface-2);\n border-bottom: 1px solid var(--border);\n}\n.jks__storetitle {\n flex-shrink: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.jks__storemeta {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n text-align: right;\n font-size: 11.5px;\n color: var(--faint);\n}\n\n.jks__pw {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 12px;\n color: var(--muted);\n}\n.jks__pw--ok {\n color: var(--accent);\n}\n.jks__pwinput {\n flex: 1 1 auto;\n min-width: 0;\n height: 28px;\n padding: 0 9px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n}\n.jks__pwinput:focus {\n outline: none;\n border-color: var(--accent);\n}\n.jks__pwbtn {\n flex-shrink: 0;\n height: 28px;\n padding: 0 14px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n}\n.jks__pwbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.jks__integrity {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0;\n padding: 0 12px 8px;\n font-size: 12px;\n}\n.jks__integrity--ok {\n color: #15803d;\n}\n.jks__integrity--bad {\n color: #b91c1c;\n}\n.jks__integrity--unknown {\n color: var(--accent);\n}\n.jks__hint {\n margin: 0;\n padding: 0 12px 10px;\n font-size: 11.5px;\n line-height: 1.5;\n color: var(--faint);\n}\n.jks__warn {\n margin: 0;\n padding: 9px 12px;\n font-size: 12px;\n line-height: 1.5;\n color: #b45309;\n background: color-mix(in srgb, #f59e0b 12%, transparent);\n border-top: 1px solid var(--border);\n}\n\n.jks__locked {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 10px;\n padding: 40px 24px;\n text-align: center;\n color: var(--muted);\n font-size: 12.5px;\n}\n.jks__lockicon {\n color: var(--faint);\n}\n\n/* Badges ------------------------------------------------------------------------------------- */\n.jks__badge {\n flex-shrink: 0;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.02em;\n border-radius: 5px;\n padding: 2px 7px;\n}\n.jks__badge--type {\n color: var(--accent);\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n}\n.jks__badge--key {\n color: #b45309;\n background: color-mix(in srgb, #f59e0b 16%, transparent);\n}\n.jks__badge--cert {\n color: #15803d;\n background: color-mix(in srgb, #22c55e 16%, transparent);\n}\n.jks__badge--secret {\n color: var(--muted);\n background: var(--surface-2);\n}\n\n/* Entry cards -------------------------------------------------------------------------------- */\n.jks__entry {\n margin-bottom: 14px;\n border: 1px solid var(--border);\n border-radius: 10px;\n background: var(--surface);\n overflow: hidden;\n}\n.jks__entryhead {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 12px;\n border-bottom: 1px solid var(--border);\n}\n.jks__alias {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.jks__date {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n\n.jks__key {\n display: flex;\n align-items: center;\n gap: 7px;\n padding: 9px 12px;\n font-size: 12px;\n background: color-mix(in srgb, var(--accent) 4%, transparent);\n border-bottom: 1px solid var(--border);\n}\n.jks__keyicon {\n flex-shrink: 0;\n color: var(--faint);\n}\n.jks__keyicon--ok {\n color: #15803d;\n}\n.jks__keyicon--bad {\n color: #b91c1c;\n}\n.jks__keytext {\n min-width: 0;\n color: var(--muted);\n}\n.jks__keytext--ok {\n color: #15803d;\n font-weight: 500;\n}\n.jks__keytext--bad {\n color: #b91c1c;\n}\n\n.jks__note {\n margin: 0;\n padding: 9px 12px;\n font-size: 12px;\n line-height: 1.5;\n color: var(--muted);\n}\n\n/* Certificate chain -------------------------------------------------------------------------- */\n.jks__chain {\n padding: 10px 12px 12px;\n}\n.jks__chainlabel {\n margin-bottom: 8px;\n font-size: 11.5px;\n color: var(--faint);\n}\n.jks__cert {\n margin-bottom: 10px;\n border: 1px solid var(--border);\n border-radius: 8px;\n overflow: hidden;\n}\n.jks__cert:last-child {\n margin-bottom: 0;\n}\n.jks__certhead {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 10px;\n background: var(--surface-2);\n border-bottom: 1px solid var(--border);\n}\n.jks__certcn {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 12.5px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.jks__cbadge {\n flex-shrink: 0;\n font-size: 11px;\n font-weight: 600;\n border-radius: 5px;\n padding: 2px 8px;\n}\n.jks__cbadge--ok {\n color: #15803d;\n background: color-mix(in srgb, #22c55e 16%, transparent);\n}\n.jks__cbadge--warn {\n color: #b45309;\n background: color-mix(in srgb, #f59e0b 18%, transparent);\n}\n.jks__cbadge--bad {\n color: #b91c1c;\n background: color-mix(in srgb, #ef4444 16%, transparent);\n}\n.jks__dl {\n display: grid;\n grid-template-columns: max-content 1fr;\n gap: 6px 14px;\n margin: 0;\n padding: 10px;\n}\n.jks__dl dt {\n font-size: 12px;\n color: var(--faint);\n white-space: nowrap;\n}\n.jks__dl dd {\n margin: 0;\n font-size: 12.5px;\n color: var(--text);\n word-break: break-word;\n}\n.jks__mono {\n font-family: var(--font-mono);\n font-size: 11.5px;\n}\n.jks__fp {\n word-break: break-all;\n}\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"JksViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/JksViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, reactive, ref, shallowRef, watch } from 'vue'\nimport {\n decryptPrivateKey,\n JCEKS_MAGIC,\n JKS_MAGIC,\n JksError,\n readJks,\n verifyStorePassword,\n type JksEntry,\n type JksStore,\n type KeyDecryptResult,\n} from '@/lib/jks'\nimport { looksLikePkcs12, readPkcs12, type Pkcs12Result } from '@/lib/pkcs12'\nimport { parseCertificate, sha256Hex, type CertSummary } from '@/lib/x509'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * Side-viewer for keystores. Fetches the BYTES itself (binary-safe) and sniffs the format:\n * - JKS / JCEKS (magic 0xFEEDFEED / 0xCECECECE): the Sun binary format (see lib/jks.ts). Certificates are\n * plaintext, so the whole inventory shows immediately; a password verifies the store MAC and decrypts JKS\n * private keys.\n * - PKCS#12 / PFX (`.p12`/`.pfx`, and the very common `.jks`-that-is-actually-PKCS#12 — keytool defaults to\n * PKCS12 since JDK 9): everything is encrypted (see lib/pkcs12.ts), so nothing shows until the password\n * unlocks it; then certs + private keys appear and the integrity MAC is verified.\n *\n * Both render the same cards: per entry the alias / kind / (JKS) creation date, the private-key algorithm once\n * decrypted, and each certificate's X.509 fields (subject/issuer, validity + expiry badge, key + signature\n * algorithm, SANs, SHA-256 fingerprint — reusing lib/x509.ts). All parsing/decryption is in-browser; the\n * password and key material never leave the page.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Format = 'jks' | 'jceks' | 'pkcs12'\n\ninterface CertView {\n summary: CertSummary\n id: string\n}\ninterface EntryView {\n entry: JksEntry\n index: number\n certs: CertView[]\n}\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst format = ref<Format | null>(null)\nconst rawBuf = shallowRef<ArrayBuffer | null>(null) // kept for PKCS#12 (decrypted on password submit)\nconst jksStore = shallowRef<JksStore | null>(null)\nconst entryViews = shallowRef<EntryView[]>([])\nconst fingerprints = reactive<Record<string, string>>({})\n\n// Password state: one field drives store-MAC verification and per-key decryption (keytool defaults the store\n// password equal to each key password — the common case).\nconst password = ref('')\nconst pwTried = ref(false)\nconst integrity = ref<'unknown' | 'ok' | 'bad'>('unknown')\nconst unlocked = ref(false) // PKCS#12: bags decrypted\nconst keyResults = reactive<Record<number, KeyDecryptResult>>({})\nconst p12meta = shallowRef<Pkcs12Result | null>(null)\nconst p12error = ref('') // unsupported / malformed (distinct from wrong-password)\n\nconst SIZE_LIMIT = 8 * 1024 * 1024\n\nconst isPkcs12 = computed(() => format.value === 'pkcs12')\nconst storeTypeLabel = computed(() =>\n format.value === 'pkcs12' ? 'PKCS#12' : format.value === 'jceks' ? 'JCEKS' : 'JKS',\n)\nconst hasPrivateKeys = computed(() => entryViews.value.some((e) => e.entry.type === 'privateKey'))\nconst storeMeta = computed(() => {\n const s = jksStore.value\n if (format.value === 'pkcs12') {\n const m = p12meta.value\n if (m) return [...m.encryptionAlgos, m.macAlgorithm].filter(Boolean).join(' · ')\n return '证书与私钥均加密,输入密码解密'\n }\n return s ? `版本 ${s.version} · ${s.entries.length} 个条目` : ''\n})\n\nfunction fingerprintAll(): void {\n for (const ev of entryViews.value) {\n for (const cv of ev.certs) {\n if (cv.summary.ok) {\n void sha256Hex(cv.summary.der)\n .then((hex) => {\n fingerprints[cv.id] = hex\n })\n .catch(() => {})\n }\n }\n }\n}\n\nfunction buildJksEntryViews(store: JksStore): void {\n entryViews.value = store.entries.map((entry, index) => ({\n entry,\n index,\n certs: entry.certs.map((c, ci) => ({ summary: parseCertificate(c.der), id: `${index}.${ci}` })),\n }))\n}\n\nfunction processBuffer(buf: ArrayBuffer): void {\n const bytes = new Uint8Array(buf)\n const magic =\n bytes.length >= 4 ? ((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]) >>> 0 : 0\n if (magic === JKS_MAGIC || magic === JCEKS_MAGIC) {\n const store = readJks(buf)\n format.value = store.storeType\n jksStore.value = store\n buildJksEntryViews(store)\n fingerprintAll()\n } else if (looksLikePkcs12(bytes)) {\n format.value = 'pkcs12'\n rawBuf.value = buf // wait for the password before decrypting\n } else {\n throw new JksError(\n `文件头不匹配(magic=0x${magic.toString(16).padStart(8, '0')})—— 不是 JKS/JCEKS/PKCS#12 密钥库`,\n )\n }\n status.value = 'ready'\n}\n\nfunction resetState(): void {\n errorMsg.value = ''\n format.value = null\n rawBuf.value = null\n jksStore.value = null\n entryViews.value = []\n password.value = ''\n pwTried.value = false\n integrity.value = 'unknown'\n unlocked.value = false\n p12meta.value = null\n p12error.value = ''\n for (const k of Object.keys(fingerprints)) delete fingerprints[k]\n for (const k of Object.keys(keyResults)) delete keyResults[Number(k)]\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n resetState()\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n processBuffer(await res.arrayBuffer())\n } catch (e) {\n errorMsg.value = e instanceof JksError || e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nfunction submitJksPassword(store: JksStore): void {\n integrity.value = verifyStorePassword(store, password.value) ? 'ok' : 'bad'\n for (const ev of entryViews.value) {\n if (ev.entry.type === 'privateKey' && ev.entry.encryptedKey) {\n keyResults[ev.index] = decryptPrivateKey(ev.entry.encryptedKey, password.value)\n }\n }\n}\n\nfunction buildPkcs12EntryViews(result: Pkcs12Result): void {\n const views: EntryView[] = []\n const usedCerts = new Set<number>()\n let idx = 0\n for (const k of result.keys) {\n const chain = result.certs.filter((c, ci) => {\n const match = !!k.localKeyId && c.localKeyId === k.localKeyId\n if (match) usedCerts.add(ci)\n return match\n })\n const entry: JksEntry = {\n type: 'privateKey',\n alias: k.friendlyName || k.localKeyId || `私钥 ${idx + 1}`,\n createdMs: 0,\n certs: chain.map((c) => ({ type: 'X.509', der: c.der })),\n }\n views.push({\n entry,\n index: idx,\n certs: entry.certs.map((c, ci) => ({ summary: parseCertificate(c.der), id: `${idx}.${ci}` })),\n })\n keyResults[idx] = {\n ok: true,\n key: { pkcs8: k.pkcs8, algorithm: k.algorithm, bits: k.bits, curve: k.curve },\n }\n idx++\n }\n result.certs.forEach((c, ci) => {\n if (usedCerts.has(ci)) return\n const entry: JksEntry = {\n type: 'trustedCert',\n alias: c.friendlyName || `证书 ${idx + 1}`,\n createdMs: 0,\n certs: [{ type: 'X.509', der: c.der }],\n }\n views.push({ entry, index: idx, certs: [{ summary: parseCertificate(c.der), id: `${idx}.0` }] })\n idx++\n })\n entryViews.value = views\n}\n\nfunction submitPkcs12Password(buf: ArrayBuffer): void {\n const r = readPkcs12(buf, password.value)\n if (!r.ok) {\n unlocked.value = false\n entryViews.value = []\n for (const k of Object.keys(keyResults)) delete keyResults[Number(k)]\n if (r.reason === 'wrong-password') {\n integrity.value = 'bad'\n p12error.value = ''\n } else {\n integrity.value = 'unknown'\n p12error.value =\n (r.reason === 'unsupported' ? '暂不支持的加密方式:' : '解析失败:') + (r.detail ?? '')\n }\n return\n }\n p12error.value = ''\n p12meta.value = r.result\n buildPkcs12EntryViews(r.result)\n integrity.value = r.result.macVerified === true ? 'ok' : 'unknown'\n unlocked.value = true\n fingerprintAll()\n}\n\n// Apply the password to whichever format this keystore is.\nfunction submitPassword(): void {\n if (!password.value) return\n pwTried.value = true\n if (format.value === 'pkcs12') {\n if (rawBuf.value) submitPkcs12Password(rawBuf.value)\n } else if (jksStore.value) {\n submitJksPassword(jksStore.value)\n }\n}\n\n// ── friendly-field formatting (shared with the cert cards) ─────────────────────────────────────────\nfunction formatDn(rdns: CertSummary['subject']): string {\n return rdns.map((r) => `${r.name}=${r.value}`).join(', ')\n}\n\nconst ENTRY_LABEL: Record<JksEntry['type'], string> = {\n privateKey: '私钥条目',\n trustedCert: '受信任证书',\n secretKey: '密钥条目',\n}\nconst ENTRY_CLASS: Record<JksEntry['type'], string> = {\n privateKey: 'jks__badge--key',\n trustedCert: 'jks__badge--cert',\n secretKey: 'jks__badge--secret',\n}\n\nfunction formatDate(ms: number): string {\n if (!ms) return ''\n try {\n return new Date(ms).toLocaleString('zh-CN', { hour12: false })\n } catch {\n return ''\n }\n}\n\nfunction publicKeyText(s: CertSummary): string {\n const parts: string[] = []\n if (s.pubKeyAlg) parts.push(s.pubKeyAlg)\n if (s.pubKeyBits) parts.push(`${s.pubKeyBits} 位`)\n if (s.pubKeyCurve && s.pubKeyCurve !== s.pubKeyAlg) parts.push(s.pubKeyCurve)\n return parts.join(' · ')\n}\n\n// Validity badge: future / expired / soon (≤30 days) / valid, from notBefore/notAfter vs now.\nfunction expiry(s: CertSummary): { text: string; cls: string } | null {\n if (s.notAfterMs == null) return null\n const now = Date.now()\n if (s.notBeforeMs != null && now < s.notBeforeMs)\n return { text: '尚未生效', cls: 'jks__cbadge--warn' }\n if (now > s.notAfterMs) return { text: '已过期', cls: 'jks__cbadge--bad' }\n const days = Math.floor((s.notAfterMs - now) / 86_400_000)\n if (days <= 30) return { text: `${days} 天后过期`, cls: 'jks__cbadge--warn' }\n return { text: `有效(剩 ${days} 天)`, cls: 'jks__cbadge--ok' }\n}\n\n// The private-key row's decrypted/locked/error state for one entry.\ntype KeyState =\n | { kind: 'locked' }\n | { kind: 'ok'; text: string }\n | { kind: 'wrong' }\n | { kind: 'unsupported'; detail?: string }\n | { kind: 'malformed' }\n\nfunction keyState(index: number): KeyState {\n const r = keyResults[index]\n if (!r) return { kind: 'locked' }\n if (r.ok) {\n const parts = [r.key.algorithm]\n if (r.key.bits) parts.push(`${r.key.bits} 位`)\n if (r.key.curve && r.key.curve !== r.key.algorithm) parts.push(r.key.curve)\n return { kind: 'ok', text: parts.join(' · ') }\n }\n if (r.reason === 'wrong-password') return { kind: 'wrong' }\n if (r.reason === 'unsupported') return { kind: 'unsupported', detail: r.detail }\n return { kind: 'malformed' }\n}\n\nconst pwPlaceholder = computed(() => {\n if (isPkcs12.value)\n return unlocked.value ? '已解锁(可重新输入)' : '输入 PKCS#12 密码解密证书与私钥'\n return hasPrivateKeys.value ? '输入密码:解密私钥并校验完整性' : '输入库密码校验完整性(可选)'\n})\n\n// Integrity/unlock status line after a password attempt (format-aware).\nconst integrityText = computed(() => {\n if (integrity.value === 'bad') {\n return isPkcs12.value ? '密码错误 —— 无法解密' : '完整性校验未通过 —— 密码错误或文件已被修改'\n }\n if (integrity.value === 'ok') {\n if (isPkcs12.value) return `密码正确,完整性校验通过(${p12meta.value?.macAlgorithm ?? 'MAC'})`\n return '完整性校验通过 —— 密码正确,keystore 未被篡改'\n }\n // PKCS#12 unlocked but MAC absent/unsupported: still a successful decrypt.\n if (isPkcs12.value && unlocked.value) return '已解密(该文件无 MAC 或摘要不支持,密码经解密验证)'\n return ''\n})\nconst integrityOk = computed(() => integrity.value === 'ok' || (isPkcs12.value && unlocked.value))\n\nwatch(() => [props.url, props.name], load, { immediate: true })\nonBeforeUnmount(() => {\n // nothing to revoke; bytes are not retained as object URLs\n})\n</script>\n\n<template>\n <div class=\"jks\">\n <p v-if=\"status === 'loading'\" class=\"jks__msg\">正在解析 keystore…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"jks__msg jks__gate\">\n <p>该文件较大({{ formatBytes(props.size) }}),将在浏览器内解析。是否继续?</p>\n <button type=\"button\" class=\"jks__btn\" @click=\"doLoad\">解析并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"jks__msg jks__msg--error\">\n <p>无法解析:{{ errorMsg }}</p>\n <button type=\"button\" class=\"jks__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <div v-else-if=\"format\" class=\"jks__scroll\">\n <!-- store header + password -->\n <section class=\"jks__store\">\n <header class=\"jks__storehead\">\n <span class=\"jks__badge jks__badge--type\">{{ storeTypeLabel }}</span>\n <span class=\"jks__storetitle\">{{ isPkcs12 ? 'PKCS#12 密钥库' : 'Java KeyStore' }}</span>\n <span class=\"jks__storemeta\">{{ storeMeta }}</span>\n </header>\n\n <div class=\"jks__pw\" :class=\"{ 'jks__pw--ok': integrityOk }\">\n <Icon name=\"key\" :size=\"14\" />\n <input\n v-model=\"password\"\n type=\"password\"\n class=\"jks__pwinput\"\n :placeholder=\"pwPlaceholder\"\n autocomplete=\"off\"\n @keyup.enter=\"submitPassword\"\n />\n <button type=\"button\" class=\"jks__pwbtn\" @click=\"submitPassword\">\n {{ isPkcs12 && unlocked ? '重新解锁' : '解锁' }}\n </button>\n </div>\n\n <p\n v-if=\"pwTried && integrityText\"\n class=\"jks__integrity\"\n :class=\"`jks__integrity--${integrity}`\"\n >\n <Icon :name=\"integrityOk ? 'check' : 'close'\" :size=\"13\" />\n <span>{{ integrityText }}</span>\n </p>\n\n <p class=\"jks__hint\">全部解析与解密均在浏览器本地完成,密码与密钥不会上传。</p>\n <p v-if=\"jksStore?.truncatedNote\" class=\"jks__warn\">{{ jksStore.truncatedNote }}</p>\n <p v-if=\"p12error\" class=\"jks__warn\">{{ p12error }}</p>\n <p v-for=\"(w, i) in p12meta?.warnings ?? []\" :key=\"i\" class=\"jks__warn\">{{ w }}</p>\n </section>\n\n <!-- PKCS#12 pre-unlock: nothing is visible until decrypted -->\n <div v-if=\"isPkcs12 && !unlocked\" class=\"jks__locked\">\n <Icon name=\"key\" :size=\"26\" class=\"jks__lockicon\" />\n <p>PKCS#12 密钥库的证书与私钥均已加密,输入上方密码后在此显示内容。</p>\n </div>\n\n <!-- entries -->\n <section v-for=\"ev in entryViews\" :key=\"ev.index\" class=\"jks__entry\">\n <header class=\"jks__entryhead\">\n <span class=\"jks__badge\" :class=\"ENTRY_CLASS[ev.entry.type]\">{{\n ENTRY_LABEL[ev.entry.type]\n }}</span>\n <span class=\"jks__alias\" :title=\"ev.entry.alias\">{{ ev.entry.alias }}</span>\n <span v-if=\"ev.entry.createdMs\" class=\"jks__date\">{{\n formatDate(ev.entry.createdMs)\n }}</span>\n </header>\n\n <!-- private key material -->\n <div v-if=\"ev.entry.type === 'privateKey'\" class=\"jks__key\">\n <template v-if=\"keyState(ev.index).kind === 'locked'\">\n <Icon name=\"key\" :size=\"13\" class=\"jks__keyicon\" />\n <span class=\"jks__keytext\">私钥已加密 —— 输入密码后在此显示密钥算法</span>\n </template>\n <template v-else-if=\"keyState(ev.index).kind === 'ok'\">\n <Icon name=\"check\" :size=\"13\" class=\"jks__keyicon jks__keyicon--ok\" />\n <span class=\"jks__keytext jks__keytext--ok\"\n >私钥已解密 · {{ (keyState(ev.index) as { text: string }).text }}</span\n >\n </template>\n <template v-else-if=\"keyState(ev.index).kind === 'wrong'\">\n <Icon name=\"close\" :size=\"13\" class=\"jks__keyicon jks__keyicon--bad\" />\n <span class=\"jks__keytext jks__keytext--bad\">密码错误 —— 无法解密该私钥</span>\n </template>\n <template v-else-if=\"keyState(ev.index).kind === 'unsupported'\">\n <Icon name=\"key\" :size=\"13\" class=\"jks__keyicon\" />\n <span class=\"jks__keytext\"\n >该私钥加密方式暂不支持浏览器内解密({{\n (keyState(ev.index) as { detail?: string }).detail\n }})—— 证书链仍可查看</span\n >\n </template>\n <template v-else>\n <Icon name=\"close\" :size=\"13\" class=\"jks__keyicon jks__keyicon--bad\" />\n <span class=\"jks__keytext jks__keytext--bad\">私钥数据无法解析</span>\n </template>\n </div>\n\n <p v-else-if=\"ev.entry.type === 'secretKey'\" class=\"jks__note\">\n JCEKS 对称密钥条目(SecretKey)—— 以 Java 序列化对象存储,暂不解析。\n </p>\n\n <!-- certificate chain -->\n <div v-if=\"ev.certs.length\" class=\"jks__chain\">\n <div v-if=\"ev.entry.type === 'privateKey'\" class=\"jks__chainlabel\">\n 证书链({{ ev.certs.length }})\n </div>\n <article v-for=\"cv in ev.certs\" :key=\"cv.id\" class=\"jks__cert\">\n <div class=\"jks__certhead\">\n <span class=\"jks__certcn\">{{ cv.summary.subjectCN || cv.summary.label }}</span>\n <span v-if=\"expiry(cv.summary)\" class=\"jks__cbadge\" :class=\"expiry(cv.summary)?.cls\">\n {{ expiry(cv.summary)?.text }}\n </span>\n </div>\n <p v-if=\"cv.summary.note && !cv.summary.ok\" class=\"jks__note\">{{ cv.summary.note }}</p>\n <dl class=\"jks__dl\">\n <template v-if=\"cv.summary.subject.length\">\n <dt>主题</dt>\n <dd>{{ formatDn(cv.summary.subject) }}</dd>\n </template>\n <template v-if=\"cv.summary.issuer.length\">\n <dt>颁发者</dt>\n <dd>{{ formatDn(cv.summary.issuer) }}</dd>\n </template>\n <template v-if=\"cv.summary.notBefore || cv.summary.notAfter\">\n <dt>有效期</dt>\n <dd>{{ cv.summary.notBefore ?? '?' }} → {{ cv.summary.notAfter ?? '?' }}</dd>\n </template>\n <template v-if=\"publicKeyText(cv.summary)\">\n <dt>公钥</dt>\n <dd>{{ publicKeyText(cv.summary) }}</dd>\n </template>\n <template v-if=\"cv.summary.sigAlg\">\n <dt>签名算法</dt>\n <dd>{{ cv.summary.sigAlg }}</dd>\n </template>\n <template v-if=\"cv.summary.serialHex\">\n <dt>序列号</dt>\n <dd class=\"jks__mono\">{{ cv.summary.serialHex }}</dd>\n </template>\n <template v-if=\"cv.summary.sans.length\">\n <dt>备用名 (SAN)</dt>\n <dd>{{ cv.summary.sans.join(',') }}</dd>\n </template>\n <template v-if=\"fingerprints[cv.id]\">\n <dt>SHA-256</dt>\n <dd class=\"jks__mono jks__fp\">{{ fingerprints[cv.id] }}</dd>\n </template>\n </dl>\n </article>\n </div>\n </section>\n </div>\n </div>\n</template>\n\n<style scoped>\n.jks {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.jks__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.jks__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.jks__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.jks__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.jks__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.jks__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: 14px 14px 60px;\n}\n\n/* Store header + password ---------------------------------------------------------------------- */\n.jks__store {\n margin-bottom: 16px;\n border: 1px solid var(--border);\n border-radius: 10px;\n background: var(--surface);\n overflow: hidden;\n}\n.jks__storehead {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 12px;\n background: var(--surface-2);\n border-bottom: 1px solid var(--border);\n}\n.jks__storetitle {\n flex-shrink: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.jks__storemeta {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n text-align: right;\n font-size: 11.5px;\n color: var(--faint);\n}\n\n.jks__pw {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 12px;\n color: var(--muted);\n}\n.jks__pw--ok {\n color: var(--accent);\n}\n.jks__pwinput {\n flex: 1 1 auto;\n min-width: 0;\n height: 28px;\n padding: 0 9px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n}\n.jks__pwinput:focus {\n outline: none;\n border-color: var(--accent);\n}\n.jks__pwbtn {\n flex-shrink: 0;\n height: 28px;\n padding: 0 14px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n}\n.jks__pwbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.jks__integrity {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0;\n padding: 0 12px 8px;\n font-size: 12px;\n}\n.jks__integrity--ok {\n color: var(--hl-name);\n}\n.jks__integrity--bad {\n color: var(--danger);\n}\n.jks__integrity--unknown {\n color: var(--accent);\n}\n.jks__hint {\n margin: 0;\n padding: 0 12px 10px;\n font-size: 11.5px;\n line-height: 1.5;\n color: var(--faint);\n}\n.jks__warn {\n margin: 0;\n padding: 9px 12px;\n font-size: 12px;\n line-height: 1.5;\n color: var(--ansi-yellow);\n background: color-mix(in srgb, var(--ansi-yellow) 12%, transparent);\n border-top: 1px solid var(--border);\n}\n\n.jks__locked {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 10px;\n padding: 40px 24px;\n text-align: center;\n color: var(--muted);\n font-size: 12.5px;\n}\n.jks__lockicon {\n color: var(--faint);\n}\n\n/* Badges ------------------------------------------------------------------------------------- */\n.jks__badge {\n flex-shrink: 0;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.02em;\n border-radius: 5px;\n padding: 2px 7px;\n}\n.jks__badge--type {\n color: var(--accent);\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n}\n.jks__badge--key {\n color: var(--ansi-yellow);\n background: color-mix(in srgb, var(--ansi-yellow) 16%, transparent);\n}\n.jks__badge--cert {\n color: var(--hl-name);\n background: color-mix(in srgb, var(--hl-name) 16%, transparent);\n}\n.jks__badge--secret {\n color: var(--muted);\n background: var(--surface-2);\n}\n\n/* Entry cards -------------------------------------------------------------------------------- */\n.jks__entry {\n margin-bottom: 14px;\n border: 1px solid var(--border);\n border-radius: 10px;\n background: var(--surface);\n overflow: hidden;\n}\n.jks__entryhead {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 12px;\n border-bottom: 1px solid var(--border);\n}\n.jks__alias {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.jks__date {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n\n.jks__key {\n display: flex;\n align-items: center;\n gap: 7px;\n padding: 9px 12px;\n font-size: 12px;\n background: color-mix(in srgb, var(--accent) 4%, transparent);\n border-bottom: 1px solid var(--border);\n}\n.jks__keyicon {\n flex-shrink: 0;\n color: var(--faint);\n}\n.jks__keyicon--ok {\n color: var(--hl-name);\n}\n.jks__keyicon--bad {\n color: var(--danger);\n}\n.jks__keytext {\n min-width: 0;\n color: var(--muted);\n}\n.jks__keytext--ok {\n color: var(--hl-name);\n font-weight: 500;\n}\n.jks__keytext--bad {\n color: var(--danger);\n}\n\n.jks__note {\n margin: 0;\n padding: 9px 12px;\n font-size: 12px;\n line-height: 1.5;\n color: var(--muted);\n}\n\n/* Certificate chain -------------------------------------------------------------------------- */\n.jks__chain {\n padding: 10px 12px 12px;\n}\n.jks__chainlabel {\n margin-bottom: 8px;\n font-size: 11.5px;\n color: var(--faint);\n}\n.jks__cert {\n margin-bottom: 10px;\n border: 1px solid var(--border);\n border-radius: 8px;\n overflow: hidden;\n}\n.jks__cert:last-child {\n margin-bottom: 0;\n}\n.jks__certhead {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 10px;\n background: var(--surface-2);\n border-bottom: 1px solid var(--border);\n}\n.jks__certcn {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 12.5px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.jks__cbadge {\n flex-shrink: 0;\n font-size: 11px;\n font-weight: 600;\n border-radius: 5px;\n padding: 2px 8px;\n}\n.jks__cbadge--ok {\n color: var(--hl-name);\n background: color-mix(in srgb, var(--hl-name) 16%, transparent);\n}\n.jks__cbadge--warn {\n color: var(--ansi-yellow);\n background: color-mix(in srgb, var(--ansi-yellow) 18%, transparent);\n}\n.jks__cbadge--bad {\n color: var(--danger);\n background: color-mix(in srgb, var(--danger) 16%, transparent);\n}\n.jks__dl {\n display: grid;\n grid-template-columns: max-content 1fr;\n gap: 6px 14px;\n margin: 0;\n padding: 10px;\n}\n.jks__dl dt {\n font-size: 12px;\n color: var(--faint);\n white-space: nowrap;\n}\n.jks__dl dd {\n margin: 0;\n font-size: 12.5px;\n color: var(--text);\n word-break: break-word;\n}\n.jks__mono {\n font-family: var(--font-mono);\n font-size: 11.5px;\n}\n.jks__fp {\n word-break: break-all;\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import e from "../_virtual/_plugin-vue_export-helper.js";
|
|
|
2
2
|
import t from "./LogViewer.vue_vue_type_script_setup_true_lang.js";
|
|
3
3
|
/* empty css */
|
|
4
4
|
//#region src/components/LogViewer.vue
|
|
5
|
-
var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-
|
|
5
|
+
var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-3ecea7f7"]]);
|
|
6
6
|
//#endregion
|
|
7
7
|
export { n as default };
|
|
8
8
|
|