Оцените безопасность расширения шифрования сообщений.

Вопрос или проблема

Я создал расширение для 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. Открытость и прозрачность

Поскольку ваше расширение будет открытым, важно также предоставить пользователям и разработчикам детальную документацию по безопасности:

  • Документация: Обеспечьте доступ к технической документации, в которой будет описано, как работает шифрование и как обеспечивается безопасность ключей.

  • Обратная связь с пользователями: Активно собирайте отзывы и идеи от пользователей и разработчиков, чтобы улучшить характеристики безопасности.

Заключение

Разработка расширения для шифрования сообщений с акцентом на безопасность требует тщательной проработки всех аспектов, связанных с управлением ключами. Убедитесь, что вы учитываете все вышеупомянутые рекомендации и применяете лучшую практику работы с безопасностью. Это не только защитит ваши данные, но и создаст доверие среди пользователей в вашей разработке.

Помимо технического аспекта, важно обратить внимание на то, как вы будете информировать пользователей о том, как правильно управлять своими данными и хранить ключи. Ваше расширение имеет потенциал стать важным инструментом для повышения уровня безопасности онлайн-коммуникаций.

Оцените материал
Добавить комментарий

Капча загружается...