protected void AdjustBlockingScoreForPastTyposTreatedAsFullFailures( IpHistory clientsIpHistory, IUserAccountController <TUserAccount> accountController, TUserAccount account, DateTime whenUtc, string correctPassword, byte[] phase1HashOfCorrectPassword) { double credit = 0d; if (clientsIpHistory == null) { return; } LoginAttemptSummaryForTypoAnalysis[] recentPotentialTypos = clientsIpHistory.RecentPotentialTypos.MostRecentFirst.ToArray(); Encryption.IPrivateKey privateAccountLogKey = null; try { foreach (LoginAttemptSummaryForTypoAnalysis potentialTypo in recentPotentialTypos) { bool usernameCorrect = potentialTypo.UsernameOrAccountId == account.UsernameOrAccountId; if (usernameCorrect) { string passwordSubmittedInFailedAttempt = null; if (account.GetType().Name == "SimulatedUserAccount") { passwordSubmittedInFailedAttempt = potentialTypo.EncryptedIncorrectPassword.Ciphertext; } else { if (privateAccountLogKey == null) { try { privateAccountLogKey = accountController.DecryptPrivateAccountLogKey(account, phase1HashOfCorrectPassword); } catch (Exception) { return; } } try { passwordSubmittedInFailedAttempt = potentialTypo.EncryptedIncorrectPassword.Read(privateAccountLogKey); } catch (Exception) { } } if (passwordSubmittedInFailedAttempt != null) { bool passwordHadTypo = EditDistance.Calculate(passwordSubmittedInFailedAttempt, correctPassword) <= _options.MaxEditDistanceConsideredATypo; if (passwordHadTypo) { credit += potentialTypo.Penalty.GetValue(_options.AccountCreditLimitHalfLife, whenUtc) * (1d - _options.PenaltyMulitiplierForTypo); } } } clientsIpHistory.RecentPotentialTypos.Remove(potentialTypo); } clientsIpHistory.CurrentBlockScore.SubtractInPlace(account.CreditHalfLife, credit, whenUtc); } finally { privateAccountLogKey?.Dispose(); } }
/// <summary> /// This analysis will examine the client IP's previous failed attempts to login to this account /// to determine if any failed attempts were due to typos. /// </summary> /// <param name="clientsIpHistory">Records of this client's previous attempts to examine.</param> /// <param name="accountController"></param> /// <param name="account">The account that the client is currently trying to login to.</param> /// <param name="whenUtc"></param> /// <param name="correctPassword">The correct password for this account. (We can only know it because /// the client must have provided the correct one this loginAttempt.)</param> /// <param name="phase1HashOfCorrectPassword">The phase1 hash of that correct password (which we could /// recalculate from the information in the previous parameters, but doing so would be expensive.)</param> /// <returns></returns> protected void AdjustBlockingScoreForPastTyposTreatedAsFullFailures( IpHistory clientsIpHistory, IUserAccountController <TUserAccount> accountController, TUserAccount account, DateTime whenUtc, string correctPassword, byte[] phase1HashOfCorrectPassword) { double credit = 0d; if (clientsIpHistory == null) { return; } LoginAttemptSummaryForTypoAnalysis[] recentPotentialTypos = clientsIpHistory.RecentPotentialTypos.MostRecentFirst.ToArray(); Encryption.IPrivateKey privateAccountLogKey = null; try { foreach (LoginAttemptSummaryForTypoAnalysis potentialTypo in recentPotentialTypos) { bool usernameCorrect = potentialTypo.UsernameOrAccountId == account.UsernameOrAccountId; //bool usernameHadTypo = !usernameCorrect && // EditDistance.Calculate(potentialTypo.UsernameOrAccountId, account.UsernameOrAccountId) <= //_options.MaxEditDistanceConsideredATypo; if (usernameCorrect) // || usernameHadTypo { // Get the plaintext password from the previous login attempt string passwordSubmittedInFailedAttempt = null; if (account.GetType().Name == "SimulatedUserAccount") { passwordSubmittedInFailedAttempt = potentialTypo.EncryptedIncorrectPassword.Ciphertext; } else { if (privateAccountLogKey == null) { // Get the EC decryption key, which is stored encrypted with the Phase1 password hash try { privateAccountLogKey = accountController.DecryptPrivateAccountLogKey(account, phase1HashOfCorrectPassword); } catch (Exception) { // There's a problem with the key that prevents us from decrypting it. We won't be able to do this analysis. return; } } // Now try to decrypt the incorrect password from the previous attempt and perform the typo analysis try { // Attempt to decrypt the password. passwordSubmittedInFailedAttempt = potentialTypo.EncryptedIncorrectPassword.Read(privateAccountLogKey); } catch (Exception) { // An exception is likely due to an incorrect key (perhaps outdated). // Since we simply can't do anything with a record we can't Decrypt, we carry on // as if nothing ever happened. No. Really. Nothing to see here. } } if (passwordSubmittedInFailedAttempt != null) { //bool passwordCorrect = passwordSubmittedInFailedAttempt == correctPassword; bool passwordHadTypo = // !passwordCorrect && EditDistance.Calculate(passwordSubmittedInFailedAttempt, correctPassword) <= _options.MaxEditDistanceConsideredATypo; // Get credit for this nearly-correct attempt to counter penalty if (passwordHadTypo) { credit += potentialTypo.Penalty.GetValue(_options.AccountCreditLimitHalfLife, whenUtc) * (1d - _options.PenaltyMulitiplierForTypo); } } } // Now that we know whether this past event was a typo or not, we no longer need to keep track // of it (and we should remove it so we don't double credit it). clientsIpHistory.RecentPotentialTypos.Remove(potentialTypo); } // Remove the amount to be credited from the block score due to the discovery of typos clientsIpHistory.CurrentBlockScore.SubtractInPlace(account.CreditHalfLife, credit, whenUtc); } finally { privateAccountLogKey?.Dispose(); } }