Skip to content

Commit

Permalink
Merge pull request #193 from square/dfed--fix-shared-access-group-cat…
Browse files Browse the repository at this point in the history
…alina

Fix keychains that rely on kSecAttrAccessGroup on macOS Catalina
  • Loading branch information
dfed authored Jan 14, 2020
2 parents 50e134e + ad3ab18 commit 7447d16
Show file tree
Hide file tree
Showing 10 changed files with 120 additions and 11 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ let package = Package(
],
swiftLanguageVersions: [.v4, .v4_2, .v5]
)
let version = Version(3, 2, 7)
let version = Version(3, 2, 8)
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ However, because the Keychain is effectively disk storage, there is no guarantee
Already using the Keychain and no longer want to maintain your own Keychain code? We feel you. That’s why we wrote `migrateObjects(matching query: [String : AnyHashable], removeOnCompletion: Bool)`. This method allows you to migrate all your existing Keychain entries to a Valet instance in one line. Just pass in a Dictionary with the `kSecClass`, `kSecAttrService`, and any other `kSecAttr*` attributes you use – we’ll migrate the data for you.
### Supporting macOS Catalina (10.15) and later
If your macOS application supports macOS 10.14 or prior, you must run `myValet.migrateObjectsFromPreCatalina()` before reading values from a Valet. macOS Catalina introduced a breaking change to the macOS keychain, requiring that macOS keychain items that utilize `kSecAttrAccessible` or `kSecAttrAccessGroup` set `kSecUseDataProtectionKeychain` to `true` [when writing or accessing](https://developer.apple.com/documentation/security/ksecusedataprotectionkeychain) these items. Valet’s `migrateObjectsFromPreCatalina()` upgrades items entered into the keychain on older macOS devices or other operating systems to include the key:value pair `kSecUseDataProtectionKeychain:true`. Note that Valets that share keychain items between devices with iCloud are exempt from this requirement. Similarly, `SecureEnclaveValet` and `SinglePromptSecureEnclaveValet` are exempt from this requirement.
### Debugging
Valet guarantees it will never fail to write to or read from the keychain unless `canAccessKeychain()` returns `false`. There are only a few cases that can lead to the keychain being inaccessible:
Expand Down
21 changes: 15 additions & 6 deletions Sources/Valet/Internal/SecItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,23 +70,32 @@ internal final class SecItem {

/// Programatically grab the required prefix for the shared access group (i.e. Bundle Seed ID). The value for the kSecAttrAccessGroup key in queries for data that is shared between apps must be of the format bundleSeedID.sharedAccessGroup. For more information on the Bundle Seed ID, see https://developer.apple.com/library/ios/qa/qa1713/_index.html
internal static var sharedAccessGroupPrefix: String {
let query = [
var query: [CFString : Any] = [
kSecClass : kSecClassGenericPassword,
kSecAttrAccount : "SharedAccessGroupAlwaysAccessiblePrefixPlaceholder",
kSecReturnAttributes : true,
kSecAttrAccessible : Accessibility.alwaysThisDeviceOnly.secAccessibilityAttribute
] as CFDictionary

kSecAttrAccessible : Accessibility.alwaysThisDeviceOnly.secAccessibilityAttribute,
]

if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
// Add kSecUseDataProtectionKeychain to the query to ensure we can retrieve the shared access group prefix.
#if swift(>=5.1)
query[kSecUseDataProtectionKeychain] = true
#else
query["nleg" as CFString] = true // kSecUseDataProtectionKeychain for Xcode 9 and Xcode 10 compatibility.
#endif
}

secItemLock.lock()
defer {
secItemLock.unlock()
}

var result: AnyObject? = nil
var status = SecItemCopyMatching(query, &result)
var status = SecItemCopyMatching(query as CFDictionary, &result)

if status == errSecItemNotFound {
status = SecItemAdd(query, &result)
status = SecItemAdd(query as CFDictionary, &result)
}

guard status == errSecSuccess, let queryResult = result as? [CFString : AnyHashable], let accessGroup = queryResult[kSecAttrAccessGroup] as? String else {
Expand Down
12 changes: 10 additions & 2 deletions Sources/Valet/Internal/Service.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,17 @@ internal enum Service: CustomStringConvertible, Equatable {
internal func generateBaseQuery() -> [String : AnyHashable] {
var baseQuery: [String : AnyHashable] = [
kSecClass as String : kSecClassGenericPassword as String,
kSecAttrService as String : secService
kSecAttrService as String : secService,
]


if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
#if swift(>=5.1)
baseQuery[kSecUseDataProtectionKeychain as String] = true
#else
baseQuery["nleg"] = true // kSecUseDataProtectionKeychain for Xcode 9 and Xcode 10 compatibility.
#endif
}

let configuration: Configuration
switch self {
case let .standard(_, desiredConfiguration):
Expand Down
19 changes: 19 additions & 0 deletions Sources/Valet/Valet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,25 @@ public final class Valet: NSObject, KeychainQueryConvertible {
return migrateObjects(matching: keychain.keychainQuery, removeOnCompletion: removeOnCompletion)
}

#if os(macOS)
/// Migrates objects that were written to this Valet prior to macOS 10.15 to a format that can be read on macOS 10.15 and later. The new format is backwards compatible, allowing these values to be read on older operating systems.
/// - returns: Whether the migration succeeded or failed.
/// - note: The keychain is not modified if a failure occurs. This method can only be called from macOS 10.15 or later.
@available(macOS 10.15, *)
@objc(migrateObjectsFromPreCatalina)
public func migrateObjectsFromPreCatalina() -> MigrationResult {
var baseQuery = keychainQuery
#if swift(>=5.1)
baseQuery[kSecUseDataProtectionKeychain as String] = false
#else
baseQuery["nleg"] = false // kSecUseDataProtectionKeychain for Xcode 9 and Xcode 10 compatibility.
#endif

// We do not need to remove these items on completion, since we are updating the kSecUseDataProtectionKeychain attribute in-place.
return migrateObjects(matching: baseQuery, removeOnCompletion: false)
}
#endif

// MARK: Internal Properties

internal let configuration: Configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ class ValetBackwardsCompatibilityIntegrationTests: ValetIntegrationTests {
legacyValet.setString(passcode, forKey: key)

XCTAssertNotNil(legacyValet.string(forKey: key))
if #available(OSX 10.15, *) {
#if os(macOS)
_ = permutation.migrateObjectsFromPreCatalina()
#endif
}
XCTAssertEqual(legacyValet.string(forKey: key), permutation.string(forKey: key), "\(permutation) was not able to read from legacy counterpart: \(legacyValet)")
}
}
Expand All @@ -102,6 +107,11 @@ class ValetBackwardsCompatibilityIntegrationTests: ValetIntegrationTests {
legacyValet.setString(passcode, forKey: key)

XCTAssertNotNil(legacyValet.string(forKey: key))
if #available(OSX 10.15, *) {
#if os(macOS)
_ = permutation.migrateObjectsFromPreCatalina()
#endif
}
XCTAssertEqual(legacyValet.string(forKey: key), permutation.string(forKey: key), "\(permutation) was not able to read from legacy counterpart: \(legacyValet)")
}
}
Expand Down
33 changes: 33 additions & 0 deletions Tests/ValetIntegrationTests/MacTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,38 @@ class ValetMacTests: XCTestCase
// If you add a breakpoint here then manually inspect the keychain via Keychain.app (search for "MacOSVulnTest"), "xctest" should be the only member of the Access Control list.
// This is not be the case upon setting a breakpoint and inspecting before the valet.setString(, forKey:) call above.
}

// MARK: Migration - PreCatalina

func test_migrateObjectsFromPreCatalina_migratesDataWrittenPreCatalina() {
guard #available(macOS 10.15, *) else {
return
}
guard testEnvironmentIsSigned() else {
return
}

let valet = Valet.valet(with: Identifier(nonEmpty: "PreCatalinaTest")!, accessibility: .afterFirstUnlock)
var preCatalinaWriteQuery = valet.keychainQuery
#if swift(>=5.1)
preCatalinaWriteQuery[kSecUseDataProtectionKeychain as String] = nil
#else
preCatalinaWriteQuery["nleg"] = nil // kSecUseDataProtectionKeychain for Xcode 9 and Xcode 10 compatibility.
#endif

let key = "PreCatalinaKey"
let object = Data("PreCatalinaValue".utf8)
preCatalinaWriteQuery[kSecAttrAccount as String] = key
preCatalinaWriteQuery[kSecValueData as String] = object

// Make sure the item is not in the keychain before we start this test
SecItemDelete(preCatalinaWriteQuery as CFDictionary)

XCTAssertEqual(SecItemAdd(preCatalinaWriteQuery as CFDictionary, nil), errSecSuccess)
XCTAssertNil(valet.object(forKey: key))
XCTAssertEqual(valet.migrateObjectsFromPreCatalina(), .success)
XCTAssertEqual(valet.object(forKey: key), object)
}

}
#endif
5 changes: 4 additions & 1 deletion Tests/ValetIntegrationTests/SecItemTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ class SecItemTests: XCTestCase {
XCTAssertEqual(SecItem.sharedAccessGroupPrefix, "9XUJ7M53NG")
#endif
#elseif os(macOS)
// Do nothing.
guard testEnvironmentIsSigned() else {
return
}
XCTAssertEqual(SecItem.sharedAccessGroupPrefix, "9XUJ7M53NG")
#else
// Currently unsupported build configuration. This next line will compile-time error.
doNotCommentOutThisLine()
Expand Down
23 changes: 23 additions & 0 deletions Tests/ValetIntegrationTests/ValetIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,29 @@ class ValetIntegrationTests: XCTestCase

XCTAssertNil(anotherFlavor.string(forKey: "cookie"))
}

#if !os(macOS)
func test_objectForKey_canReadItemsWithout_kSecUseDataProtectionKeychain_when_kSecUseDataProtectionKeychain_isSetToTrueInKeychainQuery() {
let valet = Valet.valet(with: Identifier(nonEmpty: "DataProtectionTest")!, accessibility: .afterFirstUnlock)
var dataProtectionWriteQuery = valet.keychainQuery
#if swift(>=5.1)
dataProtectionWriteQuery[kSecUseDataProtectionKeychain as String] = nil
#else
dataProtectionWriteQuery["nleg"] = nil // kSecUseDataProtectionKeychain for Xcode 9 and Xcode 10 compatibility.
#endif

let key = "DataProtectionKey"
let object = Data("DataProtectionValue".utf8)
dataProtectionWriteQuery[kSecAttrAccount as String] = key
dataProtectionWriteQuery[kSecValueData as String] = object

// Make sure the item is not in the keychain before we start this test
SecItemDelete(dataProtectionWriteQuery as CFDictionary)

XCTAssertEqual(SecItemAdd(dataProtectionWriteQuery as CFDictionary, nil), errSecSuccess)
XCTAssertEqual(valet.object(forKey: key), object) // If this breaks, it means Apple has changed behavior of SecItemCopy. It means that we need to remove `kSecUseDataProtectionKeychain` from our query on non-Mac platforms.
}
#endif

// MARK: set(string:forKey:)

Expand Down
2 changes: 1 addition & 1 deletion Valet.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'Valet'
s.version = '3.2.6'
s.version = '3.2.8'
s.license = 'Apache License, Version 2.0'
s.summary = 'Securely store data on iOS, tvOS, watchOS, or macOS without knowing a thing about how the Keychain works. It\'s easy. We promise.'
s.homepage = 'https://github.com/square/Valet'
Expand Down

0 comments on commit 7447d16

Please sign in to comment.