Вопрос или проблема
Я создал расширение для Firefox, но не совсем уверен в его безопасности. Можете ли вы помочь, как моей программе следует хранить постоянные ключи? Сейчас я храню их в локальном хранилище в зашифрованном виде, но это расширение будет с открытым исходным кодом, так что лучше разобраться со всеми проблемами до релиза.
Html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="popup.css">
<title>Главное меню</title>
</head>
<body>
<header>
<div class="new_window">
<img src="materials/open_in_new_24dp_E8EAED_FILL0_wght400_GRAD0_opsz24.svg" alt="открыть"/>
</div>
<div class="setting">
<img src="materials/settings_24dp_E8EAED_FILL0_wght400_GRAD0_opsz24.svg" alt="настройки"/>
</div>
</header>
<main>
<div class="choose__action">
<div class="choose__action__encrypt">
зашифровать
</div>
<div class="choose__action__decrypt">
расшифровать
</div>
</div>
<label>
<textarea class="text_input"></textarea>
</label>
<div class="key_input_box">
<label class="switch">
одноразовый ключ
<input type="checkbox" class="checkbox__switcher"/>
постоянный ключ
</label>
<label>
<input type="text" class="key_input"/>
</label>
</div>
<label class="output__box">
<div class="output_text">Результат</div>
<textarea class="output" disabled></textarea>
</label>
<div class="send_button_box">
<button class="operation__button">
Конвертировать
</button>
</div>
</main>
<div class="settings__window">
<div class="settings__flex_block">
<div class="settings__title">Настройки</div>
<div class="settings__exit__button">
<img src="materials/close_24dp_E8EAED_FILL0_wght400_GRAD0_opsz24.svg" alt="закрыть">
</div>
</div>
<div class="settings_description">
Внимание! Чтобы избежать перебора паролей, настоятельно рекомендуем использовать пароль длиной 10 символов, который содержит хотя бы одну цифру и специальные символы.
Постоянные ключи хранятся в зашифрованном виде локально.
</div>
<div class="change__block">
<div>
<span class="settings__encryption_errors"></span>
<input type="text" class="settings__encryption__key" placeholder="Установить ключ шифрования"/>
<button class="encryption__key__button">Сохранить</button>
</div>
<div>
<span class="settings__decryption_errors"></span>
<input type="text" class="settings__decryption__key" placeholder="Установить ключ расшифрования"/>
<button class="decryption__key__button">Сохранить</button>
</div>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>
css:
*{
box-sizing: border-box;
}
input, textarea{
outline: none;
}
header{
display: flex;
justify-content: space-between;
}
.new_window, .setting{
cursor: pointer;
}
main{
display: flex;
flex-direction: column;
align-items: center;
padding: 10px 70px 50px;
}
.choose__action{
display: flex;
justify-content: center;
cursor: pointer;
margin-top: 30px;
}
.choose__action{
border: 1px solid black;
}
.choose__action__encrypt{
border-right: 1px solid black;
padding: 5px 10px;
}
.choose__action__decrypt{
padding: 5px 10px;
}
.choose__action__encrypt.active, .choose__action__decrypt.active{
background-color: #000;
color: #fff;
}
.text_input{
margin-top: 50px;
resize: none;
width: 235px;
height: 100px;
}
.key_input_box{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 20px;
}
.key_input{
width: 235px;
height: 25px;
margin-top: 20px;
}
.switch{
display: flex;
justify-content: center;
align-items: center;
gap: 5px;
/*transform: translate(-50%, -50%);*/
}
.checkbox__switcher{
position: relative;
width: 40px;
height: 20px;
-webkit-appearance: none;
background: #c6c6c6;
border-radius: 20px;
transition: .5s;
box-shadow: inset 0 0 5px rgba(135, 97, 97, 0.5);
cursor: pointer;
}
.checkbox__switcher:checked{
background-color: #171616;
}
.checkbox__switcher::before{
content: "";
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
top: 0;
left: 0;
background-color: #fff;
transition: .5s;
transform: scale(1.1);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.69);
}
.checkbox__switcher:checked::before{
left: 20px;
}
.output__box{
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
margin-top: 20px;
}
.output{
resize: none;
width: 235px;
height: 100px;
margin-top: 10px;
}
.operation__button{
margin-top: 15px;
background-color: transparent;
color: black;
border: 1px solid black;
transition: .3s ease-in-out;
cursor: pointer;
padding: 5px 20px;
}
.operation__button:hover{
background-color: #000;
color: #fff;
}
.modal{
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
/*display: flex;*/
flex-direction: column;
gap: 5px;
z-index: 2;
background-color: #fff;
padding: 10px;
border-radius: 15px;
}
.saveKeyButton{
width: max-content;
height: max-content;
padding: 3px 15px;
background-color: transparent;
border: 1px solid black;
cursor: pointer;
transition: .3s ease-in-out;
}
.saveKeyButton:hover{
background-color: black;
color: white;
}
.modal_background{
display: none;
position: absolute;
top: 0;
left: 0;
z-index: 1;
background-color: rgba(0, 0, 0, 0.49);
width: 100%;
height: 100%;
}
.modal_warning{
font-size: 15px;
}
.settings__window{
display: none;
position: absolute;
top: 0;
left: 0;
z-index: 1;
background-color: rgb(255, 255, 255);
width: 100%;
height: 100%;
}
.settings__exit__button{
position: absolute;
top: 7px;
right: 7px;
cursor: pointer;
}
.settings__flex_block{
display: flex;
justify-content: center;
margin-top: 10px;
}
.settings__title{
font-size: 20px;
font-family: Verdana, serif;
}
.change__block{
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 10px;
margin-left: 10px;
}
.settings__encryption__key, .settings__decryption__key{
padding: 5px 20px 5px 5px;
}
.encryption__key__button, .decryption__key__button{
background-color: transparent;
color: #000000;
transition: .3s ease-in-out;
border: 1px solid #000;
cursor: pointer;
padding: 5px 20px;
margin-left: 5px;
}
.encryption__key__button:hover, .decryption__key__button:hover{
background-color: #000;
color: #fff;
}
.settings_description{
padding: 10px;
font-family: Verdana, serif;
font-size: 15px;
}
js:
let encrypt__button = document.querySelector('.choose__action__encrypt')
let decrypt__button = document.querySelector('.choose__action__decrypt')
let key_input = document.querySelector('.key_input')
let switcher = document.querySelector('.checkbox__switcher')
let new_window = document.querySelector('.new_window')
let send_button = document.querySelector('.operation__button')
let input_text = document.querySelector('.text_input')
let output_text = document.querySelector('.output')
let output_warning = document.querySelector('.output_text')
let setting__button = document.querySelector('.setting')
let settings_window = document.querySelector('.settings__window')
let settings__exit = document.querySelector('.settings__exit__button')
let settings__encryption__key = document.querySelector('.settings__encryption__key')
let settings__decryption__key = document.querySelector('.settings__decryption__key')
let encryption__key__button = document.querySelector('.encryption__key__button')
let decryption__key__button = document.querySelector('.decryption__key__button')
// Удобство и сохранение методов в локальном хранилище
if (localStorage.getItem('method')){
if (localStorage.getItem('method') === 'decrypt'){
decrypt__button.classList.add('active')
} else{
encrypt__button.classList.add('active')
}
} else{
encrypt__button.classList.add('active')
}
if (localStorage.getItem('key_input')){
if (localStorage.getItem('key_input') === 'true'){
switcher.checked = true
key_input.disabled = true
} else{
switcher.checked = false
key_input.disabled = false
}
}
//открытие нового окна
new_window.addEventListener('click', () =>{
browser.windows.create({
url: "popup.html",
type: "popup",
width: 450,
height: 700
});
})
//смена методов
decrypt__button.addEventListener('click', () =>{
if (encrypt__button.classList.contains('active')){
encrypt__button.classList.remove('active');
}
decrypt__button.classList.add('active')
localStorage.setItem("method", "decrypt")
})
encrypt__button.addEventListener('click', () =>{
if (decrypt__button.classList.contains('active')){
decrypt__button.classList.remove('active');
}
encrypt__button.classList.add('active')
localStorage.setItem("method", "encrypt");
})
//выбор пароля
switcher.addEventListener('click', () =>{
if (switcher.checked){
key_input.disabled = true
localStorage.setItem('key_input', 'true')
} else{
key_input.disabled = false
localStorage.setItem('key_input', 'false')
}
})
//Кнопка настроек
setting__button.addEventListener('click', ()=>{
if (!(settings_window.style.display === 'block')){
settings_window.style.display = 'block'
}
})
//Окно настроек
settings__exit.addEventListener('click', ()=>{
if (!(settings_window.style.display === 'none')){
settings_window.style.display = 'none'
}
})
//Смена постоянных ключей
encryption__key__button.addEventListener('click', ()=>{
if (settings__encryption__key.value){
let check = password_check(settings__encryption__key.value)
if (check){
saveEncryptionKeys(settings__encryption__key.value)
settings__encryption__key.value=""
document.querySelector('.settings__encryption_errors').textContent=""
}else{
document.querySelector('.settings__encryption_errors').textContent="Пароль содержит недопустимые символы"
}
}
})
decryption__key__button.addEventListener('click', ()=>{
if (settings__decryption__key.value){
let check = password_check(settings__decryption__key.value)
if (check){
saveDecryptionKeys(settings__decryption__key.value)
settings__decryption__key.value=""
document.querySelector('.settings__decryption_errors').textContent=""
}else{
document.querySelector('.settings__decryption_errors').textContent="Пароль содержит недопустимые символы"
}
}
})
//отправка данных для шифрования/дешифрования
send_button.addEventListener('click', async () => {
let method = localStorage.getItem('method') || 'encrypt';
let key;
if (switcher.checked === true && method === 'encrypt') {
key = await getEncryptionKey();
if (!key) {
if (!(settings_window.style.display === 'block')){
settings_window.style.display = 'block'
}
return;
}
} else if (switcher.checked === true && method === 'decrypt') {
key = await getDecryptionKey();
if (!key) {
if (!(settings_window.style.display === 'block')){
settings_window.style.display = 'block'
}
return;
}
} else {
key = key_input.value;
}
let inText = input_text.value
if (method === 'encrypt'){
output_text.value = await encryptWithPassword(key, inText)
output_warning.textContent="Результат"
} else{
const decryptedText = await decryptWithPassword(key, inText)
if (decryptedText === null) {
output_text.textContent = ""
output_warning.textContent = "Неверный ключ расшифрования"
} else {
output_text.value = decryptedText
output_warning.textContent = "Результат"
}
}
})
//сохранение паролей в локальном хранилище
async function saveEncryptionKeys(encryptionKey) {
const { encryptedText, iv } = await encryptText(encryptionKey);
localStorage.setItem('encryptionKey', encryptedText);
localStorage.setItem('encryptionIV', iv);
}
async function saveDecryptionKeys(decryptionKey) {
const { encryptedText, iv } = await encryptText(decryptionKey);
localStorage.setItem('decryptionKey', encryptedText);
localStorage.setItem('decryptionIV', iv);
}
//получение паролей из локального хранилища
async function getEncryptionKey() {
const encryptedText = localStorage.getItem('encryptionKey')
const iv = localStorage.getItem('encryptionIV')
if (encryptedText && iv) {
try {
return await decryptText(encryptedText, iv)
} catch (e) {
console.error("Ошибка расшифровки ключа:", e)
return false;
}
}
return false;
}
async function getDecryptionKey() {
const encryptedText = localStorage.getItem('decryptionKey');
const iv = localStorage.getItem('decryptionIV');
if (encryptedText && iv) {
try {
return await decryptText(encryptedText, iv);
} catch (e) {
console.error("Ошибка расшифровки ключа:", e);
return false;
}
}
return false;
}
//функция получения уникального ключа
async function deriveKeyFromPassword(password, salt = null) {
const encoder = new TextEncoder();
salt = salt || crypto.getRandomValues(new Uint8Array(16))
const keyMaterial = await crypto.subtle.importKey(
"raw", encoder.encode(password), "PBKDF2", false, ["deriveKey"]
);
const cryptoKey = await crypto.subtle.deriveKey(
{ name: "PBKDF2", salt: salt, iterations: 200000, hash: "SHA-256" },
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
return { cryptoKey, salt }
}
//функция проверки пароля
function password_check(password) {
return /^[A-Za-zА-Яа-яЁё0-9!@#$%^&*()_+{}\[\]:;"'<>?,./\\|~`-]+$/.test(password)
}
// функции шифрования и дешифрования на основе пароля
async function encryptWithPassword(password, plaintext) {
const encoder = new TextEncoder();
const { cryptoKey, salt } = await deriveKeyFromPassword(password);
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv },
cryptoKey,
encoder.encode(plaintext)
);
const encryptedData = new Uint8Array(salt.length + iv.length + ciphertext.byteLength);
encryptedData.set(salt);
encryptedData.set(iv, salt.length);
encryptedData.set(new Uint8Array(ciphertext), salt.length + iv.length);
return btoa(String.fromCharCode(...encryptedData)); // Конвертируем в Base64 строку
}
async function decryptWithPassword(password, encryptedText) {
try {
const encryptedData = Uint8Array.from(atob(encryptedText), c => c.charCodeAt(0));
const salt = encryptedData.slice(0, 16);
const iv = encryptedData.slice(16, 28);
const ciphertext = encryptedData.slice(28);
const { cryptoKey } = await deriveKeyFromPassword(password, salt);
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: iv },
cryptoKey,
ciphertext
);
const decoder = new TextDecoder();
return decoder.decode(decrypted);
} catch (error) {
console.error("Ошибка расшифровки: Неверный ключ или поврежденные данные.");
return null;
}
}
//уникальная строка для хранения паролей
async function generateFixedKey() {
const encoder = new TextEncoder();
const uniqueData = `${navigator.userAgent}-fixedSalt`; // создаём уникальную строку
const keyMaterial = await crypto.subtle.importKey(
"raw", encoder.encode(uniqueData), "PBKDF2", false, ["deriveKey"]
);
return await crypto.subtle.deriveKey(
{ name: "PBKDF2", salt: encoder.encode("fixedSalt"), iterations: 200000, hash: "SHA-256" },
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
}
//шифрование текста перед отправкой в хранилище
async function encryptText(text) {
const cryptoKey = await generateFixedKey();
const iv = crypto.getRandomValues(new Uint8Array(12));
const encryptedData = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv },
cryptoKey,
new TextEncoder().encode(text)
);
return {
encryptedText: btoa(String.fromCharCode(...new Uint8Array(encryptedData))),
iv: btoa(String.fromCharCode(...iv))
};
}
async function decryptText(encryptedText, iv) {
const cryptoKey = await generateFixedKey();
const decryptedData = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: Uint8Array.from(atob(iv), c => c.charCodeAt(0)) },
cryptoKey,
Uint8Array.from(atob(encryptedText), c => c.charCodeAt(0))
);
return new TextDecoder().decode(decryptedData);
}
Я хотел бы знать, как хранить постоянные пароли и иметь к ним доступ без дополнительных паролей.
Ответ или решение
Оценка безопасности расширения для шифрования сообщений
Создание расширения для Firefox, способного шифровать и дешифровать сообщения, является важной задачей в области обеспечения безопасности данных. Однако, учитывая природу работы с ключами шифрования и необходимостью их долговременного хранения, возникает ряд вопросов и рисков, которые важно рассмотреть до выпуска расширения. Давайте подробнее проанализируем текущую реализацию вашего расширения и предложим способы улучшения его безопасности.
1. Хранение постоянных ключей
Ваше решение хранить постоянные ключи в локальном хранилище (localStorage) является распространенной практикой, однако у нее есть свои недостатки. Основная уязвимость заключается в том, что любые данные, хранящиеся в localStorage, доступны для любых скриптов, выполняемых на странице, что может привести к компрометации данных в случае наличия вредоносного кода на веб-странице. Ниже представлены несколько рекомендаций по повышению безопасности хранения ключей:
-
Используйте шифрование для шифруемых данных: Хотя вы уже используете шифрование для хранения ключей, важно убедиться, что это шифрование является надежным. Желательно применять стандартные криптографические методы (например, используя библиотеку Web Crypto API, как у вас уже реализовано).
-
Сессионное хранилище: Если данные не необходимо хранить постоянно, рассмотрите возможность использования сессионного хранилища (sessionStorage), которое очищается при закрытии вкладки.
-
Совершенствование метода шифрования: Используйте дополнительные слои безопасности путем применения многофакторной аутентификации (MFA) для доступа к ключам. Также стоит рассмотреть использование соль и уникальные ключи для каждого сеанса.
2. Политика управления ключами
Нынешняя реализация вашего кода предполагает необходимость введения нового пароля для доступа к постоянным ключам. Это может быть неудобно для пользователей, поэтому важно разработать политику, которая обеспечит баланс между удобством и безопасностью:
-
Динамическое управление ключами: Вместо статического ввода паролей, создайте возможность генерировать временные или одноразовые ключи для шифрования. Это будет снижать риск компрометации.
-
Мониторинг доступа к ключам: Создайте логи доступа, чтобы отслеживать, кто и когда использовал ключи. Это может помочь в случае возможных атак.
3. Защита от атак
Ключевыми аспектами безопасности вашего расширения являются защита от атак типа "человек посередине" (MITM) и защита от перебора паролей:
-
Шифрование данных при передаче: Обеспечьте использование безопасных протоколов (например, HTTPS) для передачи данных. Это предотвратит возможность перехвата шифрованных сообщений.
-
Защита от перебора паролей: Рекомендуется установить ограничения на количество попыток ввода пароля или внедрить капчу после нескольких неверных попыток.
4. Открытость и прозрачность
Поскольку ваше расширение будет открытым, важно также предоставить пользователям и разработчикам детальную документацию по безопасности:
-
Документация: Обеспечьте доступ к технической документации, в которой будет описано, как работает шифрование и как обеспечивается безопасность ключей.
-
Обратная связь с пользователями: Активно собирайте отзывы и идеи от пользователей и разработчиков, чтобы улучшить характеристики безопасности.
Заключение
Разработка расширения для шифрования сообщений с акцентом на безопасность требует тщательной проработки всех аспектов, связанных с управлением ключами. Убедитесь, что вы учитываете все вышеупомянутые рекомендации и применяете лучшую практику работы с безопасностью. Это не только защитит ваши данные, но и создаст доверие среди пользователей в вашей разработке.
Помимо технического аспекта, важно обратить внимание на то, как вы будете информировать пользователей о том, как правильно управлять своими данными и хранить ключи. Ваше расширение имеет потенциал стать важным инструментом для повышения уровня безопасности онлайн-коммуникаций.