/** * Encryption Dialog * * Dialog for creating new SealedSecrets by encrypting secret values * client-side using the controller's public certificate */ import { Icon } from '@iconify/react'; import { K8s } from '@kinvolk/headlamp-plugin/lib'; import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, IconButton, InputLabel, MenuItem, Select, TextField, Typography, } from '@mui/material'; import { useSnackbar } from 'notistack'; import React from 'react'; import { useSealedSecretEncryption } from '../hooks/useSealedSecretEncryption'; import { SealedSecret } from '../lib/SealedSecretCRD'; import { SealedSecretScope, SecretKeyValue } from '../types'; interface EncryptDialogProps { open: boolean; onClose: () => void; } /** * Encrypt dialog component */ export function EncryptDialog({ open, onClose }: EncryptDialogProps) { const [name, setName] = React.useState(''); const [namespace, setNamespace] = React.useState('default'); const [scope, setScope] = React.useState('strict'); const [keyValues, setKeyValues] = React.useState<(SecretKeyValue & { showValue: boolean })[]>([ { key: '', value: '', showValue: false }, ]); const { enqueueSnackbar } = useSnackbar(); const { encrypt, encrypting } = useSealedSecretEncryption(); const [namespaces] = K8s.ResourceClasses.Namespace.useList(); // Memoize callbacks to prevent re-renders const handleAddKeyValue = React.useCallback(() => { setKeyValues(prev => [...prev, { key: '', value: '', showValue: false }]); }, []); const handleRemoveKeyValue = React.useCallback((index: number) => { setKeyValues(prev => prev.filter((_, i) => i !== index)); }, []); const handleKeyChange = React.useCallback((index: number, key: string) => { setKeyValues(prev => { const updated = [...prev]; updated[index] = { ...updated[index], key }; return updated; }); }, []); const handleValueChange = React.useCallback((index: number, value: string) => { setKeyValues(prev => { const updated = [...prev]; updated[index] = { ...updated[index], value }; return updated; }); }, []); const toggleShowValue = React.useCallback((index: number) => { setKeyValues(prev => { const updated = [...prev]; updated[index] = { ...updated[index], showValue: !updated[index].showValue }; return updated; }); }, []); const handleCreate = async () => { // Filter out empty rows const validKeyValues = keyValues .filter(kv => kv.key || kv.value) .map(kv => ({ key: kv.key, value: kv.value, })); // Use the encryption hook const result = await encrypt({ name, namespace, scope, keyValues: validKeyValues, }); // If encryption failed, the hook already showed the error if (result.ok === false) { return; } try { // Apply the SealedSecret to the cluster await SealedSecret.apiEndpoint.post(result.value.sealedSecretData); enqueueSnackbar('SealedSecret created successfully', { variant: 'success' }); // Clear form and close setName(''); setNamespace('default'); setScope('strict'); setKeyValues([{ key: '', value: '', showValue: false }]); onClose(); } catch (error: unknown) { enqueueSnackbar( `Failed to create SealedSecret: ${error instanceof Error ? error.message : String(error)}`, { variant: 'error' } ); } }; return ( Create Sealed Secret setName(e.target.value)} margin="normal" required inputProps={{ 'aria-label': 'Secret name', 'aria-required': true, }} helperText="Must be a valid Kubernetes resource name (lowercase alphanumeric, hyphens)" /> Namespace Scope Secret Data {keyValues.map((kv, index) => ( handleKeyChange(index, e.target.value)} sx={{ flex: 1 }} inputProps={{ 'aria-label': `Key name ${index + 1}`, }} helperText={index === 0 ? 'Alphanumeric, hyphens, underscores, or dots' : undefined} /> handleValueChange(index, e.target.value)} sx={{ flex: 2 }} autoComplete="off" spellCheck={false} inputProps={{ 'aria-label': `Secret value for ${kv.key || `key ${index + 1}`}`, }} InputProps={{ endAdornment: ( toggleShowValue(index)} edge="end" aria-label={kv.showValue ? 'Hide password' : 'Show password'} tabIndex={0} > {kv.showValue ? : } ), }} /> handleRemoveKeyValue(index)} disabled={keyValues.length === 1} color="error" aria-label={`Remove key-value pair ${index + 1}`} title={ keyValues.length === 1 ? 'At least one key-value pair is required' : `Remove key-value pair ${index + 1}` } > ))} Security Note: Secret values are encrypted entirely in your browser using the controller's public key. The plaintext values never leave your machine. ); }