2
0
MasterPassword/MasterPassword/MPAppDelegate_Store.m

420 lines
19 KiB
Mathematica
Raw Normal View History

//
// MPAppDelegate.m
// MasterPassword
//
// Created by Maarten Billemont on 24/11/11.
// Copyright (c) 2011 Lyndir. All rights reserved.
//
#import "MPAppDelegate_Store.h"
#import "LocalyticsSession.h"
@implementation MPAppDelegate_Shared (Store)
#pragma mark - Core Data setup
+ (NSManagedObjectContext *)managedObjectContext {
2012-06-08 21:46:13 +00:00
return [[self get] managedObjectContext];
}
+ (NSManagedObjectModel *)managedObjectModel {
2012-06-08 21:46:13 +00:00
return [[self get] managedObjectModel];
}
- (NSManagedObjectModel *)managedObjectModel {
2012-06-08 21:46:13 +00:00
static NSManagedObjectModel *managedObjectModel = nil;
if (managedObjectModel)
return managedObjectModel;
2012-06-08 21:46:13 +00:00
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"MasterPassword" withExtension:@"momd"];
return managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
}
- (NSManagedObjectContext *)managedObjectContext {
2012-06-08 21:46:13 +00:00
static NSManagedObjectContext *managedObjectContext = nil;
if (managedObjectContext)
return managedObjectContext;
2012-06-08 21:46:13 +00:00
managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[managedObjectContext performBlockAndWait:^{
managedObjectContext.persistentStoreCoordinator = [self persistentStoreCoordinator];
managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;
}];
return managedObjectContext;
}
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
2012-06-08 21:46:13 +00:00
// Start loading the store.
[self storeManager];
2012-06-08 21:46:13 +00:00
// Wait until the storeManager is ready.
while (![self storeManager].isReady)
[NSThread sleepForTimeInterval:0.1];
return [self storeManager].persistentStoreCoordinator;
}
- (UbiquityStoreManager *)storeManager {
2012-06-08 21:46:13 +00:00
static UbiquityStoreManager *storeManager = nil;
if (storeManager)
return storeManager;
2012-06-08 21:46:13 +00:00
storeManager = [[UbiquityStoreManager alloc] initWithManagedObjectModel:[self managedObjectModel]
2012-06-08 21:46:13 +00:00
localStoreURL:[[self applicationFilesDirectory] URLByAppendingPathComponent:@"MasterPassword.sqlite"]
containerIdentifier:@"HL3Q45LX9N.com.lyndir.lhunath.MasterPassword.shared"
#if TARGET_OS_IPHONE
2012-06-08 21:46:13 +00:00
additionalStoreOptions:[NSDictionary dictionaryWithObject:NSFileProtectionComplete
forKey:NSPersistentStoreFileProtectionKey]
#else
additionalStoreOptions:nil
#endif
2012-06-08 21:46:13 +00:00
];
storeManager.delegate = self;
#ifdef DEBUG
storeManager.hardResetEnabled = YES;
#endif
#if TARGET_OS_IPHONE
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillEnterForegroundNotification
object:[UIApplication sharedApplication] queue:nil
2012-06-08 21:46:13 +00:00
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
2012-06-08 21:46:13 +00:00
usingBlock:^(NSNotification *note) {
[self saveContext];
}];
#else
[[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillTerminateNotification
object:[NSApplication sharedApplication] queue:nil
usingBlock:^(NSNotification *note) {
[self saveContext];
}];
#endif
2012-06-08 21:46:13 +00:00
return storeManager;
}
- (void)saveContext {
2012-06-08 21:46:13 +00:00
[self.managedObjectContext performBlock:^{
NSError *error = nil;
if ([self.managedObjectContext hasChanges])
if (![self.managedObjectContext save:&error])
2012-06-08 21:46:13 +00:00
err(@"While saving context: %@", error);
}];
}
#pragma mark - UbiquityStoreManagerDelegate
- (NSManagedObjectContext *)managedObjectContextForUbiquityStoreManager:(UbiquityStoreManager *)usm {
2012-06-08 21:46:13 +00:00
return self.managedObjectContext;
}
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager log:(NSString *)message {
2012-06-08 21:46:13 +00:00
dbg(@"[StoreManager] %@", message);
}
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didSwitchToiCloud:(BOOL)iCloudEnabled {
2012-06-08 21:46:13 +00:00
// manager.iCloudEnabled is more reliable (eg. iOS' MPAppDelegate tampers with didSwitch a bit)
iCloudEnabled = manager.iCloudEnabled;
inf(@"Using iCloud? %@", iCloudEnabled? @"YES": @"NO");
2012-06-08 21:46:13 +00:00
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:iCloudEnabled? MPCheckpointCloudEnabled: MPCheckpointCloudDisabled];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointCloud
attributes:[NSDictionary dictionaryWithObject:iCloudEnabled? @"YES": @"NO" forKey:@"enabled"]];
2012-06-08 21:46:13 +00:00
[MPConfig get].iCloud = [NSNumber numberWithBool:iCloudEnabled];
}
2012-06-08 21:46:13 +00:00
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didEncounterError:(NSError *)error cause:(UbiquityStoreManagerErrorCause)cause
context:(id)context {
err(@"StoreManager: cause=%d, context=%@, error=%@", cause, context, error);
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:PearlString(@"MPCheckpointMPErrorUbiquity_%d", cause)];
#endif
2012-06-08 21:46:13 +00:00
switch (cause) {
case UbiquityStoreManagerErrorCauseDeleteStore:
case UbiquityStoreManagerErrorCauseDeleteLogs:
case UbiquityStoreManagerErrorCauseCreateStorePath:
case UbiquityStoreManagerErrorCauseClearStore:
break;
case UbiquityStoreManagerErrorCauseOpenLocalStore: {
wrn(@"Local store could not be opened, resetting it.");
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointLocalStoreIncompatible];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointLocalStoreIncompatible
attributes:nil];
manager.hardResetEnabled = YES;
[manager hardResetLocalStorage];
2012-06-08 21:46:13 +00:00
[NSException raise:NSGenericException format:@"Local store was reset, application must be restarted to use it."];
return;
}
case UbiquityStoreManagerErrorCauseOpenCloudStore: {
wrn(@"iCloud store could not be opened, resetting it.");
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointCloudStoreIncompatible];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointCloudStoreIncompatible
attributes:nil];
manager.hardResetEnabled = YES;
[manager hardResetCloudStorage];
break;
}
}
}
#pragma mark - Import / Export
- (MPImportResult)importSites:(NSString *)importedSitesString withPassword:(NSString *)password
askConfirmation:(BOOL(^)(NSUInteger importCount, NSUInteger deleteCount))confirmation {
2012-06-08 21:46:13 +00:00
inf(@"Importing sites.");
2012-06-08 21:46:13 +00:00
static NSRegularExpression *headerPattern, *sitePattern;
__autoreleasing NSError *error;
if (!headerPattern) {
headerPattern = [[NSRegularExpression alloc]
2012-06-08 21:46:13 +00:00
initWithPattern:@"^#[[:space:]]*([^:]+): (.*)"
options:0 error:&error];
if (error)
2012-06-08 21:46:13 +00:00
err(@"Error loading the header pattern: %@", error);
}
if (!sitePattern) {
sitePattern = [[NSRegularExpression alloc]
2012-06-08 21:46:13 +00:00
initWithPattern:@"^([^[:space:]]+)[[:space:]]+([[:digit:]]+)[[:space:]]+([[:digit:]]+)[[:space:]]+([^\t]+)\t(.*)"
options:0 error:&error];
if (error)
2012-06-08 21:46:13 +00:00
err(@"Error loading the site pattern: %@", error);
}
if (!headerPattern || !sitePattern)
return MPImportResultInternalError;
2012-06-08 21:46:13 +00:00
NSData *key = nil;
NSString *keyIDHex = nil, *userName = nil;
MPUserEntity *user = nil;
BOOL headerStarted = NO, headerEnded = NO, clearText = NO;
2012-06-08 21:46:13 +00:00
NSArray *importedSiteLines = [importedSitesString componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
NSMutableSet *elementsToDelete = [NSMutableSet set];
NSMutableArray *importedSiteElements = [NSMutableArray arrayWithCapacity:[importedSiteLines count]];
2012-06-08 21:46:13 +00:00
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;
}
2012-06-08 21:46:13 +00:00
// Header
if ([headerPattern numberOfMatchesInString:importedSiteLine options:0 range:NSMakeRange(0, [importedSiteLine length])] != 1) {
err(@"Invalid header format in line: %@", importedSiteLine);
return MPImportResultMalformedInput;
}
2012-06-08 21:46:13 +00:00
NSTextCheckingResult *headerElements = [[headerPattern matchesInString:importedSiteLine options:0
range:NSMakeRange(0, [importedSiteLine length])] lastObject];
NSString *headerName = [importedSiteLine substringWithRange:[headerElements rangeAtIndex:1]];
NSString *headerValue = [importedSiteLine substringWithRange:[headerElements rangeAtIndex:2]];
if ([headerName isEqualToString:@"User Name"]) {
userName = headerValue;
key = keyForPassword(password, userName);
2012-06-08 21:46:13 +00:00
NSFetchRequest *userFetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPUserEntity class])];
userFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@", userName];
user = [[self.managedObjectContext executeFetchRequest:fetchRequest error:&error] lastObject];
}
if ([headerName isEqualToString:@"Key ID"]) {
if (![(keyIDHex = headerValue) isEqualToString:[keyIDForKey(key) encodeHex]])
return MPImportResultInvalidPassword;
}
if ([headerName isEqualToString:@"Passwords"]) {
if ([headerValue isEqualToString:@"VISIBLE"])
clearText = YES;
}
2012-06-08 21:46:13 +00:00
continue;
}
if (!headerEnded)
continue;
if (!keyIDHex || ![userName length])
return MPImportResultMalformedInput;
if (![importedSiteLine length])
continue;
2012-06-08 21:46:13 +00:00
// Site
if ([sitePattern numberOfMatchesInString:importedSiteLine options:0 range:NSMakeRange(0, [importedSiteLine length])] != 1) {
err(@"Invalid site format in line: %@", importedSiteLine);
return MPImportResultMalformedInput;
}
2012-06-08 21:46:13 +00:00
NSTextCheckingResult *siteElements = [[sitePattern matchesInString:importedSiteLine options:0
range:NSMakeRange(0, [importedSiteLine length])] lastObject];
NSString *lastUsed = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:1]];
NSString *uses = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:2]];
NSString *type = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:3]];
NSString *name = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:4]];
NSString *exportContent = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:5]];
// Find existing site.
if (user) {
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@ AND user == %@", name, user];
NSArray *existingSites = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
if (error)
2012-06-08 21:46:13 +00:00
err(@"Couldn't search existing sites: %@", error);
if (!existingSites) {
err(@"Lookup of existing sites failed for site: %@, user: %@", name, user.userID);
return MPImportResultInternalError;
}
2012-06-08 21:46:13 +00:00
[elementsToDelete addObjectsFromArray:existingSites];
[importedSiteElements addObject:[NSArray arrayWithObjects:lastUsed, uses, type, name, exportContent, nil]];
}
}
2012-06-08 21:46:13 +00:00
inf(@"Importing %u sites, deleting %u sites, for user: %@",
[importedSiteElements count], [elementsToDelete count], [MPUserEntity idFor:userName]);
// Ask for confirmation to import these sites.
if (!confirmation([importedSiteElements count], [elementsToDelete count])) {
inf(@"Import cancelled.");
return MPImportResultCancelled;
}
2012-06-08 21:46:13 +00:00
// Delete existing sites.
[elementsToDelete enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {
inf(@"Deleting site: %@, it will be replaced by an imported site.", [obj name]);
[self.managedObjectContext deleteObject:obj];
}];
[self saveContext];
2012-06-08 21:46:13 +00:00
// Import new sites.
if (!user) {
2012-06-08 21:46:13 +00:00
user = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPUserEntity class])
inManagedObjectContext:self.managedObjectContext];
user.name = userName;
user.keyID = [keyIDHex decodeHex];
}
for (NSArray *siteElements in importedSiteElements) {
NSDate *lastUsed = [[NSDateFormatter rfc3339DateFormatter] dateFromString:[siteElements objectAtIndex:0]];
2012-06-08 21:46:13 +00:00
NSUInteger uses = (unsigned)[[siteElements objectAtIndex:1] integerValue];
MPElementType type = (MPElementType)[[siteElements objectAtIndex:2] integerValue];
NSString *name = [siteElements objectAtIndex:3];
NSString *exportContent = [siteElements objectAtIndex:4];
2012-06-08 21:46:13 +00:00
// Create new site.
MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:ClassNameFromMPElementType(type)
inManagedObjectContext:self.managedObjectContext];
2012-06-08 21:46:13 +00:00
element.name = name;
element.user = user;
element.type = type;
element.uses = uses;
element.lastUsed = lastUsed;
if ([exportContent length])
if (clearText)
[element importClearTextContent:exportContent usingKey:key];
else
[element importProtectedContent:exportContent];
}
[self saveContext];
2012-06-08 21:46:13 +00:00
inf(@"Import completed successfully.");
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointSitesImported];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointSitesImported
attributes:nil];
2012-06-08 21:46:13 +00:00
return MPImportResultSuccess;
}
- (NSString *)exportSitesShowingPasswords:(BOOL)showPasswords {
2012-06-08 21:46:13 +00:00
inf(@"Exporting sites, %@, for: %@", showPasswords? @"showing passwords": @"omitting passwords", self.activeUser.userID);
2012-06-08 21:46:13 +00:00
// 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:@"# User Name: %@\n", self.activeUser.name];
[export appendFormat:@"# Key ID: %@\n", [self.activeUser.keyID encodeHex]];
[export appendFormat:@"# Date: %@\n", [[NSDateFormatter 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"];
2012-06-08 21:46:13 +00:00
// Sites.
for (MPElementEntity *element in self.activeUser.elements) {
2012-06-08 21:46:13 +00:00
NSDate *lastUsed = element.lastUsed;
NSUInteger uses = element.uses;
MPElementType type = element.type;
NSString *name = element.name;
NSString *content = nil;
// Determine the content to export.
if (!(type & MPElementFeatureDevicePrivate)) {
if (showPasswords)
content = element.content;
2012-06-08 21:46:13 +00:00
else
if (type & MPElementFeatureExportContent)
content = element.exportContent;
}
2012-06-08 21:46:13 +00:00
[export appendFormat:@"%@ %8d %8d %20s\t%@\n",
[[NSDateFormatter rfc3339DateFormatter] stringFromDate:lastUsed], uses, type, [name cStringUsingEncoding:NSUTF8StringEncoding], content
2012-06-08 21:46:13 +00:00
? content: @""];
}
2012-06-08 21:46:13 +00:00
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointSitesExported];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointSitesExported attributes:nil];
2012-06-08 21:46:13 +00:00
return export;
}
@end