2021-12-08 / Bartłomiej Kurek
LUKS - pliki jako dyski i szyfrowane kontenery danych

Video - podstawy (pliki nieszyfrowane)

Video: MP4, 9.0M, 1920x1080. Duration: 00:06:04 Link

Repozytorium kodu użytego w artykule

Repozytorium git na platformie GitHub.

Omówienie

Wszystko jest plikiem

W świecie UNIX mówi się: "wszystko jest plikiem".
Idea skryta w tej lakoniczniej frazie na pierwszy rzut może nie wydawać się tak głęboka, jaką w rzeczywistości jest. Sprowadzenie zasobów i urządzeń do pojęcia pliku pozwala nam operować jednakowo (interfejs I/O - read/write) na każdym z typów zasobu - bez względu na to czy to plik z danymi na dysku, czy plik urządzenia.
Przykładowo: możemy posłuchać jak brzmi napis "hello world" wrzucony "bezpośrednio" na kartę dźwiękową (w moim systemie to plik urządzenia /dev/dsp1:

$ ls -la /dev/dsp1
crw-rw----+ 1 root audio 14, 19 Nov 10 12:08 /dev/dsp1

A zatem (jeśli urządzenie dźwiękowe nie jest zajęte):

$ echo "hello world" > /dev/dsp1

Możemy również w ten sposób "odegrać" dowolny plik lub strumień danych:

$ man man | tee /dev/dsp1

Można wypisać zawartość pamięci na urządzenie dźwiękowe (jako root):

# cat /dev/mem > /dev/dsp1

Albo posłuchać tabeli partycji MBR (jako root):

# tail -c 512 /dev/sda > /dev/dsp1

W powyższych przykładach użyłem jako wyjścia urządzenia karty dźwiękowej, ale wyjście możemy przekierować gdzie tylko zechcemy. Wszystko jest plikiem.

Pliki jako dyski/partycje

W podobny sposób możemy ze zwykłych plików zrobić "dyski", dzielić je na partycje, formatować, itd.
Ktoś mógłby zapytać: po co? Najszybciej do głowy przychodzą scenariusze:
1. plik wymiany (swap)
2. plik dysku w środowisku testowym (np. testujemy w dockerze co nasz program robi kiedy na dysku braknie miejsca)
3. szyfrowanie danych: używamy tych samych mechanizmów co przy zwykłych dyskach (LUKS/cryptstetup).
4. automatyzacja, np. plik jako obraz maszyny wirtualnej/kontenera/wdrożenia

Wszystkie powyższe scenariusze wielokrotnie realizowałem.
W tym artykule omówimy pracę z dyskami i szyfrowanie danych, a zamiast dysków/partycji użyjemy po prostu plików. Wstęp do tego zacznę od anegdoty. Otóż, kiedyś podczas pracy w jednym projekcie zauważyłem, że mój współpracownik restartuje komputer. Wywiązał się nastepujący dialog:
- Restart?
- Tak, sprawdzam co się stanie z programem kiedy braknie miejsca na bazę danych.
- A dlaczego nie zrobisz sobie tego na pliku?
(tutaj podsyłam mini skrypt)

$ dd if=/dev/zero of=disk.img bs=1M count=32
$ mkfs.ext3 disk.img
$ sudo mount disk.img /mnt/disk
# mount | grep disk.img
# ls -la /mnt/disk
  • Gdzie nauczyłeś się tak walczyć Wiedźminie?!

Analiza: przykład pliku nieszyfrowanego jako dysku

dd - disk dump (tworzymy plik wypełniony zerami)

  • Tworzymy wypełniony zerami plik o rozmiarze 32mb.
    • dd to "disk dump",
    • if to "input file"
    • of to "output file"
    • bs to "bytes"
    • count to liczba powielenia podanej wielkości "bytes".
/tmp $ dd if=/dev/zero of=disk.img bs=1M count=32
32+0 records in
32+0 records out
33554432 bytes (34 MB, 32 MiB) copied, 0.106861 s, 314 MB/s

(Przy okazji - wysłanie w Linux do programu dd sygnału USR1 spowoduje wyświetlenie "progresu". W OSX/BSD wystarczy wcisnąć Ctrl+T. Możemy również użyć w potoku programu "pv").

mkfs - formatujemy nasz plik

Poprzednia komenda stworzyła nam plik, ja nazwałem go disk.img (nazwa nie ma znaczenia).
Tworzę system plików (wybrałem format ext3) w tym pliku ("formatuję" go).

/tmp $ /usr/bin/mkfs.ext3 disk.img
mke2fs 1.46.4 (18-Aug-2021)
Discarding device blocks: done
Creating filesystem with 32768 1k blocks and 8192 inodes
Filesystem UUID: cca2accd-c630-4d82-82ea-0b9b2f525a08
Superblock backups stored on blocks:
    8193, 24577

Allocating group tables: done
Writing inode tables: done
Creating journal (4096 blocks): done
Writing superblocks and filesystem accounting information: done

Sprawdźmy czym teraz jest plik disk.img:

/tmp $ file disk.img
disk.img: Linux rev 1.0 ext3 filesystem data, UUID=cca2accd-c630-4d82-82ea-0b9b2f525a08 (needs journal recovery) (large files)

Możemy też sprawdzić ten "dysk" programem fdisk (lub cfdisk, cgdisk):

/tmp $ fdisk -l disk.img
Disk disk.img: 32 MiB, 33554432 bytes, 65536 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes

"Dysk" jak dysk.

mount - montujemy "dysk"

Do zamontowania dysku potrzebujemy uprawnień administratora. Używam tutaj sudo, a dysk montuję w istniejącym katalogu (tutaj /mnt/disk)

/tmp $ sudo mount disk.img /mnt/disk

Dysk został zamontowany, możemy to sprawdzić w listingu komendy "mount":

/tmp $ mount | grep disk.img
/tmp/disk.img on /mnt/disk type ext3 (rw,relatime)

mkdir, touch - tworzymy katalogi i pliki

Możemy zatem normalnie działać na naszym zasobie. Dla przykładu stworzę kilka katalogów i plików, a następnie wyświetlę całe drzewo. Składnia {a,b,c} to tzw. "brace expansion", które jest rozszerzeniem używanej przeze mnie powłoki bash (non POSIX, man bash).

$ sudo mkdir -p /mnt/disk/{a,b,c}/{1,2,3}
$ sudo touch /mnt/disk/{a,b,c}/{1,2,3}/file.txt
$ tree /mnt/disk/
/mnt/disk/
├── a
│   ├── 1
│   │   └── file.txt
│   ├── 2
│   │   └── file.txt
│   └── 3
│       └── file.txt
├── b
│   ├── 1
│   │   └── file.txt
│   ├── 2
│   │   └── file.txt
│   └── 3
│       └── file.txt
├── c
│   ├── 1
│   │   └── file.txt
│   ├── 2
│   │   └── file.txt
│   └── 3
│       └── file.txt

umount - odmontowujemy (oraz sprawdzamy)

Na koniec odmontuję nasz "dysk", zamontuję ponownie i sprawdzę czy pliki wciąż na nim są.
Umount:

$ sudo umount /mnt/disk
$ mount | (grep -qs disk.img && echo "MOUNTED" || echo "NOT MOUNTED")
NOT MOUNTED

Mount:

$ sudo mount disk.img /mnt/disk/
$ mount | (grep -qs disk.img && echo "MOUNTED" || echo "NOT MOUNTED")
MOUNTED

Sprawdzamy pliki:

$ find /mnt/disk/ -type f -name 'file.txt' | wc -l
9

Szyfrowane "dyski" ("encrypted vault")

Pójdźmy dalej. Skoro możemy używać plików jako dysków, nic nie stoi na przeszkodzie aby używać ich jako dysków szyfrowanych. Co więcej - nie potrzebujemy do tego żadnych dedykowanych programów. W systemie Linux mamy gotową implementację szyfrowania urządzeń blokowych: (LUKS).
Zatem do dzieła. Dla czytelności stworzę nowy plik dysku i przygotuję go do użycia z LUKS.

Tworzę nowy plik (dla przykładu o rozmiarze 64mb), nazywam go "my-secret-vault.disk".

/tmp $ dd if=/dev/zero of=my-secret-vault.disk bs=1M count=64
64+0 records in
64+0 records out
67108864 bytes (67 MB, 64 MiB) copied, 0.0407905 s, 1.6 GB/s

luksformat

Formatuję go jako ext4 przy użyciu narzędzia luksformat (wymaga uprawnień administratora).
luksformat zapyta o hasło i zajmie się szyfrowaniem.

/tmp $ sudo luksformat --help
luksformat - Create and format an encrypted LUKS device
Usage: luksformat [-t <file system>] <device> [ mkfs options ]

/tmp $ sudo luksformat -t ext4 my-secret-vault.disk
Creating encrypted device on my-secret-vault.disk...

WARNING!
========
This will overwrite data on my-secret-vault.disk irrevocably.

Are you sure? (Type 'yes' in capital letters): YES
Enter passphrase for my-secret-vault.disk:
Verify passphrase:
Please enter your passphrase again to verify it
Enter passphrase for my-secret-vault.disk:
mke2fs 1.46.4 (18-Aug-2021)
Creating filesystem with 12288 4k blocks and 12288 inodes

Allocating group tables: done
Writing inode tables: done
Creating journal (1024 blocks): done
Writing superblocks and filesystem accounting information: done

Zamiast "luksformat" możemy również użyć "cryptsetup luksFormat". W pierwszym przypadku do komendy luksFormat możemy od razu przekazać argument systemu plików (tutaj ext4), a w przypadku drugim będziemy musieli dokonać formatowania przed pierwszym użyciem, po otwarciu/zmapowaniu zasobu (poniżej).

cryptsetup luksOpen

"Otwieram" dysk.

/tmp $ sudo cryptsetup luksOpen my-secret-vault.disk my-encrypted-disk
Enter passphrase for my-secret-vault.disk:

cryptsetup to program, który wspiera mapowanie szyfrowanych dysków (device mapping). Kiedy mamy zaszyfrowane partycje i wpisy o nich umieszczamy w /etc/crypttab, to właśnie cryptsetup odpowiada za ich otwarcie przy starcie systemu. luksOpen służy do otwarcia urządzenia typu LUKS (cryptsetup wspiera również inne mechanizmy /man cryptsetup/).

Dysk/plik został zmapowany do nazwy "my-encrypted-disk", którą podałem powyżej. W systemie pojawia się nowe urządzenie blokowe:

/tmp $ ls -la /dev/mapper/my-encrypted-disk
lrwxrwxrwx 1 root root 7 Dec  8 03:26 /dev/mapper/my-encrypted-disk -> ../dm-2

/dev/mapper/my-encrypted-disk to link do /dev/mapper/dm-2 (mam inną partycję już zmapowaną do dm-1, stąd następna wolna liczba to "2". Aby pominąć niedogodność enumeracji, system mapuje je właśnie do wygodnych nazw, które możemy określić (w przykładze: "my-encrypted-disk").

$ file $(readlink -f /dev/mapper/my-encrypted-disk)
/dev/dm-2: block special (253/2)

Jak widać - system rozpoznaje nasz zaszyfrowany plik jako urządzenie blokowe.

mount

Stworzę osobny katalog (/mnt/encrypted), w którym zamontuję zmapowany dysk:

/tmp $ sudo mkdir /mnt/encrypted
/tmp $ sudo mount /dev/mapper/my-encrypted-disk /mnt/encrypted/

Potwierdźmy, że został zamontowany:

/tmp $ mount | grep my-encrypted-disk
/dev/mapper/my-encrypted-disk on /mnt/encrypted type ext4 (rw,relatime)

A zatem możemy już na tym dysku wykonywać zwykłe operacje. Stworzę na nim plik "passwords.txt".

/tmp $ echo "Wiedzmin:TheWitcher" | sudo tee /mnt/encrypted/passwords.txt
Wiedzmin:TheWitcher
/tmp $ cat /mnt/encrypted/passwords.txt
Wiedzmin:TheWitcher

umount, luksClose

Kiedy już wykonałem swoją pracę z dyskiem, mogę go odmontować:

/tmp $ sudo umount /mnt/encrypted

Oczywiście samo odmontowanie nie powoduje zamknięcia całego szyfrowanego kontenera w device mapperze.

/tmp $ ls -la /dev/mapper/my-encrypted-disk
lrwxrwxrwx 1 root root 7 Dec  8 03:26 /dev/mapper/my-encrypted-disk -> ../dm-2

W celu zamknięcia kontenera wykonujemy cryptsetup luksClose. Kolejne otwarcie kontenera będzie znów wymagało podania hasła.

A zatem, luksClose:

/tmp $ sudo cryptsetup luksClose my-encrypted-disk

Po zamknięciu dysk nie widnieje już w device mapperze:

/tmp $ ls -la /dev/mapper/my-encrypted-disk
ls: cannot access '/dev/mapper/my-encrypted-disk': No such file or directory

Sprawdźmy zatem ponowne otwarcie kontenera:

/tmp $ sudo cryptsetup luksOpen my-secret-vault.disk my-encrypted-disk
Enter passphrase for my-secret-vault.disk:

Faktycznie - do otwarcia tego dysku jest wymagane hasło.

Automatyzacja

Definicję szyfrowanych partycji, czy właśnie takich szyfrowanych plików możemy zawrzeć w /etc/crypttab.
Przykładowo, moja partycja /home w moim systemie jest zaszyfrowana, a przy uruchomieniu system pyta o hasło.

$ cat /etc/crypttab
home_crypt UUID=15f202d8-cf07-4e12-5a3c-d8c0fe46a126 none luks,discard

Jeśli przy starcie systemu nie podam hasła (lub kilkukrotnie podam błędne), to system wystartuje bez montowania tej partycji. W takim wypadku można zawsze zamontować taką partycję ręcznie (cryptsetup luksOpen, mount).
Jeśli hasło jest poprawne, to system mapuje to urządzenie, a dalej dzieje się to co w przypadku każdej innej partycji - jeśli występuje w /etc/fstab, to zostaje zamontowana:

$ grep home /etc/fstab
/dev/mapper/home_crypt    /home    ext4    defaults    0  2

W przypadku zaszyfrowanych plików - możemy postąpić podobnie, a zamiast UUID partycji podać ścieżkę pliku. Tutaj zwracam uwagę, że ten plik musiałby być dostępny (czyli albo znajduje się na nieszyfrowanej partycji, albo partycja, na której się on znajduje, jest wcześniej odszyfrowana i zamontowana).

Jeśli jednak korzystamy z takich plików w celach bezpieczeństwa i nie chcemy zostawiać ich otwartych, to całość można sprowadzić do trywialnych skryptów. Przykładowo:

Montowanie:

#!/bin/sh

FILE=/home/me/encrypted-1.disk

if mount | grep -qs /dev/mapper/encrypted-1; then
    echo "Already mounted"
else
    sudo cryptsetup luksOpen /home/me/encrypted-1.disk encrypted-1 && \
    sudo mount /dev/mapper/encrypted-1 /mnt/encrypted-1
fi

Odmontowanie:

#!/bin/sh

if mount | grep -qs /dev/mapper/encrypted-1; then
    umount /mnt/encrypted-1
    cryptsetup luksClose encrypted-1
fi

Podsumowanie

Wszystko jest plikiem

Ta lakoniczna formułka - wszystko jest plikiem - naprawdę ma w sobie głębię. Sprowadzenie operacji na urządzeniach do interfejsu I/O daje nam niesamowite możliwości. Przy pomocy zwykłych systemowych komend możemy osiągnąć wiele. Nie potrzeba do tego zewnętrznych programów ani interfejsów graficznych. Można to zrobić na laptopie, na serwerze i na routerze (jeśli mamy np. OpenWrt, czyli dysrybucję Linux).
Jest to zatem przenośne pomiędzy dowolnymi systemami Linux.

Zastosowanie block-level encryption

Samo zastosowanie block-level encryption w sposób omówiony powyżej jest bezpieczne, wygodne i łatwe do dostosowania. Wykorzystując pliki jako urządzenia blokowe nie musimy zmieniać układu partycji na naszym dysku fizycznym. Możemy to rozwiązanie zastosować na każdej maszynie z systemem Linux. Nie ma też żadnych ograniczeń, oczywiście poza wymaganiami uprawnień administratora (a to można sobie już skonfigurować poprzez sudo/doas). Skoro na dysku możemy mieć pliki, a pliki traktować jako dyski, to nic nie stoi na przeszkodzie, aby rekurencyjnie zagnieżdżać takie pliki, jeśli tylko mamy taką potrzebę.
Dodatkowo - mając tak wygodne kontenery danych - kopię zapasową możemy wykonać po prostu przez skopiowanie takiego pliku na inną maszynę (np. przy użyciu scp lub rsync). A jeśli na tej maszynie mamy również system Linux, to wszystkie narzędzia do obsługi mamy również na tej maszynie (być może cryptsetup trzeba doinstalować: sudo apt install cryptsetup).

Wiele haseł/kluczy

Oczywiście, powyższe omówienie jedynie przybliża szerokie możliwości tych rozwiązań. Szyfrowane pliki/partycje jako urządzenia blokowe możemy również chronić osobnymi plikami kluczy. LUKS pozwala również na ochronę takiego kontenera przez kilka haseł/kluczy (slots), na wypadek gdybyśmy zapomnieli hasła.
A to się czasem zdarza. Laptopy najczęściej usypiamy, a nie wyłączamy, zatem z czasem zapominamy haseł potrzebnych choćby do zamontowania partycji. Mnie też się kiedyś zdarzyło i pomimo, iż znałem poszczególne części hasła, to nie pamiętałem ich kolejność, wielkości znaków, kombinacji shift/no-shift. Z doświadczenia mogę powiedzieć, że zautomatyzowany (skrypty) bruteforce własnego - w gruncie rzeczy znanego hasła - w przypadku LUKS zajmuje sporo czasu. LUKS w domyślnych ustawieniach (choćby dobór algorytmów szyfrowania) jest bardzo rozsądną opcją, a przy tym wygodną i przenośną pomiędzy maszynami.

Dokumentacja, artykuły

Zainteresowanych dalszymi szczegółami zachęcam do zapoznania się z:
- man 8 cryptsetup oraz dokumentacją na GitLab
- dokumentacją device mapper
- innymi ciekawymi artykułami, np. blog.elcomsoft.com: breaking-luks-encryption.