This commit is contained in:
sharevb 2024-11-30 22:28:47 +09:00 committed by GitHub
commit f6bfd2f850
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1573 additions and 23 deletions

1
components.d.ts vendored
View File

@ -33,6 +33,7 @@ declare module '@vue/runtime-core' {
CCollapse: typeof import('./src/ui/c-collapse/c-collapse.vue')['default']
'CCollapse.demo': typeof import('./src/ui/c-collapse/c-collapse.demo.vue')['default']
CDiffEditor: typeof import('./src/ui/c-diff-editor/c-diff-editor.vue')['default']
CertificateKeyParser: typeof import('./src/tools/certificate-key-parser/certificate-key-parser.vue')['default']
CFileUpload: typeof import('./src/ui/c-file-upload/c-file-upload.vue')['default']
'CFileUpload.demo': typeof import('./src/ui/c-file-upload/c-file-upload.demo.vue')['default']
ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default']

View File

@ -69,7 +69,7 @@
"highlight.js": "^11.7.0",
"iarna-toml-esm": "^3.0.5",
"ibantools": "^4.3.3",
"js-base64": "^3.7.6",
"js-base64": "^3.7.7",
"json5": "^2.2.3",
"jwt-decode": "^3.1.2",
"libphonenumber-js": "^1.10.28",
@ -82,6 +82,7 @@
"naive-ui": "^2.35.0",
"netmask": "^2.0.2",
"node-forge": "^1.3.1",
"openpgp": "^5.11.1",
"oui-data": "^1.0.10",
"pdf-signature-reader": "^1.4.2",
"pinia": "^2.0.34",
@ -89,6 +90,7 @@
"qrcode": "^1.5.1",
"randexp": "^0.5.3",
"sql-formatter": "^13.0.0",
"sshpk": "^1.18.0",
"ua-parser-js": "^1.0.35",
"ulid": "^2.3.0",
"unicode-emoji-json": "^0.4.0",
@ -121,6 +123,7 @@
"@types/node": "^18.15.11",
"@types/node-forge": "^1.3.2",
"@types/qrcode": "^1.5.0",
"@types/sshpk": "^1.17.4",
"@types/ua-parser-js": "^0.7.36",
"@types/uuid": "^9.0.0",
"@unocss/eslint-config": "^0.57.0",
@ -142,6 +145,7 @@
"unplugin-icons": "^0.17.0",
"unplugin-vue-components": "^0.25.0",
"vite": "^4.4.9",
"vite-plugin-node-polyfills": "^0.21.0",
"vite-plugin-pwa": "^0.16.0",
"vite-plugin-vue-markdown": "^0.23.5",
"vite-svg-loader": "^4.0.0",

732
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,12 @@
import { useVModel } from '@vueuse/core';
import { useCopy } from '@/composable/copy';
const props = defineProps<{ value: string }>();
const props = defineProps<{
value: string
multiline?: boolean
rows?: number | string
autosize?: boolean
}>();
const emit = defineEmits(['update:value']);
const value = useVModel(props, 'value', emit);
@ -11,7 +16,12 @@ const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : 'Copy to cli
</script>
<template>
<c-input-text v-model:value="value">
<c-input-text
v-model:value="value"
:multiline="multiline"
:rows="rows"
:autosize="autosize"
>
<template #suffix>
<c-tooltip :tooltip="tooltipText">
<c-button circle variant="text" size="small" @click="copy()">

View File

@ -17,6 +17,7 @@ const props = withDefaults(
language?: string
copyPlacement?: 'top-right' | 'bottom-right' | 'outside' | 'none'
copyMessage?: string
wordWrap?: boolean
}>(),
{
followHeightOf: null,
@ -49,7 +50,7 @@ const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : copyMessage.
:style="height ? `min-height: ${height - 40 /* card padding */ + 10 /* negative margin compensation */}px` : ''"
>
<n-config-provider :hljs="hljs">
<n-code :code="value" :language="language" :trim="false" data-test-id="area-content" />
<n-code :code="value" :language="language" :word-wrap="wordWrap" :trim="false" data-test-id="area-content" />
</n-config-provider>
</n-scrollbar>
<div absolute right-10px top-10px>

View File

@ -1,6 +1,7 @@
import { extension as getExtensionFromMimeType, extension as getMimeTypeFromExtension } from 'mime-types';
import type { Ref } from 'vue';
import type { MaybeRef, Ref } from 'vue';
import _ from 'lodash';
import { get } from '@vueuse/core';
export {
getMimeTypeFromBase64,
@ -75,21 +76,11 @@ function downloadFromBase64({ sourceValue, filename, extension, fileMimeType }:
}
function useDownloadFileFromBase64(
{ source, filename, extension, fileMimeType }:
{ source: Ref<string>; filename?: string; extension?: string; fileMimeType?: string }) {
return {
download() {
downloadFromBase64({ sourceValue: source.value, filename, extension, fileMimeType });
},
};
}
function useDownloadFileFromBase64Refs(
{ source, filename, extension }:
{ source: Ref<string>; filename?: Ref<string>; extension?: Ref<string> }) {
{ source: MaybeRef<string>; filename?: MaybeRef<string>; extension?: MaybeRef<string> }) {
return {
download() {
downloadFromBase64({ sourceValue: source.value, filename: filename?.value, extension: extension?.value });
downloadFromBase64({ sourceValue: get(source), filename: get(filename), extension: get(extension) });
},
};
}
@ -116,3 +107,13 @@ function previewImageFromBase64(base64String: string): HTMLImageElement {
return img;
}
function useDownloadFileFromBase64Refs(
{ source, filename, extension }:
{ source: Ref<string>; filename?: Ref<string>; extension?: Ref<string> }) {
return {
download() {
downloadFromBase64({ sourceValue: source.value, filename: filename?.value, extension: extension?.value });
},
};
}

View File

@ -0,0 +1,333 @@
import type {
Certificate,
Fingerprint,
Key,
PrivateKey, Signature,
} from 'sshpk';
import type * as openpgp from 'openpgp';
import * as forge from 'node-forge';
export interface LabelValue {
label: string
value: string
multiline?: boolean
}
function onErrorReturnErrorMessage(func: () => any) {
try {
return func();
}
catch (e: any) {
return e.toString();
}
}
function buf2Hex(buffer: ArrayBuffer) { // buffer is an ArrayBuffer
return [...new Uint8Array(buffer)]
.map(x => x.toString(16).padStart(2, '0'))
.join('');
}
export function getPublicKeyLabelValues(publicKey: Key) {
return [
{
label: 'Type:',
value: 'Public Key',
},
{
label: 'Key Type:',
value: publicKey.type,
},
{
label: 'Size:',
value: publicKey.size,
},
{
label: 'Comment:',
value: publicKey.comment,
multiline: true,
},
{
label: 'Curve:',
value: publicKey.curve ?? 'none',
},
{
label: 'Fingerprint (sha256):',
value: onErrorReturnErrorMessage(() => publicKey.fingerprint('sha256')),
multiline: true,
},
{
label: 'Fingerprint (sha512):',
value: onErrorReturnErrorMessage(() => publicKey.fingerprint('sha512')),
multiline: true,
},
] as LabelValue[];
}
export function getPrivateKeyLabelValues(privateKey: PrivateKey) {
return [
{
label: 'Type:',
value: 'Private Key',
},
{
label: 'Key Type:',
value: privateKey.type,
},
{
label: 'Size:',
value: privateKey.size,
},
{
label: 'Comment:',
value: privateKey.comment,
multiline: true,
},
{
label: 'Curve:',
value: privateKey.curve,
},
{
label: 'Fingerprint (sha256):',
value: onErrorReturnErrorMessage(() => privateKey.fingerprint('sha256')),
multiline: true,
},
{
label: 'Fingerprint (sha512):',
value: onErrorReturnErrorMessage(() => privateKey.fingerprint('sha512')),
multiline: true,
},
] as LabelValue[];
}
export function getCertificateLabelValues(cert: Certificate) {
return [
{
label: 'Type:',
value: 'Certificate',
},
{
label: 'Subjects:',
value: cert.subjects?.map(s => s.toString()).join('\n'),
multiline: true,
},
{
label: 'Issuer:',
value: cert.issuer.toString(),
multiline: true,
},
{
label: 'Subject Key:',
value: onErrorReturnErrorMessage(() => cert.subjectKey?.toString('ssh')),
multiline: true,
},
{
label: 'Subject Key Type:',
value: cert.subjectKey?.type,
},
{
label: 'Subject Size:',
value: cert.subjectKey?.size,
},
{
label: 'Subject Comment:',
value: cert.subjectKey?.comment,
multiline: true,
},
{
label: 'Subject Curve:',
value: cert.subjectKey?.curve ?? 'none',
},
{
label: 'Issuer Key:',
value: onErrorReturnErrorMessage(() => cert.issuerKey?.toString('ssh')),
multiline: true,
},
{
label: 'Serial:',
value: buf2Hex(cert.serial),
},
{
label: 'Purposes:',
value: cert.purposes?.join(', '),
},
{
label: 'Extensions:',
value: JSON.stringify(cert.getExtensions(), null, 2),
multiline: true,
},
{
label: 'Fingerprint (sha256):',
value: onErrorReturnErrorMessage(() => cert.fingerprint('sha256')),
multiline: true,
},
{
label: 'Fingerprint (sha512):',
value: onErrorReturnErrorMessage(() => cert.fingerprint('sha512')),
multiline: true,
},
{
label: 'Certificate (pem):',
value: onErrorReturnErrorMessage(() => cert.toString('pem')),
multiline: true,
},
] as LabelValue[];
}
export async function getPGPPublicKeyLabelValuesAsync(pgpPublicKey: openpgp.Key) {
return [
{
label: 'Type:',
value: 'PGP Public Key',
},
{
label: 'Creation Time:',
value: pgpPublicKey.getCreationTime().toString(),
},
{
label: 'Expiration Time:',
value: (await pgpPublicKey.getExpirationTime())?.toString() || '',
},
{
label: 'Algorithm Info:',
value: JSON.stringify(pgpPublicKey.getAlgorithmInfo()),
},
{
label: 'Fingerprint:',
value: pgpPublicKey.getFingerprint(),
},
{
label: 'User ID(s):',
value: pgpPublicKey.getUserIDs().join(', '),
},
{
label: 'Key ID(s):',
value: pgpPublicKey.getKeyIDs().map(k => k.toHex()).join(' ; '),
},
] as LabelValue[];
}
export async function getPGPPrivateKeyLabelValuesAsync(pgpPrivateKey: openpgp.Key) {
return [
{
label: 'Type:',
value: 'PGP Private Key',
},
{
label: 'Creation Time:',
value: pgpPrivateKey.getCreationTime().toString(),
},
{
label: 'Expiration Time:',
value: (await pgpPrivateKey.getExpirationTime())?.toString() || '',
},
{
label: 'Algorithm Info:',
value: JSON.stringify(pgpPrivateKey.getAlgorithmInfo()),
},
{
label: 'Fingerprint:',
value: pgpPrivateKey.getFingerprint(),
},
{
label: 'User ID(s):',
value: pgpPrivateKey.getUserIDs().join(', '),
},
{
label: 'Key ID(s):',
value: pgpPrivateKey.getKeyIDs().map(k => k.toHex()).join(' ; '),
},
] as LabelValue[];
}
export function getCSRLabelValues(csr: forge.pki.Certificate) {
return [
{
label: 'Type:',
value: 'Certificate Signing Request',
},
{
label: 'Subject:',
value: csr.subject?.attributes?.map(a => JSON.stringify(a, null, 2)).join('\n'),
multiline: true,
},
{
label: 'Issuer:',
value: csr.issuer?.toString(),
multiline: true,
},
{
label: 'Validity:',
value: JSON.stringify(csr.validity, null, 2),
},
{
label: 'Signature:',
value: csr.signature,
},
{
label: 'Signature Oid:',
value: csr.signatureOid?.toString(),
},
{
label: 'Signature parameters:',
value: JSON.stringify(csr.signatureParameters, null, 2),
},
{
label: 'Signing info:',
value: JSON.stringify(csr.siginfo, null, 2),
},
{
label: 'Serial:',
value: csr.serialNumber?.toString(),
},
{
label: 'Extensions:',
value: JSON.stringify(csr.extensions, null, 2),
multiline: true,
},
{
label: 'Public Key:',
value: onErrorReturnErrorMessage(() => forge.pki.publicKeyToPem(csr.publicKey)),
multiline: true,
},
{
label: 'Public Key Fingerprint:',
value: onErrorReturnErrorMessage(() => forge.pki.getPublicKeyFingerprint(csr.publicKey)?.toHex()),
multiline: true,
},
] as LabelValue[];
}
export function getFingerprintLabelValues(fingerprint: Fingerprint) {
return [
{
label: 'Type:',
value: 'Fingerprint',
},
{
label: 'Fingerprint (hex):',
value: fingerprint.toString('hex'),
},
{
label: 'Fingerprint (base64):',
value: fingerprint.toString('base64'),
},
] as LabelValue[];
}
export function getSignatureLabelValues(signature: Signature) {
return [
{
label: 'Type:',
value: 'Signature',
},
{
label: 'Fingerprint (asn1):',
value: signature.toString('asn1'),
},
{
label: 'Fingerprint (ssh):',
value: signature.toString('ssh'),
},
] as LabelValue[];
}

View File

@ -0,0 +1,201 @@
import { describe, expect, it } from 'vitest';
import { getKeyOrCertificateInfosAsync } from './certificate-key-parser.service';
const encryptedPrivateKey = /* NOSONAR */ `-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFHzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQILjmiBkdY16UCAggA
MB0GCWCGSAFlAwQBAgQQ+sYf2MO9hoZ4F5+LdE2vRgSCBNC6CLgqMJ6fKS3YnMMJ
yc3/iuqRniP/11OllTmOr94/2dl6Xk8TndBqhAacYizxgNCHiPP+NEmwtPqbbE+a
boNqEh0m8NUegxc3qGzon8cJgobgvpw7eml4K1OjgxCw58Y1VQixSKpdHEC1E1o6
RBBwK5bIld97GEi4VPuqYoYvffcD5cDsj9HxqvEWGTXDtu/nKEA0cVe7wI8t1CtR
/kaFoCyYOFu9RE0YprcWT1GpYTHR3TE7+lXYR8vPGdMCMgww1mLCXDAz1G4Y5+1K
WAmiVn3uQqgR9rU/upaM01oOz5LJWtgi5gDV8zX0r41i8pYYQNvGiSvvw1pQJB5Q
sFyuVNU6hOCQ+zLssfmxnhRpgM3hn07gV+LFAg+ly6WQsl9W6m8WpJ7SgLeByb7g
teT+ml7wTuFZIiPbqD3Pq0grRZir3n3NuphUk9YwR2jN4hHXEDiQ9f4D4SY41RT3
lW7roK+oM1K3N4cDin+WyGiFFLyWrwrlXWmHN5A3pOf+0rQUooZdUTGSU0N9v9Ho
17x/+aDAeCMEl7y0GfEoglNxCjoVj70E9oJL21amziZAUOXobhuzeb0dWJmAtXoj
wETYW6QH8m80eEyvKfkLsQ2Sd360ILhRJtyN1HFJAQbsC3C0VYiqA3kjN4S1Zfeg
08/odqdi0a7GSTm/h+iT3rimXXS2TLfSYIzI14LZDCeCz33tX13WQrhGPbYEJBHX
PKc1ws0HHBwdpM8d+liPHm+Czt2/sbcGCNBWSPWWZpL+uzeh9HQYEUoefk88JCPD
xQlANh8BCzq0L1pYNZdOYVNb0fT1XluuP8xtIePsNHIsKRURDlCjaEAY41DwXmTp
DFRd41BLFZztX5jtMGw4lb3RplcaaRhxCEF51hRJQdb5HMpn4cr2Hqy5UP+ke2vV
0DVRi0jp7Mkp/+qdEEotWT1AxSeYoiW7j/GwlC8tqAC9NoMmcHAuOGF5fHshxz/L
lnWSHiPjfLezpfryBwb5D4+3/TFEzc8gPsco/Ip7qU1+wMObTLs9Nq1ROW/aClYU
A/a5DyCQUHaHyYRse/BLTXxr2gsMCm7qaTdCy+pZcL1f+YEISHtITuA38eGfQzTX
cX/k75t7+mKW4nKBBT/SZbBT27gEeZpAn8ORaMixedBQZmoFHLKNlrf+F+fBFfhp
J4NFeBFnEpa3YrPVgLTJY6yN0gummIC8GA7EpggdAcNTbp5EU+IHlHhExhzr+W+f
YCOdD4Zt6LMZjAlpId2APn9NMscpwT59K/61n1CjElpWPwfTW2hyto7/1q0fIgwK
z2E615qLFJa+EFR8hTFz0wjNUOCrDgS7K7STQbGaFfjmAe2LUFZhTm86u6K0fnUY
sNJeDSnvDYEQD1MUiezD06MmzdEHqJHNKztoahqPsQIktH84RGdc1oTPMS4PLwLM
JJmEHqLF7I8T6L+BvMp2LhZTrx3g1qU4wZRC5Rys7J5WR5E+v8XttEcViEO4Lrdr
wNRoroHuXLw4nOzM58DS5cHGliw4BeErQ6XC0aan2EM789Us3Hrx0zerfIOyUdBe
N39sh8X4jo7YHMBH3yqVsAIU3e8c2Z2rayP7+AyUncAfff9EH3BNpIkQIG3xsqh1
oimmuNBEFKy1F1rSP3NQvwcZVw==
-----END ENCRYPTED PRIVATE KEY-----
`;
const openSSHPrivateKey = /* NOSONAR */ `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEA4r66AJdnJBwsKDyTr4QizQYMVhaFtcBcMcVuJHbKW/DtG985oA2V
hJRfcBz8MKNS9iTLRkgq4yoTVCIi5FDVU4EnQbLMoLfN2og6PpgSSL59bPUvYoenp6RS1i
R606eVakH/J6IIiadHMWBrLjCiChf5U/bvuSkUQWi0YLH9hI6Nw+S9MdwzBofshZZdww9Y
4udrWEVdfGrxWv5gjlaquxYZAnfaYCI0Pgh9v1nEALFufOVi1onUZaxduyvsLoQnXiwP2c
fWXsBYmjfViTTADTvHTLCF9IH+6heGk2WJkosgiajmMlc+1U7Xs+r5ZJ0ikt0ioydPN8Hi
LnVih6lENwAAA7hrsy+Ha7MvhwAAAAdzc2gtcnNhAAABAQDivroAl2ckHCwoPJOvhCLNBg
xWFoW1wFwxxW4kdspb8O0b3zmgDZWElF9wHPwwo1L2JMtGSCrjKhNUIiLkUNVTgSdBssyg
t83aiDo+mBJIvn1s9S9ih6enpFLWJHrTp5VqQf8nogiJp0cxYGsuMKIKF/lT9u+5KRRBaL
Rgsf2Ejo3D5L0x3DMGh+yFll3DD1ji52tYRV18avFa/mCOVqq7FhkCd9pgIjQ+CH2/WcQA
sW585WLWidRlrF27K+wuhCdeLA/Zx9ZewFiaN9WJNMANO8dMsIX0gf7qF4aTZYmSiyCJqO
YyVz7VTtez6vlknSKS3SKjJ083weIudWKHqUQ3AAAAAwEAAQAAAQBmKTj0+0JlaqwalPCV
rBth9M+qGgu0kC753dJ6a2tRcYPjgvgbvQMY8SDvCqA16eB/NqS/zdRE9bgvuBGwfRsgvJ
hLaZv47de6FpbnjOzwCaPJa88lvak0Rz1rbpRIuMEBVyr3WHIwU0YoYSDpdtALbDHSOvhX
nMKblelvh8KJ7jelix2R3llvcYexKdS66zzP7nPj5x8d1FDo2cxqWsy2aQxMlbZGTd3ujQ
ABzZGvI3L0tJRf4sPph/eLS28/teAExp5Uo9DuehqgU33iAYOaO2vZGqQJxBbaVtiarVK1
kRQLBrhieMIUJ9XHSwDn74VHWtBNfocTSPe6vbVMjLpBAAAAgGBTluv8WHqG1JTauYNjBI
EhSKL96MrIZR+fytEg3gcxitPnpQiPTP6XHXpQ7fxVk25bYvCj4QNi00jW6kBR46Zh90NI
nKgYvxdYoA5A7L6JvvByy00SbLssiM7kf3ByT/4EA8S911Q4cks8lKrwEUw+UzqTBR6QdG
JyZQQcUJa+AAAAgQDyIYjECyG91/X+zFYcecW+wWyLBzyEvOxFlRd4tZnvWknQTdtFqTlN
orTT+un1ygKe0DkfwXSbbjE69+xxlMtPQ2X6wd2mUruvtyBv1R8Kfj+doY5lFUfCEKj88u
ck1+Ol1K+KDnvlYZVnb5eCvMxmEMqyD+eTQ2EcNAJtjNmuDQAAAIEA77uU45tseIe9E6OZ
Hum2bUxQmqkpjrNCECiTJR99NUx+22sBZwrMAt3QzBwgSogQhLKAw+keEUG6zAl7UA6Lsc
vJdDllY2vYMRW9LZ1XNCxvl0i6QUsT8l9hwA9GuMQN1m6NRU+cnEU87KIXVBb+DRyZwo21
4WRkAc1Ru/KtrlMAAAAAAQID
-----END OPENSSH PRIVATE KEY-----
`;
const formatsData = [
{
input: `
-----BEGIN PGP PUBLIC KEY BLOCK-----
xjMEZkHgaRYJKwYBBAHaRw8BAQdAdCmEzdpkjMzOoNkzgDFk/CHd+6uYAWkZ
BPbjEzTJWtfNAMKMBBAWCgA+BYJmQeBpBAsJBwgJkEDKj7jnGr9wAxUICgQW
AAIBAhkBApsDAh4BFiEExfkkog7+aHz7TqepQMqPuOcav3AAAJaaAQCayvFQ
jxFbC7oOzX+8wOV8gmXVXXqI5dtLQYY3SeyqmwD/ftVwwe6Prl0vVFyLB/5y
lIpAti8AK1Lv8hIezzOx4QDOOARmQeBpEgorBgEEAZdVAQUBAQdA2jU3Rmt7
nMFvqyjgKdVjK5o2CQI2vJiSzn8cfV1piEgDAQgHwngEGBYKACoFgmZB4GkJ
kEDKj7jnGr9wApsMFiEExfkkog7+aHz7TqepQMqPuOcav3AAABI0AQCMW4Hg
FuIaZk9LVQsUmNknj4a70fzwDYWUYvq0C1iy/QD+KXvLKfcmky5OXJA7RsRV
SN2a4SE4c8FH22uyirzyUww=
=w51K
-----END PGP PUBLIC KEY BLOCK-----
`,
pass: '',
type: 'PGP Public Key',
},
{
// NOSONAR
input: `
-----BEGIN PGP PRIVATE KEY BLOCK-----
xVgEZkHgaRYJKwYBBAHaRw8BAQdAdCmEzdpkjMzOoNkzgDFk/CHd+6uYAWkZ
BPbjEzTJWtcAAQDXcDgEziqd9ZO/OpoyblRRxAOgPq2y8zTitwTz+ixX7RCe
zQDCjAQQFgoAPgWCZkHgaQQLCQcICZBAyo+45xq/cAMVCAoEFgACAQIZAQKb
AwIeARYhBMX5JKIO/mh8+06nqUDKj7jnGr9wAACWmgEAmsrxUI8RWwu6Ds1/
vMDlfIJl1V16iOXbS0GGN0nsqpsA/37VcMHuj65dL1Rciwf+cpSKQLYvACtS
7/ISHs8zseEAx10EZkHgaRIKKwYBBAGXVQEFAQEHQNo1N0Zre5zBb6so4CnV
YyuaNgkCNryYks5/HH1daYhIAwEIBwAA/2PxYHVWBmkLD9eiFDLJ0EtspWQ+
JKui86xylduxQWngEIrCeAQYFgoAKgWCZkHgaQmQQMqPuOcav3ACmwwWIQTF
+SSiDv5ofPtOp6lAyo+45xq/cAAAEjQBAIxbgeAW4hpmT0tVCxSY2SePhrvR
/PANhZRi+rQLWLL9AP4pe8sp9yaTLk5ckDtGxFVI3ZrhIThzwUfba7KKvPJT
DA==
=hSgY
-----END PGP PRIVATE KEY BLOCK-----
`,
pass: '',
type: 'PGP Private Key',
},
{
input: `
-----BEGIN CERTIFICATE REQUEST-----
MIIClTCCAX0CAQAwUDERMA8GA1UEAxMIdGVzdC5jb20xDzANBgNVBAYTBkZyYW5j
ZTELMAkGA1UECBMCRlIxDjAMBgNVBAcTBVBhcmlzMQ0wCwYDVQQKEwRUZXN0MIIB
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnIUCiMFffkHGuAhwTaW84uh7
/03KcfQAZB/bZKqLaxtcBGryTB1gHFSsl3uFlW+tIcjsboWnQ2JB0J1Z5Fp4a6IA
/62S6GTo6dAd9f73TR+P2vQghZOtiCoc7CN2KlosIx/EWMcMjq+CBzLRjjOOR8tX
Yn4ZAhPInO1ZGPMEpfEEfn44aJFRGaMy4KEU+RpTzFKFW6bialvKC3yGPegQ4wcz
AqvyUc9WUwG53HYLSJHldg8tZnpiJBNUh8mXiIiw51MFJ4Q9RVnz9vuoHgC6FmUv
qlg/R4gjGGfjDhAIUtz+Y98Dl+xfLmD+EzY7KQ1ur412BvQ8rXankNGLA2ea+wID
AQABoAAwDQYJKoZIhvcNAQEFBQADggEBABdtkhFSwgaXZWTcKrz6oarvuaQkrjvs
Nk9lUs1h/dfhJpnE3iZA0CuNp5PVQRdC2g+/37r21/udjNFdrX1Rm6/ldG0b2xDu
nQYZcLpIVB0fZ2TB+FHthmGw175I2niWIfNJQhIqnWJXi8unkGTMP2cD6j3axtMi
K8MUVPhWmL11ojEXItG35AU79G6GhFxel9wIByqsXreCUyOcrpYCHy2Fv85ivdE1
JyEQ2tE/f+cKwNg4yJNFoCoHSSFRn61F12J4m2nwpQ77VfD66oVkWtk/gYMrwx0d
4FlJNs+NtZDlcM7fJLNo7YsMdne7hl4aL6WG96kdWdxYEt/2dl3WXbY=
-----END CERTIFICATE REQUEST-----
`,
pass: '',
type: 'Certificate Signing Request',
},
{
input: `
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDivroAl2ckHCwoPJOvhCLNBgxWFoW1wFwxxW4kdspb8O0b3zmgDZWElF9wHPwwo1L2JMtGSCrjKhNUIiLkUNVTgSdBssygt83aiDo+mBJIvn1s9S9ih6enpFLWJHrTp5VqQf8nogiJp0cxYGsuMKIKF/lT9u+5KRRBaLRgsf2Ejo3D5L0x3DMGh+yFll3DD1ji52tYRV18avFa/mCOVqq7FhkCd9pgIjQ+CH2/WcQAsW585WLWidRlrF27K+wuhCdeLA/Zx9ZewFiaN9WJNMANO8dMsIX0gf7qF4aTZYmSiyCJqOYyVz7VTtez6vlknSKS3SKjJ083weIudWKHqUQ3
`,
pass: '',
type: 'Public Key',
title: 'ssh-rsa Public Key',
},
{
input: openSSHPrivateKey,
pass: '',
type: 'Private Key',
title: 'Unencrypted Private Key',
},
{
input: `
SHA256:qflg623OemnYEHDwUafq+XuMoB0UdJ+Ks44kHcWxDyM
`,
pass: '',
type: 'Fingerprint',
},
{
input: `
-----BEGIN CERTIFICATE-----
MIIDQDCCAiigAwIBAgIJK59gK0GUbZO3MA0GCSqGSIb3DQEBBQUAMFAxETAPBgNV
BAMTCHRlc3QuY29tMQ8wDQYDVQQGEwZGcmFuY2UxCzAJBgNVBAgTAkZSMQ4wDAYD
VQQHEwVQYXJpczENMAsGA1UEChMEVGVzdDAeFw0yNDA1MTMwOTQ4MTVaFw0yNTA1
MTMwOTQ4MTVaMFAxETAPBgNVBAMTCHRlc3QuY29tMQ8wDQYDVQQGEwZGcmFuY2Ux
CzAJBgNVBAgTAkZSMQ4wDAYDVQQHEwVQYXJpczENMAsGA1UEChMEVGVzdDCCASIw
DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMKrE3C9tUtmvHZkIwEBf1h5N7KC
FoXowfNZxKK7SHWcnQjBdv0ziqsU+GmcUqUD1GymMcBweqw4TQVg0a/UwdYIUTuQ
GXGx4ULCXKHv/NfmVSWcMsOZHAR4m/yEzTB/ZjKMSrqnIWyOdusDMRn4VRoAtrKO
/FM+SDJ6wvnJ/jNoZJXktq9avYduEi+heNekIF6NYM9clzm9Ff3Evf89KuigBcsu
rgL+S8PjotCwxMgzOWV4/paeeQluqYeU94prWIASS/D3elH7qFTAUnafBICFN2zs
XWY6ZFCR8QrDI5F/8KELq/3BaLQBxpIi9SmADLWqnPOu+6H5rzr2YV8LaxMCAwEA
AaMdMBswDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAvQwDQYJKoZIhvcNAQEFBQAD
ggEBAKhrUNnWe0VmgefvfwsAqrbk0Z6PwaibIl/l5I9oh1qM01J9BFHpvomhcLxu
cmIpD6nAqtkNyvsXtFAnZG3WNaf45yyd153wSa0QnrNo2GRH9quktm4DaRIIP7qq
EdtApYCeT16LvAGYUH3ubCdom8w6DkukLg8qMrXMywSZlx85jlJfifPvMKsJmm/a
QAq1H3cYaaj0DocF1rCP+hLzvsuM7UwS2JOK8Mw49kYPBTbCVmRDOE1rhlDIO8Kw
V7CCFr4NXsyRlM0TpKdspOmxJiyxmk6DoVgp9PeqfoyDAC9TJU0VJ6A2x+AfjK4O
Wg6xDXMx6dk6Rhh8yqGrmx05QM8=
-----END CERTIFICATE-----
`,
pass: '',
type: 'Certificate',
},
{
input: `
c6:b9:73:b8:68:49:33:ad:27:51:bb:6c:16:e7:9c:da:dd:e3:92:15
`,
pass: '',
type: 'Fingerprint',
title: 'HEX Fingerprint',
},
{
input: encryptedPrivateKey,
pass: 'test',
type: 'Private Key',
title: 'Encrypted Private Key',
},
];
describe('certificate-key-parser', () => {
for (const format of formatsData) {
const { input, pass, type, title } = format;
it(`Parse '${title ?? type}' format with right type (${type})`, async () => {
const { values } = await getKeyOrCertificateInfosAsync(input, pass);
const result_type = values.find(v => v.label === 'Type:')?.value;
expect(result_type).toBe(type);
});
}
});

View File

@ -0,0 +1,129 @@
import type { Buffer } from 'node:buffer';
import {
parseCertificate, parseFingerprint,
parseKey,
parsePrivateKey,
parseSignature,
} from 'sshpk';
import type {
AlgorithmType,
Certificate,
CertificateFormat,
Fingerprint,
Key,
PrivateKey, Signature, SignatureFormatType,
} from 'sshpk';
import { Base64 } from 'js-base64';
import * as openpgp from 'openpgp';
import * as forge from 'node-forge';
import { type LabelValue, getCSRLabelValues, getCertificateLabelValues, getFingerprintLabelValues, getPGPPrivateKeyLabelValuesAsync, getPGPPublicKeyLabelValuesAsync, getPrivateKeyLabelValues, getPublicKeyLabelValues, getSignatureLabelValues } from './certificate-key-parser.infos';
export async function getKeyOrCertificateInfosAsync(keyOrCertificateValue: string | Buffer, passphrase: string) {
try {
const canParse = (value: string | Buffer, parseFunction: (value: string | Buffer) => any) => {
try {
return parseFunction(value);
}
catch {
return null;
}
};
const canParseAsync = async (value: string | Buffer, parseFunction: (value: string | Buffer) => Promise<any>) => {
try {
return await parseFunction(value);
}
catch {
return null;
}
};
const inputKeyOrCertificateValue = (typeof keyOrCertificateValue === 'string' ? keyOrCertificateValue?.trim() : keyOrCertificateValue);
const privateKey = canParse(inputKeyOrCertificateValue,
value => parsePrivateKey(value, 'auto', { passphrase })) as PrivateKey;
if (privateKey) {
return {
values: getPrivateKeyLabelValues(privateKey),
};
}
const publicKey = canParse(inputKeyOrCertificateValue, parseKey) as Key;
if (publicKey) {
return { values: getPublicKeyLabelValues(publicKey) };
}
const pgpPrivateKey = await canParseAsync(inputKeyOrCertificateValue, value => openpgp.readPrivateKey({ armoredKey: value.toString() })) as openpgp.Key;
if (pgpPrivateKey) {
return { values: await getPGPPrivateKeyLabelValuesAsync(pgpPrivateKey) };
}
const pgpPublicKey = await canParseAsync(inputKeyOrCertificateValue, value => openpgp.readKey({ armoredKey: value.toString() })) as openpgp.Key;
if (pgpPublicKey) {
return { values: await getPGPPublicKeyLabelValuesAsync(pgpPublicKey) };
}
const cert = canParse(inputKeyOrCertificateValue, (value) => {
for (const format of ['openssh', 'pem', 'x509']) {
try {
return parseCertificate(value, format as CertificateFormat);
}
catch {}
}
return null;
}) as Certificate;
if (cert) {
let certificateX509DER = '';
try {
certificateX509DER = Base64.fromUint8Array(cert.toBuffer('x509'));
}
catch {}
return { values: getCertificateLabelValues(cert), certificateX509DER };
}
const csr = canParse(inputKeyOrCertificateValue, (value) => {
return forge.pki.certificationRequestFromPem(value.toString(), false, false);
}) as forge.pki.Certificate;
if (csr) {
return { values: getCSRLabelValues(csr) };
}
const fingerprint = canParse(inputKeyOrCertificateValue, value => parseFingerprint(value.toString())) as Fingerprint;
if (fingerprint) {
return { values: getFingerprintLabelValues(fingerprint) };
}
const signature = canParse(inputKeyOrCertificateValue, (value) => {
//
for (const algo of ['dsa', 'rsa', 'ecdsa', 'ed25519']) {
for (const format of ['asn1', 'ssh', 'raw']) {
try {
return parseSignature(value, algo as AlgorithmType, format as SignatureFormatType);
}
catch {}
}
}
return null;
}) as Signature;
if (signature) {
return { values: getSignatureLabelValues(signature) };
}
return {
values: [
{
label: 'Type:',
value: 'Unknown format or invalid passphrase',
}],
};
}
catch (e: any) {
return {
values: [
{
label: 'Error:',
value: e.toString(),
}] as LabelValue[],
};
}
}

View File

@ -0,0 +1,120 @@
<script setup lang="ts">
import { Buffer } from 'node:buffer';
import { getKeyOrCertificateInfosAsync } from './certificate-key-parser.service';
import { type LabelValue } from './certificate-key-parser.infos';
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
const inputKeyOrCertificate = ref('');
const passphrase = ref('');
const fileInput = ref() as Ref<Buffer>;
const inputType = ref<'file' | 'content'>('file');
async function onUpload(file: File) {
if (file) {
fileInput.value = Buffer.from(await file.arrayBuffer());
inputKeyOrCertificate.value = '';
}
}
const certificateX509DER = ref('');
const { download: downloadX509DER } = useDownloadFileFromBase64(
{
source: certificateX509DER,
extension: 'der',
});
function downloadX509DERFile() {
if (certificateX509DER.value === '') {
return;
}
try {
downloadX509DER();
}
catch (_) {
//
}
}
const parsedSections = computedAsync<LabelValue[]>(async () => {
const inputContent = inputKeyOrCertificate.value;
const file = fileInput.value;
let inputKeyOrCertificateValue: string | Buffer = '';
if (inputType.value === 'file' && file) {
inputKeyOrCertificateValue = file;
}
else if (inputType.value === 'content' && inputContent) {
inputKeyOrCertificateValue = inputContent;
}
const { values, certificateX509DER: certPEM } = await getKeyOrCertificateInfosAsync(inputKeyOrCertificateValue, passphrase.value);
certificateX509DER.value = certPEM || '';
return values;
});
</script>
<template>
<div>
<c-card>
<n-radio-group v-model:value="inputType" name="radiogroup" mb-2 flex justify-center>
<n-space>
<n-radio
value="file"
label="File"
/>
<n-radio
value="content"
label="Content"
/>
</n-space>
</n-radio-group>
<c-file-upload
v-if="inputType === 'file'"
title="Drag and drop a Certificate file here, or click to select a Certificate file"
@file-upload="onUpload"
/>
<c-input-text
v-if="inputType === 'content'"
v-model:value="inputKeyOrCertificate"
label="Paste your Public Key / Private Key / Signature / Fingerprint / Certificate:"
placeholder="Your Public Key / Private Key / Signature / Fingerprint / Certificate..."
multiline
rows="8"
data-test-id="input"
/>
</c-card>
<c-input-text
v-model:value="passphrase"
label="Passphrase (for encrypted keys):"
placeholder="Passphrase (for encrypted keys)..."
type="password"
data-test-id="pass"
/>
<n-divider />
<input-copyable
v-for="{ label, value, multiline } of parsedSections"
:key="label"
:label="label"
:data-test-id="label"
label-position="left"
label-width="100px"
label-align="right"
autosize mb-2
:multiline="multiline"
:value="value"
placeholder="Not Set"
/>
<div v-if="certificateX509DER !== ''" flex justify-center>
<c-button @click="downloadX509DERFile()">
Download X509 DER certificate
</c-button>
</div>
</div>
</template>

View File

@ -0,0 +1,12 @@
import { FileCertificate } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Certificate/Key parser',
path: '/certificate-key-parser',
description: 'Parse Key and Certificate',
keywords: ['certificate', 'key', 'parser'],
component: () => import('./certificate-key-parser.vue'),
icon: FileCertificate,
createdAt: new Date('2024-02-22'),
});

View File

@ -6,6 +6,7 @@ import { tool as emailNormalizer } from './email-normalizer';
import { tool as asciiTextDrawer } from './ascii-text-drawer';
import { tool as textToUnicode } from './text-to-unicode';
import { tool as certificateKeyParser } from './certificate-key-parser';
import { tool as safelinkDecoder } from './safelink-decoder';
import { tool as xmlToJson } from './xml-to-json';
import { tool as jsonToXml } from './json-to-xml';
@ -91,7 +92,20 @@ import { tool as yamlViewer } from './yaml-viewer';
export const toolsByCategory: ToolCategory[] = [
{
name: 'Crypto',
components: [tokenGenerator, hashText, bcrypt, uuidGenerator, ulidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser, pdfSignatureChecker],
components: [
tokenGenerator,
hashText,
bcrypt,
uuidGenerator,
ulidGenerator,
cypher,
bip39,
hmacGenerator,
rsaKeyPairGenerator,
certificateKeyParser,
passwordStrengthAnalyser,
pdfSignatureChecker,
],
},
{
name: 'Converter',

View File

@ -1,5 +1,6 @@
import { resolve } from 'node:path';
import { URL, fileURLToPath } from 'node:url';
import { nodePolyfills } from 'vite-plugin-node-polyfills';
import VueI18n from '@intlify/unplugin-vue-i18n/vite';
import vue from '@vitejs/plugin-vue';
@ -97,6 +98,7 @@ export default defineConfig({
resolvers: [NaiveUiResolver(), IconsResolver({ prefix: 'icon' })],
}),
Unocss(),
nodePolyfills(),
],
base: baseUrl,
resolve: {