2021-12-02 / Bartłomiej Kurek
Proste szyfrowanie danych przy użyciu OpenSSL

Czasami mamy potrzebę zaszyfrowania danych przy użyciu hasła, bez użycia rozbudowanych mechanizmów oraz generowania i przechowywania plików kluczy.
Możemy to osiągnąć przy użyciu komendy openssl.

Aspekty techniczne, solenie, liczba iteracji

OpenSSL dostarcza na subkomendę "enc":

$ openssl enc --help

Do szyfrowania plików musimy przekazać argumenty:
- algorytmu szyfrowania (w przykładzie aes-256-cbc)
- algorytmu do tworzenia klucza z hasła (key derivation algorithm, w przykładzie sha512)
- nazwy pliku wejściowego
- nazwy pliku wyjściowego

Dla bezpieczeństwa możemy użyć opcji pbkdf2 (Password-Based Key Derivation Function 2)1.
W skrócie: PBKDF2 ogranicza prędkość wykonania ewentualnych prób odgadnięcia hasła przez osoby niepowołane.
Do mechanizmu PBKDF2 możemy również przekazać liczbę iteracji hasła (opcja "-iter") przy wytwarzaniu z niego klucza. Opcja "-iter" automatycznie powoduje użycie PBKDF2, a im wyższa liczba iteracji, tym dłuższy czas potrzebny jest do przeprowadzenia ataku metodą "brute force".
W przykładzie użyję również opcji "-salt", która powoduje użycie w funkcji wytwarzania klucza dodatkowych danych losowych. Tzw. "solenie" uniemożliwia porównywanie znanych skrotów funkcji algorytmu, dzięki czemu zabezpieczamy się przed atakami wykorzystujące wcześniej wyliczone skróty (hashe).
Samo "solenie" można przedstawić np. generując dwa różne "solone" hashe z tych samych danych wejściowych (hasła), W poniższym przykładzie używam w obu przypadkach tego samego hasła "test".

$ openssl passwd -apr1
Password: 
Verifying - Password: 
$apr1$KgSzTmbv$/wii3lmslQDExNe/8cUhD0
$ openssl passwd -apr1
Password: 
Verifying - Password: 
$apr1$dqhTLjCl$LKFkMSVPX08kAVmYrCeuQ1

Użycie tego samego hasła wydało mimo wszystko dwa różne wyniki:
- $apr1$KgSzTmbv$/wii3lmslQDExNe/8cUhD0
- $apr1$dqhTLjCl$LKFkMSVPX08kAVmYrCeuQ1

W obu wynikach widzimy inną "sól".
- $apr1$KgSzTmbv$/wii3lmslQDExNe/8cUhD0
- $apr1$dqhTLjCl$LKFkMSVPX08kAVmYrCeuQ1

Jeśli nie używamy "soli", to funkcja skrótu (md5, sha256, sha512, itd) zawsze da identyczny wynik
dla identycznych danych wejściowych.
Przykład w języku Python:

>>> import hashlib
>>> hashlib.sha256(b"test").hexdigest()
'9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'
>>> hashlib.sha256(b"test").hexdigest()
'9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'

Jeśli atakujący posiadałby bazę danych z już wyliczonymi skrótami popularnych haseł, to - jeśli skrót naszego hasła byłby w tej bazie danych - atakujący mógłby w ogóle pominąć etap szyfrowania i porównać jedynie gotowe skróty.
Przykładowo - jeśli algorytm to sha256 i hash '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', to oznacza to, iż danymi wejściowymi był ciąg znaków "test". Jeśli atakujący uzyska dostęp do nieposolonych haszy, to wykorzystując gotowe bazy danych haszy, mógłby po prostu sprawdzić jakie hasło daje taki hash. Solenie zabezpiecza nas właśnie przed takim scenariuszem.

Zatem jeśli mamy dodatkowe losowe, to wyniki są różne, a liczba iteracji to tak naprawdę liczba hashowań kolejno otrzymywanych wyników.
Przykład - skrót funkcji sha256 otrzymany przez nas wyżej, możemy ponownie zahashować:

>>> hashlib.sha256(b"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08").hexdigest()
'7b3d979ca8330a94fa7e9e1b466d8b99e0bcdea1ec90596c0dcc8d7ef6b4300c'

W wyniku otrzymujemy kolejny hash, z poprzedniego hasha. Do tego służy właśnie opcja "-iter".
Taki "blockchain".

Szyfrowanie pliku

Dla przykładu stworzę plik "passwords.txt" i umieszczę w nim jakieś hasła.

$ cat > passwords.txt
admin:s3cret
user1:VeryS3cret

Użyję teraz komendy openssl, która stworzy nowy plik w wersji zaszyfrowanej. Plik wejściowy to oczywiście passwords.txt, a plik wyjściowy będzie miał nazwę "passwords.txt.enc". Liczba iteracji to 100000.

$ openssl enc -aes-256-cbc -md sha512 -pbkdf2 -iter 100000 -salt -in passwords.txt -out passwords.txt.enc
enter aes-256-cbc encryption password:
Verifying - enter aes-256-cbc encryption password:

Po stworzeniu zaszyfrowanego pliku sprawdzam czym on jest:

$ file passwords.txt.enc 
passwords.txt.enc: openssl enc'd data with salted password

Jest to zatem plik binarny z zaszyfrowanymi danymi:

$ less passwords.txt.enc 
"passwords.txt.enc" may be a binary file.  See it anyway? 

A jego zaszyfrowana zawartość wygląda tak:

$ cat passwords.txt.enc 
Salted__�>�c���$���'�{/�T&�I�%�����J��G���/

Usuwam zatem oryginalny, niezaszyfrowany plik passwords.txt i pozostawiam jedynie plik w wersji zaszyfrowanej.

$ rm -v passwords.txt
removed 'passwords.txt'

Odszyfrowanie pliku

Posiadamy zatem jedynie plik zaszyfrowany i znamy hasło.
Możemy zatem odszyfrować zawartość pliku. Komenda openssl wygląda praktycznie tak samo, jedyne różnice to:
- użycie opcji "-d" (Decrypt)
- zmiana argumentów plików: plikiem wejściowym jest passwords.txt.enc, a wyjściowym passwords.txt
- brak opcji "-salt", choć jej obecność w niczym nie przeszkadza

$ openssl enc -d -aes-256-cbc -md sha512 -pbkdf2 -iter 100000  -in passwords.txt.enc -out passwords.txt
enter aes-256-cbc decryption password:
$ cat passwords.txt
admin:s3cret
user1:VeryS3cret

Uwagi

Argumenty programu (algorytm, liczba iteracji)

Stosując to rozwiązanie musimy oczywiście pamiętać:
- jakiego algorytmu szyfrowania użyliśmy
- jakiego algorytmu użyliśmy dla funkcji wyliczania klucza
- liczbę iteracji

Przykładowo - podajac błędną liczbę iteracji, nie będziemy w stanie odszyfrować pliku:
Przy szyfrowaniu podaję liczbę iteracji "100000":

$ openssl enc -aes-256-cbc -md sha512 -pbkdf2 -iter 100000 -salt -in passwords.txt -out passwords.txt.enc
enter aes-256-cbc encryption password:
Verifying - enter aes-256-cbc encryption password:

A probuję odszyfrować podajć liczbę 200000:

$ openssl enc -d -aes-256-cbc -md sha512 -pbkdf2 -iter 200000 -salt -in passwords.txt.enc -out passwords.txt
enter aes-256-cbc decryption password:
bad decrypt
140458814309760:error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt:../crypto/evp/evp_enc.c:610:

To się nie powiedzie. Musimy znać liczbę iteracji użytych na etapie szyfrowania.

Inne algorytmy

Istnieją mocniejsze i słabsze algorytmy szyfrowania. Ich listę możemy otrzymać komendą "openssl ciphers".
Przykłady:

$ openssl ciphers MEDIUM
TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:DHE-RSA-SEED-SHA:DHE-DSS-SEED-SHA:ADH-SEED-SHA:SEED-SHA

Analogicznie możemy wyświetlić listę algorytmów najmocniejszych używając komendy:

$ openssl ciphers HIGH