Podręczne szyfrowanie danych w .NET

Ten tekst powstał w nawiązaniu do artykułu Michała Grzegorzewskiego [Zabawy z LSA - wydłubywanie haseł usług] oraz Jego nawoływaniu do napisania czegoś o ProtectedData i ProtectedMemory.

ProtectedData

Wyobraźmy sobie scenariusz gdzie posiadamy ekran logowania wraz z opcją zapamiętania hasła użytkownika. Opcja zapamiętywania służy temu głównie, aby użytkownikowi opcjonalnie sugerować jego hasło gdy działa głównie na jednej stacji roboczej. Zazwyczaj przecież jest tak, że pracujemy w domu czy pracy na jednej maszynie która jest nawet przypisana do nas jako część stanowiska pracy.

Akt 1

Przykład ten ilustruje projekt ZineNet.DataProtection.Act1. Podczas uruchamiania aplikacji użytkownik wpisuje nazwę użytkownika oraz hasło i zaznacza/odznacza opcję "zapamiętaj hasło". Hasło zapisywane jest w jeden z typowych sposobów dla aplikacji .NET, czyli ustawieniach użytkownika Properties/Settings.settings. Przy kolejnej autentykacji hasło jest już wpisane i tylko użytkownik wpisuje nazwę i wciska "Enter". Co za user experience! Przecież nikt nie lubi zapamiętywać haseł!

Zajrzyjmy teraz co stało się z zapamiętaną wartością hasła. Plik konfiguracji uzytkownika możemy w naszym przypadku znaleźć w katalogu profilu użytkownika, coś mniej więcej:
Local Settings\Application Data\ZineNet\ZineNet.DataProtection.cos_tam\1.0.1.0\user.config. Zaglądamy do środka i tu rozczarowujący widok. Hasło przechowywane jest w czystej postaci, jak go użytkownik stworzył.

<configuration>
...
  <userSettings>
    <ZineNet.DataProtection.Act1.Properties.Settings>
      <setting name="Logon_Password" serializeAs="String">
        <value>bbb</value>
      </setting>
    </ZineNet.DataProtection.Act1.Properties.Settings>
  </userSettings>
</configuration>

Akt 2

Taki sposób to dość marne zabezpieczenie, więc postaramy się teraz to naprawić. Najpierw zmieniamy typ zapamiętywanego hasła z System.String na System.Byte[] (i najlepiej nazwę wpisu ustawień użytkownika na wypadek kolizji i problemów z wersjonowaniem naszej aplikacji). W kolejnym kroku zamiast zapisywać hasło w czystej postaci jak poniżej:

if (_rememberPassword) {
  Properties.Settings.Default.Logon_Password = password;
  Properties.Settings.Default.Save();
}

Zapiszemy ją jako zaszyfrowany ciąg bajtów. Szyfrowanie możemy uzyskać za pomocą metody "ProtectedData.Protect(...)". Klasa ProtectedData ta faktycznie opakowuje funkcje systemowe DPAPI: CryptProtectData i CryptUnprotectData. Metoda Protect przyjmuje 3 parametry i zwraca ciąg bajtów, którym jest zaszyfrowany ciąg bajtów wejściowych.

Pierwszym parametrem jest "userData". Jest to ciąg bajtów dowolnej długości, który należy zaszyfrować.

Drugim parametrem jest "optionalEntropy". Jest to kolejny ciąg bajtów (może być innego rozmiaru niż userData) który zawiera losowe wartości wzmacniające szyfrowane dane. Idea jest taka sama jak z szyfrowaniem haseł i tak zwanym "ziarnem" (http://en.wikipedia.org/wiki/Salt_(cryptography)).

Trzeci parametr "scope" definiuje na jakim poziomie dane powinny być zabezpieczone. Możemy wybrać opcję szyfrowania w ramach konta użytkownika (wtedy tylko w kontekście tego samego użytkownika będzie można te dane odszyfrować) lub w ramach całej maszyny.

Poziom szyfrowania "maszyna" może być użyteczny na przykład to przechowywania haseł do serwera bazy danych, jeśli ten sam serwer jest używany przez wielu użytkowników z tej samej maszyny. Po prostu zaszyfrowane dane będzie mógł odszyfrować każdy, kto zaloguje się poprawnie do komputera.

W naszym przypadku pomijam opcjonalna wartość entropii i ustawiam poziom "użytkownika". Dzięki temu na tej samej maszynie różne osoby (konta) będą mogły używać naszej aplikacji nie przeszkadzając sobie.

Drugim krokiem jest zmiana uzyskiwania hasła już zapisanego dla kolejnych uruchomień aplikacji. Zamiast prostego:

_password = Properties.Settings.Default.Logon_Password ?? "";

Musimy użyć metody "ProtectedData.Unprotect(...)" w następujący sposób:

var encPassword = Properties.Settings.Default.Logon_PasswordEnc;
if (encPassword != null) {
  _password = Encoding.Unicode.GetString(ProtectedData.Unprotect(encPassword, null, DataProtectionScope.CurrentUser));
} else {
  _password = "";
}

Metoda Unprotect przyjmuje 3 parametry jak metoda Protect, z tym wyjątkiem że pierwszy parametr jest zaszyfrowanym ciągiem bajtów uzyskanym z wcześniejszego użycia metody Protect. Jeśli używamy opcjonalnej entropii, to musimy jako drugi parametr przekazać tą samą wartość której użyliśmy przy metodzie Protect. Poniższy pseudo kod ilustruje wymaganą zależność:

var data = ...
var entropy = ...
var scope = ...

var enc = Protect(data, entropy, scope
var data2 = Unprotect(enc, entropy, scope)

assert(data == data2)

Zaglądamy ponownie do pliku user.config tym razem w katalogu Local Settings\Application Data\ZineNet\ZineNet.DataProtection.cos_tam\1.0.2.0\user.config i widzimy tym razem coś o wiele porządniejszego:

<configuration>
...
  <userSettings>
    <ZineNet.DataProtection.Act2.Properties.Settings>
      <setting name="Logon_PasswordEnc" serializeAs="Xml">
        <value>
          <base64Binary>AQAAANCMnd8BFdERjHoAwE...</base64Binary>
        </value>
      </setting>
    </ZineNet.DataProtection.Act2.Properties.Settings>
  </userSettings>
</configuration>

Gotową aplikację, a dokładniej jej projekt można znaleźć w załączonych plikach do artykułu pod nazwą ZineNet.DataProtection.Act2

ProtectedMemory

Podobne działanie jak ProtectedData mają metody klasy ProtectedMemory. Są trzy zasadnicze różnice.

Pierwszą jest to że metody ProtectedMemory opakowują metody DPAPI CryptProtectMemory i CryptUnprotectMemory i jako takie nie przyjmują już opcjonalnej wartości entropii.

Drugą różnicą jest inny typ enumeracji dla parametru scope. Możemy wybrać opcję szyfrowania wewnątrz procesową, między procesową oraz w ramach kontekstu użytkownika.

Trzecią różnicą jest to, że możemy szyfrować i deszyfrować w ramach jednego uruchomienia maszyny. Restart komputera wymazuje informacje o tym jak zdeszyfrować poprzednio szyfrowane dane. Typowym zastosowaniem jest szyfrowanie tych danych, które są ścisłe sesyjne, czy muszą istnieć tylko podczas działania aplikacji i nie są nigdzie zapisywane.

Opublikowane 21 października 08 04:47 przez Wojciech Gebczyk

Attachment(s): ZineNet.DataProtection.zip

Komentarze:

# dotnetomaniak.pl said on kwietnia 17, 2009 09:53:

Dziękujemy za publikację - Trackback z dotnetomaniak.pl

Komentarze anonimowe wyłączone

About Wojciech Gebczyk

Code Sculptor.