//==============================================================================
// 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 .
//==============================================================================
#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 *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 *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 *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 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 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->loginContent? @(importSite->loginContent): nil;
site.loginGenerated = importSite->loginType & MPResultTypeClassTemplate;
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->loginContent = site.loginName.UTF8String;
exportSite->loginType = site.loginGenerated? MPResultTypeTemplateName: MPResultTypeStatefulPersonal;
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