Bezpieczeństwo w aplikacjach C#. Podstawy i nie tylko. Część I.

Bezpieczeństwo jest szerokim tematem i ma jeden poważny minus nigdy się do końca nie wie czy to co się wie na temat bezpieczeństwa jest wystarczające do tego by powiedzieć, że aplikacja, którą napisaliśmy jest bezpieczna. Zawsze się znajdzie, ktoś kto powie, że jakieś zabezpieczenie jest słabe lub niewystarczające. Mimo tego, podstawy trzeba znać, poniżej opisuje podstawy zabezpieczeń aplikacji C#.

Z części 1 dowiesz się:
  1. Co oznacza termin bezpieczna aplikacja?
  2. O podstawach kryptografii – algorytmy symetryczne i asymetryczne.
  3. Co to jest initialization vector (IV)?
  4. Jak w prosty sposób szyfrować i deszyfrować dane przy użyciu algorytmu symetrycznego i asymetrycznego w C#
  5. Co to jest i jak działa kontener kluczy?
Z części 2 (dostępna od 20 marca) dowiesz się:
  1. Jak zarządzać certyfikatami w aplikacjach .NET?
  2. Jak działa hashowanie czyli HashTable i Dictionary.
  3. oraz trochę informacji na temat security String.

Bezpieczeństwo aplikacji

Bezpieczne aplikacje powinny odznaczać się następującymi cechami:

Powinny umożliwić UWIERZYTELNIANIE użytkowników (authentication), czyli weryfikację tożsamości użytkownika żądającego dostępu do aplikacji.

Powinny umożliwić AUTORYZACJE (authorization) – czyli weryfikacje dostępu do zasobów aplikacji.

Powinny zapewniać POUFNOŚĆ (confidentiality) – czy dostęp do informacji powinien być ograniczony wyłącznie do grona uzytkowników do tego upoważnionych;

Powinnay zapewnać INTEGRALNOŚĆ (integrity) – czyli zagwarantować, że informacja nie zostanie zmodyfikowana w sposób nieuprawniony, a wszelkie modyfikacje zostaną wykryte

Powinno zapewniać DOSTĘPNOŚĆ (availability) – do informacji można uzyskać dostęp w każdych okolicznościach, które są dopuszczone przez politykę bezpieczeństwa informacji

NIEZAPRZECZALNOŚĆ (non-repudiation) – użytkownik podejmujący jakieś działania przy użyciu aplikacji nie może wyprzeć się, że wykonywał określone operacje z wykorzystaniem aplikacji.

Poufność, spójność i niezaprzeczalność możemy zapewnić poprzez implementacje algorytmów szyfrujących.

Teoretyczne podstawy kryptografii – algorytmy symetryczne i asymetryczne.

Podstawy są takie. Jeśli Bob chce wysłać wiadomość do Alice tak aby Oscar nie mógł tej wiadomości podejrzeć to napotkamy na 3 problemy.
Opis do aktorów znajduje się tutaj

Pierwszy – jak Bob ma zaszyfrować wiadomość aby było to bezpieczne? Drugi to jeśli Bob ma już zaszyfrowaną wiadomość to jak Bob ma przekazać Alice klucz do deszyfracji wiadomości, tak aby Oscar nie mógł go podejrzeć? Trzeci problem to skąd Alice wie, że wiadomość pochodzi od Bob-a a nie od Oscar-a?

W tym poście pokaże jak można rozwiązać 2 pierwsze problemy.

Pierwszy problem został rozwiązany już dość dawno. Wymyślanie własnych algorytmów szyfrujących może wydawać się sensowne, bo tylko my wiemy jak coś zostało zaszyfrowane. Jednak na większą skalę (a o takiej tu mówimy) to się nie sprawdza, bo tracimy możliwość, na przykład testowania odporności Naszego algorytmu na ataki przez społeczność. Wszystkie algorytmy kryptograficzne są publicznie dostępne i jeśli ktoś złamie taki algorytm lub po prostu wraz z rozwojem techniki taki algorytm przestanie być bezpieczny to wiadomość jest upubliczniania(przynajmniej teoretycznie). Dzięki temu każdy wie, że trzeba go zmienić.

Jeśli mamy publiczny algorytm to musimy mieć tajny klucz, który będzie mógł szyfrować i deszyfrować wiadomości. Ten tajny klucz musimy chronić, bo tylko dzięki niemu możemy przesyłać wiadomości między sobą.

Algorytmy symetryczne

Algorytm symetryczny to taki, gdzie do zaszyfrowania wiadomości używamy takiego samego klucza, którego używamy do deszyfracji. Czyli w naszym wypadku Alice i Bob aby przekazać sobie wiadomość muszą posiadać ten sam tajny klucz.

Algorytmy symetryczne dzielą się na:

  •  szyfry blokowe – oznaczają, że treść wiadomość dzielimy na bloki, na których następnie przeprowadzamy operacje atomowe jak podstawienie i przestawienie.

Podstawienie to zastępowanie grupy znaków innymi znakami. Przestawienie to wykonywanie permutacji danych. Większość algorytmów blokowych to algorytmy złożone, które wykonują różne kombinacje operacji atomowych.

Kombinacja operacji podstawienia i przestawienia nazywa się rundą. Zadaniem podstawienia w każdej rundzie jest utrudnienie wychwycenia związku między kluczem a wynikowym szyfrogramem, natomiast przestawianie powoduje rozproszenie informacji statystycznej czyli utrudnienie znalezienia korelacji na podstawie częstości wystąpień znaków.

  • szyfry strumieniowe – oznaczają tak naprawdę szyfry blokowe, w którym blok jest równy 1. Dzięki temu każdy znak może być zaszyfrowany innym sposobem.

W .Net 4.5 mamy 5 algorytmów symetrycznych dziedziczących po abstrakcyjnej klasie SymmetricAlgorithm.

  • Aes.
  • DES.
  • RC2.
  • Rijndael.
  • TripleDES.

Algorytmy symetryczne pozwalają na szybkie szyfrowanie dużych ilości danych. Jednak wadą jest to, że trzeba jakoś rozwiązać problem 2, czyli jak Bob ma przekazać klucz Alice aby Oscar nie mógł się do niego dobrać.

Algorytmy asymetryczne

Algorytmy asymetryczne – to takie, gdzie do szyfrowania wiadomości używamy publicznego klucza a do deszyfracji używamy prywatnego klucza. Szyfrowanie odbywa się za pomocą klucza publicznego a deszyfracja za pomocą klucza prywatnego. Dodatkowo klucz do szyfrowania danych jest całkowicie publiczny i dostępny dla Alice, Bob i Oscara czyli dla wszystkich.

Wadą takiego rozwiązania jest to, że algorytmy asymetryczne są dość wolne i nie sprawdzają się w przypadku dużych ilości danych. Dokładnie wygląda to tak, że wielkość danych, które można zakodować zależą od wielkości klucza.

Jeśli klucz ma powiedzmy 1024 bity to maksymalna długość wiadomości może być równa tylko 117 bajtów. Dla klucza 2 razy większego 2048 bitów można zakodować tylko 245 bajtów więc nie jest to za wiele.

Wzór na wyliczanie długości wiadomości do zaszyfrowania jest taki:

((Rozmiar_klucza – 384) / 8) + 37

W Naszym wypadku aby Bob mógł przesłać duże paczki danych do Alice, Bob użyje publicznego klucza Alice do zaszyfrowania swojego tajnego klucza do algorytmu symetrycznego. Alice użyje swojego klucza prywatnego do deszyfracji wiadomości od Bob-a i dzięki temu wejdzie w posiadanie klucza do algorytmu symetrycznego Bob-a. Teraz Alice i Bob mogą się wymienić nie legalnymi filmami w formie BlueRay używając do tego algorytmu symetrycznego. A Oscar nie będzie mógł nic zrobić.

 

Initialization vector

W skrócie IV. Są to dane, które wprowadzają do przekazywanej wiadomości trochę losowości. Dzięki temu ta sama wiadomość zaszyfrowana konkretnym algorytmem będzie wyglądać inaczej za każdym razem gdy będziemy ją szyfrować. Unikamy wtedy sytuacji, gdzie Oscar mógł by przechwytywać wiadomości i domyśleć się klucza szyfrującego na podstawie przesyłanych wiadomości. IV nie musi być tajny ale powinien być różny dla każdej sesji.

Jak w prosty sposób szyfrować i deszyfrować dane w C#

Poniżej prezentuje użycie algorytmu symetrycznego do kodowania wiadomości.

private byte[] Encrypt(SymmetricAlgorithm symmetricAlgorithm, string text)
        {
            ICryptoTransform encryptor = symmetricAlgorithm.CreateEncryptor(symmetricAlgorithm.Key, symmetricAlgorithm.IV);

            using (var msEncrypt = new MemoryStream())
            {
                using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                {
                    using (var swEncrypt = new StreamWriter(csEncrypt))
                    {
                        swEncrypt.Write(text);
                    }
                    return msEncrypt.ToArray();
                }
            }
        }

Klucz i IV generują się automatycznie. Oczywiście możemy przesłać swoje. Wszystkie operacje kryptograficzne w .NET działają na tablicach bajtów.

Metoda deszyfrująca jest w wielu miejscach analogiczna.

private string Decrypt(SymmetricAlgorithm symmetricAlgorithm, byte[] msg)
        {
            ICryptoTransform decryptor = symmetricAlgorithm.CreateDecryptor(symmetricAlgorithm.Key, symmetricAlgorithm.IV);
            using (var msDecrypt = new MemoryStream(msg))
            {
                using (var csDecrypt =new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
                {
                    using (var srDecrypt = new StreamReader(csDecrypt))
                    {
                        return srDecrypt.ReadToEnd();
                    }
                }
            }
        }

Używanie tych metod jest trywialne i sprowadza się do stworzenia odpowiedniego algorytmu dziedziczącego po SymmetricAlgorithm i przesłania go do metody wraz z tekstem.

public Bezpieczenstwo()
{
    SymmetricAlgorithm sa = new TripleDESCryptoServiceProvider();

    var msg = "Tajny Tekst";

    Console.WriteLine("Wiadomość: {0}", msg);

    var encryptMsg = this.Encrypt(sa, msg);

    Console.Write("Zakodowana wiadomość: ");

    foreach (var c in encryptMsg)
    {
        Console.Write(c);
    }

    Console.WriteLine();

    var msg2 = this.Decrypt(sa, encryptMsg);

    Console.WriteLine("Odkodowana: {0}", msg2);

    Console.ReadLine();
        }

Teraz czas na stworzenie obsługi algorytmu asymetrycznego(pamiętamy, że trzeba mieć do tego klucz prywatny i klucz publiczny). W .NET jest to dość proste. W C# dostępne są 2 algorytmy asymetryczne RSA i DSA. Aby móc wysłać wiadomość używając do tego algorytmu asymetrycznego, musimy stworzyć instancję klasy RSACryptoServiceProvide (RSA). Klucz prywatny i publiczny tworzy się automatycznie podczas wykonywania konstruktora.

Metoda szyfrująca:

 private byte[] EncryptRSA(byte[] msg, CspParameters csp)
 {
    using (var RSA = new RSACryptoServiceProvider(csp))
    {
      return RSA.Encrypt(msg, false);
    }
 }

Metoda deszyfrująca:

private byte[] DecryptRSA(byte[] msg, CspParameters csp)
{
     using (var RSA = new RSACryptoServiceProvider(csp))
     {
         return RSA.Decrypt(msg, false);
     }
}

Klucze do szyfrowania trzymane są w kontenerze kluczy (CspParameters) o czym poniżej. Aby wyciągnąć klucz publiczny lub prywatny należy użyć metody ToXmlString();

using (var RSA = new RSACryptoServiceProvider(csp))
{
  RSA.ToXmlString(true); // klucz prywatny i publiczny
  RSA.ToXmlString(false); // klucz publiczny

  return RSA.Decrypt(msg, false);
}

Kontener kluczy

Kontener kluczy jest to miejsce gdzie w .NET można bezpiecznie (przynajmniej tak jest napisane w dokumentacji) trzymać klucze do szyfrowania.
Działa to tak, że tworzymy kontener nadając mu nazwę. Podczas tworzenia obiektu RSACryptoServiceProvider w parametrze konstruktora podajemy właściwy kontener. Do tego kontenera zostaną zapisane klucze utworzone przez RSACryptoServiceProvider Poniżej sposób na utworzenie kontenera.

var csp = new CspParameters
{
  KeyContainerName = "KontenerRSA"
};

Kontenery mogą być dostępne albo dla jednego użytkownika(User-level RSA), który jest zalogowany i tylko dla niego lub dla wszystkich użytkowników na danej maszynie(Machine-level RSA).
Jedynym plusem tego, że klucze są dostępne tylko dla jednego użytkownika (User-level RSA) jest to, że gdy jego profil zostanie skasowany, klucze również zostaną skasowane.
Plusem dostępności kluczy dla całej maszyny(Machine-level RSA) jest to, że gdy aplikacja jest na serwerze to administrator zalogowany do serwera może zmieniać konfigurację aplikacji. Zakładając oczywiście, że konfiguracja aplikacji jest chroniona własnie przez jakiś algorytm kryptograficzny.