Skip to content

Commit b8b9c45

Browse files
authored
Merge pull request #19188 from wordpress-mobile/task/jp-shared-keychain
Shared Login: Add `KeychainUtils` class
2 parents 09dc7a3 + 05c4673 commit b8b9c45

4 files changed

Lines changed: 228 additions & 0 deletions

File tree

WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ enum FeatureFlag: Int, CaseIterable, OverrideableFlag {
3333
case jetpackPowered
3434
case jetpackPoweredBottomSheet
3535
case sharedUserDefaults
36+
case sharedLogin
3637

3738
/// Returns a boolean indicating if the feature is enabled
3839
var enabled: Bool {
@@ -107,6 +108,8 @@ enum FeatureFlag: Int, CaseIterable, OverrideableFlag {
107108
return false
108109
case .sharedUserDefaults:
109110
return false
111+
case .sharedLogin:
112+
return false
110113
}
111114
}
112115

@@ -199,6 +202,8 @@ extension FeatureFlag {
199202
return "Jetpack powered bottom sheet"
200203
case .sharedUserDefaults:
201204
return "Shared User Defaults"
205+
case .sharedLogin:
206+
return "Shared Login"
202207
}
203208
}
204209

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
@objcMembers
2+
class KeychainUtils {
3+
4+
static let shared = KeychainUtils()
5+
6+
private let shouldUseSharedKeychain: () -> Bool
7+
private let keychainUtils: SFHFKeychainUtils.Type
8+
9+
private var keychainGroup: String? {
10+
shouldUseSharedKeychain() ? WPAppKeychainAccessGroup : nil
11+
}
12+
13+
init(shouldUseSharedKeychain: @escaping @autoclosure () -> Bool = FeatureFlag.sharedLogin.enabled,
14+
keychainUtils: SFHFKeychainUtils.Type = SFHFKeychainUtils.self) {
15+
self.shouldUseSharedKeychain = shouldUseSharedKeychain
16+
self.keychainUtils = keychainUtils
17+
}
18+
19+
func storeUsername(_ username: String, password: String, serviceName: String, accessGroup: String? = nil, updateExisting: Bool) throws {
20+
try keychainUtils.storeUsername(
21+
username,
22+
andPassword: password,
23+
forServiceName: serviceName,
24+
accessGroup: accessGroup ?? keychainGroup,
25+
updateExisting: updateExisting
26+
)
27+
}
28+
29+
func getPasswordForUsername(_ username: String, serviceName: String, accessGroup: String? = nil) throws -> String? {
30+
try keychainUtils.getPasswordForUsername(
31+
username,
32+
andServiceName: serviceName,
33+
accessGroup: accessGroup ?? keychainGroup
34+
)
35+
}
36+
37+
func deleteItem(username: String, serviceName: String, accessGroup: String? = nil) throws {
38+
try keychainUtils.deleteItem(
39+
forUsername: username,
40+
andServiceName: serviceName,
41+
accessGroup: accessGroup ?? keychainGroup
42+
)
43+
}
44+
45+
}

WordPress/WordPress.xcodeproj/project.pbxproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1481,6 +1481,9 @@
14811481
837B49DC283C2AE80061A657 /* BloggingPromptSettingsReminderDays+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837B49D5283C2AE80061A657 /* BloggingPromptSettingsReminderDays+CoreDataClass.swift */; };
14821482
837B49DD283C2AE80061A657 /* BloggingPromptSettingsReminderDays+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837B49D6283C2AE80061A657 /* BloggingPromptSettingsReminderDays+CoreDataProperties.swift */; };
14831483
837B49DE283C2AE80061A657 /* BloggingPromptSettingsReminderDays+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837B49D6283C2AE80061A657 /* BloggingPromptSettingsReminderDays+CoreDataProperties.swift */; };
1484+
8384C64128AAC82600EABE26 /* KeychainUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8384C64028AAC82600EABE26 /* KeychainUtils.swift */; };
1485+
8384C64228AAC82600EABE26 /* KeychainUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8384C64028AAC82600EABE26 /* KeychainUtils.swift */; };
1486+
8384C64428AAC85F00EABE26 /* KeychainUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8384C64328AAC85F00EABE26 /* KeychainUtilsTests.swift */; };
14841487
839B150B2795DEE0009F5E77 /* UIView+Margins.swift in Sources */ = {isa = PBXBuildFile; fileRef = 839B150A2795DEE0009F5E77 /* UIView+Margins.swift */; };
14851488
839B150C2795DEE0009F5E77 /* UIView+Margins.swift in Sources */ = {isa = PBXBuildFile; fileRef = 839B150A2795DEE0009F5E77 /* UIView+Margins.swift */; };
14861489
83B1D037282C62620061D911 /* BloggingPromptsAttribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B1D036282C62620061D911 /* BloggingPromptsAttribution.swift */; };
@@ -6406,6 +6409,8 @@
64066409
837B49D4283C2AE80061A657 /* BloggingPromptSettings+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BloggingPromptSettings+CoreDataProperties.swift"; sourceTree = "<group>"; };
64076410
837B49D5283C2AE80061A657 /* BloggingPromptSettingsReminderDays+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BloggingPromptSettingsReminderDays+CoreDataClass.swift"; sourceTree = "<group>"; };
64086411
837B49D6283C2AE80061A657 /* BloggingPromptSettingsReminderDays+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BloggingPromptSettingsReminderDays+CoreDataProperties.swift"; sourceTree = "<group>"; };
6412+
8384C64028AAC82600EABE26 /* KeychainUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainUtils.swift; sourceTree = "<group>"; };
6413+
8384C64328AAC85F00EABE26 /* KeychainUtilsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainUtilsTests.swift; sourceTree = "<group>"; };
64096414
839435922847F2200019A94F /* WordPress 143.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 143.xcdatamodel"; sourceTree = "<group>"; };
64106415
839B150A2795DEE0009F5E77 /* UIView+Margins.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Margins.swift"; sourceTree = "<group>"; };
64116416
83B1D036282C62620061D911 /* BloggingPromptsAttribution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloggingPromptsAttribution.swift; sourceTree = "<group>"; };
@@ -11762,6 +11767,7 @@
1176211767
E1AB5A391E0C464700574B4E /* DelayTests.swift */,
1176311768
E1EBC3721C118ED200F638E0 /* ImmuTableTest.swift */,
1176411769
93A379EB19FFBF7900415023 /* KeychainTest.m */,
11770+
8384C64328AAC85F00EABE26 /* KeychainUtilsTests.swift */,
1176511771
1759F1711FE017F20003EC81 /* QueueTests.swift */,
1176611772
5948AD101AB73D19006E8882 /* WPAppAnalyticsTests.m */,
1176711773
E1E4CE0C177439D100430844 /* WPAvatarSourceTest.m */,
@@ -11864,6 +11870,7 @@
1186411870
3FB1929426C79EC6000F5AA3 /* Date+TimeStrings.swift */,
1186511871
E14BCABA1E0BC817002E0603 /* Delay.swift */,
1186611872
40D78238206ABD970015A3A1 /* Scheduler.swift */,
11873+
8384C64028AAC82600EABE26 /* KeychainUtils.swift */,
1186711874
E11000981CDB5F1E00E33887 /* KeychainTools.swift */,
1186811875
5DB4683918A2E718004A89A9 /* LocationService.h */,
1186911876
5DB4683A18A2E718004A89A9 /* LocationService.m */,
@@ -18809,6 +18816,7 @@
1880918816
B5AC00681BE3C4E100F8E7C3 /* DiscussionSettingsViewController.swift in Sources */,
1881018817
B0637543253E7E7A00FD45D2 /* GutenbergSuggestionsViewController.swift in Sources */,
1881118818
D816C1F620E0896F00C4D82F /* TrashComment.swift in Sources */,
18819+
8384C64128AAC82600EABE26 /* KeychainUtils.swift in Sources */,
1881218820
08AAD69F1CBEA47D002B2418 /* MenusService.m in Sources */,
1881318821
3F4370412893207C00475B6E /* JetpackOverlayView.swift in Sources */,
1881418822
1788106F260E488B00A98BD8 /* UnifiedPrologueNotificationsContentView.swift in Sources */,
@@ -20627,6 +20635,7 @@
2062720635
93D86B981C691E71003D8E3E /* LocalCoreDataServiceTests.m in Sources */,
2062820636
400A2C932217B463000A8A59 /* ReferrerStatsRecordValueTests.swift in Sources */,
2062920637
3F50945B2454ECA000C4470B /* ReaderTabItemsStoreTests.swift in Sources */,
20638+
8384C64428AAC85F00EABE26 /* KeychainUtilsTests.swift in Sources */,
2063020639
73178C2921BEE09300E37C9A /* SiteSegmentsStepTests.swift in Sources */,
2063120640
08F8CD311EBD2A960049D0C0 /* MediaImageExporterTests.swift in Sources */,
2063220641
DC06DFFC27BD679700969974 /* BlogTitleTests.swift in Sources */,
@@ -21571,6 +21580,7 @@
2157121580
FABB23962602FC2C00C8785C /* StatsTableFooter.swift in Sources */,
2157221581
FABB23972602FC2C00C8785C /* BlogSyncFacade.m in Sources */,
2157321582
FABB23982602FC2C00C8785C /* PrepublishingNavigationController.swift in Sources */,
21583+
8384C64228AAC82600EABE26 /* KeychainUtils.swift in Sources */,
2157421584
FABB23992602FC2C00C8785C /* UINavigationController+KeyboardFix.m in Sources */,
2157521585
FABB239B2602FC2C00C8785C /* ExpandableCell.swift in Sources */,
2157621586
FABB239C2602FC2C00C8785C /* PreviewNonceHandler.swift in Sources */,
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import XCTest
2+
@testable import WordPress
3+
4+
class KeychainUtilsTests: XCTestCase {
5+
6+
let username = "Username"
7+
let password = "Password"
8+
let service = "Service"
9+
let sharedGroup = WPAppKeychainAccessGroup
10+
11+
override func setUp() {
12+
super.setUp()
13+
14+
SFHFKeychainUtilsMock.configure(with: [:])
15+
}
16+
17+
func testNilAppGroupSavesToSharedGroupWhenFeatureEnabled() {
18+
let subject = KeychainUtils(shouldUseSharedKeychain: true, keychainUtils: SFHFKeychainUtilsMock.self)
19+
20+
try? subject.storeUsername(username, password: password, serviceName: service, updateExisting: true)
21+
22+
let result = getPassword(username: username, serviceName: service, accessGroup: sharedGroup)
23+
XCTAssertEqual(result, password)
24+
}
25+
26+
func testNilAppGroupDoesNotSaveToSharedGroupWhenFeatureDisabled() {
27+
let subject = KeychainUtils(shouldUseSharedKeychain: false, keychainUtils: SFHFKeychainUtilsMock.self)
28+
29+
try? subject.storeUsername(username, password: password, serviceName: service, updateExisting: true)
30+
31+
let result = getPassword(username: username, serviceName: service, accessGroup: sharedGroup)
32+
XCTAssertNotEqual(result, password)
33+
}
34+
35+
func testFeatureFlagChanging() {
36+
var enabled = false
37+
let subject = KeychainUtils(shouldUseSharedKeychain: enabled, keychainUtils: SFHFKeychainUtilsMock.self)
38+
39+
enabled = true
40+
try? subject.storeUsername(username, password: password, serviceName: service, updateExisting: true)
41+
42+
let result = getPassword(username: username, serviceName: service, accessGroup: sharedGroup)
43+
XCTAssertEqual(result, password)
44+
}
45+
46+
func testNilAppGroupReadsFromSharedGroupWhenFeatureEnabled() {
47+
let subject = KeychainUtils(shouldUseSharedKeychain: true, keychainUtils: SFHFKeychainUtilsMock.self)
48+
storeUsername(username, password: password, serviceName: service, accessGroup: sharedGroup)
49+
50+
let result = try? subject.getPasswordForUsername(username, serviceName: service)
51+
52+
XCTAssertEqual(result, password)
53+
}
54+
55+
func testNilAppGroupDoesNotReadFromSharedGroupWhenFeatureDisabled() {
56+
let subject = KeychainUtils(shouldUseSharedKeychain: false, keychainUtils: SFHFKeychainUtilsMock.self)
57+
storeUsername(username, password: password, serviceName: service, accessGroup: sharedGroup)
58+
59+
let result = try? subject.getPasswordForUsername(username, serviceName: service)
60+
61+
XCTAssertNotEqual(result, password)
62+
}
63+
64+
func testNilAppGroupDeletesFromSharedGroupWhenEnabled() {
65+
storeUsername(username, password: password, serviceName: service, accessGroup: sharedGroup)
66+
storeUsername(username, password: password, serviceName: service, accessGroup: nil)
67+
let subject = KeychainUtils(shouldUseSharedKeychain: true, keychainUtils: SFHFKeychainUtilsMock.self)
68+
69+
try? subject.deleteItem(username: username, serviceName: service)
70+
71+
XCTAssertNil(getPassword(username: username, serviceName: service, accessGroup: sharedGroup))
72+
XCTAssertEqual(getPassword(username: username, serviceName: service, accessGroup: nil), password)
73+
}
74+
75+
func testNilAppGroupDeletesFromNilGroupWhenDisabled() {
76+
storeUsername(username, password: password, serviceName: service, accessGroup: sharedGroup)
77+
storeUsername(username, password: password, serviceName: service, accessGroup: nil)
78+
let subject = KeychainUtils(shouldUseSharedKeychain: false, keychainUtils: SFHFKeychainUtilsMock.self)
79+
80+
try? subject.deleteItem(username: username, serviceName: service)
81+
82+
XCTAssertEqual(getPassword(username: username, serviceName: service, accessGroup: sharedGroup), password)
83+
XCTAssertNil(getPassword(username: username, serviceName: service, accessGroup: nil))
84+
}
85+
86+
func testAppGroupIsAPassthroughWhenSaving() {
87+
let group = "Test"
88+
let subject = KeychainUtils(shouldUseSharedKeychain: true, keychainUtils: SFHFKeychainUtilsMock.self)
89+
90+
try? subject.storeUsername(username, password: password, serviceName: service, accessGroup: group, updateExisting: true)
91+
92+
let result = getPassword(username: username, serviceName: service, accessGroup: group)
93+
XCTAssertEqual(result, password)
94+
}
95+
96+
func testAppGroupIsAPassthroughWhenReading() {
97+
let group = "Test"
98+
let subject = KeychainUtils(shouldUseSharedKeychain: true, keychainUtils: SFHFKeychainUtilsMock.self)
99+
storeUsername(username, password: password, serviceName: service, accessGroup: group)
100+
101+
let result = try? subject.getPasswordForUsername(username, serviceName: service, accessGroup: group)
102+
103+
XCTAssertEqual(result, password)
104+
}
105+
106+
func testAppGroupIsAPassthroughWhenDeleting() {
107+
let group = "Test"
108+
storeUsername(username, password: password, serviceName: service, accessGroup: group)
109+
let subject = KeychainUtils(shouldUseSharedKeychain: false, keychainUtils: SFHFKeychainUtilsMock.self)
110+
111+
try? subject.deleteItem(username: username, serviceName: service, accessGroup: group)
112+
113+
XCTAssertNil(getPassword(username: username, serviceName: service, accessGroup: group))
114+
}
115+
116+
}
117+
118+
// MARK: - Helper functions
119+
120+
private extension KeychainUtilsTests {
121+
122+
func getPassword(username: String, serviceName: String, accessGroup: String?) -> String? {
123+
let result = (try? SFHFKeychainUtilsMock.getPasswordForUsername(username, andServiceName: serviceName, accessGroup: accessGroup)) ?? ""
124+
return result.count > 0 ? result : nil
125+
}
126+
127+
func storeUsername(_ username: String, password: String, serviceName: String, accessGroup: String?) {
128+
try? SFHFKeychainUtilsMock.storeUsername(username, andPassword: password, forServiceName: serviceName, accessGroup: accessGroup, updateExisting: true)
129+
}
130+
131+
}
132+
133+
// MARK: - SFHFKeychainUtilsMock
134+
135+
final private class SFHFKeychainUtilsMock: SFHFKeychainUtils {
136+
typealias MockKeychain = [String: [String: [String: String]]]
137+
138+
static var keychain: MockKeychain = [:]
139+
140+
class func configure(with keychain: MockKeychain) {
141+
self.keychain = keychain
142+
}
143+
144+
override class func getPasswordForUsername(_ username: String!, andServiceName serviceName: String!, accessGroup: String!) throws -> String {
145+
let group = accessGroup ?? "default"
146+
if let value = keychain[group]?[serviceName]?[username] {
147+
return value
148+
}
149+
150+
return ""
151+
}
152+
153+
override class func storeUsername(_ username: String!, andPassword password: String!, forServiceName serviceName: String!, accessGroup: String!, updateExisting: Bool) throws {
154+
let group = accessGroup ?? "default"
155+
if keychain[group] == nil {
156+
keychain[group] = [:]
157+
}
158+
if keychain[group]?[serviceName] == nil {
159+
keychain[group]?[serviceName] = [:]
160+
}
161+
keychain[group]?[serviceName]?[username] = password
162+
}
163+
164+
override class func deleteItem(forUsername username: String!, andServiceName serviceName: String!, accessGroup: String!) throws {
165+
let group = accessGroup ?? "default"
166+
keychain[group]?[serviceName]?.removeValue(forKey: username)
167+
}
168+
}

0 commit comments

Comments
 (0)