Skip to content

Commit 85a4511

Browse files
authored
Keychain Migration (#180)
* First go at migrating keychain passwords if the app needs to. * Updating Share Extension Entitlements * Updating new Provisioning Profiles / Bundle Identifiers * KeychainMigrator: Fixing Swift 4 error * Share Extension: Adding legacy Keychain Access Group * Fixing AppGroup Identifiers * Adding previous-application-identifiers Entitlement * KeychainMigrator: Switching to guard let * Updating Release Target * Updating Podfile * Updating Testing Target * KeychainPasswordItem: Adding source reference * KeychainMigrator: Splitting into methods for testability purposes * Implements KeychainMigrator Unit Tests * Updates Project * Adding AdHoc Target * KeychainMigratorTests: Updating Unit Test * KeychainMigrator: Wiring Constants * KeychainMigrator: Specifying current accessGroup * Updates CocoaPods Integration * Updates CocoaPods Integration. Mark II * Updates KeychainMigrator Tests * SPAppDelegate: Updates migration call * Fixing variable name in test. * KeychainMigrator: Adding event tracking * KeychainMigrator: Failsafe Mechanism
1 parent 1bed600 commit 85a4511

20 files changed

+1114
-283
lines changed

Podfile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
source 'https://github.com/CocoaPods/Specs.git'
22

3-
platform :ios, '9.0'
3+
platform :ios, '10.0'
44
inhibit_all_warnings!
55
use_frameworks!
66

@@ -30,6 +30,12 @@ abstract_target 'Automattic' do
3030
pod 'Simperium', '0.8.19'
3131
pod 'WordPress-AppbotX', :git => 'https://github.com/wordpress-mobile/appbotx.git', :commit => '479d05f7d6b963c9b44040e6ea9f190e8bd9a47a'
3232
pod 'WordPress-Ratings-iOS', '0.0.2'
33+
34+
# Testing Target
35+
#
36+
target 'SimplenoteTests' do
37+
inherit! :search_paths
38+
end
3339
end
3440

3541
# Extension Target

Podfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,6 @@ SPEC CHECKSUMS:
8787
WordPress-AppbotX: b5abc0ba45e3da5827f84e9f346c963180f1b545
8888
WordPress-Ratings-iOS: b04108f7a45867844ba47a240bb6295ca747eebf
8989

90-
PODFILE CHECKSUM: 0fda9886a82c8a61adc629fbefbde69dafe16cf6
90+
PODFILE CHECKSUM: f6767479ddf21dd5c487fc2774dad6048bd1262c
9191

9292
COCOAPODS: 1.3.1

Simplenote.xcodeproj/project.pbxproj

Lines changed: 575 additions & 258 deletions
Large diffs are not rendered by default.

Simplenote/Classes/SPOptionsViewController.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ - (void)doneAction:(id)sender
171171

172172
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
173173
{
174-
#if BETA_DISTRIBUTION
174+
#if INTERNAL_DISTRIBUTION
175175
return SPOptionsViewSectionsCount;
176176
#else
177177
return SPOptionsViewSectionsCount - 1;
@@ -222,7 +222,7 @@ - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInte
222222

223223
- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section
224224
{
225-
#if BETA_DISTRIBUTION
225+
#if INTERNAL_DISTRIBUTION
226226
if (section == SPOptionsViewSectionsDebug) {
227227
return [[NSString alloc] initWithFormat:@"Beta Distribution Channel\nv%@ (%@)", [NSString stringWithFormat:@"%@", [[NSBundle mainBundle] objectForInfoDictionaryKey: @"CFBundleShortVersionString"]], [[NSBundle mainBundle] objectForInfoDictionaryKey: (NSString *)kCFBundleVersionKey]];
228228
}

Simplenote/Classes/SPTracker.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,10 @@
9090
+ (void)trackUserSignedIn;
9191
+ (void)trackUserSignedOut;
9292

93+
#pragma mark - Keychain Migration
94+
+ (void)trackKeychainMigrationSucceeded;
95+
+ (void)trackKeychainMigrationFailed;
96+
+ (void)trackKeychainFailsafeSucceeded;
97+
+ (void)trackKeychainFailsafeFailed;
98+
9399
@end

Simplenote/Classes/SPTracker.m

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,31 @@ + (void)trackUserSignedOut
378378
[self trackGoogleEventWithCategory:@"user" action:@"signed_out" label:nil value:nil];
379379
}
380380

381+
#pragma mark - Keychain Migration
382+
383+
+ (void)trackKeychainMigrationSucceeded
384+
{
385+
[self trackAutomatticEventWithName:@"keychain_migration_succeeded" properties:nil];
386+
[self trackGoogleEventWithCategory:@"keychain" action:@"migraiton" label:@"success" value:nil];
387+
}
388+
389+
+ (void)trackKeychainMigrationFailed
390+
{
391+
[self trackAutomatticEventWithName:@"keychain_migration_failed" properties:nil];
392+
[self trackGoogleEventWithCategory:@"keychain" action:@"migration" label:@"failure" value:nil];
393+
}
394+
395+
+ (void)trackKeychainFailsafeSucceeded
396+
{
397+
[self trackAutomatticEventWithName:@"keychain_failsafe_succeeded" properties:nil];
398+
[self trackGoogleEventWithCategory:@"keychain" action:@"failsafe" label:@"succeeded" value:nil];
399+
}
400+
401+
+ (void)trackKeychainFailsafeFailed
402+
{
403+
[self trackAutomatticEventWithName:@"keychain_failsafe_failed" properties:nil];
404+
[self trackGoogleEventWithCategory:@"keychain" action:@"failsafe" label:@"failure" value:nil];
405+
}
381406

382407

383408
#pragma mark - Google Analytics Helpers

Simplenote/KeychainMigrator.swift

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
//
2+
// MigrateKeychain.swift
3+
// Simplenote
4+
// Migrates keychain items from previous keychain group
5+
//
6+
import Foundation
7+
8+
9+
// MARK: - KeychainMigrator
10+
//
11+
@objc
12+
class KeychainMigrator: NSObject {
13+
14+
/// Keychain Constants
15+
///
16+
private struct Constants {
17+
/// Legacy TeamID
18+
///
19+
static let oldPrefix = "4ESDVWK654."
20+
21+
/// New TeamID!
22+
///
23+
static let newPrefix = "PZYM8XX95Q."
24+
25+
/// Main App's Bundle ID
26+
///
27+
static let bundleId = "com.codality.NotationalFlow"
28+
29+
/// Share Extension's Bundle ID
30+
///
31+
static let shareBundleId = bundleId + ".Share"
32+
33+
/// Username's User Defaults Key
34+
///
35+
static let usernameKey = "SPUsername"
36+
37+
/// Legacy TeamID Access Group
38+
///
39+
static let legacyAccessGroup = oldPrefix + bundleId
40+
41+
/// New TeamID Access Group
42+
///
43+
static let newAccessGroup = newPrefix + bundleId
44+
}
45+
46+
/// Migrates the Legacy Keychain Entry over to the new Access Group
47+
///
48+
@objc
49+
func migrateIfNecessary() {
50+
guard needsPasswordMigration() else {
51+
return
52+
}
53+
54+
migrateLegacyPassword()
55+
}
56+
}
57+
58+
59+
// MARK: - Internal Helpers: This should actually be *private*, but for unit testing purposes, we're keeping them this way.
60+
//
61+
extension KeychainMigrator {
62+
63+
/// Indicates if the Migration should take place. This is true whenever:
64+
///
65+
/// - The Username is accessible
66+
/// - There is no current password
67+
///
68+
func needsPasswordMigration() -> Bool {
69+
guard let username = self.username else {
70+
return false
71+
}
72+
73+
let newKeychainEntry = try? loadKeychainEntry(accessGroup: .new, username: username)
74+
return newKeychainEntry == nil
75+
}
76+
77+
/// Migrates the Keychain Entry associated with the Old TeamID Prefix
78+
///
79+
func migrateLegacyPassword() {
80+
guard let username = self.username,
81+
let legacyPassword = try? loadKeychainEntry(accessGroup: .legacy, username: username)
82+
else {
83+
return
84+
}
85+
86+
// Looks like we need to attempt a migration...
87+
do {
88+
try deleteKeychainEntry(accessGroup: .legacy, username: username)
89+
try saveKeychainEntry(accessGroup: .new, username: username, password: legacyPassword)
90+
91+
SPTracker.trackKeychainMigrationSucceeded()
92+
} catch {
93+
// :(
94+
NSLog("Keychain Migration Error: \(error)")
95+
96+
SPTracker.trackKeychainMigrationFailed()
97+
self.restoreLegacyPassword(password: legacyPassword, for: username)
98+
}
99+
}
100+
101+
/// On error, we'll attempt to restore the legacy Password
102+
///
103+
func restoreLegacyPassword(password: String, for username: String) {
104+
do {
105+
try saveKeychainEntry(accessGroup: .legacy, username: username, password: password)
106+
SPTracker.trackKeychainFailsafeSucceeded()
107+
} catch {
108+
NSLog("Keychain Failsafe Error: \(error)")
109+
110+
SPTracker.trackKeychainFailsafeFailed()
111+
}
112+
}
113+
}
114+
115+
116+
// MARK: - User Defaults Wrappers
117+
//
118+
extension KeychainMigrator {
119+
/// Username
120+
///
121+
var username: String? {
122+
set {
123+
UserDefaults.standard.set(newValue, forKey: Constants.usernameKey)
124+
UserDefaults.standard.synchronize()
125+
}
126+
get {
127+
return UserDefaults.standard.string(forKey: Constants.usernameKey)
128+
}
129+
}
130+
}
131+
132+
133+
// MARK: - Keychain Wrappers: This should actually be *private*, but for unit testing purposes, we're keeping them this way.
134+
//
135+
extension KeychainMigrator {
136+
137+
enum AccessGroup {
138+
case new
139+
case legacy
140+
141+
var stringValue: String {
142+
switch self {
143+
case .new:
144+
return Constants.newAccessGroup
145+
case .legacy:
146+
return Constants.legacyAccessGroup
147+
}
148+
}
149+
}
150+
151+
func loadKeychainEntry(accessGroup: AccessGroup, username: String) throws -> String {
152+
let passwordItem = KeychainPasswordItem(
153+
service: SPCredentials.simperiumAppID(),
154+
account: username,
155+
accessGroup: accessGroup.stringValue
156+
)
157+
158+
return try passwordItem.readPassword()
159+
}
160+
161+
func deleteKeychainEntry(accessGroup: AccessGroup, username: String) throws {
162+
let passwordItem = KeychainPasswordItem(
163+
service: SPCredentials.simperiumAppID(),
164+
account: username,
165+
accessGroup: accessGroup.stringValue
166+
)
167+
168+
try passwordItem.deleteItem()
169+
}
170+
171+
func saveKeychainEntry(accessGroup: AccessGroup, username: String, password: String) throws {
172+
let passwordItem = KeychainPasswordItem(
173+
service: SPCredentials.simperiumAppID(),
174+
account: username,
175+
accessGroup: accessGroup.stringValue
176+
)
177+
178+
try passwordItem.savePassword(password)
179+
}
180+
181+
#if RELEASE
182+
/// This method tests the Migration Flow. This should only be executed in the *Release* target, since the AppID's won't
183+
/// match with the one(s) used in the other targets.
184+
///
185+
/// For testing purposes only.
186+
///
187+
@objc
188+
func testMigration() {
189+
let dummyUsername = "TestingUsername"
190+
let dummyPassword = "TestingPassword"
191+
192+
// Recreate Pre-Migration Scenario
193+
username = dummyUsername
194+
try? deleteKeychainEntry(accessGroup: .new, username: dummyUsername)
195+
try? saveKeychainEntry(accessGroup: .legacy, username: dummyUsername, password: dummyPassword)
196+
197+
// Migrate
198+
migrateIfNecessary()
199+
200+
// Verify
201+
let migrated = try? loadKeychainEntry(accessGroup: .new, username: dummyUsername)
202+
assert(migrated == dummyPassword)
203+
}
204+
#endif
205+
}

0 commit comments

Comments
 (0)