2
0

Exporting of sites.

[ADDED]     Ability to export site data.
[UPDATED]   Type of passwords can now include feature bits.
This commit is contained in:
Maarten Billemont 2012-05-08 15:57:11 +02:00
parent 7c5cea9c8d
commit 04bc7a497c
10 changed files with 178 additions and 28 deletions

View File

@ -1854,6 +1854,7 @@
DA4426071557C1990052177D /* MPAppDelegate_Store.m */, DA4426071557C1990052177D /* MPAppDelegate_Store.m */,
DA600C2615056427008E9AB6 /* MPConfig.h */, DA600C2615056427008E9AB6 /* MPConfig.h */,
DA600C2715056427008E9AB6 /* MPConfig.m */, DA600C2715056427008E9AB6 /* MPConfig.m */,
DAB8D45C15036BCF00CED3BC /* MPElementStoredEntity.h */,
DAB8D45515036BCF00CED3BC /* MPElementStoredEntity.m */, DAB8D45515036BCF00CED3BC /* MPElementStoredEntity.m */,
DAB8D45915036BCF00CED3BC /* MPTypes.h */, DAB8D45915036BCF00CED3BC /* MPTypes.h */,
DAB8D45615036BCF00CED3BC /* MPTypes.m */, DAB8D45615036BCF00CED3BC /* MPTypes.m */,
@ -1861,7 +1862,6 @@
DAB8D45815036BCF00CED3BC /* MPElementEntity.m */, DAB8D45815036BCF00CED3BC /* MPElementEntity.m */,
DAB8D45A15036BCF00CED3BC /* MPElementGeneratedEntity.h */, DAB8D45A15036BCF00CED3BC /* MPElementGeneratedEntity.h */,
DAB8D45B15036BCF00CED3BC /* MPElementGeneratedEntity.m */, DAB8D45B15036BCF00CED3BC /* MPElementGeneratedEntity.m */,
DAB8D45C15036BCF00CED3BC /* MPElementStoredEntity.h */,
); );
path = MasterPassword; path = MasterPassword;
sourceTree = "<group>"; sourceTree = "<group>";

View File

@ -21,4 +21,6 @@
- (void)saveContext; - (void)saveContext;
- (void)printStore; - (void)printStore;
- (NSString *)exportSitesShowingPasswords:(BOOL)showPasswords;
@end @end

View File

@ -164,4 +164,63 @@
}]; }];
} }
- (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 @end

View File

@ -17,7 +17,9 @@
@property (nonatomic, assign) int16_t type; @property (nonatomic, assign) int16_t type;
@property (nonatomic, assign) int16_t uses; @property (nonatomic, assign) int16_t uses;
@property (nonatomic, assign) NSTimeInterval lastUsed; @property (nonatomic, assign) NSTimeInterval lastUsed;
@property (nonatomic, retain, readonly) id content; @property (nonatomic, retain, readonly) id content;
@property (nonatomic, retain, readonly) NSString *exportContent;
- (int16_t)use; - (int16_t)use;

View File

@ -28,6 +28,11 @@
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Content implementation missing." userInfo:nil]; @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Content implementation missing." userInfo:nil];
} }
- (NSString *)exportContent {
return nil;
}
- (NSString *)description { - (NSString *)description {
return str(@"%@:%@", [self class], [self name]); return str(@"%@:%@", [self class], [self name]);
@ -35,8 +40,8 @@
- (NSString *)debugDescription { - (NSString *)debugDescription {
return [NSString stringWithFormat:@"{%@: name=%@, mpHashHex=%@, type=%d, uses=%d, lastUsed=%@}", return str(@"{%@: name=%@, mpHashHex=%@, type=%d, uses=%d, lastUsed=%@}",
NSStringFromClass([self class]), self.name, self.mpHashHex, self.type, self.uses, self.lastUsed]; NSStringFromClass([self class]), self.name, self.mpHashHex, self.type, self.uses, self.lastUsed);
} }
@end @end

View File

@ -34,7 +34,7 @@
assert(self.type & MPElementTypeClassStored); assert(self.type & MPElementTypeClassStored);
NSData *encryptedContent; NSData *encryptedContent;
if (self.type == MPElementTypeStoredDevicePrivate) if (self.type & MPElementFeatureDevicePrivate)
encryptedContent = [PearlKeyChain dataOfItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]]; encryptedContent = [PearlKeyChain dataOfItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]];
else else
encryptedContent = self.contentObject; encryptedContent = self.contentObject;
@ -49,7 +49,7 @@
NSData *encryptedContent = [[content description] encryptWithSymmetricKey:[[MPAppDelegate get] keyWithLength:PearlCryptKeySize] NSData *encryptedContent = [[content description] encryptWithSymmetricKey:[[MPAppDelegate get] keyWithLength:PearlCryptKeySize]
padding:YES]; padding:YES];
if (self.type == MPElementTypeStoredDevicePrivate) { if (self.type & MPElementFeatureDevicePrivate) {
[PearlKeyChain addOrUpdateItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name] [PearlKeyChain addOrUpdateItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys: withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
encryptedContent, (__bridge id)kSecValueData, encryptedContent, (__bridge id)kSecValueData,
@ -62,4 +62,9 @@
self.contentObject = encryptedContent; self.contentObject = encryptedContent;
} }
- (NSString *)exportContent {
return [self.contentObject encodeBase64];
}
@end @end

View File

@ -17,19 +17,28 @@ typedef enum {
} MPElementContentType; } MPElementContentType;
typedef enum { typedef enum {
MPElementTypeClassCalculated = 2 << 7, /** Generate the password. */
MPElementTypeClassStored = 2 << 8, MPElementTypeClassGenerated = 1 << 4,
/** Store the password. */
MPElementTypeClassStored = 1 << 5,
} MPElementTypeClass; } MPElementTypeClass;
typedef enum { typedef enum {
MPElementTypeCalculatedLong = MPElementTypeClassCalculated | 0x01, /** Export the key-protected content data. */
MPElementTypeCalculatedMedium = MPElementTypeClassCalculated | 0x02, MPElementFeatureExportContent = 1 << 10,
MPElementTypeCalculatedShort = MPElementTypeClassCalculated | 0x03, /** Never export content. */
MPElementTypeCalculatedBasic = MPElementTypeClassCalculated | 0x04, MPElementFeatureDevicePrivate = 1 << 11,
MPElementTypeCalculatedPIN = MPElementTypeClassCalculated | 0x05, } MPElementFeature;
MPElementTypeStoredPersonal = MPElementTypeClassStored | 0x01, typedef enum {
MPElementTypeStoredDevicePrivate = MPElementTypeClassStored | 0x02, MPElementTypeGeneratedLong = 0x0 | MPElementTypeClassGenerated | 0x0,
MPElementTypeGeneratedMedium = 0x1 | MPElementTypeClassGenerated | 0x0,
MPElementTypeGeneratedShort = 0x2 | MPElementTypeClassGenerated | 0x0,
MPElementTypeGeneratedBasic = 0x3 | MPElementTypeClassGenerated | 0x0,
MPElementTypeGeneratedPIN = 0x4 | MPElementTypeClassGenerated | 0x0,
MPElementTypeStoredPersonal = 0x0 | MPElementTypeClassStored | MPElementFeatureExportContent | MPElementFeatureDevicePrivate,
MPElementTypeStoredDevicePrivate = 0x1 | MPElementTypeClassStored | 0x0,
} MPElementType; } MPElementType;
#define MPTestFlightCheckpointAction @"MPTestFlightCheckpointAction" #define MPTestFlightCheckpointAction @"MPTestFlightCheckpointAction"

View File

@ -7,10 +7,13 @@
// //
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#import <MessageUI/MessageUI.h>
@interface MPAppDelegate : PearlAppDelegate @interface MPAppDelegate : PearlAppDelegate <MFMailComposeViewControllerDelegate>
- (void)showGuide; - (void)showGuide;
- (void)loadKey:(BOOL)animated; - (void)loadKey:(BOOL)animated;
- (void)export;
@end @end

View File

@ -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 { - (void)applicationDidEnterBackground:(UIApplication *)application {
[[LocalyticsSession sharedLocalyticsSession] close]; [[LocalyticsSession sharedLocalyticsSession] close];
@ -258,6 +300,26 @@
[TestFlight passCheckpoint:MPTestFlightCheckpointDeactivated]; [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 #pragma mark - TestFlight

View File

@ -398,34 +398,37 @@
case 2: case 2:
[[MPAppDelegate get] showGuide]; [[MPAppDelegate get] showGuide];
break; break;
case 3: { case 3:
{
IASKAppSettingsViewController *settingsVC = [IASKAppSettingsViewController new]; IASKAppSettingsViewController *settingsVC = [IASKAppSettingsViewController new];
settingsVC.delegate = self; settingsVC.delegate = self;
[self.navigationController pushViewController:settingsVC animated:YES]; [self.navigationController pushViewController:settingsVC animated:YES];
break; break;
} }
#ifdef ADHOC
case 4: case 4:
[[MPAppDelegate get] export];
break;
#ifdef ADHOC
case 5:
[TestFlight openFeedbackView]; [TestFlight openFeedbackView];
break; break;
case 5: case 6:
#else #else
case 4: case 5:
#endif #endif
#ifdef DEBUG #ifdef DEBUG
{
[[MPAppDelegate get].storeManager hardResetCloudStorage]; [[MPAppDelegate get].storeManager hardResetCloudStorage];
} break;
#ifdef ADHOC #ifdef ADHOC
case 6: {
[[MPAppDelegate get].storeManager useiCloudStore:![MPAppDelegate get].storeManager.iCloudEnabled];
}
case 7: case 7:
#else
case 5: {
[[MPAppDelegate get].storeManager useiCloudStore:![MPAppDelegate get].storeManager.iCloudEnabled]; [[MPAppDelegate get].storeManager useiCloudStore:![MPAppDelegate get].storeManager.iCloudEnabled];
} break;
case 8:
#else
case 6: case 6:
[[MPAppDelegate get].storeManager useiCloudStore:![MPAppDelegate get].storeManager.iCloudEnabled];
break;
case 7:
#endif #endif
#endif #endif
{ {
@ -438,7 +441,7 @@
[TestFlight passCheckpoint:MPTestFlightCheckpointAction]; [TestFlight passCheckpoint:MPTestFlightCheckpointAction];
} cancelTitle:[PearlStrings get].commonButtonCancel destructiveTitle:nil } cancelTitle:[PearlStrings get].commonButtonCancel destructiveTitle:nil
otherTitles: otherTitles:
[self isHelpVisible]? @"Hide Help": @"Show Help", @"FAQ", @"Tutorial", @"Settings", [self isHelpVisible]? @"Hide Help": @"Show Help", @"FAQ", @"Tutorial", @"Settings", @"Export", @"Import",
#ifdef ADHOC #ifdef ADHOC
@"Feedback", @"Feedback",
#endif #endif