Skip to content

Commit e1e5bd7

Browse files
authored
Use separate Keychain for each app (#224)
* Use separate Keychain for each app * Fix macOS and Linux tests * Allow macOS app keychain to upgrade
1 parent ba3d60e commit e1e5bd7

File tree

7 files changed

+133
-26
lines changed

7 files changed

+133
-26
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
# Parse-Swift Changelog
22

33
### main
4-
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.9.7...main)
4+
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.9.8...main)
55
* _Contributing to this repo? Add info about your change here to be included in the next release_
66

7+
### 1.9.8
8+
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.9.7...1.9.8)
9+
10+
__Fixes__
11+
- Use a seperate Keychain for each app bundleId. This only effects macOS apps as their Keychain is handled by the OS differently. For macOS app developers only, the user who logged in last to your app will have their Keychain upgraded to the patched version. Other users/apps will either need to login again or logout then login again ([#224](https://github.com/parse-community/Parse-Swift/pull/224)), thanks to [Corey Baker](https://github.com/cbaker6).
12+
713
### 1.9.7
814
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.9.6...1.9.7)
915

Sources/ParseSwift/Parse.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,18 @@ public struct ParseSwift {
138138
do {
139139
let previousSDKVersion = try ParseVersion(ParseVersion.current)
140140
let currentSDKVersion = try ParseVersion(ParseConstants.version)
141+
let oneNineEightSDKVersion = try ParseVersion("1.9.8")
141142

142143
// All migrations from previous versions to current should occur here:
143-
144+
#if !os(Linux) && !os(Android)
145+
if previousSDKVersion < oneNineEightSDKVersion {
146+
// Old macOS Keychain can't be used because it's global to all apps.
147+
_ = KeychainStore.old
148+
KeychainStore.shared.copy(keychain: KeychainStore.old)
149+
// Need to delete the old Keychain because a new one is created with bundleId.
150+
try? KeychainStore.old.deleteAll()
151+
}
152+
#endif
144153
if currentSDKVersion > previousSDKVersion {
145154
ParseVersion.current = currentSDKVersion.string
146155
}

Sources/ParseSwift/ParseConstants.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Foundation
1010

1111
enum ParseConstants {
1212
static let sdk = "swift"
13-
static let version = "1.9.7"
13+
static let version = "1.9.8"
1414
static let fileManagementDirectory = "parse/"
1515
static let fileManagementPrivateDocumentsDirectory = "Private Documents/"
1616
static let fileManagementLibraryDirectory = "Library/"

Sources/ParseSwift/Storage/KeychainStore.swift

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,43 @@ func getKeychainQueryTemplate(forService service: String) -> [String: String] {
3131
struct KeychainStore: SecureStorage {
3232
let synchronizationQueue: DispatchQueue
3333
private let keychainQueryTemplate: [String: String]
34-
35-
public static var shared = KeychainStore(service: "shared")
36-
37-
init(service: String) {
38-
synchronizationQueue = DispatchQueue(label: "com.parse.keychain.\(service)",
34+
static var shared = KeychainStore()
35+
// This Keychain was used by SDK <= 1.9.7
36+
static var old = KeychainStore(service: "shared")
37+
38+
init(service: String? = nil) {
39+
var keychainService = ".parseSwift.sdk"
40+
if let service = service {
41+
keychainService = service
42+
} else if let identifier = Bundle.main.bundleIdentifier {
43+
keychainService = "\(identifier)\(keychainService)"
44+
} else {
45+
keychainService = "com\(keychainService)"
46+
}
47+
synchronizationQueue = DispatchQueue(label: "\(keychainService).keychain",
3948
qos: .default,
4049
attributes: .concurrent,
4150
autoreleaseFrequency: .inherit,
4251
target: nil)
43-
keychainQueryTemplate = getKeychainQueryTemplate(forService: service)
52+
keychainQueryTemplate = getKeychainQueryTemplate(forService: keychainService)
53+
}
54+
55+
func copy(keychain: KeychainStore) {
56+
if let user = keychain.data(forKey: ParseStorage.Keys.currentUser) {
57+
_ = try? set(user, forKey: ParseStorage.Keys.currentUser)
58+
}
59+
if let installation = keychain.data(forKey: ParseStorage.Keys.currentInstallation) {
60+
_ = try? set(installation, forKey: ParseStorage.Keys.currentInstallation)
61+
}
62+
if let version = keychain.data(forKey: ParseStorage.Keys.currentVersion) {
63+
_ = try? set(version, forKey: ParseStorage.Keys.currentVersion)
64+
}
65+
if let config = keychain.data(forKey: ParseStorage.Keys.currentConfig) {
66+
_ = try? set(config, forKey: ParseStorage.Keys.currentConfig)
67+
}
68+
if let acl = keychain.data(forKey: ParseStorage.Keys.defaultACL) {
69+
_ = try? set(acl, forKey: ParseStorage.Keys.defaultACL)
70+
}
4471
}
4572

4673
private func keychainQuery(forKey key: String) -> [String: Any] {
@@ -69,20 +96,7 @@ struct KeychainStore: SecureStorage {
6996
}
7097
do {
7198
let data = try ParseCoding.jsonEncoder().encode(object)
72-
let query = keychainQuery(forKey: key)
73-
let update = [
74-
kSecValueData as String: data
75-
]
76-
77-
let status = synchronizationQueue.sync(flags: .barrier) { () -> OSStatus in
78-
if self.data(forKey: key) != nil {
79-
return SecItemUpdate(query as CFDictionary, update as CFDictionary)
80-
}
81-
let mergedQuery = query.merging(update) { (_, otherValue) -> Any in otherValue }
82-
return SecItemAdd(mergedQuery as CFDictionary, nil)
83-
}
84-
85-
return status == errSecSuccess
99+
return try set(data, forKey: key)
86100
} catch {
87101
return false
88102
}
@@ -145,13 +159,31 @@ struct KeychainStore: SecureStorage {
145159
return data
146160
}
147161

162+
private func set(_ data: Data, forKey key: String) throws -> Bool {
163+
let query = keychainQuery(forKey: key)
164+
let update = [
165+
kSecValueData as String: data
166+
]
167+
168+
let status = synchronizationQueue.sync(flags: .barrier) { () -> OSStatus in
169+
if self.data(forKey: key) != nil {
170+
return SecItemUpdate(query as CFDictionary, update as CFDictionary)
171+
}
172+
let mergedQuery = query.merging(update) { (_, otherValue) -> Any in otherValue }
173+
return SecItemAdd(mergedQuery as CFDictionary, nil)
174+
}
175+
176+
return status == errSecSuccess
177+
}
178+
148179
private func removeObject(forKeyUnsafe key: String) -> Bool {
149180
dispatchPrecondition(condition: .onQueue(synchronizationQueue))
150181
return SecItemDelete(keychainQuery(forKey: key) as CFDictionary) == errSecSuccess
151182
}
152183
}
153184

154-
extension KeychainStore /* TypedSubscript */ {
185+
// MARK: TypedSubscript
186+
extension KeychainStore {
155187
subscript(string key: String) -> String? {
156188
get {
157189
return object(forKey: key)

Sources/ParseSwift/Storage/SecureStorage.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import Foundation
1010

1111
protocol SecureStorage {
12-
init(service: String)
12+
init(service: String?)
1313
func object<T>(forKey key: String) -> T? where T: Decodable
1414
func set<T>(object: T?, forKey: String) -> Bool where T: Encodable
1515
subscript <T>(key: String) -> T? where T: Codable { get }

Sources/ParseSwift/Types/ParseVersion.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,13 @@ public struct ParseVersion: Encodable {
2222
guard let versionFromKeyChain: String =
2323
try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentVersion)
2424
else {
25-
return nil
25+
guard let versionFromKeyChain: String =
26+
try? KeychainStore.old.get(valueFor: ParseStorage.Keys.currentVersion)
27+
else {
28+
return nil
29+
}
30+
try? KeychainStore.shared.set(versionFromKeyChain, for: ParseStorage.Keys.currentVersion)
31+
return versionFromKeyChain
2632
}
2733
return versionFromKeyChain
2834
#else

Tests/ParseSwiftTests/InitializeSDKTests.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ class InitializeSDKTests: XCTestCase {
3030
var customKey: String?
3131
}
3232

33+
struct Config: ParseConfig {
34+
var welcomeMessage: String?
35+
var winningNumber: Int?
36+
}
37+
3338
override func setUpWithError() throws {
3439
try super.setUpWithError()
3540
}
@@ -250,6 +255,55 @@ class InitializeSDKTests: XCTestCase {
250255
}
251256

252257
#if !os(Linux) && !os(Android)
258+
func testMigrateOldKeychainToNew() throws {
259+
var user = BaseParseUser()
260+
user.objectId = "wow"
261+
var userContainer = CurrentUserContainer<BaseParseUser>()
262+
userContainer.currentUser = user
263+
userContainer.sessionToken = "session"
264+
var installation = Installation()
265+
installation.objectId = "now"
266+
var installationContainer = CurrentInstallationContainer<Installation>()
267+
installationContainer.currentInstallation = installation
268+
installationContainer.installationId = "id"
269+
let config = Config(welcomeMessage: "hello", winningNumber: 5)
270+
var configContainer = CurrentConfigContainer<Config>()
271+
configContainer.currentConfig = config
272+
var acl = ParseACL()
273+
acl.setReadAccess(objectId: "hello", value: true)
274+
acl.setReadAccess(objectId: "wow", value: true)
275+
acl.setWriteAccess(objectId: "wow", value: true)
276+
let aclContainer = DefaultACL(defaultACL: acl,
277+
lastCurrentUserObjectId: user.objectId,
278+
useCurrentUser: true)
279+
let version = "1.9.7"
280+
try? KeychainStore.old.set(version, for: ParseStorage.Keys.currentVersion)
281+
try? KeychainStore.old.set(userContainer, for: ParseStorage.Keys.currentUser)
282+
try? KeychainStore.old.set(installationContainer, for: ParseStorage.Keys.currentInstallation)
283+
try? KeychainStore.old.set(configContainer, for: ParseStorage.Keys.currentConfig)
284+
try? KeychainStore.old.set(aclContainer, for: ParseStorage.Keys.defaultACL)
285+
let expectation1 = XCTestExpectation(description: "Wait")
286+
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
287+
guard let url = URL(string: "http://localhost:1337/1") else {
288+
XCTFail("Should create valid URL")
289+
return
290+
}
291+
ParseSwift.initialize(applicationId: "applicationId",
292+
clientKey: "clientKey",
293+
masterKey: "masterKey",
294+
serverURL: url)
295+
XCTAssertEqual(ParseVersion.current, ParseConstants.version)
296+
XCTAssertEqual(BaseParseUser.current, user)
297+
XCTAssertEqual(Installation.current, installation)
298+
XCTAssertEqual(Config.current?.welcomeMessage, config.welcomeMessage)
299+
XCTAssertEqual(Config.current?.winningNumber, config.winningNumber)
300+
let defaultACL = try? ParseACL.defaultACL()
301+
XCTAssertEqual(defaultACL, acl)
302+
expectation1.fulfill()
303+
}
304+
wait(for: [expectation1], timeout: 10.0)
305+
}
306+
253307
func testMigrateObjcSDK() {
254308

255309
//Set keychain the way objc sets keychain

0 commit comments

Comments
 (0)