Spaces:
Running
Running
| <html lang="en" class="theme-dark"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>SecureCrypt | Client-Side Encryption</title> | |
| <style> | |
| /* Design System */ | |
| :root { | |
| /* Colors - Dark Mode */ | |
| --bg: #0b0f14; | |
| --bg-elev: #121720; | |
| --panel: #161c27; | |
| --muted: #8ba2b0; | |
| --text: #e2e8f0; | |
| --text-muted: #94a3b8; | |
| /* Accent - Sky */ | |
| --pri: #7dd3fc; | |
| --pri-600: #38bdf8; | |
| --pri-700: #0ea5e9; | |
| /* Semantic */ | |
| --ok: #34d399; | |
| --warn: #fbbf24; | |
| --err: #f87171; | |
| --info: #60a5fa; | |
| /* Typography */ | |
| --font-sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Inter, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, Arial, "Apple Color Emoji", "Segoe UI Emoji"; | |
| --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; | |
| --text-xs: 0.75rem; | |
| --text-sm: 0.875rem; | |
| --text-base: 1rem; | |
| --text-lg: 1.125rem; | |
| --text-xl: 1.25rem; | |
| --text-2xl: 1.5rem; | |
| /* Spacing */ | |
| --sp-1: 0.5rem; | |
| --sp-2: 0.75rem; | |
| --sp-3: 1rem; | |
| --sp-4: 1.25rem; | |
| --sp-5: 1.5rem; | |
| --sp-6: 2rem; | |
| /* Radii & Shadows */ | |
| --r: 0.875rem; | |
| --r-sm: 0.625rem; | |
| --r-pill: 9999px; | |
| --shadow-1: 0 2px 10px rgba(0,0,0,0.25); | |
| --shadow-2: 0 10px 30px rgba(0,0,0,0.35); | |
| } | |
| .theme-light { | |
| --bg: #f8fafc; | |
| --bg-elev: #ffffff; | |
| --panel: #ffffff; | |
| --muted: #64748b; | |
| --text: #1e293b; | |
| --text-muted: #475569; | |
| --shadow-1: 0 2px 10px rgba(0,0,0,0.05); | |
| --shadow-2: 0 10px 30px rgba(0,0,0,0.1); | |
| } | |
| /* Base Styles */ | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| position: relative; | |
| overflow-x: hidden; | |
| } | |
| .bg-animation { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: -1; | |
| pointer-events: none; | |
| overflow: hidden; | |
| } | |
| .bg-lock, | |
| .bg-key, | |
| .bg-shield { | |
| position: absolute; | |
| transition: transform 0.8s cubic-bezier(0.25, 0.1, 0.25, 1), opacity 0.3s ease; | |
| will-change: transform; | |
| } | |
| body { | |
| font-family: var(--font-sans); | |
| background-color: var(--bg); | |
| color: var(--text); | |
| line-height: 1.5; | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* Utility Classes */ | |
| .container { | |
| width: 100%; | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 0 var(--sp-4); | |
| } | |
| .card { | |
| background-color: var(--panel); | |
| border-radius: var(--r); | |
| box-shadow: var(--shadow-1); | |
| padding: var(--sp-5); | |
| } | |
| .btn { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: var(--sp-2) var(--sp-4); | |
| border-radius: var(--r-pill); | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.15s ease; | |
| border: none; | |
| outline: none; | |
| gap: var(--sp-2); | |
| } | |
| .btn--primary { | |
| background-color: var(--pri); | |
| color: var(--bg); | |
| } | |
| .btn--primary:hover { | |
| background-color: var(--pri-600); | |
| box-shadow: 0 4px 12px rgba(125, 211, 252, 0.25); | |
| } | |
| .btn--primary:active { | |
| transform: scale(0.98); | |
| } | |
| .btn--primary:focus-visible { | |
| box-shadow: 0 0 0 3px color-mix(in srgb, var(--pri) 35%, transparent); | |
| } | |
| .btn--ghost { | |
| background-color: transparent; | |
| color: var(--pri); | |
| border: 1px solid rgba(125, 211, 252, 0.3); | |
| } | |
| .btn--ghost:hover { | |
| background-color: rgba(125, 211, 252, 0.1); | |
| } | |
| .badge { | |
| display: inline-flex; | |
| align-items: center; | |
| padding: var(--sp-1) var(--sp-2); | |
| border-radius: var(--r-pill); | |
| font-size: var(--text-xs); | |
| font-weight: 500; | |
| background-color: rgba(125, 211, 252, 0.1); | |
| color: var(--pri); | |
| gap: var(--sp-1); | |
| } | |
| .input { | |
| width: 100%; | |
| padding: var(--sp-3); | |
| background-color: var(--bg-elev); | |
| border: 1px solid transparent; | |
| border-radius: var(--r-sm); | |
| color: var(--text); | |
| font-family: var(--font-mono); | |
| font-size: var(--text-sm); | |
| transition: all 0.15s ease; | |
| } | |
| .input:focus { | |
| outline: none; | |
| border-color: var(--pri); | |
| box-shadow: 0 0 0 1px var(--pri); | |
| } | |
| .input.is-invalid { | |
| border-color: var(--err); | |
| } | |
| .tabs { | |
| display: flex; | |
| gap: var(--sp-2); | |
| margin-bottom: var(--sp-4); | |
| } | |
| .tab { | |
| padding: var(--sp-2) var(--sp-4); | |
| border-radius: var(--r-pill); | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.15s ease; | |
| position: relative; | |
| color: var(--text-muted); | |
| } | |
| .tab.is-active { | |
| color: var(--pri); | |
| } | |
| .tab.is-active::after { | |
| content: ''; | |
| position: absolute; | |
| bottom: -2px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 50%; | |
| height: 2px; | |
| background-color: var(--pri); | |
| border-radius: 1px; | |
| } | |
| /* Header */ | |
| .header { | |
| position: sticky; | |
| top: 0; | |
| z-index: 50; | |
| backdrop-filter: blur(8px); | |
| background-color: rgba(18, 23, 32, 0.6); | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.06); | |
| } | |
| .header-container { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: var(--sp-3) 0; | |
| } | |
| .logo { | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .logo h1 { | |
| font-size: var(--text-xl); | |
| font-weight: 600; | |
| line-height: 1.2; | |
| } | |
| .logo p { | |
| font-size: var(--text-xs); | |
| color: var(--text-muted); | |
| } | |
| .header-actions { | |
| display: flex; | |
| align-items: center; | |
| gap: var(--sp-4); | |
| } | |
| .badge-group { | |
| display: flex; | |
| gap: var(--sp-2); | |
| } | |
| /* Hero */ | |
| .hero { | |
| padding: var(--sp-6) 0 var(--sp-4); | |
| text-align: center; | |
| } | |
| .hero p { | |
| color: var(--text-muted); | |
| font-size: var(--text-sm); | |
| margin-top: var(--sp-2); | |
| } | |
| /* Main Content */ | |
| .main { | |
| flex: 1; | |
| padding-bottom: var(--sp-6); | |
| } | |
| .content-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: var(--sp-4); | |
| margin-top: var(--sp-4); | |
| } | |
| /* Dropzone */ | |
| .dropzone { | |
| border: 2px dashed var(--muted); | |
| border-radius: var(--r); | |
| padding: var(--sp-6); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.15s ease; | |
| min-height: 200px; | |
| } | |
| .dropzone:hover { | |
| border-color: var(--pri); | |
| background-color: rgba(125, 211, 252, 0.05); | |
| } | |
| .dropzone.is-active { | |
| border-color: var(--ok); | |
| background-color: rgba(52, 211, 153, 0.05); | |
| } | |
| .dropzone-icon { | |
| width: 48px; | |
| height: 48px; | |
| margin-bottom: var(--sp-3); | |
| color: var(--muted); | |
| } | |
| .dropzone:hover .dropzone-icon { | |
| color: var(--pri); | |
| } | |
| /* Output Actions */ | |
| .output-actions { | |
| display: flex; | |
| gap: var(--sp-2); | |
| margin-top: var(--sp-4); | |
| } | |
| /* Advanced Panel */ | |
| .advanced-panel { | |
| overflow: hidden; | |
| max-height: 0; | |
| opacity: 0; | |
| transition: all 0.3s ease; | |
| background-color: var(--bg-elev); | |
| border-radius: 0 0 var(--r) var(--r); | |
| margin-top: -1px; | |
| } | |
| .advanced-panel.is-open { | |
| max-height: 500px; | |
| opacity: 1; | |
| padding: var(--sp-4); | |
| } | |
| /* Toasts */ | |
| .toast-container { | |
| position: fixed; | |
| top: var(--sp-4); | |
| right: var(--sp-4); | |
| display: flex; | |
| flex-direction: column; | |
| gap: var(--sp-2); | |
| z-index: 100; | |
| } | |
| .toast { | |
| padding: var(--sp-3) var(--sp-4); | |
| border-radius: var(--r-sm); | |
| background-color: var(--panel); | |
| box-shadow: var(--shadow-2); | |
| display: flex; | |
| align-items: center; | |
| gap: var(--sp-3); | |
| animation: slideIn 0.3s ease; | |
| } | |
| .toast--success { | |
| border-left: 4px solid var(--ok); | |
| } | |
| .toast--error { | |
| border-left: 4px solid var(--err); | |
| } | |
| .toast--info { | |
| border-left: 4px solid var(--info); | |
| } | |
| @keyframes slideIn { | |
| from { | |
| transform: translateY(-20px); | |
| opacity: 0; | |
| } | |
| to { | |
| transform: translateY(0); | |
| opacity: 1; | |
| } | |
| } | |
| /* Modal */ | |
| .modal { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: rgba(11, 15, 20, 0.8); | |
| backdrop-filter: blur(4px); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 200; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: all 0.3s ease; | |
| } | |
| .modal.is-open { | |
| opacity: 1; | |
| pointer-events: all; | |
| } | |
| .modal-content { | |
| background-color: var(--panel); | |
| border-radius: var(--r); | |
| width: 100%; | |
| max-width: 500px; | |
| padding: var(--sp-5); | |
| box-shadow: var(--shadow-2); | |
| transform: translateY(20px); | |
| transition: all 0.3s ease; | |
| } | |
| .modal.is-open .modal-content { | |
| transform: translateY(0); | |
| } | |
| .modal-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: var(--sp-4); | |
| } | |
| .modal-title { | |
| font-size: var(--text-xl); | |
| font-weight: 600; | |
| } | |
| /* Progress */ | |
| .progress { | |
| height: 4px; | |
| background-color: var(--bg-elev); | |
| border-radius: 2px; | |
| overflow: hidden; | |
| margin-top: var(--sp-4); | |
| } | |
| .progress-bar { | |
| height: 100%; | |
| background-color: var(--pri); | |
| transition: width 0.3s ease; | |
| } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| .content-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .header-container { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| gap: var(--sp-3); | |
| } | |
| .header-actions { | |
| width: 100%; | |
| justify-content: space-between; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Header --> | |
| <header class="header"> | |
| <div class="container header-container"> | |
| <div class="logo"> | |
| <h1>SecureCrypt</h1> | |
| <p>Client-Side Encryption</p> | |
| </div> | |
| <div class="header-actions"> | |
| <div class="badge-group"> | |
| <span class="badge">AES-GCM 256</span> | |
| <span class="badge">PBKDF2 600k</span> | |
| <span class="badge">10 MB</span> | |
| </div> | |
| <button class="btn btn--ghost" id="theme-toggle"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path> | |
| </svg> | |
| Theme | |
| </button> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Hero --> | |
| <section class="hero"> | |
| <div class="container"> | |
| <h2 class="text-2xl">Secure Encryption & Decryption</h2> | |
| <p>100% client-side · no uploads</p> | |
| </div> | |
| </section> | |
| <!-- User Instructions --> | |
| <section class="card" style="margin: var(--sp-4) auto; max-width: 1200px;"> | |
| <h3>How to Use SecureCrypt</h3> | |
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--sp-4); margin-top: var(--sp-3);"> | |
| <div> | |
| <h4 style="display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-2);"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path> | |
| </svg> | |
| Step 1: Choose Mode | |
| </h4> | |
| <p>Select either <strong>Text</strong> for encrypting/decrypting messages or <strong>File</strong> for documents (max 10MB).</p> | |
| </div> | |
| <div> | |
| <h4 style="display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-2);"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M12 15l8-8-8-8-8 8z"></path> | |
| </svg> | |
| Step 2: Set Key | |
| </h4> | |
| <p>Enter a strong passphrase or generate a secure 256-bit key. <strong>Never lose your key!</strong></p> | |
| </div> | |
| <div> | |
| <h4 style="display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-2);"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect> | |
| <path d="M7 11V7a5 5 0 0 1 10 0v4"></path> | |
| </svg> | |
| Step 3: Encrypt/Decrypt | |
| </h4> | |
| <p>Process your content and securely download the results.</p> | |
| </div> | |
| </div> | |
| <div style="background-color: var(--bg-elev); padding: var(--sp-3); border-radius: var(--r-sm); margin-top: var(--sp-4);"> | |
| <p style="color: var(--err); font-weight: 500;"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle; margin-right: var(--sp-2);"> | |
| <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path> | |
| <line x1="12" y1="9" x2="12" y2="13"></line> | |
| <line x1="12" y1="17" x2="12.01" y2="17"></line> | |
| </svg> | |
| Security Warning: If you lose your key, your data cannot be recovered! | |
| </p> | |
| </div> | |
| </section> | |
| <!-- Main Content --> | |
| <main class="main"> | |
| <div class="container"> | |
| <!-- Tabs --> | |
| <div class="tabs"> | |
| <div class="tab is-active" data-tab="text">Text</div> | |
| <div class="tab" data-tab="file">File</div> | |
| </div> | |
| <!-- Text Tab Content --> | |
| <div class="content-grid" id="text-tab"> | |
| <!-- Input Card --> | |
| <div class="card"> | |
| <h3>Input</h3> | |
| <textarea class="input" id="text-input" rows="10" placeholder="Enter text to encrypt or decrypt..."></textarea> | |
| <div class="output-actions"> | |
| <button class="btn btn--primary" id="encrypt-btn"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect> | |
| <path d="M7 11V7a5 5 0 0 1 10 0v4"></path> | |
| </svg> | |
| Encrypt | |
| </button> | |
| <button class="btn btn--primary" id="decrypt-btn"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect> | |
| <path d="M7 11V7a5 5 0 0 1 9.9-1"></path> | |
| </svg> | |
| Decrypt | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Output Card --> | |
| <div class="card"> | |
| <h3>Output</h3> | |
| <textarea class="input" id="text-output" rows="10" placeholder="Result will appear here..." readonly></textarea> | |
| <div class="output-actions"> | |
| <button class="btn btn--ghost" id="copy-output-btn"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> | |
| <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> | |
| </svg> | |
| Copy | |
| </button> | |
| <button class="btn btn--ghost" id="download-output-btn"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> | |
| <polyline points="7 10 12 15 17 10"></polyline> | |
| <line x1="12" y1="15" x2="12" y2="3"></line> | |
| </svg> | |
| Download | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- File Tab Content (Hidden by default) --> | |
| <div class="content-grid" id="file-tab" style="display: none;"> | |
| <!-- Input Card --> | |
| <div class="card"> | |
| <h3>Input File</h3> | |
| <div class="dropzone" id="file-dropzone"> | |
| <svg class="dropzone-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> | |
| <polyline points="17 8 12 3 7 8"></polyline> | |
| <line x1="12" y1="3" x2="12" y2="15"></line> | |
| </svg> | |
| <p>Drag & drop your file here</p> | |
| <p class="text-muted">or click to browse</p> | |
| <input type="file" id="file-input" style="display: none;"> | |
| </div> | |
| <div id="file-info" style="display: none; margin-top: var(--sp-3);"> | |
| <p><strong>File:</strong> <span id="file-name"></span></p> | |
| <p><strong>Size:</strong> <span id="file-size"></span></p> | |
| </div> | |
| <div class="output-actions"> | |
| <button class="btn btn--primary" id="encrypt-file-btn" disabled> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect> | |
| <path d="M7 11V7a5 5 0 0 1 10 0v4"></path> | |
| </svg> | |
| Encrypt File | |
| </button> | |
| <button class="btn btn--primary" id="decrypt-file-btn" disabled> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect> | |
| <path d="M7 11V7a5 5 0 0 1 9.9-1"></path> | |
| </svg> | |
| Decrypt File | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Output Card --> | |
| <div class="card"> | |
| <h3>Output File</h3> | |
| <div id="file-output-placeholder" style="text-align: center; padding: var(--sp-6);"> | |
| <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-bottom: var(--sp-3);"> | |
| <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> | |
| <polyline points="14 2 14 8 20 8"></polyline> | |
| </svg> | |
| <p>Processed file will appear here</p> | |
| </div> | |
| <div id="file-output-info" style="display: none;"> | |
| <p><strong>File:</strong> <span id="output-file-name"></span></p> | |
| <p><strong>Size:</strong> <span id="output-file-size"></span></p> | |
| </div> | |
| <div class="output-actions" id="file-output-actions" style="display: none;"> | |
| <button class="btn btn--ghost" id="download-file-btn"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> | |
| <polyline points="7 10 12 15 17 10"></polyline> | |
| <line x1="12" y1="15" x2="12" y2="3"></line> | |
| </svg> | |
| Download | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Key Input --> | |
| <div class="card" style="margin-top: var(--sp-4);"> | |
| <h3>Encryption Key</h3> | |
| <div style="display: flex; gap: var(--sp-2); margin-top: var(--sp-2);"> | |
| <input type="password" class="input" id="key-input" placeholder="Enter your encryption key or passphrase"> | |
| <button class="btn btn--ghost" id="toggle-key-visibility"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path> | |
| <circle cx="12" cy="12" r="3"></circle> | |
| </svg> | |
| </button> | |
| <button class="btn btn--ghost" id="generate-key-btn"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M12 22s8-4 8-10V5l-7-1-7 1v7c0 6 8 10 8 10z"></path> | |
| </svg> | |
| Generate | |
| </button> | |
| </div> | |
| <div class="progress" style="display: none;" id="progress-bar"> | |
| <div class="progress-bar" id="progress-bar-fill" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <!-- Advanced Panel Toggle --> | |
| <button class="btn btn--ghost" id="advanced-toggle" style="margin-top: var(--sp-4); width: 100%;"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <polyline points="6 9 12 15 18 9"></polyline> | |
| </svg> | |
| Advanced Information | |
| </button> | |
| <!-- Advanced Panel --> | |
| <div class="advanced-panel" id="advanced-panel"> | |
| <h3>Technical Specifications</h3> | |
| <div style="margin-top: var(--sp-3);"> | |
| <h4 style="margin-bottom: var(--sp-2);">Encryption Specifications</h4> | |
| <p style="margin-bottom: var(--sp-3);"> | |
| SecureCrypt implements industry-standard cryptographic protocols: | |
| </p> | |
| <ul style="margin-bottom: var(--sp-3); padding-left: var(--sp-3);"> | |
| <li style="margin-bottom: var(--sp-2);"><strong>AES-256-GCM</strong> - 256-bit key size with Galois/Counter Mode (NIST approved)</li> | |
| <li style="margin-bottom: var(--sp-2);"><strong>Key Generation</strong> - 32-byte (256-bit) cryptographically secure random keys</li> | |
| <li style="margin-bottom: var(--sp-2);"><strong>Initialization Vector</strong> - 12-byte random IV per encryption</li> | |
| <li style="margin-bottom: var(--sp-2);"><strong>Authentication</strong> - 128-bit GCM authentication tags</li> | |
| <li><strong>Key Wrapping</strong> - PBKDF2 with SHA-256 for passphrase strengthening</li> | |
| </ul> | |
| <h4 style="margin-bottom: var(--sp-2);">Key Derivation Details</h4> | |
| <p style="margin-bottom: var(--sp-3);"> | |
| When using a passphrase instead of a random key: | |
| </p> | |
| <ul style="margin-bottom: var(--sp-3); padding-left: var(--sp-3);"> | |
| <li style="margin-bottom: var(--sp-2);"><strong>PBKDF2-HMAC-SHA256</strong> - Password-Based Key Derivation Function 2</li> | |
| <li style="margin-bottom: var(--sp-2);"><strong>Iterations</strong> - 600,000 (NIST recommended minimum)</li> | |
| <li style="margin-bottom: var(--sp-2);"><strong>Salt</strong> - 16-byte cryptographically random per derivation</li> | |
| <li><strong>Output</strong> - 32-byte derived key material</li> | |
| </ul> | |
| <h4 style="margin-bottom: var(--sp-2);">Data Container Format</h4> | |
| <p style="margin-bottom: var(--sp-3);"> | |
| All encrypted data follows the ENCv1 specification: | |
| </p> | |
| <pre style="background-color: var(--bg-elev); padding: var(--sp-3); border-radius: var(--r-sm); margin-bottom: var(--sp-3); font-family: var(--font-mono); font-size: var(--text-sm); overflow-x: auto;"> | |
| { | |
| "v": 1, // Format version | |
| "alg": "AES-GCM", // Encryption algorithm | |
| "kdf": { // Key derivation params | |
| "name": "PBKDF2", | |
| "hash": "SHA-256", | |
| "iters": 600000, | |
| "salt_b64": "••••••••••••" // Random salt | |
| }, | |
| "iv_b64": "••••••••••••", // Initialization vector | |
| "keyType": "passphrase", // Key source | |
| "created": "2025-09-06T00:00:00Z", | |
| "type": "text", // Content type | |
| "orig": { // Original file info (if applicable) | |
| "name": "document.pdf", | |
| "mime": "application/pdf", | |
| "size": 123456 | |
| } | |
| }</pre> | |
| <div style="background-color: var(--bg-elev); padding: var(--sp-3); border-radius: var(--r-sm);"> | |
| <h4 style="margin-bottom: var(--sp-2);">Security Considerations</h4> | |
| <ul style="padding-left: var(--sp-3);"> | |
| <li style="margin-bottom: var(--sp-2);">All operations occur <strong>locally in your browser</strong></li> | |
| <li style="margin-bottom: var(--sp-2);">No data is ever transmitted over the network</li> | |
| <li style="margin-bottom: var(--sp-2);">Keys and plaintext are <strong>never stored</strong> on disk</li> | |
| <li>Protects against <strong>offline attacks</strong> but not compromised devices</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Key Generation Modal --> | |
| <div class="modal" id="key-modal"> | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <h3 class="modal-title">Generate Secure Key</h3> | |
| <button class="btn btn--ghost" id="close-modal"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <line x1="18" y1="6" x2="6" y2="18"></line> | |
| <line x1="6" y1="6" x2="18" y2="18"></line> | |
| </svg> | |
| </button> | |
| </div> | |
| <p style="margin-bottom: var(--sp-3);">For maximum security, use a 32-byte (256-bit) random key. You can also use a passphrase, but it will be strengthened with PBKDF2.</p> | |
| <div style="margin-bottom: var(--sp-3);"> | |
| <label style="display: block; margin-bottom: var(--sp-2);">Key Type</label> | |
| <div style="display: flex; gap: var(--sp-3);"> | |
| <label style="display: flex; align-items: center; gap: var(--sp-2);"> | |
| <input type="radio" name="key-type" value="random" checked> | |
| Random Key (Recommended) | |
| </label> | |
| <label style="display: flex; align-items: center; gap: var(--sp-2);"> | |
| <input type="radio" name="key-type" value="passphrase"> | |
| Passphrase | |
| </label> | |
| </div> | |
| </div> | |
| <div id="passphrase-input" style="display: none; margin-bottom: var(--sp-3);"> | |
| <label style="display: block; margin-bottom: var(--sp-2);">Passphrase</label> | |
| <input type="text" class="input" id="passphrase-field" placeholder="Enter a strong passphrase"> | |
| </div> | |
| <div style="margin-bottom: var(--sp-3);"> | |
| <label style="display: block; margin-bottom: var(--sp-2);">Generated Key</label> | |
| <textarea class="input" id="generated-key" rows="3" readonly style="font-family: var(--font-mono);"></textarea> | |
| </div> | |
| <div style="display: flex; gap: var(--sp-2);"> | |
| <button class="btn btn--ghost" id="copy-key-btn"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> | |
| <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> | |
| </svg> | |
| Copy | |
| </button> | |
| <button class="btn btn--ghost" id="download-key-btn"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> | |
| <polyline points="7 10 12 15 17 10"></polyline> | |
| <line x1="12" y1="15" x2="12" y2="3"></line> | |
| </svg> | |
| Download | |
| </button> | |
| <button class="btn btn--primary" id="use-key-btn" style="margin-left: auto;"> | |
| Use This Key | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Toast Container --> | |
| <div class="toast-container" id="toast-container"></div> | |
| <!-- Animated Background --> | |
| <div class="bg-animation" id="bg-animation"> | |
| <svg class="bg-lock" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect> | |
| <path d="M7 11V7a5 5 0 0 1 10 0v4"></path> | |
| </svg> | |
| <svg class="bg-key" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"></path> | |
| </svg> | |
| <svg class="bg-shield" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M12 22s8-4 8-10V5l-7-1-7 1v7c0 6 8 10 8 10z"></path> | |
| </svg> | |
| </div> | |
| <script> | |
| // Animated Background | |
| const bgAnimation = document.getElementById('bg-animation'); | |
| const icons = document.querySelectorAll('.bg-lock, .bg-key, .bg-shield'); | |
| const colors = ['var(--pri)', 'var(--pri-600)', 'var(--pri-700)', 'var(--info)', 'var(--ok)']; | |
| // Position icons randomly | |
| icons.forEach(icon => { | |
| // Random size between 40-80px | |
| const size = Math.random() * 40 + 40; | |
| icon.style.width = `${size}px`; | |
| icon.style.height = `${size}px`; | |
| // Random color from our palette | |
| icon.style.color = colors[Math.floor(Math.random() * colors.length)]; | |
| // Random initial position | |
| icon.style.left = `${Math.random() * 100}%`; | |
| icon.style.top = `${Math.random() * 100}%`; | |
| icon.style.opacity = '0.2'; | |
| // Random rotation | |
| icon.style.transform = `rotate(${Math.random() * 360}deg)`; | |
| }); | |
| // Mouse move animation | |
| document.addEventListener('mousemove', (e) => { | |
| const x = e.clientX; | |
| const y = e.clientY; | |
| icons.forEach((icon, i) => { | |
| // Each icon responds with different delay and intensity | |
| const delay = i * 0.1; | |
| const intensity = 0.3 + (i * 0.05); | |
| setTimeout(() => { | |
| const newX = x * intensity - (size * 0.5); | |
| const newY = y * intensity - (size * 0.5); | |
| icon.style.transform = `translate(${newX}px, ${newY}px) rotate(${Math.random() * 15 + (i * 10)}deg)`; | |
| }, delay * 1000); | |
| }); | |
| }); | |
| // Theme Toggle | |
| const themeToggle = document.getElementById('theme-toggle'); | |
| const html = document.documentElement; | |
| // Check for saved theme preference or use the color scheme preference | |
| const savedTheme = localStorage.getItem('theme'); | |
| const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; | |
| if (savedTheme) { | |
| html.classList.toggle('theme-light', savedTheme === 'light'); | |
| } else if (!prefersDark) { | |
| html.classList.add('theme-light'); | |
| } | |
| themeToggle.addEventListener('click', () => { | |
| html.classList.toggle('theme-light'); | |
| const theme = html.classList.contains('theme-light') ? 'light' : 'dark'; | |
| localStorage.setItem('theme', theme); | |
| }); | |
| // Tab Switching | |
| const tabs = document.querySelectorAll('.tab'); | |
| const textTab = document.getElementById('text-tab'); | |
| const fileTab = document.getElementById('file-tab'); | |
| tabs.forEach(tab => { | |
| tab.addEventListener('click', () => { | |
| tabs.forEach(t => t.classList.remove('is-active')); | |
| tab.classList.add('is-active'); | |
| if (tab.dataset.tab === 'text') { | |
| textTab.style.display = 'grid'; | |
| fileTab.style.display = 'none'; | |
| } else { | |
| textTab.style.display = 'none'; | |
| fileTab.style.display = 'grid'; | |
| } | |
| }); | |
| }); | |
| // Key Visibility Toggle | |
| const keyInput = document.getElementById('key-input'); | |
| const toggleKeyVisibility = document.getElementById('toggle-key-visibility'); | |
| toggleKeyVisibility.addEventListener('click', () => { | |
| const isPassword = keyInput.type === 'password'; | |
| keyInput.type = isPassword ? 'text' : 'password'; | |
| toggleKeyVisibility.innerHTML = isPassword ? | |
| `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path> | |
| <line x1="1" y1="1" x2="23" y2="23"></line> | |
| </svg>` : | |
| `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path> | |
| <circle cx="12" cy="12" r="3"></circle> | |
| </svg>`; | |
| }); | |
| // Advanced Panel Toggle | |
| const advancedToggle = document.getElementById('advanced-toggle'); | |
| const advancedPanel = document.getElementById('advanced-panel'); | |
| advancedToggle.addEventListener('click', () => { | |
| advancedPanel.classList.toggle('is-open'); | |
| advancedToggle.innerHTML = advancedPanel.classList.contains('is-open') ? | |
| `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <polyline points="18 15 12 9 6 15"></polyline> | |
| </svg> | |
| Advanced Settings` : | |
| `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <polyline points="6 9 12 15 18 9"></polyline> | |
| </svg> | |
| Advanced Settings`; | |
| }); | |
| // Key Generation Modal | |
| const generateKeyBtn = document.getElementById('generate-key-btn'); | |
| const keyModal = document.getElementById('key-modal'); | |
| const closeModal = document.getElementById('close-modal'); | |
| const keyTypeRadios = document.querySelectorAll('input[name="key-type"]'); | |
| const passphraseInput = document.getElementById('passphrase-input'); | |
| const passphraseField = document.getElementById('passphrase-field'); | |
| const generatedKey = document.getElementById('generated-key'); | |
| const copyKeyBtn = document.getElementById('copy-key-btn'); | |
| const downloadKeyBtn = document.getElementById('download-key-btn'); | |
| const useKeyBtn = document.getElementById('use-key-btn'); | |
| generateKeyBtn.addEventListener('click', () => { | |
| keyModal.classList.add('is-open'); | |
| }); | |
| closeModal.addEventListener('click', () => { | |
| keyModal.classList.remove('is-open'); | |
| }); | |
| keyTypeRadios.forEach(radio => { | |
| radio.addEventListener('change', () => { | |
| passphraseInput.style.display = radio.value === 'passphrase' ? 'block' : 'none'; | |
| if (radio.value === 'random') { | |
| generateRandomKey(); | |
| } else { | |
| generatedKey.value = ''; | |
| } | |
| }); | |
| }); | |
| passphraseField.addEventListener('input', () => { | |
| if (passphraseField.value.length > 0) { | |
| generatedKey.value = 'Key will be derived from passphrase using PBKDF2'; | |
| } else { | |
| generatedKey.value = ''; | |
| } | |
| }); | |
| function generateRandomKey() { | |
| const randomBytes = new Uint8Array(32); | |
| window.crypto.getRandomValues(randomBytes); | |
| const keyBase64 = btoa(String.fromCharCode(...randomBytes)); | |
| generatedKey.value = keyBase64; | |
| } | |
| copyKeyBtn.addEventListener('click', () => { | |
| if (generatedKey.value) { | |
| navigator.clipboard.writeText(generatedKey.value); | |
| showToast('Key copied to clipboard', 'success'); | |
| } | |
| }); | |
| downloadKeyBtn.addEventListener('click', () => { | |
| if (generatedKey.value) { | |
| const blob = new Blob([generatedKey.value], { type: 'text/plain' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'securecrypt-key.txt'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| showToast('Key download started', 'success'); | |
| } | |
| }); | |
| useKeyBtn.addEventListener('click', () => { | |
| if (generatedKey.value) { | |
| keyInput.value = generatedKey.value; | |
| keyModal.classList.remove('is-open'); | |
| showToast('Key applied to input', 'success'); | |
| } | |
| }); | |
| // Generate a random key when modal opens | |
| keyModal.addEventListener('click', (e) => { | |
| if (e.target === keyModal) { | |
| keyModal.classList.remove('is-open'); | |
| } | |
| }); | |
| // File Dropzone | |
| const fileDropzone = document.getElementById('file-dropzone'); | |
| const fileInput = document.getElementById('file-input'); | |
| const fileInfo = document.getElementById('file-info'); | |
| const fileName = document.getElementById('file-name'); | |
| const fileSize = document.getElementById('file-size'); | |
| const encryptFileBtn = document.getElementById('encrypt-file-btn'); | |
| const decryptFileBtn = document.getElementById('decrypt-file-btn'); | |
| fileDropzone.addEventListener('click', () => { | |
| fileInput.click(); | |
| }); | |
| fileInput.addEventListener('change', handleFileSelect); | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| fileDropzone.addEventListener(eventName, preventDefaults, false); | |
| }); | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| fileDropzone.addEventListener(eventName, highlight, false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| fileDropzone.addEventListener(eventName, unhighlight, false); | |
| }); | |
| function highlight() { | |
| fileDropzone.classList.add('is-active'); | |
| } | |
| function unhighlight() { | |
| fileDropzone.classList.remove('is-active'); | |
| } | |
| fileDropzone.addEventListener('drop', handleDrop, false); | |
| function handleDrop(e) { | |
| const dt = e.dataTransfer; | |
| const files = dt.files; | |
| if (files.length) { | |
| handleFiles(files); | |
| } | |
| } | |
| function handleFileSelect(e) { | |
| const files = e.target.files; | |
| if (files.length) { | |
| handleFiles(files); | |
| } | |
| } | |
| function handleFiles(files) { | |
| const file = files[0]; | |
| // Check file size (10MB limit) | |
| if (file.size > 10 * 1024 * 1024) { | |
| showToast('File is too large (max 10MB)', 'error'); | |
| return; | |
| } | |
| fileName.textContent = file.name; | |
| fileSize.textContent = formatFileSize(file.size); | |
| fileInfo.style.display = 'block'; | |
| encryptFileBtn.disabled = false; | |
| decryptFileBtn.disabled = false; | |
| } | |
| function formatFileSize(bytes) { | |
| if (bytes < 1024) return bytes + ' bytes'; | |
| else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; | |
| else return (bytes / 1048576).toFixed(1) + ' MB'; | |
| } | |
| // Toast Notifications | |
| function showToast(message, type) { | |
| const toastContainer = document.getElementById('toast-container'); | |
| const toast = document.createElement('div'); | |
| toast.className = `toast toast--${type}`; | |
| toast.setAttribute('role', 'status'); | |
| let icon; | |
| switch (type) { | |
| case 'success': | |
| icon = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path> | |
| <polyline points="22 4 12 14.01 9 11.01"></polyline> | |
| </svg>`; | |
| break; | |
| case 'error': | |
| icon = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <line x1="12" y1="8" x2="12" y2="12"></line> | |
| <line x1="12" y1="16" x2="12.01" y2="16"></line> | |
| </svg>`; | |
| break; | |
| default: | |
| icon = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <line x1="12" y1="16" x2="12" y2="12"></line> | |
| <line x1="12" y1="8" x2="12.01" y2="8"></line> | |
| </svg>`; | |
| } | |
| toast.innerHTML = `${icon}<span>${message}</span>`; | |
| toastContainer.appendChild(toast); | |
| setTimeout(() => { | |
| toast.style.opacity = '0'; | |
| setTimeout(() => { | |
| toast.remove(); | |
| }, 300); | |
| }, 5000); | |
| } | |
| // Simulate some functionality for demo purposes | |
| document.getElementById('encrypt-btn').addEventListener('click', () => { | |
| const textInput = document.getElementById('text-input'); | |
| if (textInput.value.trim() === '') { | |
| showToast('Please enter text to encrypt', 'error'); | |
| return; | |
| } | |
| if (keyInput.value.trim() === '') { | |
| showToast('Please enter an encryption key', 'error'); | |
| return; | |
| } | |
| // Simulate encryption process | |
| const progressBar = document.getElementById('progress-bar'); | |
| const progressFill = document.getElementById('progress-bar-fill'); | |
| progressBar.style.display = 'block'; | |
| progressFill.style.width = '0%'; | |
| let progress = 0; | |
| const interval = setInterval(() => { | |
| progress += 5; | |
| progressFill.style.width = `${progress}%`; | |
| if (progress >= 100) { | |
| clearInterval(interval); | |
| setTimeout(() => { | |
| document.getElementById('text-output').value = 'Encrypted: ' + btoa(textInput.value); | |
| progressBar.style.display = 'none'; | |
| showToast('Text encrypted successfully', 'success'); | |
| }, 200); | |
| } | |
| }, 50); | |
| }); | |
| document.getElementById('decrypt-btn').addEventListener('click', () => { | |
| const textInput = document.getElementById('text-input'); | |
| if (textInput.value.trim() === '') { | |
| showToast('Please enter text to decrypt', 'error'); | |
| return; | |
| } | |
| if (keyInput.value.trim() === '') { | |
| showToast('Please enter an encryption key', 'error'); | |
| return; | |
| } | |
| // Simulate decryption process | |
| const progressBar = document.getElementById('progress-bar'); | |
| const progressFill = document.getElementById('progress-bar-fill'); | |
| progressBar.style.display = 'block'; | |
| progressFill.style.width = '0%'; | |
| let progress = 0; | |
| const interval = setInterval(() => { | |
| progress += 5; | |
| progressFill.style.width = `${progress}%`; | |
| if (progress >= 100) { | |
| clearInterval(interval); | |
| setTimeout(() => { | |
| try { | |
| document.getElementById('text-output').value = 'Decrypted: ' + atob(textInput.value.replace('Encrypted: ', '')); | |
| showToast('Text decrypted successfully', 'success'); | |
| } catch (e) { | |
| document.getElementById('text-output').value = 'Error: Invalid encrypted text'; | |
| showToast('Decryption failed', 'error'); | |
| } | |
| progressBar.style.display = 'none'; | |
| }, 200); | |
| } | |
| }, 50); | |
| }); | |
| document.getElementById('copy-output-btn').addEventListener('click', () => { | |
| const textOutput = document.getElementById('text-output'); | |
| if (textOutput.value.trim() === '') { | |
| showToast('No output to copy', 'error'); | |
| return; | |
| } | |
| navigator.clipboard.writeText(textOutput.value); | |
| showToast('Output copied to clipboard', 'success'); | |
| }); | |
| document.getElementById('download-output-btn').addEventListener('click', () => { | |
| const textOutput = document.getElementById('text-output'); | |
| if (textOutput.value.trim() === '') { | |
| showToast('No output to download', 'error'); | |
| return; | |
| } | |
| const blob = new Blob([textOutput.value], { type: 'text/plain' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'securecrypt-output.txt'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| showToast('Output download started', 'success'); | |
| }); | |
| // Generate a random key when the page loads | |
| window.addEventListener('load', () => { | |
| generateRandomKey(); | |
| // Animate circles on load | |
| circles.forEach(circle => { | |
| const x = Math.random() * window.innerWidth; | |
| const y = Math.random() * window.innerHeight; | |
| circle.style.transform = `translate(${x}px, ${y}px)`; | |
| }); | |
| // Show welcome toast | |
| setTimeout(() => { | |
| showToast('Welcome to SecureCrypt! All encryption happens locally in your browser.', 'info'); | |
| }, 1000); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |