Korzystanie z szyfrowania w bazach danych SQLAdobe AIR 1.5 i wersje późniejsze Wszystkie aplikacje Adobe AIR współużytkują ten sam lokalny mechanizm bazy danych. Dlatego każda aplikacja AIR może połączyć się z niezaszyfrowanym plikiem bazy danych oraz odczytywać i zapisywać w nim dane. Począwszy od wersji 1.5 środowisko Adobe AIR oferuje możliwość tworzenia zaszyfrowanych plików baz danych i łączenia się z takimi plikami. Aby połączyć się z zaszyfrowaną bazą danych, aplikacja musi podać prawidłowy klucz szyfrowania. Jeśli nie zostanie podany prawidłowy klucz szyfrowania (lub nie zostanie podany żaden klucz), aplikacja nie będzie mogła połączyć się z bazą. W takiej sytuacji aplikacja nie będzie mogła odczytywać danych z bazy ani zapisywać bądź zmieniać w niej danych. Aby korzystać z szyfrowanej bazy danych, należy utworzyć bazę jako zaszyfrowaną. Gdy zaszyfrowana baza danych będzie już istniała, możliwe będzie nawiązanie z nią połączenia. Można również zmienić klucz szyfrowania zaszyfrowanej bazy danych. Techniki pracy z zaszyfrowanymi bazami danych — z wyjątkiem operacji tworzenia takich baz i łączenia się z nimi — są takie same, jak w przypadku baz niezaszyfrowanych. W szczególności instrukcje SQL wykonywane są tak samo niezależnie od tego, czy baza danych jest zaszyfrowana. Zastosowania zaszyfrowanej bazy danychSzyfrowanie jest przydatne w sytuacjach, gdy chcemy ograniczyć dostęp do informacji przechowywanych w bazie danych. Funkcja szyfrowania baz danych oferowana przez środowisko Adobe AIR może być używana w kilku różnych celach. Poniżej przedstawiono przykłady sytuacji, w których celowe mogłoby okazać się użycie zaszyfrowanej bazy danych.
Prawidłowe zdefiniowanie powodu, dla którego chcemy korzystać z zaszyfrowanej bazy danych, pomaga w podjęciu decyzji co do architektury aplikacji. W szczególności może mieć wpływ na sposób tworzenia, uzyskiwania i przechowywania klucza szyfrowania dla bazy danych. Więcej informacji na temat tych zagadnień zawiera sekcja Uwarunkowania szyfrowania bazy danych. Alternatywnym dla zaszyfrowanej bazy danych mechanizmem zapewnienia poufności danych jest użycie zaszyfrowanego magazynu lokalnego. Zaszyfrowany magazyn lokalny umożliwia przechowywanie pojedynczej wartości ByteArray przy użyciu klucza typu String. Dostęp do takiej wartości ma tylko ta aplikacja AIR, która ją zapisała, i tylko na komputerze, na którym wartość jest przechowywana. W przypadku zaszyfrowanego magazynu lokalnego nie ma potrzeby tworzenia własnego klucza szyfrowania. Z tego względu zaszyfrowany magazyn lokalny jest najprostszym mechanizmem przechowywania pojedynczej wartości lub zestawu wartości, które można w prosty sposób zakodować w obiekcie ByteArray. Zaszyfrowana baza danych najlepiej nadaje się do przechowywania większych zestawów danych, gdy pożądane jest przechowywanie danych w postaci strukturyzowanej i możliwość kierowania zapytań do bazy danych. Więcej informacji na temat korzystania z zaszyfrowanego magazynu lokalnego zawiera sekcja Zaszyfrowany magazyn lokalny. Tworzenie zaszyfrowanej bazy danychAby skorzystać z szyfrowania bazy danych, plik bazy danych należy zaszyfrować już na etapie jego tworzenia. Bazy danych utworzonej jako niezaszyfrowanej nie można później zaszyfrować. Podobnie, zaszyfrowanej bazy danych nie można później przekształcić w niezaszyfrowaną. W razie potrzeby można zmienić klucz szyfrowania zaszyfrowanej bazy danych. Szczegółowe informacje zawiera sekcja Zmiana klucza szyfrowania bazy danych. Jeśli istnieje już niezaszyfrowana baza danych, a chcemy skorzystać z szyfrowania bazy danych, można utworzyć nową zaszyfrowaną bazę danych i kopiować istniejącą strukturę tabel oraz dane do nowej bazy danych. Tworzenie zaszyfrowanej bazy danych odbywa się niemal identycznie jak tworzenie niezaszyfrowanej bazy danych, co opisano w sekcji Tworzenie bazy danych. Najpierw tworzymy instancję klasy SQLConnection reprezentującą połączenie z bazą danych. Bazę danych można utworzyć, wywołując metodę open() lub metodę openAsync() obiektu SQLConnection, jako lokalizację bazy danych wskazując plik, który jeszcze nie istnieje. Jedyna różnica występująca przy tworzeniu zaszyfrowanej bazy danych polega na konieczności podania wartości parametru encryptionKey (piątego parametru metody open() i szóstego parametru metody openAsync()). Poprawną wartością parametru encryptionKey jest obiekt ByteArray zawierający dokładnie 16 bajtów. Poniższe przykłady ilustrują tworzenie zaszyfrowanej bazy danych. Dla uproszczenia przykładów klucz szyfrowania został zapisany na stałe w kodzie aplikacji. Jednak w rzeczywistych zastosowaniach nie zaleca się stosowania tego rozwiązania, ponieważ nie jest ono bezpieczne. 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);
Przykład demonstrujący zalecany sposób generowania klucza szyfrowania zawiera sekcja Przykład: Generowanie i używanie klucza szyfrowania. Nawiązywanie połączenia z zaszyfrowaną bazą danychPodobnie jak w przypadku tworzenia zaszyfrowanej bazy danych, procedura otwierania połączenia z zaszyfrowaną bazą danych jest podobna do otwierania połączenia z bazą niezaszyfrowaną. Procedurę tę opisano bardziej szczegółowo w sekcji Połączenie z bazą danych. Należy użyć metody open()http://help.adobe.com/pl_PL/Flash/CS5/AS3LR/flash/data/SQLConnection.html#open() w celu otwarcia połączenia w trybie wykonywania synchronicznego lub metody openAsync() w celu otwarcia połączenia w trybie wykonywania asynchronicznego. Jedyna różnica polega na tym, że aby utworzyć zaszyfrowaną bazę danych, należy podać prawidłową wartość parametru encryptionKey (piątego parametru metody open() i szóstego parametru metody openAsync()). Jeśli podany klucz szyfrowania nie jest prawidłowy, zgłaszany jest błąd. W przypadku metody open() generowany jest wyjątek SQLError. W przypadku metody openAsync() obiekt SQLConnection wywołuje zdarzenie SQLErrorEvent, którego właściwość error zawiera obiekt SQLError. W obu przypadkach obiekt SQLError wygenerowany przez wyjątek ma właściwość errorID równą 3138. Ten identyfikator błędu odpowiada komunikatowi „Otwarty plik nie jest plikiem bazy danych”. Poniższy przykład ilustruje otwieranie zaszyfrowanej bazy danych w trybie wykonywania asynchronicznego. W celu uproszczenia przykładu klucz szyfrowania został zapisany na stałe w kodzie aplikacji. Jednak w rzeczywistych zastosowaniach nie zaleca się stosowania tego rozwiązania, ponieważ nie jest ono bezpieczne. 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);
}
}
Poniższy przykład ilustruje otwieranie zaszyfrowanej bazy danych w trybie wykonywania synchronicznego. W celu uproszczenia przykładu klucz szyfrowania został zapisany na stałe w kodzie aplikacji. Jednak w rzeczywistych zastosowaniach nie zaleca się stosowania tego rozwiązania, ponieważ nie jest ono bezpieczne. 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);
}
}
Przykład demonstrujący zalecany sposób generowania klucza szyfrowania zawiera sekcja Przykład: Generowanie i używanie klucza szyfrowania. Zmiana klucza szyfrowania bazy danychKlucz zaszyfrowanej bazy danych można zmieniać po jej utworzeniu. Aby zmienić klucz szyfrowania bazy danych, należy najpierw otworzyć połączenie z bazą danych, tworząc instancję klasy SQLConnection i wywołując jej metodę open() lub openAsync(). Po nawiązaniu połączenia z bazą danych należy wywołać metodę reencrypt(), przekazując jako jej argument nowy klucz szyfrowania. Podobnie jak w przypadku większości operacji na bazie danych, działanie metody reencrypt() jest różne w zależności od tego, czy połączenie z bazą danych jest nawiązane w trybie wykonywania synchronicznego, czy asynchronicznego. Jeśli do połączenia z bazą danych użyto metody open(), operacja reencrypt() działa asynchronicznie. Gdy operacja zostanie zakończona, następuje wznowienie wykonywania kodu od kolejnego wiersza: var newKey:ByteArray = new ByteArray(); // ... generate the new key and store it in newKey conn.reencrypt(newKey); Natomiast jeśli połączenie z bazą danych zostało otwarte przy użyciu metody openAsync(), operacja reencrypt() jest asynchroniczna. Wywołanie metody reencrypt() rozpoczyna proces ponownego szyfrowania. Po zakończeniu operacji obiekt SQLConnection wywołuje zdarzenie reencrypt. W celu wykrycia zakończenia operacji ponownego szyfrowania należy skorzystać z detektora zdarzeń: 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
}
Operacja reencrypt() działa w ramach swojej własnej transakcji. Jeśli zostanie przerwana lub zakończy się niepowodzeniem (na przykład aplikacja zostanie zamknięta przed ukończeniem operacji), transakcja jest wycofywana. W takim wypadku w bazie danych nadal obowiązuje dotychczasowy klucz szyfrowania. Metoda reencrypt() nie umożliwia przekształcenie bazy danych w niezaszyfrowaną. Przekazanie do metody reencrypt()klucza szyfrowania o wartości null lub niebędącego 16-bajtowym obiektem ByteArray spowoduje błąd. Uwarunkowania szyfrowania bazy danychW sekcji Zastosowania zaszyfrowanej bazy danych przedstawiono kilka przypadków, w których celowe byłoby użycie zaszyfrowanej bazy danych. Oczywiste jest, że w różnych zastosowaniach (przedstawionych oraz innych) obowiązują różne wymagania w zakresie poufności danych. Sposób włączenia mechanizmu szyfrowania do struktury aplikacji istotnie wpływa na skuteczność ochrony poufności danych w bazie. Na przykład, jeśli zaszyfrowana baza danych ma zapewniać poufność danych osobistych, nawet między różnymi użytkownikami tego samego komputera, to baza danych każdego użytkownika powinna mieć inny klucz szyfrowania. Aby uzyskać najwyższy poziom bezpieczeństwa, aplikacja może generować klucze na podstawie haseł wprowadzanych przez użytkowników. Powiązanie klucza szyfrowania z hasłem gwarantuje, że nawet jeśli inna osoba zdoła podszyć się pod konto użytkownika na komputerze, nadal nie będzie mieć dostępu do danych. Przeciwstawny skrajny przypadek to sytuacja, w której plik bazy danych powinien być czytelny dla wszystkich użytkowników naszej aplikacji, ale nie dla innych aplikacji. W takim przypadku każda zainstalowana kopia aplikacji musi mieć dostęp do współużytkowanego klucza szyfrowania. Budowę aplikacji, a w szczególności technikę generowania kluczy szyfrowania, można dopasować do wymaganego poziomu poufności danych aplikacji. Na poniższej liście przedstawiono propozycje rozwiązań odpowiadających różnym poziomom poufności danych:
Poniżej przedstawiono dodatkowe uwarunkowania dotyczące bezpieczeństwa, o których należy pamiętać, projektując aplikację korzystającą z zaszyfrowanej bazy danych.
Przykład: Generowanie i używanie klucza szyfrowaniaTa przykładowa aplikacja ilustruje jedną z technik generowania klucza szyfrowania. Aplikacja została zaprojektowana w taki sposób, aby zapewniała najwyższy poziom poufności i bezpieczeństwa danych użytkowników. Jednym z ważnych aspektów ochrony danych poufnych jest wymuszenie na użytkowniku wprowadzenia hasła za każdym razem, gdy aplikacja łączy się z bazą danych. Dlatego, co ilustruje omawiany przykład, aplikacja wymagająca tak wysokiego poziomu bezpieczeństwa nie powinna nigdy bezpośrednio przechowywać klucza szyfrowania bazy danych. Aplikacja składa się z dwóch części: klasy ActionScript generującej klucz szyfrowania (klasa EncryptionKeyGenerator) oraz prostego interfejsu użytkownika, który ilustruje zastosowanie tej klasy. Pełny kod źródłowy zawiera sekcja Kompletny przykładowy kod ilustrujący generowanie i używanie klucza szyfrowania. Użycie klasy EncryptionKeyGenerator w celu uzyskania bezpiecznego klucza szyfrowaniaAby używać klasy EncryptionKeyGenerator we własnych aplikacjach, nie jest konieczna znajomość zasad jej wewnętrznego działania. Czytelnicy zainteresowani szczegółowym algorytmem generowania klucza szyfrowania dla bazy danych znajdą odpowiednie informacje w sekcji Omówienie klasy EncryptionKeyGenerator. Aby użyć klasy EncryptionKeyGenerator we własnej aplikacji, należy postępować według poniższej procedury:
Kompletny przykładowy kod ilustrujący generowanie i używanie klucza szyfrowaniaPoniżej przedstawiono kompletny kod aplikacji przykładowej „Generowanie i używanie klucza szyfrowania”. Kod składa się z dwóch części. W przykładzie zastosowano klasę EncryptionKeyGenerator do tworzenia klucza szyfrowania na podstawie hasła. Klasa EncryptionKeyGenerator wchodzi w skład biblioteki podstawowej języka ActionScript 3.0 (as3corelib). Jest to projekt open source. Pakiet as3corelib, wraz z kodem źródłowym i dokumentacją jest dostępny do pobrania. Ze strony projektu można również pobrać pliki SWC lub pliki z kodem źródłowym. Przykład w środowisku FlexPlik MXML aplikacji zawiera kod źródłowy prostej aplikacji, która tworzy lub otwiera połączenie z zaszyfrowaną bazą danych. <?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>
Przykład dla programu Flash ProfessionalPlik FLA aplikacji zawiera kod źródłowy prostej aplikacji, która tworzy lub otwiera połączenie z zaszyfrowaną bazą danych. Plik FLA zawiera cztery składniki umieszczone na stole montażowym:
Kod aplikacji jest zdefiniowany w klatce kluczowej będącej klatką nr 1 na głównej osi czasu. Kod aplikacji przedstawiono poniżej: 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.";
}
}
Omówienie klasy EncryptionKeyGeneratorZnajomość wewnętrznego mechanizmu działania klasy EncryptionKeyGenerator nie jest niezbędna do wykorzystania tej klasy w celu utworzenia bezpiecznego klucza szyfrowania bazy danych. Procedurę użycia klasy wyjaśniono w sekcji Użycie klasy EncryptionKeyGenerator w celu uzyskania bezpiecznego klucza szyfrowania. Jednak dla wielu czytelników przydatne może okazać się zrozumienie technik zastosowanych w tej klasie. Na przykład możliwe jest zaadaptowanie klasy do nietypowych potrzeb lub wykorzystanie niektórych z zastosowanych w niej technik w sytuacjach, w których wymagany jest inny poziom poufności danych. Klasa EncryptionKeyGenerator wchodzi w skład biblioteki podstawowej języka ActionScript 3.0 (as3corelib). Jest to projekt open source. Pakiet as3corelib wraz z kodem źródłowym i dokumentacją jest dostępny do pobrania. Możliwe jest także przeglądanie kodu źródłowego na stronie projektu lub pobranie go w celu przeanalizowania z pomocą niniejszych wyjaśnień. Gdy kod tworzy instancję klasy EncryptionKeyGenerator i wywołuje jej metodę getEncryptionKey(), podejmowany jest szereg działań zmierzających do zapewnienia dostępu do danych wyłącznie uprawnionemu użytkownikowi. Proces przebiega tak samo niezależnie od tego, czy klucz jest generowany na podstawie hasła przed utworzeniem bazy danych, czy też jest odtwarzany w celu otwarcia bazy danych. Uzyskanie i sprawdzenie poprawności silnego hasłaW wywołaniu metody getEncryptionKey() kod aplikacji przekazuje hasło jako parametr. Hasło stanowi podstawę dla klucza szyfrowania. Wykorzystanie w kluczu informacji znanej tylko użytkownikowi gwarantuje, że dostęp do danych w bazie będzie miała wyłącznie osoba znająca hasło. Nawet jeśli inna osoba uzyska dostęp do konta użytkownika na komputerze, nie dostanie się do bazy danych, jeśli nie zna hasła. Aby uzyskać maksymalny poziom bezpieczeństwa, aplikacja nigdy nie przechowuje hasła. Kod aplikacji tworzy instancję klasy EncryptionKeyGenerator i wywołuje jej metodę getEncryptionKey(), przekazując jako argument hasło wprowadzone przez użytkownika (w tym przykładzie w zmiennej password): var keyGenerator:EncryptionKeyGenerator = new EncryptionKeyGenerator(); var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(password); Pierwszym krokiem wykonywanym w klasie EncryptionKeyGenerator po wywołaniu metody getEncryptionKey() jest sprawdzenie, czy hasło wprowadzone przez użytkownika spełnia wymagania stawiane hasłom silnym. Klasa EncryptionKeyGenerator wymaga hasła o długości od 8 do 32 znaków. Hasło musi zawierać kombinację wielkich i małych liter oraz co najmniej jedną cyfrę lub symbol. Wyrażenie regularne sprawdzające zgodność z tymi wymogami jest zdefiniowane jako stała o nazwie STRONG_PASSWORD_PATTERN: private static const STRONG_PASSWORD_PATTERN:RegExp = /(?=^.{8,32}$)((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/;
Kod sprawdzający hasło znajduje się w metodzie validateStrongPassword() klasy EncryptionKeyGenerator. Oto wspomniany kod: public function vaidateStrongPassword(password:String):Boolean
{
if (password == null || password.length <= 0)
{
return false;
}
return STRONG_PASSWORD_PATTERN.test(password))
}
Metoda getEncryptionKey() wywołuje wewnętrznie metodę validateStrongPassword() klasy EncryptionKeyGenerator, a jeśli hasło nie spełnia wymogów, generuje wyjątek. Metoda validateStrongPassword() jest metodą publiczną, a zatem istnieje możliwość sprawdzenia hasła bez wywoływania metody getEncryptionKey() i uniknięcia w ten sposób zgłoszenia błędu. Rozwijanie hasła do 256 bitówNa dalszych etapach procesu hasło musi mieć długość 256 bitów. Kod jest napisany w taki sposób, że użytkownik nie musi wprowadzać hasła o długości dokładnie 256 bitów (32 znaków); w razie potrzeby dłuższe hasło jest tworzone przez powtórzenie znaków hasła użytkownika. Metoda getEncryptionKey() wywołuje metodę concatenatePassword() w celu uzyskania hasło o wymaganej długości. var concatenatedPassword:String = concatenatePassword(password); Oto kod metody 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;
}
Jeśli hasło jest krótsze niż 256 bitów, jest konkatenowane samo ze sobą w celu uzyskania długości 256 bitów. Jeśli 256 nie jest dokładną wielokrotnością długości hasła, ostatnie powtórzenie jest skracane w celu uzyskania dokładnie tej liczby bitów. Generowanie lub pobieranie 256-bitowego modyfikatoraNastępnym krokiem jest uzyskanie 256-bitowej wartości modyfikatora, która na późniejszym etapie zostanie połączona z hasłem. Modyfikator (ang. salt, dosłownie „sól”, czyli przyprawa) to wartość losowa dodawana do wartości wprowadzonej przez użytkownika lub w inny sposób z nią łączona. Połączenie modyfikatora z hasłem gwarantuje, że nawet jeśli użytkownik w charakterze hasła poda zwykłe słowo występujące w słownikach, kombinacja hasło+modyfikator będzie wartością losową. Ta losowość chroni przed tzw. atakami słownikowymi, w których osoba atakująca próbuje odgadnąć hasło, próbując użyć kolejnych słów z listy. Ponadto wygenerowanie modyfikatora i zapisanie go w szyfrowanym magazynie lokalnym wiąże jego wartość z kontem użytkownika na komputerze, na którym znajduje się plik bazy danych. Jeśli aplikacja wywołuje metodę getEncryptionKey() po raz pierwszy, tworzona jest losowa 256-bitowa wartość modyfikatora. W przeciwnym razie modyfikator jest ładowany z szyfrowanego magazynu lokalnego. Modyfikator jest przechowywany w zmiennej o nazwie salt. W celu sprawdzenia, czy modyfikator został już utworzony, podejmowana jest próba załadowania go z szyfrowanego magazynu lokalnego: var salt:ByteArray = EncryptedLocalStore.getItem(saltKey);
if (salt == null)
{
salt = makeSalt();
EncryptedLocalStore.setItem(saltKey, salt);
}
W przypadku tworzenia nowego modyfikatora metoda makeSalt() generuje 256-bitową wartość losową. Ponieważ docelowo wartość ta ma być zapisana w szyfrowanym magazynie lokalnym, jest generowana jako obiekt ByteArray. Metoda makeSalt() używa metody Math.random() do wygenerowania wartości losowej. Metoda Math.random() nie umożliwia wygenerowania 256 bitów naraz. Dlatego zastosowano pętlę, w której metoda Math.random() wywoływana jest osiem razy. Za każdym razem generowana jest wartość losowa z przedziału od 0 do 4294967295 (maksymalna wartość typu uint). Typu uint użyto jako najdogodniejszego, ponieważ wartości tego typu mają dokładnie po 32 bity. Zapisanie ośmiu wartości uint w obiekcie ByteArray prowadzi do wygenerowania wartości 256-bitowej. Oto kod metody 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;
}
Do zapisania modyfikatora w szyfrowanym magazynie lokalnym lub pobrania go z magazynu potrzebny jest klucz (ciąg znaków), pod którym modyfikator jest zapisany. Bez znajomości klucza nie można odczytać wartości modyfikatora. W tej sytuacji klucza szyfrowania nie dałoby się odtworzyć każdorazowo przy otwieraniu bazy danych. Domyślnie klasa EncryptionKeyGenerator używa wstępnie zdefiniowanego klucza magazynu, który jest zdefiniowany w stałej SALT_ELS_KEY. Zamiast używać klucza domyślnego, aplikacja może w wywołaniu metody getEncryptionKey() podać własny klucz magazynu. Domyślny lub podany przez aplikację klucz magazynu identyfikujący modyfikator jest przechowywany w zmiennej saltKey. Zmienna ta jest używana w wywołaniach metod EncryptedLocalStore.setItem() i EncryptedLocalStore.getItem(), tak jak przedstawiono to powyżej. Łączenie 256-bitowego hasła i modyfikatora przy użyciu operatora XORTeraz dla kodu jest już dostępne i 256-bitowe hasło, i 256-bitowy modyfikator. Następnie wykonywana jest operacja XOR w celu połączenia modyfikatora ze skonkatenowanym hasłem w pojedynczą wartość. Wynikiem zastosowania tej techniki jest 256-bitowe hasło zawierające znaki z całego dostępnego zestawu. Znaki wynikowego hasła będą pochodzić z całego zestawu znaków, nawet jeśli hasło wprowadzone przez użytkownika zawierało w większości znaki alfanumeryczne. Ten dodatkowy czynnik losowy skutecznie powiększa zbiór możliwych haseł (utrudniając złamanie hasła), a jednocześnie eliminuje konieczność ręcznego wprowadzania długich i skomplikowanych ciągów znaków. Wynik operacji XOR jest zapisywany w zmiennej unhashedKey. Właściwa operacja bitowej różnicy symetrycznej (XOR) na dwóch wartościach wykonywana jest w metodzie xorBytes(): var unhashedKey:ByteArray = xorBytes(concatenatedPassword, salt); Bitowy operator XOR (^) działa na dwóch wartościach typu uint i zwraca wartość typu uint. (Wartości typu uint są 32-bitowe). Argumenty przekazywane do metody xorBytes() należą do typu String (hasło) i ByteArray (modyfikator). Dlatego w kodzie zastosowano pętlę, która w każdym przebiegu wyodrębnia po 32 bity z każdego z argumentów przeznaczonych do wykonania operacji 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;
}
Wewnątrz pętli wyodrębniane są pierwsze 32 bity (4 bajty) parametru passwordString. Te bity są wyodrębniane i konwertowane na wartość typu uint (o1) w dwóch etapach. Najpierw metoda charCodeAt() pobiera wartość liczbową każdego ze znaków. Następnie wartość znaku jest przesuwana na odpowiednią pozycję w ramach wartości uint przy użyciu operatora przesunięcia bitowego w lewo <<), a przesunięta wartość jest dodawana do o1. Na przykład pierwszy znak (i) staje się pierwszymi 8 bitami po zastosowaniu operatora przesunięcia bitowego w lewo (<<) o 24 bity i przypisaniu uzyskanej wartości do zmiennej o1. Drugi znak (i + 1) staje się drugą grupą 8 bitów po przesunięciu jego wartości w lewo o 16 bitów i dodaniu wyniku do o1. Wartości trzeciego i czwartego znaku są dodawane w ten sam sposób. // ...
// 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);
// ...
Zmienna o1 zawiera teraz 32 bity z parametru passwordString. Następnie z parametru salt wyodrębniane są 32 bity poprzez wywołanie metody readUnsignedInt() tego parametru. Wyodrębnione 32 bity są zapisywane w zmiennej o2 typu uint. // ...
salt.position = i;
var o2:uint = salt.readUnsignedInt();
// ...
Na koniec dwie wartości 32-bitowe (uint) są łączone za pomocą operatora XOR, a wynik jest zapisywany w obiekcie ByteArray o nazwie result. // ...
var xor:uint = o1 ^ o2;
result.writeUnsignedInt(xor);
// ...
Po zakończeniu pętli zwracany jest obiekt ByteArray zawierający wynik operacji XOR. // ...
}
return result;
}
Wymieszanie kluczaNastępnym krokiem po połączeniu skonkatenowanego hasła i modyfikatora jest dodatkowe zabezpieczenie wyniku poprzez wymieszanie go przy użyciu algorytmu mieszającego SHA-256. Wymieszanie wartości utrudnia potencjalnemu agresorowi odtworzenie sposobu, w jaki została utworzona. Na tym etapie w kodzie jest dostępny obiekt ByteArray o nazwie unhashedKey zawierający skonkatenowane hasło połączone z modyfikatorem. Projekt biblioteki podstawowej ActionScript 3.0 (as3corelib) obejmuje klasę SHA256 wchodząca w skład pakietu com.adobe.crypto. Metoda SHA256.hashBytes() wykonuje mieszanie na obiekcie ByteArray zgodnie z algorytmem SHA-256 i zwraca obiekt String zawierający 256-bitowy wynik mieszania w formie liczby szesnastkowej. W klasie EncryptionKeyGenerator klasa SHA256 jest używana do mieszania klucza: var hashedKey:String = SHA256.hashBytes(unhashedKey); Wyodrębnienie klucza szyfrowania z wyniku mieszaniaKlucz szyfrowania musi być obiektem ByteArray o długości dokładnie 16 bajtów (128 bitów). Wynik działania algorytmu mieszającego SHA-256 ma zawsze długość 256 bitów. Dlatego ostatnim etapem będzie wybranie z wyniku mieszania 128 bitów, które posłużą za faktyczny klucz szyfrowania. W klasie EncryptionKeyGenerator klucz jest skracany do 128 bitów w wyniku wywołania metody generateEncryptionKey(). Wynik tej metod jest zwracany jako wynik metody getEncryptionKey(): var encryptionKey:ByteArray = generateEncryptionKey(hashedKey); return encryptionKey; Kluczem szyfrowania nie musi być pierwsze 128 bitów. Równie dobrze można wybrać ciąg bitów rozpoczynający się w arbitralnie określonym punkcie, co drugi bit lub sekwencję bitów określoną w inny sposób. Ważne jest jedynie, aby wybrać 128 odrębnych bitów i za każdym razem używać tych samych 128 bitów. W prezentowanym przypadku metoda generateEncryptionKey() jako klucz szyfrowania wybiera ciąg bitów rozpoczynający się od 18. bajtu. Jak już wcześniej wspomniano, klasa SHA256 zwraca obiekt String zawierający 256-bitowy wynik mieszania zapisany w postaci liczby szesnastkowej. Pojedynczy blok 128 bitów składa się ze zbyt wielu bajtów, aby można go było dodać do obiektu ByteArray w jednym kroku. Dlatego zastosowano pętlę for wyodrębniającą znaki z ciągu cyfr szesnastkowych, konwertującą je na wartości liczbowe i dodającą do obiektu ByteArray. Ciąg znaków będący wynikiem działania algorytmu SHA-256 ma długość 64 znaków. Ciąg 128 bitów odpowiada 32 znakom ciągu, a każdy znak reprezentuje 4 bity. Najmniejszą jednostką danych, jaką można dodać do obiektu ByteArray, jest jeden bajt (8 bitów), co odpowiada dwóm znakom w obiekcie String o nazwie hash. Dlatego pętla odlicza od 0 do 31 (32 znaki) z przyrostem 2 znaków. Wewnątrz pętli najpierw określana jest początkowa pozycja bieżącej pary znaków. Ponieważ wybrany zakres zaczyna się od znaku o indeksie 17 (18. bajtu), zmiennej position jest przypisywana bieżąca wartość iteratora (i) plus 17. Do wyodrębnienia dwóch znaków z bieżącej pozycji używana jest metoda substr(). Znaki te są przechowywane w zmiennej hex. Następnie za pomocą metody parseInt() ciąg znaków hex jest konwertowany na dziesiętną wartość całkowitą. Wartość ta jest zapisywana w zmiennej byte typu int. Na koniec wartość byte jest dodawana do obiektu ByteArray o nazwie result przy użyciu metody writeByte() tego obiektu. Po zakończeniu pętli obiekt ByteArray result zawiera 16 batów i jest gotowy do użycia w charakterze klucza szyfrowania bazy danych. 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;
}
|
|