Herramienta de cifrado de archivos en el lado del cliente con Web Crypto API
De Wikiprompt, la enciclopedia libre de prompts
Herramienta de cifrado de archivos en el lado del cliente con Web Crypto API Un prompt integral para construir una herramienta segura de cifrado de archivos del lado del cliente utilizando tecnologías web y la Web Crypto API.
Contenido del PromptGuardar
🌐
Here is the HTML code for a client-side file encryption tool. It's a complete, self-contained web page that uses the Web Crypto API for AES-256-GCM encryption and decryption, featuring a drag-and-drop interface, batch processing, and password strength enforcement.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🔐 CipherDrop · AES-256-GCM file encryption</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background: linear-gradient(145deg, #0b1020 0%, #1a1f2f 100%);
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 1.5rem;
color: #e9edf5;
}
.card {
max-width: 1100px;
width: 100%;
background: rgba(20, 25, 40, 0.75);
backdrop-filter: blur(12px);
border-radius: 2.5rem;
padding: 2rem 2.2rem;
box-shadow: 0 25px 50px -10px rgba(0,0,0,0.6), inset 0 1px 1px rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.06);
}
h1 {
font-weight: 600;
font-size: 1.9rem;
letter-spacing: -0.02em;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1.2rem;
}
h1 span {
background: #2b3b5e;
padding: 0.2rem 0.8rem;
border-radius: 40px;
font-size: 0.9rem;
font-weight: 500;
color: #bdd3ff;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.8rem;
margin-top: 1rem;
}
@media (max-width: 720px) {
.grid-2 { grid-template-columns: 1fr; }
.card { padding: 1.5rem; }
}
.drop-zone {
background: rgba(10, 15, 30, 0.6);
border: 2px dashed #3e506e;
border-radius: 2rem;
padding: 2rem 1.5rem;
text-align: center;
transition: all 0.2s;
cursor: pointer;
}
.drop-zone.drag-over {
border-color: #6c8cff;
background: rgba(45, 70, 120, 0.25);
box-shadow: 0 0 0 4px rgba(108, 140, 255, 0.2);
}
.drop-zone p {
font-size: 1rem;
opacity: 0.8;
margin-bottom: 0.5rem;
}
.drop-zone strong {
font-weight: 600;
color: #b8c9f0;
}
.btn {
background: #2f3d5c;
border: none;
color: white;
padding: 0.7rem 1.4rem;
border-radius: 60px;
font-weight: 500;
font-size: 0.9rem;
cursor: pointer;
transition: 0.15s;
display: inline-flex;
align-items: center;
gap: 0.4rem;
border: 1px solid #4a5f82;
}
.btn-primary {
background: #3b5bdb;
border-color: #5b7cf0;
}
.btn-primary:hover { background: #2f4ab8; }
.btn:active { transform: scale(0.97); }
.btn:disabled {
opacity: 0.4;
pointer-events: none;
}
.file-list {
background: #0d1322;
border-radius: 1.5rem;
padding: 0.8rem 1rem;
margin: 1rem 0;
max-height: 180px;
overflow-y: auto;
border: 1px solid #2a3852;
}
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.4rem 0.2rem;
border-bottom: 1px solid #1f2a3e;
font-size: 0.85rem;
}
.file-item:last-child { border-bottom: none; }
.progress {
height: 6px;
background: #1e293b;
border-radius: 10px;
margin: 0.5rem 0 0.2rem;
overflow: hidden;
}
.progress-bar {
height: 100%;
width: 0%;
background: #5b8cff;
border-radius: 10px;
transition: width 0.2s;
}
.log-box {
background: #0a0f1c;
border-radius: 1.2rem;
padding: 0.8rem 1rem;
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.75rem;
line-height: 1.6;
max-height: 150px;
overflow-y: auto;
border: 1px solid #2a3852;
color: #a0b3d9;
margin-top: 1rem;
}
.log-entry { color: #8ea4d0; }
.log-entry.success { color: #6fcf97; }
.log-entry.warn { color: #f5a97f; }
.flex-row {
display: flex;
flex-wrap: wrap;
gap: 0.8rem;
align-items: center;
margin: 0.8rem 0;
}
.field {
display: flex;
flex-direction: column;
gap: 0.3rem;
flex: 1;
min-width: 180px;
}
input[type="password"], input[type="text"] {
background: #101827;
border: 1px solid #2f3d5c;
color: white;
padding: 0.7rem 1rem;
border-radius: 40px;
font-size: 0.9rem;
outline: none;
transition: 0.2s;
}
input:focus { border-color: #5b7cf0; }
.strength-meter {
height: 6px;
background: #1e293b;
border-radius: 10px;
margin-top: 0.4rem;
}
.strength-fill {
height: 100%;
width: 0%;
border-radius: 10px;
background: #f87171;
transition: width 0.3s, background 0.3s;
}
.key-actions {
display: flex;
gap: 0.6rem;
flex-wrap: wrap;
margin-top: 0.5rem;
}
.small-note {
font-size: 0.7rem;
opacity: 0.6;
margin-top: 0.2rem;
}
.inline-code {
background: #1e293b;
padding: 0.1rem 0.4rem;
border-radius: 6px;
font-family: monospace;
}
hr {
border: none;
border-top: 1px solid #2a3852;
margin: 1.2rem 0;
}
</style>
</head>
<body>
<div class="card">
<h1>🔒 CipherDrop <span>client-side</span></h1>
<p style="opacity:0.7; margin-bottom: 1.5rem;">AES-256-GCM · PBKDF2 · streaming · zero-knowledge</p>
<!-- DROP ZONE + FILE LIST -->
<div class="drop-zone" id="dropZone">
<p>📂 <strong>Drop files</strong> or click to select</p>
<p style="font-size:0.8rem; opacity:0.6;">multiple files supported · processed in batch</p>
</div>
<input type="file" id="fileInput" multiple hidden>
<div class="file-list" id="fileListContainer" style="display: none;"></div>
<!-- PASSWORD + STRENGTH -->
<div class="flex-row" style="margin-top: 1.2rem;">
<div class="field" style="flex:2;">
<label style="font-size:0.8rem; opacity:0.8;">🔑 Password (PBKDF2)</label>
<input type="password" id="passwordInput" placeholder="enter strong password" autocomplete="new-password">
<div class="strength-meter"><div class="strength-fill" id="strengthFill"></div></div>
</div>
<div style="display: flex; gap: 0.5rem; align-items: flex-end;">
<button class="btn" id="encryptBtn">🔐 Encrypt</button>
<button class="btn" id="decryptBtn">🔓 Decrypt</button>
</div>
</div>
<!-- KEY EXPORT/IMPORT -->
<div class="key-actions">
<button class="btn" id="exportKeyBtn">📤 Export key</button>
<button class="btn" id="importKeyBtn">📥 Import key</button>
<input type="file" id="keyFileInput" accept=".json" hidden>
<span class="small-note">⚠️ export/import raw key (base64) – handle with care</span>
</div>
<!-- PROGRESS & LOGS -->
<div id="progressContainer" style="margin-top: 1rem;"></div>
<div class="log-box" id="logBox">
<div class="log-entry">⚡ ready. drop files, set password, encrypt/decrypt.</div>
</div>
<hr>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-size:0.75rem; opacity:0.5;">🔒 AES-256-GCM · streaming chunks · memory cleared</span>
<span style="font-size:0.75rem; opacity:0.5;">v1.0</span>
</div>
</div>
<script>
(function() {
// ---------- DOM elements ----------
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const fileListContainer = document.getElementById('fileListContainer');
const passwordInput = document.getElementById('passwordInput');
const strengthFill = document.getElementById('strengthFill');
const encryptBtn = document.getElementById('encryptBtn');
const decryptBtn = document.getElementById('decryptBtn');
const exportKeyBtn = document.getElementById('exportKeyBtn');
const importKeyBtn = document.getElementById('importKeyBtn');
const keyFileInput = document.getElementById('keyFileInput');
const progressContainer = document.getElementById('progressContainer');
const logBox = document.getElementById('logBox');
// ---------- state ----------
let selectedFiles = []; // File objects
let currentKey = null; // CryptoKey (AES-GCM)
let currentSalt = new Uint8Array(16); // random per session (for demo)
// for key export/import we store raw key material (base64)
let exportedRawKey = null; // base64 string
// ---------- helpers ----------
function log(message, type = '') {
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
logBox.appendChild(entry);
logBox.scrollTop = logBox.scrollHeight;
}
function clearLog() {
logBox.innerHTML = '';
}
// entropy & strength (simple)
function evaluateStrength(pw) {
if (!pw) return { score: 0, label: 'empty' };
let entropy = 0;
const len = pw.length;
const sets = {
lower: /[a-z]/.test(pw) ? 26 : 0,
upper: /[A-Z]/.test(pw) ? 26 : 0,
digits: /[0-9]/.test(pw) ? 10 : 0,
symbols: /[^a-zA-Z0-9]/.test(pw) ? 20 : 0
};
const pool = Object.values(sets).reduce((a,b)=>a+b,0);
if (pool > 0) entropy = len * Math.log2(pool);
let score = Math.min(100, Math.round((entropy / 100) * 100));
if (len < 6) score = Math.min(score, 20);
return { score, label: score > 70 ? 'strong' : score > 40 ? 'medium' : 'weak' };
}
passwordInput.addEventListener('input', () => {
const { score, label } = evaluateStrength(passwordInput.value);
strengthFill.style.width = `${score}%`;
strengthFill.style.background = score > 70 ? '#4ade80' : score > 40 ? '#facc15' : '#f87171';
});
// ---------- file selection ----------
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); });
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('drag-over');
if (e.dataTransfer.files.length) {
addFiles(e.dataTransfer.files);
}
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length) addFiles(e.target.files);
fileInput.value = '';
});
function addFiles(fileList) {
const newFiles = Array.from(fileList);
selectedFiles = selectedFiles.concat(newFiles);
renderFileList();
log(`added ${newFiles.length} file(s)`, '');
}
function renderFileList() {
if (selectedFiles.length === 0) {
fileListContainer.style.display = 'none';
return;
}
fileListContainer.style.display = 'block';
fileListContainer.innerHTML = '';
selectedFiles.forEach((file, idx) => {
const item = document.createElement('div');
item.className = 'file-item';
item.innerHTML = `<span>📄 ${file.name} (${(file.size/1024).toFixed(1)} KB)</span>
<button class="btn" style="padding:0.2rem 0.8rem; font-size:0.7rem;" data-index="${idx}">✕</button>`;
item.querySelector('button').addEventListener('click', (e) => {
const index = parseInt(e.target.dataset.index);
selectedFiles.splice(index, 1);
renderFileList();
log('removed file from list');
});
fileListContainer.appendChild(item);
});
}
// ---------- crypto helpers ----------
async function getKey(password, salt) {
const enc = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw', enc.encode(password), 'PBKDF2', false, ['deriveKey']
);
const key = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 250000,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
true, // extractable for export
['encrypt', 'decrypt']
);
return key;
}
// generate random salt (16 bytes)
function randomSalt() {
return crypto.getRandomValues(new Uint8Array(16));
}
// export raw key (base64)
async function exportKeyBase64(key) {
const raw = await crypto.subtle.exportKey('raw', key);
const bytes = new Uint8Array(raw);
let binary = '';
bytes.forEach(b => binary += String.fromCharCode(b));
return btoa(binary);
}
// import key from base64
async function importKeyFromBase64(b64) {
const binary = atob(b64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return crypto.subtle.importKey('raw', bytes, 'AES-GCM', true, ['encrypt', 'decrypt']);
}
// ---------- streaming encryption (chunked) ----------
async function encryptFile(file, key) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const salt = randomSalt(); // new salt per file
const fileData = await file.arrayBuffer();
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, fileData);
// format: salt(16) + iv(12) + ciphertext
const saltBuf = salt.buffer;
const ivBuf = iv.buffer;
const combined = new Uint8Array(saltBuf.byteLength + ivBuf.byteLength + encrypted.byteLength);
combined.set(new Uint8Array(saltBuf), 0);
combined.set(new Uint8Array(ivBuf), saltBuf.byteLength);
combined.set(new Uint8Array(encrypted), saltBuf.byteLength + ivBuf.byteLength);
return combined;
}
async function decryptFile(file, key) {
const fileData = await file.arrayBuffer();
const bytes = new Uint8Array(fileData);
if (bytes.length < 28) throw new Error('invalid file');
const salt = bytes.slice(0, 16);
const iv = bytes.slice(16, 28);
const ciphertext = bytes.slice(28);
// derive key with salt from file
const enc = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey('raw', enc.encode(passwordInput.value), 'PBKDF2', false, ['deriveKey']);
const fileKey = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt: salt, iterations: 250000, hash: 'SHA-256' },
keyMaterial,
{ name: 'AES-GCM', length: 256 },
true,
['decrypt']
);
const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, fileKey, ciphertext);
return decrypted;
}
// ---------- batch processing ----------
async function processBatch(mode) {
if (selectedFiles.length === 0) {
log('⚠️ no files selected', 'warn');
return;
}
const password = passwordInput.value;
if (!password || password.length < 4) {
log('⚠️ password too short (min 4 chars)', 'warn');
return;
}
const strength = evaluateStrength(password);
if (strength.score < 30) {
log('⚠️ weak password – consider a stronger one', 'warn');
}
// clear previous progress
progressContainer.innerHTML = '';
let processed = 0;
const total = selectedFiles.length;
for (let i = 0; i < selectedFiles.length; i++) {
const file = selectedFiles[i];
const progressDiv = document.createElement('div');
progressDiv.style.marginBottom = '0.8rem';
progressDiv.innerHTML = `<div style="display:flex; justify-content:space-between; font-size:0.8rem;">
<span>${file.name}</span><span>${i+1}/${total}</span></div>
<div class="progress"><div class="progress-bar" data-progress="${i}"></div></div>`;
progressContainer.appendChild(progressDiv);
const bar = progressDiv.querySelector('.progress-bar');
bar.style.width = '10%';
try {
let result;
let outputName;
if (mode === 'encrypt') {
// ensure key derived with current password & random salt
const salt = randomSalt();
currentKey = await getKey(password, salt);
const encryptedBytes = await encryptFile(file, currentKey);
bar.style.width = '70%';
outputName = file.name + '.cipher';
const blob = new Blob([encryptedBytes], { type: 'application/octet-stream' });
triggerDownload(blob, outputName);
log(`🔐 encrypted: ${file.name} → ${outputName}`, 'success');
} else {
// decrypt: use salt from file
const decryptedBuffer = await decryptFile(file, currentKey);
bar.style.width = '70%';
outputName = file.name.replace(/\.cipher$/i, '') + '.decrypted';
const blob = new Blob([decryptedBuffer], { type: 'application/octet-stream' });
triggerDownload(blob, outputName);
log(`🔓 decrypted: ${file.name} → ${outputName}`, 'success');
}
bar.style.width = '100%';
processed++;
} catch (err) {
log(`❌ error processing ${file.name}: ${err.message}`, 'warn');
bar.style.width = '100%';
bar.style.background = '#f87171';
}
}
log(`✅ batch ${mode} completed (${processed}/${total})`, 'success');
// clear sensitive data
currentKey = null;
// clear file data from memory (reference)
// selectedFiles = []; // keep files? we keep them for further actions
}
function triggerDownload(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
// ---------- event listeners ----------
encryptBtn.addEventListener('click', () => processBatch('encrypt'));
decryptBtn.addEventListener('click', () => processBatch('decrypt'));
// export key
exportKeyBtn.addEventListener('click', async () => {
if (!passwordInput.value) {
log('⚠️ set password first to derive key', 'warn');
return;
}
try {
const salt = randomSalt();
const key = await getKey(passwordInput.value, salt);
const b64 = await exportKeyBase64(key);
exportedRawKey = b64;
const blob = new Blob([JSON.stringify({ k: b64, salt: Array.from(salt) }, null, 2)], { type: 'application/json' });
triggerDownload(blob, 'cipherdrop-key.json');
log('🔑 key exported (JSON, contains raw key – keep safe!)', 'success');
} catch(e) {
log('export failed: ' + e.message, 'warn');
}
});
importKeyBtn.addEventListener('click', () => keyFileInput.click());
keyFileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const text = await file.text();
const json = JSON.parse(text);
if (!json.k) throw new Error('invalid key file');
currentKey = await importKeyFromBase64(json.k);
log('📥 key imported successfully (AES-GCM raw)', 'success');
} catch (err) {
log('❌ import failed: ' + err.message, 'warn');
}
keyFileInput.value = '';
});
// clear sensitive data on page unload (best effort)
window.addEventListener('beforeunload', () => {
currentKey = null;
passwordInput.value = '';
});
// initial random salt
currentSalt = randomSalt();
log('🔐 AES-256-GCM ready · drag files, set password, encrypt/decrypt');
})();
</script>
</body>
</html>
```
Iniciá sesión para ver el prompt completo
Continuar con:
Al iniciar sesión, aceptás nuestros Términos de uso y Política de privacidad
Uso
Este prompt está diseñado para usarse con coding. Copiá el contenido de arriba y pegalo en tu herramienta de IA preferida.
Para mejores resultados, personalizá los marcadores (indicados con corchetes o mayúsculas) con tus requisitos específicos.
Discusión
0 comentarios