Ejemplo n.º 1
0
        /// <summary>
        /// 添加记录
        /// </summary>
        /// <param name="ipHistory">实体类</param>
        /// <param name="delCache">添加成功后清理的CACHE key,支持正则</param>
        /// <param name="dbkey">存在数据库连接池中的连接key,为空时使用ConnString连接</param>
        /// <returns>添加是否成功</returns>
        public static bool Insert(IpHistory ipHistory, string dbkey = "", string[] delCache = null)
        {
            int obj = new SQL().Database(dbkey).Insert(IpHistory._)
                      .ValueP(IpHistory._IP, ipHistory.IP)
                      .ValueP(IpHistory._CreateTime, ipHistory.CreateTime)
                      .ToExec();

            if (delCache.IsNull())
            {
                return(obj == 1);
            }
            Cache2.Remove("TH.Mailer.IpHistoryCache_", delCache);
            return(obj == 1);
        }
Ejemplo n.º 2
0
        /// <summary>
        /// 修改多条记录
        /// </summary>
        /// <param name="iPList">IP地址列表,用“,”号分隔</param>
        /// <param name="ipHistory">实体类</param>
        /// <param name="where">修改时附加条件,统一的前面要加链接符(and、or等等)</param>
        /// <param name="delCache">修改成功后清理的CACHE key,支持正则</param>
        /// <param name="dbkey">存在数据库连接池中的连接key,为空时使用ConnString连接</param>
        /// <returns>修改是否成功</returns>
        public static bool UpdateByIDList(IEnumerable <string> iPList, IpHistory ipHistory, string dbkey = "", Where where = null, string[] delCache = null)
        {
            int value = new SQL().Database(dbkey).Update(IpHistory._)
                        .SetP(IpHistory._CreateTime, ipHistory.CreateTime)
                        .Where(new Where()
                               .And(IpHistory._IP, "(" + iPList.Join(",") + ")", Operator.In)
                               ).Where(where).ToExec();

            if (value <= 0)
            {
                return(false);
            }
            if (delCache.IsNull())
            {
                return(true);
            }
            Cache2.Remove("TH.Mailer.IpHistoryCache_", delCache);
            return(true);
        }
 public long AddIpHistory(IpHistoryViewModel model)
 {
     try
     {
         var IpHistory = new IpHistory()
         {
             UserId      = model.UserId,
             IpAddress   = model.IpAddress,
             CreatedDate = DateTime.UtcNow,
             CreatedBy   = model.UserId,
             Status      = 0,
             Location    = model.Location
         };
         _ipHistoryRepository.Insert(IpHistory);
         return(IpHistory.Id);
     }
     catch (Exception ex)
     {
         ex.ToString();
         throw;
     }
 }
Ejemplo n.º 4
0
        /// <summary>
        /// 修改记录
        /// </summary>
        /// <param name="ipHistory">实体类</param>
        /// <param name="where">修改时附加条件,统一的前面要加链接符(and、or等等)</param>
        /// <param name="delCache">修改成功后清理的CACHE key,支持正则</param>
        /// <param name="dbkey">存在数据库连接池中的连接key,为空时使用ConnString连接</param>
        /// <returns>修改是否成功</returns>
        public static bool Update(IpHistory ipHistory, string dbkey = "", Where where = null, string[] delCache = null)
        {
            if (ipHistory.IP.IsNullEmpty())
            {
                return(false);
            }
            int value = new SQL().Database(dbkey).Update(IpHistory._)
                        .SetP(IpHistory._CreateTime, ipHistory.CreateTime)
                        .Where(new Where()
                               .AndP(IpHistory._IP, ipHistory.IP, Operator.Equal, true)
                               ).Where(where).ToExec();

            if (value <= 0)
            {
                return(false);
            }
            if (delCache.IsNull())
            {
                return(true);
            }
            Cache2.Remove("TH.Mailer.IpHistoryCache_", delCache);
            return(true);
        }
Ejemplo n.º 5
0
 /// <summary>
 /// 添加记录
 /// </summary>
 /// <param name="ipHistory">实体类</param>
 /// <returns>添加是否成功</returns>
 public static bool Insert(IpHistory ipHistory, string dbkey)
 {
     return(Insert(ipHistory, dbkey, null));
 }
Ejemplo n.º 6
0
 /// <summary>
 /// 修改多条记录
 /// </summary>
 /// <param name="iPList">IP地址列表,用“,”号分隔</param>
 /// <param name="ipHistory">实体类</param>
 /// <returns>修改是否成功</returns>
 public static bool UpdateByIDList(IEnumerable <string> iPList, IpHistory ipHistory, string dbkey)
 {
     return(UpdateByIDList(iPList, ipHistory, dbkey, null, null));
 }
Ejemplo n.º 7
0
 /// <summary>
 /// 修改记录
 /// </summary>
 /// <param name="ipHistory">实体类</param>
 /// <returns>修改是否成功</returns>
 public static bool Update(IpHistory ipHistory, string dbkey)
 {
     return(Update(ipHistory, dbkey, null, null));
 }
Ejemplo n.º 8
0
        public async Task <LoginAttempt> DetermineLoginAttemptOutcomeAsync(
            LoginAttempt loginAttempt,
            string passwordProvidedByClient,
            byte[] phase1HashOfProvidedPassword = null,
            TimeSpan?timeout = null,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            Task <IpHistory> ipHistoryGetTask = _ipHistoryCache.GetAsync(loginAttempt.AddressOfClientInitiatingRequest,
                                                                         cancellationToken);

            IRepository <string, TUserAccount> userAccountRepository = _userAccountRepositoryFactory.Create();
            Task <TUserAccount> userAccountRequestTask = userAccountRepository.LoadAsync(loginAttempt.UsernameOrAccountId, cancellationToken);

            Task <int> passwordsHeightOnBinomialLadderTask =
                _binomialLadderFilter.GetHeightAsync(passwordProvidedByClient, cancellationToken: cancellationToken);


            IpHistory ip = await ipHistoryGetTask;


            TUserAccount account = await userAccountRequestTask;

            if (account != null)
            {
                try
                {
                    IUserAccountController <TUserAccount> userAccountController = _userAccountControllerFactory.Create();

                    loginAttempt.DeviceCookieHadPriorSuccessfulLoginForThisAccount = await
                                                                                     userAccountController.HasClientWithThisHashedCookieSuccessfullyLoggedInBeforeAsync(
                        account,
                        loginAttempt.HashOfCookieProvidedByBrowser,
                        cancellationToken);

                    if (phase1HashOfProvidedPassword == null)
                    {
                        phase1HashOfProvidedPassword =
                            userAccountController.ComputePhase1Hash(account, passwordProvidedByClient);
                    }


                    string phase2HashOfProvidedPassword =
                        userAccountController.ComputePhase2HashFromPhase1Hash(account, phase1HashOfProvidedPassword);

                    bool isSubmittedPasswordCorrect = phase2HashOfProvidedPassword == account.PasswordHashPhase2;


                    loginAttempt.PasswordsHeightOnBinomialLadder = await passwordsHeightOnBinomialLadderTask;

                    if (isSubmittedPasswordCorrect)
                    {
                        AdjustBlockingScoreForPastTyposTreatedAsFullFailures(
                            ip, userAccountController, account, loginAttempt.TimeOfAttemptUtc, passwordProvidedByClient,
                            phase1HashOfProvidedPassword);

                        double blockingThreshold = _options.BlockThresholdPopularPassword_T_base *
                                                   _options.PopularityBasedThresholdMultiplier_T_multiplier(
                            loginAttempt);
                        double blockScore = ip.CurrentBlockScore.GetValue(_options.BlockScoreHalfLife,
                                                                          loginAttempt.TimeOfAttemptUtc);

                        if (loginAttempt.DeviceCookieHadPriorSuccessfulLoginForThisAccount)
                        {
                            blockScore *= _options.MultiplierIfClientCookieIndicatesPriorSuccessfulLogin_Kappa;
                        }

                        if (blockScore > blockingThreshold)
                        {
                            loginAttempt.Outcome = AuthenticationOutcome.CredentialsValidButBlocked;
                        }
                        else
                        {
                            loginAttempt.Outcome = AuthenticationOutcome.CredentialsValid;
                            userAccountController.RecordHashOfDeviceCookieUsedDuringSuccessfulLoginBackground(
                                account,
                                loginAttempt.HashOfCookieProvidedByBrowser);

                            if (
                                ip.CurrentBlockScore.GetValue(_options.AccountCreditLimitHalfLife,
                                                              loginAttempt.TimeOfAttemptUtc) > 0)
                            {
                                TaskHelper.RunInBackground(Task.Run(async() =>
                                {
                                    double credit = await userAccountController.TryGetCreditAsync(
                                        account,
                                        _options.RewardForCorrectPasswordPerAccount_Sigma,
                                        loginAttempt.TimeOfAttemptUtc, cancellationToken);
                                    ip.CurrentBlockScore.SubtractInPlace(_options.AccountCreditLimitHalfLife, credit,
                                                                         loginAttempt.TimeOfAttemptUtc);
                                }, cancellationToken));
                            }
                        }
                    }
                    else
                    {
                        loginAttempt.Phase2HashOfIncorrectPassword = phase2HashOfProvidedPassword;
                        if (account.GetType().Name == "SimulatedUserAccount")
                        {
                            loginAttempt.EncryptedIncorrectPassword.Ciphertext = passwordProvidedByClient;
                        }
                        else
                        {
                            loginAttempt.EncryptedIncorrectPassword.Write(passwordProvidedByClient,
                                                                          account.EcPublicAccountLogKey);
                        }

                        if (await userAccountController.AddIncorrectPhaseTwoHashAsync(account, phase2HashOfProvidedPassword, cancellationToken: cancellationToken))
                        {
                            loginAttempt.Outcome = AuthenticationOutcome.CredentialsInvalidRepeatedIncorrectPassword;
                        }
                        else
                        {
                            loginAttempt.Outcome = AuthenticationOutcome.CredentialsInvalidIncorrectPassword;


                            double invalidPasswordPenalty = _options.PenaltyForInvalidPassword_Beta *
                                                            _options.PopularityBasedPenaltyMultiplier_phi(
                                loginAttempt);
                            ip.CurrentBlockScore.AddInPlace(_options.AccountCreditLimitHalfLife,
                                                            invalidPasswordPenalty,
                                                            loginAttempt.TimeOfAttemptUtc);

                            ip.RecentPotentialTypos.Add(new LoginAttemptSummaryForTypoAnalysis()
                            {
                                EncryptedIncorrectPassword = loginAttempt.EncryptedIncorrectPassword,
                                Penalty             = new DecayingDouble(invalidPasswordPenalty, loginAttempt.TimeOfAttemptUtc),
                                UsernameOrAccountId = loginAttempt.UsernameOrAccountId
                            });
                        }
                    }
                }
                finally
                {
                    TaskHelper.RunInBackground(userAccountRepository.SaveChangesAsync(cancellationToken));
                }
            }
            else
            {
                if (phase1HashOfProvidedPassword == null)
                {
                    phase1HashOfProvidedPassword =
                        ExpensiveHashFunctionFactory.Get(_options.DefaultExpensiveHashingFunction)(
                            passwordProvidedByClient,
                            ManagedSHA256.Hash(Encoding.UTF8.GetBytes(loginAttempt.UsernameOrAccountId)),
                            _options.ExpensiveHashingFunctionIterations);
                }

                loginAttempt.PasswordsHeightOnBinomialLadder = await passwordsHeightOnBinomialLadderTask;



                if (_recentIncorrectPasswords.AddMember(Convert.ToBase64String(phase1HashOfProvidedPassword)))
                {
                    loginAttempt.Outcome = AuthenticationOutcome.CredentialsInvalidRepeatedNoSuchAccount;
                }
                else
                {
                    loginAttempt.Outcome = AuthenticationOutcome.CredentialsInvalidNoSuchAccount;
                    double invalidAccontPenalty = _options.PenaltyForInvalidAccount_Alpha *
                                                  _options.PopularityBasedPenaltyMultiplier_phi(loginAttempt);
                    ip.CurrentBlockScore.AddInPlace(_options.BlockScoreHalfLife, invalidAccontPenalty,
                                                    loginAttempt.TimeOfAttemptUtc);
                }
            }


            if (loginAttempt.Outcome == AuthenticationOutcome.CredentialsInvalidNoSuchAccount ||
                loginAttempt.Outcome == AuthenticationOutcome.CredentialsInvalidIncorrectPassword)
            {
                TaskHelper.RunInBackground(_binomialLadderFilter.StepAsync(passwordProvidedByClient, cancellationToken: cancellationToken));
            }

            return(loginAttempt);
        }
Ejemplo n.º 9
0
        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();
            }
        }
Ejemplo n.º 10
0
        /// <returns></returns>
        /// <summary>
        /// Add a LoginAttempt, along the way determining whether that loginAttempt should be allowed
        /// (the user authenticated) or denied.
        /// </summary>
        /// <param name="loginAttempt">The login loginAttempt record to be stored.</param>
        /// <param name="passwordProvidedByClient">The plaintext password provided by the client.</param>
        /// <param name="phase1HashOfProvidedPassword">If the caller has already computed the phase 1 (expensive) hash of the submitted password,
        /// it can supply it via this optional parameter to avoid incurring the cost of having incurring the expense of this calculationg a second time.</param>
        /// <param name="timeout"></param>
        /// <param name="cancellationToken">To allow this async method to be cancelled.</param>
        /// <returns>If the password is correct and the IP not blocked, returns AuthenticationOutcome.CredentialsValid.
        /// Otherwise, it returns a different AuthenticationOutcome.
        /// The client should not be made aware of any information beyond whether the login was allowed or not.</returns>
        public async Task <LoginAttempt> DetermineLoginAttemptOutcomeAsync(
            LoginAttempt loginAttempt,
            string passwordProvidedByClient,
            byte[] phase1HashOfProvidedPassword = null,
            TimeSpan?timeout = null,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            //
            // In parallel, fetch information we'll need to determine the outcome
            //

            // Get information about the client's IP
            Task <IpHistory> ipHistoryGetTask = _ipHistoryCache.GetAsync(loginAttempt.AddressOfClientInitiatingRequest,
                                                                         cancellationToken);

            // Get information about the account the client is trying to login to
            //IStableStoreContext<string, UserAccount> userAccountContext = _userAccountContextFactory.Get();
            IRepository <string, TUserAccount> userAccountRepository = _userAccountRepositoryFactory.Create();
            Task <TUserAccount> userAccountRequestTask = userAccountRepository.LoadAsync(loginAttempt.UsernameOrAccountId, cancellationToken);

            // Get a binomial ladder to estimate if the password is common
            Task <int> passwordsHeightOnBinomialLadderTask =
                _binomialLadderFilter.GetHeightAsync(passwordProvidedByClient, cancellationToken: cancellationToken);


            //
            // Start processing information as it comes in
            //

            // Preform an analysis of the IPs past beavhior to determine if the IP has been performing so many failed guesses
            // that we disallow logins even if it got the right password.  We call this even when the submitted password is
            // correct lest we create a timing indicator (slower responses for correct passwords) that attackers could use
            // to guess passwords even if we'd blocked their IPs.
            IpHistory ip = await ipHistoryGetTask;

            // We'll need the salt from the account record before we can calculate the expensive hash,
            // so await that task first
            TUserAccount account = await userAccountRequestTask;

            if (account != null)
            {
                try
                {
                    IUserAccountController <TUserAccount> userAccountController = _userAccountControllerFactory.Create();
                    //
                    // This is an login attempt for a valid (existent) account.
                    //

                    // Determine whether the client provided a cookie to indicate that it has previously logged
                    // into this account successfully---a very strong indicator that it is a client used by the
                    // legitimate user and not an unknown client performing a guessing attack.
                    loginAttempt.DeviceCookieHadPriorSuccessfulLoginForThisAccount = await
                                                                                     userAccountController.HasClientWithThisHashedCookieSuccessfullyLoggedInBeforeAsync(
                        account,
                        loginAttempt.HashOfCookieProvidedByBrowser,
                        cancellationToken);

                    // Test to see if the password is correct by calculating the Phase2Hash and comparing it with the Phase2 hash
                    // in this record.  The expensive (phase1) hash which is used to encrypt the EC public key for this account
                    // (which we use to store the encryptions of incorrect passwords)
                    if (phase1HashOfProvidedPassword == null)
                    {
                        phase1HashOfProvidedPassword =
                            userAccountController.ComputePhase1Hash(account, passwordProvidedByClient);
                    }

                    // Since we can't store the phase1 hash (it can decrypt that EC key) we instead store a simple (SHA256)
                    // hash of the phase1 hash, which we call the phase 2 hash, and use that to compare the provided password
                    // with the correct password.
                    string phase2HashOfProvidedPassword =
                        userAccountController.ComputePhase2HashFromPhase1Hash(account, phase1HashOfProvidedPassword);

                    // To determine if the password is correct, compare the phase2 has we just generated (phase2HashOfProvidedPassword)
                    // with the one generated from the correct password when the user chose their password (account.PasswordHashPhase2).
                    bool isSubmittedPasswordCorrect = phase2HashOfProvidedPassword == account.PasswordHashPhase2;

                    // Get the popularity of the password provided by the client among incorrect passwords submitted in the past,
                    // as we are most concerned about frequently-guessed passwords.
                    loginAttempt.PasswordsHeightOnBinomialLadder = await passwordsHeightOnBinomialLadderTask;

                    if (isSubmittedPasswordCorrect)
                    {
                        // The password is corerct.

                        // Determine if any of the outcomes for login attempts from the client IP for this request were the result of typos,
                        // as this might impact our decision about whether or not to block this client IP in response to its past behaviors.
                        AdjustBlockingScoreForPastTyposTreatedAsFullFailures(
                            ip, userAccountController, account, loginAttempt.TimeOfAttemptUtc, passwordProvidedByClient,
                            phase1HashOfProvidedPassword);

                        // We'll get the blocking threshold, blocking condition, and block if the condition exceeds the threshold.
                        double blockingThreshold = _options.BlockThresholdPopularPassword_T_base *
                                                   _options.PopularityBasedThresholdMultiplier_T_multiplier(
                            loginAttempt);
                        double blockScore = ip.CurrentBlockScore.GetValue(_options.BlockScoreHalfLife,
                                                                          loginAttempt.TimeOfAttemptUtc);

                        // If the client provided a cookie proving a past successful login, we'll ignore the block condition
                        if (loginAttempt.DeviceCookieHadPriorSuccessfulLoginForThisAccount)
                        {
                            blockScore *= _options.MultiplierIfClientCookieIndicatesPriorSuccessfulLogin_Kappa;
                        }

                        if (blockScore > blockingThreshold)
                        {
                            // While this login attempt had valid credentials, the circumstances
                            // are so suspicious that we should block the login and pretend the
                            // credentials were invalid
                            loginAttempt.Outcome = AuthenticationOutcome.CredentialsValidButBlocked;
                        }
                        else
                        {
                            // This login attempt has valid credentials and no reason to block, so the
                            // client will be authenticated.
                            loginAttempt.Outcome = AuthenticationOutcome.CredentialsValid;
                            userAccountController.RecordHashOfDeviceCookieUsedDuringSuccessfulLoginBackground(
                                account,
                                loginAttempt.HashOfCookieProvidedByBrowser);

                            // Use this login attempt to offset harm caused by prior login failures
                            if (
                                ip.CurrentBlockScore.GetValue(_options.AccountCreditLimitHalfLife,
                                                              loginAttempt.TimeOfAttemptUtc) > 0)
                            {
                                // There is a non-zero blocking score that might be counteracted by a credit
                                TaskHelper.RunInBackground(Task.Run(async() =>
                                {
                                    double credit = await userAccountController.TryGetCreditAsync(
                                        account,
                                        _options.RewardForCorrectPasswordPerAccount_Sigma,
                                        loginAttempt.TimeOfAttemptUtc, cancellationToken);
                                    ip.CurrentBlockScore.SubtractInPlace(_options.AccountCreditLimitHalfLife, credit,
                                                                         loginAttempt.TimeOfAttemptUtc);
                                }, cancellationToken));
                            }
                        }
                    }
                    else
                    {
                        //
                        // The password was invalid.  Do bookkeeping of information about this failure so that we can
                        // block the origin IP if it appears to be engaged in guessing and so that we can track
                        // frequently guessed passwords.

                        // We'll not only store the (phase 2) hash of the incorrect password, but we'll also store
                        // the incorrect passwords itself, encrypted with the EcPublicAccountLogKey.
                        // (The decryption key to get the incorrect password plaintext back is encrypted with the
                        //  correct password, so you can't get to the plaintext of the incorrect password if you
                        //  don't already know the correct password.)
                        loginAttempt.Phase2HashOfIncorrectPassword = phase2HashOfProvidedPassword;
                        if (account.GetType().Name == "SimulatedUserAccount")
                        {
                            loginAttempt.EncryptedIncorrectPassword.Ciphertext = passwordProvidedByClient;
                        }
                        else
                        {
                            loginAttempt.EncryptedIncorrectPassword.Write(passwordProvidedByClient,
                                                                          account.EcPublicAccountLogKey);
                        }
                        // Next, if it's possible to declare more about this outcome than simply that the
                        // user provided the incorrect password, let's do so.
                        // Since users who are unsure of their passwords may enter the same username/password twice, but attackers
                        // don't learn anything from doing so, we'll want to account for these repeats differently (and penalize them less).
                        // We actually have two data structures for catching this: A large sketch of clientsIpHistory/account/password triples and a
                        // tiny LRU cache of recent failed passwords for this account.  We'll check both.
                        if (await userAccountController.AddIncorrectPhaseTwoHashAsync(account, phase2HashOfProvidedPassword, cancellationToken: cancellationToken))
                        {
                            // The same incorrect password was recently used for this account, indicate this so that we
                            // do not penalize the IP further (as attackers don't gain anything from guessing the wrong password again).
                            loginAttempt.Outcome = AuthenticationOutcome.CredentialsInvalidRepeatedIncorrectPassword;
                        }
                        else
                        {
                            // This is the first time we've (at least recently) seen this incorrect password attempted for the account,
                            loginAttempt.Outcome = AuthenticationOutcome.CredentialsInvalidIncorrectPassword;

                            // Penalize the IP for the invalid password
                            double invalidPasswordPenalty = _options.PenaltyForInvalidPassword_Beta *
                                                            _options.PopularityBasedPenaltyMultiplier_phi(
                                loginAttempt);
                            ip.CurrentBlockScore.AddInPlace(_options.AccountCreditLimitHalfLife,
                                                            invalidPasswordPenalty,
                                                            loginAttempt.TimeOfAttemptUtc);
                            // Record the penalty so that it can be reduced if this incorrect password is later discovered to be a typo.
                            ip.RecentPotentialTypos.Add(new LoginAttemptSummaryForTypoAnalysis()
                            {
                                EncryptedIncorrectPassword = loginAttempt.EncryptedIncorrectPassword,
                                Penalty             = new DecayingDouble(invalidPasswordPenalty, loginAttempt.TimeOfAttemptUtc),
                                UsernameOrAccountId = loginAttempt.UsernameOrAccountId
                            });
                        }
                    }
                }
                finally
                {
                    // Save changes to the user account record in the background (so that we don't hold up returning the result)
                    TaskHelper.RunInBackground(userAccountRepository.SaveChangesAsync(cancellationToken));
                }
            }
            else
            {
                // account == null
                // This is an login attempt for an INvalid (NONexistent) account.
                //
                if (phase1HashOfProvidedPassword == null)
                {
                    phase1HashOfProvidedPassword =
                        ExpensiveHashFunctionFactory.Get(_options.DefaultExpensiveHashingFunction)(
                            passwordProvidedByClient,
                            ManagedSHA256.Hash(Encoding.UTF8.GetBytes(loginAttempt.UsernameOrAccountId)),
                            _options.ExpensiveHashingFunctionIterations);
                }

                // Get the popularity of the password provided by the client among incorrect passwords submitted in the past,
                // as we are most concerned about frequently-guessed passwords.
                loginAttempt.PasswordsHeightOnBinomialLadder = await passwordsHeightOnBinomialLadderTask;

                // This appears to be an loginAttempt to login to a non-existent account, and so all we need to do is
                // mark it as such.  However, since it's possible that users will forget their account names and
                // repeatedly loginAttempt to login to a nonexistent account, we'll want to track whether we've seen
                // this account/password double before and note in the outcome if it's a repeat so that.
                // the IP need not be penalized for issuign a query that isn't getting it any information it
                // didn't already have.

                if (_recentIncorrectPasswords.AddMember(Convert.ToBase64String(phase1HashOfProvidedPassword)))
                {
                    // Don't penalize the incorrect <invalid account/password> pair if we've seen the same
                    // pair recently
                    loginAttempt.Outcome = AuthenticationOutcome.CredentialsInvalidRepeatedNoSuchAccount;
                }
                else
                {
                    // Penalize the IP for a login attempt with an invalid account
                    loginAttempt.Outcome = AuthenticationOutcome.CredentialsInvalidNoSuchAccount;
                    double invalidAccontPenalty = _options.PenaltyForInvalidAccount_Alpha *
                                                  _options.PopularityBasedPenaltyMultiplier_phi(loginAttempt);
                    ip.CurrentBlockScore.AddInPlace(_options.BlockScoreHalfLife, invalidAccontPenalty,
                                                    loginAttempt.TimeOfAttemptUtc);
                }
            }


            if (loginAttempt.Outcome == AuthenticationOutcome.CredentialsInvalidNoSuchAccount ||
                loginAttempt.Outcome == AuthenticationOutcome.CredentialsInvalidIncorrectPassword)
            {
                // Record the invalid password into the binomial ladder sketch that tracks freqeunt-incorrect passwords
                // Since we don't need to know the result, we'll run it in the background (so that we don't hold up returning the result)
                TaskHelper.RunInBackground(_binomialLadderFilter.StepAsync(passwordProvidedByClient, cancellationToken: cancellationToken));
            }

            return(loginAttempt);
        }
Ejemplo n.º 11
0
        /// <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();
            }
        }