| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/chrome/browser/passwords/password_checkup_utils.h" |
| |
| #import "base/ranges/algorithm.h" |
| #import "base/strings/sys_string_conversions.h" |
| #import "base/strings/utf_string_conversions.h" |
| #import "base/time/time.h" |
| #import "components/password_manager/core/browser/ui/credential_ui_entry.h" |
| #import "components/password_manager/core/common/password_manager_features.h" |
| #import "ios/chrome/grit/ios_strings.h" |
| #import "ui/base/l10n/l10n_util.h" |
| #import "ui/base/l10n/l10n_util_mac.h" |
| #import "ui/base/l10n/time_format.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| using password_manager::CredentialUIEntry; |
| |
| namespace { |
| |
| // Amount of time after which the timestamp is shown instead of "just now". |
| constexpr base::TimeDelta kJustCheckedTimeThreshold = base::Minutes(1); |
| |
| } // anonymous namespace |
| |
| namespace password_manager { |
| |
| bool operator==(const InsecurePasswordCounts& lhs, |
| const InsecurePasswordCounts& rhs) { |
| std::tuple lhs_tuple = std::tie(lhs.compromised_count, lhs.dismissed_count, |
| lhs.reused_count, lhs.weak_count); |
| std::tuple rhs_tuple = std::tie(rhs.compromised_count, rhs.dismissed_count, |
| rhs.reused_count, rhs.weak_count); |
| return lhs_tuple == rhs_tuple; |
| } |
| |
| bool IsCredentialUnmutedCompromised(const CredentialUIEntry& credential) { |
| return IsCompromised(credential) && !credential.IsMuted(); |
| } |
| |
| WarningType GetWarningOfHighestPriority( |
| const std::vector<CredentialUIEntry>& insecure_credentials) { |
| // Using a set to make sure that the `has_reused_passwords` flag is only set |
| // to `true` if there is at least a reused group of two passwords. |
| // TODO(crbug.com/1434343): This is a temporary solution to filter out the |
| // reused password groups with only one remaining password. |
| std::unordered_set<std::u16string> reused_passwords_set; |
| bool has_reused_passwords = false; |
| bool has_weak_passwords = false; |
| bool has_muted_warnings = false; |
| |
| for (const auto& credential : insecure_credentials) { |
| if (credential.IsMuted()) { |
| has_muted_warnings = true; |
| } else if (IsCompromised(credential)) { |
| return WarningType::kCompromisedPasswordsWarning; |
| } |
| |
| // A reused password warning is of higher priority than a weak password |
| // warning. So, if the credential is reused, there is no need to verify if |
| // it is also weak. |
| if (credential.IsReused()) { |
| if (reused_passwords_set.find(credential.password) != |
| reused_passwords_set.end()) { |
| has_reused_passwords = true; |
| } else { |
| reused_passwords_set.insert(credential.password); |
| } |
| } else if (credential.IsWeak()) { |
| has_weak_passwords = true; |
| } |
| } |
| |
| if (has_reused_passwords) { |
| return WarningType::kReusedPasswordsWarning; |
| } else if (has_weak_passwords) { |
| return WarningType::kWeakPasswordsWarning; |
| } else if (has_muted_warnings) { |
| return WarningType::kDismissedWarningsWarning; |
| } |
| |
| return WarningType::kNoInsecurePasswordsWarning; |
| } |
| |
| InsecurePasswordCounts CountInsecurePasswordsPerInsecureType( |
| const std::vector<password_manager::CredentialUIEntry>& |
| insecure_credentials) { |
| InsecurePasswordCounts counts{}; |
| std::map<std::u16string, int> reused_passwords; |
| for (const auto& credential : insecure_credentials) { |
| // If a compromised credential is muted, we don't want to take it into |
| // account in the compromised count. |
| if (credential.IsMuted()) { |
| counts.dismissed_count++; |
| } else if (IsCompromised(credential)) { |
| counts.compromised_count++; |
| } |
| if (credential.IsReused()) { |
| reused_passwords[credential.password]++; |
| } |
| if (credential.IsWeak()) { |
| counts.weak_count++; |
| } |
| } |
| |
| // TODO(crbug.com/1434343): This is a temporary solution to filter out the |
| // reused password groups with only one remaining password. |
| for (const auto& password : reused_passwords) { |
| if (password.second > 1) { |
| counts.reused_count += password.second; |
| } |
| } |
| |
| return counts; |
| } |
| |
| int GetPasswordCountForWarningType( |
| WarningType warningType, |
| const std::vector<password_manager::CredentialUIEntry>& |
| insecure_credentials) { |
| InsecurePasswordCounts counts = |
| CountInsecurePasswordsPerInsecureType(insecure_credentials); |
| switch (warningType) { |
| case WarningType::kCompromisedPasswordsWarning: |
| return counts.compromised_count; |
| case WarningType::kReusedPasswordsWarning: |
| return counts.reused_count; |
| case WarningType::kWeakPasswordsWarning: |
| return counts.weak_count; |
| case WarningType::kDismissedWarningsWarning: |
| return counts.dismissed_count; |
| case WarningType::kNoInsecurePasswordsWarning: |
| return 0; |
| } |
| } |
| |
| NSString* FormatElapsedTimeSinceLastCheck( |
| absl::optional<base::Time> last_completed_check, |
| bool use_title_case) { |
| if (!last_completed_check.has_value()) { |
| // The title case format is only used in the Password Checkup Homepage as of |
| // now and it is currently not possible to reach this page if no check has |
| // yet been completed. There is therefore no need for now to have a title |
| // case version of "Check never run." |
| return l10n_util::GetNSString(IDS_IOS_CHECK_NEVER_RUN); |
| } |
| |
| base::TimeDelta elapsed_time = |
| base::Time::Now() - last_completed_check.value(); |
| |
| std::u16string timestamp; |
| // If check finished in less than `kJustCheckedTimeThreshold` show |
| // "just now" instead of timestamp. |
| if (elapsed_time < kJustCheckedTimeThreshold) { |
| timestamp = l10n_util::GetStringUTF16( |
| use_title_case ? IDS_IOS_CHECK_FINISHED_JUST_NOW_TITLE_CASE |
| : IDS_IOS_CHECK_FINISHED_JUST_NOW); |
| } else { |
| timestamp = ui::TimeFormat::SimpleWithMonthAndYear( |
| use_title_case ? ui::TimeFormat::FORMAT_TITLE_CASE_ELAPSED |
| : ui::TimeFormat::FORMAT_ELAPSED, |
| ui::TimeFormat::LENGTH_LONG, elapsed_time, true); |
| } |
| |
| return features::IsPasswordCheckupEnabled() |
| ? l10n_util::GetNSStringF( |
| IDS_IOS_PASSWORD_CHECKUP_LAST_COMPLETED_CHECK, timestamp) |
| : l10n_util::GetNSStringF(IDS_IOS_LAST_COMPLETED_CHECK, timestamp); |
| } |
| |
| std::vector<CredentialUIEntry> GetPasswordsForWarningType( |
| WarningType warning_type, |
| const std::vector<CredentialUIEntry>& insecure_credentials) { |
| std::vector<CredentialUIEntry> filtered_credentials; |
| |
| switch (warning_type) { |
| case WarningType::kCompromisedPasswordsWarning: |
| base::ranges::copy_if(insecure_credentials, |
| std::back_inserter(filtered_credentials), |
| IsCredentialUnmutedCompromised); |
| break; |
| case WarningType::kWeakPasswordsWarning: |
| base::ranges::copy_if(insecure_credentials, |
| std::back_inserter(filtered_credentials), |
| std::mem_fn(&CredentialUIEntry::IsWeak)); |
| break; |
| case WarningType::kReusedPasswordsWarning: { |
| // TODO(crbug.com/1434343): This is a temporary solution to filter out the |
| // reused password groups with only one remaining password. |
| std::map<std::u16string, std::vector<CredentialUIEntry>> reused_passwords; |
| for (const auto& credential : insecure_credentials) { |
| if (credential.IsReused()) { |
| reused_passwords[credential.password].push_back(credential); |
| } |
| } |
| for (const auto& password : reused_passwords) { |
| if (password.second.size() > 1) { |
| filtered_credentials.insert(filtered_credentials.end(), |
| password.second.begin(), |
| password.second.end()); |
| } |
| } |
| break; |
| } |
| case WarningType::kDismissedWarningsWarning: |
| base::ranges::copy_if(insecure_credentials, |
| std::back_inserter(filtered_credentials), |
| std::mem_fn(&CredentialUIEntry::IsMuted)); |
| break; |
| case WarningType::kNoInsecurePasswordsWarning: |
| NOTREACHED_NORETURN(); |
| } |
| |
| return filtered_credentials; |
| } |
| |
| } // namespace password_manager |