diff --git a/MasterPassword-iOS.xcodeproj/project.pbxproj b/MasterPassword-iOS.xcodeproj/project.pbxproj index cd984af5..f641b97b 100644 --- a/MasterPassword-iOS.xcodeproj/project.pbxproj +++ b/MasterPassword-iOS.xcodeproj/project.pbxproj @@ -1854,6 +1854,7 @@ DA4426071557C1990052177D /* MPAppDelegate_Store.m */, DA600C2615056427008E9AB6 /* MPConfig.h */, DA600C2715056427008E9AB6 /* MPConfig.m */, + DAB8D45C15036BCF00CED3BC /* MPElementStoredEntity.h */, DAB8D45515036BCF00CED3BC /* MPElementStoredEntity.m */, DAB8D45915036BCF00CED3BC /* MPTypes.h */, DAB8D45615036BCF00CED3BC /* MPTypes.m */, @@ -1861,7 +1862,6 @@ DAB8D45815036BCF00CED3BC /* MPElementEntity.m */, DAB8D45A15036BCF00CED3BC /* MPElementGeneratedEntity.h */, DAB8D45B15036BCF00CED3BC /* MPElementGeneratedEntity.m */, - DAB8D45C15036BCF00CED3BC /* MPElementStoredEntity.h */, ); path = MasterPassword; sourceTree = ""; diff --git a/MasterPassword/MPAppDelegate_Store.h b/MasterPassword/MPAppDelegate_Store.h index e3bf777b..61003730 100644 --- a/MasterPassword/MPAppDelegate_Store.h +++ b/MasterPassword/MPAppDelegate_Store.h @@ -21,4 +21,6 @@ - (void)saveContext; - (void)printStore; +- (NSString *)exportSitesShowingPasswords:(BOOL)showPasswords; + @end diff --git a/MasterPassword/MPAppDelegate_Store.m b/MasterPassword/MPAppDelegate_Store.m index aceb8772..ff353c9e 100644 --- a/MasterPassword/MPAppDelegate_Store.m +++ b/MasterPassword/MPAppDelegate_Store.m @@ -163,5 +163,64 @@ 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; + } @end diff --git a/MasterPassword/MPElementEntity.h b/MasterPassword/MPElementEntity.h index 9ed3ef21..ffd46fee 100644 --- a/MasterPassword/MPElementEntity.h +++ b/MasterPassword/MPElementEntity.h @@ -17,7 +17,9 @@ @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; diff --git a/MasterPassword/MPElementEntity.m b/MasterPassword/MPElementEntity.m index e5f9a4e6..e2b93eab 100644 --- a/MasterPassword/MPElementEntity.m +++ b/MasterPassword/MPElementEntity.m @@ -28,6 +28,11 @@ @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Content implementation missing." userInfo:nil]; } +- (NSString *)exportContent { + + return nil; +} + - (NSString *)description { return str(@"%@:%@", [self class], [self name]); @@ -35,8 +40,8 @@ - (NSString *)debugDescription { - return [NSString stringWithFormat:@"{%@: name=%@, mpHashHex=%@, type=%d, uses=%d, lastUsed=%@}", - NSStringFromClass([self class]), self.name, self.mpHashHex, self.type, self.uses, self.lastUsed]; + return str(@"{%@: name=%@, mpHashHex=%@, type=%d, uses=%d, lastUsed=%@}", + NSStringFromClass([self class]), self.name, self.mpHashHex, self.type, self.uses, self.lastUsed); } @end diff --git a/MasterPassword/MPElementStoredEntity.m b/MasterPassword/MPElementStoredEntity.m index cb68cee5..82cfee91 100644 --- a/MasterPassword/MPElementStoredEntity.m +++ b/MasterPassword/MPElementStoredEntity.m @@ -34,7 +34,7 @@ assert(self.type & MPElementTypeClassStored); NSData *encryptedContent; - if (self.type == MPElementTypeStoredDevicePrivate) + if (self.type & MPElementFeatureDevicePrivate) encryptedContent = [PearlKeyChain dataOfItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]]; else encryptedContent = self.contentObject; @@ -49,7 +49,7 @@ NSData *encryptedContent = [[content description] encryptWithSymmetricKey:[[MPAppDelegate get] keyWithLength:PearlCryptKeySize] padding:YES]; - if (self.type == MPElementTypeStoredDevicePrivate) { + if (self.type & MPElementFeatureDevicePrivate) { [PearlKeyChain addOrUpdateItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name] withAttributes:[NSDictionary dictionaryWithObjectsAndKeys: encryptedContent, (__bridge id)kSecValueData, @@ -62,4 +62,9 @@ self.contentObject = encryptedContent; } +- (NSString *)exportContent { + + return [self.contentObject encodeBase64]; +} + @end diff --git a/MasterPassword/MPTypes.h b/MasterPassword/MPTypes.h index d13a12f8..4108122c 100644 --- a/MasterPassword/MPTypes.h +++ b/MasterPassword/MPTypes.h @@ -17,19 +17,28 @@ typedef enum { } MPElementContentType; typedef enum { - MPElementTypeClassCalculated = 2 << 7, - MPElementTypeClassStored = 2 << 8, + /** Generate the password. */ + MPElementTypeClassGenerated = 1 << 4, + /** Store the password. */ + MPElementTypeClassStored = 1 << 5, } MPElementTypeClass; typedef enum { - MPElementTypeCalculatedLong = MPElementTypeClassCalculated | 0x01, - MPElementTypeCalculatedMedium = MPElementTypeClassCalculated | 0x02, - MPElementTypeCalculatedShort = MPElementTypeClassCalculated | 0x03, - MPElementTypeCalculatedBasic = MPElementTypeClassCalculated | 0x04, - MPElementTypeCalculatedPIN = MPElementTypeClassCalculated | 0x05, + /** Export the key-protected content data. */ + MPElementFeatureExportContent = 1 << 10, + /** Never export content. */ + MPElementFeatureDevicePrivate = 1 << 11, +} MPElementFeature; + +typedef enum { + MPElementTypeGeneratedLong = 0x0 | MPElementTypeClassGenerated | 0x0, + MPElementTypeGeneratedMedium = 0x1 | MPElementTypeClassGenerated | 0x0, + MPElementTypeGeneratedShort = 0x2 | MPElementTypeClassGenerated | 0x0, + MPElementTypeGeneratedBasic = 0x3 | MPElementTypeClassGenerated | 0x0, + MPElementTypeGeneratedPIN = 0x4 | MPElementTypeClassGenerated | 0x0, - MPElementTypeStoredPersonal = MPElementTypeClassStored | 0x01, - MPElementTypeStoredDevicePrivate = MPElementTypeClassStored | 0x02, + MPElementTypeStoredPersonal = 0x0 | MPElementTypeClassStored | MPElementFeatureExportContent | MPElementFeatureDevicePrivate, + MPElementTypeStoredDevicePrivate = 0x1 | MPElementTypeClassStored | 0x0, } MPElementType; #define MPTestFlightCheckpointAction @"MPTestFlightCheckpointAction" diff --git a/MasterPassword/iOS/MPAppDelegate.h b/MasterPassword/iOS/MPAppDelegate.h index e0b22571..d2eb4746 100644 --- a/MasterPassword/iOS/MPAppDelegate.h +++ b/MasterPassword/iOS/MPAppDelegate.h @@ -7,10 +7,13 @@ // #import +#import -@interface MPAppDelegate : PearlAppDelegate +@interface MPAppDelegate : PearlAppDelegate - (void)showGuide; - (void)loadKey:(BOOL)animated; +- (void)export; + @end diff --git a/MasterPassword/iOS/MPAppDelegate.m b/MasterPassword/iOS/MPAppDelegate.m index 1dbb43d0..d4a06267 100644 --- a/MasterPassword/iOS/MPAppDelegate.m +++ b/MasterPassword/iOS/MPAppDelegate.m @@ -220,6 +220,48 @@ }); } +- (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]; @@ -258,6 +300,26 @@ [TestFlight passCheckpoint:MPTestFlightCheckpointDeactivated]; } +#pragma mark - MFMailComposeViewControllerDelegate + +- (void)mailComposeController:(MFMailComposeViewController *)controller + didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error { + + if (error) + err(@"Error composing mail message: %@", error); + + switch (result) { + case MFMailComposeResultSaved: + case MFMailComposeResultSent: + break; + + case MFMailComposeResultFailed: + break; + case MFMailComposeResultCancelled: + break; + } +} + #pragma mark - TestFlight diff --git a/MasterPassword/iOS/MPMainViewController.m b/MasterPassword/iOS/MPMainViewController.m index e6a1ee53..05fcb929 100644 --- a/MasterPassword/iOS/MPMainViewController.m +++ b/MasterPassword/iOS/MPMainViewController.m @@ -398,34 +398,37 @@ case 2: [[MPAppDelegate get] showGuide]; break; - case 3: { + case 3: + { IASKAppSettingsViewController *settingsVC = [IASKAppSettingsViewController new]; settingsVC.delegate = self; [self.navigationController pushViewController:settingsVC animated:YES]; break; } -#ifdef ADHOC case 4: + [[MPAppDelegate get] export]; + break; +#ifdef ADHOC + case 5: [TestFlight openFeedbackView]; break; - case 5: + case 6: #else - case 4: + case 5: #endif #ifdef DEBUG - { [[MPAppDelegate get].storeManager hardResetCloudStorage]; - } + break; #ifdef ADHOC - case 6: { - [[MPAppDelegate get].storeManager useiCloudStore:![MPAppDelegate get].storeManager.iCloudEnabled]; - } case 7: -#else - case 5: { [[MPAppDelegate get].storeManager useiCloudStore:![MPAppDelegate get].storeManager.iCloudEnabled]; - } + break; + case 8: +#else case 6: + [[MPAppDelegate get].storeManager useiCloudStore:![MPAppDelegate get].storeManager.iCloudEnabled]; + break; + case 7: #endif #endif { @@ -438,7 +441,7 @@ [TestFlight passCheckpoint:MPTestFlightCheckpointAction]; } cancelTitle:[PearlStrings get].commonButtonCancel destructiveTitle:nil otherTitles: - [self isHelpVisible]? @"Hide Help": @"Show Help", @"FAQ", @"Tutorial", @"Settings", + [self isHelpVisible]? @"Hide Help": @"Show Help", @"FAQ", @"Tutorial", @"Settings", @"Export", @"Import", #ifdef ADHOC @"Feedback", #endif