Migration of saved passwords.
[ADDED] Migrate saved passwords when master password changes. [IMPROVED] UI improvements to apps.
2
External/Pearl
vendored
@ -1 +1 @@
|
||||
Subproject commit ff1e29fc3a4c2ee7b08eb686d06f29a11466bbe5
|
||||
Subproject commit 552cf5ffc9445362f8f91357f4f7a034b2c390a3
|
@ -28,7 +28,9 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
|
||||
inf(@"Found key in keychain for: %@", user.userID);
|
||||
|
||||
else {
|
||||
user.saveKey = NO;
|
||||
[user.managedObjectContext performBlockAndWait:^{
|
||||
user.saveKey = NO;
|
||||
}];
|
||||
inf(@"No key found in keychain for: %@", user.userID);
|
||||
}
|
||||
|
||||
@ -58,7 +60,9 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
|
||||
|
||||
OSStatus result = [PearlKeyChain deleteItemForQuery:keyQuery(user)];
|
||||
if (result == noErr || result == errSecItemNotFound) {
|
||||
user.saveKey = NO;
|
||||
[user.managedObjectContext performBlockAndWait:^{
|
||||
user.saveKey = NO;
|
||||
}];
|
||||
|
||||
if (result == noErr) {
|
||||
inf(@"Removed key from keychain for: %@", user.userID);
|
||||
@ -82,14 +86,68 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
|
||||
|
||||
- (BOOL)signInAsUser:(MPUserEntity *)user usingMasterPassword:(NSString *)password {
|
||||
|
||||
assert(!password || ![NSThread isMainThread]); // If we need to computing a key, this operation shouldn't be on the main thread.
|
||||
MPKey *tryKey = nil;
|
||||
|
||||
// Method 1: When the user has no keyID set, set a new key from the given master password.
|
||||
if (!user.keyID) {
|
||||
if ([password length])
|
||||
if ((tryKey = [MPAlgorithmDefault keyForPassword:password ofUserNamed:user.name])) {
|
||||
user.keyID = tryKey.keyID;
|
||||
[user.managedObjectContext performBlockAndWait:^{
|
||||
user.keyID = tryKey.keyID;
|
||||
}];
|
||||
|
||||
// Migrate existing elements.
|
||||
MPKey *recoverKey = nil;
|
||||
PearlAlert *activityAlert = [PearlAlert showActivityWithTitle:PearlString(@"Migrating %d sites...", [user.elements count])];
|
||||
|
||||
for (MPElementEntity *element in user.elements) {
|
||||
if (element.type & MPElementTypeClassStored && ![element contentUsingKey:tryKey]) {
|
||||
id content = nil;
|
||||
if (recoverKey)
|
||||
content = [element contentUsingKey:recoverKey];
|
||||
|
||||
while (!content) {
|
||||
__block NSString *masterPassword = nil;
|
||||
dispatch_group_t recoverPasswordGroup = dispatch_group_create();
|
||||
dispatch_group_enter(recoverPasswordGroup);
|
||||
[PearlAlert showAlertWithTitle:@"Enter Old Master Password"
|
||||
message:PearlString(@"Your old master password is required to migrate the stored password for %@", element.name)
|
||||
viewStyle:UIAlertViewStyleSecureTextInput
|
||||
initAlert:nil
|
||||
tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
|
||||
@try {
|
||||
if (buttonIndex_ == [alert_ cancelButtonIndex])
|
||||
// Don't Migrate
|
||||
return;
|
||||
|
||||
masterPassword = [alert_ textFieldAtIndex:0].text;
|
||||
}
|
||||
@finally {
|
||||
dispatch_group_leave(recoverPasswordGroup);
|
||||
}
|
||||
} cancelTitle:@"Don't Migrate" otherTitles:@"Migrate", nil];
|
||||
dispatch_group_wait(recoverPasswordGroup, DISPATCH_TIME_FOREVER);
|
||||
|
||||
if (!masterPassword)
|
||||
// Don't Migrate
|
||||
break;
|
||||
|
||||
recoverKey = [element.algorithm keyForPassword:masterPassword ofUserNamed:user.name];
|
||||
content = [element contentUsingKey:recoverKey];
|
||||
}
|
||||
|
||||
if (!content)
|
||||
// Don't Migrate
|
||||
break;
|
||||
|
||||
[element.managedObjectContext performBlockAndWait:^{
|
||||
[element setContent:content usingKey:tryKey];
|
||||
}];
|
||||
}
|
||||
}
|
||||
[[MPAppDelegate_Shared get] saveContext];
|
||||
[activityAlert dismissAlert];
|
||||
}
|
||||
}
|
||||
|
||||
@ -152,9 +210,11 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
|
||||
}
|
||||
|
||||
|
||||
user.lastUsed = [NSDate date];
|
||||
self.activeUser = user;
|
||||
self.activeUser.requiresExplicitMigration = NO;
|
||||
[user.managedObjectContext performBlockAndWait:^{
|
||||
user.lastUsed = [NSDate date];
|
||||
self.activeUser = user;
|
||||
self.activeUser.requiresExplicitMigration = NO;
|
||||
}];
|
||||
[[MPAppDelegate_Shared get] saveContext];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationSignedIn object:self];
|
||||
|
@ -27,6 +27,9 @@
|
||||
@property (assign) BOOL requiresExplicitMigration;
|
||||
@property (readonly) id<MPAlgorithm> algorithm;
|
||||
|
||||
- (id)contentUsingKey:(MPKey *)key;
|
||||
- (void)setContent:(id)content usingKey:(MPKey *)key;
|
||||
|
||||
- (NSUInteger)use;
|
||||
- (NSString *)exportContent;
|
||||
- (void)importProtectedContent:(NSString *)protectedContent protectedByKey:(MPKey *)contentProtectionKey usingKey:(MPKey *)key2;
|
||||
|
@ -84,7 +84,32 @@
|
||||
|
||||
- (id)content {
|
||||
|
||||
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Content implementation missing." userInfo:nil];
|
||||
MPKey *key = [MPAppDelegate get].key;
|
||||
if (!key)
|
||||
return nil;
|
||||
|
||||
assert([key.keyID isEqualToData:self.user.keyID]);
|
||||
return [self contentUsingKey:key];
|
||||
}
|
||||
|
||||
- (void)setContent:(id)content {
|
||||
|
||||
MPKey *key = [MPAppDelegate get].key;
|
||||
if (!key)
|
||||
return;
|
||||
|
||||
assert([key.keyID isEqualToData:self.user.keyID]);
|
||||
[self setContent:content usingKey:key];
|
||||
}
|
||||
|
||||
- (id)contentUsingKey:(MPKey *)key {
|
||||
|
||||
Throw(@"Content retrieval implementation missing for: %@", [self class]);
|
||||
}
|
||||
|
||||
- (void)setContent:(id)content usingKey:(MPKey *)key {
|
||||
|
||||
Throw(@"Content assignment implementation missing for: %@", [self class]);
|
||||
}
|
||||
|
||||
- (NSString *)exportContent {
|
||||
@ -139,11 +164,7 @@
|
||||
self.counter_ = @(aCounter);
|
||||
}
|
||||
|
||||
- (id)content {
|
||||
|
||||
MPKey *key = [MPAppDelegate get].key;
|
||||
if (!key)
|
||||
return nil;
|
||||
- (id)contentUsingKey:(MPKey *)key {
|
||||
|
||||
if (!(self.type & MPElementTypeClassGenerated)) {
|
||||
err(@"Corrupt element: %@, type: %d is not in MPElementTypeClassGenerated", self.name, self.type);
|
||||
@ -156,6 +177,7 @@
|
||||
return [self.algorithm generateContentForElement:self usingKey:key];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@implementation MPElementStoredEntity (MP)
|
||||
@ -168,28 +190,9 @@
|
||||
matches:nil];
|
||||
}
|
||||
|
||||
- (id)content {
|
||||
|
||||
MPKey *key = [MPAppDelegate get].key;
|
||||
if (!key)
|
||||
return nil;
|
||||
|
||||
return [self contentUsingKey:key];
|
||||
}
|
||||
|
||||
- (void)setContent:(id)content {
|
||||
|
||||
MPKey *key = [MPAppDelegate get].key;
|
||||
if (!key)
|
||||
return;
|
||||
|
||||
[self setContent:content usingKey:key];
|
||||
}
|
||||
|
||||
- (id)contentUsingKey:(MPKey *)key {
|
||||
|
||||
assert(self.type & MPElementTypeClassStored);
|
||||
assert([key.keyID isEqualToData:self.user.keyID]);
|
||||
|
||||
NSData *encryptedContent;
|
||||
if (self.type & MPElementFeatureDevicePrivate)
|
||||
@ -201,6 +204,9 @@
|
||||
if ([encryptedContent length])
|
||||
decryptedContent = [self decryptContent:encryptedContent usingKey:key];
|
||||
|
||||
if (!decryptedContent)
|
||||
return nil;
|
||||
|
||||
return [[NSString alloc] initWithBytes:decryptedContent.bytes length:decryptedContent.length encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
|
@ -241,15 +241,7 @@
|
||||
if (!importedSitesData)
|
||||
return;
|
||||
|
||||
PearlAlert *activityAlert = [PearlAlert showAlertWithTitle:@"Importing" message:@"\n\n"
|
||||
viewStyle:UIAlertViewStyleDefault initAlert:
|
||||
^(UIAlertView *alert, UITextField *firstField) {
|
||||
UIActivityIndicatorView *activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
|
||||
activityIndicator.center = CGPointMake(140, 90);
|
||||
[activityIndicator startAnimating];
|
||||
[alert addSubview:activityIndicator];
|
||||
}
|
||||
tappedButtonBlock:nil cancelTitle:nil otherTitles:nil];
|
||||
PearlAlert *activityAlert = [PearlAlert showActivityWithTitle:@"Importing"];
|
||||
|
||||
NSString *importedSitesString = [[NSString alloc] initWithData:importedSitesData encoding:NSUTF8StringEncoding];
|
||||
MPImportResult result = [self importSites:importedSitesString askImportPassword:^NSString *(NSString *userName) {
|
||||
|
@ -96,9 +96,7 @@
|
||||
viewControllerAfterViewController:(UIViewController *)viewController {
|
||||
|
||||
NSUInteger vcIndex = [self.pageVCs indexOfObject:viewController];
|
||||
UIPageViewController *vc = [self.pageVCs objectAtIndex:(vcIndex + 1) % self.pageVCs.count];
|
||||
|
||||
return vc;
|
||||
return [self.pageVCs objectAtIndex:(vcIndex + 1) % self.pageVCs.count];
|
||||
}
|
||||
|
||||
- (void)viewDidUnload {
|
||||
|
@ -1167,11 +1167,11 @@ Pink fluffy door frame.</string>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="hkm-U7-Dm7" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<viewController storyboardIdentifier="MPAppViewController_0" id="yIx-9U-bOF" customClass="MPAppViewController" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Yea-s2-C8N">
|
||||
<rect key="frame" x="0.0" y="0.0" width="305" height="399"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="305" height="400"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" id="Wrz-tq-o1S">
|
||||
<rect key="frame" x="0.0" y="0.0" width="305" height="399"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="305" height="400"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
|
||||
<state key="normal" image="page-gorillas.png">
|
||||
@ -1186,7 +1186,7 @@ Pink fluffy door frame.</string>
|
||||
</connections>
|
||||
</button>
|
||||
<view contentMode="scaleToFill" id="JN1-cA-6yZ">
|
||||
<rect key="frame" x="0.0" y="330" width="305" height="60"/>
|
||||
<rect key="frame" x="0.0" y="331" width="305" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Gorillas At Large On Metropolis Rooftops" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="10" id="Ypg-Yc-UK3">
|
||||
@ -1632,7 +1632,7 @@ You could use the word wall for inspiration in finding a memorable master passw
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
</imageView>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" image="page-gorillas.png" id="QQT-37-azo">
|
||||
<rect key="frame" x="0.0" y="38" width="305" height="399"/>
|
||||
<rect key="frame" x="0.0" y="38" width="305" height="400"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
|
||||
</imageView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" id="drq-47-KK9">
|
||||
@ -2021,11 +2021,11 @@ You could use the word wall for inspiration in finding a memorable master passw
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="LHv-Mk-8Kp" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<viewController storyboardIdentifier="MPAppViewController_1" id="vOg-Xq-hKm" customClass="MPAppViewController" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="jzQ-Oa-Gdj">
|
||||
<rect key="frame" x="0.0" y="0.0" width="305" height="399"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="305" height="400"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" id="x6i-3e-0Rb">
|
||||
<rect key="frame" x="0.0" y="0.0" width="305" height="399"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="305" height="400"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
|
||||
<state key="normal" image="page-deblock.png">
|
||||
@ -2040,7 +2040,7 @@ You could use the word wall for inspiration in finding a memorable master passw
|
||||
</connections>
|
||||
</button>
|
||||
<view contentMode="scaleToFill" id="AC5-4y-ftd">
|
||||
<rect key="frame" x="0.0" y="330" width="305" height="60"/>
|
||||
<rect key="frame" x="0.0" y="331" width="305" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Block Destruction Puzzle" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="10" id="11H-1K-20G">
|
||||
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 122 KiB |
Before Width: | Height: | Size: 410 KiB After Width: | Height: | Size: 411 KiB |
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 162 KiB |
Before Width: | Height: | Size: 597 KiB After Width: | Height: | Size: 598 KiB |