Skip to content

Instantly share code, notes, and snippets.

@sscarduzio
Last active November 25, 2024 22:25
Show Gist options
  • Save sscarduzio/df955bde29364a6670c926b293437e58 to your computer and use it in GitHub Desktop.
Save sscarduzio/df955bde29364a6670c926b293437e58 to your computer and use it in GitHub Desktop.
import { useState, useEffect } from 'react';
import { Card, CardContent } from '../ui/card';
import { Textarea } from '../ui/textarea';
import Editor from "@monaco-editor/react";
import { Button } from '../ui/button';
import { X, Check, Copy } from 'lucide-react';
import { Algorithm, SUPPORTED_ALGORITHMS, KeyPair } from '../../types/activationKey';
import { decodeJWT, getJwtMetadata, signJWT, validateJWTSignature, ValidationResult } from '../../utils/activationKey';
import { ActivationKeyMetadataDisplay } from '../activationkey/ActivationKeyMetadata';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
import { PageHeader } from '../ui/page-header';
import { ValidationStatus } from '../activationkey/validation-status';
import './ActivationkeyEditor.css';
import { useTheme } from '../../hooks/use-theme';
const ActivationKeyEditor = () => {
const { theme } = useTheme();
const [inputValue, setInputValue] = useState('');
const [jwt, setJwt] = useState('');
const [keyPairs, setKeyPairs] = useState<KeyPair[]>([]);
const [selectedKeyId, setSelectedKeyId] = useState<string>('');
const [expiryDate, setExpiryDate] = useState<Date | undefined>(undefined);
const [showCopied, setShowCopied] = useState(false);
const [algorithm, setAlgorithm] = useState<Algorithm>('ES512');
const [editorValue, setEditorValue] = useState('{}');
const [signatureValidation, setSignatureValidation] = useState<ValidationResult | null>(null);
const exampleJwt = "eyJhbGciOiJFUzUxMiJ9.eyJleHAiOjE3NDMyMDI4MDAsImlzcyI6Imh0dHBzOi8vYW5hcGhvcmEtd2Vic2l0ZS5wYWdlcy5kZXYvIiwiaWF0IjoxNzMyNTQyNzU4LCJqdGkiOiJhbmFwaG9yYV9lbnRlcnByaXNlXzE3MjY5OTM1NDcuODQwMzUzX2pvaG4uZG9lQGFjbWUuY29tIiwiYXVkIjoiYW5hcGhvcmEuZW50ZXJwcmlzZV9saWNlbnNlIiwic3ViIjozMCwibGljZW5zb3IiOnsibmFtZSI6IkJlc2h1IExpbWl0ZWQgdC9hIGFzZCBTZWN1cml0eSIsImNvbnRhY3QiOlsic3VwcG9ydEBhY21lLmNvbSIsImZpbmFuY2VAYWNtZS5jb20iXSwiaXNzdWVyIjoic3VwcG9ydEByZWFkb25seXJlc3QuY29tIn0sImxpY2Vuc2VlIjp7Im5hbWUiOiJKb2huIERvZSIsImJ1eWluZ19mb3IiOm51bGwsImJpbGxpbmdfZW1haWwiOiJqb2huLmRvZUBhY21lLmNvbSIsImFsdF9lbWFpbHMiOlsiamFuZS5kb2VAYWNtZS5jb20iXSwiYWRkcmVzcyI6WyJSdWUgNTYsIFBhcmlzIiwiRnJhbmNlIl19LCJsaWNlbnNlIjp7ImVkaXRpb24iOiJFTlRFUlBSSVNFIiwiZWRpdGlvbl9uYW1lIjoiRU5URVJQUklTRSBFZGl0aW9uIiwiaXNUcmlhbCI6dHJ1ZX19.AUjAqQnxs9tBEgupxO2fYIxLfZthD00cGYOIzsJ7ZgbnDku0sNU_BR5P9u64s-lSv9cvM1pHKVmIXmCgsCbIjMQzACyveVTP4iJXKBM7FSf1nC1TKPIrm3Oq6uuQa1qrcWcR4tfMp4QXUGn396B2hxuMKtS9Q_Tj-cQ-LkL9kk6q4oMw";
useEffect(() => {
const savedKeys = localStorage.getItem('keyPairs');
if (savedKeys) {
const keys = JSON.parse(savedKeys);
setKeyPairs(keys);
if (keys.length > 0 && !selectedKeyId) {
setSelectedKeyId(keys[0].id);
}
}
}, [selectedKeyId]);
const handleInputChange = async (value: string) => {
setInputValue(value);
if (!value) {
clearJwt();
return;
}
if (!selectedKeyId && keyPairs.length > 0) {
setSelectedKeyId(keyPairs[0].id);
}
// Try to parse as JWT
const metadata = getJwtMetadata(value);
if (metadata) {
if (metadata.algorithm && SUPPORTED_ALGORITHMS.includes(metadata.algorithm as Algorithm)) {
setAlgorithm(metadata.algorithm as Algorithm);
}
if (metadata.expiresAt) {
setExpiryDate(new Date(metadata.expiresAt));
}
}
const decoded = await decodeJWT(value);
try {
const parsedDecoded = JSON.parse(decoded);
const payloadOnly = JSON.stringify(parsedDecoded.payload || {}, null, 2);
setEditorValue(payloadOnly);
} catch (e) {
setEditorValue('{}');
}
setJwt(value);
try {
// Only validate against the selected key pair
const selectedKey = keyPairs.find(k => k.id === selectedKeyId);
const validation = await validateJWTSignature(value, selectedKey);
setSignatureValidation(validation);
} catch (error) {
setSignatureValidation({
isValid: false,
errors: {
signature: true,
message: 'Invalid JWT format'
}
});
}
};
// Also update validation when the selected key changes
useEffect(() => {
if (jwt && selectedKeyId) {
const selectedKey = keyPairs.find(k => k.id === selectedKeyId);
validateJWTSignature(jwt, selectedKey).then(setSignatureValidation);
}
}, [selectedKeyId, jwt]);
const clearJwt = () => {
setInputValue('');
setJwt('');
setSignatureValidation(null);
if (keyPairs.length > 0) {
setSelectedKeyId(keyPairs[0].id);
} else {
setSelectedKeyId('');
}
};
const handleSign = async () => {
try {
const selectedKey = keyPairs.find(k => k.id === selectedKeyId);
if (!selectedKey) return;
let payload;
try {
// Parse the editor value directly as the payload
payload = JSON.parse(editorValue);
} catch (e) {
alert('Invalid JSON payload');
return;
}
// Only override exp if a new expiry date is selected
if (expiryDate) {
payload.exp = Math.floor(expiryDate.getTime() / 1000);
}
const newToken = await signJWT(
payload,
algorithm,
selectedKey,
// Use the expiry date from the payload if no new date is selected
expiryDate || (payload.exp ? new Date(payload.exp * 1000) : new Date())
);
setJwt(newToken);
const newDecoded = await decodeJWT(newToken);
// Extract just the payload object for the editor
try {
const parsedDecoded = JSON.parse(newDecoded);
const payloadOnly = JSON.stringify(parsedDecoded.payload || {}, null, 2);
setEditorValue(payloadOnly);
} catch (e) {
setEditorValue('{}');
}
} catch (error) {
console.error('Signing error:', error);
alert('Failed to sign JWT');
}
};
const copyToClipboard = async (text: string) => {
await navigator.clipboard.writeText(text);
setShowCopied(true);
setTimeout(() => setShowCopied(false), 2000);
};
return (
<div className="ak-editor-container">
<PageHeader
title="Activation Key Editor"
description="Create, decode, and validate Activation Keys"
/>
{!jwt ? (
<div className="space-y-2">
<Textarea
placeholder="Paste your Activation Key here"
value={inputValue}
onChange={(e) => handleInputChange(e.target.value)}
className="ak-input"
/>
<button
onClick={() => handleInputChange(exampleJwt)}
className="ak-example-link"
>
Use example
</button>
</div>
) : (
<Card className="relative">
<Button
variant="ghost"
size="icon"
className="ak-clear-button"
onClick={clearJwt}
>
<X className="h-4 w-4" />
</Button>
<CardContent className="space-y-6 pt-6">
<div className="ak-content-wrapper">
<div className="ak-editor-monaco">
<Editor
key={`monaco-${jwt}`}
defaultLanguage="json"
value={editorValue}
onChange={(value) => setEditorValue(value || '{}')}
options={{
minimap: { enabled: false },
formatOnPaste: true,
formatOnType: true,
automaticLayout: true,
scrollBeyondLastLine: false,
tabSize: 2,
fontSize: 12,
lineNumbers: 'off',
folding: false,
glyphMargin: false,
lineDecorationsWidth: 0,
lineNumbersMinChars: 0,
overviewRulerBorder: false,
overviewRulerLanes: 0,
hideCursorInOverviewRuler: true,
scrollbar: {
vertical: 'auto',
horizontal: 'auto',
verticalScrollbarSize: 10,
horizontalScrollbarSize: 10,
alwaysConsumeMouseWheel: false
}
}}
theme={theme === 'dark' ? 'vs-dark' : 'light'}
/>
</div>
<div className="ak-right-column">
<div className="ak-header-row">
<ValidationStatus validation={signatureValidation} />
<Select value={selectedKeyId} onValueChange={setSelectedKeyId}>
<SelectTrigger className="ak-key-select">
<SelectValue placeholder="Select a key for signing" />
</SelectTrigger>
<SelectContent>
{keyPairs.map((key) => (
<SelectItem key={key.id} value={key.id}>
{key.name} ({algorithm.startsWith('HS') ? 'Symmetric' : 'Asymmetric'})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{getJwtMetadata(jwt) && (
<ActivationKeyMetadataDisplay
metadata={getJwtMetadata(jwt)!}
expiryDate={expiryDate}
onExpiryChange={setExpiryDate}
/>
)}
<div className="ak-controls">
<Button
onClick={handleSign}
disabled={!selectedKeyId || !editorValue}
className="sign-button"
>
Generate Activation Key
</Button>
</div>
{jwt && (
<div className="ak-output">
<div className="ak-output-text">
{jwt}
</div>
<Button
variant="ghost"
size="icon"
className="ak-copy-button"
onClick={() => copyToClipboard(jwt)}
>
{showCopied ? (
<Check className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
)}
</div>
</div>
</CardContent>
</Card>
)}
</div>
);
};
export default ActivationKeyEditor;
import * as jose from 'jose';
import { formatDistanceToNow, formatDistanceToNowStrict } from 'date-fns';
import { ActivationKeyMetadata, KeyPair, Algorithm } from '../types/activationKey';
import { SignJWT } from 'jose';
import { generateKeyPair } from 'jose';
// Polyfill Buffer for browser environments
import { Buffer } from 'buffer';
const globalAny = typeof window !== 'undefined' ? window : global;
if (!globalAny.Buffer) {
globalAny.Buffer = Buffer;
}
export const getJwtMetadata = (token: string): ActivationKeyMetadata | null => {
try {
const decoded = jose.decodeJwt(token);
return {
algorithm: jose.decodeProtectedHeader(token).alg ?? null,
issuedAt: decoded.iat ? new Date(decoded.iat * 1000) : null,
expiresAt: decoded.exp ? new Date(decoded.exp * 1000) : null,
};
} catch (e) {
return null;
}
};
export const formatRelativeTime = (date: Date | null, isExpiry = false): string => {
if (!date) return 'Not specified';
const now = new Date();
const isPast = date < now;
if (isExpiry) {
return isPast
? `Expired ${formatDistanceToNow(date, { addSuffix: true })}`
: `${formatDistanceToNowStrict(date, { addSuffix: true })}`;
}
return `${formatDistanceToNow(date, { addSuffix: true })}`;
};
export const formatJwtDisplay = (token: string): string => {
if (token.length <= 20) return token;
return `${token.slice(0, 10)}...${token.slice(-10)}`;
};
export const decodeJWT = async (token: string ): Promise<string> => {
try {
const decoded = jose.decodeJwt(token);
const header = jose.decodeProtectedHeader(token);
return JSON.stringify(
{
header,
payload: decoded,
},
null,
2
);
} catch (e) {
return '{}';
}
};
const checkPrivateKey = async (key: string): Promise<string> => {
// Check if key is in SEC1 format
if (key.includes('-----BEGIN EC PRIVATE KEY-----')) {
throw new Error('Only PKCS8 format private keys are supported. Please convert your SEC1 key to PKCS8 format.');
}
return key;
};
export const signJWT = async (
payload: any,
algorithm: Algorithm,
keyPair: KeyPair,
expiryDate: Date
): Promise<string> => {
try {
if (!keyPair?.privateKey) {
throw new Error('Private key is required');
}
// Normalize the private key format if needed
const normalizedKey = await checkPrivateKey(keyPair.privateKey);
// Get the correct curve for the algorithm
const privateKey = await jose.importPKCS8(normalizedKey, 'ES512');
const jwt = await new SignJWT(payload)
.setProtectedHeader({ alg: algorithm })
.setIssuedAt()
.setExpirationTime(expiryDate)
.sign(privateKey);
return jwt;
} catch (error) {
console.error('Signing error:', error);
throw error;
}
};
export interface ValidationResult {
isValid: boolean;
keyName?: string;
errors?: {
expired?: boolean;
signature?: boolean;
message?: string;
};
}
export const validateJWTSignature = async (
jwt: string,
keyPair: KeyPair | undefined
): Promise<ValidationResult> => {
if (!keyPair) {
return {
isValid: false,
errors: {
signature: true,
message: 'No matching key found in your list of keys'
}
};
}
try {
const [header] = jwt.split('.');
const decodedHeader = JSON.parse(atob(header));
const algorithm = decodedHeader.alg;
const normalizedKey = await checkPrivateKey(keyPair.publicKey);
const publicKey = await jose.importSPKI(normalizedKey, algorithm);
await jose.jwtVerify(jwt, publicKey);
return {
isValid: true,
errors: undefined
};
} catch (error) {
return {
isValid: false,
errors: {
signature: true,
message: error instanceof Error ? error.message : 'Unknown error'
}
};
}
};
export const generateEC512KeyPair = async (): Promise<KeyPair> => {
try {
const { privateKey, publicKey } = await generateKeyPair('ES512', {
extractable: true // Make sure keys are extractable
});
// Export keys in PKCS8 (private) and SPKI (public) formats
const exportedPrivateKey = await jose.exportPKCS8(privateKey);
const exportedPublicKey = await jose.exportSPKI(publicKey);
return {
id: crypto.randomUUID(),
name: 'Generated ES512 Key Pair',
privateKey: exportedPrivateKey,
publicKey: exportedPublicKey,
createdAt: new Date().toISOString()
};
} catch (error) {
console.error('Key pair generation error:', error);
throw error;
}
};
// Optional helper to extract public key from private key
export const extractPublicKeyFromPrivate = async (privateKeyString: string): Promise<string> => {
try {
const privateKey = await jose.importPKCS8(privateKeyString, 'ES512');
const publicKey = await jose.exportSPKI(privateKey);
return publicKey;
} catch (error) {
console.error('Public key extraction error:', error);
throw error;
}
};
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Button } from '../ui/button';
import { Plus, ChevronDown, ChevronUp, Pencil, Trash2, Download, Upload } from 'lucide-react';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
import { Label } from '../ui/label';
import { PageHeader } from '../ui/page-header';
import { KeyPair } from '../../types/activationKey';
import { KeyPairForm } from '../keys/key-pair-form';
import { validatePublicKey } from '../../utils/key-validation';
import { validatePrivateKey } from '../../utils/key-validation';
import { generateEC512KeyPair } from '../../utils/activationKey';
import { exportKeyPairToZip, importKeyPairFromZip } from '../../utils/file-utils';
import './Keys.css';
const Keys = () => {
const [keyPairs, setKeyPairs] = useState<KeyPair[]>([]);
const [isAddingNew, setIsAddingNew] = useState(false);
const [newKeyPair, setNewKeyPair] = useState<Omit<KeyPair, 'id'>>({
name: '',
publicKey: '',
privateKey: '',
createdAt: '',
});
const [expandedId, setExpandedId] = useState<string | null>(null);
const [editingId, setEditingId] = useState<string | null>(null);
const [editingKeyPair, setEditingKeyPair] = useState<KeyPair | null>(null);
useEffect(() => {
const savedKeys = localStorage.getItem('keyPairs');
if (savedKeys) {
setKeyPairs(JSON.parse(savedKeys));
}
}, []);
const handleSave = async () => {
try {
const trimmedKeyPair = {
name: newKeyPair.name.trim(),
publicKey: newKeyPair.publicKey.trim(),
privateKey: newKeyPair.privateKey.trim(),
};
if (!trimmedKeyPair.name || !trimmedKeyPair.publicKey || !trimmedKeyPair.privateKey) {
alert('Please fill in all fields');
return;
}
const privateKeyError = validatePrivateKey(trimmedKeyPair.privateKey);
if (privateKeyError) {
alert(privateKeyError);
return;
}
const publicKeyError = validatePublicKey(trimmedKeyPair.publicKey);
if (publicKeyError) {
alert(publicKeyError);
return;
}
const newPair: KeyPair = {
...trimmedKeyPair,
id: crypto.randomUUID(),
createdAt: new Date().toISOString(),
};
const updatedPairs = [...keyPairs, newPair];
setKeyPairs(updatedPairs);
localStorage.setItem('keyPairs', JSON.stringify(updatedPairs));
setIsAddingNew(false);
setNewKeyPair({ name: '', publicKey: '', privateKey: '', createdAt: '' });
} catch (error) {
alert(error instanceof Error ? error.message : 'Error saving key pair');
}
};
const handleDelete = (id: string) => {
if (window.confirm('Are you sure you want to delete this key pair?')) {
const updatedPairs = keyPairs.filter(pair => pair.id !== id);
setKeyPairs(updatedPairs);
localStorage.setItem('keyPairs', JSON.stringify(updatedPairs));
}
};
const toggleExpand = (id: string) => {
setExpandedId(expandedId === id ? null : id);
};
const handleEdit = (pair: KeyPair) => {
if (editingId === pair.id) {
setEditingId(null);
setEditingKeyPair(null);
} else {
setEditingId(pair.id);
setEditingKeyPair(pair);
}
};
const handleUpdateKey = () => {
if (!editingKeyPair) return;
const trimmedKeyPair = {
...editingKeyPair,
name: editingKeyPair.name.trim(),
publicKey: editingKeyPair.publicKey.trim(),
privateKey: editingKeyPair.privateKey.trim(),
};
const privateKeyError = validatePrivateKey(trimmedKeyPair.privateKey);
if (privateKeyError) {
alert(privateKeyError);
return;
}
const publicKeyError = validatePublicKey(trimmedKeyPair.publicKey);
if (publicKeyError) {
alert(publicKeyError);
return;
}
const updatedPairs = keyPairs.map(pair =>
pair.id === trimmedKeyPair.id ? trimmedKeyPair : pair
);
setKeyPairs(updatedPairs);
localStorage.setItem('keyPairs', JSON.stringify(updatedPairs));
setEditingId(null);
setEditingKeyPair(null);
};
const handleGenerateKeys = async () => {
try {
const generatedPair = await generateEC512KeyPair();
setNewKeyPair({
name: generatedPair.name,
publicKey: generatedPair.publicKey,
privateKey: generatedPair.privateKey,
createdAt: generatedPair.createdAt
});
} catch (error) {
alert(error instanceof Error ? error.message : 'Error generating key pair');
}
};
const handleExport = async (pair: KeyPair) => {
try {
await exportKeyPairToZip(pair);
} catch (error) {
alert(error instanceof Error ? error.message : 'Error exporting key pair');
}
};
const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
try {
const importedPair = await importKeyPairFromZip(file);
setNewKeyPair({
...importedPair,
createdAt: new Date().toISOString(),
});
setIsAddingNew(true);
} catch (error) {
alert(error instanceof Error ? error.message : 'Error importing key pair');
}
// Reset the input
event.target.value = '';
};
return (
<div className="p-6 max-w-4xl mx-auto">
<PageHeader
title="Keys Management"
description="Manage your key pairs. All keys should be in PKCS#8 PEM format."
/>
<div className="space-y-4">
<div className="flex justify-end gap-2">
<input
type="file"
accept=".zip"
onChange={handleImport}
className="hidden"
id="import-key"
/>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<label htmlFor="import-key">
<Button variant="outline" size="icon" asChild>
<span>
<Upload className="h-4 w-4" />
</span>
</Button>
</label>
</TooltipTrigger>
<TooltipContent className="w-80">
<div className="space-y-2">
<h4 className="font-medium">Import Key Pair</h4>
<p className="text-sm">
Upload a ZIP file containing:
<ul className="list-disc list-inside mt-1">
<li>public_key.pem</li>
<li>private_key.pem</li>
</ul>
The key pair name will be taken from the ZIP filename.
</p>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Button
onClick={() => setIsAddingNew(true)}
disabled={isAddingNew}
size="icon"
>
<Plus className="h-4 w-4" />
</Button>
</div>
{isAddingNew && (
<Card className="mb-6">
<CardContent className="pt-6 space-y-6">
<KeyPairForm
mode="create"
keyPair={newKeyPair}
onChange={(e) => setNewKeyPair(e as KeyPair)}
onSave={handleSave}
onCancel={() => {
setIsAddingNew(false);
setNewKeyPair({ name: '', publicKey: '', privateKey: '', createdAt: '' });
}}
onGenerateKeys={handleGenerateKeys}
/>
</CardContent>
</Card>
)}
<div className="space-y-4">
{keyPairs.map((pair) => (
<Card key={pair.id}>
<CardHeader
className="relative cursor-pointer hover:bg-secondary/10 transition-colors px-6 py-4"
onClick={() => toggleExpand(pair.id)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Button
variant="ghost"
size="sm"
className="pointer-events-none p-0 hover:bg-transparent"
>
{expandedId === pair.id ? (
<ChevronUp className="h-5 w-5" />
) : (
<ChevronDown className="h-5 w-5" />
)}
</Button>
<CardTitle className="text-xl font-semibold">{pair.name}</CardTitle>
</div>
<div className="flex gap-2">
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
handleExport(pair);
}}
>
<Download className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent className="w-80" onClick={(e) => e.stopPropagation()}>
<div className="space-y-2">
<h4 className="font-medium">Export Key Pair</h4>
<p className="text-sm">
Downloads '{pair.name}.zip' containing:
<ul className="list-disc list-inside mt-1">
<li>public_key.pem</li>
<li>private_key.pem</li>
</ul>
</p>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{editingId === pair.id && (
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
handleDelete(pair.id);
}}
>
<Trash2 className="h-5 w-5" />
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
handleEdit(pair);
}}
>
<Pencil className={`h-5 w-5 ${editingId === pair.id ? "text-primary" : ""}`} />
</Button>
</div>
</div>
</CardHeader>
{(expandedId === pair.id || editingId === pair.id) && (
<CardContent className="space-y-4 px-6 py-4">
{editingId === pair.id ? (
<>
<KeyPairForm
mode="edit"
keyPair={editingKeyPair || {}}
onChange={(e) => setEditingKeyPair((prev) => prev ? { ...prev, ...e } : null)}
onSave={handleUpdateKey}
onCancel={() => {
setEditingId(null);
setEditingKeyPair(null);
}}
/>
</>
) : (
<>
<div className="space-y-2">
<Label className="key-label">Public Key (PEM)</Label>
<div className="key-display blurred">
{pair.publicKey}
</div>
</div>
<div className="space-y-2">
<Label className="key-label">Private Key (PEM)</Label>
<div className="key-display blurred">
{pair.privateKey}
</div>
</div>
</>
)}
</CardContent>
)}
</Card>
))}
</div>
</div>
</div>
);
};
export default Keys;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment