Zabezpieczyć, znaczy zakodować

Jadwiga Gnybek Jadwiga_Gnybek@nofer.lodz.pl

Ostatnie lata obfitują w liczne zdarzenia wymuszające na administratorach baz danych działania zwiększające poziom zabezpieczeń dla przechowywanych w bazie danych. O tym czy coś jest warte ochrony czy też nie, decyduje już nie tylko i wyłącznie właściciel bazy. Istnieje bowiem wiele uregulowań prawnych nakładających na administratorów systemów informatycznych składujących dane (a jest ich przecież zdecydowana większość) obowiązek ochrony chociażby danych osobowych.

Wiele siwych włosów przysporzyła administratorom również amerykańska ustawa znana pod kryptonimem SOX. Nakłada ona wiele wymagań zarówno na informatyków, jak i użytkowników systemów generujących dane potrzebne do oceny kondycji finansowej firmy.

Te rosnące z roku na rok wymagania, niestety przykładane są zarówno do systemów nowoprojektowanych jak i do tych, które z powodzeniem eksploatowane są już od wielu lat. Właśnie dla tych drugich dostosowanie się do zwiększonych reżimów bezpieczeństwa stanowi szczególnie trudne wyzwanie. Szczególnie poszukiwane są zatem rozwiązania nieinwazyjne. Możliwe do implementacji bez konieczności zmiany kodu aplikacji.

Kodowanie w Oracle’u

Takim właśnie rozwiązaniem jest propozycja kodowania danych zapisanych w bazie Oracle. O ile baza działająca w określonym środowisku jest zwykle w ustalony sposób zabezpieczona, o tyle wszelkie operacje prowadzone poza nią, wprowadzają dla obrotu danymi dodatkowe ryzyko. Do operacji takich zaliczyć można na przykład wykonywanie kopii zapasowych bazy. Utrata nośników na których składowane są kopie zapasowe bazy to klasyczny przykład tego, że nawet z najlepiej zabezpieczonego serwera dane potrafią wyciec „same”. Aż prosi się zatem, aby dane takie zabezpieczyć jakimś kluczem, który przechowywany będzie w innej lokalizacji i z zapewnieniem dodatkowych środków ochrony.

Zabezpieczenie takie zrealizować można wykorzystując jedną z funkcjonalności Oracle Database 10g Release 2. Pozwala ona bowiem na zdeklarowanie kolumn, w których dane będą zapisywane po ich uprzednim zakodowaniu. Ponieważ kodowanie realizowane jest po stronie bazodanowej, nie wymaga wprowadzania żadnych zmian w interfejsie użytkownika i kodzie realizującym funkcjonalność aplikacji. Wystarczy jedynie implementacja Transparent Data Encryption (TDE).

Pomysł od strony logiki zdarzeń jest banalnie prosty. Dane wprowadzane przez aplikacje poddawane są algorytmowi kodowania, który odwrócić można jedynie znając klucz zastosowany w procesie kodowania danych. Teraz tylko trzeba pilnie strzec danych, klucza i algorytmu.

 


Rys. 1.

 

W rozważanym przez nas przypadku kodowaniem objąć możemy nie tyle wszystkie dane, ale raczej ich pewien wybór. Na przykład: określone kolumny zawierające dane wrażliwe. Czy to konieczne? Pewnie nie, ale kodowanie wszystkich danych w bazie, z pewnością spowoduje zwiększenie obciążenia maszyny i spowolnienie czasu reakcji aplikacji. Nie idźmy zatem na całość. Kodujmy wybiórczo. Odnosząc rysunek przedstawiony powyżej do bazodanowego zastosowania, można by nakreślić następujący schemat:

 


Rys. 2.

 

Jak widać, sprawa powoli się komplikuje. Aby zaimplementować ten mechanizm, musimy bowiem nie tylko wskazać kolumny, które naszym zdaniem powinny zostać zabezpieczone kodowaniem. Na rysunku 2 pojawił się również nowy element – portfel kluczy. Czyli miejsce, gdzie w bezpieczny sposób przechowywane są główne klucze służące do kodowania i rozkodowywania danych. Jak widać, portfel ten jest fizycznie plikiem składowanym poza bazą danych, a dostęp do skrywanych przez niego informacji jest zabezpieczony dodatkowym hasłem dostępu. Bez względu na liczbę wskazanych do kodowania kolumn, mechanizmy Oracle Database 10g wygenerują jeden klucz przeznaczony do kodowania danych w tej tabeli. Co ważne: kodowanie danych obowiązuje w całym cyklu życia danych w bazie. W postaci zakodowanej są one zatem zapisywane nie tylko do plików dyskowych, ale również do plików archive logów, kopii zapasowych oraz exportów. To właśnie dzięki tak kompleksowemu podejściu do sprawy zabezpieczenia danych składowanych w bazie, mechanizm ten spełnia wymogi wielu regulacji prawnych i obowiązujących dyrektyw.

A teraz od kuchni

W zbiorze standardowych procedur Oracle Database 10g znaleźć można pakiet o nazwie DBMS_CRYPTO. To właśnie przy jego pomocy zabezpieczać będziemy nasze dane. Jego wcześniejszym wcieleniem, znanym już we wcześniejszych wersjach bazy, był pakiet DBMS_OBFUSCATION_TOOLKIT.

Generacja kluczy

Generacja kluczy to jedna z ważniejszych operacji w procesie kodowania danych. Jeśli klucz będzie generowany według zbyt oczywistych reguł, będzie on łatwy do odgadnięcia, a co za tym idzie nasze sekretne dane łatwe do odkodowania. Dlatego też pseudolosowa generacja kluczy odbywać się musi z wykorzystaniem dedykowanych temu celowi narzędzi. Nie jest w tym przypadku akceptowalne skorzystanie ze zwykłego generatora jakim jest DBMS_RANDOM. Narzędziem znacznie lepiej spełniającym to zadanie jest funkcja RANDOMBYTES znajdująca się w pakiecie DBMS_CRYPTO. Jedynym parametrem tej funkcji jest długość generowanego klucza.

Algorytmy kodowania

Kodowanie byłoby mało skuteczne, gdyby było realizowane według jednego, znanego powszechnie algorytmu. Dlatego też pakiet DBMS_CRYPTO oferuje nam możliwość kodowania z użyciem kilku algorytmów skutkujących różną długością klucza. I tak do dyspozycji mamy:

  • Encrypt_des z kluczem o długości 56 znaków;
  • Encrypt_3des z kluczem o długości 168 znaków;
  • Encrypt_3des_2key z kluczem o długości 112 znaków;
  • Encrypt_aes128 z kluczem o długości 128 znaków;
  • Encrypt_aes192 z kluczem o długości 192 znaków;
  • Encrypt_aes256 z kluczem o długości 256 znaków.

Tak więc, aby na przykład wywołać kodowanie z użyciem 128 znakowego klucza z użyciem algorytmu Advanced Encryption Standard (AES), nasz kodujący pakiet wywołać należy w następujący sposób:
DBMS_CRYPTO.ENCRYPT_AES128

W doborze algorytmów kodowania i towarzyszącej im długości kluczy zachować warto odrobinę rozsądku. Jest bowiem rzeczą oczywistą, że zdefiniowanie użycia dłuższego klucza zmniejsza prawdopodobieństwo jego złamania. Z drugiej jednak strony, takie działanie zwiększa obciążenie maszyny związane z realizacją operacji kodowania i rozkodowywania danych. Przy odrobinie niekontrolowanego szaleństwa możemy zatem łatwo tak zabezpieczyć nasze dane, że staną się one niedostępne – nie tyle z powodu skutecznego szyfrowania, co z powodu drastycznego spadku wydajności bazy danych i czasu odpowiedzi nieakceptowanego przez użytkowników aplikacji.

Zarządzanie kluczami

Generowanie kluczy nie stanowi zwykle szczególnego wyzwania. Prawdziwym problemem jest zarządzanie zbiorem wygenerowanych kluczy. Łatwo uzmysłowić sobie ten problem poszukując w zimny i słotny wieczór w pęku kluczy tego właśnie, którym będziemy w stanie otworzyć furtkę, aby przed poszukiwaniem klucza do drzwi wejściowych przynajmniej schronić się pod zadaszeniem ganku. Na szczęście, analogicznie jak w przypadku kluczy do zamków mechanicznych, klucze kodowania służą zarówno do kodowania jak i rozkodowywania danych. W przeciwnym wypadku mogłoby być ich wszakże dwa razy więcej! Ta z początku optymistycznie brzmiąca wiadomość szybko przeistoczyć się może w zagrożenie. Zabezpieczenie dostępu do kluczy jest bowiem gwarancją powodzenia całej operacji. Zabezpieczenie to w dodatku musi być jednocześnie skuteczne i mało uciążliwe dla użytkowników aplikacji.

Innym elementem zabezpieczania kluczy jest podjęcie decyzji o sposobie ich składowania. Tu dość intuicyjnie mamy do wyboru trzy opcje:

(1) Klucze mogą być składowane w specjalnie do tego celu przeznaczonej tablicy bazy danych. Właścicielem tej tablicy raczej nie powinien być właściciel aplikacji korzystającej z kodowanych danych. (2) Klucze mogą być składowane poza bazą, w pliku dyskowym na dowolnym zasobie dyskowym, dostępnym dla użytkowników danych. Konfiguracja ta umożliwia odcięcie dostępu do kluczy kodowania administratorom bazy, co może okazać się atutem, na przykład w sytuacji outsourcowania usług administrowania bazą. (3) Trzecim – wydaje się najbardziej niebezpiecznym rozwiązaniem – jest powierzenie opieki nad kluczami użytkownikom aplikacji. W tym przypadku nie zajmujemy się sposobem przechowywania kluczy przez użytkowników, być może zostaną one zdeponowane w sejfie, a może tylko zapisane na przyklejonej do monitora żółtej karteczce.

Portfel kluczy

Preferowanym i zdecydowanie najbardziej wspieranym przez mechanizmy bazodanowe sposobem przechowywania jest plik umieszczony na zewnątrz bazy. Plik taki nazywamy portfelem kluczy. Prześledźmy zatem procedurę tworzenia takiego portfela.

Tworzenie portfela kluczy rozpoczynamy od określenia jego lokalizacji. Jeśli nie zmienimy nic w domyślnych parametrach instalacji, portfel kluczy utworzony zostanie w katalogu $ORACLE_BASE/admin/$ORACLE_SID/wallet. Zmiana tej lokalizacji możliwa jest po dokonaniu edycji pliku sqlnet.ora, który – jak wszyscy wiedzą, znajduje się w katalogu $ORACLE_HOME/network/admin. Konieczne jest w takim przypadku dopisanie do tego pliku sekcji wskazującej na inny katalog:
ENCRYPTION_WALLET_LOCATION =
(SOURCE=
(METHOD=file)
(METHOD_DATA=
(DIRECTORY=/inny_katalog)))

Mając już jednoznaczną wiedzę o lokalizacji portfela, warto od razu zadbać o to, by katalog ten objęty był regularnymi backupami.

Teraz możemy przystąpić do aktu kreacji. Ponieważ jest to operacja na poziomie systemu bazy danych, właściwą do tego celu komendą będzie ALTER SYSTEM. Celem naszych działań będzie stworzenie portfela, ustanowienie hasła dostępu do składowanych tam zasobów i udostępnienie portfela dla procedur TDE:
alter system set encryption key
authenticated by "haslo";

Warto pamiętać, że w hasłach do portfela rozróżniane są duże i małe litery, a tekst hasła musi być oznaczony podwójnym cudzysłowem. Na plus systemu zabezpieczeń zaliczyć można również to, że polecenie ALTER SYSTEM nie odnotuje w logach bazy brzmienia ustanowionego hasła „żywym tekstem”. Teraz musimy tylko otworzyć portfel i gotowe:
alter system set encryption wallet open authenticated by "haslo";

Operację otwierania portfela wykonywać będziemy musieli tyle razy, ilekroć restartować będziemy instancję bazy. Można mechanizm ten uznać za uciążliwy. Można również potraktować to, jako jeszcze jedno zabezpieczenie danych. Nie jest bowiem możliwe odkodowanie zaszyfrowanych danych bez uprzedniego otwarcia portfela. A otwarcie portfela wymaga znajomości hasła. Oczywiście naturalnym jest, że istnieje również komenda zamykająca dostęp do portfela kluczy:
alter system set encryption wallet close;

Ale tu – już nieco niekonsekwentnie – znajomość hasła nie jest wymagana.

Kodowanie danych w praktyce

Mając już przygotowany portfel kluczy i posiadając niezbędną do jego obsługi wiedzę, możemy przystąpić do szyfrowania danych. Jak już ustaliliśmy wcześniej, szyfrowaniu poddawana będzie wybrana kolumna jednej z tabel. Musimy zatem przede wszystkim poinformować o naszej decyzji bazę danych. Zakładając, że nasza tabela ma tylko dwie kolumny:
SQL> desc account
Name Null? Type
------------------- ------------------- -------------------
ACCOUNT_NO NOT NULL NUMBER
NAME NOT NULL VARCHAR2(30)

Sprawmy aby kolumna NAME wskazana była jako szyfrowana:
alter table account modify (name encrypt);

Polecenie to przede wszystkim generuje klucz kodowania dla tej tabeli. Oznacza to ni mniej ni więcej tyle, że każda kolejna szyfrowana kolumna tej tabeli używać będzie dokładnie tego samego klucza. Po drugie, wykona to co najważniejsze, czyli zakoduje informacje zapisane we wskazanej kolumnie. W poleceniu tym możliwe jest również wskazanie konkretnego algorytmu, jakim dane zostaną zakodowane. Domyślnie do szyfrowania danych wykorzystywany jest algorytm AES wykorzystujący 192 bitowy klucz szyfrowania. Jeśli chcielibyśmy dla przykładu zaszyfrować tabelę ACCOUNT algorytmem wykorzystującym klucz 128 bitowy, musimy życzenie to wyrazić w formie polecenia:
alter table account modify (name encrypt using 'AES128');

Jak teraz będzie wyglądała struktura naszej „szyfrowanej tablicy”? Sprawdźmy:
SQL> desc account
Name Null? Type
------------------- ------------------- -------------------
ACCOUNT_NO NOT NULL NUMBER
NAME NOT NULL VARCHAR2(30) ENCRYPT

Jeśli chcielibyśmy wyszukać wszystkie szyfrowane kolumny w naszej bazie danych, informację taką znajdziemy w perspektywie DBA_ENCRYPTED_COLUMNS. Jeśli natomiast z jakichś powodów dojdziemy do wniosku, że szyfrowanie wybranych danych nie jest nam już potrzebne, możemy je z równą łatwością odwołać poleceniem:
alter table account modify (name decrypt);

W tym miejscu pamiętać należy że kodowanie danych powoduje nie tylko wzrost zapotrzebowania na moc obliczeniową podczas zapisu i odczytu, wywołany koniecznością dokonania transformacji danych. Spadek wydajności bazy danych będzie spowodowany również zmniejszeniem się wydajności mechanizmów indeksowania danych w szyfrowanych tabelach. Jeśli kolumna szyfrowana jest jednocześnie kolumną indeksowaną, użycie indeksu do realizacji zapytania może być mniej efektywne, niż regularne przeszukanie całej tabeli.

Szyfrowanie w obliczu masowego przepływu danych

Jakkolwiek dane szyfrowane byłyby ważne, domyślne działanie zastosowanych w Oracle 10g mechanizmów Data Pump może doprowadzić do zabezpieczeniowej katastrofy. Jeśli użyjemy bowiem mechanizmu EXPDP w stosunku do tabeli posiadającej zaszyfrowane kolumny, w odpowiedzi uzyskamy niestety plik tekstowy, podający nasze tajne dane w formie odkodowanej. Wprawdzie zostaniemy o tym poinformowani stosownym komunikatem, ale sama świadomość tego faktu niewiele zmienia:
$ expdp arup/arup tables=account
ORA-39173: Encrypted data has been stored unencrypted in dump file set.

Pewnego rodzaju obejściem tego problemu może być założenie hasła na tworzony podczas eksportu danych plik dyskowy poprzez użycie parametru ENCRYPTION_PASSWORD. Polecenie EXPDP będzie miało wtedy postać:
$ expdp kuku/kuku ENCRYPTION_PASSWORD=inne_haslo tables=account

I analogicznie uruchomienie importu tak utworzonego pliku będzie wymagało składni:
$ impdp kuku/kuku ENCRYPTION_PASSWORD= inne_haslo tables=account table_exists_action=replace

A teraz trochę bardziej skomplikowanie

Wszystko, co jest uproszczone poleceniami SQL, można oczywiście skomplikować do postaci kodu PL/SQL. Rozważmy zatem najbliższy naszym sercom pomysł, czyli składowanie kluczy wewnątrz bazy danych. Zakładając, że tablica poddawana kodowaniu będzie nosiła nazwę ACCOUNT, szyfrowana kolumna to NAME, a klucz będzie generowany oddzielnie dla każdego wiersza tej tabeli, to struktura tablicy przechowującej dane po zaszyfrowaniu może wyglądać następująco:
SQL> desc account_enc
Name Null? Type
------------------- ------------------- -------------------
ACCOUNT_NO NOT NULL NUMBER
NAME_ENC RAW(2000)

gdzie ACCOUNT_NO jest kluczem głównym tabeli ACCOUNT.

Tabela przechowująca klucze służące do szyfrowania i deszyfrowania danych, w tym przypadku może mieć postać:
SQL> desc account_keys
Name Null? Type
------------------- ------------------- -------------------
ACCOUNT_NO NOT NULL NUMBER
KEY NOT NULL RAW(2000)

Teraz wystarczy tylko połączyć te trzy tabele jedną perspektywą VW_ACCOUNT, która umożliwi nam wgląd w treść zakodowanych danych:
1 create or replace view
2 vw_account
3 as
4 select
5 m.account_no as account_no,
6 cast (
7 get_dec_val (e.name_enc, k.key)
8 as varchar2(20)) as name
9 from
10 account m,
11 account_enc e,
12 account_keys k
13 where
14 k.account_no = e.account_no
15 and m.account_no = e.account_no;

Dla dalszego ukrycia mechanizmów szyfrujących możemy utworzyć publiczny synonim dla perspektywy vw_account nadający jej „nazwę” ACCOUNT. Teraz potrzebujemy jeszcze narzędzi umożliwiających wykonywanie podstawowych operacji na szyfrowanych danych. W tym celu perspektywę VW_ACCOUNT wyposażymy w trigger z klauzulą INSTEAD OF.
1 create or replace trigger io_vw_acc
2 instead of insert or update on
3 vw_account
4 for each row
5 declare
6 l_key raw(2000);
7 begin
8 if (inserting) then
9 l_key := dbms_crypto.randombytes (128);
10 insert into account
11 (account_no,name)
12 values
13 (
14 :new.account_no,
15 :new.name
16 );
17 insert into account_enc
18 (account_no, name_enc)
19 values
20 (
21 :new.account_no,
22 get_enc_val (
23 :new.name,
24 l_key )
25 );
26 insert into account_keys
27 (account_no, key)
28 values
29 (
30 :new.account_no,
31 l_key
32 );
33 else
34 select key
35 into l_key
36 from account_keys
37 where account_no = :new.account_no;
38 update account
39 set name = :new.name
40 where account_no = :new.account_no;
41 update account_enc
42 set acc_name_enc =
43 get_enc_val (:new.name, l_key)
44 where account_no = :new.account_no;
45 end if;
46 end;

Tytułem podsumowania

Bez względu na to, jaki pomysł na szyfrowanie danych wybierzemy pamiętać trzeba, że mechanizm ten musi być naprawdę niezawodny. Nie ma nic bardziej irytującego niż dane zaszyfrowane tak dobrze, że nie da się ich odszyfrować 🙂 …