2
0

Adjust darwin build configuration to new project structure.

This commit is contained in:
Maarten Billemont
2017-03-06 17:25:59 -05:00
parent 18fce4eaf8
commit 94159ed11a
1013 changed files with 305 additions and 572 deletions

View File

@@ -0,0 +1,98 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPAlgorithm
//
// Created by Maarten Billemont on 16/07/12.
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
//
#import "MPKey.h"
#import "MPStoredSiteEntity.h"
#import "MPGeneratedSiteEntity.h"
#import "MPSiteQuestionEntity.h"
#import "mpw-algorithm.h"
#define MPAlgorithmDefaultVersion MPAlgorithmVersionCurrent
#define MPAlgorithmDefault MPAlgorithmForVersion(MPAlgorithmDefaultVersion)
id<MPAlgorithm> MPAlgorithmForVersion(MPAlgorithmVersion version);
id<MPAlgorithm> MPAlgorithmDefaultForBundleVersion(NSString *bundleVersion);
PearlEnum( MPAttacker,
MPAttacker1, MPAttacker5K, MPAttacker20M, MPAttacker5B );
typedef struct TimeToCrack {
unsigned long long hours;
unsigned long long days;
unsigned long long weeks;
unsigned long long months;
unsigned long long years;
unsigned long long universes;
} TimeToCrack;
NSString *NSStringFromTimeToCrack(TimeToCrack timeToCrack);
@protocol MPAlgorithm<NSObject>
@required
- (MPAlgorithmVersion)version;
- (BOOL)tryMigrateUser:(MPUserEntity *)user inContext:(NSManagedObjectContext *)moc;
- (BOOL)tryMigrateSite:(MPSiteEntity *)site explicit:(BOOL)explicit;
- (NSData *)keyIDForKeyData:(NSData *)keyData;
- (NSData *)keyDataForFullName:(NSString *)fullName withMasterPassword:(NSString *)masterPassword;
- (NSString *)nameOfType:(MPSiteType)type;
- (NSString *)shortNameOfType:(MPSiteType)type;
- (NSString *)classNameOfType:(MPSiteType)type;
- (Class)classOfType:(MPSiteType)type;
- (NSArray *)allTypes;
- (NSArray *)allTypesStartingWith:(MPSiteType)startingType;
- (MPSiteType)nextType:(MPSiteType)type;
- (MPSiteType)previousType:(MPSiteType)type;
- (NSString *)generateLoginForSiteNamed:(NSString *)name usingKey:(MPKey *)key;
- (NSString *)generatePasswordForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter
usingKey:(MPKey *)key;
- (NSString *)generateAnswerForSiteNamed:(NSString *)name onQuestion:(NSString *)question usingKey:(MPKey *)key;
- (NSString *)generateContentForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter
variant:(MPSiteVariant)variant context:(NSString *)context usingKey:(MPKey *)key;
- (NSString *)storedLoginForSite:(MPStoredSiteEntity *)site usingKey:(MPKey *)key;
- (NSString *)storedPasswordForSite:(MPStoredSiteEntity *)site usingKey:(MPKey *)key;
- (BOOL)savePassword:(NSString *)clearPassword toSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey;
- (NSString *)resolveLoginForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey;
- (NSString *)resolvePasswordForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey;
- (NSString *)resolveAnswerForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey;
- (NSString *)resolveAnswerForQuestion:(MPSiteQuestionEntity *)question usingKey:(MPKey *)siteKey;
- (void)resolveLoginForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey
result:(void ( ^ )(NSString *result))resultBlock;
- (void)resolvePasswordForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey
result:(void ( ^ )(NSString *result))resultBlock;
- (void)resolveAnswerForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey
result:(void ( ^ )(NSString *result))resultBlock;
- (void)resolveAnswerForQuestion:(MPSiteQuestionEntity *)question usingKey:(MPKey *)siteKey
result:(void ( ^ )(NSString *result))resultBlock;
- (void)importProtectedPassword:(NSString *)protectedPassword protectedByKey:(MPKey *)importKey
intoSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey;
- (void)importClearTextPassword:(NSString *)clearPassword intoSite:(MPSiteEntity *)site
usingKey:(MPKey *)siteKey;
- (NSString *)exportPasswordForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey;
- (BOOL)timeToCrack:(out TimeToCrack *)timeToCrack passwordOfType:(MPSiteType)type byAttacker:(MPAttacker)attacker;
- (BOOL)timeToCrack:(out TimeToCrack *)timeToCrack passwordString:(NSString *)password byAttacker:(MPAttacker)attacker;
@end

View File

@@ -0,0 +1,61 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPAlgorithm
//
// Created by Maarten Billemont on 16/07/12.
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
//
#import "MPAlgorithm.h"
id<MPAlgorithm> MPAlgorithmForVersion(MPAlgorithmVersion version) {
static NSMutableDictionary *versionToAlgorithm = nil;
if (!versionToAlgorithm)
versionToAlgorithm = [NSMutableDictionary dictionary];
id<MPAlgorithm> algorithm = versionToAlgorithm[@(version)];
if (!algorithm && (algorithm = (id<MPAlgorithm>)[NSClassFromString( strf( @"MPAlgorithmV%lu", (unsigned long)version ) ) new]))
versionToAlgorithm[@(version)] = algorithm;
return algorithm;
}
id<MPAlgorithm> MPAlgorithmDefaultForBundleVersion(NSString *bundleVersion) {
if (PearlCFBundleVersionCompare( bundleVersion, @"1.3" ) == NSOrderedAscending)
// Pre-1.3
return MPAlgorithmForVersion( 0 );
if (PearlCFBundleVersionCompare( bundleVersion, @"2.1" ) == NSOrderedAscending)
// Pre-2.1
return MPAlgorithmForVersion( 1 );
return MPAlgorithmDefault;
}
NSString *NSStringFromTimeToCrack(TimeToCrack timeToCrack) {
if (timeToCrack.universes > 1)
return strl( @"> age of the universe" );
else if (timeToCrack.years > 1)
return strl( @"%d years", timeToCrack.years );
else if (timeToCrack.months > 1)
return strl( @"%d months", timeToCrack.months );
else if (timeToCrack.weeks > 1)
return strl( @"%d weeks", timeToCrack.weeks );
else if (timeToCrack.days > 1)
return strl( @"%d days", timeToCrack.days );
else if (timeToCrack.hours > 1)
return strl( @"%d hours", timeToCrack.hours );
else
return strl( @"trivial" );
}

View File

@@ -0,0 +1,21 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPAlgorithmV0
//
// Created by Maarten Billemont on 16/07/12.
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
//
#import "MPAlgorithm.h"
@interface MPAlgorithmV0 : NSObject<MPAlgorithm>
@end

View File

@@ -0,0 +1,894 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPAlgorithmV0
//
// Created by Maarten Billemont on 16/07/12.
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
//
#ifndef trc
#error error
#endif
#import "MPAlgorithmV0.h"
#import "MPEntities.h"
#import "MPAppDelegate_Shared.h"
#import "MPAppDelegate_InApp.h"
#import "mpw-util.h"
#include <openssl/bn.h>
#include <openssl/err.h>
/* An AMD HD 7970 calculates 2495M SHA-1 hashes per second at a cost of ~350$ per GPU */
#define CRACKING_PER_SECOND 2495000000UL
#define CRACKING_PRICE 350
NSOperationQueue *_mpwQueue = nil;
@implementation MPAlgorithmV0 {
BN_CTX *_ctx;
}
- (id)init {
if (!(self = [super init]))
return nil;
_ctx = BN_CTX_new();
static dispatch_once_t once = 0;
dispatch_once( &once, ^{
_mpwQueue = [NSOperationQueue new];
_mpwQueue.maxConcurrentOperationCount = 1;
_mpwQueue.name = @"mpw queue";
} );
return self;
}
- (void)dealloc {
BN_CTX_free( _ctx );
_ctx = NULL;
}
- (MPAlgorithmVersion)version {
return MPAlgorithmVersion0;
}
- (NSString *)description {
return strf( @"V%lu", (unsigned long)self.version );
}
- (NSString *)debugDescription {
return strf( @"<%@: version=%lu>", NSStringFromClass( [self class] ), (unsigned long)self.version );
}
- (BOOL)isEqual:(id)other {
if (other == self)
return YES;
if (!other || ![other conformsToProtocol:@protocol(MPAlgorithm)])
return NO;
return [(id<MPAlgorithm>)other version] == [self version];
}
- (void)mpw_perform:(void ( ^ )(void))operationBlock {
if ([NSOperationQueue currentQueue] == _mpwQueue) {
operationBlock();
return;
}
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:operationBlock];
if ([operation respondsToSelector:@selector( qualityOfService )])
operation.qualityOfService = NSQualityOfServiceUserInitiated;
[_mpwQueue addOperations:@[ operation ] waitUntilFinished:YES];
}
- (BOOL)tryMigrateUser:(MPUserEntity *)user inContext:(NSManagedObjectContext *)moc {
NSError *error = nil;
NSFetchRequest *migrationRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPSiteEntity class] )];
migrationRequest.predicate = [NSPredicate predicateWithFormat:@"version_ < %d AND user == %@", self.version, user];
NSArray *migrationSites = [moc executeFetchRequest:migrationRequest error:&error];
if (!migrationSites) {
err( @"While looking for sites to migrate: %@", [error fullDescription] );
return NO;
}
BOOL success = YES;
for (MPSiteEntity *migrationSite in migrationSites)
if (![migrationSite tryMigrateExplicitly:NO])
success = NO;
return success;
}
- (BOOL)tryMigrateSite:(MPSiteEntity *)site explicit:(BOOL)explicit {
if ([site.algorithm version] != [self version] - 1)
// Only migrate from previous version.
return NO;
if (!explicit) {
// This migration requires explicit permission.
site.requiresExplicitMigration = YES;
return NO;
}
// Apply migration.
site.requiresExplicitMigration = NO;
site.algorithm = self;
return YES;
}
- (NSData *)keyDataForFullName:(NSString *)fullName withMasterPassword:(NSString *)masterPassword {
__block NSData *keyData;
[self mpw_perform:^{
NSDate *start = [NSDate date];
uint8_t const *masterKeyBytes = mpw_masterKeyForUser( fullName.UTF8String, masterPassword.UTF8String, [self version] );
if (masterKeyBytes) {
keyData = [NSData dataWithBytes:masterKeyBytes length:MP_dkLen];
trc( @"User: %@, password: %@ derives to key ID: %@ (took %0.2fs)", //
fullName, masterPassword, [self keyIDForKeyData:keyData], -[start timeIntervalSinceNow] );
mpw_free( masterKeyBytes, MP_dkLen );
}
}];
return keyData;
}
- (NSData *)keyIDForKeyData:(NSData *)keyData {
return [keyData hashWith:PearlHashSHA256];
}
- (NSString *)nameOfType:(MPSiteType)type {
if (!type)
return nil;
switch (type) {
case MPSiteTypeGeneratedMaximum:
return @"Maximum Security Password";
case MPSiteTypeGeneratedLong:
return @"Long Password";
case MPSiteTypeGeneratedMedium:
return @"Medium Password";
case MPSiteTypeGeneratedBasic:
return @"Basic Password";
case MPSiteTypeGeneratedShort:
return @"Short Password";
case MPSiteTypeGeneratedPIN:
return @"PIN";
case MPSiteTypeGeneratedName:
return @"Name";
case MPSiteTypeGeneratedPhrase:
return @"Phrase";
case MPSiteTypeStoredPersonal:
return @"Personal Password";
case MPSiteTypeStoredDevicePrivate:
return @"Device Private Password";
}
Throw( @"Type not supported: %lu", (long)type );
}
- (NSString *)shortNameOfType:(MPSiteType)type {
if (!type)
return nil;
switch (type) {
case MPSiteTypeGeneratedMaximum:
return @"Maximum";
case MPSiteTypeGeneratedLong:
return @"Long";
case MPSiteTypeGeneratedMedium:
return @"Medium";
case MPSiteTypeGeneratedBasic:
return @"Basic";
case MPSiteTypeGeneratedShort:
return @"Short";
case MPSiteTypeGeneratedPIN:
return @"PIN";
case MPSiteTypeGeneratedName:
return @"Name";
case MPSiteTypeGeneratedPhrase:
return @"Phrase";
case MPSiteTypeStoredPersonal:
return @"Personal";
case MPSiteTypeStoredDevicePrivate:
return @"Device";
}
Throw( @"Type not supported: %lu", (long)type );
}
- (NSString *)classNameOfType:(MPSiteType)type {
return NSStringFromClass( [self classOfType:type] );
}
- (Class)classOfType:(MPSiteType)type {
if (!type)
Throw( @"No type given." );
switch (type) {
case MPSiteTypeGeneratedMaximum:
return [MPGeneratedSiteEntity class];
case MPSiteTypeGeneratedLong:
return [MPGeneratedSiteEntity class];
case MPSiteTypeGeneratedMedium:
return [MPGeneratedSiteEntity class];
case MPSiteTypeGeneratedBasic:
return [MPGeneratedSiteEntity class];
case MPSiteTypeGeneratedShort:
return [MPGeneratedSiteEntity class];
case MPSiteTypeGeneratedPIN:
return [MPGeneratedSiteEntity class];
case MPSiteTypeGeneratedName:
return [MPGeneratedSiteEntity class];
case MPSiteTypeGeneratedPhrase:
return [MPGeneratedSiteEntity class];
case MPSiteTypeStoredPersonal:
return [MPStoredSiteEntity class];
case MPSiteTypeStoredDevicePrivate:
return [MPStoredSiteEntity class];
}
Throw( @"Type not supported: %lu", (long)type );
}
- (NSArray *)allTypes {
return [self allTypesStartingWith:MPSiteTypeGeneratedPhrase];
}
- (NSArray *)allTypesStartingWith:(MPSiteType)startingType {
NSMutableArray *allTypes = [[NSMutableArray alloc] initWithCapacity:8];
MPSiteType currentType = startingType;
do {
[allTypes addObject:@(currentType)];
} while ((currentType = [self nextType:currentType]) != startingType);
return allTypes;
}
- (MPSiteType)nextType:(MPSiteType)type {
switch (type) {
case MPSiteTypeGeneratedPhrase:
return MPSiteTypeGeneratedName;
case MPSiteTypeGeneratedName:
return MPSiteTypeGeneratedMaximum;
case MPSiteTypeGeneratedMaximum:
return MPSiteTypeGeneratedLong;
case MPSiteTypeGeneratedLong:
return MPSiteTypeGeneratedMedium;
case MPSiteTypeGeneratedMedium:
return MPSiteTypeGeneratedBasic;
case MPSiteTypeGeneratedBasic:
return MPSiteTypeGeneratedShort;
case MPSiteTypeGeneratedShort:
return MPSiteTypeGeneratedPIN;
case MPSiteTypeGeneratedPIN:
return MPSiteTypeStoredPersonal;
case MPSiteTypeStoredPersonal:
return MPSiteTypeStoredDevicePrivate;
case MPSiteTypeStoredDevicePrivate:
return MPSiteTypeGeneratedPhrase;
default:
return MPSiteTypeGeneratedLong;
}
}
- (MPSiteType)previousType:(MPSiteType)type {
MPSiteType previousType = type, nextType = type;
while ((nextType = [self nextType:nextType]) != type)
previousType = nextType;
return previousType;
}
- (NSString *)generateLoginForSiteNamed:(NSString *)name usingKey:(MPKey *)key {
return [self generateContentForSiteNamed:name ofType:MPSiteTypeGeneratedName withCounter:1
variant:MPSiteVariantLogin context:nil usingKey:key];
}
- (NSString *)generatePasswordForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter
usingKey:(MPKey *)key {
return [self generateContentForSiteNamed:name ofType:type withCounter:counter
variant:MPSiteVariantPassword context:nil usingKey:key];
}
- (NSString *)generateAnswerForSiteNamed:(NSString *)name onQuestion:(NSString *)question usingKey:(MPKey *)key {
return [self generateContentForSiteNamed:name ofType:MPSiteTypeGeneratedPhrase withCounter:1
variant:MPSiteVariantAnswer context:question usingKey:key];
}
- (NSString *)generateContentForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter
variant:(MPSiteVariant)variant context:(NSString *)context usingKey:(MPKey *)key {
__block NSString *content;
[self mpw_perform:^{
char const *contentBytes = mpw_passwordForSite( [key keyDataForAlgorithm:self].bytes,
name.UTF8String, type, (uint32_t)counter, variant, context.UTF8String, [self version] );
if (contentBytes) {
content = [NSString stringWithCString:contentBytes encoding:NSUTF8StringEncoding];
mpw_free_string( contentBytes );
}
}];
return content;
}
- (NSString *)storedLoginForSite:(MPStoredSiteEntity *)site usingKey:(MPKey *)key {
return nil;
}
- (NSString *)storedPasswordForSite:(MPStoredSiteEntity *)site usingKey:(MPKey *)key {
return [self decryptContent:site.contentObject usingKey:key];
}
- (BOOL)savePassword:(NSString *)clearContent toSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey {
NSAssert( [[siteKey keyIDForAlgorithm:site.user.algorithm] isEqualToData:site.user.keyID], @"Site does not belong to current user." );
switch (site.type) {
case MPSiteTypeGeneratedMaximum:
case MPSiteTypeGeneratedLong:
case MPSiteTypeGeneratedMedium:
case MPSiteTypeGeneratedBasic:
case MPSiteTypeGeneratedShort:
case MPSiteTypeGeneratedPIN:
case MPSiteTypeGeneratedName:
case MPSiteTypeGeneratedPhrase: {
wrn( @"Cannot save content to site with generated type %lu.", (long)site.type );
return NO;
}
case MPSiteTypeStoredPersonal: {
if (![site isKindOfClass:[MPStoredSiteEntity class]]) {
wrn( @"Site with stored type %lu is not an MPStoredSiteEntity, but a %@.",
(long)site.type, [site class] );
return NO;
}
NSData *encryptionKey = [siteKey keyDataForAlgorithm:self trimmedLength:PearlCryptKeySize];
NSData *encryptedContent = [[clearContent dataUsingEncoding:NSUTF8StringEncoding]
encryptWithSymmetricKey:encryptionKey padding:YES];
if ([((MPStoredSiteEntity *)site).contentObject isEqualToData:encryptedContent])
return NO;
((MPStoredSiteEntity *)site).contentObject = encryptedContent;
return YES;
}
case MPSiteTypeStoredDevicePrivate: {
if (![site isKindOfClass:[MPStoredSiteEntity class]]) {
wrn( @"Site with stored type %lu is not an MPStoredSiteEntity, but a %@.",
(long)site.type, [site class] );
return NO;
}
NSData *encryptionKey = [siteKey keyDataForAlgorithm:self trimmedLength:PearlCryptKeySize];
NSData *encryptedContent = [[clearContent dataUsingEncoding:NSUTF8StringEncoding]
encryptWithSymmetricKey:encryptionKey padding:YES];
NSDictionary *siteQuery = [self queryForDevicePrivateSiteNamed:site.name];
if (!encryptedContent)
[PearlKeyChain deleteItemForQuery:siteQuery];
else
[PearlKeyChain addOrUpdateItemForQuery:siteQuery withAttributes:@{
(__bridge id)kSecValueData : encryptedContent,
#if TARGET_OS_IPHONE
(__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
#endif
}];
((MPStoredSiteEntity *)site).contentObject = nil;
return YES;
}
}
Throw( @"Unsupported type: %ld", (long)site.type );
}
- (NSString *)resolveLoginForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey {
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter( group );
__block NSString *result = nil;
[self resolveLoginForSite:site usingKey:siteKey result:^(NSString *result_) {
result = result_;
dispatch_group_leave( group );
}];
dispatch_group_wait( group, DISPATCH_TIME_FOREVER );
return result;
}
- (NSString *)resolvePasswordForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey {
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter( group );
__block NSString *result = nil;
[self resolvePasswordForSite:site usingKey:siteKey result:^(NSString *result_) {
result = result_;
dispatch_group_leave( group );
}];
dispatch_group_wait( group, DISPATCH_TIME_FOREVER );
return result;
}
- (NSString *)resolveAnswerForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey {
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter( group );
__block NSString *result = nil;
[self resolveAnswerForSite:site usingKey:siteKey result:^(NSString *result_) {
result = result_;
dispatch_group_leave( group );
}];
dispatch_group_wait( group, DISPATCH_TIME_FOREVER );
return result;
}
- (NSString *)resolveAnswerForQuestion:(MPSiteQuestionEntity *)question usingKey:(MPKey *)siteKey {
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter( group );
__block NSString *result = nil;
[self resolveAnswerForQuestion:question usingKey:siteKey result:^(NSString *result_) {
result = result_;
dispatch_group_leave( group );
}];
dispatch_group_wait( group, DISPATCH_TIME_FOREVER );
return result;
}
- (void)resolveLoginForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey result:(void ( ^ )(NSString *result))resultBlock {
NSAssert( [[siteKey keyIDForAlgorithm:site.user.algorithm] isEqualToData:site.user.keyID], @"Site does not belong to current user." );
NSString *name = site.name;
BOOL loginGenerated = site.loginGenerated && [[MPAppDelegate_Shared get] isFeatureUnlocked:MPProductGenerateLogins];
NSString *loginName = site.loginName;
id<MPAlgorithm> algorithm = nil;
if (!name.length)
err( @"Missing name." );
else if (!siteKey)
err( @"Missing key." );
else
algorithm = site.algorithm;
dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
resultBlock( loginName || !loginGenerated? loginName:
[algorithm generateLoginForSiteNamed:name usingKey:siteKey] );
} );
}
- (void)resolvePasswordForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey result:(void ( ^ )(NSString *result))resultBlock {
NSAssert( [[siteKey keyIDForAlgorithm:site.user.algorithm] isEqualToData:site.user.keyID], @"Site does not belong to current user." );
switch (site.type) {
case MPSiteTypeGeneratedMaximum:
case MPSiteTypeGeneratedLong:
case MPSiteTypeGeneratedMedium:
case MPSiteTypeGeneratedBasic:
case MPSiteTypeGeneratedShort:
case MPSiteTypeGeneratedPIN:
case MPSiteTypeGeneratedName:
case MPSiteTypeGeneratedPhrase: {
if (![site isKindOfClass:[MPGeneratedSiteEntity class]]) {
wrn( @"Site with generated type %lu is not an MPGeneratedSiteEntity, but a %@.",
(long)site.type, [site class] );
break;
}
NSString *name = site.name;
MPSiteType type = site.type;
NSUInteger counter = ((MPGeneratedSiteEntity *)site).counter;
id<MPAlgorithm> algorithm = nil;
if (!site.name.length)
err( @"Missing name." );
else if (!siteKey)
err( @"Missing key." );
else
algorithm = site.algorithm;
dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
NSString *result = [algorithm generatePasswordForSiteNamed:name ofType:type withCounter:counter usingKey:siteKey];
resultBlock( result );
} );
break;
}
case MPSiteTypeStoredPersonal: {
if (![site isKindOfClass:[MPStoredSiteEntity class]]) {
wrn( @"Site with stored type %lu is not an MPStoredSiteEntity, but a %@.",
(long)site.type, [site class] );
break;
}
NSData *encryptedContent = ((MPStoredSiteEntity *)site).contentObject;
dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
NSString *result = [self decryptContent:encryptedContent usingKey:siteKey];
resultBlock( result );
} );
break;
}
case MPSiteTypeStoredDevicePrivate: {
NSAssert( [site isKindOfClass:[MPStoredSiteEntity class]],
@"Site with stored type %lu is not an MPStoredSiteEntity, but a %@.", (long)site.type,
[site class] );
NSDictionary *siteQuery = [self queryForDevicePrivateSiteNamed:site.name];
NSData *encryptedContent = [PearlKeyChain dataOfItemForQuery:siteQuery];
dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
NSString *result = [self decryptContent:encryptedContent usingKey:siteKey];
resultBlock( result );
} );
break;
}
}
}
- (void)resolveAnswerForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey result:(void ( ^ )(NSString *result))resultBlock {
NSAssert( [[siteKey keyIDForAlgorithm:site.user.algorithm] isEqualToData:site.user.keyID], @"Site does not belong to current user." );
NSString *name = site.name;
id<MPAlgorithm> algorithm = nil;
if (!site.name.length)
err( @"Missing name." );
else if (!siteKey)
err( @"Missing key." );
else
algorithm = site.algorithm;
dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
NSString *result = [algorithm generateAnswerForSiteNamed:name onQuestion:nil usingKey:siteKey];
resultBlock( result );
} );
}
- (void)resolveAnswerForQuestion:(MPSiteQuestionEntity *)question usingKey:(MPKey *)siteKey
result:(void ( ^ )(NSString *result))resultBlock {
NSAssert( [[siteKey keyIDForAlgorithm:question.site.user.algorithm] isEqualToData:question.site.user.keyID],
@"Site does not belong to current user." );
NSString *name = question.site.name;
NSString *keyword = question.keyword;
id<MPAlgorithm> algorithm = nil;
if (!name.length)
err( @"Missing name." );
else if (!siteKey)
err( @"Missing key." );
else if ([[MPAppDelegate_Shared get] isFeatureUnlocked:MPProductGenerateAnswers])
algorithm = question.site.algorithm;
dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
NSString *result = [algorithm generateAnswerForSiteNamed:name onQuestion:keyword usingKey:siteKey];
resultBlock( result );
} );
}
- (void)importProtectedPassword:(NSString *)protectedContent protectedByKey:(MPKey *)importKey
intoSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey {
NSAssert( [[siteKey keyIDForAlgorithm:site.user.algorithm] isEqualToData:site.user.keyID], @"Site does not belong to current user." );
switch (site.type) {
case MPSiteTypeGeneratedMaximum:
case MPSiteTypeGeneratedLong:
case MPSiteTypeGeneratedMedium:
case MPSiteTypeGeneratedBasic:
case MPSiteTypeGeneratedShort:
case MPSiteTypeGeneratedPIN:
case MPSiteTypeGeneratedName:
case MPSiteTypeGeneratedPhrase:
break;
case MPSiteTypeStoredPersonal: {
if (![site isKindOfClass:[MPStoredSiteEntity class]]) {
wrn( @"Site with stored type %lu is not an MPStoredSiteEntity, but a %@.",
(long)site.type, [site class] );
break;
}
if ([[importKey keyIDForAlgorithm:self] isEqualToData:[siteKey keyIDForAlgorithm:self]])
((MPStoredSiteEntity *)site).contentObject = [protectedContent decodeBase64];
else {
NSString *clearContent = [self decryptContent:[protectedContent decodeBase64] usingKey:importKey];
[self importClearTextPassword:clearContent intoSite:site usingKey:siteKey];
}
break;
}
case MPSiteTypeStoredDevicePrivate:
break;
}
}
- (void)importClearTextPassword:(NSString *)clearContent intoSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey {
NSAssert( [[siteKey keyIDForAlgorithm:site.user.algorithm] isEqualToData:site.user.keyID], @"Site does not belong to current user." );
switch (site.type) {
case MPSiteTypeGeneratedMaximum:
case MPSiteTypeGeneratedLong:
case MPSiteTypeGeneratedMedium:
case MPSiteTypeGeneratedBasic:
case MPSiteTypeGeneratedShort:
case MPSiteTypeGeneratedPIN:
case MPSiteTypeGeneratedName:
case MPSiteTypeGeneratedPhrase:
break;
case MPSiteTypeStoredPersonal: {
[self savePassword:clearContent toSite:site usingKey:siteKey];
break;
}
case MPSiteTypeStoredDevicePrivate:
break;
}
}
- (NSString *)exportPasswordForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey {
NSAssert( [[siteKey keyIDForAlgorithm:site.user.algorithm] isEqualToData:site.user.keyID], @"Site does not belong to current user." );
if (!(site.type & MPSiteFeatureExportContent))
return nil;
NSString *result = nil;
switch (site.type) {
case MPSiteTypeGeneratedMaximum:
case MPSiteTypeGeneratedLong:
case MPSiteTypeGeneratedMedium:
case MPSiteTypeGeneratedBasic:
case MPSiteTypeGeneratedShort:
case MPSiteTypeGeneratedPIN:
case MPSiteTypeGeneratedName:
case MPSiteTypeGeneratedPhrase: {
result = nil;
break;
}
case MPSiteTypeStoredPersonal: {
if (![site isKindOfClass:[MPStoredSiteEntity class]]) {
wrn( @"Site with stored type %lu is not an MPStoredSiteEntity, but a %@.",
(long)site.type, [site class] );
break;
}
result = [((MPStoredSiteEntity *)site).contentObject encodeBase64];
break;
}
case MPSiteTypeStoredDevicePrivate: {
result = nil;
break;
}
}
return result;
}
- (BOOL)migrateExplicitly:(BOOL)explicit {
return NO;
}
- (NSDictionary *)queryForDevicePrivateSiteNamed:(NSString *)name {
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
attributes:@{
(__bridge id)kSecAttrService : @"DevicePrivate",
(__bridge id)kSecAttrAccount : name
}
matches:nil];
}
- (NSString *)decryptContent:(NSData *)encryptedContent usingKey:(MPKey *)key {
if (!key)
return nil;
NSData *decryptedContent = nil;
if ([encryptedContent length]) {
NSData *encryptionKey = [key keyDataForAlgorithm:self trimmedLength:PearlCryptKeySize];
decryptedContent = [encryptedContent decryptWithSymmetricKey:encryptionKey padding:YES];
}
if (!decryptedContent)
return nil;
return [[NSString alloc] initWithBytes:decryptedContent.bytes length:decryptedContent.length encoding:NSUTF8StringEncoding];
}
- (BOOL)timeToCrack:(out TimeToCrack *)timeToCrack passwordOfType:(MPSiteType)type byAttacker:(MPAttacker)attacker {
if (!(type & MPSiteTypeClassGenerated))
return NO;
size_t count = 0;
const char **templates = mpw_templatesForType( type, &count );
if (!templates)
return NO;
BIGNUM *permutations = BN_new(), *templatePermutations = BN_new();
for (size_t t = 0; t < count; ++t) {
const char *template = templates[t];
BN_one( templatePermutations );
for (NSUInteger c = 0; c < strlen( template ); ++c)
BN_mul_word( templatePermutations,
(BN_ULONG)strlen( mpw_charactersInClass( template[c] ) ) );
BN_add( permutations, permutations, templatePermutations );
}
BN_free( templatePermutations );
free( templates );
return [self timeToCrack:timeToCrack permutations:permutations forAttacker:attacker];
}
- (BOOL)timeToCrack:(out TimeToCrack *)timeToCrack passwordString:(NSString *)password byAttacker:(MPAttacker)attacker {
BIGNUM *permutations = BN_new();
BN_one( permutations );
for (NSUInteger c = 0; c < [password length]; ++c) {
const char passwordCharacter = [password substringWithRange:NSMakeRange( c, 1 )].UTF8String[0];
unsigned int characterEntropy = 0;
for (NSString *characterClass in @[ @"v", @"c", @"a", @"x" ]) {
char const *charactersForClass = mpw_charactersInClass( characterClass.UTF8String[0] );
if (strchr( charactersForClass, passwordCharacter )) {
// Found class for password character.
characterEntropy = (BN_ULONG)strlen( charactersForClass );
break;
}
}
if (!characterEntropy)
characterEntropy = 256 /* a byte */;
BN_mul_word( permutations, characterEntropy );
}
return [self timeToCrack:timeToCrack permutations:permutations forAttacker:attacker];
}
- (BOOL)timeToCrack:(out TimeToCrack *)timeToCrack permutations:(BIGNUM *)permutations forAttacker:(MPAttacker)attacker {
// Determine base seconds needed to calculate the permutations.
BIGNUM *secondsToCrack = BN_dup( permutations );
BN_div_word( secondsToCrack, CRACKING_PER_SECOND );
// Modify seconds needed by applying our hardware budget.
switch (attacker) {
case MPAttacker1:
break;
case MPAttacker5K:
BN_mul_word( secondsToCrack, CRACKING_PRICE );
BN_div_word( secondsToCrack, 5000 );
break;
case MPAttacker20M:
BN_mul_word( secondsToCrack, CRACKING_PRICE );
BN_div_word( secondsToCrack, 20000000 );
break;
case MPAttacker5B:
BN_mul_word( secondsToCrack, CRACKING_PRICE );
BN_div_word( secondsToCrack, 5000 );
BN_div_word( secondsToCrack, 1000000 );
break;
}
BIGNUM *max = BN_new();
BN_set_word( max, (BN_ULONG)-1 );
BIGNUM *hoursToCrack = BN_dup( secondsToCrack );
BN_div_word( hoursToCrack, 3600 );
if (BN_cmp( hoursToCrack, max ) < 0)
timeToCrack->hours = BN_get_word( hoursToCrack );
else
timeToCrack->hours = (BN_ULONG)-1;
BIGNUM *daysToCrack = BN_dup( hoursToCrack );
BN_div_word( daysToCrack, 24 );
if (BN_cmp( daysToCrack, max ) < 0)
timeToCrack->days = BN_get_word( daysToCrack );
else
timeToCrack->days = (BN_ULONG)-1;
BIGNUM *weeksToCrack = BN_dup( daysToCrack );
BN_div_word( weeksToCrack, 7 );
if (BN_cmp( weeksToCrack, max ) < 0)
timeToCrack->weeks = BN_get_word( weeksToCrack );
else
timeToCrack->weeks = (BN_ULONG)-1;
BIGNUM *monthsToCrack = BN_dup( daysToCrack );
BN_div_word( monthsToCrack, 31 );
if (BN_cmp( monthsToCrack, max ) < 0)
timeToCrack->months = BN_get_word( monthsToCrack );
else
timeToCrack->months = (BN_ULONG)-1;
BIGNUM *yearsToCrack = BN_dup( daysToCrack );
BN_div_word( yearsToCrack, 356 );
if (BN_cmp( yearsToCrack, max ) < 0)
timeToCrack->years = BN_get_word( yearsToCrack );
else
timeToCrack->years = (BN_ULONG)-1;
BIGNUM *universesToCrack = BN_dup( yearsToCrack );
BN_div_word( universesToCrack, 14000 );
BN_div_word( universesToCrack, 1000000 );
if (BN_cmp( universesToCrack, max ) < 0)
timeToCrack->universes = BN_get_word( universesToCrack );
else
timeToCrack->universes = (BN_ULONG)-1;
for (unsigned long error = ERR_get_error(); error; error = ERR_get_error())
err( @"bignum error: %lu", error );
BN_free( max );
BN_free( permutations );
BN_free( secondsToCrack );
BN_free( hoursToCrack );
BN_free( daysToCrack );
BN_free( weeksToCrack );
BN_free( monthsToCrack );
BN_free( yearsToCrack );
BN_free( universesToCrack );
return YES;
}
@end

View File

@@ -0,0 +1,21 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPAlgorithmV1
//
// Created by Maarten Billemont on 17/07/12.
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
//
#import "MPAlgorithmV0.h"
@interface MPAlgorithmV1 : MPAlgorithmV0
@end

View File

@@ -0,0 +1,48 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPAlgorithmV1
//
// Created by Maarten Billemont on 17/07/12.
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
//
#import "MPAlgorithmV1.h"
#import "MPEntities.h"
@implementation MPAlgorithmV1
- (MPAlgorithmVersion)version {
return MPAlgorithmVersion1;
}
- (BOOL)tryMigrateSite:(MPSiteEntity *)site explicit:(BOOL)explicit {
if ([site.algorithm version] != [self version] - 1)
// Only migrate from previous version.
return NO;
if (!explicit) {
if (site.type & MPSiteTypeClassGenerated) {
// This migration requires explicit permission for types of the generated class.
site.requiresExplicitMigration = YES;
return NO;
}
}
// Apply migration.
site.requiresExplicitMigration = NO;
site.algorithm = self;
return YES;
}
@end

View File

@@ -0,0 +1,21 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPAlgorithmV2
//
// Created by Maarten Billemont on 17/07/12.
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
//
#import "MPAlgorithmV1.h"
@interface MPAlgorithmV2 : MPAlgorithmV1
@end

View File

@@ -0,0 +1,48 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPAlgorithmV2
//
// Created by Maarten Billemont on 17/07/12.
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
//
#import "MPAlgorithmV2.h"
#import "MPEntities.h"
@implementation MPAlgorithmV2
- (MPAlgorithmVersion)version {
return MPAlgorithmVersion2;
}
- (BOOL)tryMigrateSite:(MPSiteEntity *)site explicit:(BOOL)explicit {
if ([site.algorithm version] != [self version] - 1)
// Only migrate from previous version.
return NO;
if (!explicit) {
if (site.type & MPSiteTypeClassGenerated && site.name.length != [site.name dataUsingEncoding:NSUTF8StringEncoding].length) {
// This migration requires explicit permission for types of the generated class.
site.requiresExplicitMigration = YES;
return NO;
}
}
// Apply migration.
site.requiresExplicitMigration = NO;
site.algorithm = self;
return YES;
}
@end

View File

@@ -0,0 +1,21 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPAlgorithmV3
//
// Created by Maarten Billemont on 13/01/15.
// Copyright 2015 lhunath (Maarten Billemont). All rights reserved.
//
#import "MPAlgorithmV2.h"
@interface MPAlgorithmV3 : MPAlgorithmV2
@end

View File

@@ -0,0 +1,49 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPAlgorithmV3
//
// Created by Maarten Billemont on 13/01/15.
// Copyright 2015 lhunath (Maarten Billemont). All rights reserved.
//
#import "MPAlgorithmV3.h"
#import "MPEntities.h"
@implementation MPAlgorithmV3
- (MPAlgorithmVersion)version {
return MPAlgorithmVersion3;
}
- (BOOL)tryMigrateSite:(MPSiteEntity *)site explicit:(BOOL)explicit {
if ([site.algorithm version] != [self version] - 1)
// Only migrate from previous version.
return NO;
if (!explicit) {
if (site.type & MPSiteTypeClassGenerated &&
site.user.name.length != [site.user.name dataUsingEncoding:NSUTF8StringEncoding].length) {
// This migration requires explicit permission for types of the generated class.
site.requiresExplicitMigration = YES;
return NO;
}
}
// Apply migration.
site.requiresExplicitMigration = NO;
site.algorithm = self;
return YES;
}
@end

View File

@@ -0,0 +1,39 @@
//
// MPAppDelegate_Key.h
// MasterPassword
//
// Created by Maarten Billemont on 24/11/11.
// Copyright (c) 2011 Lyndir. All rights reserved.
//
#import <StoreKit/StoreKit.h>
#import "MPAppDelegate_Shared.h"
#define MPProductGenerateLogins @"com.lyndir.masterpassword.products.generatelogins"
#define MPProductGenerateAnswers @"com.lyndir.masterpassword.products.generateanswers"
#define MPProductOSIntegration @"com.lyndir.masterpassword.products.osintegration"
#define MPProductTouchID @"com.lyndir.masterpassword.products.touchid"
#define MPProductFuel @"com.lyndir.masterpassword.products.fuel"
#define MP_FUEL_HOURLY_RATE 30.f /* Tier 1 purchases/h ~> USD/h */
@protocol MPInAppDelegate
- (void)updateWithProducts:(NSArray /* SKProduct */ *)products;
- (void)updateWithTransaction:(SKPaymentTransaction *)transaction;
@end
@interface MPAppDelegate_Shared(InApp)
- (void)registerProductsObserver:(id<MPInAppDelegate>)delegate;
- (void)removeProductsObserver:(id<MPInAppDelegate>)delegate;
- (void)reloadProducts;
- (BOOL)canMakePayments;
- (BOOL)isFeatureUnlocked:(NSString *)productIdentifier;
- (void)restoreCompletedTransactions;
- (void)purchaseProductWithIdentifier:(NSString *)productIdentifier quantity:(NSInteger)quantity;
@end

View File

@@ -0,0 +1,181 @@
//
// MPAppDelegate.m
// MasterPassword
//
// Created by Maarten Billemont on 24/11/11.
// Copyright (c) 2011 Lyndir. All rights reserved.
//
#import "MPAppDelegate_InApp.h"
@interface MPAppDelegate_Shared(InApp_Private)<SKProductsRequestDelegate, SKPaymentTransactionObserver>
@end
@implementation MPAppDelegate_Shared(InApp)
PearlAssociatedObjectProperty( NSArray*, Products, products );
PearlAssociatedObjectProperty( NSMutableArray*, ProductObservers, productObservers );
- (void)registerProductsObserver:(id<MPInAppDelegate>)delegate {
if (!self.productObservers)
self.productObservers = [NSMutableArray array];
[self.productObservers addObject:delegate];
if (self.products)
[delegate updateWithProducts:self.products];
else
[self reloadProducts];
}
- (void)removeProductsObserver:(id<MPInAppDelegate>)delegate {
[self.productObservers removeObject:delegate];
}
- (void)reloadProducts {
SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:
[[NSSet alloc] initWithObjects:MPProductGenerateLogins, MPProductGenerateAnswers, MPProductTouchID, MPProductFuel, nil]];
productsRequest.delegate = self;
[productsRequest start];
}
- (SKPaymentQueue *)paymentQueue {
static dispatch_once_t once = 0;
dispatch_once( &once, ^{
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
} );
return [SKPaymentQueue defaultQueue];
}
- (BOOL)canMakePayments {
return [SKPaymentQueue canMakePayments];
}
- (BOOL)isFeatureUnlocked:(NSString *)productIdentifier {
if (![productIdentifier length])
// Missing a product.
return NO;
if ([productIdentifier isEqualToString:MPProductFuel])
// Consumable product.
return NO;
#if ADHOC || DEBUG
// All features are unlocked for beta / debug / mac versions.
return YES;
#else
// Check if product is purchased.
return [[NSUserDefaults standardUserDefaults] objectForKey:productIdentifier] != nil;
#endif
}
- (void)restoreCompletedTransactions {
[[self paymentQueue] restoreCompletedTransactions];
}
- (void)purchaseProductWithIdentifier:(NSString *)productIdentifier quantity:(NSInteger)quantity {
#if TARGET_OS_IPHONE
if (![[MPAppDelegate_Shared get] canMakePayments]) {
[PearlAlert showAlertWithTitle:@"Store Not Set Up" message:
@"Try logging using the App Store or from Settings."
viewStyle:UIAlertViewStyleDefault initAlert:nil
tappedButtonBlock:nil cancelTitle:@"Thanks" otherTitles:nil];
return;
}
#endif
for (SKProduct *product in self.products)
if ([product.productIdentifier isEqualToString:productIdentifier]) {
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
payment.quantity = quantity;
[[self paymentQueue] addPayment:payment];
return;
}
}
#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
inf( @"products: %@, invalid: %@", response.products, response.invalidProductIdentifiers );
self.products = response.products;
for (id<MPInAppDelegate> productObserver in self.productObservers)
[productObserver updateWithProducts:self.products];
}
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
#if TARGET_OS_IPHONE
[PearlAlert showAlertWithTitle:@"Purchase Failed" message:
strf( @"%@\n\n%@", error.localizedDescription,
@"Ensure you are online and try logging out and back into iTunes from your device's Settings." )
viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:nil
cancelTitle:@"OK" otherTitles:nil];
#else
#endif
err( @"StoreKit request (%@) failed: %@", request, [error fullDescription] );
}
- (void)requestDidFinish:(SKRequest *)request {
dbg( @"StoreKit request (%@) finished.", request );
}
#pragma mark - SKPaymentTransactionObserver
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
for (SKPaymentTransaction *transaction in transactions) {
dbg( @"transaction updated: %@ -> %d", transaction.payment.productIdentifier, (int)(transaction.transactionState) );
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchased: {
inf( @"purchased: %@", transaction.payment.productIdentifier );
if ([transaction.payment.productIdentifier isEqualToString:MPProductFuel]) {
float currentFuel = [[MPiOSConfig get].developmentFuelRemaining floatValue];
float purchasedFuel = transaction.payment.quantity / MP_FUEL_HOURLY_RATE;
[MPiOSConfig get].developmentFuelRemaining = @(currentFuel + purchasedFuel);
if (![MPiOSConfig get].developmentFuelChecked || currentFuel < DBL_EPSILON)
[MPiOSConfig get].developmentFuelChecked = [NSDate date];
}
[[NSUserDefaults standardUserDefaults] setObject:transaction.transactionIdentifier
forKey:transaction.payment.productIdentifier];
[queue finishTransaction:transaction];
break;
}
case SKPaymentTransactionStateRestored: {
inf( @"restored: %@", transaction.payment.productIdentifier );
[[NSUserDefaults standardUserDefaults] setObject:transaction.transactionIdentifier
forKey:transaction.payment.productIdentifier];
[queue finishTransaction:transaction];
break;
}
case SKPaymentTransactionStatePurchasing:
case SKPaymentTransactionStateDeferred:
break;
case SKPaymentTransactionStateFailed:
err( @"Transaction failed: %@, reason: %@", transaction.payment.productIdentifier, [transaction.error fullDescription] );
[queue finishTransaction:transaction];
break;
}
for (id<MPInAppDelegate> productObserver in self.productObservers)
[productObserver updateWithTransaction:transaction];
}
if (![[NSUserDefaults standardUserDefaults] synchronize])
wrn( @"Couldn't synchronize after transaction updates." );
}
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error {
err( @"StoreKit restore failed: %@", [error fullDescription] );
}
@end

View File

@@ -0,0 +1,19 @@
//
// MPAppDelegate_Key.h
// MasterPassword
//
// Created by Maarten Billemont on 24/11/11.
// Copyright (c) 2011 Lyndir. All rights reserved.
//
#import "MPAppDelegate_Shared.h"
@interface MPAppDelegate_Shared(Key)
- (BOOL)signInAsUser:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc usingMasterPassword:(NSString *)password;
- (void)signOutAnimated:(BOOL)animated;
- (void)storeSavedKeyFor:(MPUserEntity *)user;
- (void)forgetSavedKeyFor:(MPUserEntity *)user;
@end

View File

@@ -0,0 +1,269 @@
//
// MPAppDelegate.m
// MasterPassword
//
// Created by Maarten Billemont on 24/11/11.
// Copyright (c) 2011 Lyndir. All rights reserved.
//
#import "MPAppDelegate_Key.h"
#import "MPAppDelegate_Store.h"
@interface MPAppDelegate_Shared()
@property(strong, nonatomic) MPKey *key;
@end
@implementation MPAppDelegate_Shared(Key)
static NSDictionary *createKeyQuery(MPUserEntity *user, BOOL newItem, MPKeyOrigin *keyOrigin) {
#if TARGET_OS_IPHONE
if (user.touchID && kSecUseAuthenticationUI) {
if (keyOrigin)
*keyOrigin = MPKeyOriginKeyChainBiometric;
CFErrorRef acError = NULL;
id accessControl = (__bridge_transfer id)SecAccessControlCreateWithFlags( kCFAllocatorDefault,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlTouchIDCurrentSet, &acError );
if (!accessControl || acError)
err( @"Could not use TouchID on this device: %@", acError );
else
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
attributes:@{
(__bridge id)kSecAttrService : @"Saved Master Password",
(__bridge id)kSecAttrAccount : user.name?: @"",
(__bridge id)kSecAttrAccessControl : accessControl,
(__bridge id)kSecUseAuthenticationUI : (__bridge id)kSecUseAuthenticationUIAllow,
(__bridge id)kSecUseOperationPrompt :
strf( @"Access %@'s master password.", user.name ),
}
matches:nil];
}
#endif
if (keyOrigin)
*keyOrigin = MPKeyOriginKeyChain;
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
attributes:@{
(__bridge id)kSecAttrService : @"Saved Master Password",
(__bridge id)kSecAttrAccount : user.name?: @"",
#if TARGET_OS_IPHONE
(__bridge id)kSecAttrAccessible : (__bridge id)(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly?: kSecAttrAccessibleWhenUnlockedThisDeviceOnly),
#endif
}
matches:nil];
}
- (MPKey *)loadSavedKeyFor:(MPUserEntity *)user {
MPKeyOrigin keyOrigin;
NSDictionary *keyQuery = createKeyQuery( user, NO, &keyOrigin );
NSData *keyData = [PearlKeyChain dataOfItemForQuery:keyQuery];
if (!keyData) {
inf( @"No key found in keychain for user: %@", user.userID );
return nil;
}
inf( @"Found key in keychain for user: %@", user.userID );
return [[MPKey alloc] initForFullName:user.name withKeyData:keyData forAlgorithm:user.algorithm keyOrigin:keyOrigin];
}
- (void)storeSavedKeyFor:(MPUserEntity *)user {
if (user.saveKey) {
NSData *keyData = [self.key keyDataForAlgorithm:user.algorithm];
if (keyData) {
[self forgetSavedKeyFor:user];
inf( @"Saving key in keychain for user: %@", user.userID );
[PearlKeyChain addOrUpdateItemForQuery:createKeyQuery( user, YES, nil )
withAttributes:@{ (__bridge id)kSecValueData : keyData }];
}
}
}
- (void)forgetSavedKeyFor:(MPUserEntity *)user {
OSStatus result = [PearlKeyChain deleteItemForQuery:createKeyQuery( user, NO, nil )];
if (result == noErr) {
inf( @"Removed key from keychain for user: %@", user.userID );
[[NSNotificationCenter defaultCenter] postNotificationName:MPKeyForgottenNotification object:self];
}
}
- (void)signOutAnimated:(BOOL)animated {
if (self.key)
self.key = nil;
self.activeUser = nil;
[[NSNotificationCenter defaultCenter] postNotificationName:MPSignedOutNotification object:self userInfo:@{ @"animated" : @(animated) }];
}
- (BOOL)signInAsUser:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc usingMasterPassword:(NSString *)password {
NSAssert( ![NSThread isMainThread], @"Authentication should not happen on the main thread." );
if (!user)
return NO;
MPKey *tryKey = nil;
// Method 1: When the user has no keyID set, set a new key from the given master password.
if (!user.keyID) {
if ([password length] && (tryKey = [[MPKey alloc] initForFullName:user.name withMasterPassword:password])) {
user.keyID = [tryKey keyIDForAlgorithm:MPAlgorithmDefault];
// Migrate existing sites.
[self migrateSitesForUser:user saveInContext:moc toKey:tryKey];
}
}
// Method 2: Depending on the user's saveKey, load or remove the key from the keychain.
if (!user.saveKey)
// Key should not be stored in keychain. Delete it.
[self forgetSavedKeyFor:user];
else if (!tryKey) {
// Key should be saved in keychain. Load it.
if ((tryKey = [self loadSavedKeyFor:user]) && ![user.keyID isEqual:[tryKey keyIDForAlgorithm:user.algorithm]]) {
// Loaded password doesn't match user's keyID. Forget saved password: it is incorrect.
inf( @"Saved password doesn't match keyID for user: %@", user.userID );
trc( @"user keyID: %@ (version: %d) != authentication keyID: %@",
user.keyID, user.algorithm.version, [tryKey keyIDForAlgorithm:user.algorithm] );
tryKey = nil;
[self forgetSavedKeyFor:user];
}
}
// Method 3: Check the given master password string.
if (!tryKey && [password length] && (tryKey = [[MPKey alloc] initForFullName:user.name withMasterPassword:password]) &&
![user.keyID isEqual:[tryKey keyIDForAlgorithm:user.algorithm]]) {
inf( @"Key derived from password doesn't match keyID for user: %@", user.userID );
trc( @"user keyID: %@ (version: %u) != authentication keyID: %@",
user.keyID, user.algorithm.version, [tryKey keyIDForAlgorithm:user.algorithm] );
tryKey = nil;
}
// No more methods left, fail if key still not known.
if (!tryKey) {
if (password)
inf( @"Password login failed for user: %@", user.userID );
else
dbg( @"Automatic login failed for user: %@", user.userID );
return NO;
}
inf( @"Logged in user: %@", user.userID );
if (![self.key isEqualToKey:tryKey]) {
// Upgrade the user's keyID if not at the default version yet.
if (user.algorithm.version != MPAlgorithmDefaultVersion) {
user.algorithm = MPAlgorithmDefault;
user.keyID = [tryKey keyIDForAlgorithm:user.algorithm];
inf( @"Upgraded keyID to version %u for user: %@", user.algorithm.version, user.userID );
}
self.key = tryKey;
// Update the key chain if necessary.
[self storeSavedKeyFor:user];
}
@try {
if ([[MPConfig get].sendInfo boolValue]) {
#ifdef CRASHLYTICS
[[Crashlytics sharedInstance] setObjectValue:user.userID forKey:@"username"];
[[Crashlytics sharedInstance] setUserName:user.userID];
#endif
}
}
@catch (id exception) {
err( @"While setting username: %@", exception );
}
user.lastUsed = [NSDate date];
self.activeUser = user;
[moc saveToStore];
// Perform a data sanity check now that we're logged in as the user to allow fixes that require the user's key.
if ([[MPConfig get].checkInconsistency boolValue])
[MPAppDelegate_Shared managedObjectContextPerformBlockAndWait:^(NSManagedObjectContext *context) {
[self findAndFixInconsistenciesSaveInContext:context];
}];
[[NSNotificationCenter defaultCenter] postNotificationName:MPSignedInNotification object:self];
return YES;
}
- (void)migrateSitesForUser:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc toKey:(MPKey *)newKey {
if (![user.sites count])
// Nothing to migrate.
return;
MPKey *recoverKey = newKey;
#ifdef PEARL_UIKIT
PearlOverlay *activityOverlay = [PearlOverlay showProgressOverlayWithTitle:PearlString( @"Migrating %ld sites...",
(long)[user.sites count] )];
#endif
for (MPSiteEntity *site in user.sites) {
if (site.type & MPSiteTypeClassStored) {
NSString *content;
while (!(content = [site.algorithm storedPasswordForSite:(MPStoredSiteEntity *)site usingKey:recoverKey])) {
// Failed to decrypt site with the current recoveryKey. Ask user for a new one to use.
__block NSString *masterPassword = nil;
#ifdef PEARL_UIKIT
dispatch_group_t recoverPasswordGroup = dispatch_group_create();
dispatch_group_enter( recoverPasswordGroup );
[PearlAlert showAlertWithTitle:@"Enter Old Master Password"
message:PearlString( @"Your old master password is required to migrate the stored password for %@",
site.name )
viewStyle:UIAlertViewStyleSecureTextInput
initAlert:nil tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
@try {
if (buttonIndex_ == [alert_ cancelButtonIndex])
// Don't Migrate
return;
masterPassword = [alert_ textFieldAtIndex:0].text;
}
@finally {
dispatch_group_leave( recoverPasswordGroup );
}
} cancelTitle:@"Don't Migrate" otherTitles:@"Migrate", nil];
dispatch_group_wait( recoverPasswordGroup, DISPATCH_TIME_FOREVER );
#endif
if (!masterPassword)
// Don't Migrate
break;
recoverKey = [[MPKey alloc] initForFullName:user.name withMasterPassword:masterPassword];
}
if (!content)
// Don't Migrate
break;
if (![recoverKey isEqualToKey:newKey])
[site.algorithm savePassword:content toSite:site usingKey:newKey];
}
}
[moc saveToStore];
#ifdef PEARL_UIKIT
[activityOverlay cancelOverlayAnimated:YES];
#endif
}
@end

View File

@@ -0,0 +1,28 @@
//
// MPAppDelegate_Shared.h
// MasterPassword
//
// Created by Maarten Billemont on 24/11/11.
// Copyright (c) 2011 Lyndir. All rights reserved.
//
#import "MPEntities.h"
#if TARGET_OS_IPHONE
@interface MPAppDelegate_Shared : PearlAppDelegate
#else
@interface MPAppDelegate_Shared : NSObject <PearlConfigDelegate>
#endif
@property(strong, nonatomic, readonly) MPKey *key;
@property(strong, nonatomic, readonly) NSManagedObjectID *activeUserOID;
@property(strong, nonatomic, readonly) NSPersistentStoreCoordinator *storeCoordinator;
+ (instancetype)get;
- (MPUserEntity *)activeUserForMainThread;
- (MPUserEntity *)activeUserInContext:(NSManagedObjectContext *)context;
- (void)setActiveUser:(MPUserEntity *)activeUser;
- (void)handleCoordinatorError:(NSError *)error;
@end

View File

@@ -0,0 +1,78 @@
//
// MPAppDelegate.m
// MasterPassword
//
// Created by Maarten Billemont on 24/11/11.
// Copyright (c) 2011 Lyndir. All rights reserved.
//
#import "MPAppDelegate_Shared.h"
#import "MPAppDelegate_Store.h"
#import "MPAppDelegate_Key.h"
#import "NSManagedObjectModel+KCOrderedAccessorFix.h"
@interface MPAppDelegate_Shared ()
@property(strong, nonatomic) MPKey *key;
@property(strong, nonatomic) NSManagedObjectID *activeUserOID;
@property(strong, nonatomic) NSPersistentStoreCoordinator *storeCoordinator;
@end
@implementation MPAppDelegate_Shared
+ (MPAppDelegate_Shared *)get {
#if TARGET_OS_IPHONE
return (MPAppDelegate_Shared *)UIApp.delegate;
#elif defined (__MAC_OS_X_VERSION_MIN_REQUIRED)
return (MPAppDelegate_Shared *)[NSApplication sharedApplication].delegate;
#else
#error Unsupported OS.
#endif
}
- (instancetype)init {
if (!(self = [super init]))
return nil;
NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:nil];
[model kc_generateOrderedSetAccessors];
self.storeCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
return self;
}
- (MPUserEntity *)activeUserForMainThread {
return [self activeUserInContext:[MPAppDelegate_Shared managedObjectContextForMainThreadIfReady]];
}
- (MPUserEntity *)activeUserInContext:(NSManagedObjectContext *)context {
NSManagedObjectID *activeUserOID = self.activeUserOID;
if (!activeUserOID || !context)
return nil;
MPUserEntity *activeUser = [MPUserEntity existingObjectWithID:activeUserOID inContext:context];
if (!activeUser)
[self signOutAnimated:YES];
return activeUser;
}
- (void)setActiveUser:(MPUserEntity *)activeUser {
NSError *error;
if (activeUser.objectID.isTemporaryID && ![activeUser.managedObjectContext obtainPermanentIDsForObjects:@[ activeUser ] error:&error])
err(@"Failed to obtain a permanent object ID after setting active user: %@", [error fullDescription]);
self.activeUserOID = activeUser.objectID;
}
- (void)handleCoordinatorError:(NSError *)error {
}
@end

View File

@@ -0,0 +1,40 @@
//
// MPAppDelegate_Key.h
// MasterPassword
//
// Created by Maarten Billemont on 24/11/11.
// Copyright (c) 2011 Lyndir. All rights reserved.
//
#import "MPAppDelegate_Shared.h"
#import "MPFixable.h"
typedef NS_ENUM( NSUInteger, MPImportResult ) {
MPImportResultSuccess,
MPImportResultCancelled,
MPImportResultInvalidPassword,
MPImportResultMalformedInput,
MPImportResultInternalError,
};
@interface MPAppDelegate_Shared(Store)
+ (NSManagedObjectContext *)managedObjectContextForMainThreadIfReady;
+ (BOOL)managedObjectContextForMainThreadPerformBlock:(void (^)(NSManagedObjectContext *mainContext))mocBlock;
+ (BOOL)managedObjectContextForMainThreadPerformBlockAndWait:(void (^)(NSManagedObjectContext *mainContext))mocBlock;
+ (BOOL)managedObjectContextPerformBlock:(void (^)(NSManagedObjectContext *context))mocBlock;
+ (BOOL)managedObjectContextPerformBlockAndWait:(void (^)(NSManagedObjectContext *context))mocBlock;
- (MPFixableResult)findAndFixInconsistenciesSaveInContext:(NSManagedObjectContext *)context;
- (void)deleteAndResetStore;
/** @param completion The block to execute after adding the site, executed from the main thread with the new site in the main MOC. */
- (void)addSiteNamed:(NSString *)siteName completion:(void ( ^ )(MPSiteEntity *site, NSManagedObjectContext *context))completion;
- (MPSiteEntity *)changeSite:(MPSiteEntity *)site saveInContext:(NSManagedObjectContext *)context toType:(MPSiteType)type;
- (MPImportResult)importSites:(NSString *)importedSitesString
askImportPassword:(NSString *(^)(NSString *userName))importPassword
askUserPassword:(NSString *(^)(NSString *userName, NSUInteger importCount, NSUInteger deleteCount))userPassword;
- (NSString *)exportSitesRevealPasswords:(BOOL)revealPasswords;
@end

View File

@@ -0,0 +1,833 @@
//
// MPAppDelegate.m
// MasterPassword
//
// Created by Maarten Billemont on 24/11/11.
// Copyright (c) 2011 Lyndir. All rights reserved.
//
#import "MPAppDelegate_Store.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;
}
- (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;
// When privateManagedObjectContext is saved, import the changes into mainManagedObjectContext.
PearlAddNotificationObserverTo( self.mainManagedObjectContext, NSManagedObjectContextDidSaveNotification,
self.privateManagedObjectContext, nil, ^(NSManagedObjectContext *mainManagedObjectContext, NSNotification *note) {
[mainManagedObjectContext performBlock:^{
@try {
[mainManagedObjectContext 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]) {
err( @"Couldn't create our application support directory: %@", [error fullDescription] );
return;
}
if (![self.storeCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[self localStoreURL]
options:@{
NSMigratePersistentStoresAutomaticallyOption : @YES,
NSInferMappingModelAutomaticallyOption : @YES,
STORE_OPTIONS
} error:&error]) {
err( @"Failed to open store: %@", [error fullDescription] );
self.storeCorrupted = @YES;
[self handleCoordinatorError:error];
return;
}
self.storeCorrupted = @NO;
#if TARGET_OS_IPHONE
PearlAddNotificationObserver( UIApplicationWillResignActiveNotification, UIApp, [NSOperationQueue mainQueue],
#else
PearlAddNotificationObserver( NSApplicationWillResignActiveNotification, NSApp, [NSOperationQueue mainQueue],
#endif
^(MPAppDelegate_Shared *self, NSNotification *note) {
[self.mainManagedObjectContext saveToStore];
} );
// 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])
err( @"Couldn't remove persistence store from coordinator: %@", [error fullDescription] );
}
if (![[NSFileManager defaultManager] removeItemAtURL:self.localStoreURL error:&error])
err( @"Couldn't remove persistence store at URL %@: %@", self.localStoreURL, [error fullDescription] );
[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) {
err( @"Failed to fetch %@ objects: %@", entity, [error fullDescription] );
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 = (signed)[[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 }
model:nil error:&error]) {
err( @"Couldn't migrate the old store to the new location: %@", [error fullDescription] );
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
} model:nil error:&error]) {
err( @"Couldn't migrate the old store to the new location: %@", [error fullDescription] );
return NO;
}
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;
}
MPSiteType 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;
NSError *error = nil;
if (site.objectID.isTemporaryID && ![context obtainPermanentIDsForObjects:@[ site ] error:&error])
err( @"Failed to obtain a permanent object ID after creating new site: %@", [error fullDescription] );
[context saveToStore];
completion( site, context );
}];
}
- (MPSiteEntity *)changeSite:(MPSiteEntity *)site saveInContext:(NSManagedObjectContext *)context toType:(MPSiteType)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;
NSError *error = nil;
if (![context obtainPermanentIDsForObjects:@[ newSite ] error:&error])
err( @"Failed to obtain a permanent object ID after changing object type: %@", [error fullDescription] );
[context deleteObject:site];
[context saveToStore];
[[NSNotificationCenter defaultCenter] postNotificationName:MPSiteUpdatedNotification object:site.objectID];
site = newSite;
}
[[NSNotificationCenter defaultCenter] postNotificationName:MPSiteUpdatedNotification object:site.objectID];
return site;
}
- (MPImportResult)importSites:(NSString *)importedSitesString
askImportPassword:(NSString *( ^ )(NSString *userName))importPassword
askUserPassword:(NSString *( ^ )(NSString *userName, NSUInteger importCount, NSUInteger deleteCount))userPassword {
NSAssert( ![[NSThread currentThread] isMainThread], @"This method should not be invoked from the main thread." );
__block MPImportResult result = MPImportResultCancelled;
do {
if ([MPAppDelegate_Shared managedObjectContextPerformBlockAndWait:^(NSManagedObjectContext *context) {
result = [self importSites:importedSitesString askImportPassword:importPassword askUserPassword:userPassword
saveInContext:context];
}])
break;
usleep( (useconds_t)(USEC_PER_SEC * 0.2) );
} while (YES);
return result;
}
- (MPImportResult)importSites:(NSString *)importedSitesString
askImportPassword:(NSString *( ^ )(NSString *userName))askImportPassword
askUserPassword:(NSString *( ^ )(NSString *userName, NSUInteger importCount, NSUInteger deleteCount))askUserPassword
saveInContext:(NSManagedObjectContext *)context {
// Compile patterns.
static NSRegularExpression *headerPattern;
static NSArray *sitePatterns;
NSError *error = nil;
if (!headerPattern) {
headerPattern = [[NSRegularExpression alloc]
initWithPattern:@"^#[[:space:]]*([^:]+): (.*)"
options:(NSRegularExpressionOptions)0 error:&error];
if (error) {
err( @"Error loading the header pattern: %@", [error fullDescription] );
return MPImportResultInternalError;
}
}
if (!sitePatterns) {
sitePatterns = @[
[[NSRegularExpression alloc] // Format 0
initWithPattern:@"^([^ ]+) +([[:digit:]]+) +([[:digit:]]+)(:[[:digit:]]+)? +([^\t]+)\t(.*)"
options:(NSRegularExpressionOptions)0 error:&error],
[[NSRegularExpression alloc] // Format 1
initWithPattern:@"^([^ ]+) +([[:digit:]]+) +([[:digit:]]+)(:[[:digit:]]+)?(:[[:digit:]]+)? +([^\t]*)\t *([^\t]+)\t(.*)"
options:(NSRegularExpressionOptions)0 error:&error]
];
if (error) {
err( @"Error loading the site patterns: %@", [error fullDescription] );
return MPImportResultInternalError;
}
}
// Parse import data.
inf( @"Importing sites." );
__block MPUserEntity *user = nil;
id<MPAlgorithm> importAlgorithm = nil;
NSUInteger importFormat = 0;
NSUInteger importAvatar = NSNotFound;
NSString *importBundleVersion = nil, *importUserName = nil;
NSData *importKeyID = nil;
BOOL headerStarted = NO, headerEnded = NO, clearText = NO;
NSArray *importedSiteLines = [importedSitesString componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
NSMutableSet *sitesToDelete = [NSMutableSet set];
NSMutableArray *importedSiteSites = [NSMutableArray arrayWithCapacity:[importedSiteLines count]];
NSFetchRequest *siteFetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPSiteEntity 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;
}
// Header
if ([headerPattern numberOfMatchesInString:importedSiteLine options:(NSMatchingOptions)0
range:NSMakeRange( 0, [importedSiteLine length] )] != 1) {
err( @"Invalid header format in line: %@", importedSiteLine );
return MPImportResultMalformedInput;
}
NSTextCheckingResult *headerSites = [[headerPattern matchesInString:importedSiteLine options:(NSMatchingOptions)0
range:NSMakeRange( 0, [importedSiteLine length] )] lastObject];
NSString *headerName = [importedSiteLine substringWithRange:[headerSites rangeAtIndex:1]];
NSString *headerValue = [importedSiteLine substringWithRange:[headerSites rangeAtIndex:2]];
if ([headerName isEqualToString:@"User Name"]) {
importUserName = headerValue;
NSFetchRequest *userFetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPUserEntity class] )];
userFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@", importUserName];
NSArray *users = [context executeFetchRequest:userFetchRequest error:&error];
if (!users) {
err( @"While looking for user: %@, error: %@", importUserName, [error fullDescription] );
return MPImportResultInternalError;
}
if ([users count] > 1) {
err( @"While looking for user: %@, found more than one: %lu", importUserName, (unsigned long)[users count] );
return MPImportResultInternalError;
}
user = [users lastObject];
dbg( @"Existing user? %@", [user debugDescription] );
}
if ([headerName isEqualToString:@"Key ID"])
importKeyID = [headerValue decodeHex];
if ([headerName isEqualToString:@"Version"]) {
importBundleVersion = headerValue;
importAlgorithm = MPAlgorithmDefaultForBundleVersion( importBundleVersion );
}
if ([headerName isEqualToString:@"Format"]) {
importFormat = (NSUInteger)[headerValue integerValue];
if (importFormat >= [sitePatterns count]) {
err( @"Unsupported import format: %lu", (unsigned long)importFormat );
return MPImportResultInternalError;
}
}
if ([headerName isEqualToString:@"Avatar"])
importAvatar = (NSUInteger)[headerValue integerValue];
if ([headerName isEqualToString:@"Passwords"]) {
if ([headerValue isEqualToString:@"VISIBLE"])
clearText = YES;
}
continue;
}
if (!headerEnded)
continue;
if (![importUserName length])
return MPImportResultMalformedInput;
if (![importedSiteLine length])
continue;
// Site
NSRegularExpression *sitePattern = sitePatterns[importFormat];
if ([sitePattern numberOfMatchesInString:importedSiteLine options:(NSMatchingOptions)0
range:NSMakeRange( 0, [importedSiteLine length] )] != 1) {
err( @"Invalid site format in line: %@", importedSiteLine );
return MPImportResultMalformedInput;
}
NSTextCheckingResult *siteElements = [[sitePattern matchesInString:importedSiteLine options:(NSMatchingOptions)0
range:NSMakeRange( 0, [importedSiteLine length] )] lastObject];
NSString *lastUsed, *uses, *type, *version, *counter, *siteName, *loginName, *exportContent;
switch (importFormat) {
case 0:
lastUsed = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:1]];
uses = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:2]];
type = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:3]];
version = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:4]];
if ([version length])
version = [version substringFromIndex:1]; // Strip the leading colon.
counter = @"";
loginName = @"";
siteName = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:5]];
exportContent = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:6]];
break;
case 1:
lastUsed = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:1]];
uses = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:2]];
type = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:3]];
version = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:4]];
if ([version length])
version = [version substringFromIndex:1]; // Strip the leading colon.
counter = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:5]];
if ([counter length])
counter = [counter substringFromIndex:1]; // Strip the leading colon.
loginName = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:6]];
siteName = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:7]];
exportContent = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:8]];
break;
default:
err( @"Unexpected import format: %lu", (unsigned long)importFormat );
return MPImportResultInternalError;
}
// Find existing site.
if (user) {
siteFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@ AND user == %@", siteName, user];
NSArray *existingSites = [context executeFetchRequest:siteFetchRequest error:&error];
if (!existingSites) {
err( @"Lookup of existing sites failed for site: %@, user: %@, error: %@", siteName, user.userID, [error fullDescription] );
return MPImportResultInternalError;
}
if ([existingSites count]) {
dbg( @"Existing sites: %@", existingSites );
[sitesToDelete addObjectsFromArray:existingSites];
}
}
[importedSiteSites addObject:@[ lastUsed, uses, type, version, counter, loginName, siteName, exportContent ]];
dbg( @"Will import site: lastUsed=%@, uses=%@, type=%@, version=%@, counter=%@, loginName=%@, siteName=%@, exportContent=%@",
lastUsed, uses, type, version, counter, loginName, siteName, exportContent );
}
// Ask for confirmation to import these sites and the master password of the user.
inf( @"Importing %lu sites, deleting %lu sites, for user: %@", (unsigned long)[importedSiteSites count],
(unsigned long)[sitesToDelete count], [MPUserEntity idFor:importUserName] );
NSString *userMasterPassword = askUserPassword( user? user.name: importUserName, [importedSiteSites count],
[sitesToDelete count] );
if (!userMasterPassword) {
inf( @"Import cancelled." );
return MPImportResultCancelled;
}
MPKey *userKey = [[MPKey alloc] initForFullName:user? user.name: importUserName withMasterPassword:userMasterPassword];
if (user && ![[userKey keyIDForAlgorithm:user.algorithm] isEqualToData:user.keyID])
return MPImportResultInvalidPassword;
__block MPKey *importKey = userKey;
if (importKeyID && ![[importKey keyIDForAlgorithm:importAlgorithm] isEqualToData:importKeyID])
importKey = [[MPKey alloc] initForFullName:importUserName withMasterPassword:askImportPassword( importUserName )];
if (importKeyID && ![[importKey keyIDForAlgorithm:importAlgorithm] isEqualToData:importKeyID])
return MPImportResultInvalidPassword;
// Delete existing sites.
if (sitesToDelete.count)
[sitesToDelete enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {
inf( @"Deleting site: %@, it will be replaced by an imported site.", [obj name] );
[context deleteObject:obj];
}];
// Make sure there is a user.
if (user) {
if (importAvatar != NSNotFound)
user.avatar = importAvatar;
dbg( @"Updating User: %@", [user debugDescription] );
}
else {
user = [MPUserEntity insertNewObjectInContext:context];
user.name = importUserName;
user.algorithm = MPAlgorithmDefault;
user.keyID = [userKey keyIDForAlgorithm:user.algorithm];
if (importAvatar != NSNotFound)
user.avatar = importAvatar;
dbg( @"Created User: %@", [user debugDescription] );
}
// Import new sites.
for (NSArray *siteElements in importedSiteSites) {
NSDate *lastUsed = [[NSDateFormatter rfc3339DateFormatter] dateFromString:siteElements[0]];
NSUInteger uses = (unsigned)[siteElements[1] integerValue];
MPSiteType type = (MPSiteType)[siteElements[2] integerValue];
MPAlgorithmVersion version = (MPAlgorithmVersion)[siteElements[3] integerValue];
NSUInteger counter = [siteElements[4] length]? (unsigned)[siteElements[4] integerValue]: NSNotFound;
NSString *loginName = [siteElements[5] length]? siteElements[5]: nil;
NSString *siteName = siteElements[6];
NSString *exportContent = siteElements[7];
// Create new site.
id<MPAlgorithm> algorithm = MPAlgorithmForVersion( version );
Class entityType = [algorithm classOfType:type];
if (!entityType) {
err( @"Invalid site type in import file: %@ has type %lu", siteName, (long)type );
return MPImportResultInternalError;
}
MPSiteEntity *site = (MPSiteEntity *)[entityType insertNewObjectInContext:context];
site.name = siteName;
site.loginName = loginName;
site.user = user;
site.type = type;
site.uses = uses;
site.lastUsed = lastUsed;
site.algorithm = algorithm;
if ([exportContent length]) {
if (clearText)
[site.algorithm importClearTextPassword:exportContent intoSite:site usingKey:userKey];
else
[site.algorithm importProtectedPassword:exportContent protectedByKey:importKey intoSite:site usingKey:userKey];
}
if ([site isKindOfClass:[MPGeneratedSiteEntity class]] && counter != NSNotFound)
((MPGeneratedSiteEntity *)site).counter = counter;
dbg( @"Created Site: %@", [site debugDescription] );
}
if (![context saveToStore])
return MPImportResultInternalError;
inf( @"Import completed successfully." );
[[NSNotificationCenter defaultCenter] postNotificationName:MPSitesImportedNotification object:nil userInfo:@{
MPSitesImportedNotificationUserKey : user
}];
return MPImportResultSuccess;
}
- (NSString *)exportSitesRevealPasswords:(BOOL)revealPasswords {
MPUserEntity *activeUser = [self activeUserForMainThread];
inf( @"Exporting sites, %@, for user: %@", revealPasswords? @"revealing passwords": @"omitting passwords", activeUser.userID );
// Header.
NSMutableString *export = [NSMutableString new];
[export appendFormat:@"# Master Password site export\n"];
if (revealPasswords)
[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:@"# User Name: %@\n", activeUser.name];
[export appendFormat:@"# Avatar: %lu\n", (unsigned long)activeUser.avatar];
[export appendFormat:@"# Key ID: %@\n", [activeUser.keyID encodeHex]];
[export appendFormat:@"# Date: %@\n", [[NSDateFormatter rfc3339DateFormatter] stringFromDate:[NSDate date]]];
[export appendFormat:@"# Version: %@\n", [PearlInfoPlist get].CFBundleVersion];
[export appendFormat:@"# Format: 1\n"];
if (revealPasswords)
[export appendFormat:@"# Passwords: VISIBLE\n"];
else
[export appendFormat:@"# Passwords: PROTECTED\n"];
[export appendFormat:@"##\n"];
[export appendFormat:@"#\n"];
[export appendFormat:@"# Last Times Password Login\t Site\tSite\n"];
[export appendFormat:@"# used used type name\t name\tpassword\n"];
// Sites.
for (MPSiteEntity *site in activeUser.sites) {
NSDate *lastUsed = site.lastUsed;
NSUInteger uses = site.uses;
MPSiteType type = site.type;
id<MPAlgorithm> algorithm = site.algorithm;
NSUInteger counter = 0;
NSString *loginName = site.loginName;
NSString *siteName = site.name;
NSString *content = nil;
// Generated-specific
if ([site isKindOfClass:[MPGeneratedSiteEntity class]])
counter = ((MPGeneratedSiteEntity *)site).counter;
// Determine the content to export.
if (!(type & MPSiteFeatureDevicePrivate)) {
if (revealPasswords)
content = [site.algorithm resolvePasswordForSite:site usingKey:self.key];
else if (type & MPSiteFeatureExportContent)
content = [site.algorithm exportPasswordForSite:site usingKey:self.key];
}
NSString *lastUsedExport = [[NSDateFormatter rfc3339DateFormatter] stringFromDate:lastUsed];
long usesExport = (long)uses;
NSString *typeExport = strf( @"%lu:%lu:%lu", (long)type, (long)[algorithm version], (long)counter );
NSString *loginNameExport = loginName?: @"";
NSString *contentExport = content?: @"";
[export appendFormat:@"%@ %8ld %8S %25S\t%25S\t%@\n",
lastUsedExport, usesExport,
(const unsigned short *)[typeExport cStringUsingEncoding:NSUTF16StringEncoding],
(const unsigned short *)[loginNameExport cStringUsingEncoding:NSUTF16StringEncoding],
(const unsigned short *)[siteName cStringUsingEncoding:NSUTF16StringEncoding],
contentExport];
}
return export;
}
@end

View File

@@ -0,0 +1,21 @@
//
// MPConfig.h
// MasterPassword
//
// Created by Maarten Billemont on 02/01/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "Pearl.h"
@interface MPConfig : PearlConfig
@property(nonatomic, retain) NSNumber *sendInfo;
@property(nonatomic, retain) NSNumber *rememberLogin;
@property(nonatomic, retain) NSNumber *hidePasswords;
@property(nonatomic, retain) NSNumber *checkInconsistency;
@property(nonatomic, strong) NSNumber *siteAttacker;
@end

View File

@@ -0,0 +1,35 @@
//
// MPConfig.m
// MasterPassword
//
// Created by Maarten Billemont on 02/01/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "MPAppDelegate_Shared.h"
@implementation MPConfig
@dynamic sendInfo, rememberLogin, checkInconsistency, hidePasswords, siteAttacker;
- (id)init {
if (!(self = [super init]))
return nil;
[self.defaults registerDefaults:@{
NSStringFromSelector( @selector( askForReviews ) ) : @YES,
NSStringFromSelector( @selector( sendInfo ) ) : @NO,
NSStringFromSelector( @selector( rememberLogin ) ) : @NO,
NSStringFromSelector( @selector( hidePasswords ) ) : @NO,
NSStringFromSelector( @selector( checkInconsistency ) ) : @NO,
NSStringFromSelector( @selector( siteAttacker ) ) : @(MPAttacker1),
}];
self.delegate = [MPAppDelegate_Shared get];
return self;
}
@end

View File

@@ -0,0 +1,73 @@
//
// MPEntities.h
// MasterPassword-iOS
//
// Created by Maarten Billemont on 31/05/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "MPSiteEntity.h"
#import "MPStoredSiteEntity.h"
#import "MPGeneratedSiteEntity.h"
#import "MPUserEntity.h"
#import "MPAlgorithm.h"
#import "MPFixable.h"
#define MPAvatarCount 19
@interface NSManagedObjectContext(MP)
- (BOOL)saveToStore;
@end
@interface MPSiteQuestionEntity(MP)
- (NSString *)resolveQuestionAnswerUsingKey:(MPKey *)key;
- (void)resolveQuestionAnswerUsingKey:(MPKey *)key result:(void ( ^ )(NSString *))result;
@end
@interface MPSiteEntity(MP)<MPFixable>
@property(assign) BOOL loginGenerated;
@property(assign) MPSiteType type;
@property(readonly) NSString *typeName;
@property(readonly) NSString *typeShortName;
@property(readonly) NSString *typeClassName;
@property(readonly) Class typeClass;
@property(assign) NSUInteger uses;
@property(assign) BOOL requiresExplicitMigration;
@property(strong) id<MPAlgorithm> algorithm;
- (NSUInteger)use;
- (BOOL)tryMigrateExplicitly:(BOOL)explicit;
- (NSString *)resolveLoginUsingKey:(MPKey *)key;
- (NSString *)resolvePasswordUsingKey:(MPKey *)key;
- (NSString *)resolveSiteAnswerUsingKey:(MPKey *)key;
- (void)resolveLoginUsingKey:(MPKey *)key result:(void ( ^ )(NSString *))result;
- (void)resolvePasswordUsingKey:(MPKey *)key result:(void ( ^ )(NSString *))result;
- (void)resolveSiteAnswerUsingKey:(MPKey *)key result:(void ( ^ )(NSString *))result;
- (void)resolveAnswerUsingKey:(MPKey *)key forQuestion:(NSString *)question result:(void ( ^ )(NSString *))result;
@end
@interface MPGeneratedSiteEntity(MP)
@property(assign) NSUInteger counter;
@end
@interface MPUserEntity(MP)
@property(assign) NSUInteger avatar;
@property(assign) BOOL saveKey;
@property(assign) BOOL touchID;
@property(assign) MPSiteType defaultType;
@property(readonly) NSString *userID;
@property(strong) id<MPAlgorithm> algorithm;
+ (NSString *)idFor:(NSString *)userName;
@end

View File

@@ -0,0 +1,364 @@
//
// MPEntities.m
// MasterPassword-iOS
//
// Created by Maarten Billemont on 31/05/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "MPEntities.h"
#import "MPAppDelegate_Shared.h"
#import "MPAppDelegate_Key.h"
#import "MPAppDelegate_InApp.h"
@implementation NSManagedObjectContext(MP)
- (BOOL)saveToStore {
__block BOOL success = YES;
if ([self hasChanges]) {
[self performBlockAndWait:^{
@try {
NSError *error = nil;
if (!(success = [self save:&error]))
err( @"While saving: %@", [error fullDescription] );
}
@catch (NSException *exception) {
success = NO;
err( @"While saving: %@", [exception fullDescription] );
}
}];
}
return success && (!self.parentContext || [self.parentContext saveToStore]);
}
@end
@implementation MPSiteQuestionEntity(MP)
- (NSString *)resolveQuestionAnswerUsingKey:(MPKey *)key {
return [self.site.algorithm resolveAnswerForQuestion:self usingKey:key];
}
- (void)resolveQuestionAnswerUsingKey:(MPKey *)key result:(void ( ^ )(NSString *))result {
[self.site.algorithm resolveAnswerForQuestion:self usingKey:key result:result];
}
@end
@implementation MPSiteEntity(MP)
- (MPFixableResult)findAndFixInconsistenciesInContext:(NSManagedObjectContext *)context {
return MPFixableResultNoProblems;
}
- (MPSiteType)type {
return (MPSiteType)[self.type_ unsignedIntegerValue];
}
- (void)setLoginGenerated:(BOOL)aLoginGenerated {
self.loginGenerated_ = @(aLoginGenerated);
}
- (BOOL)loginGenerated {
return [self.loginGenerated_ boolValue];
}
- (void)setType:(MPSiteType)aType {
self.type_ = @(aType);
}
- (NSString *)typeName {
return [self.algorithm nameOfType:self.type];
}
- (NSString *)typeShortName {
return [self.algorithm shortNameOfType:self.type];
}
- (NSString *)typeClassName {
return [self.algorithm classNameOfType:self.type];
}
- (Class)typeClass {
return [self.algorithm classOfType:self.type];
}
- (NSUInteger)uses {
return [self.uses_ unsignedIntegerValue];
}
- (void)setUses:(NSUInteger)anUses {
self.uses_ = @(anUses);
}
- (id<MPAlgorithm>)algorithm {
return MPAlgorithmForVersion(
MIN( MPAlgorithmVersionCurrent,
MAX( MPAlgorithmVersion0, (MPAlgorithmVersion)[self.version_ unsignedIntegerValue] ) ) );
}
- (void)setAlgorithm:(id<MPAlgorithm>)algorithm {
self.version_ = @([algorithm version]);
}
- (BOOL)requiresExplicitMigration {
return [self.requiresExplicitMigration_ boolValue];
}
- (void)setRequiresExplicitMigration:(BOOL)requiresExplicitMigration {
self.requiresExplicitMigration_ = @(requiresExplicitMigration);
}
- (NSUInteger)use {
self.lastUsed = [NSDate date];
return ++self.uses;
}
- (NSString *)description {
return strf( @"%@:%@", [self class], [self name] );
}
- (NSString *)debugDescription {
__block NSString *debugDescription = strf( @"{%@: [recursing]}", [self class] );
static BOOL recursing = NO;
PearlIfNotRecursing( &recursing, ^{
@try {
debugDescription = strf(
@"{%@: name=%@, user=%@, type=%lu, uses=%ld, lastUsed=%@, version=%ld, loginName=%@, requiresExplicitMigration=%d}",
NSStringFromClass( [self class] ), self.name, self.user.name, (long)self.type, (long)self.uses, self.lastUsed,
(long)[self.algorithm version],
self.loginName, self.requiresExplicitMigration );
}
@catch (NSException *exception) {
debugDescription = strf( @"{%@: inaccessible: %@}",
NSStringFromClass( [self class] ), [exception fullDescription] );
}
} );
return debugDescription;
}
- (BOOL)tryMigrateExplicitly:(BOOL)explicit {
MPAlgorithmVersion algorithmVersion;
while ((algorithmVersion = [self.algorithm version]) < MPAlgorithmDefaultVersion) {
MPAlgorithmVersion toVersion = algorithmVersion + 1;
if (![MPAlgorithmForVersion( toVersion ) tryMigrateSite:self explicit:explicit]) {
wrn( @"%@ migration to version: %ld failed for site: %@",
explicit? @"Explicit": @"Automatic", (long)toVersion, self );
return NO;
}
inf( @"%@ migration to version: %ld succeeded for site: %@",
explicit? @"Explicit": @"Automatic", (long)toVersion, self );
}
return YES;
}
- (NSString *)resolveLoginUsingKey:(MPKey *)key {
return [self.algorithm resolveLoginForSite:self usingKey:key];
}
- (NSString *)resolvePasswordUsingKey:(MPKey *)key {
return [self.algorithm resolvePasswordForSite:self usingKey:key];
}
- (NSString *)resolveSiteAnswerUsingKey:(MPKey *)key {
return [self.algorithm resolveAnswerForSite:self usingKey:key];
}
- (void)resolveLoginUsingKey:(MPKey *)key result:(void ( ^ )(NSString *))result {
[self.algorithm resolveLoginForSite:self usingKey:key result:result];
}
- (void)resolvePasswordUsingKey:(MPKey *)key result:(void ( ^ )(NSString *))result {
[self.algorithm resolvePasswordForSite:self usingKey:key result:result];
}
- (void)resolveSiteAnswerUsingKey:(MPKey *)key result:(void ( ^ )(NSString *))result {
[self.algorithm resolveAnswerForSite:self usingKey:key result:result];
}
- (void)resolveAnswerUsingKey:(MPKey *)key forQuestion:(NSString *)question result:(void ( ^ )(NSString *))result {
MPSiteQuestionEntity *questionEntity = [MPSiteQuestionEntity new];
questionEntity.site = self;
questionEntity.keyword = question;
[questionEntity resolveQuestionAnswerUsingKey:key result:result];
}
@end
@implementation MPGeneratedSiteEntity(MP)
- (MPFixableResult)findAndFixInconsistenciesInContext:(NSManagedObjectContext *)context {
MPFixableResult result = [super findAndFixInconsistenciesInContext:context];
if (!self.type || self.type == (MPSiteType)NSNotFound || ![[self.algorithm allTypes] containsObject:self.type_])
// Invalid self.type
result = MPApplyFix( result, ^MPFixableResult {
wrn( @"Invalid type for: %@ of %@, type: %ld. Will use %ld instead.",
self.name, self.user.name, (long)self.type, (long)self.user.defaultType );
self.type = self.user.defaultType;
return MPFixableResultProblemsFixed;
} );
if (!self.type || self.type == (MPSiteType)NSNotFound || ![[self.algorithm allTypes] containsObject:self.type_])
// Invalid self.user.defaultType
result = MPApplyFix( result, ^MPFixableResult {
wrn( @"Invalid type for: %@ of %@, type: %ld. Will use %ld instead.",
self.name, self.user.name, (long)self.type, (long)MPSiteTypeGeneratedLong );
self.type = MPSiteTypeGeneratedLong;
return MPFixableResultProblemsFixed;
} );
if (![self isKindOfClass:[self.algorithm classOfType:self.type]])
// Mismatch between self.type and self.class
result = MPApplyFix( result, ^MPFixableResult {
for (MPSiteType newType = self.type; self.type != (newType = [self.algorithm nextType:newType]);)
if ([self isKindOfClass:[self.algorithm classOfType:newType]]) {
wrn( @"Mismatching type for: %@ of %@, type: %lu, class: %@. Will use %ld instead.",
self.name, self.user.name, (long)self.type, self.class, (long)newType );
self.type = newType;
return MPFixableResultProblemsFixed;
}
err( @"Mismatching type for: %@ of %@, type: %lu, class: %@. Couldn't find a type to fix problem with.",
self.name, self.user.name, (long)self.type, self.class );
return MPFixableResultProblemsNotFixed;
} );
return result;
}
- (NSUInteger)counter {
return [self.counter_ unsignedIntegerValue];
}
- (void)setCounter:(NSUInteger)aCounter {
self.counter_ = @(aCounter);
}
@end
@implementation MPStoredSiteEntity(MP)
- (MPFixableResult)findAndFixInconsistenciesInContext:(NSManagedObjectContext *)context {
MPFixableResult result = [super findAndFixInconsistenciesInContext:context];
if (self.contentObject && ![self.contentObject isKindOfClass:[NSData class]])
result = MPApplyFix( result, ^MPFixableResult {
MPKey *key = [MPAppDelegate_Shared get].key;
if (key && [[MPAppDelegate_Shared get] activeUserInContext:context] == self.user) {
wrn( @"Content object not encrypted for: %@ of %@. Will re-encrypt.", self.name, self.user.name );
[self.algorithm savePassword:[self.contentObject description] toSite:self usingKey:key];
return MPFixableResultProblemsFixed;
}
err( @"Content object not encrypted for: %@ of %@. Couldn't fix, please sign in.", self.name, self.user.name );
return MPFixableResultProblemsNotFixed;
} );
return result;
}
@end
@implementation MPUserEntity(MP)
- (NSUInteger)avatar {
return [self.avatar_ unsignedIntegerValue];
}
- (void)setAvatar:(NSUInteger)anAvatar {
self.avatar_ = @(anAvatar);
}
- (BOOL)saveKey {
return [self.saveKey_ boolValue];
}
- (void)setSaveKey:(BOOL)aSaveKey {
self.saveKey_ = @(aSaveKey);
}
- (BOOL)touchID {
return [self.touchID_ boolValue] && [[MPAppDelegate_Shared get] isFeatureUnlocked:MPProductTouchID];
}
- (void)setTouchID:(BOOL)aTouchID {
self.touchID_ = @(aTouchID);
}
- (MPSiteType)defaultType {
return (MPSiteType)[self.defaultType_ unsignedIntegerValue]?: MPSiteTypeGeneratedLong;
}
- (void)setDefaultType:(MPSiteType)aDefaultType {
self.defaultType_ = @(aDefaultType);
}
- (id<MPAlgorithm>)algorithm {
return MPAlgorithmForVersion(
MIN( MPAlgorithmVersionCurrent,
MAX( MPAlgorithmVersion0, (MPAlgorithmVersion)[self.version_ unsignedIntegerValue] ) ) );
}
- (void)setAlgorithm:(id<MPAlgorithm>)version {
self.version_ = @([version version]);
[[MPAppDelegate_Shared get] forgetSavedKeyFor:self];
}
- (NSString *)userID {
return [MPUserEntity idFor:self.name];
}
+ (NSString *)idFor:(NSString *)userName {
return [[userName hashWith:PearlHashSHA1] encodeHex];
}
@end

View File

@@ -0,0 +1,33 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPFixable.h
// MPFixable
//
// Created by lhunath on 2014-04-26.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import <Foundation/Foundation.h>
typedef NS_ENUM( NSUInteger, MPFixableResult ) {
MPFixableResultNoProblems,
MPFixableResultProblemsFixed,
MPFixableResultProblemsNotFixed,
};
MPFixableResult MPApplyFix(MPFixableResult previousResult, MPFixableResult(^fixBlock)(void));
@protocol MPFixable<NSObject>
- (MPFixableResult)findAndFixInconsistenciesInContext:(NSManagedObjectContext *)context;
@end

View File

@@ -0,0 +1,40 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPFixable.m
// MPFixable
//
// Created by lhunath on 2014-04-26.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import "MPFixable.h"
MPFixableResult MPApplyFix(MPFixableResult previousResult, MPFixableResult(^fixBlock)(void)) {
MPFixableResult additionalResult = fixBlock();
switch (previousResult) {
case MPFixableResultNoProblems:
return additionalResult;
case MPFixableResultProblemsFixed:
switch (additionalResult) {
case MPFixableResultNoProblems:
case MPFixableResultProblemsFixed:
return previousResult;
case MPFixableResultProblemsNotFixed:
return additionalResult;
}
case MPFixableResultProblemsNotFixed:
return additionalResult;
}
Throw( @"Unexpected previous=%ld or additional=%ld result.", (long)previousResult, (long)additionalResult );
}

View File

@@ -0,0 +1,18 @@
//
// MPGeneratedSiteEntity.h
// MasterPassword-Mac
//
// Created by Maarten Billemont on 2014-09-21.
// Copyright (c) 2014 Lyndir. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#import "MPSiteEntity.h"
@interface MPGeneratedSiteEntity : MPSiteEntity
@property (nonatomic, retain) NSNumber * counter_;
@end

View File

@@ -0,0 +1,16 @@
//
// MPGeneratedSiteEntity.m
// MasterPassword-Mac
//
// Created by Maarten Billemont on 2014-09-21.
// Copyright (c) 2014 Lyndir. All rights reserved.
//
#import "MPGeneratedSiteEntity.h"
@implementation MPGeneratedSiteEntity
@dynamic counter_;
@end

View File

@@ -0,0 +1,44 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPKey
//
// Created by Maarten Billemont on 16/07/12.
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
//
#import <Foundation/Foundation.h>
#import "MPAlgorithm.h"
@protocol MPAlgorithm;
typedef NS_ENUM(NSUInteger, MPKeyOrigin) {
MPKeyOriginMasterPassword,
MPKeyOriginKeyChain,
MPKeyOriginKeyChainBiometric,
};
@interface MPKey : NSObject
@property(nonatomic, readonly) NSString *fullName;
@property(nonatomic, readonly) MPKeyOrigin origin;
- (instancetype)initForFullName:(NSString *)fullName withMasterPassword:(NSString *)masterPassword;
- (instancetype)initForFullName:(NSString *)fullName withKeyData:(NSData *)keyData
forAlgorithm:(id<MPAlgorithm>)algorithm keyOrigin:(MPKeyOrigin)origin;
- (NSData *)keyIDForAlgorithm:(id<MPAlgorithm>)algorithm;
- (NSData *)keyDataForAlgorithm:(id<MPAlgorithm>)algorithm;
- (NSData *)keyDataForAlgorithm:(id<MPAlgorithm>)algorithm trimmedLength:(NSUInteger)subKeyLength;
- (BOOL)isEqualToKey:(MPKey *)key;
@end

View File

@@ -0,0 +1,94 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPKey
//
// Created by Maarten Billemont on 16/07/12.
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
//
#import "MPAlgorithm.h"
@interface MPKey()
@property(nonatomic) NSString *fullName;
@property(nonatomic) MPKeyOrigin origin;
@property(nonatomic) NSString *masterPassword;
@end
@implementation MPKey {
NSCache *_keyCache;
};
- (instancetype)initForFullName:(NSString *)fullName withMasterPassword:(NSString *)masterPassword {
if (!(self = [super init]))
return nil;
_keyCache = [NSCache new];
self.fullName = fullName;
self.origin = MPKeyOriginMasterPassword;
self.masterPassword = masterPassword;
return self;
}
- (instancetype)initForFullName:(NSString *)fullName withKeyData:(NSData *)keyData
forAlgorithm:(id<MPAlgorithm>)algorithm keyOrigin:(MPKeyOrigin)origin {
if (!(self = [self initForFullName:fullName withMasterPassword:nil]))
return nil;
self.origin = origin;
[_keyCache setObject:keyData forKey:algorithm];
return self;
}
- (NSData *)keyIDForAlgorithm:(id<MPAlgorithm>)algorithm {
return [algorithm keyIDForKeyData:[self keyDataForAlgorithm:algorithm]];
}
- (NSData *)keyDataForAlgorithm:(id<MPAlgorithm>)algorithm {
NSData *keyData = [_keyCache objectForKey:algorithm];
if (keyData)
return keyData;
keyData = [algorithm keyDataForFullName:self.fullName withMasterPassword:self.masterPassword];
if (keyData)
[_keyCache setObject:keyData forKey:algorithm];
return keyData;
}
- (NSData *)keyDataForAlgorithm:(id<MPAlgorithm>)algorithm trimmedLength:(NSUInteger)subKeyLength {
NSData *keyData = [self keyDataForAlgorithm:algorithm];
return [keyData subdataWithRange:NSMakeRange( 0, MIN( subKeyLength, keyData.length ) )];
}
- (BOOL)isEqualToKey:(MPKey *)key {
return [self.fullName isEqualToString:key.fullName] && [self.masterPassword isEqualToString:self.masterPassword];
}
- (BOOL)isEqual:(id)object {
if (![object isKindOfClass:[MPKey class]])
return NO;
return [self isEqualToKey:object];
}
@end

View File

@@ -0,0 +1,41 @@
//
// MPSiteEntity.h
// MasterPassword-Mac
//
// Created by Maarten Billemont on 2014-09-21.
// Copyright (c) 2014 Lyndir. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
@class MPSiteQuestionEntity, MPUserEntity;
@interface MPSiteEntity : NSManagedObject
//@property (nonatomic, retain) id content; // Hide here, reveal in MPStoredSiteEntity
@property (nonatomic, retain) NSDate * lastUsed;
@property (nonatomic, retain) NSNumber * loginGenerated_;
@property (nonatomic, retain) NSString * loginName;
@property (nonatomic, retain) NSString * name;
@property (nonatomic, retain) NSNumber * requiresExplicitMigration_;
@property (nonatomic, retain) NSNumber * type_;
@property (nonatomic, retain) NSNumber * uses_;
@property (nonatomic, retain) NSNumber * version_;
@property (nonatomic, retain) NSOrderedSet *questions;
@property (nonatomic, retain) MPUserEntity *user;
@end
@interface MPSiteEntity (CoreDataGeneratedAccessors)
- (void)insertObject:(MPSiteQuestionEntity *)value inQuestionsAtIndex:(NSUInteger)idx;
- (void)removeObjectFromQuestionsAtIndex:(NSUInteger)idx;
- (void)insertQuestions:(NSArray *)value atIndexes:(NSIndexSet *)indexes;
- (void)removeQuestionsAtIndexes:(NSIndexSet *)indexes;
- (void)replaceObjectInQuestionsAtIndex:(NSUInteger)idx withObject:(MPSiteQuestionEntity *)value;
- (void)replaceQuestionsAtIndexes:(NSIndexSet *)indexes withQuestions:(NSArray *)values;
- (void)addQuestionsObject:(MPSiteQuestionEntity *)value;
- (void)removeQuestionsObject:(MPSiteQuestionEntity *)value;
- (void)addQuestions:(NSOrderedSet *)values;
- (void)removeQuestions:(NSOrderedSet *)values;
@end

View File

@@ -0,0 +1,28 @@
//
// MPSiteEntity.m
// MasterPassword-Mac
//
// Created by Maarten Billemont on 2014-09-21.
// Copyright (c) 2014 Lyndir. All rights reserved.
//
#import "MPSiteEntity.h"
#import "MPSiteQuestionEntity.h"
#import "MPUserEntity.h"
@implementation MPSiteEntity
//@dynamic content;
@dynamic lastUsed;
@dynamic loginGenerated_;
@dynamic loginName;
@dynamic name;
@dynamic requiresExplicitMigration_;
@dynamic type_;
@dynamic uses_;
@dynamic version_;
@dynamic questions;
@dynamic user;
@end

View File

@@ -0,0 +1,19 @@
//
// MPSiteQuestionEntity.h
// MasterPassword-iOS
//
// Created by Maarten Billemont on 2014-09-27.
// Copyright (c) 2014 Lyndir. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
@class MPSiteEntity;
@interface MPSiteQuestionEntity : NSManagedObject
@property (nonatomic, retain) NSString * keyword;
@property (nonatomic, retain) MPSiteEntity *site;
@end

View File

@@ -0,0 +1,18 @@
//
// MPSiteQuestionEntity.m
// MasterPassword-iOS
//
// Created by Maarten Billemont on 2014-09-27.
// Copyright (c) 2014 Lyndir. All rights reserved.
//
#import "MPSiteQuestionEntity.h"
#import "MPSiteEntity.h"
@implementation MPSiteQuestionEntity
@dynamic keyword;
@dynamic site;
@end

View File

@@ -0,0 +1,18 @@
//
// MPStoredSiteEntity.h
// MasterPassword-Mac
//
// Created by Maarten Billemont on 2014-09-21.
// Copyright (c) 2014 Lyndir. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#import "MPSiteEntity.h"
@interface MPStoredSiteEntity : MPSiteEntity
@property (nonatomic, retain) NSData *contentObject;
@end

View File

@@ -0,0 +1,16 @@
//
// MPStoredSiteEntity.m
// MasterPassword-Mac
//
// Created by Maarten Billemont on 2014-09-21.
// Copyright (c) 2014 Lyndir. All rights reserved.
//
#import "MPStoredSiteEntity.h"
@implementation MPStoredSiteEntity
@dynamic contentObject;
@end

View File

@@ -0,0 +1,22 @@
//
// MPTypes.h
// MasterPassword
//
// Created by Maarten Billemont on 02/01/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
__BEGIN_DECLS
extern NSString *const MPErrorDomain;
extern NSString *const MPSignedInNotification;
extern NSString *const MPSignedOutNotification;
extern NSString *const MPKeyForgottenNotification;
extern NSString *const MPSiteUpdatedNotification;
extern NSString *const MPCheckConfigNotification;
extern NSString *const MPSitesImportedNotification;
extern NSString *const MPFoundInconsistenciesNotification;
extern NSString *const MPSitesImportedNotificationUserKey;
extern NSString *const MPInconsistenciesFixResultUserKey;
__END_DECLS

View File

@@ -0,0 +1,22 @@
//
// MPTypes.c
// MasterPassword
//
// Created by Maarten Billemont on 02/01/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "MPTypes.h"
NSString *const MPErrorDomain = @"MPErrorDomain";
NSString *const MPSignedInNotification = @"MPSignedInNotification";
NSString *const MPSignedOutNotification = @"MPSignedOutNotification";
NSString *const MPKeyForgottenNotification = @"MPKeyForgottenNotification";
NSString *const MPSiteUpdatedNotification = @"MPSiteUpdatedNotification";
NSString *const MPCheckConfigNotification = @"MPCheckConfigNotification";
NSString *const MPSitesImportedNotification = @"MPSitesImportedNotification";
NSString *const MPFoundInconsistenciesNotification = @"MPFoundInconsistenciesNotification";
NSString *const MPSitesImportedNotificationUserKey = @"MPSitesImportedNotificationUserKey";
NSString *const MPInconsistenciesFixResultUserKey = @"MPInconsistenciesFixResultUserKey";

View File

@@ -0,0 +1,34 @@
//
// MPUserEntity.h
// MasterPassword-Mac
//
// Created by Maarten Billemont on 2014-09-21.
// Copyright (c) 2014 Lyndir. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
@class MPSiteEntity;
@interface MPUserEntity : NSManagedObject
@property (nonatomic, retain) NSNumber * avatar_;
@property (nonatomic, retain) NSNumber * defaultType_;
@property (nonatomic, retain) NSData * keyID;
@property (nonatomic, retain) NSDate * lastUsed;
@property (nonatomic, retain) NSString * name;
@property (nonatomic, retain) NSNumber * saveKey_;
@property (nonatomic, retain) NSNumber * touchID_;
@property (nonatomic, retain) NSNumber * version_;
@property (nonatomic, retain) NSSet *sites;
@end
@interface MPUserEntity (CoreDataGeneratedAccessors)
- (void)addSitesObject:(MPSiteEntity *)value;
- (void)removeSitesObject:(MPSiteEntity *)value;
- (void)addSites:(NSSet *)values;
- (void)removeSites:(NSSet *)values;
@end

View File

@@ -0,0 +1,25 @@
//
// MPUserEntity.m
// MasterPassword-Mac
//
// Created by Maarten Billemont on 2014-09-21.
// Copyright (c) 2014 Lyndir. All rights reserved.
//
#import "MPUserEntity.h"
#import "MPSiteEntity.h"
@implementation MPUserEntity
@dynamic avatar_;
@dynamic defaultType_;
@dynamic keyID;
@dynamic lastUsed;
@dynamic name;
@dynamic saveKey_;
@dynamic touchID_;
@dynamic version_;
@dynamic sites;
@end

View File

@@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8"?>
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="8.00">
<data>
<int key="IBDocument.SystemTarget">1070</int>
<string key="IBDocument.SystemVersion">11C42</string>
<string key="IBDocument.InterfaceBuilderVersion">1938</string>
<string key="IBDocument.AppKitVersion">1138.17</string>
<string key="IBDocument.HIToolboxVersion">567.00</string>
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="NS.object.0">1938</string>
</object>
<array key="IBDocument.IntegratedClassDependencies">
<string>NSWindowTemplate</string>
<string>NSView</string>
<string>NSCustomObject</string>
</array>
<array key="IBDocument.PluginDependencies">
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
</array>
<object class="NSMutableDictionary" key="IBDocument.Metadata">
<string key="NS.key.0">PluginDependencyRecalculationVersion</string>
<integer value="1" key="NS.object.0"/>
</object>
<array class="NSMutableArray" key="IBDocument.RootObjects" id="1000">
<object class="NSCustomObject" id="1001">
<string key="NSClassName">NSObject</string>
</object>
<object class="NSCustomObject" id="1003">
<string key="NSClassName">FirstResponder</string>
</object>
<object class="NSCustomObject" id="1004">
<string key="NSClassName">NSApplication</string>
</object>
<object class="NSWindowTemplate" id="1005">
<int key="NSWindowStyleMask">15</int>
<int key="NSWindowBacking">2</int>
<string key="NSWindowRect">{{196, 240}, {480, 270}}</string>
<int key="NSWTFlags">544735232</int>
<string key="NSWindowTitle">Window</string>
<string key="NSWindowClass">NSWindow</string>
<nil key="NSViewClass"/>
<nil key="NSUserInterfaceItemIdentifier"/>
<object class="NSView" key="NSWindowView" id="1006">
<reference key="NSNextResponder"/>
<int key="NSvFlags">256</int>
<string key="NSFrameSize">{480, 270}</string>
<reference key="NSSuperview"/>
<reference key="NSWindow"/>
</object>
<string key="NSScreenRect">{{0, 0}, {1680, 1028}}</string>
<string key="NSMaxSize">{10000000000000, 10000000000000}</string>
<bool key="NSWindowIsRestorable">YES</bool>
</object>
</array>
<object class="IBObjectContainer" key="IBDocument.Objects">
<array class="NSMutableArray" key="connectionRecords"/>
<object class="IBMutableOrderedSet" key="objectRecords">
<array key="orderedObjects">
<object class="IBObjectRecord">
<int key="objectID">0</int>
<array key="object" id="0"/>
<reference key="children" ref="1000"/>
<nil key="parent"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">-2</int>
<reference key="object" ref="1001"/>
<reference key="parent" ref="0"/>
<string key="objectName">File's Owner</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">-1</int>
<reference key="object" ref="1003"/>
<reference key="parent" ref="0"/>
<string key="objectName">First Responder</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">-3</int>
<reference key="object" ref="1004"/>
<reference key="parent" ref="0"/>
<string key="objectName">Application</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">1</int>
<reference key="object" ref="1005"/>
<array class="NSMutableArray" key="children">
<reference ref="1006"/>
</array>
<reference key="parent" ref="0"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">2</int>
<reference key="object" ref="1006"/>
<reference key="parent" ref="1005"/>
</object>
</array>
</object>
<dictionary class="NSMutableDictionary" key="flattenedProperties">
<string key="-1.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="-2.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="-3.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="1.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="1.IBWindowTemplateEditedContentRect">{{357, 418}, {480, 270}}</string>
<integer value="1" key="1.NSWindowTemplate.visibleAtLaunch"/>
<string key="2.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
</dictionary>
<dictionary class="NSMutableDictionary" key="unlocalizedProperties"/>
<nil key="activeLocalization"/>
<dictionary class="NSMutableDictionary" key="localizations"/>
<nil key="sourceID"/>
<int key="maxID">2</int>
</object>
<object class="IBClassDescriber" key="IBDocument.Classes"/>
<int key="IBDocument.localizationMode">0</int>
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
<bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
<int key="IBDocument.defaultPropertyAccessControl">3</int>
<bool key="IBDocument.UseAutolayout">YES</bool>
</data>
</archive>

View File

@@ -0,0 +1,16 @@
//
// Created by Maarten Billemont on 2016-05-15.
// Copyright (c) 2016 Lyndir. All rights reserved.
//
#import <Foundation/Foundation.h>
IB_DESIGNABLE
@interface MPGradientView : NSView
@property(nonatomic, retain) IBInspectable NSColor *startingColor;
@property(nonatomic, retain) IBInspectable NSColor *endingColor;
@property(nonatomic, assign) IBInspectable NSInteger angle;
@property(nonatomic, assign) IBInspectable CGFloat ratio;
@end

View File

@@ -0,0 +1,81 @@
//
// Created by Maarten Billemont on 2016-05-15.
// Copyright (c) 2016 Lyndir. All rights reserved.
//
#import "MPGradientView.h"
@implementation MPGradientView {
NSGradient *gradient;
}
- (id)initWithFrame:(NSRect)frame {
if (!(self = [super initWithFrame:frame]))
return nil;
[self defaults];
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (!(self = [super initWithCoder:coder]))
return nil;
[self defaults];
return self;
}
- (void)defaults {
self.startingColor = [NSColor colorWithCalibratedWhite:0.0 alpha:0.0];
self.endingColor = [NSColor colorWithCalibratedWhite:0.0 alpha:1.0];
self.angle = 270;
self.ratio = 0.5f;
}
- (void)setStartingColor:(NSColor *)startingColor {
_startingColor = startingColor;
gradient = nil;
[self setNeedsDisplay:YES];
}
- (void)setEndingColor:(NSColor *)endingColor {
_endingColor = endingColor;
gradient = nil;
[self setNeedsDisplay:YES];
}
- (void)setAngle:(NSInteger)angle {
_angle = angle;
gradient = nil;
[self setNeedsDisplay:YES];
}
- (void)setRatio:(CGFloat)ratio {
_ratio = ratio;
gradient = nil;
[self setNeedsDisplay:YES];
}
- (void)drawRect:(NSRect)dirtyRect {
if (!self.startingColor || !self.endingColor || [self.startingColor isEqual:self.endingColor]) {
[(self.startingColor?: self.endingColor) set];
NSRectFill( dirtyRect );
return;
}
[(gradient?: (gradient = [[NSGradient alloc] initWithColorsAndLocations:
self.startingColor, (CGFloat)0.f,
[self.startingColor blendedColorWithFraction:0.5f ofColor:self.endingColor], self.ratio,
self.endingColor, (CGFloat)1.f, nil]))
drawInRect:self.bounds angle:self.angle];
}
@end

View File

@@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="10116" systemVersion="15E65" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="10116"/>
<capability name="Alignment constraints with different attributes" minToolsVersion="5.1"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="MPInitialWindowController">
<connections>
<outlet property="openAtLoginButton" destination="508" id="571"/>
<outlet property="window" destination="1" id="ngh-EA-JYN"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Welcome to Master Password" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" animationBehavior="default" id="1">
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
<windowCollectionBehavior key="collectionBehavior" moveToActiveSpace="YES"/>
<rect key="contentRect" x="0.0" y="0.0" width="1000" height="640"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="877"/>
<view key="contentView" id="2" userLabel="Container">
<rect key="frame" x="0.0" y="0.0" width="1000" height="640"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<customView wantsLayer="YES" translatesAutoresizingMaskIntoConstraints="NO" id="369">
<rect key="frame" x="0.0" y="40" width="1000" height="600"/>
<subviews>
<imageView translatesAutoresizingMaskIntoConstraints="NO" id="3" userLabel="Image">
<rect key="frame" x="0.0" y="0.0" width="1000" height="600"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="shot-laptop-leaning-iphone" id="4"/>
</imageView>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="268">
<rect key="frame" x="18" y="502" width="964" height="78"/>
<shadow key="shadow" blurRadius="1">
<size key="offset" width="0.0" height="1"/>
<color key="color" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
</shadow>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" id="269">
<font key="font" metaFont="system" size="32"/>
<string key="title">Access your passwords
from anywhere.</string>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="calibratedRGB"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="283">
<rect key="frame" x="440" y="72" width="121" height="29"/>
<shadow key="shadow" blurRadius="1">
<size key="offset" width="0.0" height="1"/>
<color key="color" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
</shadow>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Get Mobile" id="284">
<font key="font" size="24" name="HelveticaNeue-Light"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="calibratedRGB"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button translatesAutoresizingMaskIntoConstraints="NO" id="173" userLabel="Store">
<rect key="frame" x="342" y="20" width="150" height="44"/>
<shadow key="shadow" blurRadius="1">
<size key="offset" width="0.0" height="1"/>
<color key="color" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
</shadow>
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="appstore" imagePosition="overlaps" alignment="left" inset="2" id="174">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="iphoneAppStore:" target="-2" id="206"/>
</connections>
</button>
<button translatesAutoresizingMaskIntoConstraints="NO" id="C4d-ih-ARL" userLabel="Store">
<rect key="frame" x="508" y="20" width="150" height="44"/>
<shadow key="shadow" blurRadius="1">
<size key="offset" width="0.0" height="1"/>
<color key="color" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
</shadow>
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="appstore" imagePosition="overlaps" alignment="left" inset="2" id="uzk-gu-gWP">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="androidPlayStore:" target="-2" id="EDO-LD-StH"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="3" firstAttribute="bottom" secondItem="369" secondAttribute="bottom" id="378"/>
<constraint firstItem="3" firstAttribute="trailing" secondItem="369" secondAttribute="trailing" id="379"/>
<constraint firstItem="3" firstAttribute="top" secondItem="369" secondAttribute="top" id="380"/>
<constraint firstItem="3" firstAttribute="leading" secondItem="369" secondAttribute="leading" id="381"/>
<constraint firstItem="268" firstAttribute="leading" secondItem="369" secondAttribute="leading" constant="20" symbolic="YES" id="385"/>
<constraint firstAttribute="trailing" secondItem="268" secondAttribute="trailing" constant="20" symbolic="YES" id="387"/>
<constraint firstItem="268" firstAttribute="top" secondItem="369" secondAttribute="top" constant="20" symbolic="YES" id="393"/>
<constraint firstAttribute="bottom" secondItem="173" secondAttribute="bottom" constant="20" symbolic="YES" id="395"/>
<constraint firstItem="173" firstAttribute="trailing" secondItem="283" secondAttribute="centerX" constant="-8" id="542"/>
<constraint firstItem="173" firstAttribute="top" secondItem="283" secondAttribute="bottom" constant="8" symbolic="YES" id="543"/>
<constraint firstItem="283" firstAttribute="centerX" secondItem="268" secondAttribute="centerX" id="544"/>
<constraint firstItem="C4d-ih-ARL" firstAttribute="leading" secondItem="283" secondAttribute="centerX" constant="8" id="TZ0-Ud-fcb"/>
<constraint firstAttribute="bottom" secondItem="C4d-ih-ARL" secondAttribute="bottom" constant="20" symbolic="YES" id="pIA-8D-d57"/>
<constraint firstItem="C4d-ih-ARL" firstAttribute="top" secondItem="283" secondAttribute="bottom" constant="8" symbolic="YES" id="tqS-gV-jpd"/>
</constraints>
</customView>
<button horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="508">
<rect key="frame" x="770" y="8" width="210" height="25"/>
<buttonCell key="cell" type="roundTextured" title="Launch After Computer Restarts" bezelStyle="texturedRounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="510">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES" changeBackground="YES" changeGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="togglePreference:" target="-2" id="574"/>
</connections>
</button>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="562">
<rect key="frame" x="21" y="13" width="743" height="17"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="To begin: hold command, control and P (⌘⌃P) or access the ●●●| menu icon next to your clock." id="563">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="369" firstAttribute="top" secondItem="2" secondAttribute="top" id="374"/>
<constraint firstItem="369" firstAttribute="leading" secondItem="2" secondAttribute="leading" id="375"/>
<constraint firstItem="508" firstAttribute="top" secondItem="369" secondAttribute="bottom" constant="8" symbolic="YES" id="514"/>
<constraint firstAttribute="bottom" secondItem="508" secondAttribute="bottom" constant="10" id="545"/>
<constraint firstItem="562" firstAttribute="leading" secondItem="2" secondAttribute="leading" constant="23" id="565"/>
<constraint firstItem="369" firstAttribute="trailing" secondItem="2" secondAttribute="trailing" id="567"/>
<constraint firstItem="508" firstAttribute="leading" secondItem="562" secondAttribute="trailing" constant="8" symbolic="YES" id="568"/>
<constraint firstItem="562" firstAttribute="baseline" secondItem="508" secondAttribute="baseline" id="569"/>
<constraint firstAttribute="trailing" secondItem="508" secondAttribute="trailing" constant="20" symbolic="YES" id="nH9-cd-trF"/>
</constraints>
</view>
</window>
</objects>
<resources>
<image name="appstore" width="150" height="44"/>
<image name="shot-laptop-leaning-iphone" width="1000" height="600"/>
</resources>
</document>

View File

@@ -0,0 +1,29 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPInitialWindowController.h
// MPInitialWindowController
//
// Created by lhunath on 2014-06-29.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import <AppKit/AppKit.h>
@interface MPInitialWindowController : NSWindowController
@property(nonatomic, weak) IBOutlet NSButton *openAtLoginButton;
- (IBAction)iphoneAppStore:(id)sender;
- (IBAction)androidPlayStore:(id)sender;
- (IBAction)togglePreference:(id)sender;
@end

View File

@@ -0,0 +1,57 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPInitialWindowController.h
// MPInitialWindowController
//
// Created by lhunath on 2014-06-29.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import "MPInitialWindowController.h"
#import "MPMacAppDelegate.h"
#import "MPAppDelegate_Store.h"
@implementation MPInitialWindowController
#pragma mark - Life
- (void)windowDidLoad {
[super windowDidLoad];
[[NSNotificationCenter defaultCenter] addObserverForName:NSWindowWillCloseNotification object:self.window
queue:nil usingBlock:^(NSNotification *note) {
[MPMacAppDelegate get].initialWindowController = nil;
}];
}
#pragma mark - Actions
- (IBAction)iphoneAppStore:(id)sender {
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://itunes.apple.com/app/id510296984"]];
[self close];
}
- (IBAction)androidPlayStore:(id)sender {
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://masterpasswordapp.com"]];
[self close];
}
- (IBAction)togglePreference:(id)sender {
if (sender == self.openAtLoginButton)
[[MPMacAppDelegate get] setLoginItemEnabled:self.openAtLoginButton.state == NSOnState];
}
@end

View File

@@ -0,0 +1,39 @@
//
// MPMacAppDelegate.h
// MasterPassword
//
// Created by Maarten Billemont on 04/03/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import <Cocoa/Cocoa.h>
#import "MPAppDelegate_Shared.h"
#import "MPPasswordWindowController.h"
#import "MPInitialWindowController.h"
@interface MPMacAppDelegate : MPAppDelegate_Shared<NSApplicationDelegate>
@property(nonatomic, strong) NSStatusItem *statusView;
@property(nonatomic, strong) MPPasswordWindowController *passwordWindowController;
@property(nonatomic, strong) MPInitialWindowController *initialWindowController;
@property(nonatomic, weak) IBOutlet NSMenuItem *lockItem;
@property(nonatomic, weak) IBOutlet NSMenuItem *showItem;
@property(nonatomic, strong) IBOutlet NSMenu *statusMenu;
@property(nonatomic, weak) IBOutlet NSMenuItem *hidePasswordsItem;
@property(nonatomic, weak) IBOutlet NSMenuItem *rememberPasswordItem;
@property(nonatomic, weak) IBOutlet NSMenuItem *openAtLoginItem;
@property(nonatomic, weak) IBOutlet NSMenuItem *showFullScreenItem;
@property(nonatomic, weak) IBOutlet NSMenuItem *savePasswordItem;
@property(nonatomic, weak) IBOutlet NSMenuItem *createUserItem;
@property(nonatomic, weak) IBOutlet NSMenuItem *deleteUserItem;
@property(nonatomic, weak) IBOutlet NSMenuItem *usersItem;
- (IBAction)showPasswordWindow:(id)sender;
- (void)setLoginItemEnabled:(BOOL)enabled;
- (IBAction)togglePreference:(id)sender;
- (IBAction)newUser:(NSMenuItem *)sender;
- (IBAction)lock:(id)sender;
- (IBAction)terminate:(id)sender;
- (IBAction)showPopup:(id)sender;
@end

View File

@@ -0,0 +1,676 @@
//
// MPMacAppDelegate.m
// MasterPassword
//
// Created by Maarten Billemont on 04/03/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "MPMacAppDelegate.h"
#import "MPAppDelegate_Key.h"
#import "MPAppDelegate_Store.h"
#import <Carbon/Carbon.h>
#import <ServiceManagement/ServiceManagement.h>
#define LOGIN_HELPER_BUNDLE_ID @"com.lyndir.lhunath.MasterPassword.Mac.LoginHelper"
@implementation MPMacAppDelegate
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wfour-char-constants"
static EventHotKeyID MPShowHotKey = { .signature = 'show', .id = 1 };
static EventHotKeyID MPLockHotKey = { .signature = 'lock', .id = 1 };
#pragma clang diagnostic pop
+ (void)initialize {
static dispatch_once_t once = 0;
dispatch_once( &once, ^{
[MPMacConfig get];
#ifdef DEBUG
[PearlLogger get].printLevel = PearlLogLevelDebug; //Trace;
#endif
} );
}
static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData) {
// Extract the hotkey ID.
EventHotKeyID hotKeyID;
GetEventParameter( theEvent, kEventParamDirectObject, typeEventHotKeyID,
NULL, sizeof( hotKeyID ), NULL, &hotKeyID );
// Check which hotkey this was.
if (hotKeyID.signature == MPShowHotKey.signature && hotKeyID.id == MPShowHotKey.id) {
[((__bridge MPMacAppDelegate *)userData) showPasswordWindow:nil];
return noErr;
}
if (hotKeyID.signature == MPLockHotKey.signature && hotKeyID.id == MPLockHotKey.id) {
[((__bridge MPMacAppDelegate *)userData) lock:nil];
return noErr;
}
return eventNotHandledErr;
}
#pragma mark - Life
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
#ifdef CRASHLYTICS
NSString *crashlyticsAPIKey = [self crashlyticsAPIKey];
if ([crashlyticsAPIKey length]) {
inf(@"Initializing Crashlytics");
#if defined (DEBUG) || defined (ADHOC)
[Crashlytics sharedInstance].debugMode = YES;
#endif
[[Crashlytics sharedInstance] setUserIdentifier:[PearlKeyChain deviceIdentifier]];
[[Crashlytics sharedInstance] setObjectValue:[PearlKeyChain deviceIdentifier] forKey:@"deviceIdentifier"];
[[Crashlytics sharedInstance] setUserName:@"Anonymous"];
[[Crashlytics sharedInstance] setObjectValue:@"Anonymous" forKey:@"username"];
[Crashlytics startWithAPIKey:crashlyticsAPIKey];
[[PearlLogger get] registerListener:^BOOL(PearlLogMessage *message) {
PearlLogLevel level = PearlLogLevelInfo;
if ([[MPConfig get].sendInfo boolValue])
level = PearlLogLevelDebug;
if (message.level >= level)
CLSLog( @"%@", [message messageDescription] );
return YES;
}];
CLSLog( @"Crashlytics (%@) initialized for: %@ v%@.", //
[Crashlytics sharedInstance].version, [PearlInfoPlist get].CFBundleName, [PearlInfoPlist get].CFBundleVersion );
}
#endif
// Setup delegates and listeners.
[MPConfig get].delegate = self;
__weak id weakSelf = self;
[self addObserverBlock:^(NSString *keyPath, id object, NSDictionary *change, void *context) {
dispatch_async( dispatch_get_main_queue(), ^{
[weakSelf updateMenuItems];
} );
} forKeyPath:@"key" options:0 context:nil];
[self addObserverBlock:^(NSString *keyPath, id object, NSDictionary *change, void *context) {
dispatch_async( dispatch_get_main_queue(), ^{
[weakSelf updateMenuItems];
} );
} forKeyPath:@"activeUser" options:0 context:nil];
// Status item.
self.statusView = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
self.statusView.image = [NSImage imageNamed:@"menu-icon"];
self.statusView.image.template = YES;
self.statusView.menu = self.statusMenu;
self.statusView.target = self;
self.statusView.action = @selector( showMenu );
PearlAddNotificationObserver( NSPersistentStoreCoordinatorStoresWillChangeNotification, self.storeCoordinator, nil,
^(id self, NSNotification *note) {
PearlMainQueue( ^{
[self updateUsers];
} );
} );
PearlAddNotificationObserver( NSPersistentStoreCoordinatorStoresDidChangeNotification, self.storeCoordinator, nil,
^(id self, NSNotification *note) {
PearlMainQueue( ^{
[self updateUsers];
} );
} );
PearlAddNotificationObserver( MPCheckConfigNotification, nil, nil,
^(MPMacAppDelegate *self, NSNotification *note) {
PearlMainQueue( ^{
NSString *key = note.object;
if (!key || [key isEqualToString:NSStringFromSelector( @
selector( hidePasswords ) )])
self.hidePasswordsItem.state = [[MPConfig get].hidePasswords boolValue]? NSOnState: NSOffState;
if (!key || [key isEqualToString:NSStringFromSelector( @
selector( rememberLogin ) )])
self.rememberPasswordItem.state = [[MPConfig get].rememberLogin boolValue]? NSOnState: NSOffState;
} );
} );
[self updateUsers];
// Global hotkey.
EventHotKeyRef hotKeyRef;
EventTypeSpec hotKeyEvents[1] = { { .eventClass = kEventClassKeyboard, .eventKind = kEventHotKeyPressed } };
OSStatus status = InstallApplicationEventHandler( NewEventHandlerUPP( MPHotKeyHander ), GetEventTypeCount( hotKeyEvents ), hotKeyEvents, (__bridge void *)self, NULL );
if (status != noErr)
err( @"Error installing application event handler: %i", (int)status );
status = RegisterEventHotKey( 35 /* p */, controlKey + cmdKey, MPShowHotKey, GetApplicationEventTarget(), 0, &hotKeyRef );
if (status != noErr)
err( @"Error registering 'show' hotkey: %i", (int)status );
status = RegisterEventHotKey( 35 /* p */, controlKey + optionKey + cmdKey, MPLockHotKey, GetApplicationEventTarget(), 0, &hotKeyRef );
if (status != noErr)
err( @"Error registering 'lock' hotkey: %i", (int)status );
// Initial display.
if ([[MPMacConfig get].firstRun boolValue]) {
[(self.initialWindowController = [[MPInitialWindowController alloc] initWithWindowNibName:@"MPInitialWindow"])
.window makeKeyAndOrderFront:self];
[NSApp activateIgnoringOtherApps:YES];
}
}
- (void)applicationWillResignActive:(NSNotification *)notification {
if (![[MPConfig get].rememberLogin boolValue])
[self lock:nil];
}
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
// Save changes in the application's managed object context before the application terminates.
NSManagedObjectContext *context = [MPMacAppDelegate managedObjectContextForMainThreadIfReady];
if (!context)
return NSTerminateNow;
if (![context commitEditing])
return NSTerminateCancel;
if (![context hasChanges])
return NSTerminateNow;
[context saveToStore];
return NSTerminateNow;
}
#pragma mark - State
- (void)setActiveUser:(MPUserEntity *)activeUser {
[super setActiveUser:activeUser];
if (activeUser)
[MPMacConfig get].usedUserName = activeUser.name;
PearlMainQueue( ^{
[self updateUsers];
} );
}
- (void)setLoginItemEnabled:(BOOL)enabled {
BOOL loginItemEnabled = [self loginItemEnabled];
if (loginItemEnabled != enabled) {
if (SMLoginItemSetEnabled( (__bridge CFStringRef)LOGIN_HELPER_BUNDLE_ID, (Boolean)enabled ) == true)
loginItemEnabled = enabled;
else
wrn( @"Failed to set login item." );
}
self.openAtLoginItem.state = loginItemEnabled? NSOnState: NSOffState;
self.initialWindowController.openAtLoginButton.state = loginItemEnabled? NSOnState: NSOffState;
}
- (BOOL)loginItemEnabled {
// The easy and sane method (SMJobCopyDictionary) can pose problems when the app is sandboxed. -_-
NSArray *jobs = (__bridge_transfer NSArray *)SMCopyAllJobDictionaries( kSMDomainUserLaunchd );
for (NSDictionary *job in jobs)
if ([LOGIN_HELPER_BUNDLE_ID isEqualToString:job[@"Label"]])
return [job[@"OnDemand"] boolValue];
return NO;
}
- (BOOL)isFeatureUnlocked:(NSString *)productIdentifier {
// All features are unlocked for mac versions.
return YES;
}
#pragma mark - Actions
- (void)selectUser:(NSMenuItem *)item {
[self signOutAnimated:NO];
NSManagedObjectContext *mainContext = [MPMacAppDelegate managedObjectContextForMainThreadIfReady];
self.activeUser = [MPUserEntity existingObjectWithID:[item representedObject] inContext:mainContext];
}
- (IBAction)exportSitesSecure:(id)sender {
[self exportSitesAndRevealPasswords:NO];
}
- (IBAction)exportSitesReveal:(id)sender {
[self exportSitesAndRevealPasswords:YES];
}
- (IBAction)importSites:(id)sender {
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
openPanel.allowsMultipleSelection = NO;
openPanel.canChooseDirectories = NO;
openPanel.title = @"Master Password";
openPanel.message = @"Locate the Master Password export file to import.";
openPanel.prompt = @"Import";
openPanel.directoryURL = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
openPanel.allowedFileTypes = @[ @"mpsites" ];
[NSApp activateIgnoringOtherApps:YES];
if ([openPanel runModal] == NSFileHandlingPanelCancelButton)
return;
NSURL *url = openPanel.URL;
[openPanel close];
[[NSURLSession sharedSession]
dataTaskWithURL:url completionHandler:^(NSData *importedSitesData, NSURLResponse *response, NSError *error) {
if (error)
err( @"While reading imported sites from %@: %@", url, [error fullDescription] );
if (!importedSitesData)
return;
NSString *importedSitesString = [[NSString alloc] initWithData:importedSitesData encoding:NSUTF8StringEncoding];
MPImportResult result = [self importSites:importedSitesString askImportPassword:^NSString *(NSString *userName) {
__block NSString *masterPassword = nil;
PearlMainQueueWait( ^{
NSAlert *alert = [NSAlert new];
[alert addButtonWithTitle:@"Unlock"];
[alert addButtonWithTitle:@"Cancel"];
alert.messageText = @"Import File's Master Password";
alert.informativeText = strf( @"%@'s export was done using a different master password.\n"
@"Enter that master password to unlock the exported data.", userName );
alert.accessoryView = [[NSSecureTextField alloc] initWithFrame:NSMakeRect( 0, 0, 200, 22 )];
[alert layout];
if ([alert runModal] == NSAlertFirstButtonReturn)
masterPassword = ((NSTextField *)alert.accessoryView).stringValue;
} );
return masterPassword;
} askUserPassword:^NSString *(NSString *userName, NSUInteger importCount, NSUInteger deleteCount) {
__block NSString *masterPassword = nil;
PearlMainQueueWait( ^{
NSAlert *alert = [NSAlert new];
[alert addButtonWithTitle:@"Import"];
[alert addButtonWithTitle:@"Cancel"];
alert.messageText = strf( @"Master Password for\n%@", userName );
alert.informativeText = strf( @"Imports %lu sites, overwriting %lu.",
(unsigned long)importCount, (unsigned long)deleteCount );
alert.accessoryView = [[NSSecureTextField alloc] initWithFrame:NSMakeRect( 0, 0, 200, 22 )];
[alert layout];
if ([alert runModal] == NSAlertFirstButtonReturn)
masterPassword = ((NSTextField *)alert.accessoryView).stringValue;
} );
return masterPassword;
}];
PearlMainQueue( ^{
switch (result) {
case MPImportResultSuccess: {
[self updateUsers];
NSAlert *alert = [NSAlert new];
alert.messageText = @"Successfully imported sites.";
[alert runModal];
break;
}
case MPImportResultCancelled:
break;
case MPImportResultInternalError:
[[NSAlert alertWithError:[NSError errorWithDomain:MPErrorDomain code:0 userInfo:@{
NSLocalizedDescriptionKey : @"Import failed because of an internal error."
}]] runModal];
break;
case MPImportResultMalformedInput:
[[NSAlert alertWithError:[NSError errorWithDomain:MPErrorDomain code:0 userInfo:@{
NSLocalizedDescriptionKey : @"The import doesn't look like a Master Password export."
}]] runModal];
break;
case MPImportResultInvalidPassword:
[[NSAlert alertWithError:[NSError errorWithDomain:MPErrorDomain code:0 userInfo:@{
NSLocalizedDescriptionKey : @"Incorrect master password for the import sites."
}]] runModal];
break;
}
} );
}];
}
- (IBAction)togglePreference:(id)sender {
if (sender == self.hidePasswordsItem)
[MPConfig get].hidePasswords = @(self.hidePasswordsItem.state != NSOnState);
if (sender == self.rememberPasswordItem)
[MPConfig get].rememberLogin = @(self.rememberPasswordItem.state != NSOnState);
if (sender == self.openAtLoginItem)
[self setLoginItemEnabled:self.openAtLoginItem.state != NSOnState];
if (sender == self.showFullScreenItem) {
[MPMacConfig get].fullScreen = @(self.showFullScreenItem.state != NSOnState);
[NSApp updateWindows];
}
if (sender == self.savePasswordItem) {
[MPMacAppDelegate managedObjectContextPerformBlockAndWait:^(NSManagedObjectContext *context) {
MPUserEntity *activeUser = [[MPMacAppDelegate get] activeUserInContext:context];
if ((activeUser.saveKey = !activeUser.saveKey))
[[MPMacAppDelegate get] storeSavedKeyFor:activeUser];
else
[[MPMacAppDelegate get] forgetSavedKeyFor:activeUser];
[context saveToStore];
}];
}
[MPMacConfig flush];
[self updateMenuItems];
}
- (IBAction)newUser:(NSMenuItem *)sender {
NSAlert *alert = [NSAlert new];
[alert setMessageText:@"New User"];
[alert setInformativeText:@"To begin, enter your full name.\n\n"
@"IMPORTANT: Enter your name correctly, including the right capitalization, "
@"as you would on an official document."];
[alert addButtonWithTitle:@"Create User"];
[alert addButtonWithTitle:@"Cancel"];
NSTextField *nameField = [[NSTextField alloc] initWithFrame:NSMakeRect( 0, 0, 200, 22 )];
[alert setAccessoryView:nameField];
[alert layout];
[nameField becomeFirstResponder];
if ([alert runModal] != NSAlertFirstButtonReturn)
return;
NSString *name = [(NSSecureTextField *)alert.accessoryView stringValue];
[MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *moc) {
MPUserEntity *newUser = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass( [MPUserEntity class] )
inManagedObjectContext:moc];
newUser.name = name;
[moc saveToStore];
NSError *error = nil;
if (![moc obtainPermanentIDsForObjects:@[ newUser ] error:&error])
err( @"Failed to obtain permanent object ID for new user: %@", [error fullDescription] );
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self updateUsers];
[self setActiveUser:newUser];
[self showPasswordWindow:nil];
}];
}];
}
- (IBAction)deleteUser:(NSMenuItem *)sender {
NSAlert *alert = [NSAlert new];
[alert setMessageText:@"Delete User"];
[alert setInformativeText:strf( @"This will delete %@ and all their sites.", self.activeUserForMainThread.name )];
[alert addButtonWithTitle:@"Delete"];
[alert addButtonWithTitle:@"Cancel"];
if ([alert runModal] != NSAlertFirstButtonReturn)
return;
[MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *moc) {
[moc deleteObject:[self activeUserInContext:moc]];
[self setActiveUser:nil];
[moc saveToStore];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self updateUsers];
[self showPasswordWindow:nil];
}];
}];
}
- (IBAction)lock:(id)sender {
[self signOutAnimated:YES];
}
- (IBAction)terminate:(id)sender {
[self.passwordWindowController close];
self.passwordWindowController = nil;
[NSApp terminate:nil];
}
- (IBAction)showPopup:(id)sender {
[self.statusView popUpStatusItemMenu:self.statusView.menu];
}
- (IBAction)showPasswordWindow:(id)sender {
prof_new( @"showPasswordWindow" );
[NSApp activateIgnoringOtherApps:YES];
prof_rewind(@"activateIgnoringOtherApps");
// If no user, can't activate.
if (![self activeUserForMainThread]) {
NSAlert *alert = [NSAlert new];
alert.messageText = @"No User Selected";
alert.informativeText = @"Begin by selecting or creating your user from the status menu (●●●|) next to the clock.";
[alert runModal];
[self showPopup:nil];
prof_finish( @"activeUserForMainThread" );
return;
}
prof_rewind( @"activeUserForMainThread" );
// Don't show window if we weren't already running (ie. if we haven't been activated before).
if (!self.passwordWindowController)
self.passwordWindowController = [[MPPasswordWindowController alloc] initWithWindowNibName:@"MPPasswordWindowController"];
prof_rewind( @"initWithWindow" );
[self.passwordWindowController showWindow:self];
prof_finish( @"showWindow" );
}
#pragma mark - Private
- (void)exportSitesAndRevealPasswords:(BOOL)revealPasswords {
MPUserEntity *mainActiveUser = [self activeUserForMainThread];
if (!mainActiveUser) {
NSAlert *alert = [NSAlert new];
alert.messageText = @"No User Selected";
alert.informativeText = @"To export your sites, first select the user whose sites to export.";
[alert runModal];
[self showPopup:nil];
return;
}
if (!self.key) {
NSAlert *alert = [NSAlert new];
alert.messageText = @"User Locked";
alert.informativeText = @"To export your sites, first unlock your user by opening Master Password.";
[alert runModal];
[self showPopup:nil];
return;
}
NSDateFormatter *exportDateFormatter = [NSDateFormatter new];
[exportDateFormatter setDateFormat:@"yyyy'-'MM'-'dd"];
NSSavePanel *savePanel = [NSSavePanel savePanel];
savePanel.title = @"Master Password";
savePanel.message = @"Pick a location for the export Master Password's sites.";
if (revealPasswords)
savePanel.message = strf( @"%@\nWARNING: Your passwords will be visible. Make sure to always keep the file in a secure location.",
savePanel.message );
savePanel.prompt = @"Export";
savePanel.directoryURL = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
savePanel.nameFieldStringValue = strf( @"%@ (%@).mpsites", mainActiveUser.name,
[exportDateFormatter stringFromDate:[NSDate date]] );
savePanel.allowedFileTypes = @[ @"mpsites" ];
[NSApp activateIgnoringOtherApps:YES];
if ([savePanel runModal] == NSFileHandlingPanelCancelButton)
return;
NSError *coordinateError = nil;
NSString *exportedSites = [self exportSitesRevealPasswords:revealPasswords];
[[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateWritingItemAtURL:savePanel.URL options:0 error:&coordinateError
byAccessor:^(NSURL *newURL) {
NSError *writeError = nil;
if (![exportedSites writeToURL:newURL atomically:NO
encoding:NSUTF8StringEncoding
error:&writeError])
PearlMainQueue( ^{
[[NSAlert alertWithError:writeError] runModal];
} );
}];
if (coordinateError)
PearlMainQueue( ^{
[[NSAlert alertWithError:coordinateError] runModal];
} );
}
- (void)updateUsers {
[[[self.usersItem submenu] itemArray] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if (idx > 2)
[[self.usersItem submenu] removeItem:obj];
}];
NSManagedObjectContext *mainContext = [MPMacAppDelegate managedObjectContextForMainThreadIfReady];
if (!mainContext) {
self.createUserItem.title = @"New User (Not ready)";
self.createUserItem.enabled = NO;
self.createUserItem.toolTip = @"Please wait until the app is fully loaded.";
self.deleteUserItem.title = @"Delete User (Not ready)";
self.deleteUserItem.enabled = NO;
self.deleteUserItem.toolTip = @"Please wait until the app is fully loaded.";
[self.usersItem.submenu addItemWithTitle:@"Loading..." action:NULL keyEquivalent:@""].enabled = NO;
return;
}
MPUserEntity *mainActiveUser = [self activeUserInContext:mainContext];
self.createUserItem.title = @"New User";
self.createUserItem.enabled = YES;
self.createUserItem.toolTip = nil;
self.deleteUserItem.title = mainActiveUser? @"Delete User": @"Delete User (None Selected)";
self.deleteUserItem.enabled = mainActiveUser != nil;
self.deleteUserItem.toolTip = mainActiveUser? nil: @"First select the user to delete.";
NSError *error = nil;
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPUserEntity class] )];
fetchRequest.sortDescriptors = @[ [NSSortDescriptor sortDescriptorWithKey:@"lastUsed" ascending:NO] ];
NSArray *users = [mainContext executeFetchRequest:fetchRequest error:&error];
if (!users)
err( @"Failed to load users: %@", [error fullDescription] );
if (![users count]) {
NSMenuItem *noUsersItem = [self.usersItem.submenu addItemWithTitle:@"No users" action:NULL keyEquivalent:@""];
noUsersItem.enabled = NO;
noUsersItem.toolTip = @"Begin by creating a user.";
}
self.usersItem.state = NSMixedState;
for (MPUserEntity *user in users) {
NSMenuItem *userItem = [[NSMenuItem alloc] initWithTitle:user.name action:@selector( selectUser: ) keyEquivalent:@""];
[userItem setTarget:self];
[userItem setRepresentedObject:[user objectID]];
[[self.usersItem submenu] addItem:userItem];
if (!mainActiveUser && [user.name isEqualToString:[MPMacConfig get].usedUserName])
[super setActiveUser:mainActiveUser = user];
if ([mainActiveUser isEqual:user]) {
userItem.state = NSOnState;
self.usersItem.state = NSOffState;
}
else
userItem.state = NSOffState;
}
[self updateMenuItems];
}
- (void)showMenu {
[self updateMenuItems];
[self.statusView popUpStatusItemMenu:self.statusView.menu];
}
- (void)updateMenuItems {
MPUserEntity *activeUser = [self activeUserForMainThread];
// if (!(self.showItem.enabled = ![self.passwordWindowController.window isVisible])) {
// self.showItem.title = @"Show (Showing)";
// self.showItem.toolTip = @"Master Password is already showing.";
// }
// else if (!(self.showItem.enabled = (activeUser != nil))) {
// self.showItem.title = @"Show (No user)";
// self.showItem.toolTip = @"First select the user to show passwords for.";
// }
// else {
// self.showItem.title = @"Show";
// self.showItem.toolTip = nil;
// }
if (self.key) {
self.lockItem.title = @"Lock";
self.lockItem.enabled = YES;
self.lockItem.toolTip = nil;
}
else {
self.lockItem.title = @"Lock (Locked)";
self.lockItem.enabled = NO;
self.lockItem.toolTip = @"Master Password is currently locked.";
}
BOOL loginItemEnabled = [self loginItemEnabled];
self.openAtLoginItem.state = loginItemEnabled? NSOnState: NSOffState;
self.showFullScreenItem.state = [[MPMacConfig get].fullScreen boolValue]? NSOnState: NSOffState;
self.initialWindowController.openAtLoginButton.state = loginItemEnabled? NSOnState: NSOffState;
self.rememberPasswordItem.state = [[MPConfig get].rememberLogin boolValue]? NSOnState: NSOffState;
self.savePasswordItem.state = activeUser.saveKey? NSOnState: NSOffState;
if (!activeUser) {
self.savePasswordItem.title = @"Save Password (No user)";
self.savePasswordItem.enabled = NO;
self.savePasswordItem.toolTip = @"First select your user and unlock by showing the Master Password window.";
}
else if (!self.key) {
self.savePasswordItem.title = @"Save Password (Locked)";
self.savePasswordItem.enabled = NO;
self.savePasswordItem.toolTip = @"First unlock by showing the Master Password window.";
}
else {
self.savePasswordItem.title = @"Save Password";
self.savePasswordItem.enabled = YES;
self.savePasswordItem.toolTip = nil;
}
}
#pragma mark - PearlConfigDelegate
- (void)didUpdateConfigForKey:(SEL)configKey fromValue:(id)oldValue {
[[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification object:NSStringFromSelector( configKey )];
}
#pragma mark - Crashlytics
- (NSDictionary *)crashlyticsInfo {
static NSDictionary *crashlyticsInfo = nil;
if (crashlyticsInfo == nil)
crashlyticsInfo = [[NSDictionary alloc] initWithContentsOfURL:
[[NSBundle mainBundle] URLForResource:@"Crashlytics" withExtension:@"plist"]];
return crashlyticsInfo;
}
- (NSString *)crashlyticsAPIKey {
NSString *crashlyticsAPIKey = NSNullToNil( [[self crashlyticsInfo] valueForKeyPath:@"API Key"] );
if (![crashlyticsAPIKey length])
wrn( @"Crashlytics API key not set. Crash logs won't be recorded." );
return crashlyticsAPIKey;
}
@end

View File

@@ -0,0 +1,9 @@
//
// Created by Maarten Billemont on 2016-04-30.
// Copyright (c) 2016 Lyndir. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface MPMacApplication : NSApplication
@end

View File

@@ -0,0 +1,54 @@
//
// Created by Maarten Billemont on 2016-04-30.
// Copyright (c) 2016 Lyndir. All rights reserved.
//
#import "MPMacApplication.h"
@interface NSResponder (Editing)
- (void)undo:(id)sender;
- (void)redo:(id)sender;
@end
@implementation MPMacApplication {
}
- (void)sendEvent:(NSEvent *)event {
if ([event type] == NSKeyDown) {
if (([event modifierFlags] & NSDeviceIndependentModifierFlagsMask) == NSCommandKeyMask) {
if ([[event charactersIgnoringModifiers] isEqualToString:@"x"]) {
if ([self sendAction:@selector( cut: ) to:nil from:self])
return;
}
else if ([[event charactersIgnoringModifiers] isEqualToString:@"c"]) {
if ([self sendAction:@selector( copy: ) to:nil from:self])
return;
}
else if ([[event charactersIgnoringModifiers] isEqualToString:@"v"]) {
if ([self sendAction:@selector( paste: ) to:nil from:self])
return;
}
else if ([[event charactersIgnoringModifiers] isEqualToString:@"z"]) {
if ([self sendAction:@selector( undo: ) to:nil from:self])
return;
}
else if ([[event charactersIgnoringModifiers] isEqualToString:@"a"]) {
if ([self sendAction:@selector( selectAll: ) to:nil from:self])
return;
}
}
else if (([event modifierFlags] & NSDeviceIndependentModifierFlagsMask) == (NSCommandKeyMask | NSShiftKeyMask)) {
if ([[event charactersIgnoringModifiers] isEqualToString:@"Z"]) {
if ([self sendAction:@selector( redo: ) to:nil from:self])
return;
}
}
}
[super sendEvent:event];
}
@end

View File

@@ -0,0 +1,16 @@
//
// MPMacConfig.h
// MasterPassword
//
// Created by Maarten Billemont on 02/01/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "MPConfig.h"
@interface MPMacConfig : MPConfig
@property(nonatomic, retain) NSString *usedUserName;
@property(nonatomic, retain) NSNumber *fullScreen;
@end

View File

@@ -0,0 +1,27 @@
//
// MPMacConfig.m
// MasterPassword
//
// Created by Maarten Billemont on 02/01/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
@implementation MPMacConfig
@dynamic usedUserName;
@dynamic fullScreen;
- (id)init {
if (!(self = [super init]))
return self;
[self.defaults registerDefaults:@{
NSStringFromSelector( @selector( appleID ) ) : @"510296984",
NSStringFromSelector( @selector( fullScreen ) ) : @YES,
}];
return self;
}
@end

View File

@@ -0,0 +1,23 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPNoStateButton.h
// MPNoStateButton
//
// Created by lhunath on 14-10-27.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface MPNoStateButtonCell : NSButtonCell
@end

View File

@@ -0,0 +1,31 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPNoStateButton.h
// MPNoStateButton
//
// Created by lhunath on 14-10-27.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import "MPNoStateButton.h"
@implementation MPNoStateButtonCell {
}
- (void)setState:(NSInteger)state {
[super setState:NSOnState];
}
@end

View File

@@ -0,0 +1,24 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPPasswordWindow.h
// MPPasswordWindow
//
// Created by lhunath on 2014-06-19.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import <Cocoa/Cocoa.h>
@interface MPPasswordWindow : NSWindow
@end

View File

@@ -0,0 +1,49 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPPasswordWindow.h
// MPPasswordWindow
//
// Created by lhunath on 2014-06-19.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import "MPPasswordWindow.h"
@implementation MPPasswordWindow
#pragma mark - Life
- (BOOL)canBecomeKeyWindow {
return YES;
}
#pragma mark - State
- (void)update {
if ([[MPMacConfig get].fullScreen boolValue]) {
[self setLevel:NSScreenSaverWindowLevel];
[self setFrame:self.screen.frame display:YES];
}
else if (self.level != NSNormalWindowLevel) {
[self setLevel:NSNormalWindowLevel];
[self setFrame:NSMakeRect( 0, 0, 640, 600) display:NO];
[self center];
}
[super update];
}
#pragma mark - Private
@end

View File

@@ -0,0 +1,54 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPPasswordWindowController.h
// MPPasswordWindowController
//
// Created by lhunath on 2014-06-18.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import <Cocoa/Cocoa.h>
#import "MPSiteModel.h"
#import "MPSitesTableView.h"
#import "MPPasswordWindow.h"
@class MPMacAppDelegate;
@interface MPPasswordWindowController : NSWindowController<NSTextViewDelegate, NSTextFieldDelegate, NSTableViewDataSource, NSTableViewDelegate>
@property(nonatomic) NSMutableArray *sites;
@property(nonatomic) NSString *masterPassword;
@property(nonatomic) BOOL showVersionContainer;
@property(nonatomic) BOOL alternatePressed;
@property(nonatomic) BOOL shiftPressed;
@property(nonatomic) BOOL locked;
@property(nonatomic) BOOL newUser;
@property(nonatomic, weak) IBOutlet NSArrayController *sitesController;
@property(nonatomic, weak) IBOutlet NSTextField *inputLabel;
@property(nonatomic, weak) IBOutlet NSTextField *securePasswordField;
@property(nonatomic, weak) IBOutlet NSTextField *revealPasswordField;
@property(nonatomic, weak) IBOutlet NSTextField *sitePasswordTipField;
@property(nonatomic, weak) IBOutlet NSSearchField *siteField;
@property(nonatomic, weak) IBOutlet MPSitesTableView *siteTable;
@property(nonatomic, weak) IBOutlet NSProgressIndicator *progressView;
@property(nonatomic, strong) IBOutlet NSBox *passwordTypesBox;
@property(nonatomic, weak) IBOutlet NSMatrix *passwordTypesMatrix;
@property(nonatomic, strong) IBOutlet NSBox *securityQuestionsBox;
@property(nonatomic, weak) IBOutlet NSTextField *securityQuestionField;
@property(nonatomic, weak) IBOutlet NSTextField *securityAnswerField;
- (BOOL)handleCommand:(SEL)commandSelector;
@end

View File

@@ -0,0 +1,640 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPPasswordWindowController.h
// MPPasswordWindowController
//
// Created by lhunath on 2014-06-18.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import <QuartzCore/QuartzCore.h>
#import "MPPasswordWindowController.h"
#import "MPMacAppDelegate.h"
#import "MPAppDelegate_Store.h"
#import "MPAppDelegate_Key.h"
@interface MPPasswordWindowController()
@property(nonatomic, strong) CAGradientLayer *siteGradient;
@end
@implementation MPPasswordWindowController
#pragma mark - Life
- (void)windowDidLoad {
prof_new( @"windowDidLoad" );
[super windowDidLoad];
prof_rewind( @"super" );
[self replaceFonts:self.window.contentView];
prof_rewind( @"replaceFonts" );
[[NSNotificationCenter defaultCenter] addObserverForName:NSWindowDidBecomeKeyNotification object:self.window
queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
prof_new( @"didBecomeKey" );
[self.window makeKeyAndOrderFront:nil];
prof_rewind( @"fadeIn" );
[self updateUser];
prof_finish( @"updateUser" );
}];
[[NSNotificationCenter defaultCenter] addObserverForName:NSWindowWillCloseNotification object:self.window
queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
NSWindow *sheet = [self.window attachedSheet];
if (sheet)
[self.window endSheet:sheet];
}];
[[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillResignActiveNotification object:nil
queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
[self.window close];
}];
[[NSNotificationCenter defaultCenter] addObserverForName:MPSignedInNotification object:nil
queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
[self updateUser];
}];
[[NSNotificationCenter defaultCenter] addObserverForName:MPSignedOutNotification object:nil
queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
[self updateUser];
}];
[self observeKeyPath:@"sitesController.selection"
withBlock:^(id from, id to, NSKeyValueChange cause, id _self) {
prof_new( @"sitesController.selection" );
[_self updateSelection];
prof_finish( @"updateSelection" );
}];
prof_rewind( @"observers" );
NSSearchFieldCell *siteFieldCell = (NSSearchFieldCell *)self.siteField.cell;
siteFieldCell.searchButtonCell = nil;
siteFieldCell.cancelButtonCell = nil;
self.siteGradient = [CAGradientLayer layer];
self.siteGradient.colors = @[ (__bridge id)[NSColor whiteColor].CGColor, (__bridge id)[NSColor clearColor].CGColor ];
self.siteGradient.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;
self.siteGradient.frame = self.siteTable.bounds;
self.siteTable.superview.superview.layer.mask = self.siteGradient;
self.siteTable.controller = self;
prof_finish( @"ui" );
}
- (void)replaceFonts:(NSView *)view {
if (view.window.backingScaleFactor == 1)
[view enumerateViews:^(NSView *subview, BOOL *stop, BOOL *recurse) {
if ([subview respondsToSelector:@selector( setFont: )]) {
NSFont *font = [(id)subview font];
if ([font.fontName isEqualToString:@"HelveticaNeue-Thin"])
[(id)subview setFont:[NSFont fontWithName:@"HelveticaNeue" matrix:font.matrix]];
if ([font.fontName isEqualToString:@"HelveticaNeue-Light"])
[(id)subview setFont:[NSFont fontWithName:@"HelveticaNeue" matrix:font.matrix]];
}
} recurse:YES];
}
- (void)flagsChanged:(NSEvent *)theEvent {
BOOL shiftPressed = (theEvent.modifierFlags & NSShiftKeyMask) != 0;
if (shiftPressed != self.shiftPressed)
self.shiftPressed = shiftPressed;
BOOL alternatePressed = (theEvent.modifierFlags & NSAlternateKeyMask) != 0;
if (alternatePressed != self.alternatePressed) {
self.alternatePressed = alternatePressed;
self.showVersionContainer = self.alternatePressed || self.selectedSite.outdated;
[self.selectedSite updateContent];
if (self.locked) {
NSTextField *passwordField = self.securePasswordField;
if (self.securePasswordField.isHidden)
passwordField = self.revealPasswordField;
[passwordField becomeFirstResponder];
[[passwordField currentEditor] moveToEndOfLine:nil];
}
}
[super flagsChanged:theEvent];
}
#pragma mark - NSResponder
// Handle any unhandled editor command.
- (void)doCommandBySelector:(SEL)commandSelector {
[self handleCommand:commandSelector];
}
#pragma mark - NSTextFieldDelegate
// Editor command in a text field.
- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector {
if (control == self.siteField) {
if ([NSStringFromSelector( commandSelector ) rangeOfString:@"delete"].location == 0)
return NO;
}
if (control == self.securePasswordField || control == self.revealPasswordField) {
if (commandSelector == @selector( insertNewline: ))
return NO;
}
return [self handleCommand:commandSelector];
}
- (BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor {
if (control == self.siteField)
[fieldEditor replaceCharactersInRange:fieldEditor.selectedRange withString:@""];
return YES;
}
- (IBAction)doUnlockUser:(id)sender {
[self.progressView startAnimation:self];
[MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *moc) {
MPUserEntity *activeUser = [[MPMacAppDelegate get] activeUserInContext:moc];
NSString *userName = activeUser.name;
BOOL success = [[MPMacAppDelegate get] signInAsUser:activeUser saveInContext:moc usingMasterPassword:self.masterPassword];
PearlMainQueue( ^{
self.masterPassword = nil;
[self.progressView stopAnimation:self];
if (!success)
[[NSAlert alertWithError:[NSError errorWithDomain:MPErrorDomain code:0 userInfo:@{
NSLocalizedDescriptionKey : strf( @"Incorrect master password for user %@", userName )
}]] beginSheetModalForWindow:self.window completionHandler:nil];
} );
}];
}
- (IBAction)doSearchSites:(id)sender {
[self updateSites];
}
#pragma mark - NSTextViewDelegate
- (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector {
return [self handleCommand:commandSelector];
}
#pragma mark - NSTableViewDataSource
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
return (NSInteger)[self.sites count];
}
#pragma mark - NSTableViewDelegate
- (void)tableView:(NSTableView *)tableView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row {
[self replaceFonts:rowView];
}
#pragma mark - State
- (void)insertObject:(MPSiteModel *)model inSitesAtIndex:(NSUInteger)index {
[self.sites insertObject:model atIndex:index];
}
- (void)removeObjectFromSitesAtIndex:(NSUInteger)index {
[self.sites removeObjectAtIndex:index];
}
- (MPSiteModel *)selectedSite {
return [self.sitesController.selectedObjects firstObject];
}
#pragma mark - Actions
- (IBAction)settings:(id)sender {
[self.window close];
[[MPMacAppDelegate get] showPopup:sender];
}
- (IBAction)deleteSite:(id)sender {
NSAlert *alert = [NSAlert new];
[alert addButtonWithTitle:@"Delete"];
[alert addButtonWithTitle:@"Cancel"];
[alert setMessageText:@"Delete Site?"];
[alert setInformativeText:strf( @"Do you want to delete the site named:\n\n%@", self.selectedSite.name )];
[alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
switch (returnCode) {
case NSAlertFirstButtonReturn: {
// "Delete" button.
[MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
[context deleteObject:[self.selectedSite entityInContext:context]];
[context saveToStore];
}];
break;
}
default:
break;
}
}];
}
- (IBAction)changeLogin:(id)sender {
NSAlert *alert = [NSAlert new];
[alert addButtonWithTitle:@"Save"];
[alert addButtonWithTitle:@"Cancel"];
[alert setMessageText:@"Change Login Name"];
[alert setInformativeText:strf( @"Your login name for: %@", self.selectedSite.name )];
NSTextField *loginField = [NSTextField new];
[loginField bind:@"value" toObject:self.selectedSite withKeyPath:@"loginName" options:nil];
NSButton *generatedField = [NSButton new];
[generatedField setButtonType:NSSwitchButton];
[generatedField bind:@"value" toObject:self.selectedSite withKeyPath:@"loginGenerated" options:nil];
generatedField.title = @"Generated";
NSStackView *stackView = [NSStackView stackViewWithViews:@[ loginField, generatedField ]];
stackView.orientation = NSUserInterfaceLayoutOrientationVertical;
stackView.frame = NSMakeRect( 0, 0, 200, 44 );
[stackView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"[loginField(200)]"
options:0 metrics:nil
views:NSDictionaryOfVariableBindings( loginField, stackView )]];
[stackView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"[generatedField(200)]"
options:0 metrics:nil
views:NSDictionaryOfVariableBindings( generatedField, stackView )]];
[alert setAccessoryView:stackView];
[alert layout];
[loginField selectText:self];
[alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
switch (returnCode) {
case NSAlertFirstButtonReturn: {
// "Save" button.
NSString *loginName = [loginField stringValue];
[MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
MPSiteEntity *entity = [self.selectedSite entityInContext:context];
entity.loginName = !self.selectedSite.loginGenerated && [loginName length]? loginName: nil;
[context saveToStore];
[self.selectedSite updateContent];
}];
break;
}
default:
break;
}
}];
}
- (IBAction)resetMasterPassword:(id)sender {
NSAlert *alert = [NSAlert new];
[alert addButtonWithTitle:@"Reset"];
[alert addButtonWithTitle:@"Cancel"];
[alert setMessageText:@"Reset My Master Password"];
[alert setInformativeText:strf( @"This will allow you to change %@'s master password.\n\n"
@"WARNING: All your site passwords will change. Do this only if you've forgotten your "
@"master password and are fully prepared to change all your sites' passwords to the new ones.",
[MPMacAppDelegate get].activeUserForMainThread.name )];
[alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
switch (returnCode) {
case NSAlertFirstButtonReturn: {
// "Reset" button.
[MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
MPUserEntity *activeUser = [[MPMacAppDelegate get] activeUserInContext:context];
NSString *activeUserName = activeUser.name;
activeUser.keyID = nil;
[[MPMacAppDelegate get] forgetSavedKeyFor:activeUser];
[context saveToStore];
PearlMainQueue( ^{
NSAlert *alert_ = [NSAlert new];
alert_.messageText = @"Master Password Reset";
alert_.informativeText = strf( @"%@'s master password has been reset.\n\nYou can now set a new one by logging in.",
activeUserName );
[alert_ beginSheetModalForWindow:self.window completionHandler:nil];
if ([MPMacAppDelegate get].key)
[[MPMacAppDelegate get] signOutAnimated:YES];
} );
}];
}
default:
break;
}
}];
}
- (IBAction)changePassword:(id)sender {
if (!self.selectedSite.stored)
return;
NSAlert *alert = [NSAlert new];
[alert addButtonWithTitle:@"Save"];
[alert addButtonWithTitle:@"Cancel"];
[alert setMessageText:@"Change Password"];
[alert setInformativeText:strf( @"Enter the new password for: %@", self.selectedSite.name )];
NSSecureTextField *passwordField = [[NSSecureTextField alloc] initWithFrame:NSMakeRect( 0, 0, 200, 22 )];
[alert setAccessoryView:passwordField];
[alert layout];
[alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
switch (returnCode) {
case NSAlertFirstButtonReturn: {
// "Save" button.
NSString *password = [passwordField stringValue];
[MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
MPSiteEntity *entity = [self.selectedSite entityInContext:context];
[entity.algorithm savePassword:password toSite:entity usingKey:[MPMacAppDelegate get].key];
[context saveToStore];
[self.selectedSite updateContent];
}];
break;
}
default:
break;
}
}];
}
- (IBAction)changeType:(id)sender {
MPSiteModel *site = self.selectedSite;
NSArray *types = [site.algorithm allTypes];
[self.passwordTypesMatrix renewRows:(NSInteger)[types count] columns:1];
for (NSUInteger t = 0; t < [types count]; ++t) {
MPSiteType type = (MPSiteType)[types[t] unsignedIntegerValue];
NSString *title = [site.algorithm nameOfType:type];
if (type & MPSiteTypeClassGenerated)
title = strf( @"%@ %@", [site.algorithm generatePasswordForSiteNamed:site.name ofType:type withCounter:site.counter
usingKey:[MPMacAppDelegate get].key], title );
NSButtonCell *cell = [self.passwordTypesMatrix cellAtRow:(NSInteger)t column:0];
cell.tag = type;
cell.state = type == site.type? NSOnState: NSOffState;
cell.title = title;
}
self.passwordTypesBox.title = strf( @"Choose a password type for %@:", site.name );
NSAlert *alert = [NSAlert new];
[alert addButtonWithTitle:@"Save"];
[alert addButtonWithTitle:@"Cancel"];
[alert setMessageText:@"Change Password Type"];
[alert setAccessoryView:self.passwordTypesBox];
[alert layout];
[alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
switch (returnCode) {
case NSAlertFirstButtonReturn: {
// "Save" button.
MPSiteType type = (MPSiteType)[self.passwordTypesMatrix.selectedCell tag];
[MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
MPSiteEntity *entity = [[MPMacAppDelegate get] changeSite:[self.selectedSite entityInContext:context]
saveInContext:context toType:type];
if ([entity isKindOfClass:[MPStoredSiteEntity class]] && ![(MPStoredSiteEntity *)entity contentObject].length)
PearlMainQueue( ^{
[self changePassword:nil];
} );
}];
break;
}
default:
break;
}
}];
}
- (IBAction)securityQuestions:(id)sender {
MPSiteModel *site = self.selectedSite;
self.securityQuestionsBox.title = strf( @"Answer to security questions for %@:", site.name );
NSAlert *alert = [NSAlert new];
[alert addButtonWithTitle:@"Copy Answer"];
[alert addButtonWithTitle:@"Close"];
[alert setMessageText:@"Security Questions"];
[alert setAccessoryView:self.securityQuestionsBox];
[alert layout];
[alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
switch (returnCode) {
case NSAlertFirstButtonReturn: {
// "Copy Answer" button.
[self copyContent:self.securityAnswerField.stringValue];
[self.window close];
break;
}
default:
break;
}
}];
}
#pragma mark - Private
- (BOOL)handleCommand:(SEL)commandSelector {
if (commandSelector == @selector( moveUp: )) {
[self.sitesController selectPrevious:self];
return YES;
}
if (commandSelector == @selector( moveDown: )) {
[self.sitesController selectNext:self];
return YES;
}
if (commandSelector == @selector( insertNewline: )) {
[self useSite];
return YES;
}
if (commandSelector == @selector( cancel: ) || commandSelector == @selector( cancelOperation: )) {
[self.window close];
return YES;
}
return NO;
}
- (void)useSite {
MPSiteModel *selectedSite = [self selectedSite];
if (!selectedSite)
return;
if (selectedSite.transient) {
[self createNewSite:selectedSite.name];
return;
}
// Performing action while content is available. Copy it.
[self copyContent:self.shiftPressed? selectedSite.answer: selectedSite.content];
[self.window close];
NSUserNotification *notification = [NSUserNotification new];
notification.title = @"Password Copied";
if (selectedSite.loginName.length)
notification.subtitle = strf( @"%@ at %@", selectedSite.loginName, selectedSite.name );
else
notification.subtitle = selectedSite.name;
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification];
}
- (void)updateUser {
[MPMacAppDelegate managedObjectContextForMainThreadPerformBlock:^(NSManagedObjectContext *mainContext) {
self.locked = YES;
self.newUser = YES;
self.inputLabel.stringValue = @"";
self.siteField.stringValue = @"";
MPUserEntity *mainActiveUser = [[MPMacAppDelegate get] activeUserInContext:mainContext];
if (mainActiveUser) {
self.newUser = mainActiveUser.keyID == nil;
if ([MPMacAppDelegate get].key) {
self.inputLabel.stringValue = strf( @"%@'s password for:", mainActiveUser.name );
self.locked = NO;
[self.siteField becomeFirstResponder];
}
else {
self.inputLabel.stringValue = strf( @"Enter %@'s master password:", mainActiveUser.name );
NSTextField *passwordField = self.securePasswordField;
if (self.securePasswordField.isHidden)
passwordField = self.revealPasswordField;
[passwordField becomeFirstResponder];
}
}
[self updateSites];
}];
}
- (void)updateSites {
NSAssert( [NSOperationQueue currentQueue] == [NSOperationQueue mainQueue], @"updateSites should be called on the main queue." );
if (![MPMacAppDelegate get].key) {
self.sites = nil;
return;
}
static NSRegularExpression *fuzzyRE;
static dispatch_once_t once = 0;
dispatch_once( &once, ^{
fuzzyRE = [NSRegularExpression regularExpressionWithPattern:@"(.)" options:0 error:nil];
} );
NSString *queryString = self.siteField.stringValue;
NSString *queryPattern;
if ([queryString length] < 13)
queryPattern = [queryString stringByReplacingMatchesOfExpression:fuzzyRE withTemplate:@"*$1*"];
else
// If query is too long, a wildcard per character makes the CoreData fetch take excessively long.
queryPattern = strf( @"*%@*", queryString );
NSMutableArray *fuzzyGroups = [NSMutableArray new];
[fuzzyRE enumerateMatchesInString:queryString options:0 range:NSMakeRange( 0, queryString.length )
usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
[fuzzyGroups addObject:[queryString substringWithRange:result.range]];
}];
[MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPSiteEntity class] )];
fetchRequest.sortDescriptors = @[ [[NSSortDescriptor alloc] initWithKey:@"lastUsed" ascending:NO] ];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name LIKE[cd] %@) AND user == %@",
queryPattern, queryPattern, [MPMacAppDelegate get].activeUserOID];
NSError *error = nil;
NSArray *siteResults = [context executeFetchRequest:fetchRequest error:&error];
if (!siteResults) {
err( @"While fetching sites for completion: %@", [error fullDescription] );
return;
}
BOOL exact = NO;
NSMutableArray *newSites = [NSMutableArray arrayWithCapacity:[siteResults count]];
for (MPSiteEntity *site in siteResults) {
[newSites addObject:[[MPSiteModel alloc] initWithEntity:site fuzzyGroups:fuzzyGroups]];
exact |= [site.name isEqualToString:queryString];
}
if (!exact && [queryString length]) {
MPUserEntity *activeUser = [[MPAppDelegate_Shared get] activeUserInContext:context];
[newSites addObject:[[MPSiteModel alloc] initWithName:queryString forUser:activeUser]];
}
dbg( @"newSites: %@", newSites );
if (![newSites isEqualToArray:self.sites])
PearlMainQueue( ^{
self.sites = newSites;
} );
}];
}
- (void)updateSelection {
[self.siteTable scrollRowToVisible:(NSInteger)self.sitesController.selectionIndex];
NSView *siteScrollView = self.siteTable.superview.superview;
NSRect selectedCellFrame = [self.siteTable frameOfCellAtColumn:0 row:((NSInteger)self.sitesController.selectionIndex)];
CGFloat selectedOffset = [siteScrollView convertPoint:selectedCellFrame.origin fromView:self.siteTable].y;
CGFloat gradientOpacity = selectedOffset / siteScrollView.bounds.size.height;
self.siteGradient.colors = @[
(__bridge id)[NSColor whiteColor].CGColor,
(__bridge id)[NSColor colorWithDeviceWhite:1 alpha:1 - (1 - gradientOpacity) * 4 / 5].CGColor,
(__bridge id)[NSColor colorWithDeviceWhite:1 alpha:gradientOpacity].CGColor
];
self.showVersionContainer = self.alternatePressed || self.selectedSite.outdated;
[self.sitePasswordTipField setAttributedStringValue:straf( @"Your password for %@:", self.selectedSite.displayedName )];
}
- (void)createNewSite:(NSString *)siteName {
PearlMainQueue( ^{
NSAlert *alert = [NSAlert new];
[alert addButtonWithTitle:@"Create"];
[alert addButtonWithTitle:@"Cancel"];
[alert setMessageText:@"Create site?"];
[alert setInformativeText:strf( @"Do you want to create a new site named:\n\n%@", siteName )];
[alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
switch (returnCode) {
case NSAlertFirstButtonReturn: {
// "Create" button.
[[MPMacAppDelegate get] addSiteNamed:[self.siteField stringValue] completion:
^(MPSiteEntity *site, NSManagedObjectContext *context) {
if (site)
PearlMainQueue( ^{ [self updateSites]; } );
}];
break;
}
default:
break;
}
}];
} );
}
- (void)copyContent:(NSString *)content {
[[NSPasteboard generalPasteboard] declareTypes:@[ NSStringPboardType ] owner:nil];
if (![[NSPasteboard generalPasteboard] setString:content forType:NSPasteboardTypeString]) {
wrn( @"Couldn't copy password to pasteboard." );
return;
}
[MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *moc) {
[[self.selectedSite entityInContext:moc] use];
[moc saveToStore];
}];
}
@end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPSiteModel.h
// MPSiteModel
//
// Created by lhunath on 2/11/2014.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "MPSiteEntity.h"
#import "MPAlgorithm.h"
#import "MPUserEntity.h"
@class MPSiteEntity;
@interface MPSiteModel : NSObject
@property (nonatomic) NSString *name;
@property (nonatomic) NSAttributedString *displayedName;
@property (nonatomic) MPSiteType type;
@property (nonatomic) NSString *typeName;
@property (nonatomic) NSString *content;
@property (nonatomic) NSString *displayedContent;
@property (nonatomic) NSString *question;
@property (nonatomic) NSString *answer;
@property (nonatomic) NSString *loginName;
@property (nonatomic) BOOL loginGenerated;
@property (nonatomic) NSNumber *uses;
@property (nonatomic) NSUInteger counter;
@property (nonatomic) NSDate *lastUsed;
@property (nonatomic) id<MPAlgorithm> algorithm;
@property (nonatomic) MPAlgorithmVersion algorithmVersion;
@property (nonatomic, readonly) BOOL outdated;
@property (nonatomic, readonly) BOOL generated;
@property (nonatomic, readonly) BOOL stored;
@property (nonatomic, readonly) BOOL transient;
- (instancetype)initWithEntity:(MPSiteEntity *)entity fuzzyGroups:(NSArray *)fuzzyGroups;
- (instancetype)initWithName:(NSString *)siteName forUser:(MPUserEntity *)user;
- (MPSiteEntity *)entityInContext:(NSManagedObjectContext *)moc;
- (void)updateContent;
@end

View File

@@ -0,0 +1,290 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPSiteModel.h
// MPSiteModel
//
// Created by lhunath on 2/11/2014.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import "MPSiteModel.h"
#import "MPSiteEntity.h"
#import "MPEntities.h"
#import "MPAppDelegate_Shared.h"
#import "MPAppDelegate_Store.h"
#import "MPMacAppDelegate.h"
@implementation MPSiteModel {
NSManagedObjectID *_entityOID;
BOOL _initialized;
}
- (instancetype)initWithEntity:(MPSiteEntity *)entity fuzzyGroups:(NSArray *)fuzzyGroups {
if (!(self = [super init]))
return nil;
[self setEntity:entity fuzzyGroups:fuzzyGroups];
_initialized = YES;
return self;
}
- (instancetype)initWithName:(NSString *)siteName forUser:(MPUserEntity *)user {
if (!(self = [super init]))
return nil;
[self setTransientSiteName:siteName forUser:user];
_initialized = YES;
return self;
}
- (void)setEntity:(MPSiteEntity *)entity fuzzyGroups:(NSArray *)fuzzyGroups {
if ([_entityOID isEqual:entity.objectID])
return;
_entityOID = entity.objectID;
NSString *siteName = entity.name;
NSMutableAttributedString *attributedSiteName = [[NSMutableAttributedString alloc] initWithString:siteName];
for (NSUInteger f = 0, s = (NSUInteger)-1; f < [fuzzyGroups count]; ++f) {
s = [siteName rangeOfString:fuzzyGroups[f] options:NSDiacriticInsensitiveSearch | NSCaseInsensitiveSearch
range:NSMakeRange( s + 1, [siteName length] - (s + 1) )].location;
if (s == NSNotFound)
break;
[attributedSiteName addAttribute:NSBackgroundColorAttributeName value:[NSColor alternateSelectedControlColor]
range:NSMakeRange( s, [fuzzyGroups[f] length] )];
}
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.alignment = NSCenterTextAlignment;
[attributedSiteName addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange( 0, [siteName length] )];
self.displayedName = attributedSiteName;
self.name = siteName;
self.algorithm = entity.algorithm;
self.lastUsed = entity.lastUsed;
self.type = entity.type;
self.typeName = entity.typeName;
self.uses = entity.uses_;
self.counter = [entity isKindOfClass:[MPGeneratedSiteEntity class]]? [(MPGeneratedSiteEntity *)entity counter]: 0;
self.loginGenerated = entity.loginGenerated;
NSLog( @"%@: loginGenerated: %d", self.name, self.loginGenerated );
// Find all password types and the index of the current type amongst them.
[self updateContent:entity];
}
- (void)setTransientSiteName:(NSString *)siteName forUser:(MPUserEntity *)user {
_entityOID = nil;
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.alignment = NSCenterTextAlignment;
self.displayedName = stra( siteName, @{
NSBackgroundColorAttributeName : [NSColor alternateSelectedControlColor],
NSParagraphStyleAttributeName : paragraphStyle,
} );
self.name = siteName;
self.algorithm = MPAlgorithmDefault;
self.lastUsed = nil;
self.type = user.defaultType;
self.typeName = [self.algorithm nameOfType:self.type];
self.uses = @0;
self.counter = 1;
// Find all password types and the index of the current type amongst them.
[self updateContent];
}
- (MPSiteEntity *)entityInContext:(NSManagedObjectContext *)moc {
if (!_entityOID)
return nil;
NSError *error;
MPSiteEntity *entity = (MPSiteEntity *)[moc existingObjectWithID:_entityOID error:&error];
if (!entity)
err( @"Couldn't retrieve active site: %@", [error fullDescription] );
return entity;
}
- (void)setCounter:(NSUInteger)counter {
if (counter == _counter)
return;
_counter = counter;
if (!_initialized)
// This wasn't a change to the entity.
return;
if (_entityOID)
[MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
MPSiteEntity *entity = [self entityInContext:context];
if ([entity isKindOfClass:[MPGeneratedSiteEntity class]]) {
((MPGeneratedSiteEntity *)entity).counter = counter;
[context saveToStore];
[self updateContent:entity];
}
}];
else
[self updateContent];
}
- (void)setLoginGenerated:(BOOL)loginGenerated {
if (loginGenerated == _loginGenerated)
return;
_loginGenerated = loginGenerated;
if (!_initialized)
// This wasn't a change to the entity.
return;
if (_entityOID)
[MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
MPSiteEntity *entity = [self entityInContext:context];
entity.loginGenerated = loginGenerated;
[context saveToStore];
[self updateContent:entity];
}];
else
[self updateContent];
}
- (MPAlgorithmVersion)algorithmVersion {
return self.algorithm.version;
}
- (void)setAlgorithmVersion:(MPAlgorithmVersion)algorithmVersion {
if (algorithmVersion == self.algorithm.version)
return;
[self willChangeValueForKey:@"outdated"];
self.algorithm = MPAlgorithmForVersion( algorithmVersion )?: self.algorithm;
[self didChangeValueForKey:@"outdated"];
if (_entityOID)
[MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
MPSiteEntity *entity = [self entityInContext:context];
entity.algorithm = self.algorithm;
[context saveToStore];
[self updateContent:entity];
}];
else
[self updateContent];
}
- (void)setQuestion:(NSString *)question {
if ([question isEqualToString:_question])
return;
_question = question;
[self updateContent];
}
- (BOOL)outdated {
return self.algorithmVersion < MPAlgorithmVersionCurrent;
}
- (BOOL)generated {
return self.type & MPSiteTypeClassGenerated;
}
- (BOOL)stored {
return self.type & MPSiteTypeClassStored;
}
- (BOOL)transient {
return _entityOID == nil;
}
- (void)updateContent {
if (_entityOID)
[MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
[self updateContent:[MPSiteEntity existingObjectWithID:_entityOID inContext:context]];
}];
else
PearlNotMainQueue( ^{
[self updatePasswordWithResult:
[self.algorithm generatePasswordForSiteNamed:self.name ofType:self.type withCounter:self.counter
usingKey:[MPAppDelegate_Shared get].key]];
[self updateLoginNameWithResult:
[self.algorithm generateLoginForSiteNamed:self.name
usingKey:[MPAppDelegate_Shared get].key]];
[self updateAnswerWithResult:
[self.algorithm generateAnswerForSiteNamed:self.name onQuestion:self.question
usingKey:[MPAppDelegate_Shared get].key]];
} );
}
- (void)updateContent:(MPSiteEntity *)entity {
[entity resolvePasswordUsingKey:[MPAppDelegate_Shared get].key result:^(NSString *result) {
[self updatePasswordWithResult:result];
}];
[entity resolveLoginUsingKey:[MPAppDelegate_Shared get].key result:^(NSString *result) {
[self updateLoginNameWithResult:result];
}];
[self updateAnswerWithResult:[self.algorithm generateAnswerForSiteNamed:self.name onQuestion:self.question
usingKey:[MPAppDelegate_Shared get].key]];
}
- (void)updatePasswordWithResult:(NSString *)result {
static NSRegularExpression *re_anyChar;
static dispatch_once_t once = 0;
dispatch_once( &once, ^{
re_anyChar = [NSRegularExpression regularExpressionWithPattern:@"." options:0 error:nil];
} );
NSString *displayResult = result;
if ([[MPConfig get].hidePasswords boolValue] && !([NSEvent modifierFlags] & NSAlternateKeyMask))
displayResult = [displayResult stringByReplacingMatchesOfExpression:re_anyChar withTemplate:@"●"];
PearlMainQueue( ^{
self.content = result;
self.displayedContent = displayResult;
} );
}
- (void)updateLoginNameWithResult:(NSString *)loginName {
PearlMainQueue( ^{
self.loginName = loginName;
NSLog( @"%@: loginGenerated: %d, loginName: %@", self.name, self.loginGenerated, loginName );
} );
}
- (void)updateAnswerWithResult:(NSString *)answer {
PearlMainQueue( ^{
self.answer = answer;
} );
}
@end

View File

@@ -0,0 +1,27 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPSitesTableView.h
// MPSitesTableView
//
// Created by lhunath on 2014-06-30.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import <AppKit/AppKit.h>
@class MPPasswordWindowController;
@interface MPSitesTableView : NSTableView
@property(nonatomic, weak) MPPasswordWindowController *controller;
@end

View File

@@ -0,0 +1,34 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPSitesTableView.h
// MPSitesTableView
//
// Created by lhunath on 2014-06-30.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import "MPSitesTableView.h"
#import "MPPasswordWindowController.h"
@implementation MPSitesTableView
- (void)doCommandBySelector:(SEL)aSelector {
[self.controller handleCommand:aSelector];
}
- (void)keyDown:(NSEvent *)theEvent {
[self interpretKeyEvents:@[ theEvent ]];
}
@end

View File

@@ -0,0 +1,109 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ATSApplicationFontsPath</key>
<string>.</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Master Password</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>mpsites</string>
</array>
<key>CFBundleTypeIconFile</key>
<string></string>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeMIMETypes</key>
<array>
<string>text/plain</string>
</array>
<key>CFBundleTypeName</key>
<string>Master Password sites</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>com.lyndir.masterpassword.sites</string>
</array>
<key>LSTypeIsPackage</key>
<integer>0</integer>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Master Password</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>[auto]</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>[auto]</string>
<key>Fabric</key>
<dict>
<key>APIKey</key>
<string>0d10c90776f5ef5acd01ddbeaca9a6cba4814560</string>
<key>Kits</key>
<array>
<dict>
<key>KitInfo</key>
<dict/>
<key>KitName</key>
<string>Crashlytics</string>
</dict>
</array>
</dict>
<key>LSApplicationCategoryType</key>
<string>public.app-category.productivity</string>
<key>LSMinimumSystemVersion</key>
<string>${MACOSX_DEPLOYMENT_TARGET}</string>
<key>LSUIElement</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2011-2014 Lyndir</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>MPMacApplication</string>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.utf8-plain-text</string>
</array>
<key>UTTypeDescription</key>
<string>Master Password sites</string>
<key>UTTypeIconFile</key>
<string></string>
<key>UTTypeIdentifier</key>
<string>com.lyndir.masterpassword.sites</string>
<key>UTTypeSize320IconFile</key>
<string></string>
<key>UTTypeSize64IconFile</key>
<string></string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>mpsites</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,366 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
DAD9B5D1176299B9001835F9 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = DAD9B5D0176299B9001835F9 /* main.m */; };
DAD9B5EC1762AAA6001835F9 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DAD9B5EB1762AAA6001835F9 /* AppKit.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
DA3B84571915577F00246EEA /* Crashlytics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Crashlytics.framework; path = ../../../../External/Mac/Crashlytics.framework; sourceTree = "<group>"; };
DAD9B5C1176299B9001835F9 /* MasterPassword-Mac-LoginHelper.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "MasterPassword-Mac-LoginHelper.app"; sourceTree = BUILT_PRODUCTS_DIR; };
DAD9B5CC176299B9001835F9 /* MasterPassword-Mac-LoginHelper-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "MasterPassword-Mac-LoginHelper-Info.plist"; sourceTree = "<group>"; };
DAD9B5D0176299B9001835F9 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
DAD9B5EA17629C56001835F9 /* MasterPassword-Mac-LoginHelper.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = "MasterPassword-Mac-LoginHelper.entitlements"; sourceTree = "<group>"; };
DAD9B5EB1762AAA6001835F9 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
DAD9B5BE176299B9001835F9 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
DAD9B5EC1762AAA6001835F9 /* AppKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
DAD9B5B8176299B9001835F9 = {
isa = PBXGroup;
children = (
DAD9B5CA176299B9001835F9 /* MasterPassword-Mac-LoginHelper */,
DAD9B5C3176299B9001835F9 /* Frameworks */,
DAD9B5C2176299B9001835F9 /* Products */,
);
sourceTree = "<group>";
};
DAD9B5C2176299B9001835F9 /* Products */ = {
isa = PBXGroup;
children = (
DAD9B5C1176299B9001835F9 /* MasterPassword-Mac-LoginHelper.app */,
);
name = Products;
sourceTree = "<group>";
};
DAD9B5C3176299B9001835F9 /* Frameworks */ = {
isa = PBXGroup;
children = (
DA3B84571915577F00246EEA /* Crashlytics.framework */,
DAD9B5EB1762AAA6001835F9 /* AppKit.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
DAD9B5CA176299B9001835F9 /* MasterPassword-Mac-LoginHelper */ = {
isa = PBXGroup;
children = (
DAD9B5EA17629C56001835F9 /* MasterPassword-Mac-LoginHelper.entitlements */,
DAD9B5CB176299B9001835F9 /* Supporting Files */,
);
path = "MasterPassword-Mac-LoginHelper";
sourceTree = "<group>";
};
DAD9B5CB176299B9001835F9 /* Supporting Files */ = {
isa = PBXGroup;
children = (
DAD9B5CC176299B9001835F9 /* MasterPassword-Mac-LoginHelper-Info.plist */,
DAD9B5D0176299B9001835F9 /* main.m */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
DAD9B5C0176299B9001835F9 /* MasterPassword-Mac-LoginHelper */ = {
isa = PBXNativeTarget;
buildConfigurationList = DAD9B5DE176299B9001835F9 /* Build configuration list for PBXNativeTarget "MasterPassword-Mac-LoginHelper" */;
buildPhases = (
DAD9B5BD176299B9001835F9 /* Sources */,
DAD9B5BE176299B9001835F9 /* Frameworks */,
DAD9B5BF176299B9001835F9 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = "MasterPassword-Mac-LoginHelper";
productName = "MasterPassword-Mac-LoginHelper";
productReference = DAD9B5C1176299B9001835F9 /* MasterPassword-Mac-LoginHelper.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
DAD9B5B9176299B9001835F9 /* Project object */ = {
isa = PBXProject;
attributes = {
CLASSPREFIX = MP;
LastUpgradeCheck = 0710;
ORGANIZATIONNAME = "Maarten Billemont";
TargetAttributes = {
DAD9B5C0176299B9001835F9 = {
DevelopmentTeam = HL3Q45LX9N;
SystemCapabilities = {
com.apple.Sandbox = {
enabled = 1;
};
};
};
};
};
buildConfigurationList = DAD9B5BC176299B9001835F9 /* Build configuration list for PBXProject "MasterPassword-Mac-LoginHelper" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
en,
);
mainGroup = DAD9B5B8176299B9001835F9;
productRefGroup = DAD9B5C2176299B9001835F9 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
DAD9B5C0176299B9001835F9 /* MasterPassword-Mac-LoginHelper */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
DAD9B5BF176299B9001835F9 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
DAD9B5BD176299B9001835F9 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
DAD9B5D1176299B9001835F9 /* main.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
DA3B845A1916AC5100246EEA /* AppStore-Mac */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES;
CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES;
CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_CXX0X_EXTENSIONS = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES;
CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES;
COPY_PHASE_STRIP = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_ENABLE_OBJC_EXCEPTIONS = YES;
GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES;
GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES;
GCC_WARN_ABOUT_MISSING_NEWLINE = YES;
GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES;
GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES;
GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES;
GCC_WARN_SIGN_COMPARE = YES;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNKNOWN_PRAGMAS = YES;
GCC_WARN_UNUSED_LABEL = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.8;
PRODUCT_NAME = "${TARGET_NAME}";
SDKROOT = macosx;
SKIP_INSTALL = YES;
};
name = "AppStore-Mac";
};
DA3B845B1916AC5100246EEA /* AppStore-Mac */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = "MasterPassword-Mac-LoginHelper/MasterPassword-Mac-LoginHelper.entitlements";
CODE_SIGN_IDENTITY = "3rd Party Mac Developer Application";
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = "$(SRCROOT)/MasterPassword-Mac-LoginHelper/MasterPassword-Mac-LoginHelper-Info.plist";
PRODUCT_BUNDLE_IDENTIFIER = com.lyndir.lhunath.MasterPassword.Mac.LoginHelper;
};
name = "AppStore-Mac";
};
DAD9B5DC176299B9001835F9 /* Debug-Mac */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES;
CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES;
CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_CXX0X_EXTENSIONS = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES;
CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES;
COPY_PHASE_STRIP = NO;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_ENABLE_OBJC_EXCEPTIONS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES;
GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES;
GCC_WARN_ABOUT_MISSING_NEWLINE = YES;
GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES;
GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES;
GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES;
GCC_WARN_SIGN_COMPARE = YES;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNKNOWN_PRAGMAS = YES;
GCC_WARN_UNUSED_LABEL = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.8;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_NAME = "${TARGET_NAME}";
SDKROOT = macosx;
SKIP_INSTALL = YES;
};
name = "Debug-Mac";
};
DAD9B5DD176299B9001835F9 /* AdHoc-Mac */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES;
CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES;
CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_CXX0X_EXTENSIONS = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES;
CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES;
COPY_PHASE_STRIP = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_ENABLE_OBJC_EXCEPTIONS = YES;
GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES;
GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES;
GCC_WARN_ABOUT_MISSING_NEWLINE = YES;
GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES;
GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES;
GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES;
GCC_WARN_SIGN_COMPARE = YES;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNKNOWN_PRAGMAS = YES;
GCC_WARN_UNUSED_LABEL = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.8;
PRODUCT_NAME = "${TARGET_NAME}";
SDKROOT = macosx;
SKIP_INSTALL = YES;
};
name = "AdHoc-Mac";
};
DAD9B5DF176299B9001835F9 /* Debug-Mac */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = "MasterPassword-Mac-LoginHelper/MasterPassword-Mac-LoginHelper.entitlements";
CODE_SIGN_IDENTITY = "Mac Developer";
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = "$(SRCROOT)/MasterPassword-Mac-LoginHelper/MasterPassword-Mac-LoginHelper-Info.plist";
PRODUCT_BUNDLE_IDENTIFIER = com.lyndir.lhunath.MasterPassword.Mac.LoginHelper;
};
name = "Debug-Mac";
};
DAD9B5E0176299B9001835F9 /* AdHoc-Mac */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = "MasterPassword-Mac-LoginHelper/MasterPassword-Mac-LoginHelper.entitlements";
CODE_SIGN_IDENTITY = "Developer ID Application: Maarten Billemont (HL3Q45LX9N)";
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = "$(SRCROOT)/MasterPassword-Mac-LoginHelper/MasterPassword-Mac-LoginHelper-Info.plist";
PRODUCT_BUNDLE_IDENTIFIER = com.lyndir.lhunath.MasterPassword.Mac.LoginHelper;
};
name = "AdHoc-Mac";
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
DAD9B5BC176299B9001835F9 /* Build configuration list for PBXProject "MasterPassword-Mac-LoginHelper" */ = {
isa = XCConfigurationList;
buildConfigurations = (
DAD9B5DC176299B9001835F9 /* Debug-Mac */,
DAD9B5DD176299B9001835F9 /* AdHoc-Mac */,
DA3B845A1916AC5100246EEA /* AppStore-Mac */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = "AdHoc-Mac";
};
DAD9B5DE176299B9001835F9 /* Build configuration list for PBXNativeTarget "MasterPassword-Mac-LoginHelper" */ = {
isa = XCConfigurationList;
buildConfigurations = (
DAD9B5DF176299B9001835F9 /* Debug-Mac */,
DAD9B5E0176299B9001835F9 /* AdHoc-Mac */,
DA3B845B1916AC5100246EEA /* AppStore-Mac */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = "AdHoc-Mac";
};
/* End XCConfigurationList section */
};
rootObject = DAD9B5B9176299B9001835F9 /* Project object */;
}

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSBackgroundOnly</key>
<true/>
<key>LSMinimumSystemVersion</key>
<string>${MACOSX_DEPLOYMENT_TARGET}</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2013 Maarten Billemont. All rights reserved.</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>HL3Q45LX9N.com.lyndir.lhunath.MasterPassword.Mac</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,28 @@
//
// main.m
// MasterPassword-Mac-LoginHelper
//
// Created by Maarten Billemont on 2013-06-07.
// Copyright (c) 2013 Maarten Billemont. All rights reserved.
//
#import <Cocoa/Cocoa.h>
int main(int argc, char *argv[]) {
NSURL *bundleURL = [[[[[[NSBundle mainBundle] bundleURL]
URLByDeletingLastPathComponent] URLByDeletingLastPathComponent]
URLByDeletingLastPathComponent] URLByDeletingLastPathComponent];
NSError *error = nil;
NSRunningApplication *application = [[NSWorkspace sharedWorkspace]
launchApplicationAtURL:bundleURL options:NSWorkspaceLaunchWithoutActivation
configuration:@{} error:&error];
if (!application || error) {
NSLog( @"Error launching main app: %@", [error debugDescription] );
return (int)error.code?: 1;
}
return 0;
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>HL3Q45LX9N.com.lyndir.lhunath.MasterPassword.Mac</string>
</array>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1 @@
/* Localized versions of Info.plist keys */

View File

@@ -0,0 +1,226 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="10116" systemVersion="15E65" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="10116"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
<connections>
<outlet property="delegate" destination="494" id="495"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<menu title="AMainMenu" systemMenu="main" id="29"/>
<customObject id="494" customClass="MPMacAppDelegate">
<connections>
<outlet property="createUserItem" destination="757" id="763"/>
<outlet property="deleteUserItem" destination="ZgZ-p2-463" id="smU-PF-mKA"/>
<outlet property="hidePasswordsItem" destination="9G7-17-PzY" id="qPX-VT-jVx"/>
<outlet property="lockItem" destination="720" id="726"/>
<outlet property="openAtLoginItem" destination="785" id="788"/>
<outlet property="rememberPasswordItem" destination="744" id="750"/>
<outlet property="savePasswordItem" destination="747" id="751"/>
<outlet property="showFullScreenItem" destination="Rvo-MK-GgF" id="NYi-85-Yuc"/>
<outlet property="showItem" destination="719" id="783"/>
<outlet property="statusMenu" destination="716" id="731"/>
<outlet property="usersItem" destination="755" id="762"/>
</connections>
</customObject>
<userDefaultsController representsSharedInstance="YES" id="548"/>
<menu autoenablesItems="NO" id="716">
<items>
<menuItem title="Users" id="755">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Users" id="756">
<items>
<menuItem title="New User" enabled="NO" toolTip="Creating users is not yet supported. Please use the iOS app with iCloud enabled to create users and sites." id="757">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="newUser:" target="494" id="761"/>
</connections>
</menuItem>
<menuItem title="Delete User" enabled="NO" toolTip="Creating users is not yet supported. Please use the iOS app with iCloud enabled to create users and sites." id="ZgZ-p2-463">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="deleteUser:" target="494" id="eia-X5-QMc"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="759"/>
</items>
</menu>
</menuItem>
<menuItem title="Preferences" id="739">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Preferences" autoenablesItems="NO" id="742">
<items>
<menuItem title="Open At Login" id="785">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="togglePreference:" target="494" id="787"/>
</connections>
</menuItem>
<menuItem title="Always open Master Password at start-up." enabled="NO" id="786">
<attributedString key="attributedTitle">
<fragment content="Always open Master Password at start-up.">
<attributes>
<font key="NSFont" size="12" name="Helvetica"/>
<paragraphStyle key="NSParagraphStyle" alignment="natural" lineBreakMode="wordWrapping" baseWritingDirection="natural"/>
</attributes>
</fragment>
</attributedString>
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Show Full Screen" id="Rvo-MK-GgF">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="togglePreference:" target="494" id="wfI-yQ-Kla"/>
</connections>
</menuItem>
<menuItem title="Claim the entire screen while looking up passwords." enabled="NO" id="3k1-4V-IZ7">
<attributedString key="attributedTitle">
<fragment content="Always open Master Password at start-up.">
<attributes>
<font key="NSFont" size="12" name="Helvetica"/>
<paragraphStyle key="NSParagraphStyle" alignment="natural" lineBreakMode="wordWrapping" baseWritingDirection="natural"/>
</attributes>
</fragment>
</attributedString>
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Hide Passwords" id="9G7-17-PzY">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="togglePreference:" target="494" id="xgz-pN-csV"/>
</connections>
</menuItem>
<menuItem title="Don't display the password. Hold alt ⌥ to reveal temporarily." enabled="NO" id="HnK-hQ-cM9">
<attributedString key="attributedTitle">
<fragment content="Don't display the password. Hold alt ">
<attributes>
<font key="NSFont" size="12" name="Helvetica"/>
<paragraphStyle key="NSParagraphStyle" alignment="natural" lineBreakMode="wordWrapping" baseWritingDirection="natural"/>
</attributes>
</fragment>
<fragment content="⌥">
<attributes>
<font key="NSFont" size="12" name="LucidaGrande"/>
<paragraphStyle key="NSParagraphStyle" alignment="natural" lineBreakMode="wordWrapping" baseWritingDirection="natural"/>
</attributes>
</fragment>
<fragment content=" to reveal temporarily.">
<attributes>
<font key="NSFont" size="12" name="Helvetica"/>
<paragraphStyle key="NSParagraphStyle" alignment="natural" lineBreakMode="wordWrapping" baseWritingDirection="natural"/>
</attributes>
</fragment>
</attributedString>
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Remember Password" id="744">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="togglePreference:" target="494" id="753"/>
</connections>
</menuItem>
<menuItem title="Remember the password while the application is running." enabled="NO" id="Avu-uY-2Rh">
<attributedString key="attributedTitle">
<fragment content="Remember the password while the application is running.">
<attributes>
<font key="NSFont" size="12" name="Helvetica"/>
<paragraphStyle key="NSParagraphStyle" alignment="natural" lineBreakMode="wordWrapping" baseWritingDirection="natural"/>
</attributes>
</fragment>
</attributedString>
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Save Password" id="747">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="togglePreference:" target="494" id="754"/>
</connections>
</menuItem>
<menuItem title="Save the password in your keychain so you don't need to enter it again." enabled="NO" id="748">
<attributedString key="attributedTitle">
<fragment content="Save the password in your keychain so you don't need to enter it again.">
<attributes>
<font key="NSFont" size="12" name="Helvetica"/>
<paragraphStyle key="NSParagraphStyle" alignment="natural" lineBreakMode="wordWrapping" baseWritingDirection="natural"/>
</attributes>
</fragment>
</attributedString>
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Export Sites" id="r1P-hr-mh5">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Export Sites" id="fnk-gK-yCi">
<items>
<menuItem title="Secure Export" id="06i-og-eLt">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="exportSitesSecure:" target="494" id="LVH-es-imA"/>
</connections>
</menuItem>
<menuItem title="Your passwords are not visible." enabled="NO" id="ybY-P3-eao">
<attributedString key="attributedTitle">
<fragment content="Your passwords are not visible.">
<attributes>
<font key="NSFont" size="12" name="Helvetica"/>
<paragraphStyle key="NSParagraphStyle" alignment="natural" lineBreakMode="wordWrapping" baseWritingDirection="natural"/>
</attributes>
</fragment>
</attributedString>
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Reveal Passwords" id="fMG-TT-bTn">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="exportSitesReveal:" target="494" id="1IW-VT-Oeu"/>
</connections>
</menuItem>
<menuItem title="Keep this file secure or delete it when you're done with it!" enabled="NO" id="cQu-oR-SUa">
<attributedString key="attributedTitle">
<fragment content="Keep this file secure or delete it when you're done with it!">
<attributes>
<font key="NSFont" size="12" name="Helvetica"/>
<paragraphStyle key="NSParagraphStyle" alignment="natural" lineBreakMode="wordWrapping" baseWritingDirection="natural"/>
</attributes>
</fragment>
</attributedString>
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Import Sites..." id="EFK-zt-EvJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="importSites:" target="494" id="CNv-4j-036"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="718"/>
<menuItem title="Open" keyEquivalent="p" id="719">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="showPasswordWindow:" target="494" id="782"/>
</connections>
</menuItem>
<menuItem title="Lock" enabled="NO" keyEquivalent="p" id="720">
<modifierMask key="keyEquivalentModifierMask" control="YES" option="YES" command="YES"/>
<connections>
<action selector="lock:" target="494" id="764"/>
</connections>
</menuItem>
<menuItem title="Quit" keyEquivalent="q" id="717">
<connections>
<action selector="terminate:" target="494" id="784"/>
</connections>
</menuItem>
</items>
</menu>
</objects>
</document>

View File

@@ -0,0 +1,12 @@
//
// main.m
// MasterPassword
//
// Created by Maarten Billemont on 04/03/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
int main(int argc, char *argv[]) {
return NSApplicationMain( argc, (const char **)argv );
}

View File

@@ -0,0 +1,51 @@
//
// Prefix header for all source files of the 'MasterPassword' target in the 'MasterPassword' project
//
#import <Availability.h>
#import "Pearl-Prefix.pch"
#ifdef __OBJC__
#import <UIKit/UIKit.h>
//#import <Cocoa/Cocoa.h>
#import <CoreData/CoreData.h>
#ifdef CRASHLYTICS
#import <Crashlytics/Crashlytics.h>
#endif
#import "MPTypes.h"
#import "MPiOSConfig.h"
//#import "MPMacConfig.h"
#else
#import <libgen.h>
#import <CoreFoundation/CFString.h>
#import <objc/runtime.h>
#import <objc/message.h>
#import <objc/NSObjCRuntime.h>
#import <stdlib.h>
#define log(level, format, ...) \
do { \
void (*_sendMsg)(id, SEL, CFStringRef, NSInteger, CFStringRef, NSUInteger, CFStringRef) = (void *)objc_msgSend; \
char *_msg = NULL; \
asprintf( &_msg, format, ##__VA_ARGS__ ); \
CFStringRef fileStr = CFStringCreateWithCString( NULL, basename( (char *)__FILE__ ), kCFStringEncodingUTF8 ); \
CFStringRef funcStr = CFStringCreateWithCString( NULL, __FUNCTION__, kCFStringEncodingUTF8 ); \
CFStringRef msgStr = CFStringCreateWithCString( NULL, _msg, kCFStringEncodingUTF8 ); \
_sendMsg( objc_msgSend( (id)objc_getClass( "PearlLogger" ), sel_getUid( "get" ) ), \
sel_getUid( "inFile:atLine:fromFunction:withLevel:text:" ), fileStr, __LINE__, funcStr, level, msgStr ); \
CFRelease( fileStr ); \
CFRelease( funcStr ); \
CFRelease( msgStr ); \
} while (0)
#define trc(format, ...) log( 0, format, ##__VA_ARGS__ );
#define dbg(format, ...) log( 1, format, ##__VA_ARGS__ );
#define inf(format, ...) log( 2, format, ##__VA_ARGS__ );
#define wrn(format, ...) log( 3, format, ##__VA_ARGS__ );
#define err(format, ...) log( 4, format, ##__VA_ARGS__ );
#define ftl(format, ...) log( 5, format, ##__VA_ARGS__ );
#endif

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>MasterPassword 8.xcdatamodel</string>
</dict>
</plist>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="6244" systemVersion="13E28" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
<entity name="MPElementEntity" representedClassName="MPSiteEntity" isAbstract="YES" elementID="58EE245C-6827-4C11-BB7E-5722F2426EC2" syncable="YES">
<attribute name="content" optional="YES" transient="YES" attributeType="Transformable" syncable="YES"/>
<attribute name="lastUsed" attributeType="Date" syncable="YES"/>
<attribute name="name" attributeType="String" minValueString="1" indexed="YES" syncable="YES"/>
<attribute name="type_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
<attribute name="uses_" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
<relationship name="user" minCount="1" maxCount="1" deletionRule="Nullify" destinationEntity="MPUserEntity" inverseName="elements" inverseEntity="MPUserEntity" elementID="FC8AE32E-7BE3-4FA6-8611-B7DC0DB063EF" syncable="YES"/>
</entity>
<entity name="MPElementGeneratedEntity" representedClassName="MPGeneratedSiteEntity" parentEntity="MPElementEntity" elementID="789304EA-070D-4982-8C20-54EECFC20CB6" syncable="YES">
<attribute name="counter_" optional="YES" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
</entity>
<entity name="MPElementStoredEntity" representedClassName="MPStoredSiteEntity" parentEntity="MPElementEntity" elementID="BEEF1688-0CCD-4699-A86A-4D860FE2CEB8" syncable="YES">
<attribute name="contentObject" optional="YES" attributeType="Transformable" storedInTruthFile="YES" syncable="YES"/>
</entity>
<entity name="MPUserEntity" representedClassName="MPUserEntity" elementID="D18D6772-040E-4CFE-8F32-A34B08E9E9BC" syncable="YES">
<attribute name="avatar_" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
<attribute name="defaultType_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
<attribute name="keyID" optional="YES" attributeType="Binary" syncable="YES"/>
<attribute name="lastUsed" optional="YES" attributeType="Date" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="saveKey_" attributeType="Boolean" defaultValueString="NO">
<userInfo/>
</attribute>
<relationship name="elements" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="MPElementEntity" inverseName="user" inverseEntity="MPElementEntity" syncable="YES"/>
</entity>
<elements>
<element name="MPElementEntity" positionX="160" positionY="192" width="128" height="135"/>
<element name="MPElementGeneratedEntity" positionX="160" positionY="192" width="128" height="60"/>
<element name="MPElementStoredEntity" positionX="160" positionY="192" width="128" height="60"/>
<element name="MPUserEntity" positionX="160" positionY="192" width="128" height="150"/>
</elements>
</model>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="6244" systemVersion="13E28" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
<entity name="MPElementEntity" representedClassName="MPSiteEntity" isAbstract="YES" elementID="58EE245C-6827-4C11-BB7E-5722F2426EC2" syncable="YES">
<attribute name="content" optional="YES" transient="YES" attributeType="Transformable" syncable="YES"/>
<attribute name="lastUsed" attributeType="Date" indexed="YES" syncable="YES"/>
<attribute name="name" attributeType="String" minValueString="1" indexed="YES" syncable="YES"/>
<attribute name="requiresExplicitMigration_" attributeType="Boolean" defaultValueString="NO">
<userInfo/>
</attribute>
<attribute name="type_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
<attribute name="userName" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="uses_" attributeType="Integer 16" defaultValueString="0" indexed="YES" syncable="YES"/>
<attribute name="version_" attributeType="Integer 16" minValueString="0" defaultValueString="0" syncable="YES"/>
<relationship name="user" minCount="1" maxCount="1" deletionRule="Nullify" destinationEntity="MPUserEntity" inverseName="elements" inverseEntity="MPUserEntity" elementID="FC8AE32E-7BE3-4FA6-8611-B7DC0DB063EF" syncable="YES"/>
</entity>
<entity name="MPElementGeneratedEntity" representedClassName="MPGeneratedSiteEntity" parentEntity="MPElementEntity" elementID="789304EA-070D-4982-8C20-54EECFC20CB6" syncable="YES">
<attribute name="counter_" optional="YES" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
</entity>
<entity name="MPElementStoredEntity" representedClassName="MPStoredSiteEntity" parentEntity="MPElementEntity" elementID="BEEF1688-0CCD-4699-A86A-4D860FE2CEB8" syncable="YES">
<attribute name="contentObject" optional="YES" attributeType="Transformable" storedInTruthFile="YES" syncable="YES"/>
</entity>
<entity name="MPUserEntity" representedClassName="MPUserEntity" elementID="D18D6772-040E-4CFE-8F32-A34B08E9E9BC" syncable="YES">
<attribute name="avatar_" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
<attribute name="defaultType_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
<attribute name="keyID" optional="YES" attributeType="Binary" syncable="YES"/>
<attribute name="lastUsed" optional="YES" attributeType="Date" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="requiresExplicitMigration_" transient="YES" attributeType="Boolean" defaultValueString="NO" syncable="YES"/>
<attribute name="saveKey_" attributeType="Boolean" defaultValueString="NO">
<userInfo/>
</attribute>
<relationship name="elements" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="MPElementEntity" inverseName="user" inverseEntity="MPElementEntity" syncable="YES"/>
</entity>
<elements>
<element name="MPElementEntity" positionX="0" positionY="0" width="128" height="180"/>
<element name="MPElementGeneratedEntity" positionX="216" positionY="-0" width="128" height="60"/>
<element name="MPElementStoredEntity" positionX="216" positionY="144" width="128" height="60"/>
<element name="MPUserEntity" positionX="-216" positionY="0" width="128" height="165"/>
</elements>
</model>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="6244" systemVersion="13E28" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
<entity name="MPElementEntity" representedClassName="MPSiteEntity" isAbstract="YES" elementID="58EE245C-6827-4C11-BB7E-5722F2426EC2" syncable="YES">
<attribute name="content" optional="YES" transient="YES" attributeType="Transformable" syncable="YES"/>
<attribute name="lastUsed" attributeType="Date" indexed="YES" syncable="YES"/>
<attribute name="loginName" optional="YES" attributeType="String" elementID="A1B9F981-D33C-4BFE-9F94-C9D3E1F78E51" syncable="YES"/>
<attribute name="name" attributeType="String" minValueString="1" indexed="YES" syncable="YES"/>
<attribute name="requiresExplicitMigration_" attributeType="Boolean" defaultValueString="NO">
<userInfo/>
</attribute>
<attribute name="type_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
<attribute name="uses_" attributeType="Integer 16" defaultValueString="0" indexed="YES" syncable="YES"/>
<attribute name="version_" attributeType="Integer 16" minValueString="0" defaultValueString="0" syncable="YES"/>
<relationship name="user" minCount="1" maxCount="1" deletionRule="Nullify" destinationEntity="MPUserEntity" inverseName="elements" inverseEntity="MPUserEntity" elementID="FC8AE32E-7BE3-4FA6-8611-B7DC0DB063EF" syncable="YES"/>
</entity>
<entity name="MPElementGeneratedEntity" representedClassName="MPGeneratedSiteEntity" parentEntity="MPElementEntity" elementID="789304EA-070D-4982-8C20-54EECFC20CB6" syncable="YES">
<attribute name="counter_" optional="YES" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
</entity>
<entity name="MPElementStoredEntity" representedClassName="MPStoredSiteEntity" parentEntity="MPElementEntity" elementID="BEEF1688-0CCD-4699-A86A-4D860FE2CEB8" syncable="YES">
<attribute name="contentObject" optional="YES" attributeType="Transformable" storedInTruthFile="YES" syncable="YES"/>
</entity>
<entity name="MPUserEntity" representedClassName="MPUserEntity" syncable="YES">
<attribute name="avatar_" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
<attribute name="defaultType_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
<attribute name="keyID" optional="YES" attributeType="Binary" syncable="YES"/>
<attribute name="lastUsed" optional="YES" attributeType="Date" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="requiresExplicitMigration_" transient="YES" attributeType="Boolean" defaultValueString="NO" syncable="YES"/>
<attribute name="saveKey_" attributeType="Boolean" defaultValueString="NO">
<userInfo/>
</attribute>
<relationship name="elements" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="MPElementEntity" inverseName="user" inverseEntity="MPElementEntity" elementID="D18D6772-040E-4CFE-8F32-A34B08E9E9BC" syncable="YES"/>
</entity>
<elements>
<element name="MPElementEntity" positionX="-0" positionY="-286" width="128" height="178"/>
<element name="MPElementGeneratedEntity" positionX="216" positionY="-288" width="128" height="58"/>
<element name="MPElementStoredEntity" positionX="214" positionY="-171" width="128" height="58"/>
<element name="MPUserEntity" positionX="-218" positionY="-288" width="128" height="163"/>
</elements>
</model>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="6244" systemVersion="13E28" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
<entity name="MPElementEntity" representedClassName="MPSiteEntity" isAbstract="YES" elementID="58EE245C-6827-4C11-BB7E-5722F2426EC2" syncable="YES">
<attribute name="content" optional="YES" transient="YES" attributeType="Transformable" syncable="YES"/>
<attribute name="lastUsed" attributeType="Date" indexed="YES" syncable="YES"/>
<attribute name="loginName" optional="YES" attributeType="String" elementID="A1B9F981-D33C-4BFE-9F94-C9D3E1F78E51" syncable="YES"/>
<attribute name="name" attributeType="String" minValueString="1" indexed="YES" syncable="YES"/>
<attribute name="requiresExplicitMigration_" attributeType="Boolean" defaultValueString="NO">
<userInfo/>
</attribute>
<attribute name="type_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
<attribute name="uses_" attributeType="Integer 16" defaultValueString="0" indexed="YES" syncable="YES"/>
<attribute name="version_" attributeType="Integer 16" minValueString="0" defaultValueString="0" syncable="YES"/>
<relationship name="user" minCount="1" maxCount="1" deletionRule="Nullify" destinationEntity="MPUserEntity" inverseName="elements" inverseEntity="MPUserEntity" elementID="FC8AE32E-7BE3-4FA6-8611-B7DC0DB063EF" syncable="YES"/>
</entity>
<entity name="MPElementGeneratedEntity" representedClassName="MPGeneratedSiteEntity" parentEntity="MPElementEntity" elementID="789304EA-070D-4982-8C20-54EECFC20CB6" syncable="YES">
<attribute name="counter_" optional="YES" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
</entity>
<entity name="MPElementStoredEntity" representedClassName="MPStoredSiteEntity" parentEntity="MPElementEntity" elementID="BEEF1688-0CCD-4699-A86A-4D860FE2CEB8" syncable="YES">
<attribute name="contentObject" optional="YES" attributeType="Transformable" storedInTruthFile="YES" syncable="YES"/>
</entity>
<entity name="MPUserEntity" representedClassName="MPUserEntity" syncable="YES">
<attribute name="avatar_" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
<attribute name="defaultType_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
<attribute name="keyID" optional="YES" attributeType="Binary" syncable="YES"/>
<attribute name="lastUsed" optional="YES" attributeType="Date" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="saveKey_" attributeType="Boolean" defaultValueString="NO">
<userInfo/>
</attribute>
<relationship name="elements" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="MPElementEntity" inverseName="user" inverseEntity="MPElementEntity" elementID="D18D6772-040E-4CFE-8F32-A34B08E9E9BC" syncable="YES"/>
</entity>
<elements>
<element name="MPElementEntity" positionX="-0" positionY="-286" width="128" height="178"/>
<element name="MPElementGeneratedEntity" positionX="216" positionY="-288" width="128" height="58"/>
<element name="MPElementStoredEntity" positionX="214" positionY="-171" width="128" height="58"/>
<element name="MPUserEntity" positionX="-218" positionY="-288" width="128" height="148"/>
</elements>
</model>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="6244" systemVersion="13E28" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
<entity name="MPElementEntity" representedClassName="MPElementEntity" isAbstract="YES" elementID="58EE245C-6827-4C11-BB7E-5722F2426EC2" syncable="YES">
<attribute name="content" optional="YES" transient="YES" attributeType="Transformable" syncable="YES"/>
<attribute name="lastUsed" attributeType="Date" indexed="YES" syncable="YES"/>
<attribute name="loginGenerated_" attributeType="Boolean" defaultValueString="NO" syncable="YES"/>
<attribute name="loginName" optional="YES" attributeType="String" elementID="A1B9F981-D33C-4BFE-9F94-C9D3E1F78E51" syncable="YES"/>
<attribute name="name" attributeType="String" minValueString="1" indexed="YES" syncable="YES"/>
<attribute name="requiresExplicitMigration_" attributeType="Boolean" defaultValueString="NO">
<userInfo/>
</attribute>
<attribute name="type_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
<attribute name="uses_" attributeType="Integer 16" defaultValueString="0" indexed="YES" syncable="YES"/>
<attribute name="version_" attributeType="Integer 16" minValueString="0" defaultValueString="0" syncable="YES"/>
<relationship name="user" minCount="1" maxCount="1" deletionRule="Nullify" destinationEntity="MPUserEntity" inverseName="elements" inverseEntity="MPUserEntity" elementID="FC8AE32E-7BE3-4FA6-8611-B7DC0DB063EF" syncable="YES"/>
</entity>
<entity name="MPElementGeneratedEntity" representedClassName="MPElementGeneratedEntity" parentEntity="MPElementEntity" elementID="789304EA-070D-4982-8C20-54EECFC20CB6" syncable="YES">
<attribute name="counter_" optional="YES" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
</entity>
<entity name="MPElementStoredEntity" representedClassName="MPElementStoredEntity" parentEntity="MPElementEntity" elementID="BEEF1688-0CCD-4699-A86A-4D860FE2CEB8" syncable="YES">
<attribute name="contentObject" optional="YES" attributeType="Transformable" storedInTruthFile="YES" syncable="YES"/>
</entity>
<entity name="MPUserEntity" representedClassName="MPUserEntity" syncable="YES">
<attribute name="avatar_" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
<attribute name="defaultType_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
<attribute name="keyID" optional="YES" attributeType="Binary" syncable="YES"/>
<attribute name="lastUsed" optional="YES" attributeType="Date" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="saveKey_" attributeType="Boolean" defaultValueString="NO">
<userInfo/>
</attribute>
<relationship name="elements" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="MPElementEntity" inverseName="user" inverseEntity="MPElementEntity" elementID="D18D6772-040E-4CFE-8F32-A34B08E9E9BC" syncable="YES"/>
</entity>
<elements>
<element name="MPElementEntity" positionX="-0" positionY="-286" width="128" height="193"/>
<element name="MPElementGeneratedEntity" positionX="216" positionY="-288" width="128" height="58"/>
<element name="MPElementStoredEntity" positionX="214" positionY="-171" width="128" height="58"/>
<element name="MPUserEntity" positionX="-218" positionY="-288" width="128" height="148"/>
</elements>
</model>

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="6254" systemVersion="14C109" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
<entity name="MPGeneratedSiteEntity" representedClassName="MPGeneratedSiteEntity" parentEntity="MPSiteEntity" elementID="789304EA-070D-4982-8C20-54EECFC20CB6" syncable="YES">
<attribute name="counter_" optional="YES" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
</entity>
<entity name="MPSiteEntity" representedClassName="MPSiteEntity" isAbstract="YES" elementID="58EE245C-6827-4C11-BB7E-5722F2426EC2" syncable="YES">
<attribute name="content" optional="YES" transient="YES" attributeType="Transformable" syncable="YES"/>
<attribute name="lastUsed" attributeType="Date" indexed="YES" syncable="YES"/>
<attribute name="loginGenerated_" attributeType="Boolean" defaultValueString="NO" syncable="YES"/>
<attribute name="loginName" optional="YES" attributeType="String" elementID="A1B9F981-D33C-4BFE-9F94-C9D3E1F78E51" syncable="YES"/>
<attribute name="name" attributeType="String" minValueString="1" indexed="YES" syncable="YES"/>
<attribute name="requiresExplicitMigration_" attributeType="Boolean" defaultValueString="NO">
<userInfo/>
</attribute>
<attribute name="type_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
<attribute name="uses_" attributeType="Integer 16" defaultValueString="0" indexed="YES" syncable="YES"/>
<attribute name="version_" attributeType="Integer 16" minValueString="0" defaultValueString="0" syncable="YES"/>
<relationship name="questions" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="MPSiteQuestionEntity" inverseName="site" inverseEntity="MPSiteQuestionEntity" syncable="YES"/>
<relationship name="user" minCount="1" maxCount="1" deletionRule="Nullify" destinationEntity="MPUserEntity" inverseName="sites" inverseEntity="MPUserEntity" elementID="FC8AE32E-7BE3-4FA6-8611-B7DC0DB063EF" syncable="YES"/>
</entity>
<entity name="MPSiteQuestionEntity" representedClassName="MPSiteQuestionEntity" syncable="YES">
<attribute name="keyword" attributeType="String" syncable="YES"/>
<relationship name="site" maxCount="1" deletionRule="Nullify" destinationEntity="MPSiteEntity" inverseName="questions" inverseEntity="MPSiteEntity" syncable="YES"/>
</entity>
<entity name="MPStoredSiteEntity" representedClassName="MPStoredSiteEntity" parentEntity="MPSiteEntity" elementID="BEEF1688-0CCD-4699-A86A-4D860FE2CEB8" syncable="YES">
<attribute name="contentObject" optional="YES" attributeType="Transformable" storedInTruthFile="YES" syncable="YES"/>
</entity>
<entity name="MPUserEntity" representedClassName="MPUserEntity" syncable="YES">
<attribute name="avatar_" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
<attribute name="defaultType_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
<attribute name="keyID" optional="YES" attributeType="Binary" syncable="YES"/>
<attribute name="lastUsed" optional="YES" attributeType="Date" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="saveKey_" attributeType="Boolean" defaultValueString="NO">
<userInfo/>
</attribute>
<relationship name="sites" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="MPSiteEntity" inverseName="user" inverseEntity="MPSiteEntity" elementID="D18D6772-040E-4CFE-8F32-A34B08E9E9BC" syncable="YES"/>
</entity>
<elements>
<element name="MPGeneratedSiteEntity" positionX="216" positionY="-288" width="128" height="58"/>
<element name="MPSiteEntity" positionX="-0" positionY="-286" width="128" height="208"/>
<element name="MPSiteQuestionEntity" positionX="-2" positionY="-9" width="128" height="73"/>
<element name="MPStoredSiteEntity" positionX="214" positionY="-171" width="128" height="58"/>
<element name="MPUserEntity" positionX="-218" positionY="-288" width="128" height="148"/>
</elements>
</model>

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="6254" systemVersion="14C109" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
<entity name="MPGeneratedSiteEntity" representedClassName="MPGeneratedSiteEntity" parentEntity="MPSiteEntity" elementID="789304EA-070D-4982-8C20-54EECFC20CB6" syncable="YES">
<attribute name="counter_" optional="YES" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
</entity>
<entity name="MPSiteEntity" representedClassName="MPSiteEntity" isAbstract="YES" elementID="58EE245C-6827-4C11-BB7E-5722F2426EC2" syncable="YES">
<attribute name="content" optional="YES" transient="YES" attributeType="Transformable" syncable="YES"/>
<attribute name="lastUsed" attributeType="Date" indexed="YES" syncable="YES"/>
<attribute name="loginGenerated_" attributeType="Boolean" defaultValueString="NO" syncable="YES"/>
<attribute name="loginName" optional="YES" attributeType="String" elementID="A1B9F981-D33C-4BFE-9F94-C9D3E1F78E51" syncable="YES"/>
<attribute name="name" attributeType="String" minValueString="1" indexed="YES" syncable="YES"/>
<attribute name="requiresExplicitMigration_" attributeType="Boolean" defaultValueString="NO">
<userInfo/>
</attribute>
<attribute name="type_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
<attribute name="uses_" attributeType="Integer 16" defaultValueString="0" indexed="YES" syncable="YES"/>
<attribute name="version_" attributeType="Integer 16" minValueString="0" defaultValueString="0" syncable="YES"/>
<relationship name="questions" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="MPSiteQuestionEntity" inverseName="site" inverseEntity="MPSiteQuestionEntity" syncable="YES"/>
<relationship name="user" minCount="1" maxCount="1" deletionRule="Nullify" destinationEntity="MPUserEntity" inverseName="sites" inverseEntity="MPUserEntity" elementID="FC8AE32E-7BE3-4FA6-8611-B7DC0DB063EF" syncable="YES"/>
</entity>
<entity name="MPSiteQuestionEntity" representedClassName="MPSiteQuestionEntity" syncable="YES">
<attribute name="keyword" attributeType="String" syncable="YES"/>
<relationship name="site" maxCount="1" deletionRule="Nullify" destinationEntity="MPSiteEntity" inverseName="questions" inverseEntity="MPSiteEntity" syncable="YES"/>
</entity>
<entity name="MPStoredSiteEntity" representedClassName="MPStoredSiteEntity" parentEntity="MPSiteEntity" elementID="BEEF1688-0CCD-4699-A86A-4D860FE2CEB8" syncable="YES">
<attribute name="contentObject" optional="YES" attributeType="Transformable" storedInTruthFile="YES" syncable="YES"/>
</entity>
<entity name="MPUserEntity" representedClassName="MPUserEntity" syncable="YES">
<attribute name="avatar_" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
<attribute name="defaultType_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
<attribute name="keyID" optional="YES" attributeType="Binary" syncable="YES"/>
<attribute name="lastUsed" optional="YES" attributeType="Date" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="saveKey_" attributeType="Boolean" defaultValueString="NO">
<userInfo/>
</attribute>
<attribute name="version_" attributeType="Integer 16" minValueString="0" defaultValueString="2" syncable="YES"/>
<relationship name="sites" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="MPSiteEntity" inverseName="user" inverseEntity="MPSiteEntity" elementID="D18D6772-040E-4CFE-8F32-A34B08E9E9BC" syncable="YES"/>
</entity>
<elements>
<element name="MPGeneratedSiteEntity" positionX="216" positionY="-288" width="128" height="58"/>
<element name="MPSiteEntity" positionX="-0" positionY="-286" width="128" height="208"/>
<element name="MPSiteQuestionEntity" positionX="-2" positionY="-9" width="128" height="73"/>
<element name="MPStoredSiteEntity" positionX="214" positionY="-171" width="128" height="58"/>
<element name="MPUserEntity" positionX="-218" positionY="-288" width="128" height="165"/>
</elements>
</model>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="9525" systemVersion="15C50" minimumToolsVersion="Automatic">
<entity name="MPGeneratedSiteEntity" representedClassName="MPGeneratedSiteEntity" parentEntity="MPSiteEntity" elementID="789304EA-070D-4982-8C20-54EECFC20CB6" syncable="YES">
<attribute name="counter_" optional="YES" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
</entity>
<entity name="MPSiteEntity" representedClassName="MPSiteEntity" isAbstract="YES" elementID="58EE245C-6827-4C11-BB7E-5722F2426EC2" syncable="YES">
<attribute name="content" optional="YES" transient="YES" attributeType="Transformable" syncable="YES"/>
<attribute name="lastUsed" attributeType="Date" indexed="YES" syncable="YES"/>
<attribute name="loginGenerated_" attributeType="Boolean" defaultValueString="NO" syncable="YES"/>
<attribute name="loginName" optional="YES" attributeType="String" elementID="A1B9F981-D33C-4BFE-9F94-C9D3E1F78E51" syncable="YES"/>
<attribute name="name" attributeType="String" minValueString="1" indexed="YES" syncable="YES"/>
<attribute name="requiresExplicitMigration_" attributeType="Boolean" defaultValueString="NO">
<userInfo/>
</attribute>
<attribute name="type_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
<attribute name="uses_" attributeType="Integer 16" defaultValueString="0" indexed="YES" syncable="YES"/>
<attribute name="version_" attributeType="Integer 16" minValueString="0" defaultValueString="0" syncable="YES"/>
<relationship name="questions" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="MPSiteQuestionEntity" inverseName="site" inverseEntity="MPSiteQuestionEntity" syncable="YES"/>
<relationship name="user" minCount="1" maxCount="1" deletionRule="Nullify" destinationEntity="MPUserEntity" inverseName="sites" inverseEntity="MPUserEntity" elementID="FC8AE32E-7BE3-4FA6-8611-B7DC0DB063EF" syncable="YES"/>
</entity>
<entity name="MPSiteQuestionEntity" representedClassName="MPSiteQuestionEntity" syncable="YES">
<attribute name="keyword" attributeType="String" syncable="YES"/>
<relationship name="site" maxCount="1" deletionRule="Nullify" destinationEntity="MPSiteEntity" inverseName="questions" inverseEntity="MPSiteEntity" syncable="YES"/>
</entity>
<entity name="MPStoredSiteEntity" representedClassName="MPStoredSiteEntity" parentEntity="MPSiteEntity" elementID="BEEF1688-0CCD-4699-A86A-4D860FE2CEB8" syncable="YES">
<attribute name="contentObject" optional="YES" attributeType="Transformable" storedInTruthFile="YES" syncable="YES"/>
</entity>
<entity name="MPUserEntity" representedClassName="MPUserEntity" syncable="YES">
<attribute name="avatar_" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
<attribute name="defaultType_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
<attribute name="keyID" optional="YES" attributeType="Binary" syncable="YES"/>
<attribute name="lastUsed" optional="YES" attributeType="Date" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="saveKey_" attributeType="Boolean" defaultValueString="NO">
<userInfo/>
</attribute>
<attribute name="touchID_" attributeType="Boolean" defaultValueString="0">
<userInfo/>
</attribute>
<attribute name="version_" attributeType="Integer 16" minValueString="0" defaultValueString="2" syncable="YES"/>
<relationship name="sites" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="MPSiteEntity" inverseName="user" inverseEntity="MPSiteEntity" elementID="D18D6772-040E-4CFE-8F32-A34B08E9E9BC" syncable="YES"/>
</entity>
<elements>
<element name="MPGeneratedSiteEntity" positionX="216" positionY="-288" width="128" height="58"/>
<element name="MPSiteEntity" positionX="-0" positionY="-286" width="128" height="208"/>
<element name="MPSiteQuestionEntity" positionX="-2" positionY="-9" width="128" height="73"/>
<element name="MPStoredSiteEntity" positionX="214" positionY="-171" width="128" height="58"/>
<element name="MPUserEntity" positionX="-218" positionY="-288" width="128" height="178"/>
</elements>
</model>

View File

@@ -0,0 +1,28 @@
//
// Prefix header for all source files of the 'Pearl' target in the 'Pearl' project
//
#ifdef __OBJC__
#import <Foundation/Foundation.h>
#define PEARL_WITH_SCRYPT
#define PEARL_WITH_MESSAGEUI
#define PEARL
#define PEARL_CRYPTO
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
#define PEARL_UIKIT
#endif
#ifdef __MAC_OS_X_VERSION_MIN_REQUIRED
#define PEARL_COCOA
#endif
#import "Pearl.h"
#import "Pearl-Crypto.h"
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
#import "Pearl-UIKit.h"
#endif
#ifdef __MAC_OS_X_VERSION_MIN_REQUIRED
#import "Pearl-Cocoa.h"
#endif
#endif

View File

@@ -0,0 +1,46 @@
//
// MPPreferencesViewController.h
// MasterPassword-iOS
//
// Created by Maarten Billemont on 04/06/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "MPTypeViewController.h"
#import "MPSiteQuestionEntity.h"
@interface MPAnswersViewController : UIViewController
@property (nonatomic) IBOutlet UITableView *tableView;
- (void)setSite:(MPSiteEntity *)site;
- (MPSiteEntity *)siteInContext:(NSManagedObjectContext *)context;
@end
@interface MPGlobalAnswersCell : UITableViewCell
@property (nonatomic) IBOutlet UILabel *titleLabel;
@property (nonatomic) IBOutlet UITextField *answerField;
- (void)setSite:(MPSiteEntity *)site;
@end
@interface MPSendAnswersCell : UITableViewCell
@end
@interface MPMultipleAnswersCell : UITableViewCell <UITextFieldDelegate>
@end
@interface MPAnswersQuestionCell : UITableViewCell
@property(nonatomic) IBOutlet UITextField *questionField;
@property(nonatomic) IBOutlet UITextField *answerField;
- (void)setQuestion:(MPSiteQuestionEntity *)question forSite:(MPSiteEntity *)site inVC:(MPAnswersViewController *)VC;
@end

View File

@@ -0,0 +1,349 @@
//
// MPPreferencesViewController.m
// MasterPassword-iOS
//
// Created by Maarten Billemont on 04/06/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "MPAnswersViewController.h"
#import "MPiOSAppDelegate.h"
#import "MPAppDelegate_Store.h"
#import "MPOverlayViewController.h"
@interface MPAnswersViewController()
@end
@implementation MPAnswersViewController {
NSManagedObjectID *_siteOID;
BOOL _multiple;
}
#pragma mark - Life
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.tableHeaderView = [UIView new];
self.tableView.tableFooterView = [UIView new];
self.view.backgroundColor = [UIColor clearColor];
[self.tableView automaticallyAdjustInsetsForKeyboard];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
PearlAddNotificationObserver( MPSignedOutNotification, nil, [NSOperationQueue mainQueue],
^(MPAnswersViewController *self, NSNotification *note) {
if (![note.userInfo[@"animated"] boolValue])
[UIView setAnimationsEnabled:NO];
[[MPOverlaySegue dismissViewController:self] perform];
[UIView setAnimationsEnabled:YES];
} );
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.view.window endEditing:YES];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
PearlRemoveNotificationObserversFrom( self.tableView );
PearlRemoveNotificationObservers();
}
- (UIStatusBarStyle)preferredStatusBarStyle {
return UIStatusBarStyleLightContent;
}
#pragma mark - State
- (void)setSite:(MPSiteEntity *)site {
_siteOID = [site objectID];
_multiple = [site.questions count] > 0;
[self.tableView reloadData];
[self updateAnimated:NO];
}
- (void)setMultiple:(BOOL)multiple animated:(BOOL)animated {
_multiple = multiple;
[self updateAnimated:animated];
}
- (MPSiteEntity *)siteInContext:(NSManagedObjectContext *)context {
return [MPSiteEntity existingObjectWithID:_siteOID inContext:context];
}
#pragma mark - UITableViewDelegate
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 2;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (section == 0)
return 3;
if (!_multiple)
return 0;
return [[self siteInContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]].questions count] + 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MPSiteEntity *site = [self siteInContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]];
if (indexPath.section == 0) {
if (indexPath.item == 0) {
MPGlobalAnswersCell *cell = [MPGlobalAnswersCell dequeueCellFromTableView:tableView indexPath:indexPath];
[cell setSite:site];
return cell;
}
if (indexPath.item == 1)
return [MPSendAnswersCell dequeueCellFromTableView:tableView indexPath:indexPath];
if (indexPath.item == 2) {
MPMultipleAnswersCell *cell = [MPMultipleAnswersCell dequeueCellFromTableView:tableView indexPath:indexPath];
cell.accessoryType = _multiple? UITableViewCellAccessoryCheckmark: UITableViewCellAccessoryNone;
return cell;
}
Throw( @"Unsupported row index: %@", indexPath );
}
MPAnswersQuestionCell *cell = [MPAnswersQuestionCell dequeueCellFromTableView:tableView indexPath:indexPath];
MPSiteQuestionEntity *question = nil;
if ([site.questions count] > indexPath.item)
question = site.questions[indexPath.item];
[cell setQuestion:question forSite:site inVC:self];
return cell;
}
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.section == 0) {
if (indexPath.item == 0)
return 133;
return 44;
}
return 130;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
MPSiteEntity *site = [self siteInContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]];
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
if ([cell isKindOfClass:[MPGlobalAnswersCell class]]) {
[PearlOverlay showTemporaryOverlayWithTitle:strl( @"Answer Copied" ) dismissAfter:2];
[UIPasteboard generalPasteboard].string = ((MPGlobalAnswersCell *)cell).answerField.text;
}
else if ([cell isKindOfClass:[MPMultipleAnswersCell class]]) {
if (!_multiple)
[self setMultiple:YES animated:YES];
else if (_multiple) {
if (![site.questions count])
[self setMultiple:NO animated:YES];
else
[PearlAlert showAlertWithTitle:@"Remove Site Questions?" message:
@"Do you want to remove the questions you have configured for this site?"
viewStyle:UIAlertViewStyleDefault initAlert:nil
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
if (buttonIndex == [alert cancelButtonIndex])
return;
[MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
MPSiteEntity *site_ = [self siteInContext:context];
NSOrderedSet *questions = [site_.questions copy];
for (MPSiteQuestionEntity *question in questions)
[context deleteObject:question];
[context saveToStore];
[self setMultiple:NO animated:YES];
}];
} cancelTitle:@"Cancel" otherTitles:@"Remove Questions", nil];
}
}
else if ([cell isKindOfClass:[MPSendAnswersCell class]]) {
NSString *body;
if (!_multiple) {
NSObject *answer = [site resolveSiteAnswerUsingKey:[MPiOSAppDelegate get].key];
body = strf( @"Master Password generated the following security answer for your site: %@\n\n"
@"%@\n"
@"\n\nYou should use this as the answer to each security question the site asks you.\n"
@"Do not share this answer with others!", site.name, answer );
}
else {
NSMutableString *bodyBuilder = [NSMutableString string];
[bodyBuilder appendFormat:@"Master Password generated the following security answers for your site: %@\n\n", site.name];
for (MPSiteQuestionEntity *question in site.questions) {
NSObject *answer = [question resolveQuestionAnswerUsingKey:[MPiOSAppDelegate get].key];
[bodyBuilder appendFormat:@"For question: '%@', use answer: %@\n", question.keyword, answer];
}
[bodyBuilder appendFormat:@"\n\nUse the answer for the matching security question.\n"
@"Do not share this answer with others!"];
body = bodyBuilder;
}
[PearlEMail sendEMailTo:nil fromVC:self subject:strf( @"Master Password security answers for %@", site.name ) body:body];
}
else if ([cell isKindOfClass:[MPAnswersQuestionCell class]]) {
[PearlOverlay showTemporaryOverlayWithTitle:strl( @"Answer Copied" ) dismissAfter:2];
[UIPasteboard generalPasteboard].string = ((MPAnswersQuestionCell *)cell).answerField.text;
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
#pragma mark - Private
- (void)updateAnimated:(BOOL)animated {
PearlMainQueue( ^{
UITableViewCell *multipleAnswersCell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForItem:2 inSection:0]];
multipleAnswersCell.accessoryType = _multiple? UITableViewCellAccessoryCheckmark: UITableViewCellAccessoryNone;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationAutomatic];
}];
} );
}
- (void)didAddQuestion:(MPSiteQuestionEntity *)question toSite:(MPSiteEntity *)site {
NSUInteger newQuestionRow = [site.questions count];
PearlMainQueue( ^{
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:@[ [NSIndexPath indexPathForRow:newQuestionRow inSection:1] ]
withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
} );
}
@end
@implementation MPGlobalAnswersCell
#pragma mark - State
- (void)setSite:(MPSiteEntity *)site {
self.titleLabel.text = strl( @"Answer for %@:", site.name );
self.answerField.text = @"...";
[site resolveSiteAnswerUsingKey:[MPiOSAppDelegate get].key result:^(NSString *result) {
PearlMainQueue( ^{
self.answerField.text = result;
} );
}];
}
@end
@implementation MPSendAnswersCell
@end
@implementation MPMultipleAnswersCell
@end
@implementation MPAnswersQuestionCell {
NSManagedObjectID *_siteOID;
NSManagedObjectID *_questionOID;
__weak MPAnswersViewController *_answersVC;
}
#pragma mark - State
- (void)setQuestion:(MPSiteQuestionEntity *)question forSite:(MPSiteEntity *)site inVC:(MPAnswersViewController *)answersVC {
_siteOID = site.objectID;
_questionOID = question.objectID;
_answersVC = answersVC;
[self updateAnswerForQuestion:question ofSite:site];
}
#pragma mark - UITextFieldDelegate
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[textField resignFirstResponder];
return NO;
}
- (IBAction)textFieldDidChange:(UITextField *)textField {
NSString *keyword = textField.text;
[MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
BOOL didAddQuestionObject = NO;
MPSiteEntity *site = [MPSiteEntity existingObjectWithID:_siteOID inContext:context];
MPSiteQuestionEntity *question = [MPSiteQuestionEntity existingObjectWithID:_questionOID inContext:context];
if (!question) {
didAddQuestionObject = YES;
[site addQuestionsObject:question = [MPSiteQuestionEntity insertNewObjectInContext:context]];
question.site = site;
}
question.keyword = keyword;
if ([context saveToStore]) {
if ([question.objectID isTemporaryID]) {
NSError *error = nil;
[context obtainPermanentIDsForObjects:@[ question ] error:&error];
if (error)
err( @"Failed to obtain permanent object ID: %@", [error fullDescription] );
}
_questionOID = question.objectID;
[self updateAnswerForQuestion:question ofSite:site];
if (didAddQuestionObject)
[_answersVC didAddQuestion:question toSite:site];
}
}];
}
#pragma mark - Private
- (void)updateAnswerForQuestion:(MPSiteQuestionEntity *)question ofSite:(MPSiteEntity *)site {
if (!question)
PearlMainQueue( ^{
self.questionField.text = self.answerField.text = nil;
} );
else {
NSString *keyword = question.keyword;
PearlMainQueue( ^{
self.answerField.text = @"...";
} );
[question resolveQuestionAnswerUsingKey:[MPiOSAppDelegate get].key result:^(NSString *result) {
PearlMainQueue( ^{
self.questionField.text = keyword;
self.answerField.text = result;
} );
}];
}
}
@end

View File

@@ -0,0 +1,23 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPAppSettingsViewController.h
// MPAppSettingsViewController
//
// Created by lhunath on 2014-04-18.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "IASKAppSettingsViewController.h"
@interface MPAppSettingsViewController : IASKAppSettingsViewController
@end

View File

@@ -0,0 +1,46 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPAppSettingsViewController.h
// MPAppSettingsViewController
//
// Created by lhunath on 2014-04-18.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import "MPAppSettingsViewController.h"
#import "UIColor+Expanded.h"
@implementation MPAppSettingsViewController {
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.tableView.contentInset = UIEdgeInsetsMake( 64, 0, 49, 0 );
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath];
cell.backgroundColor = [UIColor clearColor];
cell.textLabel.textColor = [UIColor whiteColor];
if (cell.selectionStyle != UITableViewCellSelectionStyleNone) {
cell.selectedBackgroundView = [[UIView alloc] initWithFrame:cell.bounds];
cell.selectedBackgroundView.backgroundColor = [UIColor colorWithRGBAHex:0x78DDFB33];
}
return cell;
}
@end

View File

@@ -0,0 +1,47 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPAvatarCell.h
// MPAvatarCell
//
// Created by lhunath on 2014-03-11.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "MPEntities.h"
@class MPAvatarCell;
/* Avatar with a "+" symbol. */
extern const long MPAvatarAdd;
typedef NS_ENUM(NSUInteger, MPAvatarMode) {
MPAvatarModeLowered,
MPAvatarModeRaisedButInactive,
MPAvatarModeRaisedAndActive,
MPAvatarModeRaisedAndHidden,
MPAvatarModeRaisedAndMinimized,
};
@interface MPAvatarCell : UICollectionViewCell
@property (copy, nonatomic) NSString *name;
@property (assign, nonatomic) NSUInteger avatar;
@property (assign, nonatomic) MPAvatarMode mode;
@property (assign, nonatomic) CGFloat visibility;
@property (assign, nonatomic) BOOL spinnerActive;
@property (assign, nonatomic, readonly) BOOL newUser;
+ (NSString *)reuseIdentifier;
- (void)setVisibility:(CGFloat)visibility animated:(BOOL)animated;
- (void)setMode:(MPAvatarMode)mode animated:(BOOL)animated;
@end

View File

@@ -0,0 +1,302 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPAvatarCell.h
// MPAvatarCell
//
// Created by lhunath on 2014-03-11.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import "MPAvatarCell.h"
const long MPAvatarAdd = 10000;
@interface MPAvatarCell()
@property(strong, nonatomic) IBOutlet UIImageView *avatarImageView;
@property(strong, nonatomic) IBOutlet UILabel *nameLabel;
@property(strong, nonatomic) IBOutlet UIView *nameContainer;
@property(strong, nonatomic) IBOutlet UIImageView *spinner;
@property(strong, nonatomic) IBOutlet NSLayoutConstraint *nameToCenterConstraint;
@property(strong, nonatomic) IBOutlet NSLayoutConstraint *avatarSizeConstraint;
@property(strong, nonatomic) IBOutlet NSLayoutConstraint *avatarToTopConstraint;
@property(strong, nonatomic) IBOutlet NSLayoutConstraint *avatarRaisedConstraint;
@property(strong, nonatomic) IBOutlet NSLayoutConstraint *keyboardHeightConstraint;
@end
@implementation MPAvatarCell {
CAAnimationGroup *_targetedShadowAnimation;
}
+ (NSString *)reuseIdentifier {
return NSStringFromClass( self );
}
#pragma mark - Life cycle
- (void)awakeFromNib {
[super awakeFromNib];
self.alpha = 0;
self.nameContainer.layer.cornerRadius = 5;
self.avatarImageView.hidden = NO;
self.avatarImageView.layer.cornerRadius = self.avatarImageView.bounds.size.height / 2;
self.avatarImageView.layer.masksToBounds = NO;
self.avatarImageView.backgroundColor = [UIColor clearColor];
[self observeKeyPath:@"bounds" withBlock:^(id from, id to, NSKeyValueChange cause, MPAvatarCell *_self) {
_self.contentView.frame = _self.bounds;
}];
[self observeKeyPath:@"selected" withBlock:^(id from, id to, NSKeyValueChange cause, MPAvatarCell *_self) {
[_self updateAnimated:_self.superview != nil];
}];
[self observeKeyPath:@"highlighted" withBlock:^(id from, id to, NSKeyValueChange cause, MPAvatarCell *_self) {
[_self updateAnimated:_self.superview != nil];
}];
PearlAddNotificationObserver( UIKeyboardWillShowNotification, nil, [NSOperationQueue mainQueue],
^(MPAvatarCell *self, NSNotification *note) {
CGRect keyboardRect = [note.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGFloat keyboardHeight = CGRectGetHeight( self.window.screen.bounds ) - CGRectGetMinY( keyboardRect );
[self.keyboardHeightConstraint updateConstant:keyboardHeight];
} );
CABasicAnimation *toShadowOpacityAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"];
toShadowOpacityAnimation.toValue = @0.2f;
toShadowOpacityAnimation.duration = 0.5f;
CABasicAnimation *pulseShadowOpacityAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"];
pulseShadowOpacityAnimation.fromValue = @0.2f;
pulseShadowOpacityAnimation.toValue = @0.6f;
pulseShadowOpacityAnimation.beginTime = 0.5f;
pulseShadowOpacityAnimation.duration = 2.0f;
pulseShadowOpacityAnimation.autoreverses = YES;
pulseShadowOpacityAnimation.repeatCount = MAXFLOAT;
_targetedShadowAnimation = [CAAnimationGroup new];
_targetedShadowAnimation.animations = @[ toShadowOpacityAnimation, pulseShadowOpacityAnimation ];
_targetedShadowAnimation.duration = MAXFLOAT;
self.avatarImageView.layer.shadowColor = [UIColor whiteColor].CGColor;
self.avatarImageView.layer.shadowOffset = CGSizeZero;
}
- (void)prepareForReuse {
[super prepareForReuse];
_newUser = NO;
_visibility = 0;
_mode = MPAvatarModeLowered;
_spinnerActive = NO;
}
- (void)dealloc {
[self removeKeyPathObservers];
PearlRemoveNotificationObservers();
}
#pragma mark - Properties
- (void)setAvatar:(NSUInteger)avatar {
_avatar = avatar == MPAvatarAdd? MPAvatarAdd: (avatar + MPAvatarCount) % MPAvatarCount;
if (_avatar == MPAvatarAdd) {
self.avatarImageView.image = [UIImage imageNamed:@"avatar-add"];
self.name = strl( @"New User" );
_newUser = YES;
}
else
self.avatarImageView.image = [UIImage imageNamed:strf( @"avatar-%lu", (unsigned long)_avatar )];
}
- (NSString *)name {
return self.nameLabel.text;
}
- (void)setName:(NSString *)name {
self.nameLabel.text = name;
}
- (void)setVisibility:(CGFloat)visibility {
[self setVisibility:visibility animated:YES];
}
- (void)setVisibility:(CGFloat)visibility animated:(BOOL)animated {
if (visibility == _visibility)
return;
_visibility = visibility;
[self updateAnimated:animated];
}
- (void)setHighlighted:(BOOL)highlighted {
super.highlighted = highlighted;
self.avatarImageView.transform = highlighted? CGAffineTransformMakeScale( 1.1f, 1.1f ): CGAffineTransformIdentity;
}
- (void)setMode:(MPAvatarMode)mode {
[self setMode:mode animated:YES];
}
- (void)setMode:(MPAvatarMode)mode animated:(BOOL)animated {
if (mode == _mode)
return;
_mode = mode;
[self updateAnimated:animated];
}
- (void)setSpinnerActive:(BOOL)spinnerActive {
[self setSpinnerActive:spinnerActive animated:YES];
}
- (void)setSpinnerActive:(BOOL)spinnerActive animated:(BOOL)animated {
if (_spinnerActive == spinnerActive)
return;
_spinnerActive = spinnerActive;
CABasicAnimation *rotate = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
rotate.toValue = @(2 * M_PI);
rotate.duration = 5.0;
if (spinnerActive) {
rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
rotate.fromValue = @0.0;
rotate.repeatCount = MAXFLOAT;
}
else {
rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
rotate.repeatCount = 1;
}
[self.spinner.layer removeAnimationForKey:@"rotation"];
[self.spinner.layer addAnimation:rotate forKey:@"rotation"];
[self updateAnimated:animated];
}
#pragma mark - Private
- (void)updateAnimated:(BOOL)animated {
[self.contentView layoutIfNeeded];
[UIView animateWithDuration:animated? 0.2f: 0 animations:^{
self.avatarImageView.transform = CGAffineTransformIdentity;
}];
[UIView animateWithDuration:animated? 0.5f: 0 delay:0
options:UIViewAnimationOptionOverrideInheritedDuration | UIViewAnimationOptionBeginFromCurrentState
animations:^{
self.alpha = 1;
if (self.newUser) {
if (self.mode == MPAvatarModeLowered)
self.avatar = MPAvatarAdd;
else if (self.avatar == MPAvatarAdd)
self.avatar = arc4random() % MPAvatarCount;
}
switch (self.mode) {
case MPAvatarModeLowered: {
[self.avatarSizeConstraint updateConstant:
self.avatarImageView.image.size.height * (self.visibility * 0.3f + 0.7f)];
[self.avatarRaisedConstraint updatePriority:UILayoutPriorityDefaultLow];
[self.avatarToTopConstraint updatePriority:UILayoutPriorityDefaultLow];
[self.nameToCenterConstraint updatePriority:UILayoutPriorityDefaultLow];
self.nameContainer.alpha = self.visibility;
self.nameContainer.backgroundColor = [UIColor clearColor];
self.avatarImageView.alpha = self.visibility * 0.7f + 0.3f;
self.avatarImageView.layer.shadowRadius = 15 * self.visibility * self.visibility;
break;
}
case MPAvatarModeRaisedButInactive: {
[self.avatarSizeConstraint updateConstant:
self.avatarImageView.image.size.height * (self.visibility * 0.7f + 0.3f)];
[self.avatarRaisedConstraint updatePriority:UILayoutPriorityDefaultHigh];
[self.avatarToTopConstraint updatePriority:UILayoutPriorityDefaultLow];
[self.nameToCenterConstraint updatePriority:UILayoutPriorityDefaultLow];
self.nameContainer.alpha = self.visibility;
self.nameContainer.backgroundColor = [UIColor clearColor];
self.avatarImageView.alpha = 0;
self.avatarImageView.layer.shadowRadius = 15 * self.visibility * self.visibility;
break;
}
case MPAvatarModeRaisedAndActive: {
[self.avatarSizeConstraint updateConstant:
self.avatarImageView.image.size.height * (self.visibility * 0.7f + 0.3f)];
[self.avatarRaisedConstraint updatePriority:UILayoutPriorityDefaultHigh];
[self.avatarToTopConstraint updatePriority:UILayoutPriorityDefaultLow];
[self.nameToCenterConstraint updatePriority:UILayoutPriorityDefaultHigh];
self.nameContainer.alpha = self.visibility;
self.nameContainer.backgroundColor = [UIColor blackColor];
self.avatarImageView.alpha = 1;
self.avatarImageView.layer.shadowRadius = 15 * self.visibility * self.visibility;
break;
}
case MPAvatarModeRaisedAndHidden: {
[self.avatarSizeConstraint updateConstant:
self.avatarImageView.image.size.height * (self.visibility * 0.7f + 0.3f)];
[self.avatarRaisedConstraint updatePriority:UILayoutPriorityDefaultHigh];
[self.avatarToTopConstraint updatePriority:UILayoutPriorityDefaultLow];
[self.nameToCenterConstraint updatePriority:UILayoutPriorityDefaultHigh];
self.nameContainer.alpha = 0;
self.nameContainer.backgroundColor = [UIColor blackColor];
self.avatarImageView.alpha = 0;
self.avatarImageView.layer.shadowRadius = 15 * self.visibility * self.visibility;
break;
}
case MPAvatarModeRaisedAndMinimized: {
[self.avatarSizeConstraint updateConstant:36];
[self.avatarRaisedConstraint updatePriority:UILayoutPriorityDefaultLow];
[self.avatarToTopConstraint updatePriority:UILayoutPriorityDefaultHigh + 2];
[self.nameToCenterConstraint updatePriority:UILayoutPriorityDefaultHigh];
self.nameContainer.alpha = 0;
self.nameContainer.backgroundColor = [UIColor blackColor];
self.avatarImageView.alpha = 1;
break;
}
}
// Avatar minimized.
if (self.mode == MPAvatarModeRaisedAndMinimized)
[self.avatarImageView.layer removeAnimationForKey:@"targetedShadow"];
else if (![self.avatarImageView.layer animationForKey:@"targetedShadow"])
[self.avatarImageView.layer addAnimation:_targetedShadowAnimation forKey:@"targetedShadow"];
// Avatar selection and spinner.
if (self.mode != MPAvatarModeRaisedAndMinimized && (self.selected || self.highlighted) && !self.spinnerActive)
self.avatarImageView.backgroundColor = self.avatarImageView.tintColor;
else
self.avatarImageView.backgroundColor = [UIColor clearColor];
self.avatarImageView.layer.cornerRadius = self.avatarImageView.bounds.size.height / 2;
self.spinner.alpha = self.spinnerActive? 1: 0;
[self.contentView layoutIfNeeded];
} completion:nil];
}
@end

View File

@@ -0,0 +1,23 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPCell.h
// MPCell
//
// Created by lhunath on 2014-03-27.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface MPCell : UICollectionViewCell
@end

View File

@@ -0,0 +1,24 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPCell.h
// MPCell
//
// Created by lhunath on 2014-03-27.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import "MPCell.h"
@implementation MPCell {
}
@end

View File

@@ -0,0 +1,48 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPCoachmarkViewController.h
// MPCoachmarkViewController
//
// Created by lhunath on 2014-04-22.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface MPCoachmark : NSObject
@property(nonatomic, strong) Class coachedClass;
@property(nonatomic) NSInteger coachedVersion;
@property(nonatomic) BOOL coached;
+ (instancetype)coachmarkForClass:(Class)class version:(NSInteger)version;
@end
@interface MPCoachmarkViewController : UIViewController
@property(nonatomic, strong) MPCoachmark *coachmark;
@property(nonatomic, strong) IBOutlet UIView *view0;
@property(nonatomic, strong) IBOutlet UIView *view1;
@property(nonatomic, strong) IBOutlet UIView *view2;
@property(nonatomic, strong) IBOutlet UIView *view3;
@property(nonatomic, strong) IBOutlet UIView *view4;
@property(nonatomic, strong) IBOutlet UIView *view5;
@property(nonatomic, strong) IBOutlet UIView *view6;
@property(nonatomic, strong) IBOutlet UIView *view7;
@property(nonatomic, strong) IBOutlet UIView *view8;
@property(nonatomic, strong) IBOutlet UIView *view9;
@property(nonatomic, strong) IBOutlet UIProgressView *viewProgress;
- (IBAction)close:(id)sender;
@end

View File

@@ -0,0 +1,106 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPCoachmarkViewController.h
// MPCoachmarkViewController
//
// Created by lhunath on 2014-04-22.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import "MPCoachmarkViewController.h"
@implementation MPCoachmarkViewController {
NSArray *_views;
NSUInteger _nextView;
__weak NSTimer *_viewTimer;
}
- (void)viewDidLoad {
[super viewDidLoad];
_views = [NSArray arrayWithObjects:
self.view0, self.view1, self.view2, self.view3, self.view4, self.view5, self.view6, self.view7, self.view8, self.view9, nil];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.viewProgress.hidden = NO;
self.viewProgress.progress = 0;
[_views makeObjectsPerformSelector:@selector( setAlpha: ) withObject:@0];
_nextView = 0;
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[UIView animateWithDuration:0.3f animations:^{
[_views[_nextView++] setAlpha:1];
}];
_viewTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 block:^(NSTimer *timer) {
self.viewProgress.progress += 1.0f / 50;
if (self.viewProgress.progress == 1)
[UIView animateWithDuration:0.3f animations:^{
self.viewProgress.progress = 0;
[_views[_nextView++] setAlpha:1];
if (_nextView >= [_views count]) {
[_viewTimer invalidate];
self.viewProgress.hidden = YES;
}
}];
} repeats:YES];
}
- (UIStatusBarStyle)preferredStatusBarStyle {
return UIStatusBarStyleLightContent;
}
- (IBAction)close:(id)sender {
[self dismissViewControllerAnimated:YES completion:^{
self.coachmark.coached = YES;
}];
}
@end
@implementation MPCoachmark
+ (instancetype)coachmarkForClass:(Class)coachedClass version:(NSInteger)coachedVersion {
MPCoachmark *coachmark = [self new];
coachmark.coachedClass = coachedClass;
coachmark.coachedVersion = coachedVersion;
return coachmark;
}
- (BOOL)coached {
return [[NSUserDefaults standardUserDefaults] boolForKey:strf( @"%@.%ld.coached", self.coachedClass, (long)self.coachedVersion )];
}
- (void)setCoached:(BOOL)coached {
[[NSUserDefaults standardUserDefaults] setBool:coached forKey:strf( @"%@.%ld.coached", self.coachedClass, (long)self.coachedVersion )];
if (![[NSUserDefaults standardUserDefaults] synchronize])
wrn( @"Couldn't synchronize after coachmark updates." );
}
@end

View File

@@ -0,0 +1,35 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPCombinedViewController.h
// MPCombinedViewController
//
// Created by lhunath on 2014-03-08.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import "MPUsersViewController.h"
#import "MPPasswordsViewController.h"
#import "MPEmergencyViewController.h"
typedef NS_ENUM(NSUInteger, MPCombinedMode) {
MPCombinedModeUserSelection,
MPCombinedModePasswordSelection,
};
@interface MPCombinedViewController : UIViewController
@property(nonatomic) MPCombinedMode mode;
@property(nonatomic, weak) MPUsersViewController *usersVC;
@property(nonatomic, weak) MPPasswordsViewController *passwordsVC;
@property(nonatomic, weak) MPEmergencyViewController *emergencyVC;
@end

View File

@@ -0,0 +1,150 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPCombinedViewController.h
// MPCombinedViewController
//
// Created by lhunath on 2014-03-08.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import "MPCombinedViewController.h"
#import "MPUsersViewController.h"
#import "MPPasswordsViewController.h"
#import "MPEmergencyViewController.h"
#import "MPPasswordsSegue.h"
@implementation MPCombinedViewController
#pragma mark - Life
- (void)viewDidLoad {
[super viewDidLoad];
_mode = MPCombinedModeUserSelection;
[self performSegueWithIdentifier:@"users" sender:self];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[[self navigationController] setNavigationBarHidden:YES animated:animated];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
PearlAddNotificationObserver( MPSignedInNotification, nil, [NSOperationQueue mainQueue],
^(MPCombinedViewController *self, NSNotification *note) {
[self setMode:MPCombinedModePasswordSelection];
} );
PearlAddNotificationObserver( MPSignedOutNotification, nil, [NSOperationQueue mainQueue],
^(MPCombinedViewController *self, NSNotification *note) {
[self setMode:MPCombinedModeUserSelection animated:[note.userInfo[@"animated"] boolValue]];
} );
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
PearlRemoveNotificationObservers();
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:@"users"])
self.usersVC = segue.destinationViewController;
if ([segue.identifier isEqualToString:@"passwords"]) {
NSAssert( [segue isKindOfClass:[MPPasswordsSegue class]], @"passwords segue should be MPPasswordsSegue: %@", segue );
NSAssert( [sender isKindOfClass:[NSDictionary class]], @"sender should be dictionary: %@", sender );
NSAssert( [[sender objectForKey:@"animated"] isKindOfClass:[NSNumber class]], @"sender should contain 'animated': %@", sender );
[(MPPasswordsSegue *)segue setAnimated:[sender[@"animated"] boolValue]];
UIViewController *destinationVC = segue.destinationViewController;
_passwordsVC = [destinationVC isKindOfClass:[MPPasswordsViewController class]]? (MPPasswordsViewController *)destinationVC: nil;
}
if ([segue.identifier isEqualToString:@"emergency"])
self.emergencyVC = segue.destinationViewController;
}
- (BOOL)prefersStatusBarHidden {
return self.mode == MPCombinedModeUserSelection;
}
- (UIStatusBarStyle)preferredStatusBarStyle {
return UIStatusBarStyleLightContent;
}
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event {
if (motion == UIEventSubtypeMotionShake && !self.emergencyVC)
[self performSegueWithIdentifier:@"emergency" sender:self];
}
#pragma mark - Actions
- (IBAction)unwindToCombined:(UIStoryboardSegue *)sender {
dbg( @"unwindToCombined:%@", sender );
}
#pragma mark - State
- (void)setMode:(MPCombinedMode)mode {
[self setMode:mode animated:YES];
}
- (void)setMode:(MPCombinedMode)mode animated:(BOOL)animated {
if (_mode == mode && animated)
return;
_mode = mode;
[self setNeedsStatusBarAppearanceUpdate];
[self becomeFirstResponder];
[self.usersVC setNeedsStatusBarAppearanceUpdate];
[self.usersVC.view setNeedsUpdateConstraints];
[self.usersVC.view setNeedsLayout];
switch (self.mode) {
case MPCombinedModeUserSelection: {
self.usersVC.view.userInteractionEnabled = YES;
[self.usersVC setActive:YES animated:animated];
if (_passwordsVC) {
MPPasswordsSegue *segue = [[MPPasswordsSegue alloc] initWithIdentifier:@"passwords" source:_passwordsVC destination:self];
[self prepareForSegue:segue sender:@{ @"animated" : @(animated) }];
[segue perform];
}
break;
}
case MPCombinedModePasswordSelection: {
self.usersVC.view.userInteractionEnabled = NO;
[self.usersVC setActive:NO animated:animated];
[self performSegueWithIdentifier:@"passwords" sender:@{ @"animated" : @(animated) }];
break;
}
}
}
#pragma mark - Private
@end

View File

@@ -0,0 +1,38 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPCombinedViewController.h
// MPCombinedViewController
//
// Created by lhunath on 2014-03-08.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
@interface MPEmergencyViewController : UIViewController <UITextFieldDelegate>
@property(weak, nonatomic) IBOutlet UIScrollView *scrollView;
@property(weak, nonatomic) IBOutlet UIView *dialogView;
@property(weak, nonatomic) IBOutlet UIView *containerView;
@property(weak, nonatomic) IBOutlet UITextField *fullNameField;
@property(weak, nonatomic) IBOutlet UITextField *masterPasswordField;
@property(weak, nonatomic) IBOutlet UITextField *siteField;
@property(weak, nonatomic) IBOutlet UIStepper *counterStepper;
@property(weak, nonatomic) IBOutlet UISegmentedControl *typeControl;
@property(weak, nonatomic) IBOutlet UILabel *counterLabel;
@property(weak, nonatomic) IBOutlet UIActivityIndicatorView *activity;
@property(weak, nonatomic) IBOutlet UILabel *passwordLabel;
@property(weak, nonatomic) IBOutlet UIView *tipContainer;
- (IBAction)controlChanged:(UIControl *)control;
- (IBAction)copyPassword:(UITapGestureRecognizer *)recognizer;
@end

View File

@@ -0,0 +1,176 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPCombinedViewController.h
// MPCombinedViewController
//
// Created by lhunath on 2014-03-08.
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
#import "MPEmergencyViewController.h"
#import "MPEntities.h"
@implementation MPEmergencyViewController {
MPKey *_key;
NSOperationQueue *_emergencyKeyQueue;
NSOperationQueue *_emergencyPasswordQueue;
}
- (void)viewDidLoad {
[super viewDidLoad];
[_emergencyKeyQueue = [NSOperationQueue new] setMaxConcurrentOperationCount:1];
[_emergencyPasswordQueue = [NSOperationQueue new] setMaxConcurrentOperationCount:1];
self.view.backgroundColor = [UIColor clearColor];
self.dialogView.layer.cornerRadius = 5;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self reset];
PearlAddNotificationObserver( UIApplicationWillResignActiveNotification, nil, [NSOperationQueue mainQueue],
^(MPEmergencyViewController *self, NSNotification *note) {
[self performSegueWithIdentifier:@"unwind-popover" sender:self];
} );
[self.scrollView automaticallyAdjustInsetsForKeyboard];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
PearlRemoveNotificationObservers();
PearlRemoveNotificationObserversFrom( self.scrollView );
[self reset];
}
- (UIStatusBarStyle)preferredStatusBarStyle {
return UIStatusBarStyleLightContent;
}
#pragma mark - UITextFieldDelegate
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[textField resignFirstResponder];
return YES;
}
#pragma mark - Actions
- (IBAction)controlChanged:(UIControl *)control {
if (control == self.fullNameField || control == self.masterPasswordField)
[self updateKey];
else
[self updatePassword];
}
- (IBAction)copyPassword:(UITapGestureRecognizer *)recognizer {
if (recognizer.state == UIGestureRecognizerStateEnded) {
NSString *sitePassword = self.passwordLabel.text;
if ([sitePassword length]) {
[UIPasteboard generalPasteboard].string = sitePassword;
[UIView animateWithDuration:0.3f animations:^{
self.tipContainer.alpha = 1;
} completion:^(BOOL finished) {
if (finished)
PearlMainQueueAfter( 3, ^{
self.tipContainer.alpha = 0;
} );
}];
}
}
}
#pragma mark - Private
- (void)updateKey {
NSString *fullName = self.fullNameField.text;
NSString *masterPassword = self.masterPasswordField.text;
self.passwordLabel.text = nil;
[self.activity startAnimating];
[_emergencyKeyQueue cancelAllOperations];
[_emergencyKeyQueue addOperationWithBlock:^{
if ([masterPassword length] && [fullName length])
_key = [[MPKey alloc] initForFullName:fullName withMasterPassword:masterPassword];
else
_key = nil;
PearlMainQueue( ^{
[self updatePassword];
} );
}];
}
- (void)updatePassword {
NSString *siteName = self.siteField.text;
MPSiteType siteType = [self siteType];
NSUInteger siteCounter = (NSUInteger)self.counterStepper.value;
self.counterLabel.text = strf( @"%lu", (unsigned long)siteCounter );
self.passwordLabel.text = nil;
[self.activity startAnimating];
[_emergencyPasswordQueue cancelAllOperations];
[_emergencyPasswordQueue addOperationWithBlock:^{
NSString *sitePassword = nil;
if (_key && [siteName length])
sitePassword = [MPAlgorithmDefault generatePasswordForSiteNamed:siteName ofType:siteType withCounter:siteCounter usingKey:_key];
PearlMainQueue( ^{
[self.activity stopAnimating];
self.passwordLabel.text = sitePassword;
} );
}];
}
- (enum MPSiteType)siteType {
switch (self.typeControl.selectedSegmentIndex) {
case 0:
return MPSiteTypeGeneratedMaximum;
case 1:
return MPSiteTypeGeneratedLong;
case 2:
return MPSiteTypeGeneratedMedium;
case 3:
return MPSiteTypeGeneratedBasic;
case 4:
return MPSiteTypeGeneratedShort;
case 5:
return MPSiteTypeGeneratedPIN;
default:
Throw( @"Unsupported type index: %ld", (long)self.typeControl.selectedSegmentIndex );
}
}
- (void)reset {
self.fullNameField.text = nil;
self.masterPasswordField.text = nil;
self.siteField.text = nil;
self.counterStepper.value = 1;
self.typeControl.selectedSegmentIndex = 1;
[self updateKey];
}
@end

View File

@@ -0,0 +1,20 @@
//
// MPGuideViewController.h
// MasterPassword
//
// Created by Maarten Billemont on 30/01/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface MPGuideViewController : UIViewController<UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
@property(nonatomic) IBOutlet UICollectionView *collectionView;
@property(nonatomic) IBOutlet UILabel *captionLabel;
@property(nonatomic) IBOutlet UIPageControl *pageControl;
@property(nonatomic) IBOutlet UINavigationBar *navigationBar;
- (IBAction)close:(id)sender;
@end

View File

@@ -0,0 +1,203 @@
//
// MPGuideViewController.m
// MasterPassword
//
// Created by Maarten Billemont on 30/01/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "MPGuideViewController.h"
#import "markdown_lib.h"
#import "NSString+MPMarkDown.h"
@interface MPGuideStep : NSObject
@property(nonatomic) UIImage *image;
@property(nonatomic) NSString *caption;
+ (instancetype)stepWithImage:(UIImage *)image caption:(NSString *)caption;
@end
@interface MPGuideStepCell : UICollectionViewCell
@property(nonatomic) IBOutlet UIImageView *imageView;
@end
@interface MPGuideViewController()
@property(nonatomic, strong) NSArray *steps;
@end
@implementation MPGuideViewController
#pragma mark - Life
- (void)viewDidLoad {
[super viewDidLoad];
self.steps = @[
[MPGuideStep stepWithImage:[UIImage imageNamed:@"initial"] caption:
@"To begin, tap the *New User* icon and add yourself as a user to the application."],
[MPGuideStep stepWithImage:[UIImage imageNamed:@"name_new"] caption:
@"Enter your full name. \n"
@"**Double-check** that you have spelled your name correctly and capitalized it appropriately. \n"
@"Your passwords will depend on it."],
[MPGuideStep stepWithImage:[UIImage imageNamed:@"mpw_new"] caption:
@"Choose a master password: Make it *new* and *long*. \n"
@"A short phrase makes a great password. \n"
@"**DO NOT FORGET THIS ONE PASSWORD**."],
[MPGuideStep stepWithImage:[UIImage imageNamed:@"login_new"] caption:
@"After logging in, you'll see an empty screen with a search box. \n"
@"Tap the search box to begin adding sites."],
[MPGuideStep stepWithImage:[UIImage imageNamed:@"site_new"] caption:
@"To add a site, just enter its name and tap the result. \n"
@"*We recommend* always using a site's **bare** domain name: eg. *apple.com*. \n"
@"(NOT *www.*apple.com or *store.*apple.com)"],
[MPGuideStep stepWithImage:[UIImage imageNamed:@"copy_pw"] caption:
@"Tap any site to copy its password. \n"
@"The first time, change your site's old password into this new one."],
[MPGuideStep stepWithImage:[UIImage imageNamed:@"settings"] caption:
@"To make changes to the site password, tap the settings icon or swipe left to reveal extra buttons."],
[MPGuideStep stepWithImage:[UIImage imageNamed:@"login_name"] caption:
@"You can save the login name for the site. \n"
@"This is useful if you find it hard to remember your user name for this site."],
[MPGuideStep stepWithImage:[UIImage imageNamed:@"counter"] caption:
@"If you ever need a new password for the site, just tap the plus icon to increment its counter. \n"
@"You can hold down to reset it back to 1."],
[MPGuideStep stepWithImage:[UIImage imageNamed:@"choose_type"] caption:
@"Use the list icon to upgrade or downgrade your password's complexity. \n"
@"Some sites won't let you use complex passwords."],
[MPGuideStep stepWithImage:[UIImage imageNamed:@"personal_pw"] caption:
@"If you have a password that you cannot change, you can save it as a *personal* password. "
@"*Device private* means the site will not be backed up."],
];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.pageControl observeKeyPath:@"currentPage"
withBlock:^(id from, id to, NSKeyValueChange cause, UIPageControl *pageControl) {
MPGuideStep *activeStep = self.steps[pageControl.currentPage];
self.captionLabel.attributedText =
[activeStep.caption attributedMarkdownStringWithFontSize:self.captionLabel.font.pointSize];
}];
[self.collectionView setContentOffset:CGPointZero];
self.pageControl.currentPage = 0;
if (self.navigationController)
[self.navigationBar removeFromSuperview];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.pageControl removeKeyPathObservers];
}
- (BOOL)shouldAutorotate {
return NO;
}
- (BOOL)prefersStatusBarHidden {
return NO;
}
- (UIStatusBarStyle)preferredStatusBarStyle {
return UIStatusBarStyleLightContent;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationPortrait;
}
#pragma mark - UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return self.pageControl.numberOfPages = [self.steps count];
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MPGuideStepCell *cell = [MPGuideStepCell dequeueCellFromCollectionView:collectionView indexPath:indexPath];
cell.imageView.image = ((MPGuideStep *)self.steps[indexPath.item]).image;
cell.contentView.frame = cell.bounds;
return cell;
}
#pragma mark - UICollectionViewDelegateFlowLayout
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return collectionView.bounds.size;
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == self.collectionView)
self.pageControl.currentPage = [self.collectionView indexPathForItemAtPoint:CGRectGetCenter( self.collectionView.bounds )].item;
}
#pragma mark - Actions
- (IBAction)close:(id)sender {
[MPiOSConfig get].showSetup = @NO;
[self.navigationController dismissViewControllerAnimated:YES completion:nil];
[self dismissViewControllerAnimated:YES completion:nil];
}
#pragma mark - Private
@end
@implementation MPGuideStep
+ (instancetype)stepWithImage:(UIImage *)image caption:(NSString *)caption {
MPGuideStep *step = [self new];
step.image = image;
step.caption = caption;
return step;
}
@end
@implementation MPGuideStepCell
- (void)awakeFromNib {
[super awakeFromNib];
self.imageView.layer.shadowColor = [UIColor grayColor].CGColor;
self.imageView.layer.shadowOffset = CGSizeZero;
self.imageView.layer.shadowOpacity = 0.5f;
}
@end

View File

@@ -0,0 +1,29 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPLogsViewController.h
// MPLogsViewController
//
// Created by lhunath on 2013-04-29.
// Copyright, lhunath (Maarten Billemont) 2013. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface MPLogsViewController : UIViewController
@property (weak, nonatomic) IBOutlet UITextView *logView;
@property (weak, nonatomic) IBOutlet UISegmentedControl *levelControl;
- (IBAction)toggleLevelControl:(UISegmentedControl *)sender;
- (IBAction)refresh:(UIBarButtonItem *)sender;
- (IBAction)mail:(UIBarButtonItem *)sender;
@end

View File

@@ -0,0 +1,94 @@
/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPLogsViewController.h
// MPLogsViewController
//
// Created by lhunath on 2013-04-29.
// Copyright, lhunath (Maarten Billemont) 2013. All rights reserved.
//
#import "MPLogsViewController.h"
#import "MPiOSAppDelegate.h"
#import "MPAppDelegate_Store.h"
@implementation MPLogsViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor clearColor];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
PearlAddNotificationObserver( NSUserDefaultsDidChangeNotification, nil, [NSOperationQueue mainQueue],
^(MPLogsViewController *self, NSNotification *note) {
self.levelControl.selectedSegmentIndex = [[MPiOSConfig get].traceMode boolValue]? 1: 0;
} );
self.logView.contentInset = UIEdgeInsetsMake( 64, 0, 93, 0 );
[self refresh:nil];
self.levelControl.selectedSegmentIndex = [[MPiOSConfig get].traceMode boolValue]? 1: 0;
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
PearlRemoveNotificationObservers();
}
- (IBAction)toggleLevelControl:(UISegmentedControl *)sender {
BOOL traceEnabled = (BOOL)self.levelControl.selectedSegmentIndex;
if (traceEnabled) {
[PearlAlert showAlertWithTitle:@"Enable Trace Mode?" message:
@"Trace mode will log the internal operation of the application.\n"
@"Unless you're looking for the cause of a problem, you should leave this off to save memory."
viewStyle:UIAlertViewStyleDefault initAlert:nil
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
if (buttonIndex == [alert cancelButtonIndex])
return;
[MPiOSConfig get].traceMode = @YES;
} cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Enable Trace", nil];
}
else
[MPiOSConfig get].traceMode = @NO;
}
- (IBAction)refresh:(UIBarButtonItem *)sender {
self.logView.text = [[PearlLogger get] formatMessagesWithLevel:PearlLogLevelTrace];
}
- (IBAction)mail:(UIBarButtonItem *)sender {
if ([[MPiOSConfig get].traceMode boolValue]) {
[PearlAlert showAlertWithTitle:@"Hiding Trace Messages" message:
@"Trace-level log messages will not be mailed. "
@"These messages contain sensitive and personal information."
viewStyle:UIAlertViewStyleDefault initAlert:nil
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
[[MPiOSAppDelegate get] openFeedbackWithLogs:YES forVC:self];
} cancelTitle:[PearlStrings get].commonButtonOkay otherTitles:nil];
}
else
[[MPiOSAppDelegate get] openFeedbackWithLogs:YES forVC:self];
}
@end

View File

@@ -0,0 +1,25 @@
//
// MPPreferencesViewController.h
// MasterPassword-iOS
//
// Created by Maarten Billemont on 04/06/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface MPMessage : NSObject
@property(nonatomic) NSString *title;
@property(nonatomic) NSString *text;
@property(nonatomic) BOOL info;
+ (instancetype)messageWithTitle:(NSString *)title text:(NSString *)text info:(BOOL)info;
@end
@interface MPMessageViewController : UIViewController
@property (nonatomic) MPMessage *message;
@end

Some files were not shown because too many files have changed in this diff Show More