Files
Агальцов Антон 690abf379c daily update 2026-03-29
2026-03-30 00:02:10 +03:00

146 lines
4.8 KiB
JavaScript

// Credentials encryption/decryption using Web Crypto API
document.addEventListener('DOMContentLoaded', function() {
const decryptButtons = document.querySelectorAll('.decrypt-btn');
decryptButtons.forEach(button => {
button.addEventListener('click', async function() {
const id = this.dataset.id;
const encryptedDiv = document.getElementById('encrypted-' + id);
const decryptedDiv = document.getElementById('decrypted-' + id);
// Get master password
const password = prompt('Введите мастер-пароль:');
if (!password) return;
try {
const encryptedBase64 = encryptedDiv.textContent.trim();
const decrypted = await decrypt(encryptedBase64, password);
// Parse and display decrypted content
const data = JSON.parse(decrypted);
let html = '';
for (const [key, value] of Object.entries(data)) {
const label = formatLabel(key);
html += `<div class="field-row">
<span class="field-label">${label}:</span>
<span class="field-value">${value}</span>
</div>`;
}
decryptedDiv.innerHTML = html;
decryptedDiv.style.display = 'block';
encryptedDiv.style.display = 'none';
this.textContent = 'Скрыть';
this.classList.remove('btn-primary');
this.classList.add('btn-secondary');
// Change button to toggle
this.onclick = function() {
if (decryptedDiv.style.display === 'none') {
decryptedDiv.style.display = 'block';
encryptedDiv.style.display = 'none';
this.textContent = 'Скрыть';
} else {
decryptedDiv.style.display = 'none';
encryptedDiv.style.display = 'block';
this.textContent = 'Расшифровать';
}
};
} catch (error) {
console.error('Decryption error:', error);
alert('Ошибка расшифровки. Проверьте мастер-пароль.');
}
});
});
});
function formatLabel(key) {
const labels = {
'user': 'Пользователь',
'password': 'Пароль',
'host': 'Хост',
'port': 'Порт',
'database': 'База данных',
'notes': 'Заметки',
'ip': 'IP',
'auth': 'Авторизация',
'vpn': 'VPN'
};
return labels[key] || key;
}
// Derive encryption key from password using PBKDF2
async function deriveKey(password, salt) {
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
{ name: 'PBKDF2' },
false,
['deriveBits', 'deriveKey']
);
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
// Decrypt data
async function decrypt(encryptedBase64, password) {
const encryptedData = Uint8Array.from(atob(encryptedBase64), c => c.charCodeAt(0));
// Extract salt (first 16 bytes), IV (next 12 bytes), and ciphertext
const salt = encryptedData.slice(0, 16);
const iv = encryptedData.slice(16, 28);
const ciphertext = encryptedData.slice(28);
const key = await deriveKey(password, salt);
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv },
key,
ciphertext
);
return new TextDecoder().decode(decrypted);
}
// Encrypt data (for creating new credentials)
async function encrypt(plaintext, password) {
const encoder = new TextEncoder();
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await deriveKey(password, salt);
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
key,
encoder.encode(plaintext)
);
// Combine salt + iv + ciphertext
const combined = new Uint8Array(16 + 12 + ciphertext.byteLength);
combined.set(salt, 0);
combined.set(iv, 16);
combined.set(new Uint8Array(ciphertext), 28);
return btoa(String.fromCharCode(...combined));
}
// Utility function for encrypting credentials (can be used in console)
window.encryptCreds = async function(data, password) {
const json = JSON.stringify(data);
return await encrypt(json, password);
};