Шифрование/дешифрование данных на стороне клиента в web-ориентированных системах

В наши дни всё больше программ переводятся в так называемый «web-ориентированный» вид, то есть используется принцип клиент-сервер, что позволяет хранить данные удалённо и получать к ним доступ через тонкий клиент (браузер).
Одновременно с удобством использования остро встаёт вопрос о защищённости этих данных. Конфиденциальная информация может стать доступна другим людям несколькими путями. Во-первых, к пользователю могут быть применены физические меры для выпытывания. Во-вторых, при передаче данные могут быть перехвачены различными снифферами. И, в-третьих, на сервер могут быть произведены хакерские атаки, что позволит злоумышленникам похитить информацию, либо недобросовестный администратор сервера воспользуется ею в личных целях.

Задача


Некоторое время назад у меня возникла задача разработать прототип программы шифрования/дешифрования данных на стороне клиента в web-ориентированных системах.

То есть, было необходимо разработать программу, которая позволит не просто хранить данные на сервере, но и предоставит возможность работы с ними через web-интерфейс и при этом обеспечит их бесполезность для злоумышленников в случае кражи, что достигается шифрованием/дешифрованием исключительно на стороне клиента.
Для типичного сценария использования возможна работа с тремя типами данных:
  • обычный текст, вводимый в поля ввода формы и хранящийся на сервере в базе данных в зашифрованном виде
  • файлы, которые хранятся на сервере в зашифрованном виде, и при необходимости пользователь может их скачать
  • изображения, хранящиеся на сервере как зашифрованные файлы, но при необходимости они расшифровываются на стороне клиента и вставляются на web-страницу как обычные картинки.


Реализация


Тот факт, что обработка данных должна производиться исключительно на стороне клиента, ограничивал выбор средств для реализации. На начальной стадии разработки была опробована связка «Java-апплет – Java-сервлет», но через какое-то время пришлось искать другой способ, потому что были трудности в отладке и передаче данных между апплетом и сервлетом.
Я остановился на использовании возможностей HTML5 и JavaScript-объекта «XmlHttpRequest Level 2» в частности, потому что они позволили с меньшими усилиями реализовать необходимый функционал.

Работа с текстом

Алгоритм шифрования:
  • вносим текст в поле формы на web-странице
  • шифруем текст с помощью функций Java Script
  • отправляем зашифрованный текст на сервер, где сохраняем в базу данных.

Обратный процесс:
  • получаем зашифрованные данные из базы данных с сервера
  • дешифруем их с помощью функций Java Script
  • выводим расшифрованный текст в нужное место на web-странице.


Работа с файлами

Процесс шифрования/дешифрования файлов происходит немного другим образом.
Алгоритм шифрования:
  • выбираем файл с компьютера пользователя
  • получаем содержимое файла в объект Java Script, используя XmlHttpRequest Level 2 и возможности HTML 5
  • шифруем его с помощью функций Java Script
  • отправляем зашифрованные данные на сервер, где сохраняем как файл.

Обратный процесс:
  • получаем содержимое зашифрованного файла с сервера
  • записываем его в объект Java Script, используя XmlHttpRequest Level 2 и возможности HTML 5
  • дешифруем с помощью функций Java Script
  • передаём расшифрованные данные в Java-апплет, чтобы дать пользователю возможность указать путь и имя для сохраняемого файла, т. к. на данный момент развития технологий в браузерах нельзя штатно вызывать диалог сохранения файла в произвольное место на компьютере пользователя, только в ограниченную «песочницу», что нам не подходит. Если по каким-либо причинам использование Java-апплета не подходит, эту часть можно заменить на Flash с аналогичным функционалом.


Работа с изображениями

Алгоритм шифрования:
  • выбираем файл с изображением с компьютера пользователя
  • записываем его содержимое в объект Java Script, используя XmlHttpRequest Level 2 и возможности HTML 5
  • кодируем в формат Base64
  • шифруем с помощью функций Java Script
  • отправляем зашифрованные данные на сервер, где сохраняем в файл.

Обратный процесс:
  • получаем содержимое зашифрованного изображения с сервера
  • записываем его в объект Java Script, используя XmlHttpRequest Level 2 и возможности HTML 5
  • дешифруем с помощью функций Java Script. На этом этапе получаем изображение, закодированное в формате Base64
  • вставляем содержимое в тег на web-странице (браузеры по умолчанию поддерживают вставку изображений в формате Base64).


Немного ключевого исходного кода для работы с файлами:
<script type="text/javascript">
/**
 * Функция загрузки файла на сервер с использованием
 * XMLHttpRequest level 2
 */
function upload(blobOrFile) {
	var xhr = new XMLHttpRequest();
	// открываем соединение методом POST, вказываем URL и true=асинхронный запрос
	xhr.open('POST', '/File/UploadFile/', true);
	// тип ответа - набор байт
	xhr.responseType = "arraybuffer";
	// устанавливаем заголовок ответа
	xhr.setRequestHeader("Content-type", "multipart/form-data");
	xhr.onload = function(e) {
		// ...
	};
	xhr.send(blobOrFile); // отправляем запрос
}

// добавляем слушателя события выбора файла
document.querySelector('input[type="file"]').addEventListener('change', function(e) {
	var file = this.files[0];	// первый выбранный в диалоге файл
        var reader = new FileReader();
	reader.onloadend = function(e) {
		var result = this.result;		// считанный поток байт
		var arr = new Int8Array(result);
		
		var i;	// счётчик байт
		var newResult = new ArrayBuffer(arr.byteLength);
		var newRes = new Int8Array(newResult);
		var keyForEncrypt = $('#keyForEncrypt').val();
		for(i = 0; i < arr.byteLength; i++) {
			// шифруем данные побайтово
			newRes[i] = arr[i] + parseInt(keyForEncrypt, 10);
		}
		// отправка данных
		upload(newRes.buffer);
	};
	// читаем файл как массив байт
	// работает в Chrome 11.0.696.68, не работает в FireFox 4.0.1
	reader.readAsArrayBuffer(file);
	
}, false);

/***
 * Функция получает файл в виде массива байт с сервера,
 * расшифровывает эти данные и передаёт их в Java апплет
 * для сохранения указанный пользователем файл
 */
function download() {
	var xhr = new XMLHttpRequest();
	xhr.open('POST', '/File/DownloadFile/', false);
	xhr.responseType = 'arraybuffer';
	xhr.onload = function(e) {
		var arr = new Int8Array(this.response); // this.response == arr.buffer
		// передаём данные в апплет
		upload(arr.buffer);
		var result = new Array(arr.byteLength);
		var keyForDecrypt = $('#keyForDecrypt').val();
		for(var i = 0; i < arr.byteLength; i++) {
			result[i] = arr[i] - parseInt(keyForDecrypt, 10);
		}
		saveFileByApplet(result);
	};
	xhr.send();
}

function saveFileByApplet(data) {
	// посылаем данные в апплет
	var cryptApplet = document.CryptApplet;
	cryptApplet.saveFile(data);
}

$(document).ready(function() {
	$('#saveFileButton').click(function() {
		download();
	});
});

</script>


Исходный код для работы с изображениями:
<script type="text/javascript">

var selectedFile = null;

//добавляем слушателя события выбора файла
document.querySelector('input[type="file"]').addEventListener('change', function(e) {
	selectedFile = this.files[0];// первый выбранный в диалоге файл
}, false);

$(document).ready(function() {
	$('#uploadPictureButton').click(function() {
            // если мы выбрали какой-нибудь файл
	    if(selectedFile != null) {
	    	var reader = new FileReader();
			reader.onload = function(e) {
				// считанная бинарная строка
				var result = this.result;
				// переводим в Base64
				var base64Result = Base64.encode(result);
				// шифруем строку XOR с ключом
				var keyForEncrypt = $('#keyForEncrypt').val();
				var encryptedData = XOREncrypt(base64Result, keyForEncrypt);
				// отправка данных
				uploadPicture(encryptedData);
			};
			// читаем файл как бинарную строку
			reader.readAsBinaryString(selectedFile);
		}
	});
	
	$('#downloadPictureButton').click(function() {
		downloadPicture();
	});
});

/**
 * Функция возвращает расширение файла с дописанными справа до 5 символов пробелами
 */
function buildExtension() {
	if(selectedFile != null) {
		var ext = "";
		var fullName = selectedFile.name;
		for(var i = fullName.length - 1; i >= 0; i--) {
			if(fullName[i] == '.')
				break;
			else
				ext += fullName[i];
		}

		ext = ext.split('').reverse().join('');
		
		if(ext.length < 5) {
			for(var i = 0; i <= 5-ext.length; i++) {
				ext += " ";
			}
		}
		
		return ext;
	}
}


/**
 * Функция загрузки изображения на сервер с использованием
 * XMLHttpRequest level 2
 */
function uploadPicture(picture) {
	var xhr = new XMLHttpRequest();
	// открываем соединение методом POST, вказываем URL и true=асинхронный запрос
	xhr.open('POST', '/Picture/UploadPicture/', true);
	xhr.onload = function(e) {
		// ...
	};

	var sentData = "jpg  " + picture;
	
	// отправляем запрос	
	xhr.send(sentData);
}


/***
 * Функция получает файл в виде строки, закодированной XOR, с сервера,
 * расшифровывает эти данные и вставляет в аттрибут SRC тега IMG
 */
function downloadPicture() {
	var xhr = new XMLHttpRequest();
	xhr.open('POST', '/Picture/DownloadPicture/', true);
	xhr.onload = function(e) {
		// ответ - строка Base64, закодированная XOR
		var result = this.response;

		// первые 5 символов - расширение
		var ext = rtrim(result.substr(0, 5));
		// строка в Base64
		
		var base64Data = result.substr(5);

		var keyForDecrypt = $('#keyForDecrypt').val();
		var decryptedData = XORDecrypt(base64Data, keyForDecrypt);

		// устанавливаем MIME тип в зависимости от расширения
		var mime = "";
		switch (ext) {
			case "jpeg" :
			case "jpg" :
			case "jpe" :
				mime = "image/jpeg";
				break;

			case "gif" :
				mime = "image/gif";
				break;

			case "png" :
				mime = "image/png";
				break;
				
			default:
				mime = "image/jpeg";
				break;
		}
		$('#pict').attr('src', "data:" + mime + ";base64," + decryptedData);
	};

	xhr.send();
}


/**
 * Аналог PHP-функции rtrim - удаление пробелов справа
 */
function rtrim ( str, charlist ) {
	charlist = !charlist ? ' \\s\u00A0' : (charlist + '').replace(/([\[\]\(\)\.\?\/\*\{\}\+\$\^\:])/g, '\\$1');
    var re = new RegExp('[' + charlist + ']+$', 'g');
    return (str + '').replace(re, '');
}

</script>


Я не стал здесь приводить реализацию функций XOREncrypt, XORDecrypt и класса Base64, чтобы не загромождать и без того длинный листинг. Их можно посмотреть в прилагаемом архиве с исходным кодом.

Код Java-апплета для вывода диалога сохранения файла.
package ExtPackage;

import java.applet.*;
import java.io.*;
import java.io.FileOutputStream;
import javax.swing.JFileChooser;
import javax.swing.*;

public class CryptApplet extends Applet{

    public void saveFile(byte[] data) throws FileNotFoundException, IOException {
        // вызываем диалог сохранения файла
        final JFileChooser fc = new JFileChooser();
        fc.showSaveDialog(CryptApplet.this);
        
        // путь и имя файла, указанные пользователем
        File file = fc.getSelectedFile();

        OutputStream out = new FileOutputStream(file);

        // записываем пришедшие данные в файл
        out.write(data);
        out.flush();
        out.close();
    }
}


Резюме


Надеюсь, полученные мной результаты сэкономят немного времени тем, кто столкнётся с задачей шифрования/дешифрования данных на стороне клиента в web-ориентированных системах. Исходный код — в прикреплённом архиве.
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 16
  • +3
    Я чего-то не понимаю, или вместо всего этого можно было бы использовать стандартный HTTPS???
    • +1
      И, в-третьих, на сервер могут быть произведены хакерские атаки, что позволит злоумышленникам похитить информацию, либо недобросовестный администратор сервера воспользуется ею в личных целях
      Это тоже сомнительная выгода перед HTTPS, т.к. очень сложно доказать широким массам пользователей, что ваша реализация JS-шифрования не содержит уязвимостей и бэкдоров.
      • +1
        Когда вся обработка информации происходит на сервере, пользователи точно не уверены, что там данные хранятся действительно в зашифрованном виде. Если же обработка происходит у них, то это можно проверить, изучив JS-код. Согласен, обычный пользователь вряд ли знаком с этим. Но люди, которые будут использовать это на серьёзном уровне (работа с информацией, защищённой авторским правом, например), думаю, могут себе позволить найти людей, которые проверят отсутствие всякой нечисти в коде. Плюс, если нет доверия стандартным алгоритмам шифрования, можно подключить свои. Насчёт HTTPS — никто не запрещает использовать его в связке с предложенным способом.
        • +1
          js можно подменить, взломав сайт, или стырив https-сертификат. но в этом случае это тупо вопрос доверия к сайту, ничего более.

          а вот если https не будет, то ваш скрипт злые дяди смогут легко подменить на прозрачной проксе или в открытой wifi-сети.
    • 0
      Эээ, а в сторону нормальных алгоритмов шифрования смотреть не пробовали?
      Есть например Stanford Javascript Crypto Library. Работает достаточно шустро.
      • 0
        Упор был на том, чтобы обработка данных производилась на стороне клиента. Реализация конкретных методов шифрования (будь то банальное XOR-шифрование или использование методов указанной Вами библиотеки) в данном случае не столь важна, потому что их легко подключать и отключать при необходимости простой заменой функции шифрования в коде.
      • +1
        Э… А в чем смысл этих манипуляций? Хакер будет настолько ленив, что даже не станет смотреть в Javascript-код?
        • 0
          Конечно, не лень. Но, как минимум, ему нужно будет узнать ключ, используемый для шифрования. Плюс, пользователю можно предложить возможность выбора алгоритма из нескольких. То есть, хакеру нужно будет узнать не только ключ, но и метод шифрования, который использовал конкретный пользователь для конкретной информации.
          • +2
            Ключ он найдет на странице в открытом виде. Алгоритм шифрования он узнает, читая Javascript-код. Потратит он на это сильно меньше времени, чем потрачено на написание шифровального кода ))))
            • 0
              Ключ он узнает с помощью сниффера клавиатуры или как?
              • +1
                А, так ключ вводит пользователь. Да, ломать будет чуток сложнее. Сначала хакер скачает файл (который, видимо, ничем, кроме пароля, не защищен?). Потом в зависимости от типа файла будет искать текст для дешифрования. Например, для JPEG картинок он поищет строку JFIF.

                Кстати, а как пользователь узнает, что файл расшифрован правильно?

                В принципе, схема имеет право на жизнь, но она сильно слабее, скажем, клиентских сертификатов.
                • 0
                  Да, файл хранится на сервере зашифрованным с помощью ключа. Если выбирать достаточно сложные алгоритмы, то и взломать его будет сложно.
                  Насчёт правильности расшифровки — думаю, пользователь, шифруя данные и запоминая ключ, знает, как они выглядят изначально. Честно говоря, я не уверен, что какая-либо существующая технология/схема защиты информации может обеспечить 100% надёжность. Но описанная мной схема как отдельная часть системы защиты информации вполне жизнеспособна.
        • 0
          Жесть-то какая, просто слов нет.

          особенно:
          кодируем в формат Base64
          шифруем с помощью функций Java Script

          какой смысл шифровать почти в полтора раза больше данных?

          Да и вообще все эти криптографии на базе js — это так, поиграться.
          Нужно шифрование — используйте https и не выпендривайтесь.
          Пользователь ведь никогда не будет уверен в том, что js, загруженный как-бы с вашего сайта будет что-то расшифровывать, а не просто сольёт ваши пароли/куки/данные нехорошим людям.
          • 0
            Это, конечно интересно. Но если применять это на практике остаётся вопрос о хранении ключа (уже не помню какой длины должен быть ключ того же RSA, но она точно больше, чем может запомнить человек). Допустим, вы решили не использовать https, и генерировать какой-нибудь ключ рандомно. Юзать локалсторадж (кукисы и пр.) не вариант по двум причинам:
            1. Кулхацкер может включить ваш компьютер и посмотреть ключ (человеческий фактор)
            2. На другой машине вы не сможете это расшифровать, не зная своего ключа. Это касается и обычной переустановки браузера.
            Можно использовать какой-нибудь удаленный сервер аутентификации, который по запросу отдаст вам ваш ключ, но здесь придется юзать тот же https.

            И да, нет такого языка, как Java Script. Есть Javascript одним словом.

            Вы случайно в институте преподом не работаете (исходя из «важности» заголовка)?
            • 0
              Хранение ключа планировалось только в голове у пользователя, как обычный пароль. Думаю, ничто не мешает налету делать из легко запоминаемого пароля криптостойкий ключ нужной длины. И, как я писал выше, никто не отменяет связку описанной мной схемы и htpps.
              Про «Javascript» запомню, спасибо. Преподом не работаю, хотя название, согласен, звучит академически.
              • 0
                Гляньте вот этот пост, может Cryptico.js пригодится.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.