Exporting of sites.
[ADDED] Ability to export site data. [UPDATED] Type of passwords can now include feature bits.
This commit is contained in:
parent
7c5cea9c8d
commit
04bc7a497c
@ -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 = "<group>";
|
||||
|
@ -21,4 +21,6 @@
|
||||
- (void)saveContext;
|
||||
- (void)printStore;
|
||||
|
||||
- (NSString *)exportSitesShowingPasswords:(BOOL)showPasswords;
|
||||
|
||||
@end
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
MPElementTypeStoredPersonal = MPElementTypeClassStored | 0x01,
|
||||
MPElementTypeStoredDevicePrivate = MPElementTypeClassStored | 0x02,
|
||||
typedef enum {
|
||||
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;
|
||||
|
||||
#define MPTestFlightCheckpointAction @"MPTestFlightCheckpointAction"
|
||||
|
@ -7,10 +7,13 @@
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <MessageUI/MessageUI.h>
|
||||
|
||||
@interface MPAppDelegate : PearlAppDelegate
|
||||
@interface MPAppDelegate : PearlAppDelegate <MFMailComposeViewControllerDelegate>
|
||||
|
||||
- (void)showGuide;
|
||||
- (void)loadKey:(BOOL)animated;
|
||||
|
||||
- (void)export;
|
||||
|
||||
@end
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user