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 */,
|
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>";
|
||||||
|
@ -21,4 +21,6 @@
|
|||||||
- (void)saveContext;
|
- (void)saveContext;
|
||||||
- (void)printStore;
|
- (void)printStore;
|
||||||
|
|
||||||
|
- (NSString *)exportSitesShowingPasswords:(BOOL)showPasswords;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -163,5 +163,64 @@
|
|||||||
trc(@"Not printing sites: master password not set.");
|
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
|
@end
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
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,
|
MPElementTypeStoredPersonal = 0x0 | MPElementTypeClassStored | MPElementFeatureExportContent | MPElementFeatureDevicePrivate,
|
||||||
MPElementTypeStoredDevicePrivate = MPElementTypeClassStored | 0x02,
|
MPElementTypeStoredDevicePrivate = 0x1 | MPElementTypeClassStored | 0x0,
|
||||||
} MPElementType;
|
} MPElementType;
|
||||||
|
|
||||||
#define MPTestFlightCheckpointAction @"MPTestFlightCheckpointAction"
|
#define MPTestFlightCheckpointAction @"MPTestFlightCheckpointAction"
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user