O jej, zapomniałem hasła.

Co się stało to się nie odstanie i hasło się zapomniało. Co możemy z tym zrobić? Trzeba mieć wcześniej ustaloną formę komunikacji, może email, może sms, może adres pocztowy.
Jeśli mamy coś takiego, to spoko, jesteśmy uratowani i nasz użytkownik też. Teraz już pozostało już tylko kilka kroków które należy wykonać, aby użytkownik mógł bezpiecznie zmienić swoje zapomniane hasło, na nowe które zapomni kiedy indziej.
Robimy tak, przynajmniej ja tak robię:
Grzecznie prosimy o podanie nam adresu na który mamy przysłać (mając nadzieję, że chociaż tyle pamięta).
Po tym jak poda nam adres, warto sprawdzić czy mamy taki adres. Jeśli go posiadamy, generujemy jednorazowy token, który zostanie przypisany do właśnie tego przypomnienia i tego użytkownika.
Ja robię to tak (co nie oznacza, że jest to jedyny słuszny sposób czy najlepszy):

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #a31515; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }

   1:  public ViewModelResult<RequestResetPasswordViewModel> RequestResetPassword(string emailAddress, string baseUrl)
   2:  {
   3:      var userToResetPassword = this.database.Users.Get(x => x.Email == emailAddress).FirstOrDefault();
   4:      if (userToResetPassword == null)
   5:      {
   6:          var result = new ViewModelResult<RequestResetPasswordViewModel>
   7:                           {
   8:                               Success = false,
   9:                               ViewModel = null,
  10:                               ErrorMessage =
  11:                                   Translations
  12:                                   .AccoutService_ResetPassword_Email_NotExists
  13:                           };
  14:          return result;
  15:      }
  16:      else
  17:      {
  18:          var token = Guid.NewGuid().ToString().Replace("-", string.Empty);
  19:          var passwordResetRequest = new PasswordResetRequest
  20:                                         {
  21:                                             Created = DateTime.Now,
  22:                                             Token = token,
  23:                                             Used = false,
  24:                                             UserId = userToResetPassword.Id
  25:                                         };
  26:          this.database.PasswordResetRequests.Insert(passwordResetRequest);
  27:          this.emailService.SendPasswordRequestEmail(
  28:              userToResetPassword.Email,
  29:              userToResetPassword.DisplayName,
  30:              baseUrl + token);
  31:          this.database.Save();
  32:          var result = new ViewModelResult<RequestResetPasswordViewModel>
  33:                     {
  34:                         Success = true,
  35:                         ViewModel =
  36:                             new RequestResetPasswordViewModel
  37:                                 {
  38:                                     Message =
  39:                                         Translations
  40:                                         .AccoutService_ResetPassword_EmailSend
  41:                                 }
  42:                     };
  43:          return result;
  44:      }
  45:  }

Co jest powyżej? Szukam gościa z podanego email’a, jeśli go znajdę, to generuje token. W moim przypadku jest to guid bez kresek (może warto to przemyśleć). Później taką informację zapisuje do bazy, a potem korzystając z dostępu do serwisu mailowego wysyłam informację.
W baseUrl ukrywa się podstawowy link go którego zostanie doklejony token – średnio ładne, jestem otwarty na propozycje.
Produkcja mail jest raczej prosta:

   1:  public void SendPasswordRequestEmail(string email, string displayName, string url)
   2:  {
   3:      var smtpClient = new SmtpClient(this.communicationConfiguration.Host, this.communicationConfiguration.Port);
   4:   
   5:      smtpClient.EnableSsl = this.communicationConfiguration.MailSsl;
   6:      smtpClient.Credentials = new NetworkCredential(
   7:          this.communicationConfiguration.HostUser,
   8:          this.communicationConfiguration.HostPassword);
   9:   
  10:   
  11:      var from = new MailAddress(
  12:          this.communicationConfiguration.PasswordResetRequestAddress,
  13:          this.communicationConfiguration.PasswordResetRequestName);
  14:   
  15:      var to = new MailAddress(email, displayName);
  16:   
  17:      var bodyFormat = Translations.EmailsService_PasswordResetRequestBody_DisplayName_Url;
  18:      var subject = Translations.EmailsService_PasswordResetRequestSubject;
  19:      var body = string.Format(bodyFormat, displayName, url);
  20:   
  21:      var message = new MailMessage(from, to);
  22:   
  23:      message.Subject = subject;
  24:      message.Body = body;
  25:      smtpClient.Send(message);
  26:  }

Zwykły plain text do wysłania informacji, część ludzi ładnie wysyła piękne maile, ja tego na razie nie robię. Pewnie tak będzie, jak już zdobędę miliony użytkowników.

To w zasadzie pierwszy etap, kolejna część do obsługa takiego żądania. Ja, nowicjusz robię to tak:

   1:  [HttpGet]
   2:  public ActionResult ResetPassword(string token)
   3:  {
   4:      var serviceResult = this.accountService.IsRequestTokenValid(token);
   5:      var viewModel = new ResetPasswordViewModel2();
   6:      viewModel.Token = token;
   7:      this.ModelState.AddModelError(string.Empty, serviceResult.ErrorMessage);
   8:   
   9:      return this.View("ResetPassword", viewModel);
  10:  }

Wybaczcie mi ResetPasswordViewModel2 – coś testowałem i mi zostało – oops!
Zwykły get do kontrolera, co ważne należy pamiętać żeby dostęp do tej metody był możliwy także dla nie zalogowanych użytkowników.
Sprawdzenie poprawności tokena wygląda tak:

   1:  public MessagesResult IsRequestTokenValid(string token)
   2:  {
   3:      var passwordResetRequest = this.database.PasswordResetRequests.Get(x => x.Token == token).FirstOrDefault();
   4:   
   5:      if (passwordResetRequest != null && passwordResetRequest.Used == false)
   6:      {
   7:          return new MessagesResult { Success = true };
   8:      }
   9:      else
  10:      {
  11:          return new MessagesResult
  12:                     {
  13:                         Success = false,
  14:                         ErrorMessage = Translations.AccoutService_HandlePasswordReset_TokenNotExists
  15:                     };
  16:      }
  17:  }

Opcje z przetrzymywaniem tokena są różne, ja aktualnie postanowiłem trzymać token w bazie i po użyciu oznaczać go jako użyty. Można też go usuwać, można go też trzymać w tabeli użytkowników jako dodatkowe pole. Tysiąc programistów, tysiąc problemów, tysiąc rozwiązań.

Jeśli token jest poprawny renderowany jest widok i czekamy aż pan zapominalski wprowadzi nowe hasło, po tym jak się zdecyduje zostanie obsłużony w taki sposób:

   1:  [HttpPost]
   2:  [ValidateAntiForgeryToken]
   3:  public ActionResult ResetPassword(ResetPasswordViewModel2 model)
   4:  {
   5:      if (ModelState.IsValid)
   6:      {
   7:          var serviceResult = this.accountService.HandleResetPassword(model);
   8:          if (serviceResult.Success)
   9:          {
  10:              return this.RedirectToAction("Login", "Account");
  11:          }
  12:          this.ModelState.AddModelError(string.Empty, serviceResult.ErrorMessage);
  13:      }
  14:   
  15:      return this.View("ResetPassword", model);
  16:  }

Oraz serwis, który wykonuje właściwą pracę:

   1:  public MessagesResult HandleResetPassword(ResetPasswordViewModel2 resetPasswordViewModel)
   2:  {
   3:      if (resetPasswordViewModel.NewPassword != resetPasswordViewModel.ConfirmNewPassword)
   4:      {
   5:          return new MessagesResult
   6:                     {
   7:                         Success = false,
   8:                         ErrorMessage =
   9:                             Translations.AccoutService_HandlePasswordReset_PasswordDoesNotMatch
  10:                     };
  11:      }
  12:   
  13:      var passwordResetRequest =
  14:          this.database.PasswordResetRequests.Get(x => x.Token == resetPasswordViewModel.Token && x.Used == false)
  15:              .FirstOrDefault();
  16:   
  17:      if (passwordResetRequest == null)
  18:      {
  19:          return new MessagesResult
  20:                     {
  21:                         Success = false,
  22:                         ErrorMessage = Translations.AccoutService_HandlePasswordReset_TokenNotExists
  23:                     };
  24:      }
  25:   
  26:      var user = this.database.Users.Get(x => x.Id == passwordResetRequest.UserId).First();
  27:      var hash = this.hashProvider.Get(resetPasswordViewModel.NewPassword);
  28:      
  29:      user.Hash = hash;
  30:      passwordResetRequest.Used = true;
  31:   
  32:      this.database.Users.Update(user);
  33:      this.database.PasswordResetRequests.Update(passwordResetRequest);
  34:   
  35:      this.database.Save();
  36:   
  37:      return new MessagesResult { Success = true };
  38:  }

Nie pamiętam czemu mam powtórzoną walidację pól view-modelu, pamiętam że miałem poważny powód żeby tak zrobić – deal with it!
Ale do rzeczy, jeśli podane hasła się zgadzają i token jest poprawny, generowany jest nowy hash dla hasła i jest ono zapisywane dla użytkownika, z którym powiązany jest token. Po wszystkim w moim przypadku MUSZĘ pamiętać o oznaczeniu tokena jako użytego.
Będzie?!

enkrypting, dekrypting, sajning, walidejting i chaszing

Uważni czytelnicy zapewne przerobili już poprzedni temat i wiedzą jak nie pozwolić nie powołanym osobom wykonać kodu, który nie jest dla nich przeznaczony. Ale może się zdarzyć, że zostaniemy przechytrzeni i ktoś nas perfidnie podsiądzie i skorzysta z sytuacji, gdy akurat zapomnimy zablokować swojego kompa. Ojej, co możemy wtedy zrobić? Nie peniajcie, albowiem z pomocą przychodzi technologia i geniusz matematyki. Nasze poufne dane możemy zaszyfrować i w ten sposób poczuć się jeszcze bardziej bezpieczni. Oprócz tego, można wygenerować sumę kontrolną, która będzie poprawna tylko dla jednej pliku. To na wypadek, gdyby ktoś chciał coś dopisać do listy zakupów. Nawet po najmniejszej zmianie jednego znaczka, hasz suma będzie całkowicie inna. Jeżeli nam tego mało, możemy wygenerować cyfrowy podpis dla pliku – tak wiem szaleństwo.

Jak tego wszystkiego dokonać?
Wystarczy skorzystać z jednego z gotowych rozwiązań, które oferuje nam Microsoft i C# (lub skorzystać z tego Truecrypt ale jakimi bylibyśmy programistami, gdyby nie skusiło nas napisanie własnej aplikacji, która też potrafi takie rzeczy lub przynajmniej podobne).


SZYFROWANIE
Aktualnie dostępne są dwa gotowe rodzaje szyfrowania symetryczne (szybsze) i asymetryczne (bezpieczniejsze, wolniejsze). zaimplementowane w C#. Symetryczne podejście do problemu możemy rozwiązać przy pomocy jednej z pięciu gotowych klas:

Jeżeli chcemy być bardziej pro i mamy więcej czasu,  można skorzystać z Asymetrycznego rozwiązania przy pomocy klasy:

O ile dla ludzi prostszym (czytelniejszym) jest symetryczny, podajemy hasło i sól (za wiki: losowa wartość (ang. salt) modulująca wynik tak, by użycie tego samego hasła dawało w wyniku inny klucz), można korzystać z dowolnych rozmiarów bloków danych, jest naprawdę szybciej, ale jest tylko jedno hasło, które służy do obu operacji szyfrowania i odszyfrowania.
O tyle asymetryczne korzystają z automagicznie (nawet nie wiedziałem, że znajdzie się do tego wiki) generowanych kluczy, które nie należą do najbardziej czytelnych (oczywiście dla ludzi), mają ograniczone rozmiary bloków szyfrowania, co za tym idzie dla większej porcji danych należy stworzyć większy klucz, co trwa naprawdę długo, a najgorsze jest to (naprawdę jest to złe i nie dobre) to nawet nie można używać własnego hasła. Bezpieczeństwo kosztuje.

Dobrze, po krótkim wstępie chciałbym zapisać jak korzystać z poszczególnych rozwiązań, kod jest do pobrania jak zwykle z xp-dev, także tutaj nie będę przyklejać kodu, żeby nie powiększać i tak długiego wpisu. Można to potraktować jako swojego rodzaju ściągawkę.
SYMETRYCZNIE

  • Tworzymy instancje klasy, która ma zaszyfrować nasze dane. Jedna z wymienionych powyżej.
  • Korzystając z klasy Rfc2898DeriveBytes zapodajemy hasło i sól od użytkownika, oraz pobieramy losowe bajty wygenerowane dla nas. Tylko w przypadku szyfrowania symetrycznego.
  • Należy pamiętać o poprawnej inicjalizacji KeySize, BlockSize, Key oraz IV (initialize vector)
  • Otwieramy strumień z danymi, które chcemy zaszyfrować i zaczytujemy porcję danych
  • Pobieramy interfejs do ICryptoTransform z klasy szyfrującej, tworzymy strumień wyjściowy szyfrujący, oraz otwieramy strumień do pliku wyjściowego
  • Piszemy do strumienia szyfrującego, korzystając z pobranego wcześniej interfejsu, strumienia wyjściowego i zaczytanych wcześniej danych, powtarzamy dla kolejnej porcji danych
  • Następnie trzeba już tylko wszytko pozamykać i powinno działać
  • Jeżeli użyty został algorytm asymetryczny, warto wyeksportować klucz publiczny i prywatny, aby nie utracić możliwość ponownego odczytania zaszyfrowanych danych. W przypadku symetrycznego “wystarczy”, że zapamiętamy hasło i sól.

ASYMETRYCZNIE

  • Stwórz klasę RSACryptoServiceProvider z rozmiarem klucza, z którego chcesz skorzystać. Tutaj trzeba uważać, jeżeli poda się za mały klucz, poleci wyjątek BadLength podczas próby szyfrowania
  • Zaczytaj dane do bufora bajtów (lub strumień)
  • Zaszyfruj dane
  • Zapamiętaj klucz prywatny i publiczny. Dla tych, którzy nie wiedzą co to, na początku wpisu jest link do wiki

Podczas odszyfrowywania (deszyfrowania) postępujemy podobnie tylko, że w drugą stronę, należy także pamiętać o zaimportowaniu odpowiedniego klucza, który pozwoli nam poprawnie odczytać dane.

HASZ SUMY
Generowanie hasz sum to już prosta sprawa. Ciekawa (przynajmniej dla mnie to było ciekawe) sprawa do obliczania sum możemy także skorzystać z pomocy haseł. Suma bez hasła można wyliczyć za pomocą jednej następujących klas:

Jeśli chcemy skorzystać z hasła podczas generowania sumy, powinniśmy skorzystać z jednej z następujących klas:

Sposób działania jest banalnie prosty

  • Stworzyć klasę haszującą, jeżeli korzystamy dodatkowo z hasła – przesłać do klasy w konstruktorze
  • Wysłać do niej porcję danych do obliczenia (lub strumień)
  • Zapytać się o hasz sumę i gdzieś sobie zapisać
  • Posprzątać po sobie

PODPISYWANIE PLIKÓW (PODPIS CYFROWY)
I już ostatni ze sposobów na zabezpieczenie się przed elektronicznymi bandytami to podpisanie pliku, czyli kolejny ciąg bajtów, którym możemy oznaczyć nasz i tylko nasz plik. Także tutaj w cały proces możemy wciągnąć akcję z hasłem.

Cały algorytm działania jest prosty

  • Tworzymy odpowiednią instancje
  • Ładnie prosimy o wygenerowanie podpisu dla zadanych danych (i ew. zaszyfrowanie)
  • Pobieramy wyniczek i coś z nim robimy
  • Zamykamy wszystko co zostało otworzone

W drugą stronę prawie tak samo, należy tylko zaimportować wynik poprzedniej operacji, zapewnić identyczny ciąg bajtów i poprosić o weryfikację.

Mam nadzieję, że to krótkie podsumowanie tematu choć trochę się to komuś przyda. Jeżeli są jakieś pytania, niejasności lub uwagi śmiało pisać.

JS

ps. Strach mnie bierze, gdy miałem kliknąć “publikuj posta” po napisaniu czegoś tak długiego.