Information Security
October 10 2011

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

From Sandbox
В наши дни всё больше программ переводятся в так называемый «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-ориентированных системах. Исходный код — в прикреплённом архиве.
+5
29.5k 30
Comments 18
Similar
Top of the day