blob: 4eaec0b368b2ef7825f86517f348d8d55733b650 [file]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "crypto/apple/keychain_secitem.h"
#import <Foundation/Foundation.h>
#import <array>
#import "base/apple/bridging.h"
#import "base/apple/foundation_util.h"
#import "base/apple/scoped_cftyperef.h"
#import "base/containers/span.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/scoped_feature_list.h"
#import "crypto/features.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/platform_test.h"
namespace {
using base::apple::CFToNSPtrCast;
// Use unique names for the test item to avoid conflicts.
const char kTestServiceName[] = "KeychainAccessibilityMigrationTest.Service";
const char kTestAccountName[] = "KeychainAccessibilityMigrationTest.Account";
const auto kTestPassword =
std::to_array<uint8_t>({'s', 'e', 'c', 'r', 'e', 't'});
} // namespace
class KeychainAccessibilityMigrationIOSTest : public PlatformTest {
public:
KeychainAccessibilityMigrationIOSTest() = default;
void SetUp() override {
PlatformTest::SetUp();
// Get current test name to distinguish PRE_ phase.
const std::string& test_name =
testing::UnitTest::GetInstance()->current_test_info()->name();
if (test_name.rfind("PRE_", 0) == 0) {
feature_list_.InitAndDisableFeature(
crypto::features::kMigrateIOSKeychainAccessibility);
} else {
feature_list_.InitAndEnableFeature(
crypto::features::kMigrateIOSKeychainAccessibility);
}
}
static void TearDownTestSuite() {
// Clean up the keychain item after all tests in the suite have run.
DeleteGenericPasswordForTest(kTestServiceName, kTestAccountName);
}
protected:
// Gets the accessibility attribute of the test keychain item.
base::expected<std::string, OSStatus> GetKeychainItemAccessibility() {
NSDictionary* query = @{
CFToNSPtrCast(kSecClass) : CFToNSPtrCast(kSecClassGenericPassword),
CFToNSPtrCast(kSecAttrService) :
base::SysUTF8ToNSString(kTestServiceName),
CFToNSPtrCast(kSecAttrAccount) :
base::SysUTF8ToNSString(kTestAccountName),
CFToNSPtrCast(kSecReturnAttributes) : @YES,
};
base::apple::ScopedCFTypeRef<CFTypeRef> result;
OSStatus status = SecItemCopyMatching(base::apple::NSToCFPtrCast(query),
result.InitializeInto());
if (status != noErr) {
return base::unexpected(status);
}
CFDictionaryRef result_dict =
base::apple::CFCast<CFDictionaryRef>(result.get());
return base::SysCFStringRefToUTF8(
base::apple::GetValueFromDictionary<CFStringRef>(result_dict,
kSecAttrAccessible));
}
static void DeleteGenericPasswordForTest(std::string_view service_name,
std::string_view account_name) {
NSDictionary* query = @{
CFToNSPtrCast(kSecClass) : CFToNSPtrCast(kSecClassGenericPassword),
CFToNSPtrCast(kSecAttrService) : base::SysUTF8ToNSString(service_name),
CFToNSPtrCast(kSecAttrAccount) : base::SysUTF8ToNSString(account_name),
};
SecItemDelete(base::apple::NSToCFPtrCast(query));
}
crypto::apple::KeychainSecItem keychain_;
base::test::ScopedFeatureList feature_list_;
};
// Phase 1: Add keychain item with kMigrateIOSKeychainAccessibility disabled.
// The item will be stored using kSecAttrAccessibleWhenUnlocked.
TEST_F(KeychainAccessibilityMigrationIOSTest, PRE_MigrateItem) {
// Verify that the keychain item doesn't already exist. This is a check
// that the test suite's cleanup is working correctly.
base::expected<std::vector<uint8_t>, OSStatus> result_before =
keychain_.FindGenericPassword(kTestServiceName, kTestAccountName);
ASSERT_FALSE(result_before.has_value())
<< "Keychain item existed before test. Prior test state not cleaned up.";
// Verify feature is disabled as configured in SetUp.
ASSERT_FALSE(base::FeatureList::IsEnabled(
crypto::features::kMigrateIOSKeychainAccessibility));
// Add a password to the keychain.
OSStatus status = keychain_.AddGenericPassword(
kTestServiceName, kTestAccountName, kTestPassword);
ASSERT_EQ(status, noErr)
<< "PRE_MigrateItem: AddGenericPassword failed with OSStatus " << status;
// Sanity check: ensure the password can be retrieved immediately.
base::expected<std::vector<uint8_t>, OSStatus> result =
keychain_.FindGenericPassword(kTestServiceName, kTestAccountName);
ASSERT_TRUE(result.has_value())
<< "PRE_MigrateItem: FindGenericPassword failed. OSStatus: "
<< result.error();
EXPECT_EQ(base::as_byte_span(result.value()),
base::as_byte_span(kTestPassword));
}
// Phase 2: Read keychain item with kMigrateIOSKeychainAccessibility enabled.
// This should find the item and trigger an update of its accessibility
// attribute to kSecAttrAccessibleAfterFirstUnlock within FindGenericPassword.
TEST_F(KeychainAccessibilityMigrationIOSTest, MigrateItem) {
// Verify feature is enabled as configured in SetUp.
ASSERT_TRUE(base::FeatureList::IsEnabled(
crypto::features::kMigrateIOSKeychainAccessibility));
// Check that the accessibility is `kSecAttrAccessibleWhenUnlocked` before
// migration.
base::expected<std::string, OSStatus> accessibility_before =
GetKeychainItemAccessibility();
ASSERT_TRUE(accessibility_before.has_value());
EXPECT_EQ(accessibility_before.value(),
base::SysCFStringRefToUTF8(kSecAttrAccessibleWhenUnlocked));
// Attempt to find the password. This call should succeed and trigger the
// migration.
base::expected<std::vector<uint8_t>, OSStatus> result =
keychain_.FindGenericPassword(kTestServiceName, kTestAccountName);
ASSERT_TRUE(result.has_value())
<< "MigrateItem: FindGenericPassword failed. OSStatus: "
<< result.error();
EXPECT_EQ(base::as_byte_span(result.value()),
base::as_byte_span(kTestPassword));
// Check that the accessibility is `kSecAttrAccessibleAfterFirstUnlock` after
// migration.
base::expected<std::string, OSStatus> accessibility_after =
GetKeychainItemAccessibility();
ASSERT_TRUE(accessibility_after.has_value());
EXPECT_EQ(accessibility_after.value(),
base::SysCFStringRefToUTF8(kSecAttrAccessibleAfterFirstUnlock));
}