daily update 2026-03-29

This commit is contained in:
Агальцов Антон
2026-03-30 00:02:10 +03:00
commit 690abf379c
145 changed files with 14399 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
// 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);
};
+90
View File
@@ -0,0 +1,90 @@
// Search functionality using Fuse.js
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('search-input');
const searchForm = document.querySelector('.search-form');
const searchResults = document.getElementById('search-results');
const resultsList = document.getElementById('results-list');
const closeSearch = document.getElementById('close-search');
if (!searchInput) return;
let fuse;
// Load search index
fetch('/index.json')
.then(response => response.json())
.then(data => {
fuse = new Fuse(data, {
keys: ['title', 'content', 'tags'],
includeScore: true,
threshold: 0.3,
ignoreLocation: true,
minMatchCharLength: 2
});
})
.catch(err => console.error('Failed to load search index:', err));
function performSearch(query) {
if (!fuse || !query.trim()) {
searchResults.style.display = 'none';
return;
}
const results = fuse.search(query, { limit: 10 });
if (results.length === 0) {
resultsList.innerHTML = '<li>Ничего не найдено</li>';
} else {
resultsList.innerHTML = results.map(result => {
const item = result.item;
const sectionLabels = {
'diary': 'Дневник',
'kb': 'База знаний',
'plans': 'Планы',
'guides': 'Инструкции',
'creds': 'Креды'
};
const label = sectionLabels[item.section] || item.section;
return `<li>
<a href="${item.url}">
<strong>${item.title}</strong>
<small class="text-muted d-block">${label}</small>
</a>
</li>`;
}).join('');
}
searchResults.style.display = 'block';
}
// Debounce function
let debounceTimer;
function debounce(func, delay) {
return function() {
const context = this;
const args = arguments;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => func.apply(context, args), delay);
};
}
searchInput.addEventListener('input', debounce(function(e) {
performSearch(e.target.value);
}, 200));
searchForm.addEventListener('submit', function(e) {
e.preventDefault();
performSearch(searchInput.value);
});
closeSearch.addEventListener('click', function() {
searchResults.style.display = 'none';
});
// Close search when clicking outside
document.addEventListener('click', function(e) {
if (!searchForm.contains(e.target) && !searchResults.contains(e.target)) {
searchResults.style.display = 'none';
}
});
});