Использование шифрования с базами данных SQLAdobe AIR 1.5 и более новых версий Все приложения Adobe AIR используют один и тот же механизм локальной базы данных. Следовательно, любое приложение AIR может подключаться к незашифрованному файлу данных для чтения и записи данных в него. Начиная с Adobe AIR версии 1.5 это приложение обеспечивает возможность создания зашифрованных файлов базы данных и подключения к ним. При использовании зашифрованной базы данных для подключения к ней приложение должно предоставить правильный ключ шифрования. Если предоставлен неверный ключ шифрования (или не предоставлен никакой ключ), приложение не подключится к базе данных. А значит приложение не сможет считывать данные из базы данных, а также записывать в нее данные или изменять их. Для работы с зашифрованной базой данных должна быть создана такая база. Если зашифрованная база уже существует, можно установить соединение с ней. Также можно изменить ключ шифрования зашифрованной базы данных. За исключением процедур создания и подключения к зашифрованной базе данных, порядок работы с такой базой сходен с порядком работы с незашифрованной базой данных. В частности, выполнение инструкций SQL происходит точно также, независимо от того, зашифрована ли база данных или нет. Использование зашифрованной базы данныхШифрование всегда полезно в тех случаях, когда требуется ограничить доступ к информации, хранимой в базе данных. Функция шифрования базы данных в Adobe AIR может использоваться в следующих целях. Далее приведены несколько примеров, когда может потребоваться использование зашифрованной базы данных:
Понимание причины, зачем требуется использовать зашифрованную базу данных, помогает решить, как спроектировать приложение. В частности, это может повлиять на то, как приложение создает, получает и хранит ключ шифрования для базы данных. Дополнительные сведения об этих аспектах см. в разделе «Предпосылки для использования зашифрованной базы данных». Помимо зашифрованных баз данных существует альтернативный механизм конфиденциального хранения важных данных в зашифрованном локальном хранилище. Зашифрованное локальное хранилище позволяет хранить значение ByteArray с помощью ключа строки. Только то приложение AIR, которое хранит значение, может обращаться к нему, и только на том компьютере, где хранится это значение. Благодаря зашифрованному локальному хранилищу нет необходимости создавать собственный ключ шифрования. По этой причине зашифрованное локальное хранилище лучше всего подходит для удобного хранения отдельного значения или набора значений, которые легко могут быть зашифрованными в ByteArray. Зашифрованная база данных лучше всего подходит для больших наборов данных, где желательны структурированное хранение данных и выполнение запросов. Дополнительные сведения о зашифрованной локальной системе хранения данных см. в разделе «Зашифрованная локальная система хранения данных». Создание зашифрованной базы данныхДля использования зашифрованной базы данных файл базы данных должен быть зашифрован при создании. Если база данных создана как незашифрованная, ее нельзя будет зашифровать в будущем. Точно так же нельзя будет расшифровать зашифрованную базу данных. Если необходимо, можно изменить ключ шифрования зашифрованной базы данных. Дополнительные сведения см. в разделе Изменение ключа шифрования для базы данных. Если имеется существующая база данных, которая не зашифрована, и требуется использовать шифрование базы данных, можно создать новую зашифрованную базу данных и скопировать существующую структуру таблиц и данных в эту новую базу данных. Создание зашифрованной базы данных практически полностью совпадает с процедурой создания незашифрованной базы данных, как это описано в разделе Создание базы данных. Вначале создается экземпляр класса SQLConnection, который воспроизводит подключение к базе данных. База данных создается путем вызова метода open() или метода openAsync() объекта SQLConnection, указывающих базе данных местонахождение файла, который еще не существует. Единственным отличием при создании зашифрованной базы данных является то, что указывается значение для параметра encryptionKey (пятый параметр метода open() и шестой параметр метода openAsync()). Допустимым значением параметра encryptionKey является объект ByteArray, содержащий ровно 16 байт. Следующие примеры демонстрируют создание зашифрованной базы данных. Для простоты в этих примерах ключ шифрования жестко закодирован в коде приложения. Однако настоятельно не рекомендуется применять эту технологию, поскольку она не безопасна. var conn:SQLConnection = new SQLConnection();
var encryptionKey:ByteArray = new ByteArray();
encryptionKey.writeUTFBytes("Some16ByteString"); // This technique is not secure!
// Create an encrypted database in asynchronous mode
conn.openAsync(dbFile, SQLMode.CREATE, null, false, 1024, encryptionKey);
// Create an encrypted database in synchronous mode
conn.open(dbFile, SQLMode.CREATE, false, 1024, encryptionKey);
В качестве примера, демонстрирующего рекомендуемый способ создания ключа шифрования, см. раздел Пример: создание и использование ключа шифрования. Подключение к зашифрованной базе данныхПодобно созданию зашифрованной базы данных, процедура открытия соединения с зашифрованной базой данных подобна процедуре открытия соединения с незашифрованной базой. Эта процедура подробно описана в разделе Подключение к базе данных. Метод open() используется для открытия соединения в синхронном режиме выполнения, а метод openAsync() — для открытия соединения в асинхронном режиме выполнения. Единственным отличием является то, что при открытии зашифрованной базы данных указывается допустимое значение для параметра encryptionKey (пятый параметр метода open() и шестой параметр метода openAsync()). Если указанный ключ шифрования не верен, происходит ошибка. Для метода open() вызывается исключение SQLError. Для метода openAsync() объект SQLConnection отправляет событие SQLErrorEvent, свойство error которого содержит объект SQLError. В любом случае в объекте SQLError, созданном исключением, в свойстве errorID содержится значение 3138. Этот идентификатор ошибки соответствует сообщению об ошибке «Открытый файл не является файлом базы данных». В следующем примере продемонстрировано открытие зашифрованной базы данных в асинхронном режиме выполнения. Для упрощения в этом примере ключ шифрования жестко закодирован в коде приложения. Однако настоятельно не рекомендуется применять эту технологию, поскольку она не безопасна. import flash.data.SQLConnection;
import flash.data.SQLMode;
import flash.events.SQLErrorEvent;
import flash.events.SQLEvent;
import flash.filesystem.File;
var conn:SQLConnection = new SQLConnection();
conn.addEventListener(SQLEvent.OPEN, openHandler);
conn.addEventListener(SQLErrorEvent.ERROR, errorHandler);
var dbFile:File = File.applicationStorageDirectory.resolvePath("DBSample.db");
var encryptionKey:ByteArray = new ByteArray();
encryptionKey.writeUTFBytes("Some16ByteString"); // This technique is not secure!
conn.openAsync(dbFile, SQLMode.UPDATE, null, false, 1024, encryptionKey);
function openHandler(event:SQLEvent):void
{
trace("the database opened successfully");
}
function errorHandler(event:SQLErrorEvent):void
{
if (event.error.errorID == 3138)
{
trace("Incorrect encryption key");
}
else
{
trace("Error message:", event.error.message);
trace("Details:", event.error.details);
}
}
В следующем примере продемонстрировано открытие зашифрованной базы данных в синхронном режиме выполнения. Для упрощения в этом примере ключ шифрования жестко закодирован в коде приложения. Однако настоятельно не рекомендуется применять эту технологию, поскольку она не безопасна. import flash.data.SQLConnection;
import flash.data.SQLMode;
import flash.filesystem.File;
var conn:SQLConnection = new SQLConnection();
var dbFile:File = File.applicationStorageDirectory.resolvePath("DBSample.db");
var encryptionKey:ByteArray = new ByteArray();
encryptionKey.writeUTFBytes("Some16ByteString"); // This technique is not secure!
try
{
conn.open(dbFile, SQLMode.UPDATE, false, 1024, encryptionKey);
trace("the database was created successfully");
}
catch (error:SQLError)
{
if (error.errorID == 3138)
{
trace("Incorrect encryption key");
}
else
{
trace("Error message:", error.message);
trace("Details:", error.details);
}
}
В качестве примера, демонстрирующего рекомендуемый способ создания ключа шифрования, см. раздел Пример: создание и использование ключа шифрования. Изменение ключа шифрования для базы данныхЕсли база данных зашифрована, можно изменить ключ для ее шифрования в последующем. Для изменения ключа шифрования базы данных прежде всего откройте соединение к этой базе данных, создав экземпляр объекта SQLConnection и вызвав для него методы open() или openAsync(). Как только соединение с базой данных будет открыто, вызовите метод reencrypt(), передав ему в качестве аргумента новый ключ шифрования. Так же, как и для большинства операций с базой данных, поведение метода reencrypt() зависит от того, будет ли соединение с базой данных использовать синхронный или асинхронный режимы выполнения. Если для соединения с базой данных используется метод open(), операция reencrypt() выполняется в синхронном режиме. Когда эта операция завершается, продолжается выполнение следующей строки программного кода: var newKey:ByteArray = new ByteArray(); // ... generate the new key and store it in newKey conn.reencrypt(newKey); Однако, если соединение с базой данных открыто с помощью метода openAsync(), операция reencrypt() выполняется в асинхронном режиме. Вызов reencrypt() начинает процесс перешифрования. Когда эта операция завершается, объект SQLConnection отправляет событие reencrypt. Для определения того, не завершилось ли перешифрование, используется процесс прослушивания событий: var newKey:ByteArray = new ByteArray();
// ... generate the new key and store it in newKey
conn.addEventListener(SQLEvent.REENCRYPT, reencryptHandler);
conn.reencrypt(newKey);
function reencryptHandler(event:SQLEvent):void
{
// save the fact that the key changed
}
Операция reencrypt() выполняется в собственной отдельной транзакции. Если операция прерывается или завершается со сбоем (например, если приложение закрывается до завершения операции), выполняется откат этой транзакции. В этом случае исходный ключ шифрования по-прежнему является ключом шифрования базы данных. Метод reencrypt() может использоваться для устранения шифрования из базы данных. Если передать значение null или ключ шифрования, который содержит не 16-байтовый ByteArray, в метод reencrypt(), произойдет ошибка. Предпосылки для использования зашифрованной базы данныхВ разделе Использование зашифрованной базы данных представлено несколько случаев, в которых может потребоваться использование зашифрованной базы данных. Очевидно, что использование сценариев в различных приложениях (включая эти и другие сценарии) предполагает различные требования к конфиденциальности. Способ, которым проектируется использование шифрования в приложении, играет важную роль в управлении тем, насколько конфиденциальной будет база данных. Например, если зашифрованная база данных используется для конфиденциального хранения личных данных, защищая их даже от других пользователей данного компьютера, тогда каждому пользователю базы данных требуется свой собственный ключ шифрования. Для обеспечения максимального уровня безопасности приложение может создавать ключ шифрования из введенного пользователем пароля. Создание ключа безопасности на основе пароля обеспечивает уровень защиты, при котором даже если один пользователь попытается выдать себя за другого, он все равно не сможет обратиться к чужим данным. Альтернативным уровнем безопасности является ситуация, когда требуется, чтобы файлы данных были доступны на чтение другим пользователям этого приложения, но не других приложений. В этом случае каждой установленной копии приложения требуется доступ к совместно используемому ключу шифрования. Можно спроектировать приложение, а в особенности технику создания ключа шифрования, в соответствии с уровнем безопасности, который требуется для данных приложения. В следующем списке представлены предпосылки для различных уровней конфиденциальности данных.
Далее изложены дополнительные соображения безопасности, которые важно учитывать при разработке приложения для работы с зашифрованной базой данных:
Пример: создание и использование ключа шифрованияЭтот пример демонстрирует одну из методик создания ключа шифрования. Приложение спроектировано для обеспечения максимального уровня конфиденциальности и безопасности данных пользователя. Одним из важнейших аспектов защиты конфиденциальных данных является требование к пользователю вводить пароль при каждом подключении приложения к базе данных. Следовательно, как показано в этом примере, приложение, которому требуется этот уровень конфиденциальности, никогда не должно обычным способом сохранять ключ шифрования базы данных. Приложение состоит из двух частей: класса ActionScript, создающего ключ шифрования (класс EncryptionKeyGenerator), и простейшего пользовательского интерфейса, демонстрирующего, как применять этот класс. Полный исходный код см. в разделе Комплексный пример программного кода, создающего и использующего ключ шифрования. Использование класса EncryptionKeyGenerator для получения защищенного ключа шифрованияДля использования класса EncryptionKeyGenerator важно иметь четкое представление о принципах его работы. Дополнительные сведения о способах создания классом ключа шифрования см. в разделе «Знакомство с классом EncryptionKeyGenerator». Выполните следующие шаги, чтобы использовать класс EncryptionKeyGenerator в своем приложении:
Комплексный пример программного кода, создающего и использующего ключ шифрованияДалее приведен полный код демонстрационного приложения «Создание и использование ключа шифрования». Этот код состоит из двух частей. В примере используется класс EncryptionKeyGenerator для создания ключа шифрования из пароля. Класс EncryptionKeyGenerator включен в проект корневой библиотеки ActionScript 3.0 (as3corelib) с открытым кодом. Можно загрузить пакет as3corelib, включая исходный код и документацию к нему. SWC или файлы с исходным кодом можно загрузить со страницы проекта. Пример FlexMXML-файл приложения содержит исходный код для простейшего приложения, создающего или открывающего соединение с зашифрованной базой данных: <?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="init();">
<mx:Script>
<![CDATA[
import com.adobe.air.crypto.EncryptionKeyGenerator;
private const dbFileName:String = "encryptedDatabase.db";
private var dbFile:File;
private var createNewDB:Boolean = true;
private var conn:SQLConnection;
// ------- Event handling -------
private function init():void
{
conn = new SQLConnection();
dbFile = File.applicationStorageDirectory.resolvePath(dbFileName);
if (dbFile.exists)
{
createNewDB = false;
instructions.text = "Enter your database password to open the encrypted database.";
openButton.label = "Open Database";
}
}
private function openConnection():void
{
var password:String = passwordInput.text;
var keyGenerator:EncryptionKeyGenerator = new EncryptionKeyGenerator();
if (password == null || password.length <= 0)
{
statusMsg.text = "Please specify a password.";
return;
}
if (!keyGenerator.validateStrongPassword(password))
{
statusMsg.text = "The password must be 8-32 characters long. It must contain at least one lowercase letter, at least one uppercase letter, and at least one number or symbol.";
return;
}
passwordInput.text = "";
passwordInput.enabled = false;
openButton.enabled = false;
var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(password);
conn.addEventListener(SQLEvent.OPEN, openHandler);
conn.addEventListener(SQLErrorEvent.ERROR, openError);
conn.openAsync(dbFile, SQLMode.CREATE, null, false, 1024, encryptionKey);
}
private function openHandler(event:SQLEvent):void
{
conn.removeEventListener(SQLEvent.OPEN, openHandler);
conn.removeEventListener(SQLErrorEvent.ERROR, openError);
statusMsg.setStyle("color", 0x009900);
if (createNewDB)
{
statusMsg.text = "The encrypted database was created successfully.";
}
else
{
statusMsg.text = "The encrypted database was opened successfully.";
}
}
private function openError(event:SQLErrorEvent):void
{
conn.removeEventListener(SQLEvent.OPEN, openHandler);
conn.removeEventListener(SQLErrorEvent.ERROR, openError);
if (!createNewDB && event.error.errorID == EncryptionKeyGenerator.ENCRYPTED_DB_PASSWORD_ERROR_ID)
{
statusMsg.text = "Incorrect password!";
}
else
{
statusMsg.text = "Error creating or opening database.";
}
}
]]>
</mx:Script>
<mx:Text id="instructions" text="Enter a password to create an encrypted database. The next time you open the application, you will need to re-enter the password to open the database again." width="75%" height="65"/>
<mx:HBox>
<mx:TextInput id="passwordInput" displayAsPassword="true"/>
<mx:Button id="openButton" label="Create Database" click="openConnection();"/>
</mx:HBox>
<mx:Text id="statusMsg" color="#990000" width="75%"/>
</mx:WindowedApplication>
Пример Flash ProfessionalFLA-файл приложения содержит исходный код для простейшего приложения, создающего или открывающего соединение с зашифрованной базой данных. FLA-файл содержит четыре компонента, размещаемые в рабочей области:
Код для приложения определяется в ключевом кадре кадра 1 основной временной шкалы. Далее показан этот код для приложения: import com.adobe.air.crypto.EncryptionKeyGenerator;
const dbFileName:String = "encryptedDatabase.db";
var dbFile:File;
var createNewDB:Boolean = true;
var conn:SQLConnection;
init();
// ------- Event handling -------
function init():void
{
passwordInput.displayAsPassword = true;
openButton.addEventListener(MouseEvent.CLICK, openConnection);
statusMsg.setStyle("textFormat", new TextFormat(null, null, 0x990000));
conn = new SQLConnection();
dbFile = File.applicationStorageDirectory.resolvePath(dbFileName);
if (dbFile.exists)
{
createNewDB = false;
instructions.text = "Enter your database password to open the encrypted database.";
openButton.label = "Open Database";
}
else
{
instructions.text = "Enter a password to create an encrypted database. The next time you open the application, you will need to re-enter the password to open the database again.";
openButton.label = "Create Database";
}
}
function openConnection(event:MouseEvent):void
{
var keyGenerator:EncryptionKeyGenerator = new EncryptionKeyGenerator();
var password:String = passwordInput.text;
if (password == null || password.length <= 0)
{
statusMsg.text = "Please specify a password.";
return;
}
if (!keyGenerator.validateStrongPassword(password))
{
statusMsg.text = "The password must be 8-32 characters long. It must contain at least one lowercase letter, at least one uppercase letter, and at least one number or symbol.";
return;
}
passwordInput.text = "";
passwordInput.enabled = false;
openButton.enabled = false;
var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(password);
conn.addEventListener(SQLEvent.OPEN, openHandler);
conn.addEventListener(SQLErrorEvent.ERROR, openError);
conn.openAsync(dbFile, SQLMode.CREATE, null, false, 1024, encryptionKey);
}
function openHandler(event:SQLEvent):void
{
conn.removeEventListener(SQLEvent.OPEN, openHandler);
conn.removeEventListener(SQLErrorEvent.ERROR, openError);
statusMsg.setStyle("textFormat", new TextFormat(null, null, 0x009900));
if (createNewDB)
{
statusMsg.text = "The encrypted database was created successfully.";
}
else
{
statusMsg.text = "The encrypted database was opened successfully.";
}
}
function openError(event:SQLErrorEvent):void
{
conn.removeEventListener(SQLEvent.OPEN, openHandler);
conn.removeEventListener(SQLErrorEvent.ERROR, openError);
if (!createNewDB && event.error.errorID == EncryptionKeyGenerator.ENCRYPTED_DB_PASSWORD_ERROR_ID)
{
statusMsg.text = "Incorrect password!";
}
else
{
statusMsg.text = "Error creating or opening database.";
}
}
Знакомство с классом EncryptionKeyGeneratorНеобходимо понимать внутреннюю механику работы класса EncryptionKeyGenerator, чтобы использовать его для создания защищенных ключей шифрования для базы данных приложения. Процесс использования класса объясняется в разделе Использование класса EncryptionKeyGenerator для получения защищенного ключа шифрования. Однако, возможно, покажется полезным ознакомиться с теми методиками, которые использует этот класс. Например, может потребоваться адаптация класса или реализация каких-либо из его методик в ситуациях, где необходим другой уровень конфиденциальности данных. Класс EncryptionKeyGenerator включен в проект корневой библиотеки ActionScript 3.0 (as3corelib) с открытым кодом. Можно загрузить пакет as3corelib, включая исходный код и документацию. Исходный код можно также просмотреть на сервере проекта или загрузить его для большей наглядности объяснений. Когда код создает экземпляр EncryptionKeyGenerator и вызывает его метод getEncryptionKey(), выполняется несколько шагов, обеспечивающих доступ к данным только уполномоченных на это пользователей. Процесс совпадает с процессом создания ключа шифрования из введенного пользователем пароля перед созданием базы данных, а также с пересозданием ключа шифрования для открытия базы данных. Получение и проверка надежного пароляКогда код вызывает метод getEncryptionKey(), он передает пароль в качестве параметра. Этот пароль используется в качестве основы для ключа шифрования. Благодаря использованию информации, известной только пользователю, такая схема гарантирует, что только пользователь, знающий пароль, сможет обратиться к данным в базе данных. Даже если злоумышленнику удастся получить доступ к учетной записи пользователя на компьютере, он не сможет войти в базу данных, не зная пароля. В целях обеспечения максимальной безопасности приложение никогда не сохраняет этот пароль. Код приложения создает экземпляр EncryptionKeyGenerator и вызывает его метод getEncryptionKey(), передавая введенный пользователем пароль в качестве аргумента (переменная password в этом примере): var keyGenerator:EncryptionKeyGenerator = new EncryptionKeyGenerator(); var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(password); Первым шагом, который выполняет класс EncryptionKeyGenerator при вызове метода getEncryptionKey(), является проверка введенного пользователем пароля на предмет его соответствия требованиям надежности пароля. Для класса EncryptionKeyGenerator необходимо использовать пароль, состоящий из 8-32 символов. Пароль должен содержать сочетание букв верхнего и нижнего регистров и хотя бы одной цифры или символа. Регулярное выражение, проверяющее соответствие этому шаблону, определяется как константа с именем STRONG_PASSWORD_PATTERN: private static const STRONG_PASSWORD_PATTERN:RegExp = /(?=^.{8,32}$)((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/;
Программный код, проверяющий пароль, размещается в методе validateStrongPassword() класса EncryptionKeyGenerator. Этот код имеет следующий вид: public function vaidateStrongPassword(password:String):Boolean
{
if (password == null || password.length <= 0)
{
return false;
}
return STRONG_PASSWORD_PATTERN.test(password))
}
При внутренней обработке метод getEncryptionKey() вызывает метод validateStrongPassword() класса EncryptionKeyGenerator и, если пароль недействителен, порождает исключение. Метод validateStrongPassword() — это общий метод, поэтому из программного кода можно выполнить проверку пароля без вызова метода getEncryptionKey(), чтобы избежать возникновения ошибки. Расширение пароля до 256 битовПозднее в процессе обработки требуется, чтобы пароль был длиной 256 битов. Вместо того, чтобы требовать от каждого пользователя вводить пароль точно 256 битов (32 символа) длиной, в коде создается более длинный пароль путем повторения символов пароля. Метод getEncryptionKey() вызывает метод concatenatePassword() для выполнения задачи создания длинного пароля. var concatenatedPassword:String = concatenatePassword(password); Следующий код предназначен для метода concatenatePassword(). private function concatenatePassword(pwd:String):String
{
var len:int = pwd.length;
var targetLength:int = 32;
if (len == targetLength)
{
return pwd;
}
var repetitions:int = Math.floor(targetLength / len);
var excess:int = targetLength % len;
var result:String = "";
for (var i:uint = 0; i < repetitions; i++)
{
result += pwd;
}
result += pwd.substr(0, excess);
return result;
}
Если длина пароля меньше 256 битов, код последовательно соединяет несколько экземпляров пароля, доводя длину до 256 битов. Если требуемую длину невозможно получить путем составления целого числа экземпляров пароля, последний экземпляр укорачивается, чтобы в результате получилось 256 битов. Создание или извлечение 256-разрядного солт-значенияСледующий шаг предназначен для получения 256-разрядного солт-значения, которое в последующих шагах соединяется паролем. Значение солт — это произвольное значение, которое добавляется или комбинируется с вводимым пользователем значением для получения пароля. Использование солт-значения с паролем гарантирует, что даже если пользователь выберет обычное слово или общеупотребимый термин в качестве пароля, комбинация пароль+солт, используемая системой, окажется произвольным значением. Эта произвольность помогает защититься от словарных атак, когда злоумышленник использует список слов в попытках угадать пароль. Создавая солт-значение и сохраняя его в зашифрованном локальном хранилище, выполняется привязка к учетной записи пользователя на компьютере, где находятся файлы базы данных. Если приложение вызывает метод getEncryptionKey() в первый раз, программный код создает произвольное 256-разрядное солт-значение. В противном случае код загружает солт-значение из зашифрованного локального хранилища. Солт-значение хранится в переменной с названием salt. Этот код определяет, не создано ли уже солт-значение, пытаясь загрузить его из зашифрованной локальной системы хранения. var salt:ByteArray = EncryptedLocalStore.getItem(saltKey);
if (salt == null)
{
salt = makeSalt();
EncryptedLocalStore.setItem(saltKey, salt);
}
Если код создает новое солт-значение, метод makeSalt() создает 256-разрядное произвольное значение. Поскольку значение в конечном итоге хранится в зашифрованном локальном хранилище, оно создается как объект ByteArray. Метод makeSalt() использует метод Math.random() для произвольного создания значений. Метод Math.random() не может создавать 256 битов одновременно. Вместо этого в программном коде организован цикл, вызывающий метод Math.random() восемь раз. Каждый раз создается произвольное значение uint в интервале от 0 до 4294967295 (максимальное значение uint). Значение uint используется для удобства, поскольку в uint содержится ровно 32 бита. 256-разрядное значение создается путем записи восьми значений uint в ByteArray. Следующий код предназначен для метода makeSalt(): private function makeSalt():ByteArray
{
var result:ByteArray = new ByteArray;
for (var i:uint = 0; i < 8; i++)
{
result.writeUnsignedInt(Math.round(Math.random() * uint.MAX_VALUE));
}
return result;
}
Когда код сохраняет солт-значение в зашифрованное локальное хранилище (ELS) или извлекает его из ELS, ему требуется ключ String, с которым сохраняется это солт-значение. Невозможно извлечь это солт-значение, не зная ключа. В этом случае ключ шифрования не может быть воссоздан для каждого открытия базы данных. По умолчанию EncryptionKeyGenerator использует предварительно определенный ключ ELS, определяемый константой SALT_ELS_KEY. Вместо использования ключа по умолчанию код приложения может также указать ключ ELS для использования при вызове метода getEncryptionKey(). Либо заданный по умолчанию, либо указанный приложением, ключ ELS хранится в переменной с именем saltKey. Эта переменная используется в вызовах EncryptedLocalStore.setItem() и EncryptedLocalStore.getItem(), как было показано ранее. Объединение 256-разрядного пароля и солт-значения с помощью оператора XORТеперь у кода есть 256-разрядный пароль и 256-разрядное солт-значение. Затем применяется побитовая операция XOR для объединения солт-значения и составного пароля в единое значение. В действительности эта методика позволяет создать 256-разрядный пароль, состоящий из целого спектра возможных символов. Это и происходит в действительности, несмотря на то, что в большинстве случаев фактически вводимый пароль состоит преимущественно из буквенно-цифровых символов. Подобное увеличение произвольности обеспечивает возможность расширения набора возможных паролей, не заставляя пользователя вводить сложные значения. Результат операции XOR сохраняется в переменной unhashedKey. Фактический процесс выполнения побитовой операции XOR для двух значений происходит в методе xorBytes(): var unhashedKey:ByteArray = xorBytes(concatenatedPassword, salt); Побитовый оператор XOR ^) берет два значения uint и возвращает одно значение uint. (Значение uint содержит 32 бита.) Входные значения, передаваемые как аргументы методу xorBytes(), являются значениями String (пароль) и ByteArray (солт). Затем код приложения использует цикл, чтобы извлечь 32 бита из каждого вводимого значения и скомбинировать их с помощью оператора XOR. private function xorBytes(passwordString:String, salt:ByteArray):ByteArray
{
var result:ByteArray = new ByteArray();
for (var i:uint = 0; i < 32; i += 4)
{
// ...
}
return result;
}
Внутри цикла первые 32 бита (4 байта) извлекаются из параметра passwordString. Эти биты извлекаются и преобразуются в uint (o1) с помощью двухфазного процесса. Вначале метод charCodeAt() получает цифровое значение для каждого символа. Затем это значение сдвигается в нужную позицию в uint с помощью оператора побитового сдвига влево (<<), после чего значение со сдвигом добавляется к o1. Например, первый символ (i) становится первыми 8 битами с помощью оператора побитового сдвига влево (<<); биты сдвигаются влево на 24 бита, получившееся значение назначается o1. Второй символ (i + 1) становится вторыми 8 битами путем сдвига влево на 16 битов и добавления результата к o1. Значения третьего и четвертого символов добавляются сходным образом. // ...
// Extract 4 bytes from the password string and convert to a uint
var o1:uint = passwordString.charCodeAt(i) << 24;
o1 += passwordString.charCodeAt(i + 1) << 16;
o1 += passwordString.charCodeAt(i + 2) << 8;
o1 += passwordString.charCodeAt(i + 3);
// ...
Переменная o1 теперь содержит 32 бита из параметра passwordString. Следующие 32 бита извлекаются из параметра salt вызовом метода readUnsignedInt(). Эти 32 бита сохраняются в переменной uint с названием o2. // ...
salt.position = i;
var o2:uint = salt.readUnsignedInt();
// ...
В завершение оба 32-разрядных (uint) значения объединяются с помощью оператора XOR, а результат записывается в значение ByteArray с названием result. // ...
var xor:uint = o1 ^ o2;
result.writeUnsignedInt(xor);
// ...
После завершения цикла возвращается значение ByteArray, содержащее результат XOR. // ...
}
return result;
}
Хэширование ключаКак только составной пароль и солт-значение объединены, следующий шаг призван дополнительно обезопасить это значение путем хэширования с помощью алгоритма SHA-256. Хэширование значения означает создание дополнительного препятствия для злоумышленника при попытке восстановить это значение. В этот момент у кода есть значение ByteArray с именем unhashedKey, содержащее составной пароль, объединенный с солт-значением. Проект корневой библиотеки ActionScript 3.0 (as3corelib) содержит класс SHA256 в пакете com.adobe.crypto. Метод SHA256.hashBytes() выполняет хэширование SHA-256 для ByteArray и возвращает значение String, содержащее 256-разрядный результат хэширования в виде шестнадцатеричного числа. Класс EncryptionKeyGenerator использует класс SHA256 для хэширования ключа. var hashedKey:String = SHA256.hashBytes(unhashedKey); Извлечение ключа шифрования из хэшаКлюч шифрования должен быть значением ByteArray длиной ровно 16 байтов (128 битов). Результатом выполнения алгоритма хэширования SHA-256 всегда является 256-разрядное значение. Следовательно, финальным шагом является выбор 128 битов из хэшированного результата для использования в качестве фактического ключа шифрования. В классе EncryptionKeyGenerator код приложения уменьшает ключ до 128 битов, вызывая метод generateEncryptionKey(). Затем результат этого метода возвращается как результат метода getEncryptionKey(): var encryptionKey:ByteArray = generateEncryptionKey(hashedKey); return encryptionKey; Совсем не обязательно использовать в качестве ключа шифрования первые 128 битов. Можно выбрать диапазон битов, начинающийся в некоторой произвольной точке, можно выбрать любой другой бит или использовать другие способы выбора битов. Важным является то, что программа выбирает 128 конкретных битов и именно эти 128 битов будут использоваться всегда. В рассматриваемом случае метод generateEncryptionKey() использует в качестве ключа шифрования биты, начиная с 18-го байта. Как уже упоминалось ранее, класс SHA256 возвращает значение String, содержащее в 256-разрядном хэш-значении шестнадцатеричный номер. Отдельный блок из 128 битов содержит слишком много байтов для одновременного добавления к ByteArray. Поэтому программа использует цикл for для извлечения символов из шестнадцатеричного значения String, преобразует их в фактические цифровые значения и добавляет к ByteArray. Полученное в результате обработки алгоритмом SHA-256 значение String имеет длину 64 символа. Диапазон в 128 битов равен 32 символам в объекте String, каждый символ соответствует 4 битам. Наименьшее приращение данных, которое можно добавить к ByteArray, составляет 1 байт (8 битов), что эквивалентно двум символам в переменной hash объекта String. Поэтому счетчик цикла изменяется 0 до 31 (32 символа) с приращением 2 символа. Внутри цикла программа вначале определяет начальную позицию для текущей пары символов. Поскольку требуемый диапазон начинается символом с позицией 17 в индексе (18-ый байт), то переменной position назначается текущее значение итератора (i) плюс 17. Код использует метод substr() объекта String для извлечения двух символов в текущей позиции. Эти символы хранятся в переменной hex. Затем программа использует метод parseInt() для преобразования переменной hex объекта String в десятеричное целое значение. Это значение сохраняется в целочисленной переменной byte. Наконец, программа добавляет значение из byte к переменной result объекта ByteArray, используя его метод writeByte(). Когда цикл завершается, переменная result объекта ByteArray содержит 16 байтов и уже готова к использованию в качестве ключа шифрования. private function generateEncryptionKey(hash:String):ByteArray
{
var result:ByteArray = new ByteArray();
for (var i:uint = 0; i < 32; i += 2)
{
var position:uint = i + 17;
var hex:String = hash.substr(position, 2);
var byte:int = parseInt(hex, 16);
result.writeByte(byte);
}
return result;
}
|
|