Szyfrowanie w PostgreSQL

W artykule przedstawię dosyć prosty, ale bardzo często całkiem wystarczający, sposób szyfrowania danych za pomocą standardowych mechanizmów bazy danych PostgreSQL. Szyfrowanie zwiększa bezpieczeństwo przechowywanych danych, na przykład w przypadku utraty nośnika. Ostatnio temat szyfrowania zyskał na aktualności w związku z bliskim rozpoczęciem obowiązywania RODO.

 

Szyfrowanie

Istnieją różne rodzaje szyfrowania:

Jednokierunkowe - stosowane do haseł. Podane przez użytkownika hasło jest szyfrowane i zapamiętywane w bazie danych. Przy weryfikacji hasła system szyfruje podaną wartość i sprawdza, czy jest ona równa zapamiętanej zaszyfrowanej wartości. Niezaszyfrowana wartość hasła nie jest nigdzie zapamiętywana. Użycie losowego ciągu znaków (ang. salt) powoduje, że zaszyfrowanie tego samego tekstu w dwóch różnych procesach, dla tych samych wartości hasła,  da w wyniku różny ciąg znaków, co utrudnia analizę haseł. Nawet gdy zbiór zaszyfrowanych haseł dostanie się w niepowołane ręce.

Dwukierunkowe - stosowane do danych przy użyciu klucza. Szyfrowanie dwukierunkowe dzielimy na symetryczne i asymetryczne. Przy szyfrowaniu symetrycznym używamy tego samego klucza do szyfrowania i odszyfrowywania. Przy szyfrowaniu asymetrycznym używamy pary kluczy – publicznego do szyfrowania danych i prywatnego do odszyfrowania. Szyfrowania asymetrycznego używamy do podpisywania dokumentów.

Moc szyfrowania (można ją określić jako moc obliczeniową potrzebną do złamania szyfru) zależy od użytego algorytmu szyfrowania i długości klucza. Odszyfrowywanie danych jest bardziej czasochłonne niż ich szyfrowanie. Odszyfrowywanie przy szyfrowaniu symetrycznym jest z reguły szybsze od asymetrycznego dla podobnej mocy szyfrowania. Praktycznie nie jest możliwe złamanie współczesnego szyfru bez znajomości klucza potrzebnego do odszyfrowania. Przy znajomości klucza (zakładamy,  że aplikacja „zna” klucz do odszyfrowania) możliwe jest oglądanie i manipulowanie zaszyfrowanymi danymi w aplikacji przez uprawnione osoby. Od strony użytkownika aplikacji szyfrowanie nie powinno być odczuwalne.

Szyfrowanie ma następujące potencjalne wady:

  • zaszyfrowane dane trudniej zweryfikować, że są poprawne – oglądanie zawartości danych należy do standardowych narzędzi administrowania systemem
  • konieczna jest akcja inicjalnego szyfrowania danych
  • odszyfrowywanie danych w locie wiąże się ze spadkiem wydajności w porównaniu z pracą na danych nieszyfrowanych.

 

Generowanie kluczy szyfrujących przy użyciu GnuPG

Aby wygenerować klucze szyfrujące, których będę mógł użyć z PostgreSQL, ściągnąłem program instalacyjny gnupg-w32cli-1.4.22.exe ze strony https://www.gnupg.org/download/. Po instalacji otworzyłem okno poleceń w katalogu, w którym program został zainstalowany.

c:\Program Files (x86)\GNU\GnuPG>gpg --gen-key

Powyższe polecenie generuje parę kluczy: publiczny i prywatny. Następnie w trybie interakcyjnym trzeba odpowiedzieć na parę pytań, w tym podać imię i nazwisko oraz adres mailowy. Gdzie mogłem, wybierałem domyślne wartości:

Please select what kind of key you want:
   (1) RSA and RSA (default)
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
Your selection?
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (2048)
Requested keysize is 2048 bits
Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0)
Key does not expire at all
Is this correct? (y/N) y
You need a user ID to identify your key; the software constructs the user ID
from the Real Name, Comment and Email Address in this form:
    "Heinrich Heine (Der Dichter) <heinrichh@duesseldorf.de>"
Real name: Wojtek Was
Email address: wojtek_was@o2.pl
Comment: moj klucz
You selected this USER-ID:
    "Wojtek Was (moj klucz) <wojtek_was@o2.pl>"
Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
You need a Passphrase to protect your secret key.
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
.+++++
.......+++++
gpg: key DE56D379 marked as ultimately trusted
public and secret key created and signed
 
gpg: checking the trustdb
gpg: 3 marginal(s) needed, 1 complete(s) needed, PGP trust model
gpg: depth: 0  valid:   3  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 3u
pub   2048R/DE56D379 2018-04-14
      Key fingerprint = 9160 2BB8 0924 3E41 9FEE  1EE1 A791 9515 DE56 D379
uid                  Wojtek Was (moj klucz) <wojtek_was@o2.pl>
sub   2048R/B3321202 2018-04-14

Podanie hasła szyfrującego klucz nie jest konieczne. DE56D379, które widać w sekcji „pub” na końcu, to nazwa klucza publicznego, a B3321202 w sekcji „sub”, to nazwa klucza prywatnego. Mamy już wygenerowane klucze, teraz musimy je przekształcić do postaci tekstowej, w której będziemy mogli nimi manipulować.

c:\Program Files (x86)\GNU\GnuPG>gpg -a --export DE56D379 > /Wojtek/public.key
c:\Program Files (x86)\GNU\GnuPG>gpg -a --export-secret-keys B3321202 > /Wojtek/secret.key

-a to skrót od armor, funkcji przekształcającej wartości binarne na tekstowe. Ponieważ będziemy się dalej posługiwali postacią tekstową, w PostgreSQL będziemy musieli użyć funkcji dearmor. Pliki wyeksportowałem do innego katalogu (c:\Wojtek), dlatego, że proces nie ma prawa do zapisu plików w katalogu systemowym c:\Program Files (x86). Wygenerowane pliki wyglądają jak poniżej (plik public.key jest mniejszy):

 

 

 

 

PostgreSQL moduł pgcrypto

PostgreSQL to popularna darmowa relacyjna baza danych. Zawiera ona moduł pgcrypto z funkcjami kryptograficznymi, który zapewnia wygodne wsparcie dla szyfrowania. Następujące funkcje z tego modułu będą nam przydatne:

  • pgp_pub_encrypt – funkcja do szyfrowania danych z użyciem klucza publicznego
  • pgp_pub_decrypt – funkcja do odszyfrowania danych z użyciem klucza prywatnego
  • dearmor - funkcja do przekształcania kluczy binarnych do postaci tekstowej umożliwiająca kopiowanie i wklejanie kluczy.

Powyższych funkcji możemy używać w kwerendach i procedurach SQLowych. Funkcje te współpracują z kluczami w standardzie OpenPGP. Takie klucze właśnie wygenerowaliśmy w poprzednim punkcie.

Pierwszą czynnością będzie instalacja modułu pgcrypto dla naszej bazy danych. Zakładamy, że PostgreSQL w wersji 9.4 zainstalowano w systemie Windows w katalogu C:\PostgreSQL. Wywołujemy w oknie poleceń psql – narzędzie do administrowania bazą danych.  „postgres” to standardowy użytkownik bazy danych o uprawnieniach administracyjnych, a „bazadanych” to nazwa naszej bazy danych. Nazwa naszej bazy danych (aktualnej) to prompt narzędzia psql. Ważne, by moduł pgcrypto zainstalować dla tej konkretnej bazy, której dane chcemy szyfrować.

C:\PostgreSQL\pg94\bin>psql -U postgres -d bazadanych
bazadanych=# CREATE EXTENSION IF NOT EXISTS "pgcrypto" SCHEMA public;

Teraz możemy już używać funkcji do szyfrowania. Najpierw dodamy nową kolumnę na zaszyfrowane nazwisko:

ALTER TABLE daneosobowe ADD COLUMN nazwiskoszyfr bytea;

Bytea to typ w PostgreSQL na dane binarne, odpowiada standardowemu typowi SQL BLOB (BINARY LARGE OBJECT). Teraz wypełnimy nową kolumnę zaszyfrowanymi danymi. Od tego miejsca będę się posługiwał narzędziem pgAdmin III zamiast psql, gdyż wygodniej sobie radzi z łańcuchami znaków zawierającymi wiele linii.

UPDATE daneosobowe SET nazwiskoszyfr = pgp_pub_encrypt(nazwisko, dearmor('-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBFq8+MgBCADxvgzhGQC1MEhBL9ZalrpXWVgm8FLFsTyrc9AJZk1gelwQ96UC
XsXQA4mqFQdUEHCsZMyL/YsMgIKSxHuPDZqC6i+4W14iEwrDz7C5UWP7QZfT+w==
=sExz
-----END PGP PUBLIC KEY BLOCK-----'));

Funkcja pgp_pub_encrypt bierze dwa argumenty: wartość szyfrowaną (tu nazwisko) i klucz szyfrujący – dla wygody lektury nieco go skróciłem. Argument klucz jest wartością binarną, dlatego użyta została funkcja dearmor, która konwertuje klucz w postaci ciągu znaków do postaci binarnej. Do powyższej instrukcji trzeba wkleić całą zawartość wygenerowanego pliku public.key razem ze znakami --- na początku i na końcu tekstu. (Otworzyłem plik public.key Notatnikiem, zrobiłem Ctrl-A, Ctrl-C, a potem wkleiłem do okienka z instrukcją SQL). Na moim laptopie zaszyfrowanie 300 tysięcy nazwisk trwało 50 sekund.Na poniższym ekranie widać, że zaszyfrowane dane są nieczytelne:

 

 

A teraz możemy pobrać odszyfrowane dane i sprawdzić, czy się zgadzają z oryginalnymi:

select nazwisko, pgp_pub_decrypt(nazwiskoszyfr, dearmor('-----BEGIN PGP PRIVATE KEY BLOCK-----
lQOYBFq8+MgBCADxvgzhGQC1MEhBL9ZalrpXWVgm8FLFsTyrc9AJZk1gelwQ96UC
R+6HNM66XsXQA4mqFQdUEHCsZMyL/YsMgIKSxHuPDZqC6i+4W14iEwrDz7C5UWP7
QZfT+w==
=q9qY
-----END PGP PRIVATE KEY BLOCK-----') , 'HASLO') from daneosobowe;

Znów skróciłem klucz, tym razem prywatny, służący do odszyfrowania. Trzeci, opcjonalny parametr funkcji pg_pub_decrypt to hasło, które ustawiliśmy w trakcie generowania kluczy w programie GnuPG. Jeżeli podamy złe hasło, to PostgreSQL zwróci komunikat ERROR: Wrong key or corrupt data. Podobnie, jeżeli użyjemy uszkodzonego tekstu klucza, to dostaniemy błąd ERROR: Corrupt ascii-armor. Na moim laptopie odszyfrowanie 300 tysięcy nazwisk trwało niestety ponad 15 minut. W sensownej aplikacji nie powinno się zdarzyć, żeby naraz pobierane było tysiące danych osobowych. Jak widać z poniższego ekranu, pobranie tysiąca nazwisk trwało niecałe trzy sekundy. Końcowy użytkownik raczej zauważy takie opóźnienie, ale nie powinno ono stanowić większego problemu.

 

 

Na koniec możemy skasować kolumnę nazwisko, zmienić nazwę kolumny nazwiskoszyfr na nazwisko i używać tylko zaszyfrowanych nazwisk.

 

Przechowywanie kluczy

Powyższe rozwiązanie, w którym klucze szyfrujące pojawiają się wprost w instrukcjach SQL UPDATE i SELECT, jest mało praktyczne i niewystarczająco się kojarzy z bezpieczeństwem. Na szczęście PostgreSQL oferuje funkcje systemowe set_config i current_setting, które mogą być użyte do zapamiętywania i odczytywania wartości kluczy użytych do szyfrowania.

SELECT set_config('public.key', '-----BEGIN PGP PUBLIC KEY BLOCK-----
GGceRp7Q5WzWFB31S4P37Wo/QFL6ksaQQnC8M2Fsl1v44h8Swg7KuQ==
=Iq5w
-----END PGP PUBLIC KEY BLOCK-----', false);

Powyższa komenda (do wykonania pod pg Admin III) zapamiętuje klucz jako parametr o nazwie public.key. Ważne, żeby w nazwie parametru użyć kropki, poniważ nazwy bez kropek zarezerwowane są dla systemowych parametrów PostgreSQL. Ostatni parametr mówi, czy nasz public.key ma być dostępny tylko w bieżącej sesji. Oczywiście, jak zwykle, skróciłem ciąg znaków klucza.

SELECT set_config('private.key', '-----BEGIN PGP PRIVATE KEY BLOCK-----
xJAYZx5GntDlbNYUHfVLg/ftaj9AUvqSxpBCcLwzYWyXW/jiHxLCDsq5
=DsMH
-----END PGP PRIVATE KEY BLOCK-----', false);

Analogicznie tworzymy parametr z kluczem prywatnym. Teraz komendy szyfrujące i deszyfrujące, które omówiliśmy powyżej, będą miały postać:

UPDATE daneosobowe SET nazwiskoszyfr = pgp_pub_encrypt(nazwisko, dearmor(current_setting('public.key')));
SELECT nazwisko,  pgp_pub_decrypt(nazwiskoszyfr, dearmor(current_setting('private.key')), 'HASLO') FROM daneosobowe;

Widać, że wywołanie funkcji current_setting zastąpiło tekst kluczy. 

 

Podsumowanie

W artykule omówiłem szyfrowanie wybranych danych za pomocą standardowych narzędzi bazy danych PostgreSQL. Zaprezentowane rozwiązanie ma następujące zalety:

  • niezbyt duży nakład pracy developerskiej
  • rozwiązanie zrealizowane na bazie danych z bardzo małym wpływem na inne części systemu
  • wystarczająca w większości wypadków i konfigurowalna moc szyfrowania
  • niewielki nakład pracy na instalację rozwiązania (zarówno wstępne przetwarzanie, jak i konieczność instalacji dodatkowych komponentów).

Wadą jest możliwe istotne pogorszenie się wydajności systemu.

Ważna kwestia przechowywania kluczy szyfrujących została tylko zasygnalizowana w artykule. Przy pisaniu artykułu używałem wersji 9.4.17 PostgreSQL. Pracowałem na standardowym laptopie z systemem Windows 10.

 

*Skorzystano z grafiki Freepik


Wojtek Wąs

Powrót