Шифрование строки запроса

192

Ранее вы уже видели несколько примеров, в которых система безопасности ASP.NET работала "за кулисами", защищая ваши данные. Например при использовании аутентификации пользователей было показано, как ASP.NET использует шифрование и хеш-коды для обеспечения постоянной защиты данных в cookie-наборах. Вы также узнали, как использовать некоторые инструменты для защиты состояния представления.

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

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

В следующем примере будет продемонстрирован простой способ укрепления безопасности за счет шифрования информации перед помещением ее в строку запроса. Здесь снова можно положиться на криптографические классы .NET. Фактически, будет использоваться DPAPI.

Оболочка для строки запроса

Начальная точка - это создание класса EncryptedQueryString. Этот класс должен принимать коллекцию строковой информации (такой как строка запроса) и давать возможность извлекать ее на другой странице. "За кулисами" класс EncryptedQueryString должен шифровать данные перед помещением их в строку запроса и незаметно расшифровывать где-то на пути.

Ниже показана начальная версия класса EncryptedQueryString:

using System;
using System.Text;
using System.Web;
using System.Web.Security;
using System.Security.Cryptography;

public class EncryptedQueryString :
    System.Collections.Specialized.StringDictionary
{
    public EncryptedQueryString()
    {
        // Здесь ничего не делать
    }

    public EncryptedQueryString(string encryptedData)
    {
        // Расшифровать информацию и добавить к словарю
    }

    public override string ToString()
    {
        // Зашифровать информацию и вернуть 
        // в виде строки шестнадцатеричных кодов
    }
}

Вы сразу должны заметить одну вещь, касающуюся класса EncryptedQueryString: он унаследован от класса StringDictionary, который представляет коллекцию строк, проиндексированную строками. За счет наследования от StringDictionary вы получаете возможность работать с EncryptedQueryString как с обычной строковой коллекцией. В результате можно добавлять информацию в EncryptedQueryString таким же образом, как это делается с коллекцией Request.QueryString.

Главное, что эта функциональность достается бесплатно, без необходимости писать какой-либо дополнительный код. Таким образом, благодаря этому рудиментарному классу, появляется возможность хранить коллекцию строк "имя-значение". Но как поместить эту информацию в строку запроса? Класс EncryptedQueryString предоставляет метод ToString(), который просматривает все данные коллекции и комбинирует их в единственную зашифрованную строку.

Первым делом, класс EncryptedQueryString должен скомбинировать отдельные значения коллекции в строку с разделителями, которую легко будет обратно превратить в коллекцию на целевой странице. В этом случае метод ToString() использует соглашения строки запроса, отделяя каждое значение от имени знаком равенства (=), а каждую пару "имя-значение" - знаком амперсанда (&). Однако чтобы это работало, следует удостовериться, что имена и значения каждого элемента коллекции не содержат специальные символы. Для решения этой проблемы ToString() использует метод HttpServerUtility.UrlEncode() для защиты строки перед ее объединением.

Ниже показана реализация метода ToString(), которая защищает и объединяет элементы коллекции в одну строку:

public override string ToString()
{
        StringBuilder Content = new StringBuilder();

        // Пройти no содержимому коллекции 
        // и построить типичную строку запроса
        foreach (string key in base.Keys)
        {
            Content.Append(HttpUtility.UrlEncode(key));
            Content.Append("=");
            Content.Append(HttpUtility.UrlEncode(base[key]));
            Content.Append("&");
        }

        // Удалить последний символ '&'
        Content.Remove(Content.Length - 1, 1);

        // Зашифровать содержимое, используя DPAPI
        byte[] EncryptedData = ProtectedData.Protect(
                    Encoding.UTF8.GetBytes(Content.ToString()),
                    null, DataProtectionScope.LocalMachine);

        // Преобразовать зашифрованный байтовый массив в допустимую строку URL.
        // Это может быть подходящим местом для проверки данных на предмет 
        // того, что они не превышают типичного размера в 4 Кбайт
        return HexEncoding.GetString(EncryptedData);
}

Здесь используется класс ProtectData для шифрования данных. Этот класс применяет DPAPI для шифрования информации и метод Protect() для возврата байтового массива, так что потребуется предпринять дополнительные шаги для преобразования байтового массива в строку, подходящую для строки запроса.

Один из подходов, который кажется разумным - применить статический метод Convert.ToBase64String(), который создает закодированную Base64 строку. К сожалению, строки Base64 могут включать символы, недопустимые для строки запроса (и именно - знак равенства). Хотя можно создать строку Base64, а потом подвергнуть ее URL-кодированию, это излишне усложнит стадию расшифровки. Проблема состоит в том, что метод ToBase64String() может также вставить серии строк, которые выглядят как URL-кодированные последовательности символов. Такие последовательности затем будут неправильно заменены в процессе декодирования.

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

public static class HexEncoding
{
    public static string GetString(byte[] data)
    {
        StringBuilder Results = new StringBuilder();
        foreach (byte b in data)
        {
            Results.Append(b.ToString("X2"));
        }

        return Results.ToString();
    }

    public static byte[] GetBytes(string data)
    {
        // GetString кодирует шестнадцатеричные числа двумя десятичными
        byte[] Results = new byte[data.Length / 2];
        for (int i = 0; i < data.Length; i += 2)
        {
            Results[i / 2] = Convert.ToByte(data.Substring(i, 2), 16);
        }

        return Results;
    }
}

Метод GetString() просто возвращает строку с шестнадцатеричными числами, созданными из байтового массива, в то время как GetBytes() преобразует строку с шестнадцатеричными цифрами обратно в байтовый массив для дальнейшей обработки. Это очень просто реализовать, поскольку используются существующие методы преобразования, инкапсулированные в класс Convert из библиотеки .NET. Эти методы затем легко применять.

Строку, возвращенную EncryptedQueryString.ToString(), можно поместить непосредственно в строку запроса, используя метод Response.Redirect().

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

public EncryptedQueryString(string encryptedData)
{
        // Расшифровать переданные данные с помощью DPAPI
        byte[] RawData = HexEncoding.GetBytes(encryptedData);
        byte[] ClearRawData = ProtectedData.Unprotect(
                    RawData, null, DataProtectionScope.LocalMachine);
        string StringData = Encoding.UTF8.GetString(ClearRawData);

        // Расшифровать данные и добавить содержимое
        int Index;
        string[] SplittedData = StringData.Split(new char[] { '&' });
        foreach (string SingleData in SplittedData)
        {
            Index = SingleData.IndexOf('=');
            base.Add(
                HttpUtility.UrlDecode(SingleData.Substring(0, Index)),
                HttpUtility.UrlDecode(SingleData.Substring(Index + 1))
            );
        }
}

Конструктор первым делом декодирует шестнадцатеричную информацию из переданной строки и воспользуется DPAPI для расшифровки информации, записанной в строке запроса. Затем он разбивает информацию на части и добавляет пары "имя-значение" в базовую коллекцию StringCollection.

Теперь имеется готовая инфраструктура для создания простой тестовой страницы и безопасной передачи ее от одной страницы к другой.

Создание тестовой страницы

Чтобы протестировать класс EncryptedQueryString, понадобятся две страницы - одна, устанавливающая строку запроса и перенаправляющая пользователя, и другая, извлекающая строку запроса. Первая страница содержит текстовое поле для ввода информации, как показано ниже:

<form id="form1" runat="server">
    <div>
        Введите здесь какие-нибудь данные:
        <asp:TextBox runat="server" ID="MyData" />
        <br />
        <br />
        <asp:Button ID="SendCommand" runat="server" Text="Отправить данные" OnClick="SendCommand_Click" />
    </div>
</form>

Когда пользователь щелкает на кнопке SendCommand, страница отправляет зашифрованную строку запроса принимающей странице следующим образом:

protected void SendCommand_Click(object sender, EventArgs e)
{
        EncryptedQueryString QueryString = new EncryptedQueryString();

        QueryString.Add("MyData", MyData.Text);
        QueryString.Add("MyTime", DateTime.Now.ToLongTimeString());
        QueryString.Add("MyDate", DateTime.Now.ToLongDateString());

        Response.Redirect("QueryStringRecipient.aspx?data=" + QueryString.ToString());
}

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

Исходная страница в действии

Целевая страница десериализует переданную через параметр строку в параметр строки запроса с помощью ранее созданного класса:

<form id="form1" runat="server">
	<div>
		<h2>Извлечение информации из строки запроса:</h2>
		<asp:Label runat="server" ID="QueryStringLabel" />
	</div>
</form>
protected void Page_Load(object sender, EventArgs e)
{
        // Десериализовать зашифрованную строку запроса
        EncryptedQueryString QueryString =
            new EncryptedQueryString(Request.QueryString["data"]);

        // Вывести информацию на экран
        StringBuilder Info = new StringBuilder();
        foreach (String key in QueryString.Keys)
        {
            Info.AppendFormat("{0} = {1}<br>", key, QueryString[key]);
        }
        QueryStringLabel.Text = Info.ToString();
}

Этот код добавляет информацию в метку на странице. Результат с ранее отправленной информацией показан на рисунке ниже:

Результат приема информации, зашифрованной в строке запроса
Пройди тесты
Лучший чат для C# программистов