From 21c05656192b7b3ef7edc3702748e76e5c270635 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Wed, 9 May 2012 10:11:34 +0200 Subject: [PATCH] Importing of mpsites + renames + fixes. [ADDED] Importing mpsites exports. [RENAMED] keyHashHex -> keyID. [RENAMED] Calculated -> Generated. [FIXED] iOS: Dismiss mail VC when done with it. [FIXED] iOS: Properly hide content tip icons when a content tip is shown while one with an icon is still active. [FIXED] iOS: Settings bundle was using old keys. --- MasterPassword/MPAppDelegate_Key.m | 4 +- MasterPassword/MPAppDelegate_Shared.h | 2 +- MasterPassword/MPAppDelegate_Store.h | 10 + MasterPassword/MPAppDelegate_Store.m | 281 +++++++++++++----- MasterPassword/MPElementEntity.h | 5 +- MasterPassword/MPElementEntity.m | 10 +- MasterPassword/MPElementGeneratedEntity.m | 2 +- MasterPassword/MPElementStoredEntity.m | 5 + MasterPassword/MPTypes.h | 12 +- MasterPassword/MPTypes.m | 24 +- MasterPassword/Mac/MPAppDelegate.m | 2 +- .../Mac/MPPasswordWindowController.m | 8 +- .../MasterPassword.xcdatamodel/contents | 9 +- MasterPassword/iOS/MPAppDelegate.m | 206 +++++++++---- MasterPassword/iOS/MPMainViewController.h | 2 + MasterPassword/iOS/MPMainViewController.m | 21 +- MasterPassword/iOS/MPSearchDelegate.m | 10 +- MasterPassword/iOS/MPTypeViewController.m | 14 +- MasterPassword/iOS/MasterPassword-Info.plist | 39 +++ MasterPassword/iOS/Settings.bundle/Root.plist | 6 +- 20 files changed, 475 insertions(+), 197 deletions(-) diff --git a/MasterPassword/MPAppDelegate_Key.m b/MasterPassword/MPAppDelegate_Key.m index 58f3f428..865ded1e 100644 --- a/MasterPassword/MPAppDelegate_Key.m +++ b/MasterPassword/MPAppDelegate_Key.m @@ -111,9 +111,9 @@ static NSDictionary *keyHashQuery() { if (key) { self.keyHash = keyHashForKey(key); - self.keyHashHex = [self.keyHash encodeHex]; + self.keyID = [self.keyHash encodeHex]; - dbg(@"Updating key hash to: %@.", self.keyHashHex); + dbg(@"Updating key ID to: %@.", self.keyID); [PearlKeyChain addOrUpdateItemForQuery:keyHashQuery() withAttributes:[NSDictionary dictionaryWithObjectsAndKeys: self.keyHash, (__bridge id)kSecValueData, diff --git a/MasterPassword/MPAppDelegate_Shared.h b/MasterPassword/MPAppDelegate_Shared.h index ba95fc18..06c746f0 100644 --- a/MasterPassword/MPAppDelegate_Shared.h +++ b/MasterPassword/MPAppDelegate_Shared.h @@ -15,7 +15,7 @@ @property (strong, nonatomic) NSData *key; @property (strong, nonatomic) NSData *keyHash; -@property (strong, nonatomic) NSString *keyHashHex; +@property (strong, nonatomic) NSString *keyID; @end diff --git a/MasterPassword/MPAppDelegate_Store.h b/MasterPassword/MPAppDelegate_Store.h index 61003730..f6ed5251 100644 --- a/MasterPassword/MPAppDelegate_Store.h +++ b/MasterPassword/MPAppDelegate_Store.h @@ -10,6 +10,14 @@ #import "UbiquityStoreManager.h" +typedef enum { + MPImportResultSuccess, + MPImportResultCancelled, + MPImportResultInvalidPassword, + MPImportResultMalformedInput, + MPImportResultInternalError, +} MPImportResult; + @interface MPAppDelegate (Store) + (NSManagedObjectContext *)managedObjectContext; @@ -21,6 +29,8 @@ - (void)saveContext; - (void)printStore; +- (MPImportResult)importSites:(NSString *)importedSitesString withPassword:(NSString *)password + askConfirmation:(BOOL(^)(NSUInteger importCount, NSUInteger deleteCount))confirmation; - (NSString *)exportSitesShowingPasswords:(BOOL)showPasswords; @end diff --git a/MasterPassword/MPAppDelegate_Store.m b/MasterPassword/MPAppDelegate_Store.m index ff353c9e..b07d125b 100644 --- a/MasterPassword/MPAppDelegate_Store.m +++ b/MasterPassword/MPAppDelegate_Store.m @@ -11,6 +11,20 @@ @implementation MPAppDelegate (Store) +static NSDateFormatter *rfc3339DateFormatter = nil; + +- (void)loadRFC3339DateFormatter { + + if (rfc3339DateFormatter) + return; + + rfc3339DateFormatter = [NSDateFormatter new]; + NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; + [rfc3339DateFormatter setLocale:enUSPOSIXLocale]; + [rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"]; + [rfc3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; +} + + (NSManagedObjectContext *)managedObjectContext { return [[self get] managedObjectContext]; @@ -83,23 +97,29 @@ #if TARGET_OS_IPHONE [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillEnterForegroundNotification object:[UIApplication sharedApplication] queue:nil -#else - [[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillBecomeActiveNotification - object:[NSApplication sharedApplication] queue:nil -#endif usingBlock:^(NSNotification *note) { [storeManager checkiCloudStatus]; }]; +#else + [[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillBecomeActiveNotification + object:[NSApplication sharedApplication] queue:nil + usingBlock:^(NSNotification *note) { + [storeManager checkiCloudStatus]; + }]; +#endif #if TARGET_OS_IPHONE [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillTerminateNotification object:[UIApplication sharedApplication] queue:nil -#else - [[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillTerminateNotification - object:[NSApplication sharedApplication] queue:nil -#endif usingBlock:^(NSNotification *note) { [self saveContext]; }]; +#else + [[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillTerminateNotification + object:[NSApplication sharedApplication] queue:nil + usingBlock:^(NSNotification *note) { + [self saveContext]; + }]; +#endif return storeManager; } @@ -136,91 +156,210 @@ for(NSManagedObject *o in results) { if ([o isKindOfClass:[MPElementEntity class]]) { MPElementEntity *e = (MPElementEntity *)o; - trc(@"For descriptor: %@, found: %@: %@ (%@)", entity.name, [o class], e.name, e.mpHashHex); + trc(@"For descriptor: %@, found: %@: %@ (%@)", entity.name, [o class], e.name, e.keyID); } else { trc(@"For descriptor: %@, found: %@", entity.name, [o class]); } } } trc(@"---"); - if ([MPAppDelegate get].keyHashHex) { + if ([MPAppDelegate get].keyID) { trc(@"=== Known sites ==="); NSFetchRequest *fetchRequest = [[self managedObjectModel] fetchRequestFromTemplateWithName:@"MPElements" substitutionVariables:[NSDictionary dictionaryWithObjectsAndKeys: @"", @"query", - [MPAppDelegate get].keyHashHex, @"mpHashHex", + [MPAppDelegate get].keyID, @"keyID", nil]]; [fetchRequest setSortDescriptors: [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]]]; NSError *error = nil; for (MPElementEntity *e in [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error]) { - trc(@"Found site: %@ (%@): %@", e.name, e.mpHashHex, e); + trc(@"Found site: %@ (%@): %@", e.name, e.keyID, e); } trc(@"---"); } else trc(@"Not printing sites: master password not set."); }]; } - - - (NSString *)exportSitesShowingPasswords:(BOOL)showPasswords { - - static NSDateFormatter *rfc3339DateFormatter = nil; - if (!rfc3339DateFormatter) { - rfc3339DateFormatter = [NSDateFormatter new]; - NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; - [rfc3339DateFormatter setLocale:enUSPOSIXLocale]; - [rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"]; - [rfc3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; - } - - // Header. - NSMutableString *export = [NSMutableString new]; - [export appendFormat:@"# MasterPassword %@\n", [PearlInfoPlist get].CFBundleVersion]; - if (showPasswords) - [export appendFormat:@"# Export of site names and passwords in clear-text.\n"]; - else - [export appendFormat:@"# Export of site names and stored passwords (unless device-private) encrypted with the master key.\n"]; - [export appendFormat:@"\n"]; - [export appendFormat:@"# Key ID: %@\n", self.keyHashHex]; - [export appendFormat:@"# Date: %@\n", [rfc3339DateFormatter stringFromDate:[NSDate date]]]; - if (showPasswords) - [export appendFormat:@"# Passwords: VISIBLE\n"]; - else - [export appendFormat:@"# Passwords: PROTECTED\n"]; - [export appendFormat:@"\n"]; - - // Sites. - NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])]; - fetchRequest.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]]; - fetchRequest.predicate = [NSPredicate predicateWithFormat:@"mpHashHex == %@", self.keyHashHex]; - __autoreleasing NSError *error = nil; - NSArray *elements = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; - if (error) - err(@"Error fetching sites for export: %@", error); - - for (MPElementEntity *element in elements) { - NSString *name = element.name; - MPElementType type = (unsigned)element.type; - int16_t uses = element.uses; - NSTimeInterval lastUsed = element.lastUsed; - NSString *content = nil; - - // Determine the content to export. - if (!(type & MPElementFeatureDevicePrivate)) { - if (showPasswords) - content = element.content; - else if (type & MPElementFeatureExportContent) - content = element.exportContent; - } - - [export appendFormat:@"%@\t%d\t%d\t%@\t%@\n", - name, type, uses, [rfc3339DateFormatter stringFromDate:[NSDate dateWithTimeIntervalSinceReferenceDate:lastUsed]], content]; - - } - - return export; - } + +- (MPImportResult)importSites:(NSString *)importedSitesString withPassword:(NSString *)password + askConfirmation:(BOOL(^)(NSUInteger importCount, NSUInteger deleteCount))confirmation { + + [self loadRFC3339DateFormatter]; + + static NSRegularExpression *headerPattern, *sitePattern; + __autoreleasing NSError *error; + if (!headerPattern) { + headerPattern = [[NSRegularExpression alloc] + initWithPattern:@"^#[[:space:]]*([^:]+): (.*)" + options:0 error:&error]; + if (error) + err(@"Error loading the header pattern: %@", error); + } + if (!sitePattern) { + sitePattern = [[NSRegularExpression alloc] + initWithPattern:@"^([^[:space:]]+)[[:space:]]+([[:digit:]]+)[[:space:]]+([[:digit:]]+)[[:space:]]+([^\t]+)\t(.*)" + options:0 error:&error]; + if (error) + err(@"Error loading the site pattern: %@", error); + } + if (!headerPattern || !sitePattern) + return MPImportResultInternalError; + + NSString *keyID = nil; + BOOL headerStarted = NO, headerEnded = NO; + NSArray *importedSiteLines = [importedSitesString componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; + NSMutableSet *elementsToDelete = [NSMutableSet set]; + NSMutableArray *importedSiteElements = [NSMutableArray arrayWithCapacity:[importedSiteLines count]]; + NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])]; + for(NSString *importedSiteLine in importedSiteLines) { + if ([importedSiteLine hasPrefix:@"#"]) { + // Comment or header + if (!headerStarted) { + if ([importedSiteLine isEqualToString:@"##"]) + headerStarted = YES; + continue; + } + if (headerEnded) + continue; + if ([importedSiteLine isEqualToString:@"##"]) { + headerEnded = YES; + continue; + } + + // Header + if ([headerPattern numberOfMatchesInString:importedSiteLine options:0 range:NSRangeFromString(importedSiteLine)] != 2) { + err(@"Invalid header format in line: %@", importedSiteLine); + return MPImportResultMalformedInput; + } + NSArray *headerElements = [headerPattern matchesInString:importedSiteLine options:0 range:NSRangeFromString(importedSiteLine)]; + NSString *key = [importedSiteLine substringWithRange:[[headerElements objectAtIndex:0] range]]; + NSString *value = [importedSiteLine substringWithRange:[[headerElements objectAtIndex:1] range]]; + if ([key isEqualToString:@"Key ID"]) { + if (![(keyID = value) isEqualToString:[keyHashForPassword(password) encodeHex]]) + return MPImportResultInvalidPassword; + } + + continue; + } + if (!headerEnded) + continue; + if (!keyID) + return MPImportResultMalformedInput; + if (![importedSiteLine length]) + continue; + + // Site + if ([sitePattern numberOfMatchesInString:importedSiteLine options:0 range:NSRangeFromString(importedSiteLine)] != 2) { + err(@"Invalid site format in line: %@", importedSiteLine); + return MPImportResultMalformedInput; + } + NSArray *siteElements = [headerPattern matchesInString:importedSiteLine options:0 range:NSRangeFromString(importedSiteLine)]; + NSString *lastUsed = [importedSiteLine substringWithRange:[[siteElements objectAtIndex:0] range]]; + NSString *uses = [importedSiteLine substringWithRange:[[siteElements objectAtIndex:1] range]]; + NSString *type = [importedSiteLine substringWithRange:[[siteElements objectAtIndex:2] range]]; + NSString *name = [importedSiteLine substringWithRange:[[siteElements objectAtIndex:3] range]]; + NSString *exportContent = [importedSiteLine substringWithRange:[[siteElements objectAtIndex:4] range]]; + + // Find existing site. + fetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@ AND keyID == %@", name, keyID]; + NSArray *existingSites = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; + if (error) + err(@"Couldn't search existing sites: %@", error); + if (!existingSites) + return MPImportResultInternalError; + + [elementsToDelete addObjectsFromArray:existingSites]; + [importedSiteElements addObject:[NSArray arrayWithObjects:lastUsed, uses, type, name, exportContent, nil]]; + } + + // Ask for confirmation to import these sites. + if (!confirmation([importedSiteElements count], [elementsToDelete count])) + return MPImportResultCancelled; + + // Delete existing sites. + [elementsToDelete enumerateObjectsUsingBlock:^(id obj, BOOL *stop) { + [self.managedObjectContext deleteObject:obj]; + }]; + + // Import new sites. + for (NSArray *siteElements in importedSiteElements) { + NSDate *lastUsed = [rfc3339DateFormatter dateFromString:[siteElements objectAtIndex:0]]; + NSInteger uses = [[siteElements objectAtIndex:1] integerValue]; + MPElementType type = (unsigned)[[siteElements objectAtIndex:2] integerValue]; + NSString *name = [siteElements objectAtIndex:3]; + NSString *exportContent = [siteElements objectAtIndex:4]; + + // Create new site. + MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:ClassNameFromMPElementType(type) + inManagedObjectContext:self.managedObjectContext]; + element.lastUsed = [lastUsed timeIntervalSinceReferenceDate]; + element.uses = uses; + element.type = type; + element.name = name; + if ([exportContent length]) + [element importContent:exportContent]; + } + + return MPImportResultSuccess; +} + +- (NSString *)exportSitesShowingPasswords:(BOOL)showPasswords { + + [self loadRFC3339DateFormatter]; + + // Header. + NSMutableString *export = [NSMutableString new]; + [export appendFormat:@"# Master Password site export\n"]; + if (showPasswords) + [export appendFormat:@"# Export of site names and passwords in clear-text.\n"]; + else + [export appendFormat:@"# Export of site names and stored passwords (unless device-private) encrypted with the master key.\n"]; + [export appendFormat:@"# \n"]; + [export appendFormat:@"##\n"]; + [export appendFormat:@"# Version: %@\n", [PearlInfoPlist get].CFBundleVersion]; + [export appendFormat:@"# Key ID: %@\n", self.keyID]; + [export appendFormat:@"# Date: %@\n", [rfc3339DateFormatter stringFromDate:[NSDate date]]]; + if (showPasswords) + [export appendFormat:@"# Passwords: VISIBLE\n"]; + else + [export appendFormat:@"# Passwords: PROTECTED\n"]; + [export appendFormat:@"##\n"]; + [export appendFormat:@"#\n"]; + [export appendFormat:@"# Last Times Password Site\tSite\n"]; + [export appendFormat:@"# used used type name\tpassword\n"]; + + // Sites. + NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])]; + fetchRequest.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]]; + fetchRequest.predicate = [NSPredicate predicateWithFormat:@"keyID == %@", self.keyID]; + __autoreleasing NSError *error = nil; + NSArray *elements = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; + if (error) + err(@"Error fetching sites for export: %@", error); + + for (MPElementEntity *element in elements) { + NSTimeInterval lastUsed = element.lastUsed; + int16_t uses = element.uses; + MPElementType type = (unsigned)element.type; + NSString *name = element.name; + NSString *content = nil; + + // Determine the content to export. + if (!(type & MPElementFeatureDevicePrivate)) { + if (showPasswords) + content = element.content; + else if (type & MPElementFeatureExportContent) + content = element.exportContent; + } + + [export appendFormat:@"%@ %8d %8d %20s\t%@\n", + [rfc3339DateFormatter stringFromDate:[NSDate dateWithTimeIntervalSinceReferenceDate:lastUsed]], uses, type, [name cStringUsingEncoding:NSUTF8StringEncoding], content? content: @""]; + } + + return export; +} @end diff --git a/MasterPassword/MPElementEntity.h b/MasterPassword/MPElementEntity.h index ffd46fee..4da7d035 100644 --- a/MasterPassword/MPElementEntity.h +++ b/MasterPassword/MPElementEntity.h @@ -13,14 +13,15 @@ @interface MPElementEntity : NSManagedObject @property (nonatomic, retain) NSString *name; -@property (nonatomic, retain) NSString *mpHashHex; +@property (nonatomic, retain) NSString *keyID; @property (nonatomic, assign) int16_t type; @property (nonatomic, assign) int16_t uses; @property (nonatomic, assign) NSTimeInterval lastUsed; @property (nonatomic, retain, readonly) id content; -@property (nonatomic, retain, readonly) NSString *exportContent; - (int16_t)use; +- (NSString *)exportContent; +- (void)importContent:(NSString *)content; @end diff --git a/MasterPassword/MPElementEntity.m b/MasterPassword/MPElementEntity.m index e2b93eab..84349155 100644 --- a/MasterPassword/MPElementEntity.m +++ b/MasterPassword/MPElementEntity.m @@ -12,7 +12,7 @@ @implementation MPElementEntity @dynamic name; -@dynamic mpHashHex; +@dynamic keyID; @dynamic type; @dynamic uses; @dynamic lastUsed; @@ -33,6 +33,10 @@ return nil; } +- (void)importContent:(NSString *)content { + +} + - (NSString *)description { return str(@"%@:%@", [self class], [self name]); @@ -40,8 +44,8 @@ - (NSString *)debugDescription { - return str(@"{%@: name=%@, mpHashHex=%@, type=%d, uses=%d, lastUsed=%@}", - NSStringFromClass([self class]), self.name, self.mpHashHex, self.type, self.uses, self.lastUsed); + return str(@"{%@: name=%@, keyID=%@, type=%d, uses=%d, lastUsed=%@}", + NSStringFromClass([self class]), self.name, self.keyID, self.type, self.uses, self.lastUsed); } @end diff --git a/MasterPassword/MPElementGeneratedEntity.m b/MasterPassword/MPElementGeneratedEntity.m index e00145d7..83ca2042 100644 --- a/MasterPassword/MPElementGeneratedEntity.m +++ b/MasterPassword/MPElementGeneratedEntity.m @@ -16,7 +16,7 @@ - (id)content { - if (!(self.type & MPElementTypeClassCalculated)) { + if (!(self.type & MPElementTypeClassGenerated)) { err(@"Corrupt element: %@, type: %d, does not match class: %@", self.name, self.type, [self class]); return nil; } diff --git a/MasterPassword/MPElementStoredEntity.m b/MasterPassword/MPElementStoredEntity.m index 82cfee91..a2d6bfd0 100644 --- a/MasterPassword/MPElementStoredEntity.m +++ b/MasterPassword/MPElementStoredEntity.m @@ -67,4 +67,9 @@ return [self.contentObject encodeBase64]; } +- (void)importContent:(NSString *)content { + + self.contentObject = [content decodeBase64]; +} + @end diff --git a/MasterPassword/MPTypes.h b/MasterPassword/MPTypes.h index 4108122c..57644056 100644 --- a/MasterPassword/MPTypes.h +++ b/MasterPassword/MPTypes.h @@ -18,16 +18,16 @@ typedef enum { typedef enum { /** Generate the password. */ - MPElementTypeClassGenerated = 1 << 4, + MPElementTypeClassGenerated = 1 << 4, /** Store the password. */ - MPElementTypeClassStored = 1 << 5, + MPElementTypeClassStored = 1 << 5, } MPElementTypeClass; typedef enum { /** Export the key-protected content data. */ - MPElementFeatureExportContent = 1 << 10, + MPElementFeatureExportContent = 1 << 10, /** Never export content. */ - MPElementFeatureDevicePrivate = 1 << 11, + MPElementFeatureDevicePrivate = 1 << 11, } MPElementFeature; typedef enum { @@ -37,8 +37,8 @@ typedef enum { MPElementTypeGeneratedBasic = 0x3 | MPElementTypeClassGenerated | 0x0, MPElementTypeGeneratedPIN = 0x4 | MPElementTypeClassGenerated | 0x0, - MPElementTypeStoredPersonal = 0x0 | MPElementTypeClassStored | MPElementFeatureExportContent | MPElementFeatureDevicePrivate, - MPElementTypeStoredDevicePrivate = 0x1 | MPElementTypeClassStored | 0x0, + MPElementTypeStoredPersonal = 0x0 | MPElementTypeClassStored | MPElementFeatureExportContent, + MPElementTypeStoredDevicePrivate = 0x1 | MPElementTypeClassStored | MPElementFeatureDevicePrivate, } MPElementType; #define MPTestFlightCheckpointAction @"MPTestFlightCheckpointAction" diff --git a/MasterPassword/MPTypes.m b/MasterPassword/MPTypes.m index 773e4c0d..0e46a6cd 100644 --- a/MasterPassword/MPTypes.m +++ b/MasterPassword/MPTypes.m @@ -38,19 +38,19 @@ NSString *NSStringFromMPElementType(MPElementType type) { return nil; switch (type) { - case MPElementTypeCalculatedLong: + case MPElementTypeGeneratedLong: return @"Long Password"; - case MPElementTypeCalculatedMedium: + case MPElementTypeGeneratedMedium: return @"Medium Password"; - case MPElementTypeCalculatedShort: + case MPElementTypeGeneratedShort: return @"Short Password"; - case MPElementTypeCalculatedBasic: + case MPElementTypeGeneratedBasic: return @"Basic Password"; - case MPElementTypeCalculatedPIN: + case MPElementTypeGeneratedPIN: return @"PIN"; case MPElementTypeStoredPersonal: @@ -70,19 +70,19 @@ Class ClassFromMPElementType(MPElementType type) { return nil; switch (type) { - case MPElementTypeCalculatedLong: + case MPElementTypeGeneratedLong: return [MPElementGeneratedEntity class]; - case MPElementTypeCalculatedMedium: + case MPElementTypeGeneratedMedium: return [MPElementGeneratedEntity class]; - case MPElementTypeCalculatedShort: + case MPElementTypeGeneratedShort: return [MPElementGeneratedEntity class]; - case MPElementTypeCalculatedBasic: + case MPElementTypeGeneratedBasic: return [MPElementGeneratedEntity class]; - case MPElementTypeCalculatedPIN: + case MPElementTypeGeneratedPIN: return [MPElementGeneratedEntity class]; case MPElementTypeStoredPersonal: @@ -108,8 +108,8 @@ NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, in err(@"Missing name."); return nil; } - if (!(type & MPElementTypeClassCalculated)) { - err(@"Incorrect type (is not MPElementTypeClassCalculated): %d, for: %@", type, name); + if (!(type & MPElementTypeClassGenerated)) { + err(@"Incorrect type (is not MPElementTypeClassGenerated): %d, for: %@", type, name); return nil; } if (!key) { diff --git a/MasterPassword/Mac/MPAppDelegate.m b/MasterPassword/Mac/MPAppDelegate.m index 44a3d867..b1877ade 100644 --- a/MasterPassword/Mac/MPAppDelegate.m +++ b/MasterPassword/Mac/MPAppDelegate.m @@ -30,7 +30,7 @@ @synthesize key; @synthesize keyHash; -@synthesize keyHashHex; +@synthesize keyID; #pragma GCC diagnostic ignored "-Wfour-char-constants" static EventHotKeyID MPShowHotKey = { .signature = 'show', .id = 1 }; diff --git a/MasterPassword/Mac/MPPasswordWindowController.m b/MasterPassword/Mac/MPPasswordWindowController.m index 0b0ab652..966c5be7 100644 --- a/MasterPassword/Mac/MPPasswordWindowController.m +++ b/MasterPassword/Mac/MPPasswordWindowController.m @@ -110,14 +110,14 @@ - (NSArray *)control:(NSControl *)control textView:(NSTextView *)textView completions:(NSArray *)words forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index { NSString *query = [[control stringValue] substringWithRange:charRange]; - if (![query length] || ![MPAppDelegate get].keyHashHex) + if (![query length] || ![MPAppDelegate get].keyID) return nil; NSFetchRequest *fetchRequest = [MPAppDelegate.managedObjectModel fetchRequestFromTemplateWithName:@"MPElements" substitutionVariables:[NSDictionary dictionaryWithObjectsAndKeys: query, @"query", - [MPAppDelegate get].keyHashHex, @"mpHashHex", + [MPAppDelegate get].keyID, @"keyID", nil]]; [fetchRequest setSortDescriptors: [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]]]; @@ -222,10 +222,10 @@ MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPElementGeneratedEntity class]) inManagedObjectContext:[MPAppDelegate get].managedObjectContext]; assert([element isKindOfClass:ClassFromMPElementType(element.type)]); - assert([MPAppDelegate get].keyHashHex); + assert([MPAppDelegate get].keyID); element.name = siteName; - element.mpHashHex = [MPAppDelegate get].keyHashHex; + element.keyID = [MPAppDelegate get].keyID; NSString *description = [element.content description]; [element use]; diff --git a/MasterPassword/MasterPassword.xcdatamodeld/MasterPassword.xcdatamodel/contents b/MasterPassword/MasterPassword.xcdatamodeld/MasterPassword.xcdatamodel/contents index 16b4548f..9bc67eeb 100644 --- a/MasterPassword/MasterPassword.xcdatamodeld/MasterPassword.xcdatamodel/contents +++ b/MasterPassword/MasterPassword.xcdatamodeld/MasterPassword.xcdatamodel/contents @@ -1,10 +1,10 @@ - + + - - + @@ -13,9 +13,8 @@ - - + diff --git a/MasterPassword/iOS/MPAppDelegate.m b/MasterPassword/iOS/MPAppDelegate.m index d4a06267..53b4e2e9 100644 --- a/MasterPassword/iOS/MPAppDelegate.m +++ b/MasterPassword/iOS/MPAppDelegate.m @@ -32,7 +32,7 @@ @synthesize key; @synthesize keyHash; -@synthesize keyHashHex; +@synthesize keyID; + (void)initialize { @@ -43,6 +43,75 @@ //[NSClassFromString(@"WebView") performSelector:NSSelectorFromString(@"_enableRemoteInspector")]; #endif } +- (void)showGuide { + + [self.navigationController performSegueWithIdentifier:@"MP_Guide" sender:self]; + + [TestFlight passCheckpoint:MPTestFlightCheckpointShowGuide]; +} + +- (void)loadKey:(BOOL)animated { + + if (!self.key) + // Try and load the key from the keychain. + [self loadStoredKey]; + + if (!self.key) + // Ask the user to set the key through his master password. + dispatch_async(dispatch_get_main_queue(), ^{ + [self.navigationController presentViewController: + [self.navigationController.storyboard instantiateViewControllerWithIdentifier:@"MPUnlockViewController"] + animated:animated completion:nil]; + }); +} + +- (void)export { + + [PearlAlert showNotice: + @"This will export all your site names.\n\n" + @"You can open the export with a text editor to get an overview of all your sites.\n\n" + @"The file also acts as a personal backup of your site list in case you don't sync with iCloud/iTunes." + tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) { + [PearlAlert showAlertWithTitle:@"Reveal Passwords?" message: + @"Would you like to make all your passwords visible in the export?\n\n" + @"A safe export will only include your stored passwords, in an encrypted manner, " + @"making the result safe from falling in the wrong hands.\n\n" + @"If all your passwords are shown and somebody else finds the export, " + @"they could gain access to all your sites!" + viewStyle:UIAlertViewStyleDefault tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) { + if (buttonIndex == [alert firstOtherButtonIndex] + 0) + // Safe Export + [self exportShowPasswords:NO]; + if (buttonIndex == [alert firstOtherButtonIndex] + 1) + // Safe Export + [self exportShowPasswords:YES]; + } cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Safe Export", @"Show Passwords", nil]; + } otherTitles:nil]; +} + +- (void)exportShowPasswords:(BOOL)showPasswords { + + NSString *exportedSites = [self exportSitesShowingPasswords:showPasswords]; + NSString *message; + if (showPasswords) + message = @"Export of my Master Password sites with passwords visible.\n\nREMINDER: Make sure nobody else sees this file!\n"; + else + message = @"Backup of my Master Password sites.\n"; + + NSDateFormatter *exportDateFormatter = [NSDateFormatter new]; + [exportDateFormatter setDateFormat:@"'Master Password sites ('yyyy'-'MM'-'DD').mpsites'"]; + + MFMailComposeViewController *composer = [[MFMailComposeViewController alloc] init]; + [composer setMailComposeDelegate:self]; + [composer setSubject:@"Master Password site export"]; + [composer setMessageBody:message isHTML:NO]; + [composer addAttachmentData:[exportedSites dataUsingEncoding:NSUTF8StringEncoding] mimeType:@"text/plain" + fileName:[exportDateFormatter stringFromDate:[NSDate date]]]; + [self.window.rootViewController presentModalViewController:composer animated:YES]; +} + +#pragma mark - UIApplicationDelegate + - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { @@ -188,6 +257,69 @@ return [super application:application didFinishLaunchingWithOptions:launchOptions]; } +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url + sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { + + __autoreleasing NSError *error; + __autoreleasing NSURLResponse *response; + NSData *importedSitesData = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:url] + returningResponse:&response error:&error]; + if (error) + err(@"While reading imported sites from %@: %@", url, error); + if (!importedSitesData) + return NO; + + NSString *importedSitesString = [[NSString alloc] initWithData:importedSitesData encoding:NSUTF8StringEncoding]; + [PearlAlert showAlertWithTitle:@"Import Password" message: + @"Enter the master password for this export:" + viewStyle:UIAlertViewStyleSecureTextInput tappedButtonBlock: + ^(UIAlertView *alert, NSInteger buttonIndex) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + MPImportResult result = [self importSites:importedSitesString withPassword:[alert textFieldAtIndex:0].text + askConfirmation:^BOOL(NSUInteger importCount, NSUInteger deleteCount) { + __block BOOL confirmation = NO; + + dispatch_group_t confirmationGroup = dispatch_group_create(); + dispatch_group_enter(confirmationGroup); + dispatch_async(dispatch_get_main_queue(), ^{ + [PearlAlert showAlertWithTitle:@"Import Sites?" + message:l(@"Import %d sites, overwriting %d existing sites?", importCount, deleteCount) + viewStyle:UIAlertViewStyleDefault + tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) { + if (buttonIndex != [alert cancelButtonIndex]) + confirmation = YES; + + dispatch_group_leave(confirmationGroup); + } + cancelTitle:[PearlStrings get].commonButtonCancel + otherTitles:@"Import", nil]; + }); + dispatch_group_wait(confirmationGroup, DISPATCH_TIME_FOREVER); + + return confirmation; + }]; + + switch (result) { + case MPImportResultSuccess: + case MPImportResultCancelled: + break; + case MPImportResultInternalError: + [PearlAlert showError:@"Import failed because of an internal error."]; + break; + case MPImportResultMalformedInput: + [PearlAlert showError:@"The import doesn't look like a Master Password export."]; + break; + case MPImportResultInvalidPassword: + [PearlAlert showError:@"Incorrect master password for the import sites."]; + break; + } + }); + } + cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Unlock File", nil]; + + return YES; +} + - (void)applicationDidBecomeActive:(UIApplication *)application { if ([[MPiOSConfig get].showQuickStart boolValue]) @@ -198,70 +330,6 @@ [TestFlight passCheckpoint:MPTestFlightCheckpointActivated]; } -- (void)showGuide { - - [self.navigationController performSegueWithIdentifier:@"MP_Guide" sender:self]; - - [TestFlight passCheckpoint:MPTestFlightCheckpointShowGuide]; -} - -- (void)loadKey:(BOOL)animated { - - if (!self.key) - // Try and load the key from the keychain. - [self loadStoredKey]; - - if (!self.key) - // Ask the user to set the key through his master password. - dispatch_async(dispatch_get_main_queue(), ^{ - [self.navigationController presentViewController: - [self.navigationController.storyboard instantiateViewControllerWithIdentifier:@"MPUnlockViewController"] - animated:animated completion:nil]; - }); -} - -- (void)export { - - [PearlAlert showNotice: - @"This export contains the names of all your sites. " - @"You can even open it in a text editor to view its contents.\n\n" - @"If you ever loose your device and don't have iCloud enabled or sync with iTunes, " - @"this will help you remember what sites you had an account with.\n" - @"Don't worry: Even if you don't have an export of your sites, " - @"loosing your device never means loosing your generated passwords."]; - [PearlAlert showAlertWithTitle:@"Reveal Passwords?" message: - @"Would you like to make all your passwords visible in the export?\n\n" - @"By default, only your stored passwords are exported, in an encrypted manner, " - @"making it safe from falling in the wrong hands.\n" - @"If you make all your passwords visible and somebody else finds the file, " - @"they can gain access to all your sites with it!" - viewStyle:UIAlertViewStyleDefault tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) { - if (buttonIndex == [alert firstOtherButtonIndex] + 0) - // Safe Export - [self exportShowPasswords:NO]; - if (buttonIndex == [alert firstOtherButtonIndex] + 1) - // Safe Export - [self exportShowPasswords:YES]; - } cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Safe Export", @"Show Passwords", nil]; -} - -- (void)exportShowPasswords:(BOOL)showPasswords { - - NSString *exportedSites = [self exportSitesShowingPasswords:showPasswords]; - - NSDateFormatter *exportDateFormatter = [NSDateFormatter new]; - [exportDateFormatter setDateFormat:@"yyyy'-'MM'-'DD'T'HH':'mm'.mpsites'"]; - - MFMailComposeViewController *composer = [[MFMailComposeViewController alloc] init]; - [composer setMailComposeDelegate:self]; - [composer setSubject:@"Master Password site export"]; - [composer addAttachmentData:[exportedSites dataUsingEncoding:NSUTF8StringEncoding] mimeType:@"text/plain" - fileName:[exportDateFormatter stringFromDate:[NSDate date]]]; - [self.window.rootViewController presentModalViewController:composer animated:YES]; -} - -#pragma mark - UIApplicationDelegate - - (void)applicationDidEnterBackground:(UIApplication *)application { [[LocalyticsSession sharedLocalyticsSession] close]; @@ -314,10 +382,16 @@ break; case MFMailComposeResultFailed: - break; + [PearlAlert showError:@"A problem occurred while sending the message." tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) { + if (buttonIndex == [alert firstOtherButtonIndex]) + return; + } otherTitles:@"Retry", nil]; + return; case MFMailComposeResultCancelled: break; } + + [controller dismissModalViewControllerAnimated:YES]; } #pragma mark - TestFlight diff --git a/MasterPassword/iOS/MPMainViewController.h b/MasterPassword/iOS/MPMainViewController.h index 038a2d26..b7b74fbc 100644 --- a/MasterPassword/iOS/MPMainViewController.h +++ b/MasterPassword/iOS/MPMainViewController.h @@ -32,6 +32,8 @@ @property (weak, nonatomic) IBOutlet UIImageView *contentTipEditIcon; @property (weak, nonatomic) IBOutlet UIView *searchTipContainer; +@property (copy) void (^contentTipCleanup)(BOOL finished); + - (IBAction)copyContent; - (IBAction)incrementPasswordCounter; - (IBAction)resetPasswordCounter:(UILongPressGestureRecognizer *)sender; diff --git a/MasterPassword/iOS/MPMainViewController.m b/MasterPassword/iOS/MPMainViewController.m index 05fcb929..cb227f52 100644 --- a/MasterPassword/iOS/MPMainViewController.m +++ b/MasterPassword/iOS/MPMainViewController.m @@ -45,6 +45,7 @@ @synthesize contentTipEditIcon = _contentTipEditIcon; @synthesize searchTipContainer = _searchTip; @synthesize contentField = _contentField; +@synthesize contentTipCleanup; #pragma mark - View lifecycle @@ -161,8 +162,8 @@ [self setHelpChapter:self.activeElement? @"2": @"1"]; self.siteName.text = self.activeElement.name; - self.passwordCounter.alpha = self.activeElement.type & MPElementTypeClassCalculated? 0.5f: 0; - self.passwordIncrementer.alpha = self.activeElement.type & MPElementTypeClassCalculated? 0.5f: 0; + self.passwordCounter.alpha = self.activeElement.type & MPElementTypeClassGenerated? 0.5f: 0; + self.passwordIncrementer.alpha = self.activeElement.type & MPElementTypeClassGenerated? 0.5f: 0; self.passwordEdit.alpha = self.activeElement.type & MPElementTypeClassStored? 0.5f: 0; [self.typeButton setTitle:NSStringFromMPElementType((unsigned)self.activeElement.type) @@ -235,7 +236,14 @@ - (void)showContentTip:(NSString *)message withIcon:(UIImageView *)icon { dispatch_async(dispatch_get_main_queue(), ^{ + if (self.contentTipCleanup) + self.contentTipCleanup(NO); + self.contentTipBody.text = message; + self.contentTipCleanup = ^(BOOL finished) { + icon.hidden = YES; + self.contentTipCleanup = nil; + }; icon.hidden = NO; [UIView animateWithDuration:0.2f animations:^{ @@ -246,10 +254,7 @@ dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ [UIView animateWithDuration:0.2f animations:^{ self.contentTipContainer.alpha = 0; - } completion:^(BOOL finished) { - if (finished) - icon.hidden = YES; - }]; + } completion:self.contentTipCleanup]; }); } }]; @@ -441,7 +446,7 @@ [TestFlight passCheckpoint:MPTestFlightCheckpointAction]; } cancelTitle:[PearlStrings get].commonButtonCancel destructiveTitle:nil otherTitles: - [self isHelpVisible]? @"Hide Help": @"Show Help", @"FAQ", @"Tutorial", @"Settings", @"Export", @"Import", + [self isHelpVisible]? @"Hide Help": @"Show Help", @"FAQ", @"Tutorial", @"Settings", @"Export", #ifdef ADHOC @"Feedback", #endif @@ -472,7 +477,7 @@ MPElementEntity *newElement = [NSEntityDescription insertNewObjectForEntityForName:ClassNameFromMPElementType(type) inManagedObjectContext:[MPAppDelegate managedObjectContext]]; newElement.name = self.activeElement.name; - newElement.mpHashHex = self.activeElement.mpHashHex; + newElement.keyID = self.activeElement.keyID; newElement.uses = self.activeElement.uses; newElement.lastUsed = self.activeElement.lastUsed; diff --git a/MasterPassword/iOS/MPSearchDelegate.m b/MasterPassword/iOS/MPSearchDelegate.m index 116bb958..c8952a5c 100644 --- a/MasterPassword/iOS/MPSearchDelegate.m +++ b/MasterPassword/iOS/MPSearchDelegate.m @@ -113,10 +113,10 @@ - (void)update { assert(self.query); - assert([MPAppDelegate get].keyHashHex); + assert([MPAppDelegate get].keyID); - self.fetchedResultsController.fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name BEGINSWITH[cd] %@) AND mpHashHex == %@", - self.query, self.query, [MPAppDelegate get].keyHashHex]; + self.fetchedResultsController.fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name BEGINSWITH[cd] %@) AND keyID == %@", + self.query, self.query, [MPAppDelegate get].keyID]; NSError *error; if (![self.fetchedResultsController performFetch:&error]) @@ -259,10 +259,10 @@ MPElementGeneratedEntity *element = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPElementGeneratedEntity class]) inManagedObjectContext:self.fetchedResultsController.managedObjectContext]; assert([element isKindOfClass:ClassFromMPElementType((unsigned)element.type)]); - assert([MPAppDelegate get].keyHashHex); + assert([MPAppDelegate get].keyID); element.name = siteName; - element.mpHashHex = [MPAppDelegate get].keyHashHex; + element.keyID = [MPAppDelegate get].keyID; dispatch_async(dispatch_get_main_queue(), ^{ [self.delegate didSelectElement:element]; diff --git a/MasterPassword/iOS/MPTypeViewController.m b/MasterPassword/iOS/MPTypeViewController.m index a70aae46..353ebd26 100644 --- a/MasterPassword/iOS/MPTypeViewController.m +++ b/MasterPassword/iOS/MPTypeViewController.m @@ -65,22 +65,22 @@ switch (indexPath.section) { case 0: { - // Calculated + // Generated switch (indexPath.row) { case 0: - return MPElementTypeCalculatedLong; + return MPElementTypeGeneratedLong; case 1: - return MPElementTypeCalculatedMedium; + return MPElementTypeGeneratedMedium; case 2: - return MPElementTypeCalculatedShort; + return MPElementTypeGeneratedShort; case 3: - return MPElementTypeCalculatedBasic; + return MPElementTypeGeneratedBasic; case 4: - return MPElementTypeCalculatedPIN; + return MPElementTypeGeneratedPIN; default: [NSException raise:NSInternalInconsistencyException - format:@"Unsupported row: %d, when selecting calculated element type.", indexPath.row]; + format:@"Unsupported row: %d, when selecting generated element type.", indexPath.row]; } break; } diff --git a/MasterPassword/iOS/MasterPassword-Info.plist b/MasterPassword/iOS/MasterPassword-Info.plist index d383d18e..3fbdada9 100644 --- a/MasterPassword/iOS/MasterPassword-Info.plist +++ b/MasterPassword/iOS/MasterPassword-Info.plist @@ -6,6 +6,25 @@ en CFBundleDisplayName Passwords + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + mpsites + + CFBundleTypeIconFiles + + CFBundleTypeName + Master Password sites + LSHandlerRank + Owner + LSItemContentTypes + + com.lyndir.lhunath.MasterPassword.sites + + + CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIconFiles @@ -85,5 +104,25 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UTExportedTypeDeclarations + + + UTTypeDescription + Master Password sites + UTTypeIdentifier + com.lyndir.lhunath.MasterPassword.sites + UTTypeSize320IconFile + + UTTypeSize64IconFile + + UTTypeTagSpecification + + public.filename-extension + + mpsites + + + + diff --git a/MasterPassword/iOS/Settings.bundle/Root.plist b/MasterPassword/iOS/Settings.bundle/Root.plist index 58434e0e..2c213121 100644 --- a/MasterPassword/iOS/Settings.bundle/Root.plist +++ b/MasterPassword/iOS/Settings.bundle/Root.plist @@ -54,7 +54,7 @@ DefaultValue Key - rememberKeyPhrase + rememberKey Title Remember my password Type @@ -72,9 +72,9 @@ DefaultValue Key - storeKeyPhrase + saveKey Title - Store my password + Save my password Type PSToggleSwitchSpecifier