В четвертой части серии учебных материалов, посвященных расширениям Intel Software Guard Extensions (Intel SGX), мы займемся созданием анклава и его интерфейса. Мы рассмотрим границы анклава, определенные в третьей части, и определим необходимые функции моста, рассмотрим влияние функций моста на объектную модель и создадим инфраструктуру проекта, необходимую для интеграции анклава в наше приложение. Вместо ECALL анклава мы пока используем заглушки; к полной интеграции анклава мы перейдем в пятой части этой серии.



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

Архитектура приложения


Перед проектированием интерфейса анклава нужно подумать об общей архитектуре приложения. Как мы обсуждали в первой части, анклавы реализуются в виде библиотек динамической компоновки (DLL-библиотеки в Windows* и общие библиотеки в Linux*) и должны компоноваться только с 100 % собственным кодом C.

При этом графический пользовательский интерфейс программы Tutorial Password Manager написан на C#. Используется смешанная сборка, написанная на C++/CLI, чтобы перейти от управляемого кода к неуправляемому, но хотя эта сборка содержит собственный код, она не состоит из собственного кода на 100 %, поэтому не может напрямую взаимодействовать с анклавом Intel SGX. Попытки внедрения недоверенных функций моста анклава в сборки C++/CLI приведут к невосстановимым ошибкам:

Command line error D8045: cannot compile C file ’Enclave_u.c’; with the /clr option

Это означает, что необходимо разместить функции недоверенного моста в отдельной DLL-библиотеке, состоящей полностью из собственного кода. В результате в нашем приложении будет по крайней мере три DLL-библиотеки: ядро C++/CLI, мост анклава и сам анклав. Эта структура изображена на рис. 1.


Рисунок 1. Компоненты смешанного приложения с анклавами.

Дальнейшие доработки


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

В нашем случае класс PasswordManagerCoreNative не следует обременять дополнительной задачей по созданию экземпляров анклавов. Этот класс должен лишь знать, поддерживает ли платформа расширения Intel SGX, чтобы выполнить соответствующую функцию.

В качестве примера в следующем фрагменте кода показан метод unlock():

int PasswordManagerCoreNative::vault_unlock(const LPWSTR wpassphrase)
{
	int rv;
	UINT16 size;

	char *mbpassphrase = tombs(wpassphrase, -1, &size);
	if (mbpassphrase == NULL) return NL_STATUS_ALLOC;

	rv= vault.unlock(mbpassphrase);

	SecureZeroMemory(mbpassphrase, size);
	delete[] mbpassphrase;

	return rv;
} 

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

int PasswordManagerCoreNative::vault_unlock(const LPWSTR wpassphrase)
{
	int rv;
	UINT16 size;

	char *mbpassphrase = tombs(wpassphrase, -1, &size);
	if (mbpassphrase == NULL) return NL_STATUS_ALLOC;

	// Call the enclave bridge function if we support Intel SGX
	if (supports_sgx()) rv = ew_unlock(mbpassphrase);
	else rv= vault.unlock(mbpassphrase);

	SecureZeroMemory(mbpassphrase, size);
	delete[] mbpassphrase;

	return rv;
}

Наша цель — в наибольшей степени освободить этот класс от работы с анклавом. Другие необходимые добавления для класса PasswordManagerCoreNative: поддержка флага Intel SGX и методы для задания и получения этого флага.

class PASSWORDMANAGERCORE_API PasswordManagerCoreNative
{
	int _supports_sgx;

	// Other class members ommitted for clarity

protected:
	void set_sgx_support(void) { _supports_sgx = 1; }
	int supports_sgx(void) { return _supports_sgx; }

Проектирование анклава


Общий план приложения готов, поэтому можно заняться проектированием анклава и его интерфейса. Для этого вернемся к схеме классов ядра приложения, которую мы впервые описали в третьей части — она показана на рис. 2. Объекты, находящиеся в анклаве, закрашены зеленым, а недоверенные компоненты — синим.


Рисунок 2. Схема классов в Tutorial Password Manager с Intel Software Guard Extensions.

Границу анклава пересекает только одно подключение: связь между объектом PasswordManagerCoreNative и объетом Vault. Это означает, что большинство наших ECALL будут просто оболочками методов классов в Vault. Также нужно добавить дополнительные ECALL для управления инфраструктурой анклава. Одно из затруднений при разработке анклава состоит в том, что ECALL, OCALL и функции моста должны быть собственным кодом C, а мы широко используем компоненты C++. После запуска анклава нам также понадобятся функции, заполняющие разрыв между C и C++ (объекты, конструкторы, перегрузки и другие).

Оболочки и функции моста будут находиться в собственной DLL-библиотеке, которую мы назовем EnclaveBridge.dll. Для ясности мы снабдим функции-оболочки префиксом «ew_» (enclave wrapper — оболочка анклава), а функции моста, образующие ECALL — префиксом «ve_» (vault enclave — анклав хранилища).

Вызовы из PasswordManagerCoreNative к соответствующему методу в Vault будут следовать по пути, показанному на рис. 3.


Рисунок 3. Путь выполнения функций моста и ECALL.

Метод в PasswordManagerCoreNative вызывает функцию-оболочку в EnclaveBridge.dll. Эта оболочка, в свою очередь, вызывает один или несколько ECALL, которые входят в анклав и вызывают соответствующий метод класса в объекте Vault. После завершения всех ECALL функция-оболочка возвращается в вызывающий метод в PasswordManagerCoreNative и предоставляет ему возвращенное значение.

Логистика анклава


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

Самое простое решение для Tutorial Password Manager — использовать глобальные переменные в DLL-библиотеке EnclaveBridge для размещения информации об анклаве. Такое решение сопряжено с ограничением: в анклаве одновременно может быть только один активный поток. Это целесообразное решение, поскольку производительность диспетчера паролей все равно не увеличится при использовании нескольких потоков для работы с хранилищем. Большинство действий управляется пользовательским интерфейсом, они не образуют существенную нагрузку на ЦП.

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

#define ENCLAVE_FILE _T("Enclave.signed.dll")

static sgx_enclave_id_t enclaveId = 0;
static sgx_launch_token_t launch_token = { 0 };
static int updated= 0;
static int launched = 0;
static sgx_status_t sgx_status= SGX_SUCCESS;

// Ensure the enclave has been created/launched.

static int get_enclave(sgx_enclave_id_t *eid)
{
	if (launched) return 1;
	else return create_enclave(eid);
}

static int create_enclave(sgx_enclave_id_t *eid)
{
	sgx_status = sgx_create_enclave(ENCLAVE_FILE, SGX_DEBUG_FLAG, &launch_token, &updated, &enclaveId, NULL);
	if (sgx_status == SGX_SUCCESS) {
		if ( eid != NULL ) *eid = enclaveId;
		launched = 1;
		return 1;
	}

	return 0;
}

Сначала каждая функция-оболочка вызывает функцию get_enclave(), которая проверяет, запущен ли анклав, по статической переменной. Если да, то эта функция (при необходимости) помещает идентификатор анклава в указатель eid. Это необязательный этап, поскольку идентификатор анклава также хранится в глобальной переменной enclaveID, и можно использовать ее напрямую.

Что произойдет, если анклав будет утрачен из-за сбоя электропитания или из-за ошибки, которая вызовет аварийное завершение? Для этого мы проверяем возвращаемое значение ECALL: оно указывает успешность или сбой самой операции ECALL, а не функции, вызываемой в анклаве.

sgx_status = ve_initialize(enclaveId, &vault_rv);

Возвращаемое значение функции, вызываемой в анклаве, если оно есть, передается через указатель, предоставляемый в качестве второго аргумента ECALL (эти прототипы функций автоматически создаются программой Edger8r). Следует всегда проверять возвращаемое значение ECALL. Любой результат, отличный от SGX_SUCCESS, указывает, что программе не удалось успешно войти в анклав, а запрошенная функция не была запущена. (Обратите внимание, что мы также определили sgx_status в качестве глобальной переменной. Это еще одно упрощение, обусловленное однопоточной архитектурой нашего приложения).

Мы добавим функцию, которая анализирует ошибку, возвращаемую функцией ECALL, и проверяет состояние анклава (утрачен, аварийный сбой):

static int lost_enclave()
{
	if (sgx_status == SGX_ERROR_ENCLAVE_LOST || sgx_status == SGX_ERROR_ENCLAVE_CRASHED) {
		launched = 0;
		return 1;
	}

	return 0;
}

Это исправимые ошибки. В верхних уровнях пока нет логики, способной справиться с этими условиями, но мы предоставляем ее в DLL-библиотеке EnclaveBridge для поддержки дальнейшего развития программы.

Также обратите внимание на отсутствие функции для уничтожения анклава. Пока в системе пользователя открыто приложение диспетчера паролей, в памяти существует анклав, даже если пользователь запер свое хранилище. Это не вполне правильный способ работы с анклавами. Анклавы потребляют ресурсы из далеко не безграничного пула даже при бездействии. Мы займемся этой проблемой в последующем выпуске этой серии, когда поговорим о запечатывании данных.

Язык Enclave Definition Language


Перед тем, как перейти к устройству анклава, поговорим немного о синтаксисе языка Enclave Definition Language (EDL). Функции моста анклава, и ECALL, и OCALLs, имеют прототипы в файле EDL со следующей общей структурой:

enclave {
	// Include files
	 
	// Import other edl files
	 
	// Data structure declarations to be used as parameters of the function prototypes in edl

	trusted {
	// Include file if any. It will be inserted in the trusted header file (enclave_t.h)
	 
	// Trusted function prototypes (ECALLs)
	 
	};
	 
	untrusted {
	// Include file if any. It will be inserted in the untrusted header file (enclave_u.h)
	 
	// Untrusted function prototypes (OCALLs)
	 
	};
};

Прототипы ECALL находятся в доверенной части, а OCALL — в недоверенной части. Синтаксис языка EDL схож с синтаксисом C, а прототипы функций EDL очень похожи на прототипы функций C, но не идентичны им. В частности, параметры функции моста и возвращаемые значения ограничены некоторыми фундаментальными типами данных, а EDL включает дополнительные ключевые слова и синтаксис для определения поведения анклава. В руководстве пользователя Intel Software Guard Extensions (Intel SGX) SDK исключительно подробно описан синтаксис EDL и приводится учебное руководство по созданию примера анклава. Мы не станем повторять все, что там написано, а просто обсудим элементы этого языка, относящиеся к нашему приложению.

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

Для параметров, передаваемых в качестве указателей, данные, на которые ссылается указатель, должны быть переданы в анклав и из анклава. Граничные процедуры, выполняющие эту передачу данных, должны «знать» две вещи:

  1. В каком направлении следует копировать данные: в функцию моста, из функции моста или в обе стороны?
  2. Каков размер буфера данных, на который ссылается указатель?

Направление указателя


При предоставлении функции параметра указателя необходимо указать направление с помощью ключевых слов в квадратных скобках: Соответственно [in], [out] или [in, out]. Значение этих ключевых слов указано в таблице 1.
Направление ECALL OCALL
in Буфер копируется из приложения в анклав. Изменения повлияют только на буфер внутри анклава. Буфер копируется из анклава в приложение. Изменения повлияют только на буфер вне анклава.
out Буфер будет выделен внутри анклава и инициализирован с нулевыми значениями. Он будет скопирован в исходный буфер при выходе ECALL. Буфер будет выделен вне анклава и инициализирован с нулевыми значениями. Этот недоверенный буфер будет скопирован в исходный буфер при выходе OCALL.
in, out Данные копируются туда и обратно. Так же, как в ECALL.
Таблица 1. Параметры направления указателей и их значения в ECALL и OCALL.

Обратите внимание, что направление указывается относительно вызываемой функции моста. Для функции ECALL [in] означает «копировать буфер в анклав», но для OCALL этот же параметр означает «копировать буфер в недоверенную функцию». (Также существует параметр user_check, который можно использовать вместо них, но он не относится к предмету нашего обсуждения. Сведения о его назначении и использовании см. в документации SDK.)

Размер буфера


Граничные процедуры вычисляют суммарный размер буфера в байтах следующим образом:

количество байт = element_size * element_count

По умолчанию для граничных процедур значение element_count равно 1, а element_size вычисляется на основе элемента, на который ссылается параметр указателя, например, для целочисленного указателя element_size будет таким:

sizeof(int)

Для одиночного элемента фиксированного типа данных, например int или float, не требуется предоставлять никакой дополнительной информации в прототипе функции EDL. Для указателя void необходимо задать размер элемента, иначе при компиляции возникнет ошибка. Для массивов, строк char и wchar_t и других типов, где длина буфера данных превышает один элемент, необходимо указать количество элементов в буфере, иначе будет скопирован только один элемент.

Добавьте параметр count или size (или оба) к ключевым словам в квадратных скобках. Им можно задать постоянное значение или один из параметров функции. В большинстве случаев функциональность count и size одинакова, но рекомендуется использовать их в правильном контексте. Строго говоря, size следует указывать только при передаче указателя void. В остальных случаях следует использовать count.

При передаче строки C и wstring (массива char или wchar_t с завершающим NULL) можно использовать параметр string или wstring вместо count или size. В этом случае граничные процедуры определят размер буфера, получив длину строки напрямую.

function([in, size=12] void *param);
function([in, count=len] char *buffer, uint32_t len);
function([in, string] char *cstr);

Обратите внимание, что можно использовать string или wstring только в случае, если задано направление [in] или [in, out]. Если задано только направление [out], строка еще не была создана, поэтому граничная процедура не может получить размер буфера. Если указать [out, string], при компиляции возникнет ошибка.

Оболочки и функции моста


Теперь можно определить оболочки и функции моста. Как было сказано выше, большинство наших ECALL будут просто оболочками методов классов в Vault. Определение класса для публичных членов-функций показано ниже:

class PASSWORDMANAGERCORE_API Vault
{
	// Non-public methods and members ommitted for brevity

public:
	Vault();
	~Vault();

	int initialize();
	int initialize(const char *header, UINT16 size);
	int load_vault(const char *edata);

	int get_header(unsigned char *header, UINT16 *size);
	int get_vault(unsigned char *edate, UINT32 *size);
	
	UINT32 get_db_size();

	void lock();
	int unlock(const char *password);

	int set_master_password(const char *password);
	int change_master_password(const char *oldpass, const char *newpass);

	int accounts_get_count(UINT32 *count);
	int accounts_get_info(UINT32 idx, char *mbname, UINT16 *mbname_len, char *mblogin, UINT16 *mblogin_len, char *mburl, UINT16 *mburl_len);

	int accounts_get_password(UINT32 idx, char **mbpass, UINT16 *mbpass_len);
	
	int accounts_set_info(UINT32 idx, const char *mbname, UINT16 mbname_len, const char *mblogin, UINT16 mblogin_len, const char *mburl, UINT16 mburl_len);
	int accounts_set_password(UINT32 idx, const char *mbpass, UINT16 mbpass_len);

	int accounts_generate_password(UINT16 length, UINT16 pwflags, char *cpass);

	int is_valid() { return _VST_IS_VALID(state); }
	int is_locked() { return ((state&_VST_LOCKED) == _VST_LOCKED) ? 1 : 0; }
};

В этом классе есть несколько проблемных функций. Некоторые из них очевидны: это, к примеру, конструктор, деструктор и перегрузки для initialize(). Это компоненты C++, которые нам нужно вызывать с помощью функций C. Некоторые проблемы не столь очевидны, поскольку они присущи устройству функций. Некоторые из этих проблемных методов были неправильно созданы нарочно, чтобы мы могли рассмотреть определенные проблемы в этом учебном руководстве, но другие методы были неправильно созданы без каких-либо далеко идущих целей, просто так получилось. Мы решим эти проблемы последовательно, представив и прототипы для функций оболочек, и прототипы EDL для процедур прокси/моста.

Конструктор и деструктор


В ветви кода без использования Intel SGX класс Vault является членом PasswordManagerCoreNative. Это невозможно сделать в ветви кода Intel SGX. Тем не менее, анклав может включать код C++, если сами по себе функции моста являются функциями C.

Поскольку мы ограничили анклав одним потоком, можно сделать класс Vault статическим глобальным объектом в анклаве. Это существенно упрощает код и исключает необходимость создания функций моста и логики для создания экземпляров.

Перегрузка метода initialize()


Существует два прототипа метода initialize():

  1. Метод без аргументов инициализирует объект Vault для нового хранилища паролей без содержимого. Это хранилище паролей, создаваемое пользователем впервые.
  2. Метод с двумя аргументами инициализирует объект Vault из заголовка файла хранилища. Это существующее хранилище паролей, которое пользователь открывает (а затем попытается отпереть).

Этот метод разделяется на две функции-оболочки:

ENCLAVEBRIDGE_API int ew_initialize();
ENCLAVEBRIDGE_API int ew_initialize_from_header(const char *header, uint16_t hsize);

Соответствующие функции ECALL определены так:

public int ve_initialize ();
public int ve_initialize_from_header ([in, count=len] unsigned char *header, uint16_t len);

get_header()


Этому методу присуща фундаментальная проблема. Вот прототип:

int get_header(unsigned char *header, uint16_t *size);

Эта функция выполняет следующие задачи:

  1. Она получает блок заголовка для файла хранилища и помещает его в буфер, на который указывает заголовок. Вызывающий метод должен выделить достаточно памяти для хранения этих данных.
  2. Если передать в параметре заголовка указатель NULL, то для uint16_t, на который ведет указатель, устанавливается размер блока заголовка, поэтому вызывающий метод знает, сколько памяти нужно выделить.

Это достаточно распространенная методика сжатия в некоторых программистских сообществах, но для анклавов здесь образуется проблема: при передаче указателя на ECALL или OCALL граничные функции копируют данные, на которые ссылается указатель, в анклав или из него (или в обоих направлениях). Этим граничным функциям необходим размер буфера данных, чтобы знать, сколько байт копировать. В первом случае используется действующий указатель с переменным размером, что не представляет затруднений, но во втором случае мы имеем указатель NULL и размер, равный нулю.

Можно было бы придумать такой прототип EDL для функции ECALL, при котором все это заработало бы, но обычно ясность важнее краткости. Поэтому лучше разделить код на две функции ECALL:

public int ve_get_header_size ([out] uint16_t *sz);
public int ve_get_header ([out, count=len] unsigned char *header, uint16_t len);

Функция-оболочка анклава обеспечит необходимую логику, чтобы нам не потребовалось изменять другие классы:

ENCLAVEBRIDGE_API int ew_get_header(unsigned char *header, uint16_t *size)
{
	int vault_rv;

	if (!get_enclave(NULL)) return NL_STATUS_SGXERROR;

	if ( header == NULL ) sgx_status = ve_get_header_size(enclaveId, &vault_rv, size);
	else sgx_status = ve_get_header(enclaveId, &vault_rv, header, *size);

	RETURN_SGXERROR_OR(vault_rv);
}

accounts_get_info()


Этот метод работает аналогично get_header(): передает указатель NULL и возвращает размер объекта в соответствующем параметре. Впрочем, этот метод не отличается изяществом и удобством из-за множества аргументов параметров. Лучше разделить его на две функции-оболочки:

ENCLAVEBRIDGE_API int ew_accounts_get_info_sizes(uint32_t idx, uint16_t *mbname_sz, uint16_t *mblogin_sz, uint16_t *mburl_sz);
ENCLAVEBRIDGE_API int ew_accounts_get_info(uint32_t idx, char *mbname, uint16_t mbname_sz, char *mblogin, uint16_t mblogin_sz, char *mburl, uint16_t mburl_sz);

И две соответствующих функции ECALL:

public int ve_accounts_get_info_sizes (uint32_t idx, [out] uint16_t *mbname_sz, [out] uint16_t *mblogin_sz, [out] uint16_t *mburl_sz);
public int ve_accounts_get_info (uint32_t idx, 
	[out, count=mbname_sz] char *mbname, uint16_t mbname_sz, 
	[out, count=mblogin_sz] char *mblogin, uint16_t mblogin_sz,
	[out, count=mburl_sz] char *mburl, uint16_t mburl_sz
);

accounts_get_password()


Это самый проблемный код во всем приложении. Вот прототип:

int accounts_get_password(UINT32 idx, char **mbpass, UINT16 *mbpass_len);

Первое, что бросается в глаза: он передает указатель на указатель в mbpass. Этот метод выделяет память.

Ясно, что это не лучшая идея. Никакой другой метод в классе Vault не выделяет память, поэтому он внутренне несогласован, а API нарушает соглашение, поскольку не предоставляет метод для высвобождения памяти от имени вызывающего. При этом также возникает проблема, свойственная только анклавам: анклав не может выделять память в недоверенном пространстве.

Это можно обрабатывать в функции-оболочке. Она могла бы выделить память, а затем образовать ECALL, и все это было бы прозрачно для вызывающего, но несмотря на это мы вынуждены изменить метод в классе Vault, поэтому достаточно просто исправить все как нужно и внести соответствующие изменения в класс PasswordManagerCoreNative. Вызывающий метод должен получить две функции: одну для получения длины пароля, другую — для получения пароля, как в двух предыдущих примерах. Класс PasswordManagerCoreNative должен отвечать за выделение памяти, а не какая-либо из этих функций (следует изменить и ветвь кода без использования Intel SGX).

ENCLAVEBRIDGE_API int ew_accounts_get_password_size(uint32_t idx, uint16_t *len); 
ENCLAVEBRIDGE_API int ew_accounts_get_password(uint32_t idx, char *mbpass, uint16_t len);

Теперь определение EDL должно выглядеть вполне привычно:

public int ve_accounts_get_password_size (uint32_t idx, [out] uint16_t *mbpass_sz); 
public int ve_accounts_get_password (uint32_t idx, [out, count=mbpass_sz] char *mbpass, uint16_t mbpass_sz);

load_vault()


Проблема с load_vault() не вполне очевидна. Прототип достаточно прост и может показаться совершенно безобидным:

int load_vault(const char *edata);

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

А проблема здесь состоит в том, что граничные функции анклава не имеют этой информации. Необходимо явным образом передавать длину в ECALL, чтобы граничная функция знала, сколько байт копировать из входящего буфера во внутренний буфер анклава, но информация о размере хранится внутри анклава. Она недоступна для граничной функции.

Прототип функции-оболочки может воспроизводить прототип метода класса:

ENCLAVEBRIDGE_API int ew_load_vault(const unsigned char *edata);

При этом функция ECALL должна передавать размер заголовка в качестве параметра, чтобы можно было его использовать для определения размера буфера входящих данных в файле EDL:

public int ve_load_vault ([in, count=len] unsigned char *edata, uint32_t len)

Чтобы все это было прозрачно для вызывающего метода, в функцию-оболочку будет добавлена дополнительная логика. Она будет отвечать за получение размера хранилища из анклава и за передачу этого размера в качестве параметра в функцию ECALL.

ENCLAVEBRIDGE_API int ew_load_vault(const unsigned char *edata)
{
	int vault_rv;
	uint32_t dbsize;

	if (!get_enclave(NULL)) return NL_STATUS_SGXERROR;

	// We need to get the size of the password database before entering the enclave
	// to send the encrypted blob.

	sgx_status = ve_get_db_size(enclaveId, &dbsize);
	if (sgx_status == SGX_SUCCESS) {
		// Now we can send the encrypted vault data across.

		sgx_status = ve_load_vault(enclaveId, &vault_rv, (unsigned char *) edata, dbsize);
	}

	RETURN_SGXERROR_OR(vault_rv);
}

Несколько слов о Юникоде


В третьей части мы упомянули, что класс PasswordManagerCoreNative также занимается преобразованием строк между форматами wchar_t и char. Зачем вообще это делать, ведь анклавы поддерживают тип данных wchar_t?

Это решение направлено на снижение ресурсоемкости нашего приложения. В Windows тип данных wchar_t является собственной кодировкой для API-интерфейсов Win32, он сохраняет символы в кодировке UTF-16. В кодировке UTF-16 каждый символ занимает 16 бит: такое решение принято для поддержки символов, отличных от ASCII, в частности, для языков, не использующих латинский алфавит или содержащих множество символов. Проблема с кодировкой UTF-16 состоит в том, что любой символ всегда имеет длину 16 бит, даже если это обычный текст ASCII.

Вряд ли целесообразно хранить вдвое больше данных на диске и внутри анклава, поскольку информация учетных записей пользователя обычно представляет собой обычный текст ASCII. Отметим также снижение производительности, обусловленное необходимостью копировать и зашифровывать больше данных. Поэтому в Tutorial Password Manager все строки, поступающие из .NET, преобразуются в кодировку UTF-8. UTF-8 — это кодировка переменной длины, в которой для представления каждого символа используется от одного до четырех байт длиной по 8 бит каждый. Эта кодировка обратно совместима с ASCII и отличается большей компактностью по сравнению с UTF-16 для обычного текста ASCII. Существуют ситуации, когда использование UTF-8 приведет к образованию более длинных строк, чем при использовании UTF-16, но поскольку мы создаем не коммерческую программу, а учебный диспетчер пароля, то примиримся с этим.

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

Пример кода


Как уже было сказано выше, в этой части предоставляется пример кода для загрузки. Прилагаемый архив включает исходный код DLL-библиотеки моста Tutorial Password Manager и DLL-библиотеки анклава. Функции анклава пока представляют собой просто заглушки, они будут заполнены в пятой части.

В дальнейших выпусках


В пятой части этого учебного руководства мы завершим создание анклава, для чего перенесем классы Crypto, DRNG и Vault в анклав и соединим их с функциями ECALL. Следите за новостями!
Поделиться с друзьями
-->

Комментарии (0)