/** * Custom Hook for SealedSecret Encryption * * Encapsulates the business logic for encrypting secrets and creating SealedSecrets. * Handles certificate fetching, validation, expiry warnings, encryption, and object creation. */ import { useSnackbar } from 'notistack'; import React from 'react'; import { fetchPublicCertificate, getPluginConfig } from '../lib/controller'; import { encryptKeyValues, isCertificateExpiringSoon, parseCertificateInfo, parsePublicKeyFromCert, } from '../lib/crypto'; import { validateSecretKey, validateSecretName, validateSecretValue } from '../lib/validators'; import { AsyncResult, CertificateInfo, Err, Ok, PlaintextValue, SealedSecretScope } from '../types'; /** * Request parameters for encryption */ export interface EncryptionRequest { /** Name of the SealedSecret to create */ name: string; /** Namespace to create the SealedSecret in */ namespace: string; /** Encryption scope (strict, namespace-wide, cluster-wide) */ scope: SealedSecretScope; /** Key-value pairs to encrypt */ keyValues: Array<{ key: string; value: string }>; } /** * Shape of the SealedSecret manifest constructed for API submission */ interface SealedSecretManifest { apiVersion: string; kind: string; metadata: { name: string; namespace: string; annotations: Record; }; spec: { encryptedData: Record; template: { metadata: Record; }; }; } /** * Result of successful encryption */ export interface EncryptionResult { /** The complete SealedSecret object ready to apply */ sealedSecretData: SealedSecretManifest; /** Information about the certificate used */ certificateInfo?: CertificateInfo; } /** * Custom hook for SealedSecret encryption * * Provides encryption functionality with built-in validation, error handling, * and user notifications. * * @returns Object with encrypt function and encrypting state * * @example * const { encrypt, encrypting } = useSealedSecretEncryption(); * * const result = await encrypt({ * name: 'my-secret', * namespace: 'default', * scope: 'strict', * keyValues: [{ key: 'password', value: 'secret123' }] * }); * * if (result.ok) { * // Use result.value.sealedSecretData * } */ export function useSealedSecretEncryption() { const [encrypting, setEncrypting] = React.useState(false); const { enqueueSnackbar } = useSnackbar(); const encrypt = React.useCallback( async (request: EncryptionRequest): AsyncResult => { setEncrypting(true); try { // Step 1: Validate inputs const nameValidation = validateSecretName(request.name); if (!nameValidation.valid) { enqueueSnackbar(nameValidation.error, { variant: 'error' }); return Err(nameValidation.error || 'Invalid secret name'); } // Validate all key-value pairs for (const kv of request.keyValues) { const keyValidation = validateSecretKey(kv.key); if (!keyValidation.valid) { const error = `Invalid key "${kv.key}": ${keyValidation.error}`; enqueueSnackbar(error, { variant: 'error' }); return Err(error); } const valueValidation = validateSecretValue(kv.value); if (!valueValidation.valid) { const error = `Invalid value for key "${kv.key}": ${valueValidation.error}`; enqueueSnackbar(error, { variant: 'error' }); return Err(error); } } if (request.keyValues.length === 0) { const error = 'At least one key-value pair is required'; enqueueSnackbar(error, { variant: 'error' }); return Err(error); } // Step 2: Fetch the controller's public certificate const config = getPluginConfig(); const certResult = await fetchPublicCertificate(config); if (certResult.ok === false) { const error = `Failed to fetch certificate: ${certResult.error}`; enqueueSnackbar(error, { variant: 'error' }); return Err(error); } // Step 3: Check certificate expiry and warn user let certInfo: CertificateInfo | undefined; const certInfoResult = parseCertificateInfo(certResult.value); if (certInfoResult.ok) { certInfo = certInfoResult.value; if (certInfo.isExpired) { enqueueSnackbar( `Warning: Controller certificate expired on ${certInfo.validTo.toLocaleDateString()}. ` + 'Secrets may not be decryptable.', { variant: 'warning' } ); } else if (isCertificateExpiringSoon(certInfo, 30)) { enqueueSnackbar( `Warning: Controller certificate expires in ${certInfo.daysUntilExpiry} days ` + `(${certInfo.validTo.toLocaleDateString()}).`, { variant: 'warning' } ); } } // Step 4: Parse the public key from certificate const keyResult = parsePublicKeyFromCert(certResult.value); if (keyResult.ok === false) { const error = `Invalid certificate: ${keyResult.error}`; enqueueSnackbar(error, { variant: 'error' }); return Err(error); } // Step 5: Encrypt all values client-side const encryptResult = encryptKeyValues( keyResult.value, request.keyValues.map(kv => ({ key: kv.key, value: PlaintextValue(kv.value) })), request.namespace, request.name, request.scope ); if (encryptResult.ok === false) { const error = `Encryption failed: ${encryptResult.error}`; enqueueSnackbar(error, { variant: 'error' }); return Err(error); } // Step 6: Construct the SealedSecret object const sealedSecretData: SealedSecretManifest = { apiVersion: 'bitnami.com/v1alpha1', kind: 'SealedSecret', metadata: { name: request.name, namespace: request.namespace, annotations: {}, }, spec: { encryptedData: encryptResult.value, template: { metadata: {}, }, }, }; // Add scope annotations if (request.scope === 'namespace-wide') { sealedSecretData.metadata.annotations['sealedsecrets.bitnami.com/namespace-wide'] = 'true'; } else if (request.scope === 'cluster-wide') { sealedSecretData.metadata.annotations['sealedsecrets.bitnami.com/cluster-wide'] = 'true'; } return Ok({ sealedSecretData, certificateInfo: certInfo, }); } catch (error: unknown) { const errorMsg = error instanceof Error ? error.message : String(error); enqueueSnackbar(errorMsg, { variant: 'error' }); return Err(errorMsg); } finally { setEncrypting(false); } }, [enqueueSnackbar] ); return { encrypt, encrypting }; }