Skip to content

Commit

Permalink
Added File Download option for AES, ECDSA, RSA and CSR Generation pages.
Browse files Browse the repository at this point in the history
  • Loading branch information
reznik99 committed Jun 15, 2024
1 parent fb9aa9c commit 7ec035d
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 25 deletions.
1 change: 1 addition & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class App extends React.Component<IProps, IState> {
<Box sx={{ flexGrow: 1, overflowY: 'scroll', py: 5 }}>

<Sidebar open={this.state.menuOpen}
setState={this.updateState}
path={this.props.location?.pathname || ""}
toggleMenu={() => this.setState({ menuOpen: !this.state.menuOpen })}
/>
Expand Down
15 changes: 10 additions & 5 deletions src/components/Crypto/AES.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
import { Button, ButtonGroup, CircularProgress, FormControl, InputLabel, MenuItem, Select, Stack, TextField, Typography } from '@mui/material';
import { CloudUpload, Key, Lock, LockOpen } from '@mui/icons-material';
import { CloudUpload, Key, Lock, LockOpen, Download } from '@mui/icons-material';
import { Buffer } from 'buffer';

import { Props, CryptoSettings } from 'types/SharedTypes';
import FileUploadBtn from 'components/FileUploadBtn';
import FileDownloadBtn from 'components/FileDownloadBtn';

const keyUsages: Array<KeyUsage> = ["encrypt", "decrypt"];

Expand Down Expand Up @@ -139,7 +140,7 @@ export default function AES(props: Props) {
alignItems="center"
sx={{ minHeight: '50vh' }}>
<Typography variant='h4'> Generate Key </Typography>
<FormControl fullWidth sx={{ my: 1 }}>
<FormControl fullWidth sx={{ my: 2 }}>
<InputLabel id='keysize-label'>Key Size</InputLabel>
<Select labelId='keysize-label'
label='Key Size'
Expand All @@ -152,10 +153,14 @@ export default function AES(props: Props) {
</FormControl>
{props.loading
? <Button variant='contained' disabled><CircularProgress size={18} sx={{ mx: 1 }} /> Generating...</Button>
: <Button variant='contained'
startIcon={<Key />}
: <Button variant='contained' startIcon={<Key />}
onClick={() => generateAES(props, keyLength)}>Generate AES Key</Button>
}

<FileDownloadBtn hide={!props.output} data={props.output || ''} fileName='AES.key'>
Download Key (Base64)
</FileDownloadBtn>

</Stack>
)
case 'Enc':
Expand Down Expand Up @@ -185,7 +190,7 @@ export default function AES(props: Props) {
value={message}
onChange={(e) => setMessage(e.target.value)}
InputProps={{
sx: {paddingRight: 0},
sx: { paddingRight: 0 },
endAdornment:
<FileUploadBtn onRead={(data) => setMessage(String(data))}
startIcon={<CloudUpload />}>
Expand Down
29 changes: 23 additions & 6 deletions src/components/Crypto/CSR.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import * as pkijs from 'pkijs';
import { MultiInput, RowContent } from 'components/MultiInput'
import { CryptoSettings, Props } from 'types/SharedTypes';
import { createC, createCN, createSANExtension, createL, createO, createOU, createSKIExtension, oidExtensionsRequest } from 'lib/PKCS10';
import { encodePEM } from 'lib/encoding';
import { encodePEM, keypairToPEM } from 'lib/encoding';
import { KeyUploadModal } from 'components/KeyUploadModal';
import { importRSAPriv, importRSAPub } from './RSA';
import { importECDSAPriv, importECDSAPub } from './ECDSA';
import FileDownloadBtn from 'components/FileDownloadBtn';

type keyDetails = {
algorithm: string;
Expand Down Expand Up @@ -89,7 +90,7 @@ const importCSRKeypair = async (keyDetails: keyDetails, privPem: string, pubPem:
return output
}

const generateCSR = async (props: Props, keyDetails: keyDetails, subject: subject, extensions: RowContent[], privateKey: string, publicKey: string, doGenerate: boolean) => {
const generateCSR = async (props: Props, keyDetails: keyDetails, subject: subject, extensions: RowContent[], privateKey: string, publicKey: string, doGenerate: boolean, setToDownload: (arg0: any) => void) => {
const { algorithm, hash, keyLength, curve } = keyDetails
const { commonName, organisation, organisationalUnit, locality, country } = subject

Expand Down Expand Up @@ -162,10 +163,13 @@ const generateCSR = async (props: Props, keyDetails: keyDetails, subject: subjec

// Export as PEM
const csrBER = csr.toSchema().toBER(false);
const csrPEM = Buffer.from(csrBER).toString('base64');
const algoString = `${algorithm} with ${hash} (${algorithm === 'ECDSA' ? curve : keyLength + '-bit'})`
const csrPEM = encodePEM(Buffer.from(csrBER).toString('base64'), 'CERTIFICATE REQUEST');

const [private_pem, public_pem] = await keypairToPEM(keypair)
setToDownload({ private_pem: private_pem, public_pem: public_pem, csr_pem: csrPEM })

props.setState({ output: encodePEM(csrPEM, 'CERTIFICATE REQUEST'), successMsg: `CSR Generated successfully: ${algoString}` });
const algoString = `${algorithm} with ${hash} (${algorithm === 'ECDSA' ? curve : keyLength + '-bit'})`
props.setState({ output: csrPEM, successMsg: `CSR Generated successfully: ${algoString}` });
} catch (err: any) {
console.error(err);
props.setState({ errorMsg: `Failed to generate CSR: ${err?.message || err}`, output: '' })
Expand Down Expand Up @@ -194,6 +198,7 @@ export default function CSR(props: Props) {
const [modalOpen, setModalOpen] = useState(false)
const [privateKey, setPrivateKey] = useState('')
const [publicKey, setPublicKey] = useState('')
const [toDownload, setToDownload] = useState({ private_pem: '', public_pem: '', csr_pem: '' })

return <Stack spacing={2}
direction="column"
Expand Down Expand Up @@ -328,9 +333,21 @@ export default function CSR(props: Props) {
</Button>
<Button hidden={props.loading} variant='contained'
startIcon={<Create />}
onClick={() => generateCSR(props, keyDetails, subject, extensions, privateKey, publicKey, doGenerateKey)}>
onClick={() => generateCSR(props, keyDetails, subject, extensions, privateKey, publicKey, doGenerateKey, setToDownload)}>
Generate CSR
</Button>

<Stack spacing={2} direction='row'>
<FileDownloadBtn hide={!toDownload.public_pem} data={toDownload.public_pem || ''} fileName={`${keyDetails.algorithm}-public.pem`}>
Public Key (SPKI)
</FileDownloadBtn>
<FileDownloadBtn hide={!toDownload.private_pem} data={toDownload.private_pem || ''} fileName={`${keyDetails.algorithm}-private.pem`}>
Private Key (PKCS8)
</FileDownloadBtn>
<FileDownloadBtn hide={!toDownload.csr_pem} data={toDownload.csr_pem || ''} fileName={`${keyDetails.algorithm}.req`}>
CSR (PKCS10)
</FileDownloadBtn>
</Stack>

</Stack>
}
21 changes: 17 additions & 4 deletions src/components/Crypto/ECDSA.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Check, Draw, Key } from '@mui/icons-material';

import { Props, CryptoSettings } from 'types/SharedTypes';
import * as encoding from 'lib/encoding';
import FileDownloadBtn from 'components/FileDownloadBtn';


const sigSettings: CryptoSettings = {
Expand Down Expand Up @@ -45,7 +46,7 @@ export const importECDSAPriv = async (pem: string, settings: CryptoSettings) =>
);
}

const generateECDSA = async (props: Props, curve: string) => {
const generateECDSA = async (props: Props, curve: string, setKeypair: (arg0: any) => void) => {
try {
props.setState({ loading: true })
const keypair = await window.crypto.subtle.generateKey(
Expand All @@ -56,8 +57,10 @@ const generateECDSA = async (props: Props, curve: string) => {
true,
["sign", "verify"]
);
const [private_pem, public_pem] = await encoding.keypairToPEM(keypair)
setKeypair({ private_pem: private_pem, public_pem: public_pem })

props.setState({ output: await encoding.keypairToPEM(keypair), successMsg: `(ECDSA-${curve}) Generated successfully` })
props.setState({ output: `${private_pem}\n${public_pem}`, successMsg: `(ECDSA-${curve}) Generated successfully` })
} catch (err) {
console.error(`Failed to generate ECDSA ${curve}: ${err}`)
props.setState({ errorMsg: `Failed to generate ECDSA ${curve}: ${err}` })
Expand Down Expand Up @@ -121,7 +124,7 @@ export default function ECDSA(props: Props) {
const [hashAlgo, setHashAlgo] = useState('SHA-256')
const [message, setMessage] = useState('')
const [signature, setSignature] = useState('')

const [keypair, setKeypair] = useState({ private_pem: '', public_pem: '' })
const { action } = useParams();

switch (action) {
Expand Down Expand Up @@ -151,10 +154,20 @@ export default function ECDSA(props: Props) {
</Button>
: <Button variant='contained'
startIcon={<Key />}
onClick={() => generateECDSA(props, curve)}>
onClick={() => generateECDSA(props, curve, setKeypair)}>
Generate ECDSA Key
</Button>
}

<Stack spacing={2} direction='row'>
<FileDownloadBtn hide={!keypair.public_pem} data={keypair.public_pem || ''} fileName='ECDSA-public.pem'>
Public Key (SPKI)
</FileDownloadBtn>
<FileDownloadBtn hide={!keypair.private_pem} data={keypair.private_pem || ''} fileName='ECDSA-private.pem'>
Private Key (PKCS8)
</FileDownloadBtn>
</Stack>

</Stack>
case 'Sig':
return <Stack spacing={2}
Expand Down
20 changes: 16 additions & 4 deletions src/components/Crypto/RSA.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Check, Draw, Key, Lock, LockOpen } from '@mui/icons-material';

import { Props, CryptoSettings } from 'types/SharedTypes';
import * as encoding from 'lib/encoding';
import FileDownloadBtn from 'components/FileDownloadBtn';


export const importRSAPub = async (pem: string, settings: CryptoSettings) => {
Expand All @@ -32,7 +33,7 @@ export const importRSAPriv = async (pem: string, settings: CryptoSettings) => {
);
}

const generateRSA = async (props: Props, keyLength: number) => {
const generateRSA = async (props: Props, keyLength: number, setKeypair: (arg0: any) => void) => {
try {
props.setState({ loading: true })
const keypair = await window.crypto.subtle.generateKey(
Expand All @@ -45,8 +46,10 @@ const generateRSA = async (props: Props, keyLength: number) => {
true,
["encrypt", "decrypt"]
);
const [private_pem, public_pem] = await encoding.keypairToPEM(keypair)
setKeypair({ private_pem: private_pem, public_pem: public_pem })

props.setState({ output: await encoding.keypairToPEM(keypair), successMsg: `(RSA-${keyLength}) Generated successfully` })
props.setState({ output: `${private_pem}\n${public_pem}`, successMsg: `(RSA-${keyLength}) Generated successfully` })
} catch (err) {
console.error(`Failed to generate RSA-${keyLength} keypair: ${err}`)
props.setState({ errorMsg: `Failed to generate RSA-${keyLength} keypair: ${err}` })
Expand Down Expand Up @@ -187,7 +190,7 @@ export default function RSA(props: Props) {
const [hashAlgo, setHashAlgo] = useState('SHA-256')
const [message, setMessage] = useState('')
const [signature, setSignature] = useState('')

const [keypair, setKeypair] = useState({ private_pem: '', public_pem: '' })
const { action } = useParams();

switch (action) {
Expand All @@ -213,10 +216,19 @@ export default function RSA(props: Props) {
</Button>
: <Button variant='contained'
startIcon={<Key />}
onClick={() => generateRSA(props, keyLength)}>
onClick={() => generateRSA(props, keyLength, setKeypair)}>
Generate RSA Key
</Button>
}

<Stack spacing={2} direction='row'>
<FileDownloadBtn hide={!keypair.public_pem} data={keypair.public_pem || ''} fileName='rsa-public.pem'>
Public Key (SPKI)
</FileDownloadBtn>
<FileDownloadBtn hide={!keypair.private_pem} data={keypair.private_pem || ''} fileName='rsa-private.pem'>
Private Key (PKCS8)
</FileDownloadBtn>
</Stack>
</Stack>
case 'Enc':
return <Stack spacing={2}
Expand Down
43 changes: 43 additions & 0 deletions src/components/FileDownloadBtn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { useCallback } from 'react';
import { Button, SxProps, Theme } from '@mui/material';
import { Download } from '@mui/icons-material';

type Props = {
children: React.ReactNode;
data: string | Buffer;
fileName: string;
hide?: boolean;
sx?: SxProps<Theme>;
color?: "primary" | "inherit" | "secondary" | "success" | "error" | "info" | "warning";
variant?: "text" | "outlined" | "contained";
startIcon?: React.ReactNode;
}

export default function FileDownloadBtn(props: Props) {

const handleDownload = useCallback(() => {
const url = window.URL.createObjectURL(new Blob([props.data]));
const link = document.createElement("a");
link.href = url;
link.download = `crypto-tools-${props.fileName}`;
document.body.appendChild(link);

link.click();

document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}, [props.data, props.fileName])

if (props.hide) return null;

return (
<Button sx={props.sx}
variant={props.variant ?? 'outlined'}
color={props.color ?? 'primary'}
component='label'
startIcon={props.startIcon ?? <Download />}
onClick={() => handleDownload()}>
{props.children}
</Button>
)
}
2 changes: 1 addition & 1 deletion src/components/KeyUploadModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function KeyUploadModal(props: Props) {
<DialogContent>

<DialogContentText marginBottom={3}>
Supply a Private and Public key in <strong>PKCS8 & PKIX</strong> formats to be used in the generation of the CSR.
Supply a Private and Public key in <strong>PKCS8 & SPKI</strong> formats to be used in the generation of the CSR.
</DialogContentText>

<Stack direction='row' spacing={2} marginY={2} width='100%'>
Expand Down
9 changes: 5 additions & 4 deletions src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,10 @@ const actions = new Map<string, menuObject>(
)

interface IProps {
toggleMenu: () => void
open: boolean,
path: string,
toggleMenu: () => void;
setState: Function;
open: boolean;
path: string;
}

const Sidebar = (props: IProps) => {
Expand Down Expand Up @@ -144,7 +145,7 @@ const Sidebar = (props: IProps) => {
const path = subvalue.link + action
const selected = props.path === path
return (
<Link to={path} key={idx}
<Link to={path} onClick={() => props.setState({ output: '' })} key={idx}
className='text-light'
style={{ textDecoration: 'none' }}>
<ListItemButton sx={{ pl: 4 }} selected={selected}>
Expand Down
3 changes: 2 additions & 1 deletion src/lib/encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ export const keypairToPEM = async (keypair: CryptoKeyPair) => {
const exportedPrivAsBase64 = Buffer.from(exportedPriv).toString('base64')
const exportedPubAsBase64 = Buffer.from(exportedPub).toString('base64')

return `${encodePEM(exportedPrivAsBase64, 'PRIVATE KEY')}\n${encodePEM(exportedPubAsBase64, 'PUBLIC KEY')}`

return [encodePEM(exportedPrivAsBase64, 'PRIVATE KEY'), encodePEM(exportedPubAsBase64, 'PUBLIC KEY')]
}

export const encodePEM = (value: string, pemHeader: string) => {
Expand Down

0 comments on commit 7ec035d

Please sign in to comment.