2
0

Serious houskeeping: Better file structure & preparation for Xcode workspace.

This commit is contained in:
Maarten Billemont
2013-03-29 11:58:27 -04:00
parent 02b323d640
commit 1c79545245
846 changed files with 6048 additions and 5771 deletions

View File

@@ -0,0 +1,45 @@
/**
* 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 "MPElementGeneratedEntity.h"
#define MPAlgorithmDefaultVersion 1
#define MPAlgorithmDefault MPAlgorithmForVersion(MPAlgorithmDefaultVersion)
@protocol MPAlgorithm<NSObject>
@required
- (NSUInteger)version;
- (BOOL)migrateUser:(MPUserEntity *)user;
- (BOOL)migrateElement:(MPElementEntity *)element explicit:(BOOL)explicit;
- (MPKey *)keyForPassword:(NSString *)password ofUserNamed:(NSString *)userName;
- (MPKey *)keyFromKeyData:(NSData *)keyData;
- (NSData *)keyIDForKeyData:(NSData *)keyData;
- (NSString *)nameOfType:(MPElementType)type;
- (NSString *)shortNameOfType:(MPElementType)type;
- (NSString *)classNameOfType:(MPElementType)type;
- (Class)classOfType:(MPElementType)type;
- (NSString *)generateContentForElement:(MPElementGeneratedEntity *)element usingKey:(MPKey *)key;
@end
id<MPAlgorithm> MPAlgorithmForVersion(NSUInteger version);
id<MPAlgorithm> MPAlgorithmDefaultForBundleVersion(NSString *bundleVersion);

View File

@@ -0,0 +1,41 @@
/**
* 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(NSUInteger version) {
static NSMutableDictionary *versionToAlgorithm = nil;
if (!versionToAlgorithm)
versionToAlgorithm = [NSMutableDictionary dictionary];
id<MPAlgorithm> algorithm = [versionToAlgorithm objectForKey:@(version)];
if (!algorithm)
if ((algorithm = [NSClassFromString(PearlString(@"MPAlgorithmV%lu", (unsigned long)version)) new]))
[versionToAlgorithm setObject:algorithm forKey:@(version)];
return algorithm;
}
id<MPAlgorithm> MPAlgorithmDefaultForBundleVersion(NSString *bundleVersion) {
if (PearlCFBundleVersionCompare(bundleVersion, @"1.3") == NSOrderedAscending)
// Pre-1.3
return MPAlgorithmForVersion(0);
return MPAlgorithmDefault;
}

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,269 @@
/**
* 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 "MPAlgorithmV0.h"
#import "MPEntities.h"
#define MP_N 32768
#define MP_r 8
#define MP_p 2
#define MP_dkLen 64
#define MP_hash PearlHashSHA256
@implementation MPAlgorithmV0
- (NSUInteger)version {
return 0;
}
- (BOOL)migrateUser:(MPUserEntity *)user {
NSError *error = nil;
NSFetchRequest *migrationRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
migrationRequest.predicate = [NSPredicate predicateWithFormat:@"version_ < %d AND user == %@", MPAlgorithmDefaultVersion, user];
NSArray *migrationElements = [user.managedObjectContext executeFetchRequest:migrationRequest error:&error];
if (!migrationElements) {
err(@"While looking for elements to migrate: %@", error);
return NO;
}
BOOL requiresExplicitMigration = NO;
for (MPElementEntity *migrationElement in migrationElements)
if (![migrationElement migrateExplicitly:NO])
requiresExplicitMigration = YES;
return requiresExplicitMigration;
}
- (BOOL)migrateElement:(MPElementEntity *)element explicit:(BOOL)explicit {
if (element.version != [self version] - 1)
// Only migrate from previous version.
return NO;
if (!explicit) {
// This migration requires explicit permission.
element.requiresExplicitMigration = YES;
return NO;
}
// Apply migration.
element.requiresExplicitMigration = NO;
element.version = [self version];
return YES;
}
- (MPKey *)keyForPassword:(NSString *)password ofUserNamed:(NSString *)userName {
uint32_t nuserNameLength = htonl(userName.length);
NSDate *start = [NSDate date];
NSData *keyData = [PearlSCrypt deriveKeyWithLength:MP_dkLen fromPassword:[password dataUsingEncoding:NSUTF8StringEncoding]
usingSalt:[NSData dataByConcatenatingDatas:
[@"com.lyndir.masterpassword" dataUsingEncoding:NSUTF8StringEncoding],
[NSData dataWithBytes:&nuserNameLength
length:sizeof(nuserNameLength)],
[userName dataUsingEncoding:NSUTF8StringEncoding],
nil] N:MP_N r:MP_r p:MP_p];
MPKey *key = [self keyFromKeyData:keyData];
trc(@"User: %@, password: %@ derives to key ID: %@ (took %0.2fs)", userName, password, [key.keyID encodeHex], -[start timeIntervalSinceNow]);
return key;
}
- (MPKey *)keyFromKeyData:(NSData *)keyData {
return [[MPKey alloc] initWithKeyData:keyData algorithm:self];
}
- (NSData *)keyIDForKeyData:(NSData *)keyData {
return [keyData hashWith:MP_hash];
}
- (NSString *)nameOfType:(MPElementType)type {
if (!type)
return nil;
switch (type) {
case MPElementTypeGeneratedMaximum:
return @"Maximum Security Password";
case MPElementTypeGeneratedLong:
return @"Long Password";
case MPElementTypeGeneratedMedium:
return @"Medium Password";
case MPElementTypeGeneratedBasic:
return @"Basic Password";
case MPElementTypeGeneratedShort:
return @"Short Password";
case MPElementTypeGeneratedPIN:
return @"PIN";
case MPElementTypeStoredPersonal:
return @"Personal Password";
case MPElementTypeStoredDevicePrivate:
return @"Device Private Password";
}
Throw(@"Type not supported: %d", type);
}
- (NSString *)shortNameOfType:(MPElementType)type {
if (!type)
return nil;
switch (type) {
case MPElementTypeGeneratedMaximum:
return @"Maximum";
case MPElementTypeGeneratedLong:
return @"Long";
case MPElementTypeGeneratedMedium:
return @"Medium";
case MPElementTypeGeneratedBasic:
return @"Basic";
case MPElementTypeGeneratedShort:
return @"Short";
case MPElementTypeGeneratedPIN:
return @"PIN";
case MPElementTypeStoredPersonal:
return @"Personal";
case MPElementTypeStoredDevicePrivate:
return @"Device";
}
Throw(@"Type not supported: %d", type);
}
- (NSString *)classNameOfType:(MPElementType)type {
return NSStringFromClass([self classOfType:type]);
}
- (Class)classOfType:(MPElementType)type {
if (!type)
Throw(@"No type given.");
switch (type) {
case MPElementTypeGeneratedMaximum:
return [MPElementGeneratedEntity class];
case MPElementTypeGeneratedLong:
return [MPElementGeneratedEntity class];
case MPElementTypeGeneratedMedium:
return [MPElementGeneratedEntity class];
case MPElementTypeGeneratedBasic:
return [MPElementGeneratedEntity class];
case MPElementTypeGeneratedShort:
return [MPElementGeneratedEntity class];
case MPElementTypeGeneratedPIN:
return [MPElementGeneratedEntity class];
case MPElementTypeStoredPersonal:
return [MPElementStoredEntity class];
case MPElementTypeStoredDevicePrivate:
return [MPElementStoredEntity class];
}
Throw(@"Type not supported: %d", type);
}
- (NSString *)generateContentForElement:(MPElementGeneratedEntity *)element usingKey:(MPKey *)key {
static NSDictionary *MPTypes_ciphers = nil;
if (!element)
return nil;
if (!(element.type & MPElementTypeClassGenerated)) {
err(@"Incorrect type (is not MPElementTypeClassGenerated): %@, for: %@", [self nameOfType:element.type], element.name);
return nil;
}
if (!element.name.length) {
err(@"Missing name.");
return nil;
}
if (!key.keyData.length) {
err(@"Missing key.");
return nil;
}
if (MPTypes_ciphers == nil)
MPTypes_ciphers = [NSDictionary dictionaryWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"ciphers"
withExtension:@"plist"]];
// Determine the seed whose bytes will be used for calculating a password
uint32_t ncounter = htonl(element.counter), nnameLength = htonl(element.name.length);
NSData *counterBytes = [NSData dataWithBytes:&ncounter length:sizeof(ncounter)];
NSData *nameLengthBytes = [NSData dataWithBytes:&nnameLength length:sizeof(nnameLength)];
trc(@"seed from: hmac-sha256(%@, 'com.lyndir.masterpassword' | %@ | %@ | %@)", [key.keyData encodeBase64], [nameLengthBytes encodeHex], element.name, [counterBytes encodeHex]);
NSData *seed = [[NSData dataByConcatenatingDatas:
[@"com.lyndir.masterpassword" dataUsingEncoding:NSUTF8StringEncoding],
nameLengthBytes,
[element.name dataUsingEncoding:NSUTF8StringEncoding],
counterBytes,
nil]
hmacWith:PearlHashSHA256 key:key.keyData];
trc(@"seed is: %@", [seed encodeBase64]);
const char *seedBytes = seed.bytes;
// Determine the cipher from the first seed byte.
assert([seed length]);
NSArray *typeCiphers = [[MPTypes_ciphers valueForKey:[self classNameOfType:element.type]]
valueForKey:[self nameOfType:element.type]];
NSString *cipher = [typeCiphers objectAtIndex:htons(seedBytes[0]) % [typeCiphers count]];
trc(@"type %@, ciphers: %@, selected: %@", [self nameOfType:element.type], typeCiphers, cipher);
// Encode the content, character by character, using subsequent seed bytes and the cipher.
assert([seed length] >= [cipher length] + 1);
NSMutableString *content = [NSMutableString stringWithCapacity:[cipher length]];
for (NSUInteger c = 0; c < [cipher length]; ++c) {
uint16_t keyByte = htons(seedBytes[c + 1]);
NSString *cipherClass = [cipher substringWithRange:NSMakeRange(c, 1)];
NSString *cipherClassCharacters = [[MPTypes_ciphers valueForKey:@"MPCharacterClasses"] valueForKey:cipherClass];
NSString *character = [cipherClassCharacters substringWithRange:NSMakeRange(keyByte % [cipherClassCharacters length],
1)];
trc(@"class %@ has characters: %@, index: %u, selected: %@", cipherClass, cipherClassCharacters, keyByte, character);
[content appendString:character];
}
return content;
}
@end

View File

@@ -0,0 +1,22 @@
/**
* 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,113 @@
/**
* 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
- (NSUInteger)version {
return 1;
}
- (BOOL)migrateElement:(MPElementEntity *)element explicit:(BOOL)explicit {
if (element.version != [self version] - 1)
// Only migrate from previous version.
return NO;
if (!explicit) {
if (element.type & MPElementTypeClassGenerated) {
// This migration requires explicit permission for types of the generated class.
element.requiresExplicitMigration = YES;
return NO;
}
}
// Apply migration.
element.requiresExplicitMigration = NO;
element.version = [self version];
return YES;
}
- (NSString *)generateContentForElement:(MPElementGeneratedEntity *)element usingKey:(MPKey *)key {
static NSDictionary *MPTypes_ciphers = nil;
if (!element)
return nil;
if (!(element.type & MPElementTypeClassGenerated)) {
err(@"Incorrect type (is not MPElementTypeClassGenerated): %@, for: %@", [self nameOfType:element.type], element.name);
return nil;
}
if (!element.name.length) {
err(@"Missing name.");
return nil;
}
if (!key.keyData.length) {
err(@"Missing key.");
return nil;
}
if (MPTypes_ciphers == nil)
MPTypes_ciphers = [NSDictionary dictionaryWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"ciphers"
withExtension:@"plist"]];
// Determine the seed whose bytes will be used for calculating a password
uint32_t ncounter = htonl(element.counter), nnameLength = htonl(element.name.length);
NSData *counterBytes = [NSData dataWithBytes:&ncounter length:sizeof(ncounter)];
NSData *nameLengthBytes = [NSData dataWithBytes:&nnameLength length:sizeof(nnameLength)];
trc(@"seed from: hmac-sha256(%@, 'com.lyndir.masterpassword' | %@ | %@ | %@)", [key.keyData encodeBase64], [nameLengthBytes encodeHex], element.name, [counterBytes encodeHex]);
NSData *seed = [[NSData dataByConcatenatingDatas:
[@"com.lyndir.masterpassword" dataUsingEncoding:NSUTF8StringEncoding],
nameLengthBytes,
[element.name dataUsingEncoding:NSUTF8StringEncoding],
counterBytes,
nil]
hmacWith:PearlHashSHA256 key:key.keyData];
trc(@"seed is: %@", [seed encodeBase64]);
const unsigned char *seedBytes = seed.bytes;
// Determine the cipher from the first seed byte.
assert([seed length]);
NSArray *typeCiphers = [[MPTypes_ciphers valueForKey:[self classNameOfType:element.type]]
valueForKey:[self nameOfType:element.type]];
NSString *cipher = [typeCiphers objectAtIndex:seedBytes[0] % [typeCiphers count]];
trc(@"type %@, ciphers: %@, selected: %@", [self nameOfType:element.type], typeCiphers, cipher);
// Encode the content, character by character, using subsequent seed bytes and the cipher.
assert([seed length] >= [cipher length] + 1);
NSMutableString *content = [NSMutableString stringWithCapacity:[cipher length]];
for (NSUInteger c = 0; c < [cipher length]; ++c) {
uint16_t keyByte = seedBytes[c + 1];
NSString *cipherClass = [cipher substringWithRange:NSMakeRange(c, 1)];
NSString *cipherClassCharacters = [[MPTypes_ciphers valueForKey:@"MPCharacterClasses"] valueForKey:cipherClass];
NSString *character = [cipherClassCharacters substringWithRange:NSMakeRange(keyByte % [cipherClassCharacters length],
1)];
trc(@"class %@ has characters: %@, index: %u, selected: %@", cipherClass, cipherClassCharacters, keyByte, character);
[content appendString:character];
}
return content;
}
@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 usingMasterPassword:(NSString *)password;
- (void)signOutAnimated:(BOOL)animated;
- (void)storeSavedKeyFor:(MPUserEntity *)user;
- (void)forgetSavedKeyFor:(MPUserEntity *)user;
@end

View File

@@ -0,0 +1,233 @@
//
// 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"
@implementation MPAppDelegate_Shared (Key)
static NSDictionary *keyQuery(MPUserEntity *user) {
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
attributes:@{
(__bridge id)kSecAttrService: @"Saved Master Password",
(__bridge id)kSecAttrAccount: IfNotNilElse(user.name, @"")
}
matches:nil];
}
- (MPKey *)loadSavedKeyFor:(MPUserEntity *)user {
NSData *keyData = [PearlKeyChain dataOfItemForQuery:keyQuery(user)];
if (keyData)
inf(@"Found key in keychain for: %@", user.userID);
else {
user.saveKey = NO;
inf(@"No key found in keychain for: %@", user.userID);
}
return [MPAlgorithmDefault keyFromKeyData:keyData];
}
- (void)storeSavedKeyFor:(MPUserEntity *)user {
if (user.saveKey) {
NSData *existingKeyData = [PearlKeyChain dataOfItemForQuery:keyQuery(user)];
if (![existingKeyData isEqualToData:self.key.keyData]) {
inf(@"Saving key in keychain for: %@", user.userID);
[PearlKeyChain addOrUpdateItemForQuery:keyQuery(user)
withAttributes:@{
(__bridge id)kSecValueData : self.key.keyData,
#if TARGET_OS_IPHONE
(__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
#endif
}];
}
}
}
- (void)forgetSavedKeyFor:(MPUserEntity *)user {
OSStatus result = [PearlKeyChain deleteItemForQuery:keyQuery(user)];
if (result == noErr || result == errSecItemNotFound) {
user.saveKey = NO;
if (result == noErr) {
inf(@"Removed key from keychain for: %@", user.userID);
[[NSNotificationCenter defaultCenter] postNotificationName:MPKeyForgottenNotification object:self];
}
}
}
- (void)signOutAnimated:(BOOL)animated {
if (self.key)
self.key = nil;
if (self.activeUser) {
self.activeUser = nil;
[[NSNotificationCenter defaultCenter] postNotificationName:MPSignedOutNotification object:self userInfo:
@{@"animated": @(animated)}];
}
}
- (BOOL)signInAsUser:(MPUserEntity *)user usingMasterPassword:(NSString *)password {
assert(!password || ![NSThread isMainThread]); // If we need to computing a key, this operation shouldn't be on the main thread.
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])
if ((tryKey = [MPAlgorithmDefault keyForPassword:password ofUserNamed:user.name])) {
user.keyID = tryKey.keyID;
// Migrate existing elements.
MPKey *recoverKey = nil;
#ifdef PEARL_UIKIT
PearlAlert *activityAlert = [PearlAlert showActivityWithTitle:PearlString(@"Migrating %d sites...", [user.elements count])];
#endif
for (MPElementEntity *element in user.elements) {
if (element.type & MPElementTypeClassStored && ![element contentUsingKey:tryKey]) {
id content = nil;
if (recoverKey)
content = [element contentUsingKey:recoverKey];
while (!content) {
__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 %@", element.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 = [element.algorithm keyForPassword:masterPassword ofUserNamed:user.name];
content = [element contentUsingKey:recoverKey];
}
if (!content)
// Don't Migrate
break;
[element setContent:content usingKey:tryKey];
}
}
[user saveContext];
#ifdef PEARL_UIKIT
[activityAlert dismissAlert];
#endif
}
}
// 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]))
if (![user.keyID isEqual:tryKey.keyID]) {
// Loaded password doesn't match user's keyID. Forget saved password: it is incorrect.
inf(@"Saved password doesn't match keyID for: %@", user.userID);
tryKey = nil;
[self forgetSavedKeyFor:user];
}
}
// Method 3: Check the given master password string.
if (!tryKey) {
if ([password length])
if ((tryKey = [MPAlgorithmDefault keyForPassword:password ofUserNamed:user.name]))
if (![user.keyID isEqual:tryKey.keyID]) {
inf(@"Key derived from password doesn't match keyID for: %@", user.userID);
tryKey = nil;
}
}
// No more methods left, fail if key still not known.
if (!tryKey) {
if (password) {
inf(@"Login failed for: %@", user.userID);
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointSignInFailed];
#endif
#ifdef LOCALYTICS
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointSignInFailed attributes:nil];
#endif
}
return NO;
}
inf(@"Logged in: %@", user.userID);
if (![self.key isEqualToKey:tryKey]) {
self.key = tryKey;
[self storeSavedKeyFor:user];
}
@try {
if ([[MPConfig get].sendInfo boolValue]) {
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight addCustomEnvironmentInformation:user.userID forKey:@"username"];
#endif
#ifdef CRASHLYTICS
[Crashlytics setObjectValue:user.userID forKey:@"username"];
[Crashlytics setUserName:user.userID];
#endif
}
}
@catch (id exception) {
err(@"While setting username: %@", exception);
}
user.lastUsed = [NSDate date];
[user saveContext];
self.activeUser = user;
[[NSNotificationCenter defaultCenter] postNotificationName:MPSignedInNotification object:self];
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointSignedIn];
#endif
#ifdef LOCALYTICS
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointSignedIn attributes:nil];
#endif
return YES;
}
@end

View File

@@ -0,0 +1,25 @@
//
// 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) MPUserEntity *activeUser;
@property (strong, nonatomic) MPKey *key;
+ (instancetype)get;
- (MPUserEntity *)activeUserInContext:(NSManagedObjectContext *)moc;
@end

View File

@@ -0,0 +1,62 @@
//
// 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"
@implementation MPAppDelegate_Shared {
NSManagedObjectID *_activeUserOID;
}
+ (MPAppDelegate_Shared *)get {
#if TARGET_OS_IPHONE
return (MPAppDelegate_Shared *)[UIApplication sharedApplication].delegate;
#elif defined (__MAC_OS_X_VERSION_MIN_REQUIRED)
return (MPAppDelegate_Shared *)[NSApplication sharedApplication].delegate;
#else
#error Unsupported OS.
#endif
}
- (MPUserEntity *)activeUser {
if (!_activeUserOID)
return nil;
NSManagedObjectContext *moc = [MPAppDelegate_Shared managedObjectContextForThreadIfReady];
if (!moc)
return nil;
NSError *error;
MPUserEntity *activeUser = (MPUserEntity *)[moc existingObjectWithID:_activeUserOID error:&error];
if (!activeUser)
err(@"Failed to retrieve active user: %@", error);
return activeUser;
}
- (MPUserEntity *)activeUserInContext:(NSManagedObjectContext *)moc {
if (!_activeUserOID || !moc)
return nil;
NSError *error;
MPUserEntity *activeUser = (MPUserEntity *)[moc existingObjectWithID:_activeUserOID error:&error];
if (!activeUser)
err(@"Failed to retrieve active user: %@", error);
return activeUser;
}
- (void)setActiveUser:(MPUserEntity *)activeUser {
_activeUserOID = activeUser.objectID;
}
@end

View File

@@ -0,0 +1,32 @@
//
// MPAppDelegate_Key.h
// MasterPassword
//
// Created by Maarten Billemont on 24/11/11.
// Copyright (c) 2011 Lyndir. All rights reserved.
//
#import "MPAppDelegate_Shared.h"
#import "UbiquityStoreManager.h"
typedef enum {
MPImportResultSuccess,
MPImportResultCancelled,
MPImportResultInvalidPassword,
MPImportResultMalformedInput,
MPImportResultInternalError,
} MPImportResult;
@interface MPAppDelegate_Shared (Store)<UbiquityStoreManagerDelegate>
+ (NSManagedObjectContext *)managedObjectContextForThreadIfReady;
+ (BOOL)managedObjectContextPerformBlock:(void (^)(NSManagedObjectContext *moc))mocBlock;
+ (BOOL)managedObjectContextPerformBlockAndWait:(void (^)(NSManagedObjectContext *))mocBlock;
- (MPImportResult)importSites:(NSString *)importedSitesString
askImportPassword:(NSString *(^)(NSString *userName))importPassword
askUserPassword:(NSString *(^)(NSString *userName, NSUInteger importCount, NSUInteger deleteCount))userPassword;
- (NSString *)exportSitesShowingPasswords:(BOOL)showPasswords;
@end

View File

@@ -0,0 +1,657 @@
//
// MPAppDelegate.m
// MasterPassword
//
// Created by Maarten Billemont on 24/11/11.
// Copyright (c) 2011 Lyndir. All rights reserved.
//
#import <objc/runtime.h>
#import "MPAppDelegate_Store.h"
@implementation MPAppDelegate_Shared (Store)
static char privateManagedObjectContextKey, mainManagedObjectContextKey;
#pragma mark - Core Data setup
+ (NSManagedObjectContext *)managedObjectContextForThreadIfReady {
NSManagedObjectContext *mainManagedObjectContext = [[self get] mainManagedObjectContextIfReady];
if ([[NSThread currentThread] isMainThread])
return mainManagedObjectContext;
NSManagedObjectContext *threadManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
threadManagedObjectContext.parentContext = mainManagedObjectContext;
return threadManagedObjectContext;
}
+ (BOOL)managedObjectContextPerformBlock:(void (^)(NSManagedObjectContext *))mocBlock {
NSManagedObjectContext *mainManagedObjectContext = [[self get] mainManagedObjectContextIfReady];
if (!mainManagedObjectContext)
return NO;
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
moc.parentContext = mainManagedObjectContext;
[moc performBlock:^{
mocBlock(moc);
}];
return YES;
}
+ (BOOL)managedObjectContextPerformBlockAndWait:(void (^)(NSManagedObjectContext *))mocBlock {
NSManagedObjectContext *mainManagedObjectContext = [[self get] mainManagedObjectContextIfReady];
if (!mainManagedObjectContext)
return NO;
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
moc.parentContext = mainManagedObjectContext;
[moc performBlockAndWait:^{
mocBlock(moc);
}];
return YES;
}
- (NSManagedObjectContext *)mainManagedObjectContextIfReady {
if (![self privateManagedObjectContextIfReady])
return nil;
return objc_getAssociatedObject(self, &mainManagedObjectContextKey);
}
- (NSManagedObjectContext *)privateManagedObjectContextIfReady {
NSManagedObjectContext *privateManagedObjectContext = objc_getAssociatedObject(self, &privateManagedObjectContextKey);
if (!privateManagedObjectContext) {
privateManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[privateManagedObjectContext performBlockAndWait:^{
privateManagedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;
privateManagedObjectContext.persistentStoreCoordinator = self.storeManager.persistentStoreCoordinator;
}];
NSManagedObjectContext *mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
mainManagedObjectContext.parentContext = privateManagedObjectContext;
objc_setAssociatedObject(self, &privateManagedObjectContextKey, privateManagedObjectContext, OBJC_ASSOCIATION_RETAIN);
objc_setAssociatedObject(self, &mainManagedObjectContextKey, mainManagedObjectContext, OBJC_ASSOCIATION_RETAIN);
}
if (![privateManagedObjectContext.persistentStoreCoordinator.persistentStores count])
// Store not available yet.
return nil;
return privateManagedObjectContext;
}
- (void)migrateStoreForManager:(UbiquityStoreManager *)storeManager {
NSNumber *cloudEnabled = [[NSUserDefaults standardUserDefaults] objectForKey:@"iCloudEnabledKey"];
if (!cloudEnabled)
// No old data to migrate.
return;
if ([cloudEnabled boolValue]) {
if ([storeManager cloudSafeForSeeding]) {
NSString *uuid = [[NSUserDefaults standardUserDefaults] stringForKey:@"LocalUUIDKey"];
NSURL *cloudContainerURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:@"HL3Q45LX9N.com.lyndir.lhunath.MasterPassword.shared"];
NSURL *newCloudStoreURL = [storeManager URLForCloudStore];
NSURL *newCloudContentURL = [storeManager URLForCloudContent];
//NSURL *oldCloudContentURL = [[cloudContainerURL URLByAppendingPathComponent:@"Data" isDirectory:YES]
// URLByAppendingPathComponent:uuid isDirectory:YES];
NSURL *oldCloudStoreDirectoryURL = [cloudContainerURL URLByAppendingPathComponent:@"Database.nosync" isDirectory:YES];
NSURL *oldCloudStoreURL = [[oldCloudStoreDirectoryURL URLByAppendingPathComponent:uuid isDirectory:NO]
URLByAppendingPathExtension:@"sqlite"];
if (![[NSFileManager defaultManager] fileExistsAtPath:oldCloudStoreURL.path isDirectory:NO]) {
// No old store to migrate from, cannot migrate.
wrn(@"Cannot migrate cloud store, old store not found at: %@", oldCloudStoreURL.path);
return;
}
NSError *error = nil;
NSDictionary *oldCloudStoreOptions = @{
// This is here in an attempt to have iCloud recreate the old store file from
// the baseline and transaction logs from the iCloud account.
// In my tests however only the baseline was used to recreate the store which then ended up being empty.
/*NSPersistentStoreUbiquitousContentNameKey : uuid,
NSPersistentStoreUbiquitousContentURLKey : oldCloudContentURL,*/
// So instead, we'll just open up the old store as read-only, if it exists.
NSReadOnlyPersistentStoreOption : @YES,
NSMigratePersistentStoresAutomaticallyOption : @YES,
NSInferMappingModelAutomaticallyOption : @YES};
NSDictionary *newCloudStoreOptions = @{
NSPersistentStoreUbiquitousContentNameKey : [storeManager valueForKey:@"contentName"],
NSPersistentStoreUbiquitousContentURLKey : newCloudContentURL,
NSMigratePersistentStoresAutomaticallyOption : @YES,
NSInferMappingModelAutomaticallyOption : @YES};
// Create the directory to hold the new cloud store.
// This is only necessary if we want to try to rebuild the old store. See comment above about how that failed.
//if (![[NSFileManager defaultManager] createDirectoryAtPath:oldCloudStoreDirectoryURL.path
// withIntermediateDirectories:YES attributes:nil error:&error])
//err(@"While creating directory for old cloud store: %@", error);
if (![[NSFileManager defaultManager] createDirectoryAtPath:[storeManager URLForCloudStoreDirectory].path
withIntermediateDirectories:YES attributes:nil error:&error]) {
err(@"While creating directory for new cloud store: %@", error);
return;
}
NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:nil];
NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
// Open the old cloud store.
NSPersistentStore *oldStore = [psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:oldCloudStoreURL
options:oldCloudStoreOptions error:&error];
if (!oldStore) {
err(@"While opening old store for migration %@: %@", oldCloudStoreURL.path, error);
return;
}
// Migrate to the new cloud store.
if (![psc migratePersistentStore:oldStore toURL:newCloudStoreURL options:newCloudStoreOptions withType:NSSQLiteStoreType
error:&error]) {
err(@"While migrating cloud store from %@ -> %@: %@", oldCloudStoreURL.path, newCloudStoreURL.path, error);
return;
}
// Clean-up.
if (![psc removePersistentStore:[psc.persistentStores lastObject] error:&error])
err(@"While removing the migrated store from the store context: %@", error);
if (![[NSFileManager defaultManager] removeItemAtURL:oldCloudStoreURL error:&error])
err(@"While deleting the old cloud store: %@", error);
}
} else {
NSURL *applicationFilesDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory
inDomains:NSUserDomainMask] lastObject];
NSURL *oldLocalStoreURL = [[applicationFilesDirectory URLByAppendingPathComponent:@"MasterPassword" isDirectory:NO]
URLByAppendingPathExtension:@"sqlite"];
NSURL *newLocalStoreURL = [storeManager URLForLocalStore];
if ([[NSFileManager defaultManager] fileExistsAtPath:oldLocalStoreURL.path isDirectory:NO] &&
![[NSFileManager defaultManager] fileExistsAtPath:newLocalStoreURL.path isDirectory:NO]) {
NSError *error = nil;
NSDictionary *options = @{
NSMigratePersistentStoresAutomaticallyOption : @YES,
NSInferMappingModelAutomaticallyOption : @YES};
NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:nil];
NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
// Create the directory to hold the new local store.
if (![[NSFileManager defaultManager] createDirectoryAtPath:[storeManager URLForLocalStoreDirectory].path
withIntermediateDirectories:YES attributes:nil error:&error]) {
err(@"While creating directory for new local store: %@", error);
return;
}
// Open the old local store.
NSPersistentStore *oldStore = [psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil
URL:oldLocalStoreURL options:options error:&error];
if (!oldStore) {
err(@"While opening old store for migration %@: %@", oldLocalStoreURL.path, error);
return;
}
// Migrate to the new local store.
if (![psc migratePersistentStore:oldStore toURL:newLocalStoreURL options:options withType:NSSQLiteStoreType error:&error]) {
err(@"While migrating local store from %@ -> %@: %@", oldLocalStoreURL, newLocalStoreURL, error);
return;
}
// Clean-up.
if (![psc removePersistentStore:[psc.persistentStores lastObject] error:&error])
err(@"While removing the migrated store from the store context: %@", error);
if (![[NSFileManager defaultManager] removeItemAtURL:oldLocalStoreURL error:&error])
err(@"While deleting the old local store: %@", error);
}
}
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"LocalUUIDKey"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"iCloudEnabledKey"];
}
- (UbiquityStoreManager *)storeManager {
static UbiquityStoreManager *storeManager = nil;
if (storeManager)
return storeManager;
storeManager = [[UbiquityStoreManager alloc] initStoreNamed:nil withManagedObjectModel:nil localStoreURL:nil
containerIdentifier:@"HL3Q45LX9N.com.lyndir.lhunath.MasterPassword.shared"
#if TARGET_OS_IPHONE
additionalStoreOptions:@{
NSPersistentStoreFileProtectionKey : NSFileProtectionComplete
}];
#else
additionalStoreOptions:nil];
#endif
storeManager.delegate = self;
// Migrate old store to new store location.
[self migrateStoreForManager:storeManager];
[[NSNotificationCenter defaultCenter] addObserverForName:UbiquityManagedStoreDidChangeNotification
object:storeManager queue:nil
usingBlock:^(NSNotification *note) {
objc_setAssociatedObject(self, &privateManagedObjectContextKey, nil, OBJC_ASSOCIATION_RETAIN);
}];
[[NSNotificationCenter defaultCenter] addObserverForName:MPCheckConfigNotification object:nil queue:nil usingBlock:
^(NSNotification *note) {
if ([[MPConfig get].iCloud boolValue] != [self.storeManager cloudEnabled])
self.storeManager.cloudEnabled = [[MPConfig get].iCloud boolValue];
}];
#if TARGET_OS_IPHONE
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillTerminateNotification
object:[UIApplication sharedApplication] queue:nil
usingBlock:^(NSNotification *note) {
[self saveContexts];
}];
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillResignActiveNotification
object:[UIApplication sharedApplication] queue:nil
usingBlock:^(NSNotification *note) {
[self saveContexts];
}];
#else
[[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillTerminateNotification
object:[NSApplication sharedApplication] queue:nil
usingBlock:^(NSNotification *note) {
[self saveContexts];
}];
#endif
return storeManager;
}
- (void)saveContexts {
NSManagedObjectContext *mainManagedObjectContext = objc_getAssociatedObject(self, &mainManagedObjectContextKey);
[mainManagedObjectContext performBlockAndWait:^{
NSError *error = nil;
if (![mainManagedObjectContext save:&error])
err(@"While saving main context: %@", error);
}];
NSManagedObjectContext *privateManagedObjectContext = [self privateManagedObjectContextIfReady];
[privateManagedObjectContext performBlockAndWait:^{
NSError *error = nil;
if (![privateManagedObjectContext save:&error])
err(@"While saving private context: %@", error);
}];
}
#pragma mark - UbiquityStoreManagerDelegate
- (NSManagedObjectContext *)managedObjectContextForUbiquityStoreManager:(UbiquityStoreManager *)usm {
return [self privateManagedObjectContextIfReady];
}
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager log:(NSString *)message {
dbg(@"[StoreManager] %@", message);
}
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didSwitchToCloud:(BOOL)cloudEnabled {
// manager.cloudEnabled is more reliable (eg. iOS' MPAppDelegate tampers with didSwitch a bit)
cloudEnabled = manager.cloudEnabled;
inf(@"Using iCloud? %@", cloudEnabled? @"YES": @"NO");
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:cloudEnabled? MPCheckpointCloudEnabled: MPCheckpointCloudDisabled];
#endif
#ifdef LOCALYTICS
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointCloud attributes:@{
@"enabled": cloudEnabled? @"YES": @"NO"
}];
#endif
[MPConfig get].iCloud = @(cloudEnabled);
}
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didEncounterError:(NSError *)error cause:(UbiquityStoreManagerErrorCause)cause
context:(id)context {
err(@"StoreManager: cause=%d, context=%@, error=%@", cause, context, error);
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:PearlString(MPCheckpointMPErrorUbiquity @"_%d", cause)];
#endif
#ifdef LOCALYTICS
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointMPErrorUbiquity attributes:@{
@"cause": @(cause),
@"error.domain": error.domain,
@"error.code": @(error.code)
}];
#endif
switch (cause) {
case UbiquityStoreManagerErrorCauseDeleteStore:
case UbiquityStoreManagerErrorCauseCreateStorePath:
case UbiquityStoreManagerErrorCauseClearStore:
break;
case UbiquityStoreManagerErrorCauseOpenLocalStore: {
wrn(@"Local store could not be opened: %@", error);
if (error.code == NSMigrationMissingSourceModelError) {
wrn(@"Resetting the local store.");
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointLocalStoreReset];
#endif
#ifdef LOCALYTICS
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointLocalStoreReset attributes:nil];
#endif
[manager deleteLocalStore];
Throw(@"Local store was reset, application must be restarted to use it.");
} else
// Try again.
[manager persistentStoreCoordinator];
}
case UbiquityStoreManagerErrorCauseOpenCloudStore: {
wrn(@"iCloud store could not be opened: %@", error);
if (error.code == NSMigrationMissingSourceModelError) {
wrn(@"Resetting the iCloud store.");
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointCloudStoreReset];
#endif
#ifdef LOCALYTICS
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointCloudStoreReset attributes:nil];
#endif
[manager deleteCloudStore];
break;
} else
// Try again.
[manager persistentStoreCoordinator];
}
case UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore: {
wrn(@"Couldn't migrate local store to the cloud: %@", error);
wrn(@"Resetting the iCloud store.");
[manager deleteCloudStore];
};
}
}
#pragma mark - Import / Export
- (MPImportResult)importSites:(NSString *)importedSitesString
askImportPassword:(NSString *(^)(NSString *userName))importPassword
askUserPassword:(NSString *(^)(NSString *userName, NSUInteger importCount, NSUInteger deleteCount))userPassword {
// Compile patterns.
static NSRegularExpression *headerPattern, *sitePattern;
NSError *error = nil;
if (!headerPattern) {
headerPattern = [[NSRegularExpression alloc] initWithPattern:@"^#[[:space:]]*([^:]+): (.*)"
options:0 error:&error];
if (error) {
err(@"Error loading the header pattern: %@", error);
return MPImportResultInternalError;
}
}
if (!sitePattern) {
sitePattern = [[NSRegularExpression alloc] initWithPattern:@"^([^[:space:]]+)[[:space:]]+([[:digit:]]+)[[:space:]]+([[:digit:]]+)(:[[:digit:]]+)?[[:space:]]+([^\t]+)\t(.*)"
options:0 error:&error];
if (error) {
err(@"Error loading the site pattern: %@", error);
return MPImportResultInternalError;
}
}
// Get a MOC.
NSAssert(![[NSThread currentThread] isMainThread], @"This method should not be invoked from the main thread.");
NSManagedObjectContext *moc;
while (!(moc = [MPAppDelegate_Shared managedObjectContextForThreadIfReady]))
usleep((useconds_t)(USEC_PER_SEC * 0.2));
// Parse import data.
inf(@"Importing sites.");
__block MPUserEntity *user = nil;
id<MPAlgorithm> importAlgorithm = nil;
NSString *importBundleVersion = nil, *importUserName = nil;
NSData *importKeyID = nil;
BOOL headerStarted = NO, headerEnded = NO, clearText = NO;
NSArray *importedSiteLines = [importedSitesString componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
NSMutableSet *elementsToDelete = [NSMutableSet set];
NSMutableArray *importedSiteElements = [NSMutableArray arrayWithCapacity:[importedSiteLines count]];
NSFetchRequest *elementFetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
for (NSString *importedSiteLine in importedSiteLines) {
if ([importedSiteLine hasPrefix:@"#"]) {
// Comment or header
if (!headerStarted) {
if ([importedSiteLine isEqualToString:@"##"])
headerStarted = YES;
continue;
}
if (headerEnded)
continue;
if ([importedSiteLine isEqualToString:@"##"]) {
headerEnded = YES;
continue;
}
// Header
if ([headerPattern numberOfMatchesInString:importedSiteLine options:0 range:NSMakeRange(0, [importedSiteLine length])] != 1) {
err(@"Invalid header format in line: %@", importedSiteLine);
return MPImportResultMalformedInput;
}
NSTextCheckingResult *headerElements = [[headerPattern matchesInString:importedSiteLine options:0
range:NSMakeRange(0, [importedSiteLine length])] lastObject];
NSString *headerName = [importedSiteLine substringWithRange:[headerElements rangeAtIndex:1]];
NSString *headerValue = [importedSiteLine substringWithRange:[headerElements rangeAtIndex:2]];
if ([headerName isEqualToString:@"User Name"]) {
importUserName = headerValue;
NSFetchRequest *userFetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPUserEntity class])];
userFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@", importUserName];
NSArray *users = [moc executeFetchRequest:userFetchRequest error:&error];
if (!users) {
err(@"While looking for user: %@, error: %@", importUserName, error);
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 count]? [users lastObject]: nil;
dbg(@"Found user: %@", [user debugDescription]);
}
if ([headerName isEqualToString:@"Key ID"])
importKeyID = [headerValue decodeHex];
if ([headerName isEqualToString:@"Version"]) {
importBundleVersion = headerValue;
importAlgorithm = MPAlgorithmDefaultForBundleVersion(importBundleVersion);
}
if ([headerName isEqualToString:@"Passwords"]) {
if ([headerValue isEqualToString:@"VISIBLE"])
clearText = YES;
}
continue;
}
if (!headerEnded)
continue;
if (!importKeyID || ![importUserName length])
return MPImportResultMalformedInput;
if (![importedSiteLine length])
continue;
// Site
if ([sitePattern numberOfMatchesInString:importedSiteLine options:0 range:NSMakeRange(0, [importedSiteLine length])] != 1) {
err(@"Invalid site format in line: %@", importedSiteLine);
return MPImportResultMalformedInput;
}
NSTextCheckingResult *siteElements = [[sitePattern matchesInString:importedSiteLine options:0
range:NSMakeRange(0, [importedSiteLine length])] lastObject];
NSString *lastUsed = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:1]];
NSString *uses = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:2]];
NSString *type = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:3]];
NSString *version = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:4]];
NSString *name = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:5]];
NSString *exportContent = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:6]];
// Find existing site.
if (user) {
elementFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@ AND user == %@", name, user];
NSArray *existingSites = [moc executeFetchRequest:elementFetchRequest error:&error];
if (!existingSites) {
err(@"Lookup of existing sites failed for site: %@, user: %@, error: %@", name, user.userID, error);
return MPImportResultInternalError;
} else
if (existingSites.count)
dbg(@"Existing sites: %@", existingSites);
[elementsToDelete addObjectsFromArray:existingSites];
[importedSiteElements addObject:@[lastUsed, uses, type, version, name, 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)[importedSiteElements count], (unsigned long)[elementsToDelete count], [MPUserEntity idFor:importUserName]);
NSString *userMasterPassword = userPassword(user.name, [importedSiteElements count], [elementsToDelete count]);
if (!userMasterPassword) {
inf(@"Import cancelled.");
return MPImportResultCancelled;
}
MPKey *userKey = [MPAlgorithmDefault keyForPassword:userMasterPassword ofUserNamed:user.name];
if (![userKey.keyID isEqualToData:user.keyID])
return MPImportResultInvalidPassword;
__block MPKey *importKey = userKey;
if ([importKey.keyID isEqualToData:importKeyID])
importKey = nil;
// Delete existing sites.
if (elementsToDelete.count)
[elementsToDelete enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {
inf(@"Deleting site: %@, it will be replaced by an imported site.", [obj name]);
[moc deleteObject:obj];
}];
// Make sure there is a user.
if (!user) {
user = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPUserEntity class])
inManagedObjectContext:moc];
user.name = importUserName;
user.keyID = importKeyID;
dbg(@"Created User: %@", [user debugDescription]);
}
// Import new sites.
for (NSArray *siteElements in importedSiteElements) {
NSDate *lastUsed = [[NSDateFormatter rfc3339DateFormatter] dateFromString:[siteElements objectAtIndex:0]];
NSUInteger uses = (unsigned)[[siteElements objectAtIndex:1] integerValue];
MPElementType type = (MPElementType)[[siteElements objectAtIndex:2] integerValue];
NSUInteger version = (unsigned)[[siteElements objectAtIndex:3] integerValue];
NSString *name = [siteElements objectAtIndex:4];
NSString *exportContent = [siteElements objectAtIndex:5];
// Create new site.
MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:[MPAlgorithmForVersion(version) classNameOfType:type]
inManagedObjectContext:moc];
element.name = name;
element.user = user;
element.type = type;
element.uses = uses;
element.lastUsed = lastUsed;
element.version = version;
if ([exportContent length]) {
if (clearText)
[element importClearTextContent:exportContent usingKey:userKey];
else {
if (!importKey)
importKey = [importAlgorithm keyForPassword:importPassword(user.name) ofUserNamed:user.name];
if (![importKey.keyID isEqualToData:importKeyID])
return MPImportResultInvalidPassword;
[element importProtectedContent:exportContent protectedByKey:importKey usingKey:userKey];
}
}
dbg(@"Created Element: %@", [element debugDescription]);
}
if (![moc save:&error]) {
err(@"While saving imported sites: %@", error);
return MPImportResultInternalError;
}
inf(@"Import completed successfully.");
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointSitesImported];
#endif
#ifdef LOCALYTICS
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointSitesImported attributes:nil];
#endif
return MPImportResultSuccess;
}
- (NSString *)exportSitesShowingPasswords:(BOOL)showPasswords {
MPUserEntity *activeUser = self.activeUser;
inf(@"Exporting sites, %@, for: %@", showPasswords? @"showing passwords": @"omitting passwords", activeUser.userID);
// Header.
NSMutableString *export = [NSMutableString new];
[export appendFormat:@"# Master Password site export\n"];
if (showPasswords)
[export appendFormat:@"# Export of site names and passwords in clear-text.\n"];
else
[export appendFormat:@"# Export of site names and stored passwords (unless device-private) encrypted with the master key.\n"];
[export appendFormat:@"# \n"];
[export appendFormat:@"##\n"];
[export appendFormat:@"# Version: %@\n", [PearlInfoPlist get].CFBundleVersion];
[export appendFormat:@"# User Name: %@\n", activeUser.name];
[export appendFormat:@"# Key ID: %@\n", [activeUser.keyID encodeHex]];
[export appendFormat:@"# Date: %@\n", [[NSDateFormatter rfc3339DateFormatter] stringFromDate:[NSDate date]]];
if (showPasswords)
[export appendFormat:@"# Passwords: VISIBLE\n"];
else
[export appendFormat:@"# Passwords: PROTECTED\n"];
[export appendFormat:@"##\n"];
[export appendFormat:@"#\n"];
[export appendFormat:@"# Last Times Password Site\tSite\n"];
[export appendFormat:@"# used used type name\tpassword\n"];
// Sites.
for (MPElementEntity *element in activeUser.elements) {
NSDate *lastUsed = element.lastUsed;
NSUInteger uses = element.uses;
MPElementType type = element.type;
NSUInteger version = element.version;
NSString *name = element.name;
NSString *content = nil;
// Determine the content to export.
if (!(type & MPElementFeatureDevicePrivate)) {
if (showPasswords)
content = element.content;
else
if (type & MPElementFeatureExportContent)
content = element.exportContent;
}
[export appendFormat:@"%@ %8ld %8s %20s\t%@\n",
[[NSDateFormatter rfc3339DateFormatter] stringFromDate:lastUsed], (long)uses,
[PearlString(@"%u:%lu", type, (unsigned long)version) UTF8String], [name UTF8String], content
? content: @""];
}
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointSitesExported];
#endif
#ifdef LOCALYTICS
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointSitesExported attributes:nil];
#endif
return export;
}
@end

View File

@@ -0,0 +1,19 @@
//
// 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 *iCloud;
@property (nonatomic, retain) NSNumber *iCloudDecided;
@end

View File

@@ -0,0 +1,31 @@
//
// MPConfig.m
// MasterPassword
//
// Created by Maarten Billemont on 02/01/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "MPAppDelegate.h"
@implementation MPConfig
@dynamic sendInfo, rememberLogin, iCloud, iCloudDecided;
- (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(iCloud)): @NO,
NSStringFromSelector(@selector(iCloudDecided)): @NO}];
self.delegate = [MPAppDelegate get];
return self;
}
@end

View File

@@ -0,0 +1,26 @@
//
// MPElementEntity.h
// MasterPassword-iOS
//
// Created by Maarten Billemont on 2013-01-29.
// Copyright (c) 2013 Lyndir. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
@class MPUserEntity;
@interface MPElementEntity : NSManagedObject
@property (nonatomic, retain) id content;
@property (nonatomic, retain) NSDate * lastUsed;
@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) MPUserEntity *user;
@end

View File

@@ -0,0 +1,25 @@
//
// MPElementEntity.m
// MasterPassword-iOS
//
// Created by Maarten Billemont on 2013-01-29.
// Copyright (c) 2013 Lyndir. All rights reserved.
//
#import "MPElementEntity.h"
#import "MPUserEntity.h"
@implementation MPElementEntity
@dynamic content;
@dynamic lastUsed;
@dynamic loginName;
@dynamic name;
@dynamic requiresExplicitMigration_;
@dynamic type_;
@dynamic uses_;
@dynamic version_;
@dynamic user;
@end

View File

@@ -0,0 +1,18 @@
//
// MPElementGeneratedEntity.h
// MasterPassword-iOS
//
// Created by Maarten Billemont on 2013-01-29.
// Copyright (c) 2013 Lyndir. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#import "MPElementEntity.h"
@interface MPElementGeneratedEntity : MPElementEntity
@property (nonatomic, retain) NSNumber * counter_;
@end

View File

@@ -0,0 +1,16 @@
//
// MPElementGeneratedEntity.m
// MasterPassword-iOS
//
// Created by Maarten Billemont on 2013-01-29.
// Copyright (c) 2013 Lyndir. All rights reserved.
//
#import "MPElementGeneratedEntity.h"
@implementation MPElementGeneratedEntity
@dynamic counter_;
@end

View File

@@ -0,0 +1,18 @@
//
// MPElementStoredEntity.h
// MasterPassword-iOS
//
// Created by Maarten Billemont on 2013-01-29.
// Copyright (c) 2013 Lyndir. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#import "MPElementEntity.h"
@interface MPElementStoredEntity : MPElementEntity
@property (nonatomic, retain) id contentObject;
@end

View File

@@ -0,0 +1,16 @@
//
// MPElementStoredEntity.m
// MasterPassword-iOS
//
// Created by Maarten Billemont on 2013-01-29.
// Copyright (c) 2013 Lyndir. All rights reserved.
//
#import "MPElementStoredEntity.h"
@implementation MPElementStoredEntity
@dynamic contentObject;
@end

View File

@@ -0,0 +1,62 @@
//
// MPElementEntities.h
// MasterPassword-iOS
//
// Created by Maarten Billemont on 31/05/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "MPElementEntity.h"
#import "MPElementStoredEntity.h"
#import "MPElementGeneratedEntity.h"
#import "MPUserEntity.h"
#import "MPAlgorithm.h"
#define MPAvatarCount 19
@interface NSManagedObject (MP)
- (BOOL)saveContext;
@end
@interface MPElementEntity (MP)
@property (assign) MPElementType type;
@property (readonly) NSString *typeName;
@property (readonly) NSString *typeShortName;
@property (readonly) NSString *typeClassName;
@property (readonly) Class typeClass;
@property (assign) NSUInteger uses;
@property (assign) NSUInteger version;
@property (assign) BOOL requiresExplicitMigration;
@property (readonly) id<MPAlgorithm> algorithm;
- (id)contentUsingKey:(MPKey *)key;
- (void)setContent:(id)content usingKey:(MPKey *)key;
- (NSUInteger)use;
- (NSString *)exportContent;
- (void)importProtectedContent:(NSString *)protectedContent protectedByKey:(MPKey *)contentProtectionKey usingKey:(MPKey *)key2;
- (void)importClearTextContent:(NSString *)clearContent usingKey:(MPKey *)key;
- (BOOL)migrateExplicitly:(BOOL)explicit;
@end
@interface MPElementGeneratedEntity (MP)
@property (assign) NSUInteger counter;
@end
@interface MPUserEntity (MP)
@property (assign) NSUInteger avatar;
@property (assign) BOOL saveKey;
@property (assign) MPElementType defaultType;
@property (readonly) NSString *userID;
+ (NSString *)idFor:(NSString *)userName;
@end

View File

@@ -0,0 +1,341 @@
//
// MPElementEntities.m
// MasterPassword-iOS
//
// Created by Maarten Billemont on 31/05/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "MPEntities.h"
#import "MPAppDelegate.h"
@implementation NSManagedObject (MP)
- (BOOL)saveContext {
NSError *error;
NSManagedObjectContext *moc = [self managedObjectContext];
if (![moc save:&error]) {
err(@"While saving %@: %@", NSStringFromClass([self class]), error);
return NO;
}
if (![moc.parentContext save:&error]) {
err(@"While saving parent %@: %@", NSStringFromClass([self class]), error);
return NO;
}
return YES;
}
@end
@implementation MPElementEntity (MP)
- (MPElementType)type {
// Some people got elements with type == 0.
MPElementType type = (MPElementType)[self.type_ unsignedIntegerValue];
if (!type || type == NSNotFound)
type = [self.user defaultType];
if (!type || type == NSNotFound)
type = MPElementTypeGeneratedLong;
return type;
}
- (void)setType:(MPElementType)aType {
// Make sure we don't poison our model data with invalid values.
if (!aType || aType == NSNotFound)
aType = [self.user defaultType];
if (!aType || aType == NSNotFound)
aType = MPElementTypeGeneratedLong;
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);
}
- (NSUInteger)version {
return [self.version_ unsignedIntegerValue];
}
- (void)setVersion:(NSUInteger)version {
self.version_ = @(version);
}
- (BOOL)requiresExplicitMigration {
return [self.requiresExplicitMigration_ boolValue];
}
- (void)setRequiresExplicitMigration:(BOOL)requiresExplicitMigration {
self.requiresExplicitMigration_ = @(requiresExplicitMigration);
}
- (id<MPAlgorithm>)algorithm {
return MPAlgorithmForVersion(self.version);
}
- (NSUInteger)use {
self.lastUsed = [NSDate date];
return ++self.uses;
}
- (id)content {
MPKey *key = [MPAppDelegate get].key;
if (!key)
return nil;
assert([key.keyID isEqualToData:self.user.keyID]);
return [self contentUsingKey:key];
}
- (void)setContent:(id)content {
MPKey *key = [MPAppDelegate get].key;
if (!key)
return;
assert([key.keyID isEqualToData:self.user.keyID]);
[self setContent:content usingKey:key];
}
- (id)contentUsingKey:(MPKey *)key {
Throw(@"Content retrieval implementation missing for: %@", [self class]);
}
- (void)setContent:(id)content usingKey:(MPKey *)key {
Throw(@"Content assignment implementation missing for: %@", [self class]);
}
- (NSString *)exportContent {
return nil;
}
- (void)importProtectedContent:(NSString *)protectedContent protectedByKey:(MPKey *)contentProtectionKey usingKey:(MPKey *)key {
}
- (void)importClearTextContent:(NSString *)clearContent usingKey:(MPKey *)key {
}
- (NSString *)description {
return PearlString(@"%@:%@", [self class], [self name]);
}
- (NSString *)debugDescription {
return PearlString(@"{%@: name=%@, user=%@, type=%d, uses=%ld, lastUsed=%@, version=%ld, loginName=%@, requiresExplicitMigration=%d}",
NSStringFromClass([self class]), self.name, self.user.name, self.type, (long)self.uses, self.lastUsed, (long)self.version,
self.loginName, self.requiresExplicitMigration);
}
- (BOOL)migrateExplicitly:(BOOL)explicit {
while (self.version < MPAlgorithmDefaultVersion)
if ([MPAlgorithmForVersion(self.version + 1) migrateElement:self explicit:explicit])
inf(@"%@ migration to version: %ld succeeded for element: %@", explicit? @"Explicit": @"Automatic", (long)self.version + 1, self);
else {
wrn(@"%@ migration to version: %ld failed for element: %@", explicit? @"Explicit": @"Automatic", (long)self.version + 1, self);
return NO;
}
return YES;
}
@end
@implementation MPElementGeneratedEntity (MP)
- (NSUInteger)counter {
return [self.counter_ unsignedIntegerValue];
}
- (void)setCounter:(NSUInteger)aCounter {
self.counter_ = @(aCounter);
}
- (id)contentUsingKey:(MPKey *)key {
if (!(self.type & MPElementTypeClassGenerated)) {
err(@"Corrupt element: %@, type: %d is not in MPElementTypeClassGenerated", self.name, self.type);
return nil;
}
if (![self.name length])
return nil;
return [self.algorithm generateContentForElement:self usingKey:key];
}
@end
@implementation MPElementStoredEntity (MP)
+ (NSDictionary *)queryForDevicePrivateElementNamed:(NSString *)name {
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
attributes:@{(__bridge id)kSecAttrService: @"DevicePrivate",
(__bridge id)kSecAttrAccount: name}
matches:nil];
}
- (id)contentUsingKey:(MPKey *)key {
assert(self.type & MPElementTypeClassStored);
NSData *encryptedContent;
if (self.type & MPElementFeatureDevicePrivate)
encryptedContent = [PearlKeyChain dataOfItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]];
else
encryptedContent = self.contentObject;
NSData *decryptedContent = nil;
if ([encryptedContent length])
decryptedContent = [self decryptContent:encryptedContent usingKey:key];
if (!decryptedContent)
return nil;
return [[NSString alloc] initWithBytes:decryptedContent.bytes length:decryptedContent.length encoding:NSUTF8StringEncoding];
}
- (NSData *)decryptContent:(NSData *)encryptedContent usingKey:(MPKey *)key {
return [encryptedContent decryptWithSymmetricKey:[key subKeyOfLength:PearlCryptKeySize].keyData padding:YES];
}
- (void)setContent:(id)content usingKey:(MPKey *)key {
assert(self.type & MPElementTypeClassStored);
assert([key.keyID isEqualToData:self.user.keyID]);
NSData *encryptedContent = [[[content description] dataUsingEncoding:NSUTF8StringEncoding]
encryptWithSymmetricKey:[key subKeyOfLength:PearlCryptKeySize].keyData padding:YES];
if (self.type & MPElementFeatureDevicePrivate) {
[PearlKeyChain addOrUpdateItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
encryptedContent, (__bridge id)kSecValueData,
#if TARGET_OS_IPHONE
(__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
(__bridge id)kSecAttrAccessible,
#endif
nil]];
self.contentObject = nil;
} else
self.contentObject = encryptedContent;
}
- (NSString *)exportContent {
return [self.contentObject encodeBase64];
}
- (void)importProtectedContent:(NSString *)protectedContent protectedByKey:(MPKey *)contentProtectionKey usingKey:(MPKey *)key {
if ([contentProtectionKey.keyID isEqualToData:key.keyID])
self.contentObject = [protectedContent decodeBase64];
else {
NSString *clearContent = [[NSString alloc] initWithData:[self decryptContent:[protectedContent decodeBase64]
usingKey:contentProtectionKey]
encoding:NSUTF8StringEncoding];
[self importClearTextContent:clearContent usingKey:key];
}
}
- (void)importClearTextContent:(NSString *)clearContent usingKey:(MPKey *)key {
[self setContent:clearContent usingKey:key];
}
@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);
}
- (MPElementType)defaultType {
return (MPElementType)[self.defaultType_ unsignedIntegerValue];
}
- (void)setDefaultType:(MPElementType)aDefaultType {
self.defaultType_ = @(aDefaultType);
}
- (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
*/
//
// MPKey
//
// Created by Maarten Billemont on 16/07/12.
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
//
#import <Foundation/Foundation.h>
@protocol MPAlgorithm;
@interface MPKey : NSObject
@property (nonatomic, readonly, strong) id<MPAlgorithm> algorithm;
@property (nonatomic, readonly, strong) NSData *keyData;
@property (nonatomic, readonly, strong) NSData *keyID;
- (id)initWithKeyData:(NSData *)keyData algorithm:(id<MPAlgorithm>)algorithm;
- (MPKey *)subKeyOfLength:(NSUInteger)subKeyLength;
- (BOOL)isEqualToKey:(MPKey *)key;
@end

View File

@@ -0,0 +1,65 @@
/**
* 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, readwrite, strong) id<MPAlgorithm> algorithm;
@property (nonatomic, readwrite, strong) NSData *keyData;
@property (nonatomic, readwrite, strong) NSData *keyID;
@end
@implementation MPKey
@synthesize algorithm = _algorithm, keyData = _keyData, keyID = _keyID;
- (id)initWithKeyData:(NSData *)keyData algorithm:(id<MPAlgorithm>)algorithm {
if (!(self = [super init]))
return nil;
self.keyData = keyData;
self.algorithm = algorithm;
self.keyID = [self.algorithm keyIDForKeyData:keyData];
return self;
}
- (MPKey *)subKeyOfLength:(NSUInteger)subKeyLength {
NSData *subKeyData = [self.keyData subdataWithRange:NSMakeRange(0, MIN(subKeyLength, self.keyData.length))];
return [self.algorithm keyFromKeyData:subKeyData];
}
- (BOOL)isEqualToKey:(MPKey *)key {
return [self.keyID isEqualToData:key.keyID];
}
- (BOOL)isEqual:(id)object {
if (![object isKindOfClass:[MPKey class]])
return NO;
return [self isEqualToKey:object];
}
@end

View File

@@ -0,0 +1,77 @@
//
// MPTypes.h
// MasterPassword
//
// Created by Maarten Billemont on 02/01/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "MPKey.h"
typedef enum {
MPElementContentTypePassword,
MPElementContentTypeNote,
MPElementContentTypePicture,
} MPElementContentType;
typedef enum {
/** Generate the password. */
MPElementTypeClassGenerated = 1 << 4,
/** Store the password. */
MPElementTypeClassStored = 1 << 5,
} MPElementTypeClass;
typedef enum {
/** Export the key-protected content data. */
MPElementFeatureExportContent = 1 << 10,
/** Never export content. */
MPElementFeatureDevicePrivate = 1 << 11,
} MPElementFeature;
typedef enum {
MPElementTypeGeneratedMaximum = 0x0 | MPElementTypeClassGenerated | 0x0,
MPElementTypeGeneratedLong = 0x1 | MPElementTypeClassGenerated | 0x0,
MPElementTypeGeneratedMedium = 0x2 | MPElementTypeClassGenerated | 0x0,
MPElementTypeGeneratedBasic = 0x4 | MPElementTypeClassGenerated | 0x0,
MPElementTypeGeneratedShort = 0x3 | MPElementTypeClassGenerated | 0x0,
MPElementTypeGeneratedPIN = 0x5 | MPElementTypeClassGenerated | 0x0,
MPElementTypeStoredPersonal = 0x0 | MPElementTypeClassStored | MPElementFeatureExportContent,
MPElementTypeStoredDevicePrivate = 0x1 | MPElementTypeClassStored | MPElementFeatureDevicePrivate,
} MPElementType;
#define MPErrorDomain @"MPErrorDomain"
#define MPCheckpointHelpChapter @"MPCheckpointHelpChapter"
#define MPCheckpointCopyToPasteboard @"MPCheckpointCopyToPasteboard"
#define MPCheckpointCopyLoginNameToPasteboard @"MPCheckpointCopyLoginNameToPasteboard"
#define MPCheckpointResetPasswordCounter @"MPCheckpointResetPasswordCounter"
#define MPCheckpointIncrementPasswordCounter @"MPCheckpointIncrementPasswordCounter"
#define MPCheckpointEditPassword @"MPCheckpointEditPassword"
#define MPCheckpointEditLoginName @"MPCheckpointEditLoginName"
#define MPCheckpointUseType @"MPCheckpointUseType"
#define MPCheckpointDeleteElement @"MPCheckpointDeleteElement"
#define MPCheckpointShowGuide @"MPCheckpointShowGuide"
#define MPCheckpointChangeMP @"MPCheckpointChangeMP"
#define MPCheckpointMPErrorUbiquity @"MPCheckpointMPErrorUbiquity"
#define MPCheckpointLocalStoreReset @"MPCheckpointLocalStoreReset"
#define MPCheckpointCloudStoreReset @"MPCheckpointCloudStoreReset"
#define MPCheckpointSignInFailed @"MPCheckpointSignInFailed"
#define MPCheckpointSignedIn @"MPCheckpointSignedIn"
#define MPCheckpointConfig @"MPCheckpointConfig"
#define MPCheckpointCloud @"MPCheckpointCloud"
#define MPCheckpointCloudEnabled @"MPCheckpointCloudEnabled"
#define MPCheckpointCloudDisabled @"MPCheckpointCloudDisabled"
#define MPCheckpointSitesImported @"MPCheckpointSitesImported"
#define MPCheckpointSitesExported @"MPCheckpointSitesExported"
#define MPCheckpointExplicitMigration @"MPCheckpointExplicitMigration"
#define MPCheckpointReview @"MPCheckpointReview"
#define MPCheckpointApps @"MPCheckpointApps"
#define MPCheckpointAppGorillas @"MPCheckpointAppGorillas"
#define MPCheckpointAppDeBlock @"MPCheckpointAppDeBlock"
#define MPSignedInNotification @"MPSignedInNotification"
#define MPSignedOutNotification @"MPSignedOutNotification"
#define MPKeyForgottenNotification @"MPKeyForgottenNotification"
#define MPElementUpdatedNotification @"MPElementUpdatedNotification"
#define MPCheckConfigNotification @"MPCheckConfigNotification"

View File

@@ -0,0 +1,32 @@
//
// MPUserEntity.h
// MasterPassword-iOS
//
// Created by Maarten Billemont on 2013-01-29.
// Copyright (c) 2013 Lyndir. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
@class MPElementEntity;
@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) NSSet *elements;
@end
@interface MPUserEntity (CoreDataGeneratedAccessors)
- (void)addElementsObject:(MPElementEntity *)value;
- (void)removeElementsObject:(MPElementEntity *)value;
- (void)addElements:(NSSet *)values;
- (void)removeElements:(NSSet *)values;
@end

View File

@@ -0,0 +1,23 @@
//
// MPUserEntity.m
// MasterPassword-iOS
//
// Created by Maarten Billemont on 2013-01-29.
// Copyright (c) 2013 Lyndir. All rights reserved.
//
#import "MPUserEntity.h"
#import "MPElementEntity.h"
@implementation MPUserEntity
@dynamic avatar_;
@dynamic defaultType_;
@dynamic keyID;
@dynamic lastUsed;
@dynamic name;
@dynamic saveKey_;
@dynamic elements;
@end

View File

@@ -0,0 +1,32 @@
//
// MPAppDelegate.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"
@interface MPAppDelegate : MPAppDelegate_Shared<NSApplicationDelegate>
@property (nonatomic, strong) NSStatusItem *statusItem;
@property (nonatomic, strong) MPPasswordWindowController *passwordWindow;
@property (nonatomic, weak) IBOutlet NSMenuItem *lockItem;
@property (nonatomic, weak) IBOutlet NSMenuItem *showItem;
@property (nonatomic, strong) IBOutlet NSMenu *statusMenu;
@property (nonatomic, weak) IBOutlet NSMenuItem *useICloudItem;
@property (nonatomic, weak) IBOutlet NSMenuItem *rememberPasswordItem;
@property (nonatomic, weak) IBOutlet NSMenuItem *savePasswordItem;
@property (nonatomic, weak) IBOutlet NSMenuItem *createUserItem;
@property (nonatomic, weak) IBOutlet NSMenuItem *usersItem;
- (IBAction)activate:(id)sender;
- (IBAction)togglePreference:(NSMenuItem *)sender;
- (IBAction)newUser:(NSMenuItem *)sender;
- (IBAction)signOut:(id)sender;
- (IBAction)lock:(id)sender;
@end

View File

@@ -0,0 +1,369 @@
//
// MPAppDelegate.m
// MasterPassword
//
// Created by Maarten Billemont on 04/03/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "MPAppDelegate.h"
#import "MPAppDelegate_Key.h"
#import "MPAppDelegate_Store.h"
#import <Carbon/Carbon.h>
@implementation MPAppDelegate
@synthesize statusItem;
@synthesize lockItem;
@synthesize showItem;
@synthesize statusMenu;
@synthesize useICloudItem;
@synthesize rememberPasswordItem;
@synthesize savePasswordItem;
@synthesize passwordWindow;
@synthesize key;
#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 initialize;
dispatch_once(&initialize, ^{
[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 MPAppDelegate *)userData) activate:nil];
return noErr;
}
if (hotKeyID.signature == MPLockHotKey.signature && hotKeyID.id == MPLockHotKey.id) {
[((__bridge MPAppDelegate *)userData) lock:nil];
return noErr;
}
return eventNotHandledErr;
}
- (void)updateUsers {
[[[self.usersItem submenu] itemArray] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if (idx > 1)
[[self.usersItem submenu] removeItem:obj];
}];
NSManagedObjectContext *moc = [MPAppDelegate managedObjectContextForThreadIfReady];
if (!moc) {
self.createUserItem.title = @"New User (Not ready)";
self.createUserItem.enabled = NO;
self.createUserItem.toolTip = @"Please wait until the app is fully loaded.";
[self.usersItem.submenu addItemWithTitle:@"Loading..." action:NULL keyEquivalent:@""].enabled = NO;
return;
}
self.createUserItem.title = @"New User";
self.createUserItem.enabled = YES;
self.createUserItem.toolTip = nil;
NSError *error = nil;
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPUserEntity class])];
fetchRequest.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"lastUsed" ascending:NO]];
NSArray *users = [moc executeFetchRequest:fetchRequest error:&error];
if (!users)
err(@"Failed to load users: %@", error);
if (![users count]) {
NSMenuItem *noUsersItem = [self.usersItem.submenu addItemWithTitle:@"No users" action:NULL keyEquivalent:@""];
noUsersItem.enabled = NO;
noUsersItem.toolTip = @"Use the iOS app to create users and make sure iCloud is enabled in its preferences as well. "
@"Then give iCloud some time to sync the new user to your Mac.";
}
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 ([user.name isEqualToString:[MPMacConfig get].usedUserName])
[self selectUser:userItem];
}
}
- (void)selectUser:(NSMenuItem *)item {
self.activeUser = (MPUserEntity *)[[MPAppDelegate managedObjectContextForThreadIfReady] objectRegisteredForID:[item representedObject]];
}
- (void)showMenu {
[self updateMenuItems];
[self.statusItem popUpStatusItemMenu:self.statusMenu];
}
- (IBAction)activate:(id)sender {
if (!self.activeUser)
// No user, can't activate.
return;
if ([[NSApplication sharedApplication] isActive])
[self applicationDidBecomeActive:nil];
else
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
}
- (IBAction)togglePreference:(NSMenuItem *)sender {
if (sender == useICloudItem)
[MPConfig get].iCloud = @(sender.state == NSOnState);
if (sender == rememberPasswordItem)
[MPConfig get].rememberLogin = [NSNumber numberWithBool:![[MPConfig get].rememberLogin boolValue]];
if (sender == savePasswordItem) {
MPUserEntity *activeUser = [MPAppDelegate get].activeUser;
if ((activeUser.saveKey = !activeUser.saveKey))
[[MPAppDelegate get] storeSavedKeyFor:activeUser];
else
[[MPAppDelegate get] forgetSavedKeyFor:activeUser];
[activeUser saveContext];
}
}
- (IBAction)newUser:(NSMenuItem *)sender {
}
- (IBAction)signOut:(id)sender {
[self signOutAnimated:YES];
}
- (IBAction)lock:(id)sender {
self.key = nil;
}
- (void)didUpdateConfigForKey:(SEL)configKey fromValue:(id)oldValue {
[[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification
object:NSStringFromSelector(configKey) userInfo:nil];
}
#pragma mark - NSApplicationDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Setup delegates and listeners.
[MPConfig get].delegate = self;
__weak id weakSelf = self;
[self addObserverBlock:^(NSString *keyPath, id object, NSDictionary *change, void *context) {
[weakSelf updateMenuItems];
} forKeyPath:@"key" options:NSKeyValueObservingOptionInitial context:nil];
[self addObserverBlock:^(NSString *keyPath, id object, NSDictionary *change, void *context) {
[weakSelf updateMenuItems];
} forKeyPath:@"activeUser" options:NSKeyValueObservingOptionInitial context:nil];
// Status item.
self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
self.statusItem.image = [NSImage imageNamed:@"menu-icon"];
self.statusItem.highlightMode = YES;
self.statusItem.target = self;
self.statusItem.action = @selector(showMenu);
__weak MPAppDelegate *wSelf = self;
[self addObserverBlock:^(NSString *keyPath, id object, NSDictionary *change, void *context) {
MPUserEntity *activeUser = wSelf.activeUser;
[[[wSelf.usersItem submenu] itemArray] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if ([[obj representedObject] isEqual:[activeUser objectID]])
[obj setState:NSOnState];
else
[obj setState:NSOffState];
}];
[MPMacConfig get].usedUserName = activeUser.name;
} forKeyPath:@"activeUserObjectID" options:0 context:nil];
[[NSNotificationCenter defaultCenter] addObserverForName:UbiquityManagedStoreDidChangeNotification object:nil queue:nil usingBlock:
^(NSNotification *note) {
[self updateUsers];
}];
[[NSNotificationCenter defaultCenter] addObserverForName:UbiquityManagedStoreDidImportChangesNotification object:nil queue:nil
usingBlock:
^(NSNotification *note) {
[self updateUsers];
}];
[[NSNotificationCenter defaultCenter] addObserverForName:MPCheckConfigNotification object:nil queue:nil usingBlock:
^(NSNotification *note) {
self.rememberPasswordItem.state = [[MPConfig get].rememberLogin boolValue]? NSOnState: NSOffState;
self.savePasswordItem.state = [MPAppDelegate get].activeUser.saveKey? 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: %d", status);
status = RegisterEventHotKey(35 /* p */, controlKey + cmdKey, MPShowHotKey, GetApplicationEventTarget(), 0, &hotKeyRef);
if (status != noErr)
err(@"Error registering 'show' hotkey: %d", status);
status = RegisterEventHotKey(35 /* p */, controlKey + optionKey + cmdKey, MPLockHotKey, GetApplicationEventTarget(), 0, &hotKeyRef);
if (status != noErr)
err(@"Error registering 'lock' hotkey: %d", status);
}
- (void)updateMenuItems {
if (!(self.showItem.enabled = ![self.passwordWindow.window isVisible])) {
self.showItem.title = @"Show (Showing)";
self.showItem.toolTip = @"Master Password is already showing.";
} else if (!(self.showItem.enabled = (self.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.";
}
self.rememberPasswordItem.state = [[MPConfig get].rememberLogin boolValue]? NSOnState: NSOffState;
self.savePasswordItem.state = [MPAppDelegate get].activeUser.saveKey? NSOnState: NSOffState;
if (!self.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;
}
self.useICloudItem.state = [[MPMacConfig get].iCloud boolValue]? NSOnState: NSOffState;
if (!(self.useICloudItem.enabled = ![[MPMacConfig get].iCloud boolValue])) {
self.useICloudItem.title = @"Use iCloud (Required)";
self.useICloudItem.toolTip = @"iCloud is required in this version. Future versions will work without iCloud as well.";
}
else {
self.useICloudItem.title = @"Use iCloud (Required)";
self.useICloudItem.toolTip = nil;
}
}
- (void)applicationWillBecomeActive:(NSNotification *)notification {
if (!self.passwordWindow)
self.passwordWindow = [[MPPasswordWindowController alloc] initWithWindowNibName:@"MPPasswordWindowController"];
}
- (void)applicationDidBecomeActive:(NSNotification *)notification {
[self.passwordWindow showWindow:self];
}
- (void)applicationWillResignActive:(NSNotification *)notification {
if (![[MPConfig get].rememberLogin boolValue])
self.key = nil;
}
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
// Save changes in the application's managed object context before the application terminates.
NSManagedObjectContext *moc = [MPAppDelegate managedObjectContextForThreadIfReady];
if (!moc)
return NSTerminateNow;
if (![moc commitEditing])
return NSTerminateCancel;
if (![moc hasChanges])
return NSTerminateNow;
NSError *error = nil;
if (![moc save:&error])
err(@"While terminating: %@", error);
return NSTerminateNow;
}
#pragma mark - UbiquityStoreManagerDelegate
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didSwitchToCloud:(BOOL)cloudEnabled {
[super ubiquityStoreManager:manager didSwitchToCloud:cloudEnabled];
[self updateMenuItems];
if (![[MPConfig get].iCloudDecided boolValue]) {
if (cloudEnabled)
return;
switch ([[NSAlert alertWithMessageText:@"iCloud Is Disabled"
defaultButton:@"Enable iCloud" alternateButton:@"Leave iCloud Off" otherButton:@"Explain?"
informativeTextWithFormat:@"It is highly recommended you enable iCloud."] runModal]) {
case NSAlertDefaultReturn: {
[MPConfig get].iCloudDecided = @YES;
manager.cloudEnabled = YES;
break;
}
case NSAlertOtherReturn: {
[[NSAlert alertWithMessageText:@"About iCloud"
defaultButton:[PearlStrings get].commonButtonThanks alternateButton:nil otherButton:nil
informativeTextWithFormat:
@"iCloud is Apple's solution for saving your data in \"the cloud\" "
@"and making sure your other iPhones, iPads and Macs are in sync.\n\n"
@"For Master Password, that means your sites are available on all your "
@"Apple devices, and you always have a backup of them in case "
@"you loose one or need to restore.\n\n"
@"Because of the way Master Password works, it doesn't need to send your "
@"site's passwords to Apple. Only their names are saved to make it easier "
@"for you to find the site you need. For some sites you may have set "
@"a user-specified password: these are sent to iCloud after being encrypted "
@"with your master password.\n\n"
@"Apple can never see any of your passwords."] runModal];
[self ubiquityStoreManager:manager didSwitchToCloud:cloudEnabled];
break;
}
default:
break;
};
}
}
@end

View File

@@ -0,0 +1,15 @@
//
// 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;
@end

View File

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

View File

@@ -0,0 +1,27 @@
//
// MPPasswordWindowController.h
// MasterPassword-Mac
//
// Created by Maarten Billemont on 04/03/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import <Cocoa/Cocoa.h>
@interface MPPasswordWindowController : NSWindowController<NSTextFieldDelegate> {
NSString *_content;
}
@property (nonatomic, strong) NSString *content;
@property (nonatomic, weak) IBOutlet NSTextField *siteField;
@property (nonatomic, weak) IBOutlet NSTextField *contentField;
@property (nonatomic, weak) IBOutlet NSTextField *tipField;
@property (nonatomic, weak) IBOutlet NSView *contentContainer;
@property (nonatomic, weak) IBOutlet NSProgressIndicator *progressView;
@property (nonatomic, weak) IBOutlet NSTextField *userLabel;
- (void)unlock;
@end

View File

@@ -0,0 +1,346 @@
//
// MPPasswordWindowController.m
// MasterPassword-Mac
//
// Created by Maarten Billemont on 04/03/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "MPPasswordWindowController.h"
#import "MPAppDelegate.h"
#import "MPAppDelegate_Key.h"
#import "MPAppDelegate_Store.h"
#define MPAlertUnlockMP @"MPAlertUnlockMP"
#define MPAlertIncorrectMP @"MPAlertIncorrectMP"
@interface MPPasswordWindowController ()
@property (nonatomic, strong) NSArray /* MPElementEntity */ *siteResults;
@property (nonatomic) BOOL inProgress;
@property (nonatomic) BOOL siteFieldPreventCompletion;
@end
@implementation MPPasswordWindowController
- (void)windowDidLoad {
[self setContent:@""];
[self.tipField setStringValue:@""];
[[MPAppDelegate get] addObserverBlock:^(NSString *keyPath, id object, NSDictionary *change, void *context) {
[self.userLabel setStringValue:PearlString(@"%@'s password for:", [MPAppDelegate get].activeUser.name)];
} forKeyPath:@"activeUser" options:NSKeyValueObservingOptionInitial context:nil];
[[MPAppDelegate get] addObserverBlock:^(NSString *keyPath, id object, NSDictionary *change, void *context) {
if (![MPAppDelegate get].key) {
[self unlock];
return;
}
[MPAppDelegate managedObjectContextPerform:^(NSManagedObjectContext *moc) {
MPUserEntity *activeUser = [MPAppDelegate get].activeUser;
if (![MPAlgorithmDefault migrateUser:activeUser])
[NSAlert alertWithMessageText:@"Migration Needed" defaultButton:@"OK" alternateButton:nil otherButton:nil
informativeTextWithFormat:@"Certain sites require explicit migration to get updated to the latest version of the "
@"Master Password algorithm. For these sites, a migration button will appear. Migrating these sites will cause "
@"their passwords to change. You'll need to update your profile for that site with the new password."];
[activeUser saveContext];
}];
} forKeyPath:@"key" options:NSKeyValueObservingOptionInitial context:nil];
[[NSNotificationCenter defaultCenter] addObserverForName:NSWindowDidBecomeKeyNotification object:self.window queue:nil
usingBlock:^(NSNotification *note) {
if (!self.inProgress)
[self unlock];
[self.siteField selectText:self];
}];
[[NSNotificationCenter defaultCenter] addObserverForName:NSWindowWillCloseNotification object:self.window queue:nil
usingBlock:^(NSNotification *note) {
[[NSApplication sharedApplication] hide:self];
}];
[[NSNotificationCenter defaultCenter] addObserverForName:MPSignedOutNotification object:nil queue:nil
usingBlock:^(NSNotification *note) {
[self.window close];
}];
[super windowDidLoad];
}
- (void)unlock {
if (![MPAppDelegate get].activeUser)
// No user to sign in with.
return;
if ([MPAppDelegate get].key)
// Already logged in.
return;
if ([[MPAppDelegate get] signInAsUser:[MPAppDelegate get].activeUser usingMasterPassword:nil])
// Load the key from the keychain.
return;
if (![MPAppDelegate get].key)
// Ask the user to set the key through his master password.
dispatch_async(dispatch_get_main_queue(), ^{
if ([MPAppDelegate get].key)
return;
self.content = @"";
[self.siteField setStringValue:@""];
[self.tipField setStringValue:@""];
NSAlert *alert = [NSAlert alertWithMessageText:@"Master Password is locked."
defaultButton:@"Unlock" alternateButton:@"Change" otherButton:@"Cancel"
informativeTextWithFormat:@"The master password is required to unlock the application for:\n\n%@", [MPAppDelegate get].activeUser.name];
NSSecureTextField *passwordField = [[NSSecureTextField alloc] initWithFrame:NSMakeRect(0, 0, 200, 22)];
[alert setAccessoryView:passwordField];
[alert layout];
[passwordField becomeFirstResponder];
[alert beginSheetModalForWindow:self.window modalDelegate:self
didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:MPAlertUnlockMP];
});
}
- (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo {
if (contextInfo == MPAlertIncorrectMP) {
[self.window close];
return;
}
if (contextInfo == MPAlertUnlockMP) {
switch (returnCode) {
case NSAlertAlternateReturn:
// "Change" button.
if ([[NSAlert alertWithMessageText:@"Changing Master Password"
defaultButton:nil alternateButton:[PearlStrings get].commonButtonCancel otherButton:nil
informativeTextWithFormat:
@"This will allow you to log in with a different master password.\n\n"
@"Note that you will only see the sites and passwords for the master password you log in with.\n"
@"If you log in with a different master password, your current sites will be unavailable.\n\n"
@"You can always change back to your current master password later.\n"
@"Your current sites and passwords will then become available again."] runModal]
== 1) {
[MPAppDelegate get].activeUser.keyID = nil;
[[MPAppDelegate get] forgetSavedKeyFor:[MPAppDelegate get].activeUser];
[[MPAppDelegate get] signOutAnimated:YES];
}
break;
case NSAlertOtherReturn:
// "Cancel" button.
[self.window close];
return;
case NSAlertDefaultReturn: {
// "Unlock" button.
self.contentContainer.alphaValue = 0;
[self.progressView startAnimation:nil];
self.inProgress = YES;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
BOOL success = [[MPAppDelegate get] signInAsUser:[MPAppDelegate get].activeUser
usingMasterPassword:[(NSSecureTextField *)alert.accessoryView stringValue]];
self.inProgress = NO;
dispatch_async(dispatch_get_main_queue(), ^{
[self.progressView stopAnimation:nil];
if (success)
self.contentContainer.alphaValue = 1;
else {
[[NSAlert alertWithError:[NSError errorWithDomain:MPErrorDomain code:0 userInfo:@{
NSLocalizedDescriptionKey : PearlString(@"Incorrect master password for user %@",
[MPAppDelegate get].activeUser.name)
}]] beginSheetModalForWindow:self.window modalDelegate:self
didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:MPAlertIncorrectMP];
}
});
});
}
default:
break;
}
return;
}
}
- (NSArray *)control:(NSControl *)control textView:(NSTextView *)textView completions:(NSArray *)words
forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index {
NSString *query = [[control stringValue] substringWithRange:charRange];
if (![query length] || ![MPAppDelegate get].key)
return nil;
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses_" ascending:NO]];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(name BEGINSWITH[cd] %@) AND user == %@",
query, [MPAppDelegate get].activeUser];
NSError *error = nil;
self.siteResults = [[MPAppDelegate managedObjectContextForThreadIfReady] executeFetchRequest:fetchRequest error:&error];
if (error)
err(@"While fetching elements for completion: %@", error);
if ([self.siteResults count] == 1) {
[textView setString:[(MPElementEntity *)[self.siteResults objectAtIndex:0] name]];
[textView setSelectedRange:NSMakeRange([query length], [[textView string] length] - [query length])];
if ([self trySite])
return nil;
}
NSMutableArray *mutableResults = [NSMutableArray arrayWithCapacity:[self.siteResults count] + 1];
if (self.siteResults)
for (MPElementEntity *element in self.siteResults)
[mutableResults addObject:element.name];
// [mutableResults addObject:query]; // For when the app should be able to create new sites.
return mutableResults;
}
- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector {
if (commandSelector == @selector(cancel:)) {
[self.window close];
return YES;
}
if ((self.siteFieldPreventCompletion = [NSStringFromSelector(commandSelector) hasPrefix:@"delete"]))
return NO;
if (commandSelector == @selector(insertNewline:) && [self.content length]) {
if ([self trySite])
[self copyContents];
return YES;
}
return NO;
}
- (void)copyContents {
[[NSPasteboard generalPasteboard] declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil];
if (![[NSPasteboard generalPasteboard] setString:self.content forType:NSPasteboardTypeString]) {
wrn(@"Couldn't copy password to pasteboard.");
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
self.tipField.alphaValue = 1;
[self.tipField setStringValue:@"Copied! Hit ⎋ (ESC) to close window."];
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0f * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^{
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:0.2f];
[self.tipField.animator setAlphaValue:0];
[NSAnimationContext endGrouping];
});
});
[[self findElement] use];
}
- (void)controlTextDidEndEditing:(NSNotification *)note {
if (note.object != self.siteField)
return;
[self trySite];
}
- (void)controlTextDidChange:(NSNotification *)note {
if (note.object != self.siteField)
return;
// Update the site content as the site name changes.
BOOL hasValidSite = [self trySite];
if ([[NSApp currentEvent] type] == NSKeyDown && [[[NSApp currentEvent] charactersIgnoringModifiers] isEqualToString:@"\r"]) {
if (hasValidSite)
[self copyContents];
return;
}
if (self.siteFieldPreventCompletion) {
self.siteFieldPreventCompletion = NO;
return;
}
self.siteFieldPreventCompletion = YES;
[(NSText *)[note.userInfo objectForKey:@"NSFieldEditor"] complete:self];
self.siteFieldPreventCompletion = NO;
}
- (NSString *)content {
return _content;
}
- (void)setContent:(NSString *)content {
_content = content;
NSMutableParagraphStyle *paragraph = [NSMutableParagraphStyle new];
paragraph.alignment = NSCenterTextAlignment;
[self.contentField setAttributedStringValue:
[[NSAttributedString alloc] initWithString:_content
attributes:[[NSMutableDictionary alloc] initWithObjectsAndKeys:
paragraph, NSParagraphStyleAttributeName,
nil]]];
}
- (BOOL)trySite {
MPElementEntity *result = [self findElement];
if (!result) {
[self setContent:@""];
[self.tipField setStringValue:@""];
return NO;
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSString *description = [result.content description];
if (!description)
description = @"";
dispatch_async(dispatch_get_main_queue(), ^{
[self setContent:description];
[self.tipField setStringValue:@"Hit ⌤ (ENTER) to copy the password."];
self.tipField.alphaValue = 1;
});
});
// For when the app should be able to create new sites.
/*
else
[[MPAppDelegate get].managedObjectContext performBlock:^{
MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPElementGeneratedEntity class])
inManagedObjectContext:[MPAppDelegate get].managedObjectContext];
assert([element isKindOfClass:ClassFromMPElementType(element.type)]);
assert([MPAppDelegate get].keyID);
element.name = siteName;
element.keyID = [MPAppDelegate get].keyID;
NSString *description = [element.content description];
[element use];
dispatch_async(dispatch_get_main_queue(), ^{
[self setContent:description];
});
}];
*/
return YES;
}
- (MPElementEntity *)findElement {
for (MPElementEntity *element in self.siteResults)
if ([element.name isEqualToString:[self.siteField stringValue]])
return element;
return nil;
}
@end

View File

@@ -0,0 +1,860 @@
<?xml version="1.0" encoding="UTF-8"?>
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="8.00">
<data>
<int key="IBDocument.SystemTarget">1080</int>
<string key="IBDocument.SystemVersion">12C60</string>
<string key="IBDocument.InterfaceBuilderVersion">2840</string>
<string key="IBDocument.AppKitVersion">1187.34</string>
<string key="IBDocument.HIToolboxVersion">625.00</string>
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="NS.object.0">2840</string>
</object>
<array key="IBDocument.IntegratedClassDependencies">
<string>IBNSLayoutConstraint</string>
<string>NSCustomObject</string>
<string>NSCustomView</string>
<string>NSProgressIndicator</string>
<string>NSTextField</string>
<string>NSTextFieldCell</string>
<string>NSView</string>
<string>NSWindowTemplate</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">MPPasswordWindowController</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="45434518">
<int key="NSWindowStyleMask">287</int>
<int key="NSWindowBacking">2</int>
<string key="NSWindowRect">{{600, 530}, {480, 200}}</string>
<int key="NSWTFlags">611845120</int>
<string key="NSWindowTitle">Master Password</string>
<string key="NSWindowClass">NSPanel</string>
<nil key="NSViewClass"/>
<nil key="NSUserInterfaceItemIdentifier"/>
<string key="NSWindowContentMaxSize">{480, 320}</string>
<string key="NSWindowContentMinSize">{480, 134}</string>
<object class="NSView" key="NSWindowView" id="258451033">
<reference key="NSNextResponder"/>
<int key="NSvFlags">256</int>
<array class="NSMutableArray" key="NSSubviews">
<object class="NSCustomView" id="1072816887">
<reference key="NSNextResponder" ref="258451033"/>
<int key="NSvFlags">268</int>
<array class="NSMutableArray" key="NSSubviews">
<object class="NSTextField" id="642967193">
<reference key="NSNextResponder" ref="1072816887"/>
<int key="NSvFlags">268</int>
<string key="NSFrame">{{131, 163}, {219, 17}}</string>
<reference key="NSSuperview" ref="1072816887"/>
<reference key="NSNextKeyView" ref="402376051"/>
<string key="NSReuseIdentifierKey">_NS:1535</string>
<bool key="NSEnabled">YES</bool>
<object class="NSTextFieldCell" key="NSCell" id="406294418">
<int key="NSCellFlags">68157504</int>
<int key="NSCellFlags2">272630784</int>
<string key="NSContents">Maarten Billemont's password for:</string>
<object class="NSFont" key="NSSupport" id="590895625">
<string key="NSName">LucidaGrande</string>
<double key="NSSize">13</double>
<int key="NSfFlags">1044</int>
</object>
<string key="NSCellIdentifier">_NS:1535</string>
<reference key="NSControlView" ref="642967193"/>
<object class="NSColor" key="NSBackgroundColor" id="245864165">
<int key="NSColorSpace">6</int>
<string key="NSCatalogName">System</string>
<string key="NSColorName">controlColor</string>
<object class="NSColor" key="NSColor">
<int key="NSColorSpace">3</int>
<bytes key="NSWhite">MC42NjY2NjY2NjY3AA</bytes>
</object>
</object>
<object class="NSColor" key="NSTextColor">
<int key="NSColorSpace">6</int>
<string key="NSCatalogName">System</string>
<string key="NSColorName">controlTextColor</string>
<object class="NSColor" key="NSColor" id="714751679">
<int key="NSColorSpace">3</int>
<bytes key="NSWhite">MAA</bytes>
</object>
</object>
</object>
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
</object>
<object class="NSTextField" id="49669222">
<reference key="NSNextResponder" ref="1072816887"/>
<int key="NSvFlags">268</int>
<string key="NSFrame">{{17, 20}, {446, 17}}</string>
<reference key="NSSuperview" ref="1072816887"/>
<reference key="NSNextKeyView" ref="104294954"/>
<string key="NSReuseIdentifierKey">_NS:1505</string>
<bool key="NSEnabled">YES</bool>
<object class="NSTextFieldCell" key="NSCell" id="249851874">
<int key="NSCellFlags">68157504</int>
<int key="NSCellFlags2">138413056</int>
<string key="NSContents">Hit enter to copy the password.</string>
<reference key="NSSupport" ref="590895625"/>
<string key="NSCellIdentifier">_NS:1505</string>
<reference key="NSControlView" ref="49669222"/>
<reference key="NSBackgroundColor" ref="245864165"/>
<object class="NSColor" key="NSTextColor">
<int key="NSColorSpace">6</int>
<string key="NSCatalogName">System</string>
<string key="NSColorName">controlLightHighlightColor</string>
<object class="NSColor" key="NSColor" id="370404594">
<int key="NSColorSpace">3</int>
<bytes key="NSWhite">MQA</bytes>
</object>
</object>
</object>
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
</object>
<object class="NSTextField" id="402376051">
<reference key="NSNextResponder" ref="1072816887"/>
<int key="NSvFlags">268</int>
<string key="NSFrame">{{140, 133}, {200, 22}}</string>
<reference key="NSSuperview" ref="1072816887"/>
<reference key="NSNextKeyView" ref="139778114"/>
<string key="NSReuseIdentifierKey">_NS:9</string>
<bool key="NSEnabled">YES</bool>
<object class="NSTextFieldCell" key="NSCell" id="961966865">
<int key="NSCellFlags">-1804599231</int>
<int key="NSCellFlags2">138413120</int>
<string key="NSContents">apple.com</string>
<reference key="NSSupport" ref="590895625"/>
<string key="NSPlaceholderString">Site name</string>
<string key="NSCellIdentifier">_NS:9</string>
<reference key="NSControlView" ref="402376051"/>
<int key="NSTextBezelStyle">1</int>
<object class="NSColor" key="NSBackgroundColor">
<int key="NSColorSpace">6</int>
<string key="NSCatalogName">System</string>
<string key="NSColorName">textBackgroundColor</string>
<reference key="NSColor" ref="370404594"/>
</object>
<object class="NSColor" key="NSTextColor">
<int key="NSColorSpace">6</int>
<string key="NSCatalogName">System</string>
<string key="NSColorName">textColor</string>
<reference key="NSColor" ref="714751679"/>
</object>
</object>
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
</object>
<object class="NSTextField" id="139778114">
<reference key="NSNextResponder" ref="1072816887"/>
<int key="NSvFlags">268</int>
<string key="NSFrame">{{17, 61}, {446, 64}}</string>
<reference key="NSSuperview" ref="1072816887"/>
<reference key="NSNextKeyView" ref="49669222"/>
<object class="NSShadow" key="NSViewShadow">
<double key="NSShadowHoriz">1</double>
<double key="NSShadowVert">1</double>
<double key="NSShadowBlurRadius">1</double>
<object class="NSColor" key="NSShadowColor" id="444840817">
<int key="NSColorSpace">3</int>
<bytes key="NSWhite">MCAwLjYAA</bytes>
</object>
</object>
<string key="NSReuseIdentifierKey">_NS:9</string>
<string key="NSAntiCompressionPriority">{250, 750}</string>
<bool key="NSEnabled">YES</bool>
<object class="NSTextFieldCell" key="NSCell" id="678134424">
<int key="NSCellFlags">67108864</int>
<int key="NSCellFlags2">138412032</int>
<string key="NSContents">S3cretP4s$w0rD</string>
<object class="NSFont" key="NSSupport">
<string key="NSName">Exo-Black</string>
<double key="NSSize">48</double>
<int key="NSfFlags">16</int>
</object>
<string key="NSCellIdentifier">_NS:9</string>
<reference key="NSControlView" ref="139778114"/>
<bool key="NSDrawsBackground">YES</bool>
<reference key="NSBackgroundColor" ref="245864165"/>
<object class="NSColor" key="NSTextColor">
<int key="NSColorSpace">1</int>
<bytes key="NSRGB">MC40NzQ1MDk4MDM5IDAuODY2NjY2NjY2NyAwLjk4NDMxMzcyNTUAA</bytes>
</object>
</object>
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
</object>
</array>
<string key="NSFrameSize">{480, 200}</string>
<reference key="NSSuperview" ref="258451033"/>
<reference key="NSNextKeyView" ref="642967193"/>
<string key="NSReuseIdentifierKey">_NS:9</string>
<string key="NSClassName">NSView</string>
</object>
<object class="NSProgressIndicator" id="104294954">
<reference key="NSNextResponder" ref="258451033"/>
<int key="NSvFlags">268</int>
<string key="NSFrame">{{224, 84}, {32, 32}}</string>
<reference key="NSSuperview" ref="258451033"/>
<reference key="NSNextKeyView"/>
<string key="NSReuseIdentifierKey">_NS:945</string>
<int key="NSpiFlags">28682</int>
<double key="NSMaxValue">100</double>
</object>
</array>
<string key="NSFrameSize">{480, 200}</string>
<reference key="NSSuperview"/>
<reference key="NSNextKeyView" ref="1072816887"/>
<bool key="NSViewIsLayerTreeHost">YES</bool>
<string key="NSReuseIdentifierKey">_NS:21</string>
</object>
<string key="NSScreenRect">{{0, 0}, {1680, 1028}}</string>
<string key="NSMinSize">{480, 150}</string>
<string key="NSMaxSize">{480, 336}</string>
<bool key="NSWindowIsRestorable">YES</bool>
</object>
</array>
<object class="IBObjectContainer" key="IBDocument.Objects">
<array class="NSMutableArray" key="connectionRecords">
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">window</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="45434518"/>
</object>
<int key="connectionID">40</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">contentContainer</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="1072816887"/>
</object>
<int key="connectionID">214</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">progressView</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="104294954"/>
</object>
<int key="connectionID">215</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">userLabel</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="642967193"/>
</object>
<int key="connectionID">223</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">siteField</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="402376051"/>
</object>
<int key="connectionID">224</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">contentField</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="139778114"/>
</object>
<int key="connectionID">225</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">tipField</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="49669222"/>
</object>
<int key="connectionID">226</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">delegate</string>
<reference key="source" ref="45434518"/>
<reference key="destination" ref="1001"/>
</object>
<int key="connectionID">39</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">delegate</string>
<reference key="source" ref="402376051"/>
<reference key="destination" ref="1001"/>
</object>
<int key="connectionID">188</int>
</object>
</array>
<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">22</int>
<reference key="object" ref="45434518"/>
<array class="NSMutableArray" key="children">
<reference ref="258451033"/>
</array>
<reference key="parent" ref="0"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">23</int>
<reference key="object" ref="258451033"/>
<array class="NSMutableArray" key="children">
<object class="IBNSLayoutConstraint" id="671970801">
<reference key="firstItem" ref="104294954"/>
<int key="firstAttribute">9</int>
<int key="relation">0</int>
<reference key="secondItem" ref="1072816887"/>
<int key="secondAttribute">9</int>
<float key="multiplier">1</float>
<object class="IBLayoutConstant" key="constant">
<double key="value">0.0</double>
</object>
<float key="priority">1000</float>
<reference key="containingView" ref="258451033"/>
<int key="scoringType">6</int>
<float key="scoringTypeFloat">24</float>
<int key="contentType">2</int>
</object>
<object class="IBNSLayoutConstraint" id="534262842">
<reference key="firstItem" ref="104294954"/>
<int key="firstAttribute">10</int>
<int key="relation">0</int>
<reference key="secondItem" ref="1072816887"/>
<int key="secondAttribute">10</int>
<float key="multiplier">1</float>
<object class="IBLayoutConstant" key="constant">
<double key="value">0.0</double>
</object>
<float key="priority">1000</float>
<reference key="containingView" ref="258451033"/>
<int key="scoringType">6</int>
<float key="scoringTypeFloat">24</float>
<int key="contentType">2</int>
</object>
<object class="IBNSLayoutConstraint" id="685084851">
<reference key="firstItem" ref="1072816887"/>
<int key="firstAttribute">3</int>
<int key="relation">0</int>
<reference key="secondItem" ref="258451033"/>
<int key="secondAttribute">3</int>
<float key="multiplier">1</float>
<object class="IBLayoutConstant" key="constant">
<double key="value">0.0</double>
</object>
<float key="priority">1000</float>
<reference key="containingView" ref="258451033"/>
<int key="scoringType">8</int>
<float key="scoringTypeFloat">29</float>
<int key="contentType">3</int>
</object>
<object class="IBNSLayoutConstraint" id="88899061">
<reference key="firstItem" ref="1072816887"/>
<int key="firstAttribute">6</int>
<int key="relation">0</int>
<reference key="secondItem" ref="258451033"/>
<int key="secondAttribute">6</int>
<float key="multiplier">1</float>
<object class="IBLayoutConstant" key="constant">
<double key="value">0.0</double>
</object>
<float key="priority">1000</float>
<reference key="containingView" ref="258451033"/>
<int key="scoringType">8</int>
<float key="scoringTypeFloat">29</float>
<int key="contentType">3</int>
</object>
<object class="IBNSLayoutConstraint" id="265452638">
<reference key="firstItem" ref="1072816887"/>
<int key="firstAttribute">4</int>
<int key="relation">0</int>
<reference key="secondItem" ref="258451033"/>
<int key="secondAttribute">4</int>
<float key="multiplier">1</float>
<object class="IBLayoutConstant" key="constant">
<double key="value">0.0</double>
</object>
<float key="priority">1000</float>
<reference key="containingView" ref="258451033"/>
<int key="scoringType">8</int>
<float key="scoringTypeFloat">29</float>
<int key="contentType">3</int>
</object>
<object class="IBNSLayoutConstraint" id="216428540">
<reference key="firstItem" ref="1072816887"/>
<int key="firstAttribute">5</int>
<int key="relation">0</int>
<reference key="secondItem" ref="258451033"/>
<int key="secondAttribute">5</int>
<float key="multiplier">1</float>
<object class="IBLayoutConstant" key="constant">
<double key="value">0.0</double>
</object>
<float key="priority">1000</float>
<reference key="containingView" ref="258451033"/>
<int key="scoringType">8</int>
<float key="scoringTypeFloat">29</float>
<int key="contentType">3</int>
</object>
<reference ref="104294954"/>
<reference ref="1072816887"/>
</array>
<reference key="parent" ref="45434518"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">96</int>
<reference key="object" ref="104294954"/>
<array class="NSMutableArray" key="children"/>
<reference key="parent" ref="258451033"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">143</int>
<reference key="object" ref="1072816887"/>
<array class="NSMutableArray" key="children">
<object class="IBNSLayoutConstraint" id="314583816">
<reference key="firstItem" ref="1072816887"/>
<int key="firstAttribute">4</int>
<int key="relation">0</int>
<reference key="secondItem" ref="49669222"/>
<int key="secondAttribute">4</int>
<float key="multiplier">1</float>
<object class="IBNSLayoutSymbolicConstant" key="constant">
<double key="value">20</double>
</object>
<float key="priority">1000</float>
<reference key="containingView" ref="1072816887"/>
<int key="scoringType">8</int>
<float key="scoringTypeFloat">29</float>
<int key="contentType">3</int>
</object>
<object class="IBNSLayoutConstraint" id="602857839">
<reference key="firstItem" ref="1072816887"/>
<int key="firstAttribute">6</int>
<int key="relation">0</int>
<reference key="secondItem" ref="49669222"/>
<int key="secondAttribute">6</int>
<float key="multiplier">1</float>
<object class="IBNSLayoutSymbolicConstant" key="constant">
<double key="value">20</double>
</object>
<float key="priority">1000</float>
<reference key="containingView" ref="1072816887"/>
<int key="scoringType">8</int>
<float key="scoringTypeFloat">29</float>
<int key="contentType">3</int>
</object>
<object class="IBNSLayoutConstraint" id="63384401">
<reference key="firstItem" ref="49669222"/>
<int key="firstAttribute">5</int>
<int key="relation">0</int>
<reference key="secondItem" ref="1072816887"/>
<int key="secondAttribute">5</int>
<float key="multiplier">1</float>
<object class="IBNSLayoutSymbolicConstant" key="constant">
<double key="value">20</double>
</object>
<float key="priority">1000</float>
<reference key="containingView" ref="1072816887"/>
<int key="scoringType">8</int>
<float key="scoringTypeFloat">29</float>
<int key="contentType">3</int>
</object>
<object class="IBNSLayoutConstraint" id="310034208">
<reference key="firstItem" ref="642967193"/>
<int key="firstAttribute">9</int>
<int key="relation">0</int>
<reference key="secondItem" ref="402376051"/>
<int key="secondAttribute">9</int>
<float key="multiplier">1</float>
<object class="IBLayoutConstant" key="constant">
<double key="value">0.0</double>
</object>
<float key="priority">1000</float>
<reference key="containingView" ref="1072816887"/>
<int key="scoringType">6</int>
<float key="scoringTypeFloat">24</float>
<int key="contentType">2</int>
</object>
<object class="IBNSLayoutConstraint" id="645313537">
<reference key="firstItem" ref="642967193"/>
<int key="firstAttribute">3</int>
<int key="relation">0</int>
<reference key="secondItem" ref="1072816887"/>
<int key="secondAttribute">3</int>
<float key="multiplier">1</float>
<object class="IBNSLayoutSymbolicConstant" key="constant">
<double key="value">20</double>
</object>
<float key="priority">1000</float>
<reference key="containingView" ref="1072816887"/>
<int key="scoringType">8</int>
<float key="scoringTypeFloat">29</float>
<int key="contentType">3</int>
</object>
<object class="IBNSLayoutConstraint" id="884917592">
<reference key="firstItem" ref="402376051"/>
<int key="firstAttribute">3</int>
<int key="relation">0</int>
<reference key="secondItem" ref="642967193"/>
<int key="secondAttribute">4</int>
<float key="multiplier">1</float>
<object class="IBNSLayoutSymbolicConstant" key="constant">
<double key="value">8</double>
</object>
<float key="priority">1000</float>
<reference key="containingView" ref="1072816887"/>
<int key="scoringType">6</int>
<float key="scoringTypeFloat">24</float>
<int key="contentType">3</int>
</object>
<object class="IBNSLayoutConstraint" id="1033518145">
<reference key="firstItem" ref="402376051"/>
<int key="firstAttribute">9</int>
<int key="relation">0</int>
<reference key="secondItem" ref="139778114"/>
<int key="secondAttribute">9</int>
<float key="multiplier">1</float>
<object class="IBLayoutConstant" key="constant">
<double key="value">0.0</double>
</object>
<float key="priority">1000</float>
<reference key="containingView" ref="1072816887"/>
<int key="scoringType">6</int>
<float key="scoringTypeFloat">24</float>
<int key="contentType">2</int>
</object>
<object class="IBNSLayoutConstraint" id="566883659">
<reference key="firstItem" ref="139778114"/>
<int key="firstAttribute">3</int>
<int key="relation">0</int>
<reference key="secondItem" ref="402376051"/>
<int key="secondAttribute">4</int>
<float key="multiplier">1</float>
<object class="IBNSLayoutSymbolicConstant" key="constant">
<double key="value">8</double>
</object>
<float key="priority">1000</float>
<reference key="containingView" ref="1072816887"/>
<int key="scoringType">6</int>
<float key="scoringTypeFloat">24</float>
<int key="contentType">3</int>
</object>
<object class="IBNSLayoutConstraint" id="831384658">
<reference key="firstItem" ref="1072816887"/>
<int key="firstAttribute">6</int>
<int key="relation">0</int>
<reference key="secondItem" ref="139778114"/>
<int key="secondAttribute">6</int>
<float key="multiplier">1</float>
<object class="IBNSLayoutSymbolicConstant" key="constant">
<double key="value">20</double>
</object>
<float key="priority">1000</float>
<reference key="containingView" ref="1072816887"/>
<int key="scoringType">8</int>
<float key="scoringTypeFloat">29</float>
<int key="contentType">3</int>
</object>
<object class="IBNSLayoutConstraint" id="865006730">
<reference key="firstItem" ref="139778114"/>
<int key="firstAttribute">5</int>
<int key="relation">0</int>
<reference key="secondItem" ref="1072816887"/>
<int key="secondAttribute">5</int>
<float key="multiplier">1</float>
<object class="IBNSLayoutSymbolicConstant" key="constant">
<double key="value">20</double>
</object>
<float key="priority">1000</float>
<reference key="containingView" ref="1072816887"/>
<int key="scoringType">8</int>
<float key="scoringTypeFloat">29</float>
<int key="contentType">3</int>
</object>
<reference ref="49669222"/>
<reference ref="642967193"/>
<reference ref="139778114"/>
<reference ref="402376051"/>
</array>
<reference key="parent" ref="258451033"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">145</int>
<reference key="object" ref="216428540"/>
<reference key="parent" ref="258451033"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">147</int>
<reference key="object" ref="265452638"/>
<reference key="parent" ref="258451033"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">148</int>
<reference key="object" ref="88899061"/>
<reference key="parent" ref="258451033"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">150</int>
<reference key="object" ref="685084851"/>
<reference key="parent" ref="258451033"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">181</int>
<reference key="object" ref="139778114"/>
<array class="NSMutableArray" key="children">
<reference ref="678134424"/>
</array>
<reference key="parent" ref="1072816887"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">187</int>
<reference key="object" ref="678134424"/>
<reference key="parent" ref="139778114"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">182</int>
<reference key="object" ref="402376051"/>
<array class="NSMutableArray" key="children">
<object class="IBNSLayoutConstraint" id="102475933">
<reference key="firstItem" ref="402376051"/>
<int key="firstAttribute">7</int>
<int key="relation">0</int>
<nil key="secondItem"/>
<int key="secondAttribute">0</int>
<float key="multiplier">1</float>
<object class="IBLayoutConstant" key="constant">
<double key="value">200</double>
</object>
<float key="priority">1000</float>
<reference key="containingView" ref="402376051"/>
<int key="scoringType">3</int>
<float key="scoringTypeFloat">9</float>
<int key="contentType">1</int>
</object>
<reference ref="961966865"/>
</array>
<reference key="parent" ref="1072816887"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">186</int>
<reference key="object" ref="102475933"/>
<reference key="parent" ref="402376051"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">185</int>
<reference key="object" ref="961966865"/>
<reference key="parent" ref="402376051"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">183</int>
<reference key="object" ref="49669222"/>
<array class="NSMutableArray" key="children">
<reference ref="249851874"/>
</array>
<reference key="parent" ref="1072816887"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">184</int>
<reference key="object" ref="249851874"/>
<reference key="parent" ref="49669222"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">199</int>
<reference key="object" ref="865006730"/>
<reference key="parent" ref="1072816887"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">200</int>
<reference key="object" ref="63384401"/>
<reference key="parent" ref="1072816887"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">201</int>
<reference key="object" ref="602857839"/>
<reference key="parent" ref="1072816887"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">205</int>
<reference key="object" ref="831384658"/>
<reference key="parent" ref="1072816887"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">207</int>
<reference key="object" ref="534262842"/>
<reference key="parent" ref="258451033"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">208</int>
<reference key="object" ref="671970801"/>
<reference key="parent" ref="258451033"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">209</int>
<reference key="object" ref="314583816"/>
<reference key="parent" ref="1072816887"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">216</int>
<reference key="object" ref="642967193"/>
<array class="NSMutableArray" key="children">
<reference ref="406294418"/>
</array>
<reference key="parent" ref="1072816887"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">217</int>
<reference key="object" ref="406294418"/>
<reference key="parent" ref="642967193"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">218</int>
<reference key="object" ref="645313537"/>
<reference key="parent" ref="1072816887"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">219</int>
<reference key="object" ref="310034208"/>
<reference key="parent" ref="1072816887"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">221</int>
<reference key="object" ref="884917592"/>
<reference key="parent" ref="1072816887"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">222</int>
<reference key="object" ref="566883659"/>
<reference key="parent" ref="1072816887"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">210</int>
<reference key="object" ref="1033518145"/>
<reference key="parent" ref="1072816887"/>
</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>
<array class="NSMutableArray" key="143.IBNSViewMetadataConstraints">
<reference ref="865006730"/>
<reference ref="831384658"/>
<reference ref="566883659"/>
<reference ref="1033518145"/>
<reference ref="884917592"/>
<reference ref="645313537"/>
<reference ref="310034208"/>
<reference ref="63384401"/>
<reference ref="602857839"/>
<reference ref="314583816"/>
</array>
<boolean value="NO" key="143.IBNSViewMetadataTranslatesAutoresizingMaskIntoConstraints"/>
<string key="143.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="145.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="147.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="148.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="150.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="NO" key="181.IBNSViewMetadataTranslatesAutoresizingMaskIntoConstraints"/>
<string key="181.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<real value="1" key="181.IBViewIntegration.shadowBlurRadius"/>
<reference key="181.IBViewIntegration.shadowColor" ref="444840817"/>
<real value="1" key="181.IBViewIntegration.shadowOffsetHeight"/>
<real value="1" key="181.IBViewIntegration.shadowOffsetWidth"/>
<array class="NSMutableArray" key="182.IBNSViewMetadataConstraints">
<reference ref="102475933"/>
</array>
<boolean value="NO" key="182.IBNSViewMetadataTranslatesAutoresizingMaskIntoConstraints"/>
<string key="182.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<real value="0.0" key="182.IBViewIntegration.shadowBlurRadius"/>
<reference key="182.IBViewIntegration.shadowColor" ref="714751679"/>
<real value="0.0" key="182.IBViewIntegration.shadowOffsetHeight"/>
<real value="0.0" key="182.IBViewIntegration.shadowOffsetWidth"/>
<boolean value="NO" key="183.IBNSViewMetadataTranslatesAutoresizingMaskIntoConstraints"/>
<string key="183.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="184.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="185.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="186.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="187.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="199.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="200.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="201.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="205.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="207.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="208.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="209.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="210.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="NO" key="216.IBNSViewMetadataTranslatesAutoresizingMaskIntoConstraints"/>
<string key="216.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="217.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="218.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="219.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES" key="22.IBNSWindowAutoPositionCentersHorizontal"/>
<boolean value="YES" key="22.IBNSWindowAutoPositionCentersVertical"/>
<string key="22.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="NO" key="22.NSWindowTemplate.visibleAtLaunch"/>
<string key="221.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="222.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<array key="23.IBNSViewMetadataConstraints">
<reference ref="216428540"/>
<reference ref="265452638"/>
<reference ref="88899061"/>
<reference ref="685084851"/>
<reference ref="534262842"/>
<reference ref="671970801"/>
</array>
<string key="23.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="NO" key="96.IBNSViewMetadataTranslatesAutoresizingMaskIntoConstraints"/>
<string key="96.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">230</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,38 @@
<?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>Icon</string>
<key>CFBundleIdentifier</key>
<string>com.lyndir.lhunath.${PRODUCT_NAME:rfc1034identifier}.Mac</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>[auto]</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>[auto]</string>
<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-2013 Lyndir. All rights reserved.</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:MasterPassword-Mac.xcodeproj">
</FileRef>
</Workspace>

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>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "NO"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DAB8D987150374AD00CED3BC"
BuildableName = "MasterPassword.app"
BlueprintName = "MasterPassword"
ReferencedContainer = "container:MasterPassword-Mac.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
buildConfiguration = "Debug">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DAB8D987150374AD00CED3BC"
BuildableName = "MasterPassword.app"
BlueprintName = "MasterPassword"
ReferencedContainer = "container:MasterPassword-Mac.xcodeproj">
</BuildableReference>
</MacroExpansion>
</TestAction>
<LaunchAction
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
buildConfiguration = "Debug"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
allowLocationSimulation = "YES">
<BuildableProductRunnable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DAB8D987150374AD00CED3BC"
BuildableName = "MasterPassword.app"
BlueprintName = "MasterPassword"
ReferencedContainer = "container:MasterPassword-Mac.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
buildConfiguration = "Release"
debugDocumentVersioning = "YES">
<BuildableProductRunnable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DAB8D987150374AD00CED3BC"
BuildableName = "MasterPassword.app"
BlueprintName = "MasterPassword"
ReferencedContainer = "container:MasterPassword-Mac.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,17 @@
//
// Prefix header for all source files of the 'MasterPassword' target in the 'MasterPassword' project
//
#import "Pearl-Prefix.pch"
#ifdef __OBJC__
#import <Cocoa/Cocoa.h>
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#import "MPTypes.h"
#import "MPMacConfig.h"
#endif

View File

@@ -0,0 +1,15 @@
<?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.developer.ubiquity-container-identifiers</key>
<array>
<string>$(TeamIdentifierPrefix)com.lyndir.lhunath.MasterPassword.Mac</string>
<string>$(TeamIdentifierPrefix)com.lyndir.lhunath.MasterPassword.shared</string>
</array>
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)com.lyndir.lhunath.MasterPassword.shared</string>
<key>com.apple.security.app-sandbox</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,29 @@
{\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;}
{\colortbl;\red255\green255\blue255;}
\paperw9840\paperh8400
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural
\f0\b\fs24 \cf0 Engineering:
\b0 \
Some people\
\
\b Human Interface Design:
\b0 \
Some other people\
\
\b Testing:
\b0 \
Hopefully not nobody\
\
\b Documentation:
\b0 \
Whoever\
\
\b With special thanks to:
\b0 \
Mom\
}

View File

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

View File

@@ -0,0 +1,558 @@
<?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">12C60</string>
<string key="IBDocument.InterfaceBuilderVersion">2844</string>
<string key="IBDocument.AppKitVersion">1187.34</string>
<string key="IBDocument.HIToolboxVersion">625.00</string>
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="NS.object.0">2844</string>
</object>
<array key="IBDocument.IntegratedClassDependencies">
<string>NSCustomObject</string>
<string>NSMenu</string>
<string>NSMenuItem</string>
<string>NSUserDefaultsController</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="1048">
<object class="NSCustomObject" id="1021">
<string key="NSClassName">NSApplication</string>
</object>
<object class="NSCustomObject" id="1014">
<string key="NSClassName">FirstResponder</string>
</object>
<object class="NSCustomObject" id="1050">
<string key="NSClassName">NSApplication</string>
</object>
<object class="NSMenu" id="649796088">
<string key="NSTitle">AMainMenu</string>
<array class="NSMutableArray" key="NSMenuItems"/>
<string key="NSName">_NSMainMenu</string>
</object>
<object class="NSCustomObject" id="976324537">
<string key="NSClassName">MPAppDelegate</string>
</object>
<object class="NSUserDefaultsController" id="705910970">
<bool key="NSSharedInstance">YES</bool>
</object>
<object class="NSMenu" id="764588027">
<string key="NSTitle"/>
<array class="NSMutableArray" key="NSMenuItems">
<object class="NSMenuItem" id="11982480">
<reference key="NSMenu" ref="764588027"/>
<string key="NSTitle">Users</string>
<string key="NSKeyEquiv"/>
<int key="NSMnemonicLoc">2147483647</int>
<object class="NSCustomResource" key="NSOnImage" id="269450960">
<string key="NSClassName">NSImage</string>
<string key="NSResourceName">NSMenuCheckmark</string>
</object>
<object class="NSCustomResource" key="NSMixedImage" id="977440657">
<string key="NSClassName">NSImage</string>
<string key="NSResourceName">NSMenuMixedState</string>
</object>
<string key="NSAction">submenuAction:</string>
<object class="NSMenu" key="NSSubmenu" id="934187555">
<string key="NSTitle">Users</string>
<array class="NSMutableArray" key="NSMenuItems">
<object class="NSMenuItem" id="576787569">
<reference key="NSMenu" ref="934187555"/>
<bool key="NSIsDisabled">YES</bool>
<string key="NSTitle">New User</string>
<string key="NSKeyEquiv"/>
<int key="NSMnemonicLoc">2147483647</int>
<reference key="NSOnImage" ref="269450960"/>
<reference key="NSMixedImage" ref="977440657"/>
</object>
<object class="NSMenuItem" id="925131766">
<reference key="NSMenu" ref="934187555"/>
<bool key="NSIsDisabled">YES</bool>
<bool key="NSIsSeparator">YES</bool>
<string key="NSTitle"/>
<string key="NSKeyEquiv"/>
<int key="NSMnemonicLoc">2147483647</int>
<reference key="NSOnImage" ref="269450960"/>
<reference key="NSMixedImage" ref="977440657"/>
</object>
</array>
</object>
</object>
<object class="NSMenuItem" id="851296005">
<reference key="NSMenu" ref="764588027"/>
<string key="NSTitle">Preferences</string>
<string key="NSKeyEquiv"/>
<int key="NSMnemonicLoc">2147483647</int>
<reference key="NSOnImage" ref="269450960"/>
<reference key="NSMixedImage" ref="977440657"/>
<string key="NSAction">submenuAction:</string>
<object class="NSMenu" key="NSSubmenu" id="800575174">
<string key="NSTitle">Preferences</string>
<array class="NSMutableArray" key="NSMenuItems">
<object class="NSMenuItem" id="14397049">
<reference key="NSMenu" ref="800575174"/>
<string key="NSTitle">Use iCloud</string>
<string key="NSKeyEquiv"/>
<int key="NSMnemonicLoc">2147483647</int>
<reference key="NSOnImage" ref="269450960"/>
<reference key="NSMixedImage" ref="977440657"/>
</object>
<object class="NSMenuItem" id="461686112">
<reference key="NSMenu" ref="800575174"/>
<bool key="NSIsDisabled">YES</bool>
<string key="NSTitle">Synchronize available sites from your iCloud account.</string>
<string key="NSKeyEquiv"/>
<int key="NSMnemonicLoc">2147483647</int>
<reference key="NSOnImage" ref="269450960"/>
<reference key="NSMixedImage" ref="977440657"/>
<object class="NSAttributedString" key="NSAttributedTitle">
<string key="NSString">Synchronize available sites from your iCloud account.</string>
<dictionary key="NSAttributes" id="583461090">
<object class="NSFont" key="NSFont">
<string key="NSName">Helvetica</string>
<double key="NSSize">12</double>
<int key="NSfFlags">16</int>
</object>
<object class="NSParagraphStyle" key="NSParagraphStyle">
<int key="NSAlignment">4</int>
<nil key="NSTabStops"/>
</object>
</dictionary>
</object>
</object>
<object class="NSMenuItem" id="290760748">
<reference key="NSMenu" ref="800575174"/>
<string key="NSTitle">Remember Password</string>
<string key="NSKeyEquiv"/>
<int key="NSMnemonicLoc">2147483647</int>
<reference key="NSOnImage" ref="269450960"/>
<reference key="NSMixedImage" ref="977440657"/>
</object>
<object class="NSMenuItem" id="907921953">
<reference key="NSMenu" ref="800575174"/>
<bool key="NSIsDisabled">YES</bool>
<string key="NSTitle">Remember the password while the application is running.</string>
<string key="NSKeyEquiv"/>
<int key="NSMnemonicLoc">2147483647</int>
<reference key="NSOnImage" ref="269450960"/>
<reference key="NSMixedImage" ref="977440657"/>
<object class="NSAttributedString" key="NSAttributedTitle">
<string key="NSString">Remember the password while the application is running.</string>
<reference key="NSAttributes" ref="583461090"/>
</object>
</object>
<object class="NSMenuItem" id="110488020">
<reference key="NSMenu" ref="800575174"/>
<string key="NSTitle">Save Password</string>
<string key="NSKeyEquiv"/>
<int key="NSMnemonicLoc">2147483647</int>
<reference key="NSOnImage" ref="269450960"/>
<reference key="NSMixedImage" ref="977440657"/>
</object>
<object class="NSMenuItem" id="123831322">
<reference key="NSMenu" ref="800575174"/>
<bool key="NSIsDisabled">YES</bool>
<string key="NSTitle">Save the password in your keychain so you don't need to enter it again.</string>
<string key="NSKeyEquiv"/>
<int key="NSMnemonicLoc">2147483647</int>
<reference key="NSOnImage" ref="269450960"/>
<reference key="NSMixedImage" ref="977440657"/>
<object class="NSAttributedString" key="NSAttributedTitle">
<string key="NSString">Save the password in your keychain so you don't need to enter it again.</string>
<reference key="NSAttributes" ref="583461090"/>
</object>
</object>
</array>
<bool key="NSNoAutoenable">YES</bool>
</object>
</object>
<object class="NSMenuItem" id="466252869">
<reference key="NSMenu" ref="764588027"/>
<bool key="NSIsDisabled">YES</bool>
<bool key="NSIsSeparator">YES</bool>
<string key="NSTitle"/>
<string key="NSKeyEquiv"/>
<int key="NSMnemonicLoc">2147483647</int>
<reference key="NSOnImage" ref="269450960"/>
<reference key="NSMixedImage" ref="977440657"/>
</object>
<object class="NSMenuItem" id="846612332">
<reference key="NSMenu" ref="764588027"/>
<string key="NSTitle">Show</string>
<string key="NSKeyEquiv">p</string>
<int key="NSKeyEquivModMask">1310720</int>
<int key="NSMnemonicLoc">2147483647</int>
<reference key="NSOnImage" ref="269450960"/>
<reference key="NSMixedImage" ref="977440657"/>
</object>
<object class="NSMenuItem" id="229948989">
<reference key="NSMenu" ref="764588027"/>
<bool key="NSIsDisabled">YES</bool>
<string key="NSTitle">Lock</string>
<string key="NSKeyEquiv">p</string>
<int key="NSKeyEquivModMask">1835008</int>
<int key="NSMnemonicLoc">2147483647</int>
<reference key="NSOnImage" ref="269450960"/>
<reference key="NSMixedImage" ref="977440657"/>
</object>
<object class="NSMenuItem" id="291035877">
<reference key="NSMenu" ref="764588027"/>
<string key="NSTitle">Quit</string>
<string key="NSKeyEquiv"/>
<int key="NSMnemonicLoc">2147483647</int>
<reference key="NSOnImage" ref="269450960"/>
<reference key="NSMixedImage" ref="977440657"/>
</object>
</array>
<bool key="NSNoAutoenable">YES</bool>
</object>
</array>
<object class="IBObjectContainer" key="IBDocument.Objects">
<array class="NSMutableArray" key="connectionRecords">
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">terminate:</string>
<reference key="source" ref="1050"/>
<reference key="destination" ref="291035877"/>
</object>
<int key="connectionID">734</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">delegate</string>
<reference key="source" ref="1021"/>
<reference key="destination" ref="976324537"/>
</object>
<int key="connectionID">495</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">lockItem</string>
<reference key="source" ref="976324537"/>
<reference key="destination" ref="229948989"/>
</object>
<int key="connectionID">726</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">showItem</string>
<reference key="source" ref="976324537"/>
<reference key="destination" ref="846612332"/>
</object>
<int key="connectionID">730</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">statusMenu</string>
<reference key="source" ref="976324537"/>
<reference key="destination" ref="764588027"/>
</object>
<int key="connectionID">731</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">activate:</string>
<reference key="source" ref="976324537"/>
<reference key="destination" ref="846612332"/>
</object>
<int key="connectionID">736</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">useICloudItem</string>
<reference key="source" ref="976324537"/>
<reference key="destination" ref="14397049"/>
</object>
<int key="connectionID">749</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">rememberPasswordItem</string>
<reference key="source" ref="976324537"/>
<reference key="destination" ref="290760748"/>
</object>
<int key="connectionID">750</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">savePasswordItem</string>
<reference key="source" ref="976324537"/>
<reference key="destination" ref="110488020"/>
</object>
<int key="connectionID">751</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">togglePreference:</string>
<reference key="source" ref="976324537"/>
<reference key="destination" ref="14397049"/>
</object>
<int key="connectionID">752</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">togglePreference:</string>
<reference key="source" ref="976324537"/>
<reference key="destination" ref="290760748"/>
</object>
<int key="connectionID">753</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">togglePreference:</string>
<reference key="source" ref="976324537"/>
<reference key="destination" ref="110488020"/>
</object>
<int key="connectionID">754</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">newUser:</string>
<reference key="source" ref="976324537"/>
<reference key="destination" ref="576787569"/>
</object>
<int key="connectionID">761</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">usersItem</string>
<reference key="source" ref="976324537"/>
<reference key="destination" ref="11982480"/>
</object>
<int key="connectionID">762</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">createUserItem</string>
<reference key="source" ref="976324537"/>
<reference key="destination" ref="576787569"/>
</object>
<int key="connectionID">763</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">lock:</string>
<reference key="source" ref="976324537"/>
<reference key="destination" ref="229948989"/>
</object>
<int key="connectionID">764</int>
</object>
</array>
<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="1048"/>
<nil key="parent"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">-2</int>
<reference key="object" ref="1021"/>
<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="1014"/>
<reference key="parent" ref="0"/>
<string key="objectName">First Responder</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">-3</int>
<reference key="object" ref="1050"/>
<reference key="parent" ref="0"/>
<string key="objectName">Application</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">29</int>
<reference key="object" ref="649796088"/>
<array class="NSMutableArray" key="children"/>
<reference key="parent" ref="0"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">494</int>
<reference key="object" ref="976324537"/>
<reference key="parent" ref="0"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">548</int>
<reference key="object" ref="705910970"/>
<reference key="parent" ref="0"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">716</int>
<reference key="object" ref="764588027"/>
<array class="NSMutableArray" key="children">
<reference ref="291035877"/>
<reference ref="466252869"/>
<reference ref="229948989"/>
<reference ref="851296005"/>
<reference ref="846612332"/>
<reference ref="11982480"/>
</array>
<reference key="parent" ref="0"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">717</int>
<reference key="object" ref="291035877"/>
<reference key="parent" ref="764588027"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">718</int>
<reference key="object" ref="466252869"/>
<reference key="parent" ref="764588027"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">719</int>
<reference key="object" ref="846612332"/>
<reference key="parent" ref="764588027"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">720</int>
<reference key="object" ref="229948989"/>
<reference key="parent" ref="764588027"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">739</int>
<reference key="object" ref="851296005"/>
<array class="NSMutableArray" key="children">
<reference ref="800575174"/>
</array>
<reference key="parent" ref="764588027"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">742</int>
<reference key="object" ref="800575174"/>
<array class="NSMutableArray" key="children">
<reference ref="14397049"/>
<reference ref="290760748"/>
<reference ref="907921953"/>
<reference ref="461686112"/>
<reference ref="110488020"/>
<reference ref="123831322"/>
</array>
<reference key="parent" ref="851296005"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">743</int>
<reference key="object" ref="14397049"/>
<reference key="parent" ref="800575174"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">745</int>
<reference key="object" ref="907921953"/>
<reference key="parent" ref="800575174"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">744</int>
<reference key="object" ref="290760748"/>
<reference key="parent" ref="800575174"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">746</int>
<reference key="object" ref="461686112"/>
<reference key="parent" ref="800575174"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">747</int>
<reference key="object" ref="110488020"/>
<reference key="parent" ref="800575174"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">748</int>
<reference key="object" ref="123831322"/>
<reference key="parent" ref="800575174"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">755</int>
<reference key="object" ref="11982480"/>
<array class="NSMutableArray" key="children">
<reference ref="934187555"/>
</array>
<reference key="parent" ref="764588027"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">756</int>
<reference key="object" ref="934187555"/>
<array class="NSMutableArray" key="children">
<reference ref="576787569"/>
<reference ref="925131766"/>
</array>
<reference key="parent" ref="11982480"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">757</int>
<reference key="object" ref="576787569"/>
<reference key="parent" ref="934187555"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">759</int>
<reference key="object" ref="925131766"/>
<reference key="parent" ref="934187555"/>
</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="29.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="494.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="548.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="716.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="717.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="718.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="719.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="720.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="739.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="742.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="743.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="744.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="745.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="746.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="747.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="748.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="755.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="756.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<object class="NSMutableDictionary" key="757.IBAttributePlaceholdersKey">
<string key="NS.key.0">ToolTip</string>
<object class="IBToolTipAttribute" key="NS.object.0">
<string key="name">ToolTip</string>
<reference key="object" ref="576787569"/>
<string key="toolTip">Creating users is not yet supported. Please use the iOS app with iCloud enabled to create users and sites.</string>
</object>
</object>
<string key="757.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="759.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">764</int>
</object>
<object class="IBClassDescriber" key="IBDocument.Classes"/>
<int key="IBDocument.localizationMode">0</int>
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string>
<integer value="1070" key="NS.object.0"/>
</object>
<bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
<int key="IBDocument.defaultPropertyAccessControl">3</int>
<dictionary class="NSMutableDictionary" key="IBDocument.LastKnownImageSizes">
<string key="NSMenuCheckmark">{11, 11}</string>
<string key="NSMenuMixedState">{10, 3}</string>
</dictionary>
<bool key="IBDocument.UseAutolayout">YES</bool>
</data>
</archive>

View File

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

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 4.xcdatamodel</string>
</dict>
</plist>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model name="" userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1171" systemVersion="11E53" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
<entity name="MPElementEntity" representedClassName="MPElementEntity" isAbstract="YES" 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" syncable="YES"/>
</entity>
<entity name="MPElementGeneratedEntity" representedClassName="MPElementGeneratedEntity" parentEntity="MPElementEntity" syncable="YES">
<attribute name="counter_" optional="YES" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
</entity>
<entity name="MPElementStoredEntity" representedClassName="MPElementStoredEntity" parentEntity="MPElementEntity" 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"/>
<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 name="" userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="2057" systemVersion="12C60" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
<entity name="MPElementEntity" representedClassName="MPElementEntity" isAbstract="YES" 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" syncable="YES"/>
</entity>
<entity name="MPElementGeneratedEntity" representedClassName="MPElementGeneratedEntity" parentEntity="MPElementEntity" syncable="YES">
<attribute name="counter_" optional="YES" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
</entity>
<entity name="MPElementStoredEntity" representedClassName="MPElementStoredEntity" parentEntity="MPElementEntity" 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" 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 name="" userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1810" systemVersion="12B19" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
<entity name="MPElementEntity" representedClassName="MPElementEntity" isAbstract="YES" 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" syncable="YES"/>
</entity>
<entity name="MPElementGeneratedEntity" representedClassName="MPElementGeneratedEntity" parentEntity="MPElementEntity" syncable="YES">
<attribute name="counter_" optional="YES" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
</entity>
<entity name="MPElementStoredEntity" representedClassName="MPElementStoredEntity" parentEntity="MPElementEntity" 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" 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 name="" userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="2057" systemVersion="12C60" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
<entity name="MPElementEntity" representedClassName="MPElementEntity" isAbstract="YES" 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" syncable="YES"/>
</entity>
<entity name="MPElementGeneratedEntity" representedClassName="MPElementGeneratedEntity" parentEntity="MPElementEntity" syncable="YES">
<attribute name="counter_" optional="YES" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
</entity>
<entity name="MPElementStoredEntity" representedClassName="MPElementStoredEntity" parentEntity="MPElementEntity" 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" 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,22 @@
//
// Prefix header for all source files of the 'Pearl' target in the 'Pearl' project
//
#ifdef __OBJC__
#import <Foundation/Foundation.h>
#endif
#define PEARL_WITH_SCRYPT
#define PEARL_WITH_MESSAGEUI
#define PEARL
#define PEARL_CRYPTO
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
#define PEARL_UIKIT
#endif
#import "Pearl.h"
#import "Pearl-Crypto.h"
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
#import "Pearl-UIKit.h"
#endif

View File

@@ -0,0 +1,25 @@
//
// MPAppDelegate.h
// MasterPassword
//
// Created by Maarten Billemont on 24/11/11.
// Copyright (c) 2011 Lyndir. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <MessageUI/MessageUI.h>
#import "MPAppDelegate_Shared.h"
#import "GPPShare.h"
@interface MPAppDelegate : MPAppDelegate_Shared
@property (nonatomic, readonly) GPPShare *googlePlus;
- (void)showGuide;
- (void)showFeedbackWithLogs:(BOOL)logs forVC:(UIViewController *)viewController;
- (void)export;
- (void)changeMasterPasswordFor:(MPUserEntity *)user didResetBlock:(void(^)(void))didReset;
@end

View File

@@ -0,0 +1,739 @@
//
// MPAppDelegate.m
// MasterPassword
//
// Created by Maarten Billemont on 24/11/11.
// Copyright (c) 2011 Lyndir. All rights reserved.
//
#import "MPAppDelegate.h"
#import "MPAppDelegate_Key.h"
#import "MPAppDelegate_Store.h"
#import "IASKSettingsReader.h"
@interface MPAppDelegate ()
@property (nonatomic, readwrite) GPPShare *googlePlus;
@end
@implementation MPAppDelegate
+ (void)initialize {
[MPiOSConfig get];
#ifdef DEBUG
[PearlLogger get].printLevel = PearlLogLevelDebug;
//[NSClassFromString(@"WebView") performSelector:NSSelectorFromString(@"_enableRemoteInspector")];
#endif
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[[[NSBundle mainBundle] mutableInfoDictionary] setObject:@"Master Password" forKey:@"CFBundleDisplayName"];
[[[NSBundle mainBundle] mutableLocalizedInfoDictionary] setObject:@"Master Password" forKey:@"CFBundleDisplayName"];
#ifdef TESTFLIGHT_SDK_VERSION
@try {
NSString *testFlightToken = [self testFlightToken];
if ([testFlightToken length]) {
inf(@"Initializing TestFlight");
#ifdef ADHOC
[TestFlight setDeviceIdentifier:[(id)[UIDevice currentDevice] uniqueIdentifier]];
#else
[TestFlight setDeviceIdentifier:[PearlKeyChain deviceIdentifier]];
#endif
[TestFlight addCustomEnvironmentInformation:@"Anonymous" forKey:@"username"];
[TestFlight addCustomEnvironmentInformation:[PearlKeyChain deviceIdentifier] forKey:@"deviceIdentifier"];
[TestFlight setOptions:[NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:NO], @"logToConsole",
[NSNumber numberWithBool:NO], @"logToSTDERR",
nil]];
[TestFlight takeOff:testFlightToken];
[[PearlLogger get] registerListener:^BOOL(PearlLogMessage *message) {
PearlLogLevel level = PearlLogLevelWarn;
if ([[MPiOSConfig get].sendInfo boolValue])
level = PearlLogLevelInfo;
if (message.level >= level)
TFLog(@"%@", [message messageDescription]);
return YES;
}];
TFLog(@"TestFlight (%@) initialized for: %@ v%@.", //
TESTFLIGHT_SDK_VERSION, [PearlInfoPlist get].CFBundleName, [PearlInfoPlist get].CFBundleVersion);
}
}
@catch (id exception) {
err(@"TestFlight: %@", exception);
}
#endif
@try {
NSString *googlePlusClientID = [self googlePlusClientID];
if ([googlePlusClientID length]) {
inf(@"Initializing Google+");
self.googlePlus = [[GPPShare alloc] initWithClientID:googlePlusClientID];
}
}
@catch (id exception) {
err(@"Google+: %@", exception);
}
@try {
NSString *crashlyticsAPIKey = [self crashlyticsAPIKey];
if ([crashlyticsAPIKey length]) {
inf(@"Initializing Crashlytics");
#if defined (DEBUG) || defined (ADHOC)
[Crashlytics sharedInstance].debugMode = YES;
#endif
[Crashlytics setUserIdentifier:[PearlKeyChain deviceIdentifier]];
[Crashlytics setObjectValue:[PearlKeyChain deviceIdentifier] forKey:@"deviceIdentifier"];
[Crashlytics setUserName:@"Anonymous"];
[Crashlytics setObjectValue:@"Anonymous" forKey:@"username"];
[Crashlytics startWithAPIKey:crashlyticsAPIKey];
[[PearlLogger get] registerListener:^BOOL(PearlLogMessage *message) {
PearlLogLevel level = PearlLogLevelWarn;
if ([[MPiOSConfig get].sendInfo boolValue])
level = PearlLogLevelInfo;
if (message.level >= level)
CLSLog(@"%@", [message messageDescription]);
return YES;
}];
CLSLog(@"Crashlytics (%@) initialized for: %@ v%@.", //
[Crashlytics sharedInstance].version, [PearlInfoPlist get].CFBundleName, [PearlInfoPlist get].CFBundleVersion);
}
}
@catch (id exception) {
err(@"Crashlytics: %@", exception);
}
@try {
NSString *localyticsKey = [self localyticsKey];
if ([localyticsKey length]) {
inf(@"Initializing Localytics");
[[LocalyticsSession sharedLocalyticsSession] LocalyticsSession:localyticsKey];
[[LocalyticsSession sharedLocalyticsSession] open];
[LocalyticsSession sharedLocalyticsSession].enableHTTPS = YES;
[[LocalyticsSession sharedLocalyticsSession] upload];
[[PearlLogger get] registerListener:^BOOL(PearlLogMessage *message) {
if (message.level >= PearlLogLevelWarn)
[[LocalyticsSession sharedLocalyticsSession] tagEvent:@"Problem"
attributes:@{
@"level": @(PearlLogLevelStr(message.level)),
@"message": message.message
}];
return YES;
}];
}
}
@catch (id exception) {
err(@"Localytics exception: %@", exception);
}
UIImage *navBarImage = [[UIImage imageNamed:@"ui_navbar_container"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 5)];
[[UINavigationBar appearance] setBackgroundImage:navBarImage forBarMetrics:UIBarMetricsDefault];
[[UINavigationBar appearance] setBackgroundImage:navBarImage forBarMetrics:UIBarMetricsLandscapePhone];
[[UINavigationBar appearance] setTitleTextAttributes:
@{
UITextAttributeTextColor: [UIColor colorWithRed:1.0f green:1.0f blue:1.0f alpha:1.0f],
UITextAttributeTextShadowColor: [UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.8f],
UITextAttributeTextShadowOffset: [NSValue valueWithUIOffset:UIOffsetMake(0, -1)],
UITextAttributeFont: [UIFont fontWithName:@"Exo-Bold" size:20.0f]
}];
UIImage *navBarButton = [[UIImage imageNamed:@"ui_navbar_button"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 5)];
UIImage *navBarBack = [[UIImage imageNamed:@"ui_navbar_back"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 13, 0, 5)];
[[UIBarButtonItem appearance] setBackgroundImage:navBarButton forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
[[UIBarButtonItem appearance] setBackgroundImage:nil forState:UIControlStateNormal barMetrics:UIBarMetricsLandscapePhone];
[[UIBarButtonItem appearance] setBackButtonBackgroundImage:navBarBack forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
[[UIBarButtonItem appearance] setBackButtonBackgroundImage:nil forState:UIControlStateNormal barMetrics:UIBarMetricsLandscapePhone];
[[UIBarButtonItem appearance] setTitleTextAttributes:
@{
UITextAttributeTextColor: [UIColor colorWithRed:1.0f green:1.0f blue:1.0f alpha:1.0f],
UITextAttributeTextShadowColor: [UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.5f],
UITextAttributeTextShadowOffset: [NSValue valueWithUIOffset:UIOffsetMake(0, 1)]//,
// Causes a bug in iOS where image views get oddly stretched... or something.
//UITextAttributeFont: [UIFont fontWithName:@"HelveticaNeue" size:13.0f]
}
forState:UIControlStateNormal];
UIImage *toolBarImage = [[UIImage imageNamed:@"ui_toolbar_container"] resizableImageWithCapInsets:UIEdgeInsetsMake(25, 5, 5, 5)];
[[UISearchBar appearance] setBackgroundImage:toolBarImage];
[[UIToolbar appearance] setBackgroundImage:toolBarImage forToolbarPosition:UIToolbarPositionAny barMetrics:UIBarMetricsDefault];
/*
UIImage *minImage = [[UIImage imageNamed:@"slider-minimum.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 0)];
UIImage *maxImage = [[UIImage imageNamed:@"slider-maximum.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 0)];
UIImage *thumbImage = [UIImage imageNamed:@"slider-handle.png"];
[[UISlider appearance] setMaximumTrackImage:maxImage forState:UIControlStateNormal];
[[UISlider appearance] setMinimumTrackImage:minImage forState:UIControlStateNormal];
[[UISlider appearance] setThumbImage:thumbImage forState:UIControlStateNormal];
UIImage *segmentSelected = [[UIImage imageNamed:@"segcontrol_sel.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 4, 0, 4)];
UIImage *segmentUnselected = [[UIImage imageNamed:@"segcontrol_uns.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 15, 0, 15)];
UIImage *segmentSelectedUnselected = [UIImage imageNamed:@"segcontrol_sel-uns.png"];
UIImage *segUnselectedSelected = [UIImage imageNamed:@"segcontrol_uns-sel.png"];
UIImage *segmentUnselectedUnselected = [UIImage imageNamed:@"segcontrol_uns-uns.png"];
[[UISegmentedControl appearance] setBackgroundImage:segmentUnselected forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
[[UISegmentedControl appearance] setBackgroundImage:segmentSelected forState:UIControlStateSelected barMetrics:UIBarMetricsDefault];
[[UISegmentedControl appearance] setDividerImage:segmentUnselectedUnselected forLeftSegmentState:UIControlStateNormal rightSegmentState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
[[UISegmentedControl appearance] setDividerImage:segmentSelectedUnselected forLeftSegmentState:UIControlStateSelected rightSegmentState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
[[UISegmentedControl appearance] setDividerImage:segUnselectedSelected forLeftSegmentState:UIControlStateNormal rightSegmentState:UIControlStateSelected barMetrics:UIBarMetricsDefault];
*/
[[NSNotificationCenter defaultCenter] addObserverForName:MPSignedOutNotification object:nil queue:nil
usingBlock:^(NSNotification *note) {
if ([[note.userInfo objectForKey:@"animated"] boolValue])
[self.navigationController performSegueWithIdentifier:@"MP_Unlock" sender:nil];
else
[self.navigationController presentViewController:[self.navigationController.storyboard instantiateViewControllerWithIdentifier:@"MPUnlockViewController"]
animated:NO completion:nil];
}];
[[NSNotificationCenter defaultCenter] addObserverForName:MPCheckConfigNotification object:nil queue:nil usingBlock:
^(NSNotification *note) {
if ([[MPiOSConfig get].sendInfo boolValue]) {
if ([PearlLogger get].printLevel > PearlLogLevelInfo)
[PearlLogger get].printLevel = PearlLogLevelInfo;
[[Crashlytics sharedInstance] setBoolValue:[[MPConfig get].rememberLogin boolValue] forKey:@"rememberLogin"];
[[Crashlytics sharedInstance] setBoolValue:[[MPConfig get].iCloud boolValue] forKey:@"iCloud"];
[[Crashlytics sharedInstance] setBoolValue:[[MPConfig get].iCloudDecided boolValue] forKey:@"iCloudDecided"];
[[Crashlytics sharedInstance] setBoolValue:[[MPiOSConfig get].sendInfo boolValue] forKey:@"sendInfo"];
[[Crashlytics sharedInstance] setBoolValue:[[MPiOSConfig get].helpHidden boolValue] forKey:@"helpHidden"];
[[Crashlytics sharedInstance] setBoolValue:[[MPiOSConfig get].showQuickStart boolValue] forKey:@"showQuickStart"];
[[Crashlytics sharedInstance] setBoolValue:[[PearlConfig get].firstRun boolValue] forKey:@"firstRun"];
[[Crashlytics sharedInstance] setIntValue:[[PearlConfig get].launchCount intValue] forKey:@"launchCount"];
[[Crashlytics sharedInstance] setBoolValue:[[PearlConfig get].askForReviews boolValue] forKey:@"askForReviews"];
[[Crashlytics sharedInstance] setIntValue:[[PearlConfig get].reviewAfterLaunches intValue] forKey:@"reviewAfterLaunches"];
[[Crashlytics sharedInstance] setObjectValue:[PearlConfig get].reviewedVersion forKey:@"reviewedVersion"];
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight addCustomEnvironmentInformation:[[MPConfig get].rememberLogin boolValue]? @"YES": @"NO" forKey:@"rememberLogin"];
[TestFlight addCustomEnvironmentInformation:[[MPConfig get].iCloud boolValue]? @"YES": @"NO" forKey:@"iCloud"];
[TestFlight addCustomEnvironmentInformation:[[MPConfig get].iCloudDecided boolValue]? @"YES": @"NO" forKey:@"iCloudDecided"];
[TestFlight addCustomEnvironmentInformation:[[MPiOSConfig get].sendInfo boolValue]? @"YES": @"NO" forKey:@"sendInfo"];
[TestFlight addCustomEnvironmentInformation:[[MPiOSConfig get].helpHidden boolValue]? @"YES": @"NO" forKey:@"helpHidden"];
[TestFlight addCustomEnvironmentInformation:[[MPiOSConfig get].showQuickStart boolValue]? @"YES": @"NO"
forKey:@"showQuickStart"];
[TestFlight addCustomEnvironmentInformation:[[PearlConfig get].firstRun boolValue]? @"YES": @"NO" forKey:@"firstRun"];
[TestFlight addCustomEnvironmentInformation:[[PearlConfig get].launchCount description] forKey:@"launchCount"];
[TestFlight addCustomEnvironmentInformation:[[PearlConfig get].askForReviews boolValue]? @"YES": @"NO"
forKey:@"askForReviews"];
[TestFlight addCustomEnvironmentInformation:[[PearlConfig get].reviewAfterLaunches description] forKey:@"reviewAfterLaunches"];
[TestFlight addCustomEnvironmentInformation:[PearlConfig get].reviewedVersion forKey:@"reviewedVersion"];
[TestFlight passCheckpoint:MPCheckpointConfig];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointConfig attributes:@{
@"rememberLogin" : [[MPConfig get].rememberLogin boolValue]? @"YES": @"NO",
@"iCloud" : [[MPConfig get].iCloud boolValue]? @"YES": @"NO",
@"iCloudDecided" : [[MPConfig get].iCloudDecided boolValue]? @"YES": @"NO",
@"sendInfo" : [[MPiOSConfig get].sendInfo boolValue]? @"YES": @"NO",
@"helpHidden" : [[MPiOSConfig get].helpHidden boolValue]? @"YES": @"NO",
@"showQuickStart" : [[MPiOSConfig get].showQuickStart boolValue]? @"YES": @"NO",
@"firstRun" : [[PearlConfig get].firstRun boolValue]? @"YES": @"NO",
@"launchCount" : NilToNSNull([[PearlConfig get].launchCount description]),
@"askForReviews" : [[PearlConfig get].askForReviews boolValue]? @"YES": @"NO",
@"reviewAfterLaunches" : NilToNSNull([[PearlConfig get].reviewAfterLaunches description]),
@"reviewedVersion" : NilToNSNull([PearlConfig get].reviewedVersion)
}];
}
}];
[[NSNotificationCenter defaultCenter] addObserverForName:kIASKAppSettingChanged object:nil queue:nil
usingBlock:^(NSNotification *note) {
[[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification
object:note userInfo:nil];
}];
#ifdef ADHOC
[PearlAlert showAlertWithTitle:@"Welcome, tester!" message:
@"Thank you for taking the time to test Master Password.\n\n"
@"Please provide any feedback, however minor it may seem, via the Feedback action item accessible from the top right.\n\n"
@"Contact me directly at:\n"
@"lhunath@lyndir.com\n"
@"Or report detailed issues at:\n"
@"https://youtrack.lyndir.com\n"
viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:nil
cancelTitle:nil otherTitles:[PearlStrings get].commonButtonOkay, nil];
#endif
[super application:application didFinishLaunchingWithOptions:launchOptions];
inf(@"Started up with device identifier: %@", [PearlKeyChain deviceIdentifier]);
return YES;
}
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
// No URL?
if (!url)
return NO;
// Google+
if ([self.googlePlus handleURL:url sourceApplication:sourceApplication annotation:annotation])
return YES;
// Arbitrary URL to mpsites data.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error;
NSURLResponse *response;
NSData *importedSitesData = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:url]
returningResponse:&response error:&error];
if (error)
err(@"While reading imported sites from %@: %@", url, error);
if (!importedSitesData)
return;
PearlAlert *activityAlert = [PearlAlert showActivityWithTitle:@"Importing"];
NSString *importedSitesString = [[NSString alloc] initWithData:importedSitesData encoding:NSUTF8StringEncoding];
MPImportResult result = [self importSites:importedSitesString askImportPassword:^NSString *(NSString *userName) {
__block NSString *masterPassword = nil;
dispatch_group_t importPasswordGroup = dispatch_group_create();
dispatch_group_enter(importPasswordGroup);
dispatch_async(dispatch_get_main_queue(), ^{
[PearlAlert showAlertWithTitle:@"Import File's Master Password"
message:PearlString(@"%@'s export was done using a different master password.\n"
@"Enter that master password to unlock the exported data.", userName)
viewStyle:UIAlertViewStyleSecureTextInput
initAlert:nil tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
@try {
if (buttonIndex_ == [alert_ cancelButtonIndex])
return;
masterPassword = [alert_ textFieldAtIndex:0].text;
}
@finally {
dispatch_group_leave(importPasswordGroup);
}
}
cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Unlock Import", nil];
});
dispatch_group_wait(importPasswordGroup, DISPATCH_TIME_FOREVER);
return masterPassword;
} askUserPassword:^NSString *(NSString *userName, NSUInteger importCount, NSUInteger deleteCount) {
__block NSString *masterPassword = nil;
dispatch_group_t userPasswordGroup = dispatch_group_create();
dispatch_group_enter(userPasswordGroup);
dispatch_async(dispatch_get_main_queue(), ^{
[PearlAlert showAlertWithTitle:PearlString(@"Master Password for\n%@", userName)
message:PearlString(@"Imports %d sites, overwriting %d.", importCount,
deleteCount)
viewStyle:UIAlertViewStyleSecureTextInput
initAlert:nil tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
@try {
if (buttonIndex_ == [alert_ cancelButtonIndex])
return;
masterPassword = [alert_ textFieldAtIndex:0].text;
}
@finally {
dispatch_group_leave(userPasswordGroup);
}
}
cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Import", nil];
});
dispatch_group_wait(userPasswordGroup, DISPATCH_TIME_FOREVER);
return masterPassword;
}];
switch (result) {
case MPImportResultSuccess:
case MPImportResultCancelled:
break;
case MPImportResultInternalError:
[PearlAlert showError:@"Import failed because of an internal error."];
break;
case MPImportResultMalformedInput:
[PearlAlert showError:@"The import doesn't look like a Master Password export."];
break;
case MPImportResultInvalidPassword:
[PearlAlert showError:@"Incorrect master password for the import sites."];
break;
}
[activityAlert dismissAlert];
});
return YES;
}
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
inf(@"Received memory warning.");
[super applicationDidReceiveMemoryWarning:application];
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
[[LocalyticsSession sharedLocalyticsSession] close];
[[LocalyticsSession sharedLocalyticsSession] upload];
[super applicationDidEnterBackground:application];
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
[[LocalyticsSession sharedLocalyticsSession] resume];
[[LocalyticsSession sharedLocalyticsSession] upload];
[super applicationWillEnterForeground:application];
}
- (void)applicationWillTerminate:(UIApplication *)application {
[[LocalyticsSession sharedLocalyticsSession] close];
[[LocalyticsSession sharedLocalyticsSession] upload];
[super applicationWillTerminate:application];
}
- (void)applicationWillResignActive:(UIApplication *)application {
inf(@"Will deactivate");
if (![[MPiOSConfig get].rememberLogin boolValue])
[self signOutAnimated:NO];
[[LocalyticsSession sharedLocalyticsSession] close];
[[LocalyticsSession sharedLocalyticsSession] upload];
[super applicationWillResignActive:application];
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
inf(@"Re-activated");
[[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification
object:application userInfo:nil];
[[LocalyticsSession sharedLocalyticsSession] resume];
[[LocalyticsSession sharedLocalyticsSession] upload];
[super applicationDidBecomeActive:application];
}
#pragma mark - Behavior
- (void)showGuide {
[self.navigationController performSegueWithIdentifier:@"MP_Guide" sender:self];
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointShowGuide];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointShowGuide attributes:nil];
}
- (void)showFeedback {
[self showFeedbackWithLogs:NO forVC:nil];
}
- (void)showReview {
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointReview];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointReview attributes:nil];
[super showReview];
}
- (void)showFeedbackWithLogs:(BOOL)logs forVC:(UIViewController *)viewController {
if (![PearlEMail canSendMail])
[PearlAlert showAlertWithTitle:@"Feedback"
message:
@"Have a question, comment, issue or just saying thanks?\n\n"
@"We'd love to hear what you think!\n"
@"masterpassword@lyndir.com"
viewStyle:UIAlertViewStyleDefault
initAlert:nil tappedButtonBlock:nil cancelTitle:[PearlStrings get].commonButtonOkay
otherTitles:nil];
else
if (logs)
[PearlAlert showAlertWithTitle:@"Feedback"
message:
@"Have a question, comment, issue or just saying thanks?\n\n"
@"If you're having trouble, it may help us if you can first reproduce the problem "
@"and then include log files in your message."
viewStyle:UIAlertViewStyleDefault
initAlert:nil tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
[self openFeedbackWithLogs:(buttonIndex_ == [alert_ firstOtherButtonIndex]) forVC:viewController];
} cancelTitle:nil otherTitles:@"Include Logs", @"No Logs", nil];
else
[self openFeedbackWithLogs:NO forVC:viewController];
}
- (void)openFeedbackWithLogs:(BOOL)logs forVC:(UIViewController *)viewController {
NSString *userName = [MPAppDelegate get].activeUser.name;
PearlLogLevel logLevel = [[MPiOSConfig get].sendInfo boolValue]? PearlLogLevelDebug: PearlLogLevelInfo;
[[[PearlEMail alloc] initForEMailTo:@"Master Password Development <masterpassword@lyndir.com>"
subject:PearlString(@"Feedback for Master Password [%@]",
[[PearlKeyChain deviceIdentifier] stringByDeletingMatchesOf:@"-.*"])
body:PearlString(@"\n\n\n"
@"--\n"
@"%@"
@"Master Password %@, build %@",
userName? ([userName stringByAppendingString:@"\n"]): @"",
[PearlInfoPlist get].CFBundleShortVersionString,
[PearlInfoPlist get].CFBundleVersion)
attachments:(logs
? [[PearlEMailAttachment alloc] initWithContent:[[[PearlLogger get] formatMessagesWithLevel:logLevel] dataUsingEncoding:NSUTF8StringEncoding]
mimeType:@"text/plain"
fileName:PearlString(@"%@-%@.log",
[[NSDateFormatter rfc3339DateFormatter] stringFromDate:[NSDate date]],
[PearlKeyChain deviceIdentifier])]
: nil), nil]
showComposerForVC:viewController];
}
- (void)export {
[PearlAlert showNotice:
@"This will export all your site names.\n\n"
@"You can open the export with a text editor to get an overview of all your sites.\n\n"
@"The file also acts as a personal backup of your site list in case you don't sync with iCloud/iTunes."
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
[PearlAlert showAlertWithTitle:@"Reveal Passwords?" message:
@"Would you like to make all your passwords visible in the export?\n\n"
@"A safe export will only include your stored passwords, in an encrypted manner, "
@"making the result safe from falling in the wrong hands.\n\n"
@"If all your passwords are shown and somebody else finds the export, "
@"they could gain access to all your sites!"
viewStyle:UIAlertViewStyleDefault initAlert:nil
tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 0)
// Safe Export
[self exportShowPasswords:NO];
if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 1)
// Show Passwords
[self exportShowPasswords:YES];
} cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Safe Export", @"Show Passwords", nil];
} otherTitles:nil];
}
- (void)exportShowPasswords:(BOOL)showPasswords {
if (![PearlEMail canSendMail]) {
[PearlAlert showAlertWithTitle:@"Cannot Send Mail"
message:
@"Your device is not yet set up for sending mail.\n"
@"Close Master Password, go into Settings and add a Mail account."
viewStyle:UIAlertViewStyleDefault
initAlert:nil tappedButtonBlock:nil cancelTitle:[PearlStrings get].commonButtonOkay
otherTitles:nil];
return;
}
NSString *exportedSites = [self exportSitesShowingPasswords:showPasswords];
NSString *message;
if (showPasswords)
message = PearlString(@"Export of Master Password sites with passwords included.\n"
@"REMINDER: Make sure nobody else sees this file! Passwords are visible!\n\n\n"
@"--\n"
@"%@\n"
@"Master Password %@, build %@",
self.activeUser.name,
[PearlInfoPlist get].CFBundleShortVersionString,
[PearlInfoPlist get].CFBundleVersion);
else
message = PearlString(@"Backup of Master Password sites.\n\n\n"
@"--\n"
@"%@\n"
@"Master Password %@, build %@",
self.activeUser.name,
[PearlInfoPlist get].CFBundleShortVersionString,
[PearlInfoPlist get].CFBundleVersion);
NSDateFormatter *exportDateFormatter = [NSDateFormatter new];
[exportDateFormatter setDateFormat:@"yyyy'-'MM'-'dd"];
[PearlEMail sendEMailTo:nil subject:@"Master Password Export" body:message
attachments:[[PearlEMailAttachment alloc] initWithContent:[exportedSites dataUsingEncoding:NSUTF8StringEncoding]
mimeType:@"text/plain" fileName:
PearlString(@"%@ (%@).mpsites", self.activeUser.name, [exportDateFormatter stringFromDate:[NSDate date]])],
nil];
}
- (void)changeMasterPasswordFor:(MPUserEntity *)user didResetBlock:(void (^)(void))didReset {
[PearlAlert showAlertWithTitle:@"Changing Master Password"
message:
@"If you continue, you'll be able to set a new master password.\n\n"
@"Changing your master password will cause all your generated passwords to change!\n"
@"Changing the master password back to the old one will cause your passwords to revert as well."
viewStyle:UIAlertViewStyleDefault
initAlert:nil tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
if (buttonIndex == [alert cancelButtonIndex])
return;
[user.managedObjectContext performBlock:^{
inf(@"Unsetting master password for: %@.", user.userID);
user.keyID = nil;
[self forgetSavedKeyFor:user];
[self signOutAnimated:YES];
if (didReset)
didReset();
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointChangeMP];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointChangeMP attributes:nil];
}];
}
cancelTitle:[PearlStrings get].commonButtonAbort
otherTitles:[PearlStrings get].commonButtonContinue, nil];
}
#pragma mark - PearlConfigDelegate
- (void)didUpdateConfigForKey:(SEL)configKey fromValue:(id)value {
[[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification
object:NSStringFromSelector(configKey) userInfo:nil];
}
#pragma mark - UbiquityStoreManagerDelegate
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didSwitchToCloud:(BOOL)cloudEnabled {
[super ubiquityStoreManager:manager didSwitchToCloud:cloudEnabled];
if (![[MPConfig get].iCloudDecided boolValue]) {
if (!cloudEnabled) {
[PearlAlert showAlertWithTitle:@"iCloud"
message:
@"iCloud is now disabled.\n\n"
@"It is highly recommended you enable iCloud."
viewStyle:UIAlertViewStyleDefault initAlert:nil
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
if (buttonIndex == [alert firstOtherButtonIndex] + 0) {
[PearlAlert showAlertWithTitle:@"About iCloud"
message:
@"iCloud is Apple's solution for saving your data in \"the cloud\" "
@"and making sure your other iPhones, iPads and Macs are in sync.\n\n"
@"For Master Password, that means your sites are available on all your "
@"Apple devices, and you always have a backup of them in case "
@"you loose one or need to restore.\n\n"
@"Because of the way Master Password works, it doesn't need to send your "
@"site's passwords to Apple. Only their names are saved to make it easier "
@"for you to find the site you need. For some sites you may have set "
@"a user-specified password: these are sent to iCloud after being encrypted "
@"with your master password.\n\n"
@"Apple can never see any of your passwords."
viewStyle:UIAlertViewStyleDefault
initAlert:nil tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
[self ubiquityStoreManager:manager didSwitchToCloud:cloudEnabled];
}
cancelTitle:[PearlStrings get].commonButtonThanks otherTitles:nil];
return;
}
[MPConfig get].iCloudDecided = @YES;
if (buttonIndex == [alert cancelButtonIndex])
return;
if (buttonIndex == [alert firstOtherButtonIndex] + 1)
manager.cloudEnabled = YES;
} cancelTitle:@"Leave iCloud Off" otherTitles:@"Explain?", @"Enable iCloud", nil];
}
}
}
#pragma mark - Google+
- (NSDictionary *)googlePlusInfo {
static NSDictionary *googlePlusInfo = nil;
if (googlePlusInfo == nil)
googlePlusInfo = [[NSDictionary alloc] initWithContentsOfURL:
[[NSBundle mainBundle] URLForResource:@"Google+" withExtension:@"plist"]];
return googlePlusInfo;
}
- (NSString *)googlePlusClientID {
return NSNullToNil([[self googlePlusInfo] valueForKeyPath:@"ClientID"]);
}
#pragma mark - TestFlight
- (NSDictionary *)testFlightInfo {
static NSDictionary *testFlightInfo = nil;
if (testFlightInfo == nil)
testFlightInfo = [[NSDictionary alloc] initWithContentsOfURL:
[[NSBundle mainBundle] URLForResource:@"TestFlight" withExtension:@"plist"]];
return testFlightInfo;
}
- (NSString *)testFlightToken {
return NSNullToNil([[self testFlightInfo] valueForKeyPath:@"Team Token"]);
}
#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 {
return NSNullToNil([[self crashlyticsInfo] valueForKeyPath:@"API Key"]);
}
#pragma mark - Localytics
- (NSDictionary *)localyticsInfo {
static NSDictionary *localyticsInfo = nil;
if (localyticsInfo == nil)
localyticsInfo = [[NSDictionary alloc] initWithContentsOfURL:
[[NSBundle mainBundle] URLForResource:@"Localytics" withExtension:@"plist"]];
return localyticsInfo;
}
- (NSString *)localyticsKey {
#ifdef DEBUG
return NSNullToNil([[self localyticsInfo] valueForKeyPath:@"Key.development"]);
#else
return NSNullToNil([[self localyticsInfo] valueForKeyPath:@"Key.distribution"]);
#endif
}
@end

View File

@@ -0,0 +1,26 @@
/**
* 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
*/
//
// MPAppViewController
//
// Created by Maarten Billemont on 2012-08-31.
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
//
#import <Foundation/Foundation.h>
@interface MPAppViewController : UIViewController
- (IBAction)gorillas:(UIButton *)sender;
- (IBAction)deblock:(UIButton *)sender;
@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
*/
//
// MPAppViewController
//
// Created by Maarten Billemont on 2012-08-31.
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
//
#import "MPAppViewController.h"
@implementation MPAppViewController {
}
- (IBAction)gorillas:(UIButton *)sender {
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointAppGorillas];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointAppGorillas attributes:nil];
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"http://itunes.apple.com/app/lyndir/gorillas/id302275459?mt=8"]];
}
- (IBAction)deblock:(UIButton *)sender {
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointAppDeBlock];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointAppDeBlock attributes:nil];
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"http://itunes.apple.com/app/lyndir/deblock/id325058485?mt=8"]];
}
@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
*/
//
// MPAppsViewController
//
// Created by Maarten Billemont on 2012-08-31.
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
//
#import <Foundation/Foundation.h>
@interface MPAppsViewController : UIViewController <UIPageViewControllerDataSource, UIPageViewControllerDelegate>
@property (weak, nonatomic) IBOutlet UIImageView *pagePositionView;
- (IBAction)exit;
@end

View File

@@ -0,0 +1,125 @@
/**
* 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
*/
//
// MPAppsViewController
//
// Created by Maarten Billemont on 2012-08-31.
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
//
#import "MPAppsViewController.h"
@interface MPAppsViewController ()
@property (nonatomic, strong) NSMutableArray *pageVCs;
@property (nonatomic, strong) UIPageViewController *pageViewController;
@end
@implementation MPAppsViewController {
}
@synthesize pagePositionView = _pagePositionView;
@synthesize pageVCs = _pageVCs;
@synthesize pageViewController = _pageViewController;
- (void)viewDidLoad {
self.pageVCs = [NSMutableArray array];
UIViewController *vc;
@try {
for (NSUInteger p = 0;
(vc = [self.storyboard instantiateViewControllerWithIdentifier:PearlString(@"MPAppViewController_%u", p)]);
++p)
[self.pageVCs addObject:vc];
}
@catch (NSException *e) {
if (![e.name isEqualToString:NSInvalidArgumentException])
[e raise];
}
self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStylePageCurl
navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
options:nil];
self.pageViewController.dataSource = self;
self.pageViewController.delegate = self;
[self addChildViewController:self.pageViewController];
[self.view addSubview:self.pageViewController.view];
self.pageViewController.view.frame = self.pagePositionView.frame;
[self.pagePositionView removeFromSuperview];
[self.pageViewController didMoveToParentViewController:self];
[self.pageViewController setViewControllers:@[[self.pageVCs objectAtIndex:0]] direction:UIPageViewControllerNavigationDirectionForward
animated:NO completion:nil];
[super viewDidLoad];
}
- (void)viewWillAppear:(BOOL)animated {
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:UIStatusBarAnimationSlide];
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointApps];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointApps attributes:nil];
[super viewWillAppear:animated];
}
- (void)viewDidAppear:(BOOL)animated {
[[LocalyticsSession sharedLocalyticsSession] tagScreen:@"Apps"];
[super viewDidAppear:animated];
}
- (void)viewWillDisappear:(BOOL)animated {
[[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationSlide];
[super viewWillDisappear:animated];
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationPortrait;
}
- (BOOL)shouldAutorotate {
return NO;
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
viewControllerBeforeViewController:(UIViewController *)viewController {
NSUInteger vcIndex = [self.pageVCs indexOfObject:viewController];
return [self.pageVCs objectAtIndex:(vcIndex + [self.pageVCs count] - 1) % self.pageVCs.count];
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
viewControllerAfterViewController:(UIViewController *)viewController {
NSUInteger vcIndex = [self.pageVCs indexOfObject:viewController];
return [self.pageVCs objectAtIndex:(vcIndex + 1) % self.pageVCs.count];
}
- (IBAction)exit {
[self dismissViewControllerAnimated:YES completion:nil];
}
@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
*/
//
// MPElementListAllViewController
//
// Created by Maarten Billemont on 2013-01-31.
// Copyright 2013 lhunath (Maarten Billemont). All rights reserved.
//
#import <Foundation/Foundation.h>
#import "MPElementListController.h"
@interface MPElementListAllViewController : MPElementListController
- (IBAction)close:(id)sender;
- (IBAction)add:(id)sender;
@end

View File

@@ -0,0 +1,56 @@
/**
* 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
*/
//
// MPElementListAllViewController
//
// Created by Maarten Billemont on 2013-01-31.
// Copyright 2013 lhunath (Maarten Billemont). All rights reserved.
//
#import "MPElementListAllViewController.h"
@implementation MPElementListAllViewController
- (IBAction)close:(id)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (IBAction)add:(id)sender {
[PearlAlert showAlertWithTitle:@"Add Site" message:nil viewStyle:UIAlertViewStylePlainTextInput initAlert:nil
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
if (alert.cancelButtonIndex == buttonIndex)
return;
__weak MPElementListAllViewController *wSelf = self;
[self addElementNamed:[alert textFieldAtIndex:0].text completion:^(BOOL success) {
if (success)
[wSelf close:nil];
}];
}
cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:[PearlStrings get].commonButtonOkay, nil];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self updateData];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[super tableView:tableView didSelectRowAtIndexPath:indexPath];
[self close:nil];
}
@end

View File

@@ -0,0 +1,253 @@
<?xml version="1.0" encoding="UTF-8"?>
<archive type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="8.00">
<data>
<int key="IBDocument.SystemTarget">1552</int>
<string key="IBDocument.SystemVersion">12D78</string>
<string key="IBDocument.InterfaceBuilderVersion">3084</string>
<string key="IBDocument.AppKitVersion">1187.37</string>
<string key="IBDocument.HIToolboxVersion">626.00</string>
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
<string key="NS.key.0">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
<string key="NS.object.0">2083</string>
</object>
<array key="IBDocument.IntegratedClassDependencies">
<string>IBProxyObject</string>
<string>IBUIImageView</string>
<string>IBUILabel</string>
<string>IBUITableViewCell</string>
</array>
<array key="IBDocument.PluginDependencies">
<string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</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="IBProxyObject" id="372490531">
<string key="IBProxiedObjectIdentifier">IBFilesOwner</string>
<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
</object>
<object class="IBProxyObject" id="975951072">
<string key="IBProxiedObjectIdentifier">IBFirstResponder</string>
<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
</object>
<object class="IBUITableViewCell" id="1072502652">
<reference key="NSNextResponder"/>
<int key="NSvFlags">1280</int>
<array class="NSMutableArray" key="NSSubviews">
<object class="IBUIView" id="70825627">
<reference key="NSNextResponder" ref="1072502652"/>
<int key="NSvFlags">1280</int>
<array class="NSMutableArray" key="NSSubviews">
<object class="IBUILabel" id="169671678">
<reference key="NSNextResponder" ref="70825627"/>
<int key="NSvFlags">1280</int>
<object class="NSPSMatrix" key="NSFrameMatrix"/>
<string key="NSFrame">{{10, 4}, {38, 22}}</string>
<reference key="NSSuperview" ref="70825627"/>
<reference key="NSNextKeyView" ref="35578451"/>
<object class="NSColor" key="IBUIBackgroundColor" id="801193159">
<int key="NSColorSpace">3</int>
<bytes key="NSWhite">MCAwAA</bytes>
</object>
<bool key="IBUIOpaque">NO</bool>
<bool key="IBUIClipsSubviews">YES</bool>
<int key="IBUIContentMode">7</int>
<bool key="IBUIMultipleTouchEnabled">YES</bool>
<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
<string key="IBUIText">Title</string>
<object class="NSColor" key="IBUITextColor">
<int key="NSColorSpace">3</int>
<bytes key="NSWhite">MQA</bytes>
</object>
<object class="NSColor" key="IBUIHighlightedColor" id="748798155">
<int key="NSColorSpace">1</int>
<bytes key="NSRGB">MSAxIDEAA</bytes>
</object>
<int key="IBUIBaselineAdjustment">0</int>
<object class="IBUIFontDescription" key="IBUIFontDescription">
<int key="type">2</int>
<double key="pointSize">18</double>
</object>
<object class="NSFont" key="IBUIFont">
<string key="NSName">Helvetica-Bold</string>
<double key="NSSize">18</double>
<int key="NSfFlags">16</int>
</object>
<bool key="IBUIAdjustsFontSizeToFit">NO</bool>
</object>
<object class="IBUILabel" id="35578451">
<reference key="NSNextResponder" ref="70825627"/>
<int key="NSvFlags">1280</int>
<object class="NSPSMatrix" key="NSFrameMatrix"/>
<string key="NSFrame">{{10, 26}, {47, 18}}</string>
<reference key="NSSuperview" ref="70825627"/>
<reference key="IBUIBackgroundColor" ref="801193159"/>
<bool key="IBUIOpaque">NO</bool>
<bool key="IBUIClipsSubviews">YES</bool>
<int key="IBUIContentMode">7</int>
<bool key="IBUIMultipleTouchEnabled">YES</bool>
<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
<string key="IBUIText">Subtitle</string>
<object class="NSColor" key="IBUITextColor">
<int key="NSColorSpace">3</int>
<bytes key="NSWhite">MC42NjY2NjY2NjY3AA</bytes>
</object>
<reference key="IBUIHighlightedColor" ref="748798155"/>
<int key="IBUIBaselineAdjustment">0</int>
<object class="IBUIFontDescription" key="IBUIFontDescription">
<int key="type">1</int>
<double key="pointSize">14</double>
</object>
<object class="NSFont" key="IBUIFont">
<string key="NSName">Helvetica</string>
<double key="NSSize">14</double>
<int key="NSfFlags">16</int>
</object>
<bool key="IBUIAdjustsFontSizeToFit">NO</bool>
</object>
</array>
<object class="NSPSMatrix" key="NSFrameMatrix"/>
<string key="NSFrameSize">{320, 47}</string>
<reference key="NSSuperview" ref="1072502652"/>
<reference key="NSNextKeyView" ref="169671678"/>
<reference key="IBUIBackgroundColor" ref="801193159"/>
<bool key="IBUIOpaque">NO</bool>
<bool key="IBUIClipsSubviews">YES</bool>
<int key="IBUIContentMode">4</int>
<bool key="IBUIMultipleTouchEnabled">YES</bool>
<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
</object>
</array>
<object class="NSPSMatrix" key="NSFrameMatrix"/>
<string key="NSFrameSize">{320, 48}</string>
<reference key="NSSuperview"/>
<reference key="NSNextKeyView" ref="70825627"/>
<object class="NSColor" key="IBUIBackgroundColor">
<int key="NSColorSpace">3</int>
<bytes key="NSWhite">MAA</bytes>
</object>
<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
<bool key="IBUIHidesAccessoryWhenEditing">NO</bool>
<int key="IBUIIndentationLevel">1</int>
<float key="IBUIIndentationWidth">0.0</float>
<reference key="IBUIContentView" ref="70825627"/>
<string key="IBUIReuseIdentifier">MPElementListCell</string>
<integer value="3" key="IBUIStyle"/>
<reference key="IBUITextLabel" ref="169671678"/>
<reference key="IBUIDetailTextLabel" ref="35578451"/>
</object>
<object class="IBUIImageView" id="410525493">
<nil key="NSNextResponder"/>
<int key="NSvFlags">274</int>
<string key="NSFrameSize">{320, 48}</string>
<string key="NSReuseIdentifierKey">_NS:9</string>
<reference key="IBUIBackgroundColor" ref="801193159"/>
<bool key="IBUIUserInteractionEnabled">NO</bool>
<string key="IBUIContentStretch">{{0.10000000000000001, 0.10000000000000001}, {0.79999999999999982, 0.79999999999999982}}</string>
<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
<object class="NSCustomResource" key="IBUIImage">
<string key="NSClassName">NSImage</string>
<string key="NSResourceName">ui_list_middle.png</string>
</object>
</object>
</array>
<object class="IBObjectContainer" key="IBDocument.Objects">
<array class="NSMutableArray" key="connectionRecords">
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">view</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="1072502652"/>
</object>
<int key="connectionID">11</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">backgroundView</string>
<reference key="source" ref="1072502652"/>
<reference key="destination" ref="410525493"/>
</object>
<int key="connectionID">10</int>
</object>
</array>
<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">-1</int>
<reference key="object" ref="372490531"/>
<reference key="parent" ref="0"/>
<string key="objectName">File's Owner</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">-2</int>
<reference key="object" ref="975951072"/>
<reference key="parent" ref="0"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">4</int>
<reference key="object" ref="1072502652"/>
<array class="NSMutableArray" key="children">
<reference ref="35578451"/>
<reference ref="169671678"/>
</array>
<reference key="parent" ref="0"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">5</int>
<reference key="object" ref="35578451"/>
<reference key="parent" ref="1072502652"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">6</int>
<reference key="object" ref="169671678"/>
<reference key="parent" ref="1072502652"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">8</int>
<reference key="object" ref="410525493"/>
<reference key="parent" ref="0"/>
</object>
</array>
</object>
<dictionary class="NSMutableDictionary" key="flattenedProperties">
<string key="-1.CustomClassName">UIViewController</string>
<string key="-1.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
<string key="-2.CustomClassName">UIResponder</string>
<string key="-2.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
<string key="4.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
<reference key="4.IBUserGuides" ref="0"/>
<boolean value="NO" key="4.showNotes"/>
<string key="5.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
<reference key="5.IBUserGuides" ref="0"/>
<boolean value="NO" key="5.showNotes"/>
<string key="6.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
<reference key="6.IBUserGuides" ref="0"/>
<boolean value="NO" key="6.showNotes"/>
<string key="8.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
</dictionary>
<dictionary class="NSMutableDictionary" key="unlocalizedProperties"/>
<nil key="activeLocalization"/>
<dictionary class="NSMutableDictionary" key="localizations"/>
<nil key="sourceID"/>
<int key="maxID">11</int>
</object>
<object class="IBClassDescriber" key="IBDocument.Classes"/>
<int key="IBDocument.localizationMode">0</int>
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaTouchFramework</string>
<bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
<int key="IBDocument.defaultPropertyAccessControl">3</int>
<object class="NSMutableDictionary" key="IBDocument.LastKnownImageSizes">
<string key="NS.key.0">ui_list_middle.png</string>
<string key="NS.object.0">{300, 34}</string>
</object>
<string key="IBCocoaTouchPluginVersion">2083</string>
</data>
</archive>

View File

@@ -0,0 +1,28 @@
//
// Created by lhunath on 2013-02-09.
//
// To change the template use AppCode | Preferences | File Templates.
//
#import <Foundation/Foundation.h>
#import "MPElementListDelegate.h"
typedef enum {
MPSearchScopeAll,
MPSearchScopeOutdated,
} MPSearchScope;
@interface MPElementListController : UITableViewController <NSFetchedResultsControllerDelegate>
@property (weak, nonatomic) IBOutlet id<MPElementListDelegate> delegate;
@property (readonly) NSFetchedResultsController *fetchedResultsController;
@property (readonly) NSDateFormatter *dateFormatter;
- (void)updateData;
- (void)addElementNamed:(NSString *)siteName completion:(void (^)(BOOL success))completion;
- (void)configureCell:(UITableViewCell *)cell inTableView:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath;
- (void)customTableViewUpdates;
@end

View File

@@ -0,0 +1,269 @@
//
// Created by lhunath on 2013-02-09.
//
// To change the template use AppCode | Preferences | File Templates.
//
#import "MPElementListController.h"
#import "MPAppDelegate_Store.h"
#import "MPAppDelegate.h"
@interface MPElementListController ()
@end
@implementation MPElementListController {
NSFetchedResultsController *_fetchedResultsController;
NSDateFormatter *_dateFormatter;
}
- (void)addElementNamed:(NSString *)siteName completion:(void(^)(BOOL success))completion {
if (![siteName length]) {
if (completion)
completion(false);
return;
}
[MPAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *moc) {
MPUserEntity *activeUser = [[MPAppDelegate get] activeUserInContext:moc];
assert(activeUser);
MPElementType type = activeUser.defaultType;
if (!type)
type = activeUser.defaultType = MPElementTypeGeneratedLong;
NSString *typeEntityClassName = [MPAlgorithmDefault classNameOfType:type];
MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:typeEntityClassName
inManagedObjectContext:moc];
element.name = siteName;
element.user = activeUser;
element.type = type;
element.lastUsed = [NSDate date];
element.version = MPAlgorithmDefaultVersion;
[element saveContext];
NSManagedObjectID *elementOID = [element objectID];
dispatch_async(dispatch_get_main_queue(), ^{
MPElementEntity *element_ = (MPElementEntity *) [[MPAppDelegate managedObjectContextForThreadIfReady]
objectRegisteredForID:elementOID];
[self.delegate didSelectElement:element_];
if (completion)
completion(true);
});
}];
}
- (NSFetchedResultsController *)fetchedResultsController {
if (!_fetchedResultsController) {
NSAssert([[NSThread currentThread] isMainThread], @"The fetchedResultsController must run on the main thread.");
NSManagedObjectContext *moc = [MPAppDelegate managedObjectContextForThreadIfReady];
if (!moc)
return nil;
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
fetchRequest.sortDescriptors = @[[[NSSortDescriptor alloc] initWithKey:@"uses_" ascending:NO]];
fetchRequest.fetchBatchSize = 20;
_fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:moc
sectionNameKeyPath:nil cacheName:nil];
_fetchedResultsController.delegate = self;
}
return _fetchedResultsController;
}
- (NSDateFormatter *)dateFormatter {
if (!_dateFormatter)
(_dateFormatter = [NSDateFormatter new]).dateStyle = NSDateFormatterShortStyle;
return _dateFormatter;
}
- (void)updateData {
MPUserEntity *activeUser = [MPAppDelegate get].activeUser;
if (!activeUser)
return;
// Build predicate.
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"user == %@", activeUser];
UISearchBar *searchBar = self.searchDisplayController.searchBar;
if (searchBar) {
NSString *query = [searchBar.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if (!query)
return;
// Add query predicate.
predicate = [NSCompoundPredicate andPredicateWithSubpredicates:
@[predicate, [NSPredicate predicateWithFormat:@"name BEGINSWITH[cd] %@", query]]];
// Add scope predicate.
switch ((MPSearchScope) searchBar.selectedScopeButtonIndex) {
case MPSearchScopeAll:
break;
case MPSearchScopeOutdated:
predicate = [NSCompoundPredicate andPredicateWithSubpredicates:
@[[NSPredicate predicateWithFormat:@"requiresExplicitMigration_ == YES"], predicate]];
break;
}
}
self.fetchedResultsController.fetchRequest.predicate = predicate;
NSError *error;
if (![self.fetchedResultsController performFetch:&error])
err(@"Couldn't fetch elements: %@", error);
else
[self.tableView reloadData];
}
- (void)customTableViewUpdates {
}
// See MP-14, also crashes easily on internal assertions etc..
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
dbg(@"%@", NSStringFromSelector(_cmd));
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
switch (type) {
case NSFetchedResultsChangeInsert:
dbg(@"%@ -- NSFetchedResultsChangeInsert:%@", NSStringFromSelector(_cmd), anObject);
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
dbg(@"%@ -- NSFetchedResultsChangeDelete:%@", NSStringFromSelector(_cmd), anObject);
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
dbg(@"%@ -- NSFetchedResultsChangeUpdate:%@", NSStringFromSelector(_cmd), anObject);
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeMove:
dbg(@"%@ -- NSFetchedResultsChangeMove:%@", NSStringFromSelector(_cmd), anObject);
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch (type) {
case NSFetchedResultsChangeInsert:
dbg(@"%@ -- NSFetchedResultsChangeInsert:%d", NSStringFromSelector(_cmd), sectionIndex);
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
dbg(@"%@ -- NSFetchedResultsChangeDelete:%d", NSStringFromSelector(_cmd), sectionIndex);
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeMove:
case NSFetchedResultsChangeUpdate:
Throw(@"Invalid change type for section changes: %d", type);
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
dbg(@"%@ on %@", NSStringFromSelector(_cmd), [NSThread currentThread].name);
[self customTableViewUpdates];
[self.tableView endUpdates];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
NSInteger integer = (NSInteger)[[self.fetchedResultsController sections] count];
dbg(@"%@ = %d", NSStringFromSelector(_cmd), integer);
return integer;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSInteger integer = (NSInteger)[[[self.fetchedResultsController sections] objectAtIndex:(unsigned)section] numberOfObjects];
dbg(@"%@%d = %d", NSStringFromSelector(_cmd), section, integer);
return integer;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MPElementListCell"];
if (!cell)
cell = (UITableViewCell *) [[UIViewController alloc] initWithNibName:@"MPElementListCellView" bundle:nil].view;
[self configureCell:cell inTableView:tableView atIndexPath:indexPath];
return cell;
}
- (void)configureCell:(UITableViewCell *)cell inTableView:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath {
MPElementEntity *element = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = element.name;
cell.detailTextLabel.text = PearlString(@"%d views, last on %@: %@",
element.uses, [self.dateFormatter stringFromDate:element.lastUsed], [element.algorithm shortNameOfType:element.type]);
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[self.delegate didSelectElement:[self.fetchedResultsController objectAtIndexPath:indexPath]];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return [[[self.fetchedResultsController sections] objectAtIndex:(unsigned)section] name];
}
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
return [self.fetchedResultsController sectionIndexTitles];
}
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index {
return [self.fetchedResultsController sectionForSectionIndexTitle:title atIndex:index];
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete)
[self.fetchedResultsController.managedObjectContext performBlock:^{
MPElementEntity *element = [self.fetchedResultsController objectAtIndexPath:indexPath];
inf(@"Deleting element: %@", element.name);
[self.fetchedResultsController.managedObjectContext deleteObject:element];
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointDeleteElement];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointDeleteElement attributes:@{
@"type" : element.typeName,
@"version" : @(element.version)}];
}];
}
@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
*/
//
// MPElementListDelegate
//
// Created by Maarten Billemont on 2013-01-31.
// Copyright 2013 lhunath (Maarten Billemont). All rights reserved.
//
#import "MPElementEntity.h"
@protocol MPElementListDelegate <NSObject>
- (void)didSelectElement:(MPElementEntity *)element;
@end

View File

@@ -0,0 +1,19 @@
//
// MPSearchDelegate.h
// MasterPassword
//
// Created by Maarten Billemont on 04/01/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "MPElementListController.h"
@interface MPElementListSearchController : MPElementListController<UISearchBarDelegate, UISearchDisplayDelegate>
@property (strong, nonatomic) UILabel *tipView;
@property (strong, nonatomic) IBOutlet UISearchDisplayController *searchDisplayController;
@property (weak, nonatomic) IBOutlet UIView *searchTipContainer;
@end

View File

@@ -0,0 +1,259 @@
//
// MPSearchDelegate.m
// MasterPassword
//
// Created by Maarten Billemont on 04/01/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "MPElementListSearchController.h"
#import "MPMainViewController.h"
#import "MPAppDelegate.h"
@interface MPElementListSearchController ()
@property (nonatomic) BOOL newSiteSectionWasNeeded;
@end
@implementation MPElementListSearchController
@synthesize searchDisplayController;
- (id)init {
if (!(self = [super init]))
return nil;
self.tipView = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 320, 170)];
self.tipView.textAlignment = NSTextAlignmentCenter;
self.tipView.backgroundColor = [UIColor clearColor];
self.tipView.textColor = [UIColor lightTextColor];
self.tipView.shadowColor = [UIColor blackColor];
self.tipView.shadowOffset = CGSizeMake(0, -1);
self.tipView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight
| UIViewAutoresizingFlexibleBottomMargin;
self.tipView.numberOfLines = 0;
self.tipView.font = [UIFont systemFontOfSize:14];
self.tipView.text =
@"Tip:\n"
@"Name your sites by their domain name:\n"
@"apple.com, twitter.com\n\n"
@"For email accounts, use the address:\n"
@"john@apple.com, john@gmail.com";
return self;
}
- (void)searchBarBookmarkButtonClicked:(UISearchBar *)searchBar {
[((MPMainViewController *)self.delegate) performSegueWithIdentifier:@"MP_AllSites" sender:self];
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
// Simulate a tap on the first visible row.
UITableView *tableView = self.searchDisplayController.searchResultsTableView;
for (NSInteger section = 0; section < [self numberOfSectionsInTableView:tableView]; ++section) {
if (![self tableView:tableView numberOfRowsInSection:section])
continue;
[self tableView:tableView didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section]];
}
}
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
[self.delegate didSelectElement:nil];
}
- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller {
controller.searchBar.showsScopeBar = controller.searchBar.selectedScopeButtonIndex != MPSearchScopeAll;
controller.searchBar.text = @"";
if (controller.searchBar.showsScopeBar)
controller.searchBar.scopeButtonTitles = @[@"All", @"Outdated"];
else
controller.searchBar.scopeButtonTitles = nil;
[UIView animateWithDuration:0.2f animations:^{
self.searchTipContainer.alpha = 0;
}];
}
- (void)searchDisplayControllerDidBeginSearch:(UISearchDisplayController *)controller {
[self updateData];
}
- (void)searchDisplayControllerWillEndSearch:(UISearchDisplayController *)controller {
controller.searchBar.prompt = nil;
controller.searchBar.searchResultsButtonSelected = NO;
controller.searchBar.selectedScopeButtonIndex = MPSearchScopeAll;
controller.searchBar.showsScopeBar = NO;
}
- (void)searchDisplayController:(UISearchDisplayController *)controller didLoadSearchResultsTableView:(UITableView *)tableView {
tableView.backgroundColor = [UIColor blackColor];
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
tableView.rowHeight = 48.0f;
self.tableView = tableView;
}
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString {
[self updateData];
return NO;
}
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchScope:(NSInteger)searchOption {
[self updateData];
return NO;
}
- (void)updateData {
[super updateData];
UISearchBar *searchBar = self.searchDisplayController.searchBar;
CGRect searchBarFrame = searchBar.frame;
[searchBar.superview enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
if ([subview isKindOfClass:[UIControl class]] &&
CGPointEqualToPoint(
CGPointDistanceBetweenCGPoints(searchBarFrame.origin, subview.frame.origin),
CGPointMake(0, searchBarFrame.size.height))) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.tipView removeFromSuperview];
[subview addSubview:self.tipView];
});
*stop = YES;
}
} recurse:NO];
}
- (BOOL)newSiteSectionNeeded {
NSString *query = [self.searchDisplayController.searchBar.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if (![query length])
return NO;
__block BOOL hasExactQueryMatch = NO;
[[self.fetchedResultsController sections] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
id <NSFetchedResultsSectionInfo> sectionInfo = obj;
[[sectionInfo objects] enumerateObjectsUsingBlock:^(id obj_, NSUInteger idx_, BOOL *stop_) {
if ([[obj_ name] isEqualToString:query]) {
hasExactQueryMatch = YES;
*stop_ = YES;
}
}];
if (hasExactQueryMatch)
*stop = YES;
}];
return !hasExactQueryMatch;
}
- (void)customTableViewUpdates {
BOOL newSiteSectionIsNeeded = [self newSiteSectionNeeded];
dbg(@"isNeeded:%d, wasNeeded:%d", newSiteSectionIsNeeded, self.newSiteSectionWasNeeded);
if (newSiteSectionIsNeeded && !self.newSiteSectionWasNeeded) {
dbg(@"%@ -- insertSection:%d", NSStringFromSelector(_cmd), [[self.fetchedResultsController sections] count]);
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:[[self.fetchedResultsController sections] count]]
withRowAnimation:UITableViewRowAnimationAutomatic];
}
else if (!newSiteSectionIsNeeded && self.newSiteSectionWasNeeded) {
dbg(@"%@ -- deleteSection:%d", NSStringFromSelector(_cmd), [[self.fetchedResultsController sections] count]);
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:[[self.fetchedResultsController sections] count]]
withRowAnimation:UITableViewRowAnimationAutomatic];
}
self.newSiteSectionWasNeeded = newSiteSectionIsNeeded;
dbg(@"wasNeeded->%d", self.newSiteSectionWasNeeded);
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
NSInteger sectionCount = [super numberOfSectionsInTableView:tableView];
if ([self newSiteSectionNeeded])
++sectionCount;
dbg(@"%@ (actually) = %d", NSStringFromSelector(_cmd), sectionCount);
return sectionCount;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSUInteger fetchSections = [[self.fetchedResultsController sections] count];
if (section < (NSInteger)fetchSections)
return [super tableView:tableView numberOfRowsInSection:section];
dbg(@"%@%d = %d", NSStringFromSelector(_cmd), section, 1);
return 1;
}
- (void)configureCell:(UITableViewCell *)cell inTableView:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath {
NSUInteger fetchSections = [[self.fetchedResultsController sections] count];
if (indexPath.section < (NSInteger)fetchSections)
[super configureCell:cell inTableView:tableView atIndexPath:indexPath];
else {
// "New" section
NSString *query = [self.searchDisplayController.searchBar.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
cell.textLabel.text = query;
cell.detailTextLabel.text = PearlString(@"Add new site: %@",
[MPAlgorithmDefault shortNameOfType:[[MPAppDelegate get].activeUser defaultType]]);
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSUInteger fetchSections = [[self.fetchedResultsController sections] count];
if (indexPath.section < (NSInteger)fetchSections) {
[super tableView:tableView didSelectRowAtIndexPath:indexPath];
return;
}
// "New" section.
NSString *siteName = [self.searchDisplayController.searchBar.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
[PearlAlert showAlertWithTitle:@"New Site"
message:PearlString(@"Do you want to create a new site named:\n%@", siteName)
viewStyle:UIAlertViewStyleDefault
initAlert:nil tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
if (buttonIndex == [alert cancelButtonIndex])
return;
[self addElementNamed:siteName completion:nil];
} cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:[PearlStrings get].commonButtonYes, nil];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
NSUInteger fetchSections = [[self.fetchedResultsController sections] count];
if (section < (NSInteger)fetchSections)
return [super tableView:tableView titleForHeaderInSection:section];
return @"";
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath {
NSUInteger fetchSections = [[self.fetchedResultsController sections] count];
if (indexPath.section < (NSInteger)fetchSections)
[super tableView:tableView commitEditingStyle:editingStyle forRowAtIndexPath:indexPath];
}
@end

View File

@@ -0,0 +1,18 @@
//
// 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 <UIScrollViewDelegate>
@property (weak, nonatomic) IBOutlet UIScrollView *scrollView;
@property (weak, nonatomic) IBOutlet UIPageControl *pageControl;
- (IBAction)close;
@end

View File

@@ -0,0 +1,69 @@
//
// MPGuideViewController.m
// MasterPassword
//
// Created by Maarten Billemont on 30/01/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "MPGuideViewController.h"
@implementation MPGuideViewController
- (BOOL)shouldAutorotate {
return NO;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationPortrait;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self.scrollView autoSizeContent];
}
- (void)viewWillAppear:(BOOL)animated {
inf(@"Guide will appear.");
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:UIStatusBarAnimationSlide];
[super viewWillAppear:animated];
}
- (void)viewDidAppear:(BOOL)animated {
[[LocalyticsSession sharedLocalyticsSession] tagScreen:@"Guide"];
[super viewDidAppear:animated];
}
- (void)viewWillDisappear:(BOOL)animated {
inf(@"Guide will disappear.");
[[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationSlide];
[MPiOSConfig get].showQuickStart = @NO;
[super viewWillDisappear:animated];
}
- (IBAction)close {
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView_ {
NSInteger page = (NSInteger)(self.scrollView.contentOffset.x / self.scrollView.bounds.size.width);
self.pageControl.currentPage = page;
self.pageControl.hidden = (page == self.pageControl.numberOfPages - 1);
}
@end

View File

@@ -0,0 +1,75 @@
//
// MPMainViewController.h
// MasterPassword
//
// Created by Maarten Billemont on 24/11/11.
// Copyright (c) 2011 Lyndir. All rights reserved.
//
#import <MessageUI/MessageUI.h>
#import "MPTypeViewController.h"
#import "MPElementListSearchController.h"
@interface MPMainViewController : UIViewController<MPTypeDelegate, UITextFieldDelegate, MPElementListDelegate, UIWebViewDelegate, UIGestureRecognizerDelegate>
@property (assign, nonatomic) BOOL siteInfoHidden;
@property (strong, nonatomic) IBOutlet MPElementListSearchController *searchDelegate;
@property (strong, nonatomic) IBOutlet UIPanGestureRecognizer *pullDownGesture;
@property (strong, nonatomic) IBOutlet UIPanGestureRecognizer *pullUpGesture;
@property (weak, nonatomic) IBOutlet UITextField *contentField;
@property (weak, nonatomic) IBOutlet UIButton *typeButton;
@property (weak, nonatomic) IBOutlet UIWebView *helpView;
@property (weak, nonatomic) IBOutlet UILabel *siteName;
@property (weak, nonatomic) IBOutlet UILabel *passwordCounter;
@property (weak, nonatomic) IBOutlet UIButton *passwordIncrementer;
@property (weak, nonatomic) IBOutlet UIButton *passwordEdit;
@property (weak, nonatomic) IBOutlet UIButton *passwordUpgrade;
@property (weak, nonatomic) IBOutlet UIView *contentContainer;
@property (weak, nonatomic) IBOutlet UIView *displayContainer;
@property (weak, nonatomic) IBOutlet UIView *helpContainer;
@property (weak, nonatomic) IBOutlet UIView *contentTipContainer;
@property (weak, nonatomic) IBOutlet UIView *loginNameTipContainer;
@property (weak, nonatomic) IBOutlet UIView *alertContainer;
@property (weak, nonatomic) IBOutlet UILabel *alertTitle;
@property (weak, nonatomic) IBOutlet UITextView *alertBody;
@property (weak, nonatomic) IBOutlet UILabel *contentTipBody;
@property (weak, nonatomic) IBOutlet UILabel *loginNameTipBody;
@property (weak, nonatomic) IBOutlet UIImageView *toolTipEditIcon;
@property (weak, nonatomic) IBOutlet UIView *searchTipContainer;
@property (weak, nonatomic) IBOutlet UIView *actionsTipContainer;
@property (weak, nonatomic) IBOutlet UIView *typeTipContainer;
@property (weak, nonatomic) IBOutlet UIView *toolTipContainer;
@property (weak, nonatomic) IBOutlet UILabel *toolTipBody;
@property (weak, nonatomic) IBOutlet UIView *loginNameContainer;
@property (weak, nonatomic) IBOutlet UITextField *loginNameField;
@property (weak, nonatomic) IBOutlet UIButton *passwordUser;
@property (weak, nonatomic) IBOutlet UIView *outdatedAlertContainer;
@property (weak, nonatomic) IBOutlet UIImageView *outdatedAlertBack;
@property (weak, nonatomic) IBOutlet UIButton *outdatedAlertCloseButton;
@property (weak, nonatomic) IBOutlet UIImageView *pullUpView;
@property (weak, nonatomic) IBOutlet UIImageView *pullDownView;
@property (copy, nonatomic) void (^contentTipCleanup)(BOOL finished);
@property (copy, nonatomic) void (^toolTipCleanup)(BOOL finished);
- (IBAction)copyContent;
- (IBAction)incrementPasswordCounter;
- (IBAction)resetPasswordCounter:(UILongPressGestureRecognizer *)sender;
- (IBAction)editLoginName:(UILongPressGestureRecognizer *)sender;
- (IBAction)editPassword;
- (IBAction)closeAlert;
- (IBAction)upgradePassword;
- (IBAction)action:(UIBarButtonItem *)sender;
- (IBAction)toggleUser;
- (IBAction)searchOutdatedElements;
- (IBAction)closeOutdatedAlert;
- (IBAction)infoOutdatedAlert;
- (void)toggleHelpAnimated:(BOOL)animated;
- (void)setHelpHidden:(BOOL)hidden animated:(BOOL)animated;
- (void)setHelpChapter:(NSString *)chapter;
- (IBAction)panHelpDown:(UIPanGestureRecognizer *)sender;
- (IBAction)panHelpUp:(UIPanGestureRecognizer *)sender;
@end

View File

@@ -0,0 +1,961 @@
//
// MPMainViewController.m
// MasterPassword
//
// Created by Maarten Billemont on 24/11/11.
// Copyright (c) 2011 Lyndir. All rights reserved.
//
#import "MPMainViewController.h"
#import "MPAppDelegate.h"
#import "MPAppDelegate_Key.h"
#import "MPAppDelegate_Store.h"
#import "MPElementListAllViewController.h"
#import "MPElementListSearchController.h"
@interface MPMainViewController()
@property (nonatomic)BOOL suppressOutdatedAlert;
@end
@implementation MPMainViewController {
NSManagedObjectID *_activeElementOID;
}
#pragma mark - View lifecycle
- (BOOL)shouldAutorotate {
return YES;
}
- (NSUInteger)supportedInterfaceOrientations {
return UIInterfaceOrientationMaskAllButUpsideDown;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationPortrait;
}
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
[self updateHelpHiddenAnimated:NO];
[self updateUserHiddenAnimated:NO];
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([[segue identifier] isEqualToString:@"MP_ChooseType"])
((MPTypeViewController *)[segue destinationViewController]).delegate = self;
if ([[segue identifier] isEqualToString:@"MP_AllSites"])
((MPElementListAllViewController *)[((UINavigationController *)[segue destinationViewController]) topViewController]).delegate = self;
}
- (void)viewDidLoad {
self.searchDelegate = [MPElementListSearchController new];
self.searchDelegate.delegate = self;
self.searchDelegate.searchDisplayController = self.searchDisplayController;
self.searchDelegate.searchTipContainer = self.searchTipContainer;
self.searchDisplayController.searchBar.delegate = self.searchDelegate;
self.searchDisplayController.delegate = self.searchDelegate;
self.searchDisplayController.searchResultsDelegate = self.searchDelegate;
self.searchDisplayController.searchResultsDataSource = self.searchDelegate;
[self.passwordIncrementer addGestureRecognizer:[[UILongPressGestureRecognizer alloc] initWithTarget:self
action:@selector(resetPasswordCounter:)]];
[self.loginNameContainer addGestureRecognizer:[[UILongPressGestureRecognizer alloc] initWithTarget:self
action:@selector(editLoginName:)]];
[self.loginNameContainer addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(copyLoginName:)]];
[self.outdatedAlertBack addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(infoOutdatedAlert)]];
self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"ui_background"]];
self.alertBody.text = nil;
self.toolTipEditIcon.hidden = YES;
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification object:self queue:nil
usingBlock:^(NSNotification *note) {
self.suppressOutdatedAlert = NO;
}];
[[NSNotificationCenter defaultCenter] addObserverForName:MPElementUpdatedNotification object:nil queue:nil usingBlock:
^void(NSNotification *note) {
[self activeElementDo:^(MPElementEntity *activeElement) {
if (activeElement.type & MPElementTypeClassStored && ![[activeElement.content description] length])
[self showToolTip:@"Tap to set a password." withIcon:self.toolTipEditIcon];
if (activeElement.requiresExplicitMigration)
[self showToolTip:@"Password outdated. Tap to upgrade it." withIcon:nil];
}];
}];
[[NSNotificationCenter defaultCenter] addObserverForName:MPSignedOutNotification object:nil queue:nil
usingBlock:^void(NSNotification *note) {
_activeElementOID = nil;
self.suppressOutdatedAlert = NO;
[self updateAnimated:NO];
}];
[super viewDidLoad];
}
- (void)viewWillAppear:(BOOL)animated {
if ([[MPiOSConfig get].showQuickStart boolValue])
[[MPAppDelegate get] showGuide];
if (![MPAppDelegate get].activeUser)
// FIXME: Remove either this one or the one in -viewDidAppear:
[self presentViewController:[self.storyboard instantiateViewControllerWithIdentifier:@"MPUnlockViewController"]
animated:animated completion:nil];
[self activeElementDo:^(MPElementEntity *activeElement) {
if (activeElement.user != [MPAppDelegate get].activeUser)
_activeElementOID = nil;
}];
self.searchDisplayController.searchBar.text = nil;
self.alertContainer.alpha = 0;
self.outdatedAlertContainer.alpha = 0;
self.searchTipContainer.alpha = 0;
self.actionsTipContainer.alpha = 0;
self.typeTipContainer.alpha = 0;
self.toolTipContainer.alpha = 0;
[self updateAnimated:NO];
[super viewWillAppear:animated];
}
- (void)viewDidAppear:(BOOL)animated {
inf(@"Main will appear");
// Sometimes, the search bar gets stuck in some sort of first-responder mode that it can't get out of...
[[self.view.window findFirstResponderInHierarchy] resignFirstResponder];
// Needed for when we appear after a modal VC dismisses:
// We can't present until the other modal VC has been fully dismissed and presenting in -viewWillAppear: will fail.
if ([MPAppDelegate get].activeUser)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
if ([MPAlgorithmDefault migrateUser:[MPAppDelegate get].activeUser] && !self.suppressOutdatedAlert)
[UIView animateWithDuration:0.3f animations:^{
self.outdatedAlertContainer.alpha = 1;
self.suppressOutdatedAlert = YES;
}];
});
else
[self presentViewController:[self.storyboard instantiateViewControllerWithIdentifier:@"MPUnlockViewController"]
animated:animated completion:nil];
if (![[MPiOSConfig get].actionsTipShown boolValue])
[UIView animateWithDuration:animated? 0.3f: 0 animations:^{
self.actionsTipContainer.alpha = 1;
} completion:^(BOOL finished) {
if (!finished)
return;
[MPiOSConfig get].actionsTipShown = @YES;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[UIView animateWithDuration:0.2f animations:^{
self.actionsTipContainer.alpha = 0;
} completion:^(BOOL finished_) {
if (!_activeElementOID)
[UIView animateWithDuration:animated? 0.3f: 0 animations:^{
self.searchTipContainer.alpha = 1;
}];
}];
});
}];
[[LocalyticsSession sharedLocalyticsSession] tagScreen:@"Main"];
[super viewDidAppear:animated];
}
- (void)viewWillDisappear:(BOOL)animated {
inf(@"Main will disappear.");
[super viewWillDisappear:animated];
}
- (void)updateAnimated:(BOOL)animated {
if (animated) {
[UIView animateWithDuration:0.3f animations:^{
[self updateAnimated:NO];
}];
return;
}
[self activeElementDo:^(MPElementEntity *activeElement) {
[self setHelpChapter:activeElement? @"2": @"1"];
[self updateHelpHiddenAnimated:NO];
self.passwordCounter.alpha = 0;
self.passwordIncrementer.alpha = 0;
self.passwordEdit.alpha = 0;
self.passwordUpgrade.alpha = 0;
self.passwordUser.alpha = 0;
self.displayContainer.alpha = 0;
if (activeElement) {
self.passwordUser.alpha = 0.5f;
self.displayContainer.alpha = 1.0f;
}
if (activeElement.requiresExplicitMigration)
self.passwordUpgrade.alpha = 0.5f;
else {
if (activeElement.type & MPElementTypeClassGenerated) {
self.passwordCounter.alpha = 0.5f;
self.passwordIncrementer.alpha = 0.5f;
} else
if (activeElement.type & MPElementTypeClassStored)
self.passwordEdit.alpha = 0.5f;
}
self.siteName.text = activeElement.name;
self.typeButton.alpha = activeElement? 1: 0;
[self.typeButton setTitle:activeElement.typeName
forState:UIControlStateNormal];
if ([activeElement isKindOfClass:[MPElementGeneratedEntity class]])
self.passwordCounter.text = PearlString(@"%u", ((MPElementGeneratedEntity *)activeElement).counter);
self.contentField.enabled = NO;
self.contentField.text = @"";
if (activeElement.name && ![activeElement isDeleted])
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSString *description = [activeElement.content description];
dispatch_async(dispatch_get_main_queue(), ^{
self.contentField.text = description;
});
});
self.loginNameField.enabled = NO;
self.loginNameField.text = activeElement.loginName;
self.siteInfoHidden = !activeElement || ([[MPiOSConfig get].siteInfoHidden boolValue] && (activeElement.loginName == nil));
[self updateUserHiddenAnimated:NO];
}];
}
- (void)toggleHelpAnimated:(BOOL)animated {
[self setHelpHidden:![[MPiOSConfig get].helpHidden boolValue] animated:animated];
}
- (void)setHelpHidden:(BOOL)hidden animated:(BOOL)animated {
[MPiOSConfig get].helpHidden = @(hidden);
[self updateHelpHiddenAnimated:animated];
}
- (void)updateHelpHiddenAnimated:(BOOL)animated {
if (animated) {
[UIView animateWithDuration:0.3f animations:^{
[self updateHelpHiddenAnimated:NO];
}];
return;
}
self.pullUpView.hidden = ![[MPiOSConfig get].helpHidden boolValue];
self.pullDownView.hidden = [[MPiOSConfig get].helpHidden boolValue];
if ([[MPiOSConfig get].helpHidden boolValue]) {
self.contentContainer.frame = CGRectSetHeight(self.contentContainer.frame, self.view.bounds.size.height - 44 /* search bar */);
self.helpContainer.frame = CGRectSetY(self.helpContainer.frame, self.view.bounds.size.height - 20);
} else {
self.contentContainer.frame = CGRectSetHeight(self.contentContainer.frame, 225);
self.helpContainer.frame = CGRectSetY(self.helpContainer.frame, 246);
}
}
- (IBAction)toggleUser {
[self toggleUserAnimated:YES];
}
- (void)toggleUserAnimated:(BOOL)animated {
[MPiOSConfig get].siteInfoHidden = PearlBool(!self.siteInfoHidden);
self.siteInfoHidden = [[MPiOSConfig get].siteInfoHidden boolValue];
[self updateUserHiddenAnimated:animated];
}
- (void)updateUserHiddenAnimated:(BOOL)animated {
if (animated) {
[UIView animateWithDuration:0.3f animations:^{
[self updateUserHiddenAnimated:NO];
}];
return;
}
if (self.siteInfoHidden) {
self.displayContainer.frame = CGRectSetHeight(self.displayContainer.frame, 87);
} else {
self.displayContainer.frame = CGRectSetHeight(self.displayContainer.frame, 137);
}
}
- (void)setHelpChapter:(NSString *)chapter {
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:PearlString(MPCheckpointHelpChapter @"_%@", chapter)];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointHelpChapter attributes:@{@"chapter": chapter}];
dispatch_async(dispatch_get_main_queue(), ^{
NSURL *url = [NSURL URLWithString:[@"#" stringByAppendingString:chapter]
relativeToURL:[[NSBundle mainBundle] URLForResource:@"help" withExtension:@"html"]];
[self.helpView loadRequest:[NSURLRequest requestWithURL:url]];
});
}
- (IBAction)panHelpDown:(UIPanGestureRecognizer *)sender {
CGFloat targetY = MIN(self.view.bounds.size.height - 20, 246 + [sender translationInView:self.helpContainer].y);
BOOL hideHelp = YES;
if (targetY <= 246) {
hideHelp = NO;
targetY = 246;
}
self.helpContainer.frame = CGRectSetY(self.helpContainer.frame, targetY);
if (sender.state == UIGestureRecognizerStateEnded)
[self setHelpHidden:hideHelp animated:YES];
}
- (IBAction)panHelpUp:(UIPanGestureRecognizer *)sender {
CGFloat targetY = MAX(246, self.view.bounds.size.height - 20 + [sender translationInView:self.helpContainer].y);
BOOL hideHelp = NO;
if (targetY >= self.view.bounds.size.height - 20) {
hideHelp = YES;
targetY = self.view.bounds.size.height - 20 ;
}
self.helpContainer.frame = CGRectSetY(self.helpContainer.frame, targetY);
if (sender.state == UIGestureRecognizerStateEnded)
[self setHelpHidden:hideHelp animated:YES];
}
- (void)webViewDidFinishLoad:(UIWebView *)webView {
[self activeElementDo:^(MPElementEntity *activeElement) {
NSString *error = [self.helpView stringByEvaluatingJavaScriptFromString:
PearlString(@"setClass('%@');", activeElement.typeClassName)];
if (error.length)
err(@"helpView.setClass: %@", error);
}];
}
- (void)showContentTip:(NSString *)message withIcon:(UIImageView *)icon {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.contentTipCleanup)
self.contentTipCleanup(NO);
__weak MPMainViewController *wSelf = self;
self.contentTipBody.text = message;
self.contentTipCleanup = ^(BOOL finished) {
icon.hidden = YES;
wSelf.contentTipCleanup = nil;
};
icon.hidden = NO;
[UIView animateWithDuration:0.3f animations:^{
self.contentTipContainer.alpha = 1;
} completion:^(BOOL finished) {
if (finished) {
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
[UIView animateWithDuration:0.2f animations:^{
self.contentTipContainer.alpha = 0;
} completion:self.contentTipCleanup];
});
}
}];
});
}
- (void)showLoginNameTip:(NSString *)message {
dispatch_async(dispatch_get_main_queue(), ^{
self.loginNameTipBody.text = message;
[UIView animateWithDuration:0.3f animations:^{
self.loginNameTipContainer.alpha = 1;
} completion:^(BOOL finished) {
if (finished) {
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
[UIView animateWithDuration:0.2f animations:^{
self.loginNameTipContainer.alpha = 0;
}];
});
}
}];
});
}
- (void)showToolTip:(NSString *)message withIcon:(UIImageView *)icon {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.toolTipCleanup)
self.toolTipCleanup(NO);
__weak MPMainViewController *wSelf = self;
self.toolTipBody.text = message;
self.toolTipCleanup = ^(BOOL finished) {
icon.hidden = YES;
wSelf.toolTipCleanup = nil;
};
icon.hidden = NO;
[UIView animateWithDuration:0.3f animations:^{
self.toolTipContainer.alpha = 1;
} completion:^(BOOL finished) {
if (finished) {
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
[UIView animateWithDuration:0.2f animations:^{
self.toolTipContainer.alpha = 0;
} completion:self.toolTipCleanup];
});
}
}];
});
}
- (void)showAlertWithTitle:(NSString *)title message:(NSString *)message {
dispatch_async(dispatch_get_main_queue(), ^{
self.alertTitle.text = title;
NSRange scrollRange = NSMakeRange(self.alertBody.text.length, message.length);
if ([self.alertBody.text length])
self.alertBody.text = [NSString stringWithFormat:@"%@\n\n---\n\n%@", self.alertBody.text, message];
else
self.alertBody.text = message;
[self.alertBody scrollRangeToVisible:scrollRange];
[UIView animateWithDuration:0.3f animations:^{
self.alertContainer.alpha = 1;
}];
});
}
#pragma mark - Protocols
- (IBAction)copyContent {
[self activeElementDo:^(MPElementEntity *activeElement) {
id content = activeElement.content;
if (!content)
// Nothing to copy.
return;
inf(@"Copying password for: %@", activeElement.name);
[UIPasteboard generalPasteboard].string = [content description];
[self showContentTip:@"Copied!" withIcon:nil];
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointCopyToPasteboard];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointCopyToPasteboard attributes:@{@"type" : activeElement.typeName,
@"version" : @(activeElement.version)}];
}];
}
- (IBAction)copyLoginName:(UITapGestureRecognizer *)sender {
[self activeElementDo:^(MPElementEntity *activeElement) {
if (!activeElement.loginName)
return;
inf(@"Copying user name for: %@", activeElement.name);
[UIPasteboard generalPasteboard].string = activeElement.loginName;
[self showLoginNameTip:@"Copied!"];
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointCopyLoginNameToPasteboard];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointCopyLoginNameToPasteboard
attributes:@{@"type" : activeElement.typeName,
@"version" : @(activeElement.version)}];
}];
}
- (IBAction)incrementPasswordCounter {
[self changeActiveElementWithWarning:
@"You are incrementing the site's password counter.\n\n"
@"If you continue, a new password will be generated for this site. "
@"You will then need to update your account's old password to this newly generated password.\n\n"
@"You can reset the counter by holding down on this button."
do:^BOOL(MPElementEntity *activeElement) {
if (![activeElement isKindOfClass:[MPElementGeneratedEntity class]]) {
// Not of a type that supports a password counter.
err(@"Cannot increment password counter: Element is not generated: %@", activeElement.name);
return NO;
}
inf(@"Incrementing password counter for: %@", activeElement.name);
++((MPElementGeneratedEntity *)activeElement).counter;
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointIncrementPasswordCounter];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointIncrementPasswordCounter
attributes:@{@"type": activeElement.typeName,
@"version": @(activeElement.version)}];
return YES;
}];
}
- (IBAction)resetPasswordCounter:(UILongPressGestureRecognizer *)sender {
if (sender.state != UIGestureRecognizerStateBegan)
// Only fire when the gesture was first detected.
return;
__block BOOL abort = NO;
[self activeElementDo:^(MPElementEntity *activeElement) {
if (![activeElement isKindOfClass:[MPElementGeneratedEntity class]]) {
// Not of a type that supports a password counter.
err(@"Cannot reset password counter: Element is not generated: %@", activeElement.name);
abort = YES;
} else
if (((MPElementGeneratedEntity *)activeElement).counter == 1)
// Counter has initial value, no point resetting.
abort = YES;
}];
if (abort)
return;
[self changeActiveElementWithWarning:
@"You are resetting the site's password counter.\n\n"
@"If you continue, the site's password will change back to its original value. "
@"You will then need to update your account's password back to this original value."
do:^BOOL(MPElementEntity *activeElement){
inf(@"Resetting password counter for: %@", activeElement.name);
((MPElementGeneratedEntity *)activeElement).counter = 1;
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointResetPasswordCounter];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointResetPasswordCounter
attributes:@{@"type": activeElement.typeName,
@"version": @(activeElement.version)}];
return YES;
}];
}
- (IBAction)editLoginName:(UILongPressGestureRecognizer *)sender {
if (sender.state != UIGestureRecognizerStateBegan)
// Only fire when the gesture was first detected.
return;
[self activeElementDo:^(MPElementEntity *activeElement) {
if (!activeElement)
return;
self.loginNameField.enabled = YES;
[self.loginNameField becomeFirstResponder];
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointEditLoginName];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointEditLoginName attributes:@{@"type" : activeElement.typeName,
@"version" : @(activeElement.version)}];
}];
}
- (void)changeActiveElementWithWarning:(NSString *)warning do:(BOOL (^)(MPElementEntity *activeElement))task; {
[PearlAlert showAlertWithTitle:@"Password Change" message:warning viewStyle:UIAlertViewStyleDefault
initAlert:nil tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
if (buttonIndex == [alert cancelButtonIndex])
return;
[self changeActiveElementWithoutWarningDo:task];
} cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:[PearlStrings get].commonButtonContinue, nil];
}
- (void)changeActiveElementWithoutWarningDo:(BOOL (^)(MPElementEntity *activeElement))task; {
// Update element, keeping track of the old password.
[self activeElementDo:^(MPElementEntity *activeElement) {
NSManagedObjectContext *moc = activeElement.managedObjectContext;
[moc performBlock:^{
// Perform the task.
NSString *oldPassword = [activeElement.content description];
if (!task(activeElement))
return;
NSString *newPassword = [activeElement.content description];
// Save.
NSError *error;
if (![moc save:&error])
err(@"While saving changes to: %@, error: %@", activeElement.name, error);
// Update the UI.
dispatch_async(dispatch_get_main_queue(), ^{
[self updateAnimated:YES];
// Show new and old password.
if ([oldPassword length] && ![oldPassword isEqualToString:newPassword])
[self showAlertWithTitle:@"Password Changed!"
message:PearlString(@"The password for %@ has changed.\n\n"
@"IMPORTANT:\n"
@"Don't forget to update the site with your new password! "
@"Your old password was:\n"
@"%@", activeElement.name, oldPassword)];
});
}];
}];
}
- (void)activeElementDo:(void (^)(MPElementEntity *activeElement))task {
if (!_activeElementOID) {
task(nil);
return;
}
NSManagedObjectContext *moc = [MPAppDelegate managedObjectContextForThreadIfReady];
if (!moc) {
task(nil);
return;
}
NSError *error;
MPElementEntity *activeElement = (MPElementEntity *)[moc existingObjectWithID:_activeElementOID error:&error];
if (!activeElement)
err(@"Couldn't retrieve active element: %@", error);
task(activeElement);
}
- (IBAction)editPassword {
[self activeElementDo:^(MPElementEntity *activeElement) {
if (!(activeElement.type & MPElementTypeClassStored)) {
// Not of a type that supports editing the content.
err(@"Cannot edit content: Element is not stored: %@", activeElement.name);
return;
}
self.contentField.enabled = YES;
[self.contentField becomeFirstResponder];
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointEditPassword];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointEditPassword attributes:@{@"type" : activeElement.typeName,
@"version" : @(activeElement.version)}];
}];
}
- (IBAction)upgradePassword {
__block NSString *warning = nil;
[self activeElementDo:^(MPElementEntity *activeElement) {
warning = activeElement.type & MPElementTypeClassGenerated?
@"You are upgrading the site.\n\n"
@"This upgrade improves the site's compatibility with the latest version of Master Password.\n\n"
@"Your password will change and you will need to update your site's account."
:
@"You are upgrading the site.\n\n"
@"This upgrade improves the site's compatibility with the latest version of Master Password.";
}];
if (!warning)
return;
[self changeActiveElementWithWarning:warning
do:^BOOL(MPElementEntity *activeElement) {
inf(@"Explicitly migrating element: %@", activeElement);
[activeElement migrateExplicitly:YES];
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPCheckpointExplicitMigration];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointExplicitMigration
attributes:@{@"type" : activeElement.typeName,
@"version" : @(activeElement.version)}];
return YES;
}];
}
- (IBAction)searchOutdatedElements {
self.searchDisplayController.searchBar.selectedScopeButtonIndex = MPSearchScopeOutdated;
self.searchDisplayController.searchBar.searchResultsButtonSelected = YES;
[self.searchDisplayController.searchBar becomeFirstResponder];
}
- (IBAction)closeAlert {
[UIView animateWithDuration:0.3f animations:^{
self.alertContainer.alpha = 0;
} completion:^(BOOL finished) {
if (finished)
self.alertBody.text = nil;
}];
}
- (IBAction)closeOutdatedAlert {
[UIView animateWithDuration:0.3f animations:^{
self.outdatedAlertContainer.alpha = 0;
}];
}
- (IBAction)infoOutdatedAlert {
[self setHelpChapter:@"outdated"];
[self setHelpHidden:NO animated:YES];
[self closeOutdatedAlert];
self.suppressOutdatedAlert = NO;
}
- (IBAction)action:(id)sender {
[PearlSheet showSheetWithTitle:nil viewStyle:UIActionSheetStyleAutomatic
initSheet:nil
tappedButtonBlock:^(UIActionSheet *sheet, NSInteger buttonIndex) {
if (buttonIndex == [sheet cancelButtonIndex])
return;
switch (buttonIndex - [sheet firstOtherButtonIndex]) {
case 0: {
inf(@"Action: FAQ");
[self setHelpChapter:@"faq"];
[self setHelpHidden:NO animated:YES];
break;
}
case 1: {
inf(@"Action: Guide");
[[MPAppDelegate get] showGuide];
break;
}
case 2: {
inf(@"Action: Preferences");
[self performSegueWithIdentifier:@"MP_UserProfile" sender:self];
break;
}
case 3: {
inf(@"Action: Other Apps");
[self performSegueWithIdentifier:@"MP_OtherApps" sender:self];
break;
}
//#if defined(ADHOC) && defined(TESTFLIGHT_SDK_VERSION)
// case 4: {
// inf(@"Action: Feedback via TestFlight");
// [TestFlight openFeedbackView];
// break;
// }
// case 5:
//#else
case 4: {
inf(@"Action: Feedback via Mail");
[[MPAppDelegate get] showFeedbackWithLogs:YES forVC:self];
break;
}
case 5:
//#endif
{
inf(@"Action: Sign out");
[[MPAppDelegate get] signOutAnimated:YES];
break;
}
default: {
wrn(@"Unsupported action: %u", buttonIndex - [sheet firstOtherButtonIndex]);
break;
}
}
}
cancelTitle:[PearlStrings get].commonButtonCancel destructiveTitle:nil otherTitles:
@"FAQ", @"Tutorial", @"Preferences", @"Other Apps", @"Feedback", @"Sign Out",
nil];
}
- (MPElementType)selectedType {
return [self selectedElement].type;
}
- (MPElementEntity *)selectedElement {
__block MPElementEntity *selectedElement;
[self activeElementDo:^(MPElementEntity *activeElement) {
selectedElement = activeElement;
}];
return selectedElement;
}
- (void)didSelectType:(MPElementType)type {
[self changeActiveElementWithWarning:
@"You are about to change the type of this password.\n\n"
@"If you continue, the password for this site will change. "
@"You will need to update your account's old password to the new one."
do:^BOOL(MPElementEntity *activeElement){
if ([activeElement.algorithm classOfType:type] != activeElement.typeClass) {
// Type requires a different class of element. Recreate the element.
MPElementEntity *newElement = [NSEntityDescription insertNewObjectForEntityForName:[activeElement.algorithm classNameOfType:type]
inManagedObjectContext:activeElement.managedObjectContext];
newElement.name = activeElement.name;
newElement.user = activeElement.user;
newElement.uses = activeElement.uses;
newElement.lastUsed = activeElement.lastUsed;
newElement.version = activeElement.version;
newElement.loginName = activeElement.loginName;
[activeElement.managedObjectContext deleteObject:activeElement];
_activeElementOID = newElement.objectID;
activeElement = newElement;
}
activeElement.type = type;
[[NSNotificationCenter defaultCenter] postNotificationName:MPElementUpdatedNotification
object:activeElement.objectID];
return YES;
}];
}
- (void)didSelectElement:(MPElementEntity *)element {
if (!element)
return;
_activeElementOID = element.objectID;
[self closeAlert];
[self changeActiveElementWithoutWarningDo:^BOOL(MPElementEntity *activeElement) {
if ([activeElement use] == 1)
[self showAlertWithTitle:@"New Site" message:
PearlString(@"You've just created a password for %@.\n\n"
@"IMPORTANT:\n"
@"Go to %@ and set or change the password for your account to the password above.\n"
@"Do this right away: if you forget, you may have trouble remembering which password to use to log into the site later on.",
activeElement.name, activeElement.name)];
return YES;
}];
[self activeElementDo:^(MPElementEntity *activeElement) {
inf(@"Selected: %@", activeElement.name);
dbg(@"Element:\n%@", [activeElement debugDescription]);
if (![[MPiOSConfig get].typeTipShown boolValue])
[UIView animateWithDuration:0.5f animations:^{
self.typeTipContainer.alpha = 1;
} completion:^(BOOL finished) {
if (finished) {
[MPiOSConfig get].typeTipShown = PearlBool(YES);
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[UIView animateWithDuration:0.2f animations:^{
self.typeTipContainer.alpha = 0;
}];
});
}
}];
[self.searchDisplayController setActive:NO animated:YES];
self.searchDisplayController.searchBar.text = activeElement.name;
[[NSNotificationCenter defaultCenter] postNotificationName:MPElementUpdatedNotification object:activeElement.objectID];
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:PearlString(MPCheckpointUseType @"_%@", activeElement.typeShortName)];
#endif
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointUseType attributes:@{@"type" : activeElement.typeName,
@"version" : @(activeElement.version)}];
}];
[self updateAnimated:YES];
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
if (textField == self.contentField)
[self.contentField resignFirstResponder];
if (textField == self.loginNameField)
[self.loginNameField resignFirstResponder];
return YES;
}
- (void)textFieldDidEndEditing:(UITextField *)textField {
if (textField == self.contentField) {
self.contentField.enabled = NO;
__block BOOL abort = NO;
[self activeElementDo:^(MPElementEntity *activeElement) {
if (![activeElement isKindOfClass:[MPElementStoredEntity class]]) {
// Not of a type whose content can be edited.
err(@"Cannot update element content: Element is not stored: %@", activeElement.name);
abort = YES;
} else if ([((MPElementStoredEntity *)activeElement).content isEqual:self.contentField.text])
// Content hasn't changed.
abort = YES;
}];
if (abort)
return;
[self changeActiveElementWithoutWarningDo:^BOOL(MPElementEntity *activeElement) {
((MPElementStoredEntity *)activeElement).content = self.contentField.text;
return YES;
}];
}
if (textField == self.loginNameField) {
self.loginNameField.enabled = NO;
if (![[MPiOSConfig get].loginNameTipShown boolValue]) {
[self showLoginNameTip:@"Tap to copy or hold to edit."];
[MPiOSConfig get].loginNameTipShown = @YES;
}
[self changeActiveElementWithoutWarningDo:^BOOL(MPElementEntity *activeElement) {
if ([self.loginNameField.text length])
activeElement.loginName = self.loginNameField.text;
else
activeElement.loginName = nil;
return YES;
}];
}
}
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType {
if (navigationType == UIWebViewNavigationTypeLinkClicked) {
if ([[[request URL] query] isEqualToString:@"outdated"]) {
[self searchOutdatedElements];
return NO;
}
[[UIApplication sharedApplication] openURL:[request URL]];
return NO;
}
return YES;
}
@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>
#import "IASKAppSettingsViewController.h"
#import "MPTypeViewController.h"
@interface MPPreferencesViewController : UITableViewController<IASKSettingsDelegate, MPTypeDelegate>
@property (weak, nonatomic) IBOutlet UIScrollView *avatarsView;
@property (weak, nonatomic) IBOutlet UIButton *avatarTemplate;
@property (weak, nonatomic) IBOutlet UISwitch *savePasswordSwitch;
@property (weak, nonatomic) IBOutlet UITableViewCell *exportCell;
@property (weak, nonatomic) IBOutlet UITableViewCell *changeMPCell;
@property (weak, nonatomic) IBOutlet UILabel *defaultTypeLabel;
- (IBAction)didToggleSwitch:(UISwitch *)sender;
- (IBAction)settings:(id)sender;
@end

View File

@@ -0,0 +1,169 @@
//
// MPPreferencesViewController.m
// MasterPassword-iOS
//
// Created by Maarten Billemont on 04/06/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import <QuartzCore/QuartzCore.h>
#import "MPPreferencesViewController.h"
#import "MPAppDelegate.h"
#import "MPAppDelegate_Key.h"
#import "MPAppDelegate_Store.h"
@interface MPPreferencesViewController ()
@end
@implementation MPPreferencesViewController
- (void)viewDidLoad {
self.avatarTemplate.hidden = YES;
for (int a = 0; a < MPAvatarCount; ++a) {
UIButton *avatar = [self.avatarTemplate clone];
avatar.tag = a;
avatar.hidden = NO;
avatar.center = CGPointMake(
self.avatarTemplate.center.x * (a + 1) + self.avatarTemplate.bounds.size.width / 2 * a,
self.avatarTemplate.center.y);
[avatar setBackgroundImage:[UIImage imageNamed:PearlString(@"avatar-%d", a)]
forState:UIControlStateNormal];
[avatar setSelectionInSuperviewCandidate:YES isClearable:NO];
avatar.layer.cornerRadius = avatar.bounds.size.height / 2;
avatar.layer.shadowColor = [UIColor blackColor].CGColor;
avatar.layer.shadowOpacity = 1;
avatar.layer.shadowRadius = 5;
avatar.backgroundColor = [UIColor clearColor];
[avatar onHighlightOrSelect:^(BOOL highlighted, BOOL selected) {
if (highlighted || selected)
avatar.backgroundColor = self.avatarTemplate.backgroundColor;
else
avatar.backgroundColor = [UIColor clearColor];
} options:0];
[avatar onSelect:^(BOOL selected) {
if (selected) {
MPUserEntity *activeUser = [MPAppDelegate get].activeUser;
activeUser.avatar = (unsigned)avatar.tag;
[activeUser saveContext];
}
} options:0];
avatar.selected = (a == [MPAppDelegate get].activeUser.avatar);
}
[super viewDidLoad];
}
- (void)viewWillAppear:(BOOL)animated {
inf(@"Preferences will appear");
[self.avatarsView autoSizeContent];
[self.avatarsView enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
if (subview.tag && ((UIControl *)subview).selected) {
[self.avatarsView setContentOffset:CGPointMake(subview.center.x - self.avatarsView.bounds.size.width / 2, 0) animated:animated];
}
} recurse:NO];
self.savePasswordSwitch.on = [MPAppDelegate get].activeUser.saveKey;
self.defaultTypeLabel.text = [[MPAppDelegate get].key.algorithm shortNameOfType:[MPAppDelegate get].activeUser.defaultType];
[super viewWillAppear:animated];
}
- (void)viewDidAppear:(BOOL)animated {
[[LocalyticsSession sharedLocalyticsSession] tagScreen:@"Preferences"];
[super viewDidAppear:animated];
}
- (void)viewWillDisappear:(BOOL)animated {
inf(@"Preferences will disappear");
[super viewWillDisappear:animated];
}
- (BOOL)shouldAutorotate {
return NO;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationPortrait;
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([[segue identifier] isEqualToString:@"MP_ChooseType"])
((MPTypeViewController *)[segue destinationViewController]).delegate = self;
}
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];
if (cell == self.exportCell)
[[MPAppDelegate get] export];
else
if (cell == self.changeMPCell) {
MPUserEntity *activeUser = [MPAppDelegate get].activeUser;
[[MPAppDelegate get] changeMasterPasswordFor:activeUser didResetBlock:nil];
[activeUser saveContext];
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
#pragma mark - IASKSettingsDelegate
- (void)settingsViewControllerDidEnd:(IASKAppSettingsViewController *)sender {
while ([self.navigationController.viewControllers containsObject:sender])
[self.navigationController popViewControllerAnimated:YES];
}
#pragma mark - MPTypeDelegate
- (void)didSelectType:(MPElementType)type {
MPUserEntity *activeUser = [MPAppDelegate get].activeUser;
activeUser.defaultType = type;
[activeUser saveContext];
self.defaultTypeLabel.text = [[MPAppDelegate get].key.algorithm shortNameOfType:activeUser.defaultType];
}
- (MPElementType)selectedType {
return [MPAppDelegate get].activeUser.defaultType;
}
#pragma mark - IBActions
- (IBAction)didToggleSwitch:(UISwitch *)sender {
MPUserEntity *activeUser = [MPAppDelegate get].activeUser;
if ((activeUser.saveKey = sender.on))
[[MPAppDelegate get] storeSavedKeyFor:activeUser];
else
[[MPAppDelegate get] forgetSavedKeyFor:activeUser];
[activeUser saveContext];
}
- (IBAction)settings:(UIBarButtonItem *)sender {
IASKAppSettingsViewController *vc = [IASKAppSettingsViewController new];
vc.showDoneButton = NO;
[self.navigationController pushViewController:vc animated:YES];
[[LocalyticsSession sharedLocalyticsSession] tagScreen:@"Settings"];
}
@end

View File

@@ -0,0 +1,30 @@
//
// MPTypeViewController.h
// MasterPassword
//
// Created by Maarten Billemont on 27/11/11.
// Copyright (c) 2011 Lyndir. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "MPEntities.h"
@protocol MPTypeDelegate<NSObject>
@required
- (void)didSelectType:(MPElementType)type;
- (MPElementType)selectedType;
@optional
- (MPElementEntity *)selectedElement;
@end
@interface MPTypeViewController : UITableViewController
@property (nonatomic, weak) id<MPTypeDelegate> delegate;
@property (weak, nonatomic) IBOutlet UIView *recommendedTipContainer;
@end

View File

@@ -0,0 +1,173 @@
//
// MPTypeViewController.m
// MasterPassword
//
// Created by Maarten Billemont on 27/11/11.
// Copyright (c) 2011 Lyndir. All rights reserved.
//
#import "MPTypeViewController.h"
#import "MPAppDelegate.h"
#import "MPAppDelegate_Store.h"
#import "MPAppDelegate_Key.h"
@interface MPTypeViewController ()
- (MPElementType)typeAtIndexPath:(NSIndexPath *)indexPath;
@end
@implementation MPTypeViewController
#pragma mark - View lifecycle
- (void)viewWillAppear:(BOOL)animated {
inf(@"Type selection will appear");
self.recommendedTipContainer.alpha = 0;
[super viewWillAppear:animated];
}
- (void)viewDidAppear:(BOOL)animated {
if ([[MPiOSConfig get].firstRun boolValue])
[UIView animateWithDuration:animated? 0.3f: 0 animations:^{
self.recommendedTipContainer.alpha = 1;
} completion:^(BOOL finished) {
if (finished) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[UIView animateWithDuration:0.2f animations:^{
self.recommendedTipContainer.alpha = 0;
}];
});
}
}];
[[LocalyticsSession sharedLocalyticsSession] tagScreen:@"Type Selection"];
[super viewDidAppear:animated];
}
- (void)viewDidLoad {
self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"ui_background"]];
[super viewDidLoad];
}
- (void)viewWillDisappear:(BOOL)animated {
inf(@"Type selection will disappear");
[super viewWillDisappear:animated];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath];
MPElementEntity *selectedElement = nil;
if ([self.delegate respondsToSelector:@selector(selectedElement)])
selectedElement = [self.delegate selectedElement];
MPElementType cellType = [self typeAtIndexPath:indexPath];
MPElementType selectedType = selectedElement? selectedElement.type: [self.delegate selectedType];
cell.selected = (selectedType == cellType);
if (cellType != NSNotFound && cellType & MPElementTypeClassGenerated) {
[(UITextField *) [cell viewWithTag:2] setText:@"..."];
NSManagedObjectID *selectedElementOID = [selectedElement objectID];
[MPAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *moc) {
NSError *error;
MPElementGeneratedEntity *selectedElement_ = (MPElementGeneratedEntity *) [moc existingObjectWithID:selectedElementOID error:&error];
if (!selectedElement_) {
err(@"Failed to retrieve element for password preview: %@", error);
return;
}
MPElementGeneratedEntity *cellElement = [NSEntityDescription insertNewObjectForEntityForName:[MPAlgorithmDefault classNameOfType:cellType]
inManagedObjectContext:moc];
cellElement.type = cellType;
cellElement.name = selectedElement_.name;
cellElement.user = selectedElement_.user;
cellElement.loginName = selectedElement_.loginName;
cellElement.version = MPAlgorithmDefaultVersion;
NSString *typeContent = [cellElement.algorithm generateContentForElement:cellElement usingKey:[MPAppDelegate get].key];
dispatch_async(dispatch_get_main_queue(), ^{
[(UITextField *) [[tableView cellForRowAtIndexPath:indexPath] viewWithTag:2] setText:typeContent];
});
}];
}
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
assert(self.navigationController.topViewController == self);
MPElementType type = [self typeAtIndexPath:indexPath];
if (type == NSNotFound)
// Selected a non-type row.
return;
[self.delegate didSelectType:type];
[self.navigationController popViewControllerAnimated:YES];
}
- (MPElementType)typeAtIndexPath:(NSIndexPath *)indexPath {
switch (indexPath.section) {
case 0: {
// Generated
switch (indexPath.row) {
case 0:
return (MPElementType)NSNotFound;
case 1:
return MPElementTypeGeneratedMaximum;
case 2:
return MPElementTypeGeneratedLong;
case 3:
return MPElementTypeGeneratedMedium;
case 4:
return MPElementTypeGeneratedBasic;
case 5:
return MPElementTypeGeneratedShort;
case 6:
return MPElementTypeGeneratedPIN;
case 7:
return (MPElementType)NSNotFound;
default: {
Throw(@"Unsupported row: %d, when selecting generated element type.", indexPath.row);
}
}
}
case 1: {
// Stored
switch (indexPath.row) {
case 0:
return (MPElementType)NSNotFound;
case 1:
return MPElementTypeStoredPersonal;
case 2:
return MPElementTypeStoredDevicePrivate;
case 3:
return (MPElementType)NSNotFound;
default: {
Throw(@"Unsupported row: %d, when selecting stored element type.", indexPath.row);
}
}
}
default:
Throw(@"Unsupported section: %d, when selecting element type.", indexPath.section);
}
}
@end

View File

@@ -0,0 +1,40 @@
//
// MBUnlockViewController.h
// MasterPassword
//
// Created by Maarten Billemont on 22/02/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface MPUnlockViewController : UIViewController<UITextFieldDelegate, UIScrollViewDelegate, UIWebViewDelegate>
@property (weak, nonatomic) IBOutlet UIImageView *spinner;
@property (weak, nonatomic) IBOutlet UILabel *passwordFieldLabel;
@property (weak, nonatomic) IBOutlet UITextField *passwordField;
@property (weak, nonatomic) IBOutlet UIView *passwordView;
@property (weak, nonatomic) IBOutlet UIScrollView *avatarsView;
@property (weak, nonatomic) IBOutlet UILabel *nameLabel;
@property (weak, nonatomic) IBOutlet UILabel *oldNameLabel;
@property (weak, nonatomic) IBOutlet UIButton *avatarTemplate;
@property (weak, nonatomic) IBOutlet UIView *createPasswordTipView;
@property (weak, nonatomic) IBOutlet UILabel *tip;
@property (weak, nonatomic) IBOutlet UIView *passwordTipView;
@property (weak, nonatomic) IBOutlet UILabel *passwordTipLabel;
@property (weak, nonatomic) IBOutlet UIView *wordWall;
@property (strong, nonatomic) IBOutlet UILongPressGestureRecognizer *targetedUserActionGesture;
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *loadingUsersIndicator;
@property (weak, nonatomic) IBOutlet UIView *uiContainer;
@property (weak, nonatomic) IBOutlet UIWebView *newsView;
@property (nonatomic, strong) UIColor *avatarShadowColor;
- (IBAction)targetedUserAction:(UILongPressGestureRecognizer *)sender;
- (IBAction)facebook:(UIButton *)sender;
- (IBAction)twitter:(UIButton *)sender;
- (IBAction)google:(UIButton *)sender;
- (IBAction)mail:(UIButton *)sender;
- (IBAction)add:(UIButton *)sender;
@end

View File

@@ -0,0 +1,942 @@
//
// MBUnlockViewController.m
// MasterPassword
//
// Created by Maarten Billemont on 22/02/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import <QuartzCore/QuartzCore.h>
#import <Social/Social.h>
#import "MPUnlockViewController.h"
#import "MPAppDelegate.h"
#import "MPAppDelegate_Key.h"
#import "MPAppDelegate_Store.h"
@interface MPUnlockViewController ()
@property (strong, nonatomic) NSMutableDictionary *avatarToUserOID;
@property (nonatomic) BOOL wordWallAnimating;
@property (nonatomic, strong) NSArray *wordList;
@end
@implementation MPUnlockViewController {
NSManagedObjectID *_selectedUserOID;
}
- (void)initializeAvatarAlert:(UIAlertView *)alert forUser:(MPUserEntity *)user {
UIScrollView *alertAvatarScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(12, 30, 260, 150)];
alertAvatarScrollView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
[alertAvatarScrollView flashScrollIndicatorsContinuously];
[alert addSubview:alertAvatarScrollView];
CGPoint selectedOffset = CGPointZero;
for (int a = 0; a < MPAvatarCount; ++a) {
UIButton *avatar = [self.avatarTemplate cloneAddedTo:alertAvatarScrollView];
avatar.tag = a;
avatar.hidden = NO;
avatar.center = CGPointMake(
(20 + self.avatarTemplate.bounds.size.width / 2) * (a + 1) + self.avatarTemplate.bounds.size.width / 2 * a,
20 + self.avatarTemplate.bounds.size.height / 2);
[avatar setBackgroundImage:[UIImage imageNamed:PearlString(@"avatar-%d", a)] forState:UIControlStateNormal];
[avatar setSelectionInSuperviewCandidate:YES isClearable:NO];
avatar.layer.cornerRadius = avatar.bounds.size.height / 2;
avatar.layer.shadowColor = [UIColor blackColor].CGColor;
avatar.layer.shadowOpacity = 1;
avatar.layer.shadowRadius = 5;
avatar.backgroundColor = [UIColor clearColor];
[avatar onHighlightOrSelect:^(BOOL highlighted, BOOL selected) {
if (highlighted || selected)
avatar.backgroundColor = self.avatarTemplate.backgroundColor;
else
avatar.backgroundColor = [UIColor clearColor];
} options:0];
[avatar onSelect:^(BOOL selected) {
if (selected)
[user.managedObjectContext performBlock:^{
user.avatar = (unsigned)avatar.tag;
}];
} options:0];
avatar.selected = (a == user.avatar);
if (avatar.selected)
selectedOffset = CGPointMake(avatar.center.x - alertAvatarScrollView.bounds.size.width / 2, 0);
}
[alertAvatarScrollView autoSizeContent];
[alertAvatarScrollView setContentOffset:selectedOffset animated:YES];
}
- (void)initializeConfirmationAlert:(UIAlertView *)alert forUser:(MPUserEntity *)user {
UIView *container = [[UIView alloc] initWithFrame:CGRectMake(12, 70, 260, 110)];
[alert addSubview:container];
UIButton *alertAvatar = [self.avatarTemplate cloneAddedTo:container];
alertAvatar.center = CGPointMake(130, 55);
alertAvatar.hidden = NO;
alertAvatar.layer.shadowColor = [UIColor blackColor].CGColor;
alertAvatar.layer.shadowOpacity = 1;
alertAvatar.layer.shadowRadius = 5;
alertAvatar.backgroundColor = [UIColor clearColor];
[alertAvatar setBackgroundImage:[UIImage imageNamed:PearlString(@"avatar-%d", user.avatar)] forState:UIControlStateNormal];
UILabel *alertNameLabel = [self.nameLabel cloneAddedTo:container];
alertNameLabel.center = alertAvatar.center;
alertNameLabel.text = user.name;
alertNameLabel.bounds = CGRectSetHeight(alertNameLabel.bounds,
[alertNameLabel.text sizeWithFont:self.nameLabel.font
constrainedToSize:CGSizeMake(alertNameLabel.bounds.size.width - 10,
100)
lineBreakMode:self.nameLabel.lineBreakMode].height);
alertNameLabel.layer.cornerRadius = 5;
alertNameLabel.backgroundColor = [UIColor blackColor];
}
- (BOOL)shouldAutorotate {
return NO;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationPortrait;
}
- (void)viewDidLoad {
[self.newsView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.masterpasswordapp.com/news.html"]]];
self.avatarToUserOID = [NSMutableDictionary dictionaryWithCapacity:3];
[self.avatarsView addGestureRecognizer:self.targetedUserActionGesture];
self.avatarsView.decelerationRate = UIScrollViewDecelerationRateFast;
self.avatarsView.clipsToBounds = NO;
self.nameLabel.layer.cornerRadius = 5;
self.avatarTemplate.hidden = YES;
self.spinner.alpha = 0;
self.passwordTipView.alpha = 0;
self.createPasswordTipView.alpha = 0;
NSMutableArray *wordListLines = [NSMutableArray arrayWithCapacity:27413];
[[[NSString alloc] initWithData:[NSData dataWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"dictionary" withExtension:@"lst"]]
encoding:NSUTF8StringEncoding] enumerateLinesUsingBlock:^(NSString *line, BOOL *stop) {
[wordListLines addObject:line];
}];
self.wordList = wordListLines;
self.wordWall.alpha = 0;
[self.wordWall enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
UILabel *wordLabel = (UILabel *)subview;
[self initializeWordLabel:wordLabel];
} recurse:NO];
[[NSNotificationCenter defaultCenter] addObserverForName:UbiquityManagedStoreDidChangeNotification object:nil queue:nil
usingBlock:^(NSNotification *note) {
[self updateUsers];
}];
[[NSNotificationCenter defaultCenter] addObserverForName:UbiquityManagedStoreDidImportChangesNotification object:nil queue:nil
usingBlock:^(NSNotification *note) {
[self updateUsers];
}];
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillResignActiveNotification object:nil queue:nil
usingBlock:^(NSNotification *note) {
self.uiContainer.alpha = 0;
}];
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification object:nil queue:nil
usingBlock:^(NSNotification *note) {
[self updateLayoutAnimated:NO allowScroll:NO completion:nil];
[UIView animateWithDuration:1 animations:^{
self.uiContainer.alpha = 1;
}];
}];
[self updateLayoutAnimated:NO allowScroll:YES completion:nil];
[super viewDidLoad];
}
- (void)viewWillAppear:(BOOL)animated {
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:UIStatusBarAnimationSlide];
inf(@"Lock screen will appear");
_selectedUserOID = nil;
[self updateUsers];
self.uiContainer.alpha = 0;
[super viewWillAppear:animated];
}
- (void)viewDidAppear:(BOOL)animated {
dbg(@"Lock screen did appear: %@", animated? @"animated": @"not animated");
if (!animated)
[[self findTargetedAvatar] setSelected:YES];
else
[self updateLayoutAnimated:YES allowScroll:YES completion:nil];
[UIView animateWithDuration:0.5 animations:^{
self.uiContainer.alpha = 1;
}];
[[LocalyticsSession sharedLocalyticsSession] tagScreen:@"Unlock"];
[super viewDidAppear:animated];
}
- (void)viewWillDisappear:(BOOL)animated {
inf(@"Lock screen will disappear");
[[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationSlide];
[super viewWillDisappear:animated];
}
- (void)updateUsers {
NSManagedObjectContext *moc = [MPAppDelegate managedObjectContextForThreadIfReady];
if (!moc) {
self.tip.text = @"Loading...";
[self.loadingUsersIndicator startAnimating];
return;
}
self.tip.text = @"Tap and hold to delete or reset.";
[self.loadingUsersIndicator stopAnimating];
__block NSArray *users = nil;
[moc performBlockAndWait:^{
NSError *error = nil;
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPUserEntity class])];
fetchRequest.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"lastUsed" ascending:NO]];
users = [moc executeFetchRequest:fetchRequest error:&error];
if (!users)
err(@"Failed to load users: %@", error);
}];
// Clean up avatars.
for (UIView *subview in [self.avatarsView subviews])
if ([[self.avatarToUserOID allKeys] containsObject:[NSValue valueWithNonretainedObject:subview]])
// This subview is a former avatar.
[subview removeFromSuperview];
[self.avatarToUserOID removeAllObjects];
// Create avatars.
[moc performBlockAndWait:^{
for (MPUserEntity *user in users)
[self setupAvatar:[self.avatarTemplate clone] forUser:user];
[self setupAvatar:[self.avatarTemplate clone] forUser:nil];
}];
// Scroll view's content changed, update its content size.
[self.avatarsView autoSizeContentIgnoreHidden:YES ignoreInvisible:YES limitPadding:NO ignoreSubviews:nil];
[self updateLayoutAnimated:YES allowScroll:YES completion:nil];
}
- (UIButton *)setupAvatar:(UIButton *)avatar forUser:(MPUserEntity *)user {
avatar.center = CGPointMake(avatar.center.x + [self.avatarToUserOID count] * 160, avatar.center.y);
avatar.hidden = NO;
avatar.layer.cornerRadius = avatar.bounds.size.height / 2;
avatar.layer.shadowColor = [UIColor blackColor].CGColor;
avatar.layer.shadowOpacity = 1;
avatar.layer.shadowRadius = 20;
avatar.layer.masksToBounds = NO;
avatar.backgroundColor = [UIColor clearColor];
avatar.tag = user.avatar;
[avatar setBackgroundImage:[UIImage imageNamed:PearlString(@"avatar-%u", user.avatar)]
forState:UIControlStateNormal];
[avatar setSelectionInSuperviewCandidate:YES isClearable:YES];
[avatar onHighlightOrSelect:^(BOOL highlighted, BOOL selected) {
if (highlighted || selected)
avatar.backgroundColor = self.avatarTemplate.backgroundColor;
else
avatar.backgroundColor = [UIColor clearColor];
} options:0];
[avatar onSelect:^(BOOL selected) {
if (selected) {
if ((self.selectedUser = user))
[self didToggleUserSelection];
else
[self didSelectNewUserAvatar:avatar];
} else {
self.selectedUser = nil;
[self didToggleUserSelection];
}
} options:0];
[self.avatarToUserOID setObject:NilToNSNull([user objectID]) forKey:[NSValue valueWithNonretainedObject:avatar]];
if ([_selectedUserOID isEqual:[user objectID]]) {
self.selectedUser = user;
avatar.selected = YES;
}
return avatar;
}
- (void)didToggleUserSelection {
MPUserEntity *selectedUser = self.selectedUser;
if (!selectedUser)
[self.passwordField resignFirstResponder];
else
if ([[MPAppDelegate get] signInAsUser:selectedUser usingMasterPassword:nil]) {
[self dismissViewControllerAnimated:YES completion:nil];
return;
}
[self updateLayoutAnimated:YES allowScroll:YES completion:^(BOOL finished) {
if (self.selectedUser)
[self.passwordField becomeFirstResponder];
}];
}
- (void)didSelectNewUserAvatar:(UIButton *)newUserAvatar {
[MPAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *moc) {
MPUserEntity *newUser = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPUserEntity class])
inManagedObjectContext:moc];
[self showNewUserNameAlertFor:newUser completion:^(BOOL finished) {
newUserAvatar.selected = NO;
if (finished)
[newUser saveContext];
}];
}];
}
- (void)showNewUserNameAlertFor:(MPUserEntity *)newUser completion:(void (^)(BOOL finished))completion {
dispatch_async(dispatch_get_main_queue(), ^{
[PearlAlert showAlertWithTitle:@"Enter Your Name"
message:nil viewStyle:UIAlertViewStylePlainTextInput
initAlert:^(UIAlertView *alert, UITextField *firstField) {
firstField.autocapitalizationType = UITextAutocapitalizationTypeWords;
firstField.keyboardType = UIKeyboardTypeAlphabet;
firstField.text = newUser.name;
firstField.placeholder = @"eg. Robert Lee Mitchell";
firstField.enablesReturnKeyAutomatically = YES;
}
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
if (buttonIndex == [alert cancelButtonIndex]) {
completion(NO);
return;
}
if (![alert textFieldAtIndex:0].text.length) {
[PearlAlert showAlertWithTitle:@"Name Is Required" message:nil viewStyle:UIAlertViewStyleDefault initAlert:nil
tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
[newUser.managedObjectContext performBlock:^{
[self showNewUserNameAlertFor:newUser completion:completion];
}];
} cancelTitle:@"Try Again" otherTitles:nil];
return;
}
// Save
[newUser.managedObjectContext performBlock:^{
newUser.name = [alert textFieldAtIndex:0].text;
[self showNewUserAvatarAlertFor:newUser completion:completion];
}];
}
cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:[PearlStrings get].commonButtonSave, nil];
});
}
- (void)showNewUserAvatarAlertFor:(MPUserEntity *)newUser completion:(void (^)(BOOL finished))completion {
[PearlAlert showAlertWithTitle:@"Choose Your Avatar"
message:@"\n\n\n\n\n\n" viewStyle:UIAlertViewStyleDefault
initAlert:^(UIAlertView *_alert, UITextField *_firstField) {
[self initializeAvatarAlert:_alert forUser:newUser];
}
tappedButtonBlock:^(UIAlertView *_alert, NSInteger _buttonIndex) {
// Okay
[newUser.managedObjectContext performBlock:^{
[self showNewUserConfirmationAlertFor:newUser completion:completion];
}];
} cancelTitle:nil otherTitles:[PearlStrings get].commonButtonOkay, nil];
}
- (void)showNewUserConfirmationAlertFor:(MPUserEntity *)newUser completion:(void (^)(BOOL finished))completion {
[PearlAlert showAlertWithTitle:@"Is this correct?"
message:
@"Please double-check your name.\n"
@"\n\n\n\n\n\n"
viewStyle:UIAlertViewStyleDefault
initAlert:^void(UIAlertView *__alert, UITextField *__firstField) {
[self initializeConfirmationAlert:__alert forUser:newUser];
}
tappedButtonBlock:^void(UIAlertView *__alert, NSInteger __buttonIndex) {
if (__buttonIndex == [__alert cancelButtonIndex]) {
[newUser.managedObjectContext performBlock:^{
[self showNewUserNameAlertFor:newUser completion:completion];
}];
return;
}
// Confirm
completion(YES);
self.selectedUser = newUser;
[self updateUsers];
}
cancelTitle:@"Change" otherTitles:@"Confirm", nil];
}
- (void)updateLayoutAnimated:(BOOL)animated allowScroll:(BOOL)allowScroll completion:(void (^)(BOOL finished))completion {
if (animated) {
self.oldNameLabel.text = self.nameLabel.text;
self.oldNameLabel.alpha = 1;
self.nameLabel.alpha = 0;
[UIView animateWithDuration:0.5f animations:^{
[self updateLayoutAnimated:NO allowScroll:allowScroll completion:nil];
self.oldNameLabel.alpha = 0;
self.nameLabel.alpha = 1;
} completion:^(BOOL finished) {
if (completion)
completion(finished);
}];
return;
}
// Lay out password entry and user selection views.
if (self.selectedUser && !self.passwordView.alpha) {
// User was just selected.
self.passwordView.alpha = 1;
self.avatarsView.center = CGPointMake(160, 180);
self.avatarsView.scrollEnabled = NO;
self.nameLabel.center = CGPointMake(160, 94);
self.nameLabel.backgroundColor = [UIColor blackColor];
self.oldNameLabel.center = self.nameLabel.center;
self.avatarShadowColor = [UIColor whiteColor];
} else
if (!self.selectedUser && self.passwordView.alpha == 1) {
// User was just deselected.
self.passwordField.text = nil;
self.passwordView.alpha = 0;
self.avatarsView.center = CGPointMake(160, 310);
self.avatarsView.scrollEnabled = YES;
self.nameLabel.center = CGPointMake(160, 296);
self.nameLabel.backgroundColor = [UIColor clearColor];
self.oldNameLabel.center = self.nameLabel.center;
self.avatarShadowColor = [UIColor lightGrayColor];
}
// Lay out the word wall.
if (!self.selectedUser || self.selectedUser.keyID) {
self.passwordFieldLabel.text = @"Enter your master password:";
self.wordWall.alpha = 0;
self.createPasswordTipView.alpha = 0;
self.wordWallAnimating = NO;
} else {
self.passwordFieldLabel.text = @"Create your master password:";
if (!self.wordWallAnimating) {
self.wordWallAnimating = YES;
self.wordWall.alpha = 1;
self.createPasswordTipView.alpha = 1;
dispatch_async(dispatch_get_main_queue(), ^{
// Jump out of our UIView animation block.
[self beginWordWallAnimation];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[UIView animateWithDuration:1 animations:^{
self.createPasswordTipView.alpha = 0;
}];
});
}
}
// Lay out user targeting.
MPUserEntity *targetedUser = self.selectedUser;
UIButton *selectedAvatar = [self avatarForUser:self.selectedUser];
UIButton *targetedAvatar = selectedAvatar;
if (!targetedAvatar) {
targetedAvatar = [self findTargetedAvatar];
targetedUser = [self userForAvatar:targetedAvatar];
}
[self.avatarsView enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
if (![[self.avatarToUserOID allKeys] containsObject:[NSValue valueWithNonretainedObject:subview]])
// This subview is not one of the user avatars.
return;
UIButton *avatar = (UIButton *)subview;
BOOL isTargeted = avatar == targetedAvatar;
avatar.userInteractionEnabled = isTargeted;
avatar.alpha = isTargeted? 1: self.selectedUser? 0.1: 0.4;
[self updateAvatarShadowColor:avatar isTargeted:isTargeted];
} recurse:NO];
if (allowScroll) {
CGPoint targetContentOffset = CGPointMake(MAX(0, targetedAvatar.center.x - self.avatarsView.bounds.size.width / 2),
self.avatarsView.contentOffset.y);
if (!CGPointEqualToPoint(self.avatarsView.contentOffset, targetContentOffset))
[self.avatarsView setContentOffset:targetContentOffset animated:animated];
}
// Lay out user name label.
self.nameLabel.text = targetedAvatar? (targetedUser? targetedUser.name: @"New User"): nil;
self.nameLabel.bounds = CGRectSetHeight(self.nameLabel.bounds,
[self.nameLabel.text sizeWithFont:self.nameLabel.font
constrainedToSize:CGSizeMake(self.nameLabel.bounds.size.width - 10, 100)
lineBreakMode:self.nameLabel.lineBreakMode].height);
self.oldNameLabel.bounds = self.nameLabel.bounds;
if (completion)
completion(YES);
}
- (void)beginWordWallAnimation {
[self.wordWall enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
UILabel *wordLabel = (UILabel *)subview;
if (wordLabel.frame.origin.x < -self.wordWall.frame.size.width / 3) {
wordLabel.frame = CGRectSetX(wordLabel.frame, wordLabel.frame.origin.x + self.wordWall.frame.size.width);
[self initializeWordLabel:wordLabel];
}
} recurse:NO];
if (self.wordWallAnimating)
[UIView animateWithDuration:15 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
[self.wordWall enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
UILabel *wordLabel = (UILabel *)subview;
wordLabel.frame = CGRectSetX(wordLabel.frame, wordLabel.frame.origin.x - self.wordWall.frame.size.width / 3);
} recurse:NO];
} completion:^(BOOL finished) {
if (finished)
[self beginWordWallAnimation];
}];
}
- (void)initializeWordLabel:(UILabel *)wordLabel {
wordLabel.alpha = 0.05 + (random() % 35) / 100.0F;
wordLabel.text = [self.wordList objectAtIndex:(NSUInteger)random() % [self.wordList count]];
}
- (void)setPasswordTip:(NSString *)string {
if (string.length)
self.passwordTipLabel.text = string;
[UIView animateWithDuration:0.3f animations:^{
self.passwordTipView.alpha = string.length? 1: 0;
}];
}
- (void)tryMasterPassword {
if (!self.selectedUser)
// No user selected, can't try sign-in.
return;
[self setSpinnerActive:YES];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
BOOL unlocked = [[MPAppDelegate get] signInAsUser:self.selectedUser usingMasterPassword:self.passwordField.text];
dispatch_async(dispatch_get_main_queue(), ^{
if (unlocked)
[self dismissViewControllerAnimated:YES completion:nil];
else {
if (self.passwordField.text.length)
[self setPasswordTip:@"Incorrect password."];
[self setSpinnerActive:NO];
}
});
});
}
- (UIButton *)findTargetedAvatar {
CGFloat xOfMiddle = self.avatarsView.contentOffset.x + self.avatarsView.bounds.size.width / 2;
return (UIButton *)[PearlUIUtils viewClosestTo:CGPointMake(xOfMiddle, self.avatarsView.contentOffset.y)
ofArray:self.avatarsView.subviews];
}
- (UIButton *)avatarForUser:(MPUserEntity *)user {
NSManagedObjectID *userOID = [user objectID];
__block UIButton *avatar = nil;
if (userOID)
[self.avatarToUserOID enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if ([obj isEqual:userOID])
avatar = [key nonretainedObjectValue];
}];
return avatar;
}
- (MPUserEntity *)userForAvatar:(UIButton *)avatar {
NSManagedObjectContext *moc = [MPAppDelegate managedObjectContextForThreadIfReady];
if (!moc)
return nil;
NSManagedObjectID *userOID = NSNullToNil([self.avatarToUserOID objectForKey:[NSValue valueWithNonretainedObject:avatar]]);
if (!userOID)
return nil;
NSError *error;
MPUserEntity *user = (MPUserEntity *)[moc existingObjectWithID:userOID error:&error];
if (!user)
err(@"Failed retrieving user for avatar: %@", error);
return user;
}
- (void)setSpinnerActive:(BOOL)active {
PearlMainThread(^{
CABasicAnimation *rotate = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
rotate.toValue = [NSNumber numberWithDouble:2 * M_PI];
rotate.duration = 5.0;
if (active) {
rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
rotate.fromValue = [NSNumber numberWithFloat: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"];
[UIView animateWithDuration:0.3f animations:^{
self.spinner.alpha = active? 1: 0;
if (active)
[self avatarForUser:self.selectedUser].backgroundColor = [UIColor clearColor];
else
[self avatarForUser:self.selectedUser].backgroundColor = self.avatarTemplate.backgroundColor;
}];
});
}
- (void)updateAvatarShadowColor:(UIButton *)avatar isTargeted:(BOOL)targeted {
if (targeted) {
if (![avatar.layer animationForKey:@"targetedShadow"]) {
CABasicAnimation *toShadowColorAnimation = [CABasicAnimation animationWithKeyPath:@"shadowColor"];
toShadowColorAnimation.toValue = (__bridge id)(avatar.selected? self.avatarTemplate.backgroundColor
: [UIColor whiteColor]).CGColor;
toShadowColorAnimation.beginTime = 0.0f;
toShadowColorAnimation.duration = 0.5f;
toShadowColorAnimation.fillMode = kCAFillModeForwards;
CABasicAnimation *toShadowOpacityAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"];
toShadowOpacityAnimation.toValue = PearlFloat(0.2);
toShadowOpacityAnimation.duration = 0.5f;
CABasicAnimation *pulseShadowOpacityAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"];
pulseShadowOpacityAnimation.fromValue = PearlFloat(0.2);
pulseShadowOpacityAnimation.toValue = PearlFloat(0.6);
pulseShadowOpacityAnimation.beginTime = 0.5f;
pulseShadowOpacityAnimation.duration = 2.0f;
pulseShadowOpacityAnimation.autoreverses = YES;
pulseShadowOpacityAnimation.repeatCount = MAXFLOAT;
CAAnimationGroup *group = [[CAAnimationGroup alloc] init];
group.animations = @[toShadowColorAnimation, toShadowOpacityAnimation, pulseShadowOpacityAnimation];
group.duration = MAXFLOAT;
[avatar.layer removeAnimationForKey:@"inactiveShadow"];
[avatar.layer addAnimation:group forKey:@"targetedShadow"];
}
} else {
if ([avatar.layer animationForKey:@"targetedShadow"]) {
CABasicAnimation *toShadowColorAnimation = [CABasicAnimation animationWithKeyPath:@"shadowColor"];
toShadowColorAnimation.toValue = (__bridge id)[UIColor blackColor].CGColor;
toShadowColorAnimation.duration = 0.5f;
CABasicAnimation *toShadowOpacityAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"];
toShadowOpacityAnimation.toValue = PearlFloat(1);
toShadowOpacityAnimation.duration = 0.5f;
CAAnimationGroup *group = [[CAAnimationGroup alloc] init];
group.animations = @[toShadowColorAnimation, toShadowOpacityAnimation];
group.duration = 0.5f;
[avatar.layer removeAnimationForKey:@"targetedShadow"];
[avatar.layer addAnimation:group forKey:@"inactiveShadow"];
}
}
}
#pragma mark - UITextFieldDelegate
- (void)textFieldDidBeginEditing:(UITextField *)textField {
[self setPasswordTip:nil];
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[textField resignFirstResponder];
[self setSpinnerActive:YES];
if (self.selectedUser.keyID)
[self tryMasterPassword];
else
[PearlAlert showAlertWithTitle:@"New Master Password"
message:@"Please confirm the spelling of this new master password."
viewStyle:UIAlertViewStyleSecureTextInput
initAlert:nil tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
[self setSpinnerActive:NO];
if (buttonIndex == [alert cancelButtonIndex])
return;
if (![[alert textFieldAtIndex:0].text isEqualToString:textField.text]) {
[PearlAlert showAlertWithTitle:@"Incorrect Master Password"
message:
@"The password you entered doesn't match with the master password you tried to use. "
@"You've probably mistyped one of them.\n\n"
@"Give it another try."
viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:nil
cancelTitle:[PearlStrings get].commonButtonOkay otherTitles:nil];
return;
}
[self tryMasterPassword];
}
cancelTitle:[PearlStrings get].commonButtonCancel
otherTitles:[PearlStrings get].commonButtonContinue, nil];
return YES;
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset {
CGFloat xOfMiddle = targetContentOffset->x + scrollView.bounds.size.width / 2;
UIButton *middleAvatar = (UIButton *)[PearlUIUtils viewClosestTo:CGPointMake(xOfMiddle, targetContentOffset->y)
ofArray:scrollView.subviews];
*targetContentOffset = CGPointMake(middleAvatar.center.x - scrollView.bounds.size.width / 2, targetContentOffset->y);
[self updateLayoutAnimated:NO allowScroll:NO completion:nil];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[self updateLayoutAnimated:YES allowScroll:YES completion:nil];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
[self updateLayoutAnimated:NO allowScroll:NO completion:nil];
}
#pragma mark - UIWebViewDelegate
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType {
if (navigationType == UIWebViewNavigationTypeLinkClicked) {
[[UIApplication sharedApplication] openURL:[request URL]];
return NO;
}
return YES;
}
#pragma mark - IBActions
- (IBAction)targetedUserAction:(UILongPressGestureRecognizer *)sender {
if (sender.state != UIGestureRecognizerStateBegan)
return;
if (self.selectedUser)
return;
MPUserEntity *targetedUser = [self userForAvatar:[self findTargetedAvatar]];
if (!targetedUser)
return;
[PearlSheet showSheetWithTitle:targetedUser.name
viewStyle:UIActionSheetStyleBlackTranslucent
initSheet:nil tappedButtonBlock:^(UIActionSheet *sheet, NSInteger buttonIndex) {
if (buttonIndex == [sheet cancelButtonIndex])
return;
if (buttonIndex == [sheet destructiveButtonIndex]) {
[targetedUser.managedObjectContext performBlock:^{
[targetedUser.managedObjectContext deleteObject:targetedUser];
[targetedUser saveContext];
dispatch_async(dispatch_get_main_queue(), ^{
[self updateUsers];
});
}];
return;
}
if (buttonIndex == [sheet firstOtherButtonIndex])
[targetedUser.managedObjectContext performBlock:^{
[[MPAppDelegate get] changeMasterPasswordFor:targetedUser didResetBlock:^{
dispatch_async(dispatch_get_main_queue(), ^{
[[self avatarForUser:targetedUser] setSelected:YES];
});
}];
}];
} cancelTitle:[PearlStrings get].commonButtonCancel
destructiveTitle:@"Delete User" otherTitles:@"Reset Password", nil];
}
- (IBAction)facebook:(UIButton *)sender {
if (![SLComposeViewController isAvailableForServiceType:SLServiceTypeFacebook]) {
[PearlAlert showAlertWithTitle:@"Facebook Not Enabled" message:@"To send tweets, configure Facebook from Settings."
viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:nil cancelTitle:nil otherTitles:@"OK", nil];
return;
}
SLComposeViewController *vc = [SLComposeViewController composeViewControllerForServiceType:SLServiceTypeFacebook];
[vc setInitialText:@"I've started doing passwords properly thanks to Master Password for iOS."];
[vc addImage:[UIImage imageNamed:@"iTunesArtwork-Rounded"]];
[vc addURL:[NSURL URLWithString:@"http://masterpasswordapp.com"]];
[self presentViewController:vc animated:YES completion:nil];
}
- (IBAction)twitter:(UIButton *)sender {
if (![SLComposeViewController isAvailableForServiceType:SLServiceTypeTwitter]) {
[PearlAlert showAlertWithTitle:@"Twitter Not Enabled" message:@"To send tweets, configure Twitter from Settings."
viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:nil cancelTitle:nil otherTitles:@"OK", nil];
return;
}
SLComposeViewController *vc = [SLComposeViewController composeViewControllerForServiceType:SLServiceTypeTwitter];
[vc setInitialText:@"I've started doing passwords properly thanks to Master Password for iOS."];
[vc addImage:[UIImage imageNamed:@"iTunesArtwork-Rounded"]];
[vc addURL:[NSURL URLWithString:@"http://masterpasswordapp.com"]];
[self presentViewController:vc animated:YES completion:nil];
}
- (IBAction)google:(UIButton *)sender {
id<GPPShareBuilder> shareDialog = [[MPAppDelegate get].googlePlus shareDialog];
[[[shareDialog setURLToShare:[NSURL URLWithString:@"http://masterpasswordapp.com"]]
setPrefillText:@"I've started doing passwords properly thanks to Master Password for iOS."] open];
}
- (IBAction)mail:(UIButton *)sender {
[[MPAppDelegate get] showFeedbackWithLogs:NO forVC:self];
}
- (IBAction)add:(UIButton *)sender {
[PearlSheet showSheetWithTitle:@"Follow Master Password" viewStyle:UIActionSheetStyleBlackTranslucent
initSheet:nil tappedButtonBlock:^(UIActionSheet *sheet, NSInteger buttonIndex) {
if (buttonIndex == [sheet cancelButtonIndex])
return;
if (buttonIndex == [sheet firstOtherButtonIndex]) {
// Google+
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://plus.google.com/116256327773442623984/about"]];
return;
}
if (buttonIndex == [sheet firstOtherButtonIndex] + 1) {
// Facebook
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://www.facebook.com/masterpasswordapp"]];
return;
}
if (buttonIndex == [sheet firstOtherButtonIndex] + 2) {
// Twitter
UIApplication *application = [UIApplication sharedApplication];
for (NSString *candidate in @[
@"twitter://user?screen_name=%@", // Twitter
@"tweetbot:///user_profile/%@", // TweetBot
@"echofon:///user_timeline?%@", // Echofon
@"twit:///user?screen_name=%@", // Twittelator Pro
@"x-seesmic://twitter_profile?twitter_screen_name=%@", // Seesmic
@"x-birdfeed://user?screen_name=%@", // Birdfeed
@"tweetings:///user?screen_name=%@", // Tweetings
@"simplytweet:?link=http://twitter.com/%@", // SimplyTweet
@"icebird://user?screen_name=%@", // IceBird
@"fluttr://user/%@", // Fluttr
@"http://twitter.com/%@"]) {
NSURL *url = [NSURL URLWithString:PearlString(candidate, @"master_password")];
if ([application canOpenURL:url]) {
[application openURL:url];
break;
}
}
return;
}
if (buttonIndex == [sheet firstOtherButtonIndex] + 3) {
// Mailing List
[PearlEMail sendEMailTo:@"masterpassword-join@lists.lyndir.com" subject:@"Subscribe"
body:@"Press 'Send' now to subscribe to the Master Password mailing list.\n\n"
@"You'll be kept up-to-date on the evolution of and discussions revolving Master Password."];
return;
}
if (buttonIndex == [sheet firstOtherButtonIndex] + 4) {
// GitHub
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/Lyndir/MasterPassword"]];
return;
}
} cancelTitle:[PearlStrings get].commonButtonCancel
destructiveTitle:nil otherTitles:@"Google+", @"Facebook", @"Twitter", @"Mailing List", @"GitHub", nil];
}
#pragma mark - Core Data
- (MPUserEntity *)selectedUser {
if (!_selectedUserOID)
return nil;
NSManagedObjectContext *moc = [MPAppDelegate managedObjectContextForThreadIfReady];
if (!moc)
return nil;
NSError *error;
MPUserEntity *selectedUser = (MPUserEntity *)[moc existingObjectWithID:_selectedUserOID error:&error];
if (!selectedUser)
err(@"Failed to retrieve selected user: %@", error);
return selectedUser;
}
- (void)setSelectedUser:(MPUserEntity *)selectedUser {
_selectedUserOID = selectedUser.objectID;
}
@end

View File

@@ -0,0 +1,20 @@
//
// MPConfig.h
// MasterPassword
//
// Created by Maarten Billemont on 02/01/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "MPConfig.h"
@interface MPiOSConfig : MPConfig
@property (nonatomic, retain) NSNumber *helpHidden;
@property (nonatomic, retain) NSNumber *siteInfoHidden;
@property (nonatomic, retain) NSNumber *showQuickStart;
@property (nonatomic, retain) NSNumber *actionsTipShown;
@property (nonatomic, retain) NSNumber *typeTipShown;
@property (nonatomic, retain) NSNumber *loginNameTipShown;
@end

View File

@@ -0,0 +1,28 @@
//
// MPConfig.m
// MasterPassword
//
// Created by Maarten Billemont on 02/01/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
@implementation MPiOSConfig
@dynamic sendInfo, helpHidden, siteInfoHidden, showQuickStart, actionsTipShown, typeTipShown, loginNameTipShown;
- (id)init {
if (!(self = [super init]))
return self;
[self.defaults registerDefaults:@{ NSStringFromSelector(@selector(helpHidden)): @NO,
NSStringFromSelector(@selector(siteInfoHidden)): @YES,
NSStringFromSelector(@selector(showQuickStart)): @YES,
NSStringFromSelector(@selector(iTunesID)): @"510296984",
NSStringFromSelector(@selector(actionsTipShown)): PearlBoolNot(self.firstRun),
NSStringFromSelector(@selector(typeTipShown)): PearlBoolNot(self.firstRun),
NSStringFromSelector(@selector(loginNameTipShown)): PearlBool(NO)}];
return self;
}
@end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
<?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>CFBundleDisplayName</key>
<string>M. Password</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>mpsites</string>
</array>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>Master Password sites</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>com.lyndir.lhunath.MasterPassword.sites</string>
</array>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIconFiles</key>
<array>
<string>Icon.png</string>
<string>Icon@2x.png</string>
<string>Icon-72.png</string>
<string>Icon-72@2x.png</string>
</array>
<key>CFBundleIcons</key>
<dict>
<key>CFBundlePrimaryIcon</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>Icon.png</string>
<string>Icon@2x.png</string>
<string>Icon-72.png</string>
<string>Icon-72@2x.png</string>
</array>
<key>UIPrerenderedIcon</key>
<true/>
</dict>
</dict>
<key>CFBundleIdentifier</key>
<string>com.lyndir.lhunath.${PRODUCT_NAME:rfc1034identifier}</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>[auto]</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.lyndir.lhunath.MasterPassword</string>
<key>CFBundleURLSchemes</key>
<array>
<string>com.lyndir.lhunath.MasterPassword</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>[auto]</string>
<key>FacebookAppID</key>
<string>257095917745237</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2011-2013 Lyndir. All rights reserved.</string>
<key>ReplacementFonts</key>
<dict>
<key>AmericanTypewriter-Bold</key>
<string>SourceCodePro-Black</string>
<key>AmericanTypewriter-Light</key>
<string>SourceCodePro-ExtraLight</string>
<key>Futura-CondensedExtraBold</key>
<string>Exo-ExtraBold</string>
<key>Futura-Medium</key>
<string>Exo</string>
</dict>
<key>UIAppFonts</key>
<array>
<string>Exo-Bold.otf</string>
<string>Exo-ExtraBold.otf</string>
<string>Exo-Regular.otf</string>
<string>SourceCodePro-Black.otf</string>
<string>SourceCodePro-ExtraLight.otf</string>
</array>
<key>UIMainStoryboardFile</key>
<string>MainStoryboard_iPhone</string>
<key>UIPrerenderedIcon</key>
<true/>
<key>UIStatusBarHidden</key>
<true/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string>
<key>UIStatusBarTintParameters</key>
<dict>
<key>UINavigationBar</key>
<dict>
<key>Style</key>
<string>UIBarStyleDefault</string>
<key>TintColor</key>
<dict>
<key>Blue</key>
<real>0.42745098039215684</real>
<key>Green</key>
<real>0.39215686274509803</real>
<key>Red</key>
<real>0.37254901960784315</real>
</dict>
<key>Translucent</key>
<false/>
</dict>
</dict>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeDescription</key>
<string>Master Password sites</string>
<key>UTTypeIdentifier</key>
<string>com.lyndir.lhunath.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,28 @@
//
// Prefix header for all source files of the 'MasterPassword' target in the 'MasterPassword' project
//
#import <Availability.h>
#ifndef __IPHONE_5_0
#warning "This project uses features only available in iOS SDK 5.0 and later."
#endif
#import "Pearl-Prefix.pch"
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#import "TestFlight.h"
#import <Crashlytics/Crashlytics.h>
#import "LocalyticsSession.h"
#define LOCALYTICS 1
#define CRASHLYTICS 1
#import "MPTypes.h"
#import "MPiOSConfig.h"
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:MasterPassword-iOS.xcodeproj">
</FileRef>
</Workspace>

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>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0460"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "NO"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
BuildableName = "MasterPassword.app"
BlueprintName = "MasterPassword"
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
buildConfiguration = "Debug">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
BuildableName = "MasterPassword.app"
BlueprintName = "MasterPassword"
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
</BuildableReference>
</MacroExpansion>
</TestAction>
<LaunchAction
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
buildConfiguration = "AppStore"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
allowLocationSimulation = "YES">
<BuildableProductRunnable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
BuildableName = "MasterPassword.app"
BlueprintName = "MasterPassword"
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
buildConfiguration = "AppStore"
debugDocumentVersioning = "YES">
<BuildableProductRunnable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
BuildableName = "MasterPassword.app"
BlueprintName = "MasterPassword"
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "AppStore"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0460"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
BuildableName = "MasterPassword.app"
BlueprintName = "MasterPassword"
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
buildConfiguration = "Debug">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DA3EF17815A47744003ABF4E"
BuildableName = "Tests.octest"
BlueprintName = "Tests"
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
BuildableName = "MasterPassword.app"
BlueprintName = "MasterPassword"
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
</BuildableReference>
</MacroExpansion>
</TestAction>
<LaunchAction
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
buildConfiguration = "Debug"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
allowLocationSimulation = "YES">
<BuildableProductRunnable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
BuildableName = "MasterPassword.app"
BlueprintName = "MasterPassword"
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
<AdditionalOption
key = "NSZombieEnabled"
value = "YES"
isEnabled = "YES">
</AdditionalOption>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
buildConfiguration = "Debug"
debugDocumentVersioning = "YES">
<BuildableProductRunnable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
BuildableName = "MasterPassword.app"
BlueprintName = "MasterPassword"
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "AdHoc"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,17 @@
<?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.developer.ubiquity-container-identifiers</key>
<array>
<string>$(TeamIdentifierPrefix)com.lyndir.lhunath.MasterPassword</string>
<string>$(TeamIdentifierPrefix)com.lyndir.lhunath.MasterPassword.shared</string>
</array>
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)com.lyndir.lhunath.MasterPassword.shared</string>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.lyndir.lhunath.MasterPassword</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,95 @@
<?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>PreferenceSpecifiers</key>
<array>
<dict>
<key>FooterText</key>
<string>If you&apos;re experiencing problems, enabling this will send us details that can help diagnose and resolve them. You will remain completely anonymous amongst the other thousands of users. No sensitive information is ever sent.</string>
<key>Title</key>
<string></string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>DefaultValue</key>
<string>[auto]</string>
<key>Title</key>
<string>Version</string>
<key>Key</key>
<string>unset</string>
<key>Type</key>
<string>PSTitleValueSpecifier</string>
</dict>
<dict>
<key>DefaultValue</key>
<string>[auto]</string>
<key>Title</key>
<string>Build</string>
<key>Key</key>
<string>unset</string>
<key>Type</key>
<string>PSTitleValueSpecifier</string>
</dict>
<dict>
<key>DefaultValue</key>
<string>[auto]</string>
<key>Title</key>
<string>Copyright</string>
<key>Type</key>
<string>PSTitleValueSpecifier</string>
<key>Key</key>
<string>unset</string>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Send Diagnostic Info</string>
<key>Key</key>
<string>sendInfo</string>
<key>DefaultValue</key>
<false/>
</dict>
<dict>
<key>FooterText</key>
<string>When enabled, closing the application will not log out the user. Similar to your phone&apos;s SIM lock, you only need to log in once after powering on.</string>
<key>Title</key>
<string>Master Password</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>DefaultValue</key>
<false/>
<key>Key</key>
<string>rememberLogin</string>
<key>Title</key>
<string>Stay logged in</string>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
</dict>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
<key>Title</key>
<string></string>
<key>FooterText</key>
<string>Makes your sites available to all your Apple devices. This also works as a way of automatically keeping a back-up of your sites. Apple cannot see any of your passwords.</string>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>iCloud</string>
<key>Key</key>
<string>iCloud</string>
<key>DefaultValue</key>
<true/>
</dict>
</array>
<key>StringsTable</key>
<string>Root</string>
</dict>
</plist>

View File

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

View File

@@ -0,0 +1,16 @@
//
// main.m
// MasterPassword
//
// Created by Maarten Billemont on 24/11/11.
// Copyright (c) 2011 Lyndir. All rights reserved.
//
#import "MPAppDelegate.h"
int main(int argc, char *argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([MPAppDelegate class]));
}
}