754 lines
33 KiB
Objective-C
754 lines
33 KiB
Objective-C
//==============================================================================
|
|
// This file is part of Master Password.
|
|
// Copyright (c) 2011-2017, Maarten Billemont.
|
|
//
|
|
// Master Password is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Master Password is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You can find a copy of the GNU General Public License in the
|
|
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
|
|
//==============================================================================
|
|
|
|
#import "MPAppDelegate_Store.h"
|
|
#import "mpw-marshall.h"
|
|
#import "mpw-util.h"
|
|
|
|
#if TARGET_OS_IPHONE
|
|
#define STORE_OPTIONS NSPersistentStoreFileProtectionKey : NSFileProtectionComplete,
|
|
#else
|
|
#define STORE_OPTIONS
|
|
#endif
|
|
|
|
#define MPMigrationLevelLocalStoreKey @"MPMigrationLevelLocalStoreKey"
|
|
|
|
typedef NS_ENUM( NSInteger, MPStoreMigrationLevel ) {
|
|
MPStoreMigrationLevelV1,
|
|
MPStoreMigrationLevelV2,
|
|
MPStoreMigrationLevelV3,
|
|
MPStoreMigrationLevelCurrent = MPStoreMigrationLevelV3,
|
|
};
|
|
|
|
@implementation MPAppDelegate_Shared(Store)
|
|
|
|
PearlAssociatedObjectProperty( NSOperationQueue *, StoreQueue, storeQueue );
|
|
|
|
PearlAssociatedObjectProperty( NSManagedObjectContext*, PrivateManagedObjectContext, privateManagedObjectContext );
|
|
|
|
PearlAssociatedObjectProperty( NSManagedObjectContext*, MainManagedObjectContext, mainManagedObjectContext );
|
|
|
|
PearlAssociatedObjectProperty( NSNumber*, StoreCorrupted, storeCorrupted );
|
|
|
|
#pragma mark - Core Data setup
|
|
|
|
+ (NSManagedObjectContext *)managedObjectContextForMainThreadIfReady {
|
|
|
|
NSAssert( [[NSThread currentThread] isMainThread], @"Can only access main MOC from the main thread." );
|
|
NSManagedObjectContext *mainManagedObjectContext = [[self get] mainManagedObjectContextIfReady];
|
|
if (!mainManagedObjectContext || ![[NSThread currentThread] isMainThread])
|
|
return nil;
|
|
|
|
return mainManagedObjectContext;
|
|
}
|
|
|
|
+ (BOOL)managedObjectContextForMainThreadPerformBlock:(void ( ^ )(NSManagedObjectContext *mainContext))mocBlock {
|
|
|
|
NSManagedObjectContext *mainManagedObjectContext = [[self get] mainManagedObjectContextIfReady];
|
|
if (!mainManagedObjectContext)
|
|
return NO;
|
|
|
|
[mainManagedObjectContext performBlock:^{
|
|
@try {
|
|
mocBlock( mainManagedObjectContext );
|
|
}
|
|
@catch (id exception) {
|
|
err( @"While performing managed block:\n%@", [exception fullDescription] );
|
|
}
|
|
}];
|
|
|
|
return YES;
|
|
}
|
|
|
|
+ (BOOL)managedObjectContextForMainThreadPerformBlockAndWait:(void ( ^ )(NSManagedObjectContext *mainContext))mocBlock {
|
|
|
|
NSManagedObjectContext *mainManagedObjectContext = [[self get] mainManagedObjectContextIfReady];
|
|
if (!mainManagedObjectContext)
|
|
return NO;
|
|
|
|
[mainManagedObjectContext performBlockAndWait:^{
|
|
@try {
|
|
mocBlock( mainManagedObjectContext );
|
|
}
|
|
@catch (NSException *exception) {
|
|
err( @"While performing managed block:\n%@", [exception fullDescription] );
|
|
}
|
|
}];
|
|
|
|
return YES;
|
|
}
|
|
|
|
+ (BOOL)managedObjectContextPerformBlock:(void ( ^ )(NSManagedObjectContext *context))mocBlock {
|
|
|
|
NSManagedObjectContext *privateManagedObjectContextIfReady = [[self get] privateManagedObjectContextIfReady];
|
|
if (!privateManagedObjectContextIfReady)
|
|
return NO;
|
|
|
|
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
|
|
moc.parentContext = privateManagedObjectContextIfReady;
|
|
[moc performBlock:^{
|
|
@try {
|
|
mocBlock( moc );
|
|
}
|
|
@catch (NSException *exception) {
|
|
err( @"While performing managed block:\n%@", [exception fullDescription] );
|
|
}
|
|
}];
|
|
|
|
return YES;
|
|
}
|
|
|
|
+ (BOOL)managedObjectContextPerformBlockAndWait:(void ( ^ )(NSManagedObjectContext *context))mocBlock {
|
|
|
|
NSManagedObjectContext *privateManagedObjectContextIfReady = [[self get] privateManagedObjectContextIfReady];
|
|
if (!privateManagedObjectContextIfReady)
|
|
return NO;
|
|
|
|
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
|
|
moc.parentContext = privateManagedObjectContextIfReady;
|
|
[moc performBlockAndWait:^{
|
|
@try {
|
|
mocBlock( moc );
|
|
}
|
|
@catch (NSException *exception) {
|
|
err( @"While performing managed block:\n%@", [exception fullDescription] );
|
|
}
|
|
}];
|
|
|
|
return YES;
|
|
}
|
|
|
|
+ (id)managedObjectContextChanged:(void ( ^ )(NSDictionary<NSManagedObjectID *, NSString *> *affectedObjects))changedBlock {
|
|
|
|
NSManagedObjectContext *privateManagedObjectContextIfReady = [[self get] privateManagedObjectContextIfReady];
|
|
if (!privateManagedObjectContextIfReady)
|
|
return nil;
|
|
|
|
return PearlAddNotificationObserver( NSManagedObjectContextObjectsDidChangeNotification, privateManagedObjectContextIfReady, nil,
|
|
^(id host, NSNotification *note) {
|
|
NSMutableDictionary *affectedObjects = [NSMutableDictionary new];
|
|
for (NSManagedObject *object in note.userInfo[NSInsertedObjectsKey])
|
|
affectedObjects[object.objectID] = NSInsertedObjectsKey;
|
|
for (NSManagedObject *object in note.userInfo[NSUpdatedObjectsKey])
|
|
affectedObjects[object.objectID] = NSUpdatedObjectsKey;
|
|
for (NSManagedObject *object in note.userInfo[NSDeletedObjectsKey])
|
|
affectedObjects[object.objectID] = NSDeletedObjectsKey;
|
|
changedBlock( affectedObjects );
|
|
} );
|
|
}
|
|
|
|
- (NSManagedObjectContext *)mainManagedObjectContextIfReady {
|
|
|
|
[self loadStore];
|
|
return self.mainManagedObjectContext;
|
|
}
|
|
|
|
- (NSManagedObjectContext *)privateManagedObjectContextIfReady {
|
|
|
|
[self loadStore];
|
|
return self.privateManagedObjectContext;
|
|
}
|
|
|
|
- (NSURL *)localStoreURL {
|
|
|
|
NSURL *applicationSupportURL = [[[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory
|
|
inDomains:NSUserDomainMask] lastObject];
|
|
return [[[applicationSupportURL
|
|
URLByAppendingPathComponent:[NSBundle mainBundle].bundleIdentifier isDirectory:YES]
|
|
URLByAppendingPathComponent:@"MasterPassword" isDirectory:NO]
|
|
URLByAppendingPathExtension:@"sqlite"];
|
|
}
|
|
|
|
- (void)loadStore {
|
|
|
|
static dispatch_once_t once = 0;
|
|
dispatch_once( &once, ^{
|
|
(self.storeQueue = [NSOperationQueue new]).maxConcurrentOperationCount = 1;
|
|
} );
|
|
|
|
// Do nothing if already fully set up, otherwise (re-)load the store.
|
|
if (self.storeCoordinator && self.mainManagedObjectContext && self.privateManagedObjectContext)
|
|
return;
|
|
|
|
[self.storeQueue addOperationWithBlock:^{
|
|
// Do nothing if already fully set up, otherwise (re-)load the store.
|
|
if (self.storeCoordinator && self.mainManagedObjectContext && self.privateManagedObjectContext)
|
|
return;
|
|
|
|
// Unregister any existing observers and contexts.
|
|
PearlRemoveNotificationObserversFrom( self.mainManagedObjectContext );
|
|
[self.mainManagedObjectContext performBlockAndWait:^{
|
|
[self.mainManagedObjectContext reset];
|
|
self.mainManagedObjectContext = nil;
|
|
}];
|
|
[self.privateManagedObjectContext performBlockAndWait:^{
|
|
[self.privateManagedObjectContext reset];
|
|
self.privateManagedObjectContext = nil;
|
|
}];
|
|
|
|
// Don't load when the store is corrupted.
|
|
if ([self.storeCorrupted boolValue])
|
|
return;
|
|
|
|
// Check if migration is necessary.
|
|
[self migrateStore];
|
|
|
|
// Install managed object contexts and observers.
|
|
self.privateManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
|
|
[self.privateManagedObjectContext performBlockAndWait:^{
|
|
self.privateManagedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;
|
|
self.privateManagedObjectContext.persistentStoreCoordinator = self.storeCoordinator;
|
|
}];
|
|
|
|
self.mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
|
|
self.mainManagedObjectContext.parentContext = self.privateManagedObjectContext;
|
|
if ([self.mainManagedObjectContext respondsToSelector:@selector( automaticallyMergesChangesFromParent )]) // iOS 10+
|
|
self.mainManagedObjectContext.automaticallyMergesChangesFromParent = YES;
|
|
else
|
|
// When privateManagedObjectContext is saved, import the changes into mainManagedObjectContext.
|
|
PearlAddNotificationObserverTo( self.mainManagedObjectContext, NSManagedObjectContextDidSaveNotification,
|
|
self.privateManagedObjectContext, nil, ^(NSManagedObjectContext *mainContext, NSNotification *note) {
|
|
[mainContext performBlock:^{
|
|
@try {
|
|
[mainContext mergeChangesFromContextDidSaveNotification:note];
|
|
}
|
|
@catch (NSException *exception) {
|
|
err( @"While merging changes:\n%@", [exception fullDescription] );
|
|
}
|
|
}];
|
|
} );
|
|
|
|
|
|
// Create a new store coordinator.
|
|
NSError *error = nil;
|
|
NSURL *localStoreURL = [self localStoreURL];
|
|
if (![[NSFileManager defaultManager] createDirectoryAtURL:[localStoreURL URLByDeletingLastPathComponent]
|
|
withIntermediateDirectories:YES attributes:nil error:&error]) {
|
|
MPError( error, @"Couldn't create our application support directory." );
|
|
return;
|
|
}
|
|
if (![self.storeCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[self localStoreURL]
|
|
options:@{
|
|
NSMigratePersistentStoresAutomaticallyOption: @YES,
|
|
NSInferMappingModelAutomaticallyOption : @YES,
|
|
STORE_OPTIONS
|
|
} error:&error]) {
|
|
MPError( error, @"Failed to open store." );
|
|
self.storeCorrupted = @YES;
|
|
[self handleCoordinatorError:error];
|
|
return;
|
|
}
|
|
self.storeCorrupted = @NO;
|
|
|
|
#if TARGET_OS_IPHONE
|
|
PearlAddNotificationObserver( UIApplicationWillResignActiveNotification, UIApp, [NSOperationQueue mainQueue],
|
|
^(MPAppDelegate_Shared *self, NSNotification *note) {
|
|
[self.mainManagedObjectContext saveToStore];
|
|
} );
|
|
#else
|
|
PearlAddNotificationObserver( NSApplicationWillResignActiveNotification, NSApp, [NSOperationQueue mainQueue],
|
|
^(MPAppDelegate_Shared *self, NSNotification *note) {
|
|
[self.mainManagedObjectContext saveToStore];
|
|
} );
|
|
#endif
|
|
|
|
// Perform a data sanity check on the newly loaded store to find and fix any issues.
|
|
if ([[MPConfig get].checkInconsistency boolValue])
|
|
[MPAppDelegate_Shared managedObjectContextPerformBlockAndWait:^(NSManagedObjectContext *context) {
|
|
[self findAndFixInconsistenciesSaveInContext:context];
|
|
}];
|
|
}];
|
|
}
|
|
|
|
- (void)deleteAndResetStore {
|
|
|
|
@synchronized (self) {
|
|
// Unregister any existing observers and contexts.
|
|
PearlRemoveNotificationObserversFrom( self.mainManagedObjectContext );
|
|
[self.mainManagedObjectContext performBlockAndWait:^{
|
|
[self.mainManagedObjectContext reset];
|
|
self.mainManagedObjectContext = nil;
|
|
}];
|
|
[self.privateManagedObjectContext performBlockAndWait:^{
|
|
[self.privateManagedObjectContext reset];
|
|
self.privateManagedObjectContext = nil;
|
|
}];
|
|
NSError *error = nil;
|
|
for (NSPersistentStore *store in self.storeCoordinator.persistentStores) {
|
|
if (![self.storeCoordinator removePersistentStore:store error:&error])
|
|
MPError( error, @"Couldn't remove persistence store from coordinator." );
|
|
}
|
|
if (![[NSFileManager defaultManager] removeItemAtURL:self.localStoreURL error:&error])
|
|
MPError( error, @"Couldn't remove persistence store at URL %@.", self.localStoreURL );
|
|
|
|
[self loadStore];
|
|
}
|
|
}
|
|
|
|
- (MPFixableResult)findAndFixInconsistenciesSaveInContext:(NSManagedObjectContext *)context {
|
|
|
|
NSError *error = nil;
|
|
NSFetchRequest *fetchRequest = [NSFetchRequest new];
|
|
fetchRequest.fetchBatchSize = 50;
|
|
|
|
MPFixableResult result = MPFixableResultNoProblems;
|
|
for (NSEntityDescription *entity in [context.persistentStoreCoordinator.managedObjectModel entities])
|
|
if (class_conformsToProtocol( NSClassFromString( entity.managedObjectClassName ), @protocol(MPFixable) )) {
|
|
fetchRequest.entity = entity;
|
|
NSArray *objects = [context executeFetchRequest:fetchRequest error:&error];
|
|
if (!objects) {
|
|
MPError( error, @"Failed to fetch %@ objects.", entity );
|
|
continue;
|
|
}
|
|
|
|
for (NSManagedObject<MPFixable> *object in objects)
|
|
result = MPApplyFix( result, ^MPFixableResult {
|
|
return [object findAndFixInconsistenciesInContext:context];
|
|
} );
|
|
}
|
|
|
|
if (result == MPFixableResultNoProblems)
|
|
inf( @"Sanity check found no problems in store." );
|
|
|
|
else {
|
|
[context saveToStore];
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:MPFoundInconsistenciesNotification object:nil userInfo:@{
|
|
MPInconsistenciesFixResultUserKey: @(result)
|
|
}];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
- (void)migrateStore {
|
|
|
|
MPStoreMigrationLevel migrationLevel = (MPStoreMigrationLevel)
|
|
[[NSUserDefaults standardUserDefaults] integerForKey:MPMigrationLevelLocalStoreKey];
|
|
if (migrationLevel >= MPStoreMigrationLevelCurrent)
|
|
// Local store up-to-date.
|
|
return;
|
|
|
|
inf( @"Local store migration level: %d (current %d)", (signed)migrationLevel, (signed)MPStoreMigrationLevelCurrent );
|
|
if (migrationLevel <= MPStoreMigrationLevelV1 && ![self migrateV1LocalStore]) {
|
|
inf( @"Failed to migrate old V1 to new local store." );
|
|
return;
|
|
}
|
|
if (migrationLevel <= MPStoreMigrationLevelV2 && ![self migrateV2LocalStore]) {
|
|
inf( @"Failed to migrate old V2 to new local store." );
|
|
return;
|
|
}
|
|
|
|
[[NSUserDefaults standardUserDefaults] setInteger:MPStoreMigrationLevelCurrent forKey:MPMigrationLevelLocalStoreKey];
|
|
inf( @"Successfully migrated old to new local store." );
|
|
if (![[NSUserDefaults standardUserDefaults] synchronize])
|
|
wrn( @"Couldn't synchronize after store migration." );
|
|
}
|
|
|
|
- (BOOL)migrateV1LocalStore {
|
|
|
|
NSURL *applicationFilesDirectory = [[[NSFileManager defaultManager]
|
|
URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
|
|
NSURL *oldLocalStoreURL = [[applicationFilesDirectory
|
|
URLByAppendingPathComponent:@"MasterPassword" isDirectory:NO] URLByAppendingPathExtension:@"sqlite"];
|
|
if (![[NSFileManager defaultManager] fileExistsAtPath:oldLocalStoreURL.path isDirectory:NULL]) {
|
|
inf( @"No V1 local store to migrate." );
|
|
return YES;
|
|
}
|
|
|
|
inf( @"Migrating V1 local store" );
|
|
NSURL *newLocalStoreURL = [self localStoreURL];
|
|
if (![[NSFileManager defaultManager] fileExistsAtPath:newLocalStoreURL.path isDirectory:NULL]) {
|
|
inf( @"New local store already exists." );
|
|
return YES;
|
|
}
|
|
|
|
NSError *error = nil;
|
|
if (![NSPersistentStore migrateStore:oldLocalStoreURL withOptions:@{ STORE_OPTIONS }
|
|
toStore:newLocalStoreURL withOptions:@{ STORE_OPTIONS }
|
|
error:&error]) {
|
|
MPError( error, @"Couldn't migrate the old store to the new location." );
|
|
return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)migrateV2LocalStore {
|
|
|
|
NSURL *applicationSupportURL = [[[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory
|
|
inDomains:NSUserDomainMask] lastObject];
|
|
NSURL *oldLocalStoreURL;
|
|
// On iOS, each app is in a sandbox so we don't need to app-scope this directory.
|
|
#if TARGET_OS_IPHONE
|
|
oldLocalStoreURL = [[applicationSupportURL
|
|
URLByAppendingPathComponent:@"UbiquityStore" isDirectory:NO]
|
|
URLByAppendingPathExtension:@"sqlite"];
|
|
#else
|
|
// The directory is shared between all apps on the system so we need to scope it for the running app.
|
|
oldLocalStoreURL = [[[applicationSupportURL
|
|
URLByAppendingPathComponent:[NSRunningApplication currentApplication].bundleIdentifier isDirectory:YES]
|
|
URLByAppendingPathComponent:@"UbiquityStore" isDirectory:NO]
|
|
URLByAppendingPathExtension:@"sqlite"];
|
|
#endif
|
|
|
|
if (![[NSFileManager defaultManager] fileExistsAtPath:oldLocalStoreURL.path isDirectory:NULL]) {
|
|
inf( @"No V2 local store to migrate." );
|
|
return YES;
|
|
}
|
|
|
|
inf( @"Migrating V2 local store" );
|
|
NSURL *newLocalStoreURL = [self localStoreURL];
|
|
if ([[NSFileManager defaultManager] fileExistsAtPath:newLocalStoreURL.path isDirectory:NULL]) {
|
|
inf( @"New local store already exists." );
|
|
return YES;
|
|
}
|
|
|
|
NSError *error = nil;
|
|
if (![NSPersistentStore migrateStore:oldLocalStoreURL withOptions:@{
|
|
NSMigratePersistentStoresAutomaticallyOption: @YES,
|
|
NSInferMappingModelAutomaticallyOption : @YES,
|
|
STORE_OPTIONS
|
|
} toStore:newLocalStoreURL withOptions:@{
|
|
NSMigratePersistentStoresAutomaticallyOption: @YES,
|
|
NSInferMappingModelAutomaticallyOption : @YES,
|
|
STORE_OPTIONS
|
|
} error:&error]) {
|
|
MPError( error, @"Couldn't migrate the old store to the new location." );
|
|
return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
//- (BOOL)migrateV3LocalStore {
|
|
//
|
|
// inf( @"Migrating V3 local store" );
|
|
// NSURL *localStoreURL = [self localStoreURL];
|
|
// if (![[NSFileManager defaultManager] fileExistsAtPath:localStoreURL.path isDirectory:NULL]) {
|
|
// inf( @"No V3 local store to migrate." );
|
|
// return YES;
|
|
// }
|
|
//
|
|
// NSError *error = nil;
|
|
// NSDictionary<NSString *, id> *metadata = [NSPersistentStore metadataForPersistentStoreWithURL:localStoreURL error:&error];
|
|
// if (!metadata) {
|
|
// MPError( error, @"Couldn't inspect metadata for store: %@", localStoreURL );
|
|
// return NO;
|
|
// }
|
|
// NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:
|
|
// [NSManagedObjectModel mergedModelFromBundles:nil forStoreMetadata:metadata]];
|
|
// if (![coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil
|
|
// URL:localStoreURL options:@{ STORE_OPTIONS }
|
|
// error:&error]) {
|
|
// MPError( error, @"Couldn't open V3 local store to migrate." );
|
|
// return NO;
|
|
// }
|
|
//
|
|
// NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
|
|
// [context performBlockAndWait:^{
|
|
// context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;
|
|
// context.persistentStoreCoordinator = coordinator;
|
|
// for (MPStoredSiteEntity *storedSite in [[MPStoredSiteEntity fetchRequest] execute:&error]) {
|
|
// id contentObject = [storedSite valueForKey:@"contentObject"];
|
|
// if ([contentObject isKindOfClass:[NSData class]])
|
|
// storedSite.contentObject = contentObject;
|
|
// }
|
|
// }];
|
|
//
|
|
// return YES;
|
|
//}
|
|
|
|
#pragma mark - Utilities
|
|
|
|
- (void)addSiteNamed:(NSString *)siteName completion:(void ( ^ )(MPSiteEntity *site, NSManagedObjectContext *context))completion {
|
|
|
|
if (![siteName length]) {
|
|
completion( nil, nil );
|
|
return;
|
|
}
|
|
|
|
[MPAppDelegate_Shared managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
|
|
MPUserEntity *activeUser = [self activeUserInContext:context];
|
|
NSAssert( activeUser, @"Missing user." );
|
|
if (!activeUser) {
|
|
completion( nil, nil );
|
|
return;
|
|
}
|
|
|
|
MPResultType type = activeUser.defaultType;
|
|
id<MPAlgorithm> algorithm = MPAlgorithmDefault;
|
|
Class entityType = [algorithm classOfType:type];
|
|
|
|
MPSiteEntity *site = (MPSiteEntity *)[entityType insertNewObjectInContext:context];
|
|
site.name = siteName;
|
|
site.user = activeUser;
|
|
site.type = type;
|
|
site.lastUsed = [NSDate date];
|
|
site.algorithm = algorithm;
|
|
|
|
[context saveToStore];
|
|
|
|
completion( site, context );
|
|
}];
|
|
}
|
|
|
|
- (MPSiteEntity *)changeSite:(MPSiteEntity *)site saveInContext:(NSManagedObjectContext *)context toType:(MPResultType)type {
|
|
|
|
if (site.type == type)
|
|
return site;
|
|
|
|
if ([site.algorithm classOfType:type] == site.typeClass) {
|
|
site.type = type;
|
|
[context saveToStore];
|
|
}
|
|
|
|
else {
|
|
// Type requires a different class of site. Recreate the site.
|
|
Class entityType = [site.algorithm classOfType:type];
|
|
MPSiteEntity *newSite = (MPSiteEntity *)[entityType insertNewObjectInContext:context];
|
|
newSite.type = type;
|
|
newSite.name = site.name;
|
|
newSite.user = site.user;
|
|
newSite.uses = site.uses;
|
|
newSite.lastUsed = site.lastUsed;
|
|
newSite.algorithm = site.algorithm;
|
|
newSite.loginName = site.loginName;
|
|
|
|
[context deleteObject:site];
|
|
[context saveToStore];
|
|
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:MPSiteUpdatedNotification object:site.permanentObjectID];
|
|
site = newSite;
|
|
}
|
|
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:MPSiteUpdatedNotification object:site.permanentObjectID];
|
|
return site;
|
|
}
|
|
|
|
- (void)importSites:(NSString *)importData
|
|
askImportPassword:(NSString *( ^ )(NSString *userName))importPassword
|
|
askUserPassword:(NSString *( ^ )(NSString *userName))userPassword
|
|
result:(void ( ^ )(NSError *error))resultBlock {
|
|
|
|
NSAssert( ![[NSThread currentThread] isMainThread], @"This method should not be invoked from the main thread." );
|
|
|
|
do {
|
|
if ([MPAppDelegate_Shared managedObjectContextPerformBlockAndWait:^(NSManagedObjectContext *context) {
|
|
NSError *error = [self importSites:importData askImportPassword:importPassword askUserPassword:userPassword
|
|
saveInContext:context];
|
|
PearlMainQueue( ^{
|
|
resultBlock( error );
|
|
} );
|
|
}])
|
|
break;
|
|
usleep( (useconds_t)(USEC_PER_SEC * 0.2) );
|
|
} while (YES);
|
|
}
|
|
|
|
- (NSError *)importSites:(NSString *)importData
|
|
askImportPassword:(NSString *( ^ )(NSString *userName))askImportPassword
|
|
askUserPassword:(NSString *( ^ )(NSString *userName))askUserPassword
|
|
saveInContext:(NSManagedObjectContext *)context {
|
|
|
|
// Read metadata for the import file.
|
|
MPMarshallInfo *info = mpw_marshall_read_info( importData.UTF8String );
|
|
if (info->format == MPMarshallFormatNone)
|
|
return MPError( ([NSError errorWithDomain:MPErrorDomain code:MPErrorMarshallCode userInfo:@{
|
|
@"type" : @(MPMarshallErrorFormat),
|
|
NSLocalizedDescriptionKey: @"This is not a Master Password import file.",
|
|
}]), @"While importing sites." );
|
|
|
|
// Get master password for import file.
|
|
MPKey *importKey;
|
|
NSString *importMasterPassword;
|
|
do {
|
|
importMasterPassword = askImportPassword( @(info->fullName) );
|
|
if (!importMasterPassword) {
|
|
inf( @"Import cancelled." );
|
|
mpw_marshal_info_free( info );
|
|
return MPError( ([NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]), @"" );
|
|
}
|
|
|
|
importKey = [[MPKey alloc] initForFullName:@(info->fullName) withMasterPassword:importMasterPassword];
|
|
} while ([[[importKey keyIDForAlgorithm:MPAlgorithmForVersion( info->algorithm )] encodeHex]
|
|
caseInsensitiveCompare:@(info->keyID)] != NSOrderedSame);
|
|
|
|
// Parse import data.
|
|
MPMarshallError importError = { .type = MPMarshallSuccess };
|
|
MPMarshalledUser *importUser = mpw_marshall_read( importData.UTF8String, info->format, importMasterPassword.UTF8String, &importError );
|
|
mpw_marshal_info_free( info );
|
|
|
|
@try {
|
|
if (!importUser || importError.type != MPMarshallSuccess)
|
|
return MPError( ([NSError errorWithDomain:MPErrorDomain code:MPErrorMarshallCode userInfo:@{
|
|
@"type" : @(importError.type),
|
|
NSLocalizedDescriptionKey: @(importError.description),
|
|
}]), @"While importing sites." );
|
|
|
|
// Find an existing user to update.
|
|
NSError *error = nil;
|
|
NSFetchRequest *userFetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPUserEntity class] )];
|
|
userFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@", @(importUser->fullName)];
|
|
NSArray *users = [context executeFetchRequest:userFetchRequest error:&error];
|
|
if (!users)
|
|
return MPError( error, @"While looking for user: %@.", @(importUser->fullName) );
|
|
if ([users count] > 1)
|
|
return MPMakeError( @"While looking for user: %@, found more than one: %zu",
|
|
@(importUser->fullName), (size_t)[users count] );
|
|
|
|
// Get master key for user.
|
|
MPUserEntity *user = [users lastObject];
|
|
MPKey *userKey = importKey;
|
|
while (user && ![[userKey keyIDForAlgorithm:user.algorithm] isEqualToData:user.keyID]) {
|
|
NSString *userMasterPassword = askUserPassword( user.name );
|
|
if (!userMasterPassword) {
|
|
inf( @"Import cancelled." );
|
|
return MPError( ([NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]), @"" );
|
|
}
|
|
|
|
userKey = [[MPKey alloc] initForFullName:@(importUser->fullName) withMasterPassword:userMasterPassword];
|
|
}
|
|
|
|
// Update or create user.
|
|
if (!user) {
|
|
user = [MPUserEntity insertNewObjectInContext:context];
|
|
user.name = @(importUser->fullName);
|
|
}
|
|
user.algorithm = MPAlgorithmForVersion( importUser->algorithm );
|
|
user.keyID = [userKey keyIDForAlgorithm:user.algorithm];
|
|
user.avatar = importUser->avatar;
|
|
user.defaultType = importUser->defaultType;
|
|
user.lastUsed = [NSDate dateWithTimeIntervalSince1970:MAX( user.lastUsed.timeIntervalSince1970, importUser->lastUsed )];
|
|
dbg( @"Importing user: %@", [user debugDescription] );
|
|
|
|
// Update or create sites.
|
|
for (size_t s = 0; s < importUser->sites_count; ++s) {
|
|
MPMarshalledSite *importSite = &importUser->sites[s];
|
|
|
|
// Find an existing site to update.
|
|
NSFetchRequest *siteFetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPSiteEntity class] )];
|
|
siteFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@ AND user == %@", @(importSite->name), user];
|
|
NSArray *existingSites = [context executeFetchRequest:siteFetchRequest error:&error];
|
|
if (!existingSites)
|
|
return MPError( error, @"Lookup of existing sites failed for site: %@, user: %@", @(importSite->name), user.userID );
|
|
if ([existingSites count])
|
|
// Update existing site.
|
|
for (MPSiteEntity *site in existingSites) {
|
|
[self importSite:importSite protectedByKey:importKey intoSite:site usingKey:userKey];
|
|
dbg( @"Updated site: %@", [site debugDescription] );
|
|
}
|
|
else {
|
|
// Create new site.
|
|
id<MPAlgorithm> algorithm = MPAlgorithmForVersion( importSite->algorithm );
|
|
Class entityType = [algorithm classOfType:importSite->type];
|
|
if (!entityType)
|
|
return MPMakeError( @"Invalid site type in import file: %@ has type %lu", @(importSite->name), (long)importSite->type );
|
|
|
|
MPSiteEntity *site = (MPSiteEntity *)[entityType insertNewObjectInContext:context];
|
|
site.user = user;
|
|
|
|
[self importSite:importSite protectedByKey:importKey intoSite:site usingKey:userKey];
|
|
dbg( @"Created site: %@", [site debugDescription] );
|
|
}
|
|
}
|
|
|
|
if (![context saveToStore])
|
|
return MPMakeError( @"Failed saving imported changes." );
|
|
|
|
inf( @"Import completed successfully." );
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:MPSitesImportedNotification object:nil userInfo:@{
|
|
MPSitesImportedNotificationUserKey: user
|
|
}];
|
|
return nil;
|
|
}
|
|
@finally {
|
|
mpw_marshal_free( importUser );
|
|
}
|
|
}
|
|
|
|
- (void)importSite:(const MPMarshalledSite *)importSite protectedByKey:(MPKey *)importKey intoSite:(MPSiteEntity *)site
|
|
usingKey:(MPKey *)userKey {
|
|
|
|
site.name = @(importSite->name);
|
|
if (importSite->content)
|
|
[site.algorithm importPassword:@(importSite->content) protectedByKey:importKey intoSite:site usingKey:userKey];
|
|
site.type = importSite->type;
|
|
if ([site isKindOfClass:[MPGeneratedSiteEntity class]])
|
|
((MPGeneratedSiteEntity *)site).counter = importSite->counter;
|
|
site.algorithm = MPAlgorithmForVersion( importSite->algorithm );
|
|
site.loginName = importSite->loginName? @(importSite->loginName): nil;
|
|
site.loginGenerated = importSite->loginGenerated;
|
|
site.url = importSite->url? @(importSite->url): nil;
|
|
site.uses = importSite->uses;
|
|
site.lastUsed = [NSDate dateWithTimeIntervalSince1970:importSite->lastUsed];
|
|
}
|
|
|
|
- (void)exportSitesRevealPasswords:(BOOL)revealPasswords
|
|
askExportPassword:(NSString *( ^ )(NSString *userName))askImportPassword
|
|
result:(void ( ^ )(NSString *mpsites, NSError *error))resultBlock {
|
|
|
|
[MPAppDelegate_Shared managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
|
|
MPUserEntity *user = [self activeUserInContext:context];
|
|
NSString *masterPassword = askImportPassword( user.name );
|
|
|
|
inf( @"Exporting sites, %@, for user: %@", revealPasswords? @"revealing passwords": @"omitting passwords", user.userID );
|
|
MPMarshalledUser *exportUser = mpw_marshall_user( user.name.UTF8String, masterPassword.UTF8String, user.algorithm.version );
|
|
exportUser->redacted = !revealPasswords;
|
|
exportUser->avatar = (unsigned int)user.avatar;
|
|
exportUser->defaultType = user.defaultType;
|
|
exportUser->lastUsed = (time_t)user.lastUsed.timeIntervalSince1970;
|
|
|
|
for (MPSiteEntity *site in user.sites) {
|
|
MPCounterValue counter = MPCounterValueInitial;
|
|
if ([site isKindOfClass:[MPGeneratedSiteEntity class]])
|
|
counter = ((MPGeneratedSiteEntity *)site).counter;
|
|
NSString *content = revealPasswords
|
|
? [site.algorithm exportPasswordForSite:site usingKey:self.key]
|
|
: [site.algorithm resolvePasswordForSite:site usingKey:self.key];
|
|
|
|
MPMarshalledSite *exportSite = mpw_marshall_site( exportUser,
|
|
site.name.UTF8String, site.type, counter, site.algorithm.version );
|
|
exportSite->content = content.UTF8String;
|
|
exportSite->loginName = site.loginName.UTF8String;
|
|
exportSite->loginGenerated = site.loginGenerated;
|
|
exportSite->url = site.url.UTF8String;
|
|
exportSite->uses = (unsigned int)site.uses;
|
|
exportSite->lastUsed = (time_t)site.lastUsed.timeIntervalSince1970;
|
|
|
|
for (MPSiteQuestionEntity *siteQuestion in site.questions)
|
|
mpw_marshal_question( exportSite, siteQuestion.keyword.UTF8String );
|
|
}
|
|
|
|
char *export = NULL;
|
|
MPMarshallError exportError = (MPMarshallError){ .type= MPMarshallSuccess };
|
|
mpw_marshall_write( &export, MPMarshallFormatFlat, exportUser, &exportError );
|
|
NSString *mpsites = nil;
|
|
if (export && exportError.type == MPMarshallSuccess)
|
|
mpsites = [NSString stringWithCString:export encoding:NSUTF8StringEncoding];
|
|
mpw_free_string( export );
|
|
|
|
resultBlock( mpsites, exportError.type == MPMarshallSuccess? nil:
|
|
[NSError errorWithDomain:MPErrorDomain code:MPErrorMarshallCode userInfo:@{
|
|
@"type" : @(exportError.type),
|
|
NSLocalizedDescriptionKey: @(exportError.description),
|
|
}] );
|
|
}];
|
|
}
|
|
|
|
@end
|