Использование шифрования с базами данных SQL

Adobe AIR 1.5 и более поздних версий

Все приложения Adobe AIR используют один и тот же механизм локальной базы данных. Следовательно, любое приложение AIR может подключаться к незашифрованному файлу данных для чтения и записи данных в него. Начиная с Adobe AIR версии 1.5 это приложение обеспечивает возможность создания зашифрованных файлов базы данных и подключения к ним. При использовании зашифрованной базы данных для подключения к ней приложение должно предоставить правильный ключ шифрования. Если предоставлен неверный ключ шифрования (или не предоставлен никакой ключ), приложение не подключится к базе данных. А значит приложение не сможет считывать данные из базы данных, а также записывать в нее данные или изменять их.

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

Использование зашифрованной базы данных

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

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

  • Локальное хранилище приложений, для закрытых данных, синхронизируемых с сервером (данные отправляются на сервер и загружаются с него)

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

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

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

Помимо зашифрованных баз данных существует альтернативный механизм конфиденциального хранения важных данных в зашифрованном локальном хранилище . Зашифрованное локальное хранилище позволяет хранить значение 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() , произойдет ошибка.

Предпосылки для использования зашифрованной базы данных

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

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

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

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

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

Далее изложены дополнительные соображения безопасности, которые важно учитывать при разработке приложения для работы с зашифрованной базой данных:

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

  • Исходный код приложения AIR хранится на компьютере пользователя в виде плоского текста (для HTML-содержимого) или в виде легко декомпилируемого двоичного формата (для SWF-содержимого). Поскольку возможен доступ к исходному коду, необходимо учитывать два аспекта:

    • Никогда не кодируйте жестко ключ шифрования в исходный код

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

  • Шифрование в базах данных AIR использует стандарт AES в режиме CCM. Для обеспечения безопасности код шифрования требует комбинации вводимого пользователем ключа и солт-значения. Пример этой методики см. в разделе Пример: создание и использование ключа шифрования .

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

Пример: создание и использование ключа шифрования

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

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

Использование класса EncryptionKeyGenerator для получения защищенного ключа шифрования

Для использования класса EncryptionKeyGenerator важно иметь четкое представление о принципах его работы. Дополнительные сведения о способах создания классом ключа шифрования см. в разделе « Знакомство с классом EncryptionKeyGenerator ».

Выполните следующие шаги, чтобы использовать класс EncryptionKeyGenerator в своем приложении:

  1. Загрузите класс EncryptionKeyGenerator в качестве исходного кода или скомпилированного кода SWC. Класс EncryptionKeyGenerator включен в проект корневой библиотеки ActionScript 3.0 (as3corelib) с открытым кодом. Можно загрузить пакет as3corelib, включая исходный код и документацию к нему . SWC или файлы с исходным кодом можно загрузить со страницы проекта.

  2. Поместите исходный код класса EncryptionKeyGenerator (или as3corelib SWC) в местоположение, где исходный код приложения сможет найти его.

  3. Добавьте инструкцию import для класса EncryptionKeyGenerator в исходный код приложения.

    import com.adobe.air.crypto.EncryptionKeyGenerator;
  4. До того места в коде, где создается база данных или открывается соединение с ней, добавьте код, создающий экземпляр EncryptionKeyGenerator, вызвав конструктор EncryptionKeyGenerator() .

    var keyGenerator:EncryptionKeyGenerator = new EncryptionKeyGenerator();
  5. Получение пароля от пользователя:

    var password:String = passwordInput.text; 
     
    if (!keyGenerator.validateStrongPassword(password)) 
    { 
        // display an error message 
        return; 
    }

    Экземпляр EncryptionKeyGenerator использует этот пароль в качестве основы для ключа шифрования (показано в следующем шаге). Экземпляр EncryptionKeyGenerator проверяет пароль на соответствие определенным требованиям проверки надежности пароля. Если проверка не проходит, выдается сообщение об ошибке. В показанном демонстрационном коде можно заранее проверить пароль, вызвав метод validateStrongPassword() объекта EncryptionKeyGenerator. Таким способом можно определить, соответствует ли пароль минимальным требованиям надежности пароля и избежать ошибки.

  6. Создание ключа шифрования из пароля:

    var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(password);

    Метод getEncryptionKey() создает и возвращает ключ шифрования (16-байтовый ByteArray). Этот ключ шифрования можно использовать для создания новой зашифрованной базы данных или открытия существующей базы.

    Метод getEncryptionKey() имеет один требуемый параметр, которым является пароль, получаемый в шаге 5.

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

    Метод getEncryptionKey() также принимает второй (необязательный) параметр — overrideSaltELSKey . Класс EncryptionKeyGenerator создает произвольное значение (называемое солт- значением), которое используется как часть ключа шифрования. Чтобы можно было повторно создать ключ шифрования, солт-значение сохраняется в зашифрованном локальном хранилище (ELS) вашего приложения AIR. По умолчанию класс EncryptionKeyGenerator использует определенную строку в качестве ключа ELS. Хотя и маловероятно, но существует возможность, что этот ключ будет конфликтовать с другим ключом, используемым вашим приложением. Вместо использования ключа по умолчанию можно указать свой собственный ключ ELS. В этом случае укажите настраиваемый ключ, передав его вторым параметром getEncryptionKey() , как показано далее:

    var customKey:String = "My custom ELS salt key"; 
    var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(password, customKey);
  7. Создание или открытие базы данных

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

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

    conn.addEventListener(SQLEvent.OPEN, openHandler); 
    conn.addEventListener(SQLErrorEvent.ERROR, openError); 
     
    conn.openAsync(dbFile, SQLMode.CREATE, null, false, 1024, encryptionKey);

    В методах прослушивания программный код удаляет регистрации событий процессом прослушивания. Затем отображается сообщение о состоянии, показывающее, была ли база данных создана, открыта и не произошла ли ошибка. Особого упоминания из этих обработчиков событий заслуживает метод openError() . В этом методе инструкция if проверяет, существует ли база данных (подразумевает, что программа попыталась подключиться к существующей базе данных) и соответствует ли идентификатор ошибки константы EncryptionKeyGenerator.ENCRYPTED_DB_PASSWORD_ERROR_ID . Если оба этих условия выполняются, это скорее всего означает, что введенный пользователем пароль не верен. (Это также может означать, что указанный файл вообще не является файлом базы данных.) Далее показан код, который проверяет идентификатор ошибки:

    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 включен в проект корневой библиотеки ActionScript 3.0 (as3corelib) с открытым кодом. Можно загрузить пакет as3corelib, включая исходный код и документацию к нему . SWC или файлы с исходным кодом можно загрузить со страницы проекта.

Пример Flex

MXML-файл приложения содержит исходный код для простейшего приложения, создающего или открывающего соединение с зашифрованной базой данных:

<?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 Professional

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

Имя экземпляра

Тип компонента

Описание

instructions

Label

Содержит инструкции для пользователя

passwordInput

TextInput

Поле ввода, куда пользователь вводит пароль

openButton

Button

Кнопка, которую нажимает пользователь после ввода пароля

statusMsg

Label

Отображает сообщения о состоянии (успешно или с ошибкой)

Код для приложения определяется в ключевом кадре кадра 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; 
}