-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(new tool): Certificate/Key Parser and infos
Parse Certificate and Keys (Public, Private, Signature, Fingerprint...) and displays infos Fix #671
- Loading branch information
Showing
4 changed files
with
381 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
342 changes: 342 additions & 0 deletions
342
src/tools/certificate-key-parser/certificate-key-parser.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,342 @@ | ||
<script setup lang="ts"> | ||
import { | ||
parseCertificate, parseFingerprint, parseKey, parsePrivateKey, parseSignature, | ||
type CertificateFormat, | ||
type AlgorithmType, | ||
type SignatureFormatType | ||
} from 'sshpk'; | ||
import { getExtensionFromMimeType, getMimeTypeFromBase64, useDownloadFileFromBase64 } from '@/composable/downloadBase64'; | ||
import { Base64 } from 'js-base64'; | ||
function buf2Hex(buffer: ArrayBuffer) { // buffer is an ArrayBuffer | ||
return [...new Uint8Array(buffer)] | ||
.map(x => x.toString(16).padStart(2, '0')) | ||
.join(''); | ||
} | ||
const inputKeyOrCertificate = ref(''); | ||
const passphrase = ref(''); | ||
const fileInput = ref() as Ref<Buffer>; | ||
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 (_) { | ||
// | ||
} | ||
} | ||
type LabelValue = { | ||
label: string | ||
value: string | ||
multiline?: boolean | ||
}; | ||
const parsedSections = computed<LabelValue[]>(() => { | ||
try { | ||
certificateX509DER.value=''; | ||
const onErrorReturnErrorMessage = <T>(func: () => T) => { | ||
try { | ||
return func(); | ||
} | ||
catch (e:any) { | ||
return e.toString(); | ||
} | ||
}; | ||
const canParse = <T>(value: string | Buffer, parseFunction: (value: string | Buffer) => T) => { | ||
try { | ||
return parseFunction(value); | ||
} | ||
catch { | ||
return null; | ||
} | ||
}; | ||
const inputKeyOrCertificateValue = | ||
inputKeyOrCertificate.value !== '' ? | ||
inputKeyOrCertificate.value : | ||
fileInput.value; | ||
const publicKey = canParse(inputKeyOrCertificateValue, parseKey); | ||
if (publicKey) { | ||
return <LabelValue[]>[ | ||
{ | ||
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, | ||
}, | ||
]; | ||
} | ||
const privateKey = canParse(inputKeyOrCertificateValue, | ||
(value) => parsePrivateKey(value, 'auto', {passphrase: passphrase.value})); | ||
if (privateKey) { | ||
return <LabelValue[]>[ | ||
{ | ||
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, | ||
}, | ||
]; | ||
} | ||
const cert = canParse(inputKeyOrCertificateValue, (value => { | ||
for (const format of ["openssh" , "pem" , "x509"]){ | ||
try { | ||
return parseCertificate(value, format as CertificateFormat); | ||
} catch{} | ||
} | ||
return null; | ||
})); | ||
if (cert) { | ||
try { | ||
certificateX509DER.value = Base64.fromUint8Array(cert.toBuffer('x509')); | ||
} catch {} | ||
return <LabelValue[]>[ | ||
{ | ||
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, | ||
}, | ||
]; | ||
} | ||
const fingerprint = canParse(inputKeyOrCertificateValue, (value) => parseFingerprint(value.toString())); | ||
if (fingerprint) { | ||
return <LabelValue[]>[ | ||
{ | ||
label: 'Type: ', | ||
value: 'Fingerprint', | ||
}, | ||
{ | ||
label: 'Fingerprint (hex): ', | ||
value: fingerprint.toString('hex'), | ||
}, | ||
{ | ||
label: 'Fingerprint (base64): ', | ||
value: fingerprint.toString('base64'), | ||
}, | ||
]; | ||
} | ||
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; | ||
})); | ||
if (signature) { | ||
return <LabelValue[]>[ | ||
{ | ||
label: 'Type: ', | ||
value: 'Signature', | ||
}, | ||
{ | ||
label: 'Fingerprint (asn1): ', | ||
value: signature.toString('asn1'), | ||
}, | ||
{ | ||
label: 'Fingerprint (ssh): ', | ||
value: signature.toString('ssh'), | ||
}, | ||
]; | ||
} | ||
return <LabelValue[]>[ | ||
{ | ||
label: 'Type: ', | ||
value: 'Unknown format or invalid passphrase', | ||
}]; | ||
} | ||
catch (e: any) { | ||
return <LabelValue[]>[ | ||
{ | ||
label: 'Error: ', | ||
value: e.toString(), | ||
}]; | ||
} | ||
}); | ||
</script> | ||
|
||
<template> | ||
<div> | ||
<c-card> | ||
<c-file-upload title="Drag and drop a Certificate file here, or click to select a Certificate file" @file-upload="onUpload" /> | ||
<p>-OR-</p> | ||
<c-input-text | ||
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" | ||
/> | ||
</c-card> | ||
|
||
<c-input-text | ||
v-model:value="passphrase" | ||
label="Passphrase (for encrypted keys):" | ||
placeholder="Passphrase (for encrypted keys)..." | ||
type="password" | ||
/> | ||
|
||
<n-divider /> | ||
|
||
<input-copyable | ||
v-for="{ label, value, multiline } of parsedSections" | ||
:key="label" | ||
:label="label" | ||
label-position="left" | ||
label-width="100px" | ||
label-align="right" | ||
mb-2 | ||
autosize | ||
:multiline="multiline" | ||
:value="value" | ||
placeholder="Not Set" | ||
/> | ||
|
||
<div flex justify-center v-if="certificateX509DER !== ''"> | ||
<c-button @click="downloadX509DERFile()"> | ||
Download X509 DER certificate | ||
</c-button> | ||
</div> | ||
</div> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'), | ||
}); |
Oops, something went wrong.