2
0

User support.

[ADDED]     Support for multiple users, each their own master password.
This commit is contained in:
Maarten Billemont
2012-06-04 11:27:02 +02:00
parent 3de9a0c67e
commit ba299d4674
80 changed files with 1835 additions and 586 deletions

View File

@@ -10,12 +10,13 @@
@interface MPAppDelegate_Shared (Key)
- (void)loadStoredKey;
- (void)loadSavedKey;
- (IBAction)signOut:(id)sender;
- (BOOL)tryMasterPassword:(NSString *)tryPassword;
- (void)updateKey:(NSData *)key;
- (void)forgetKey;
- (BOOL)tryMasterPassword:(NSString *)tryPassword forUser:(MPUserEntity *)user;
- (void)storeSavedKey;
- (void)forgetSavedKey;
- (void)unsetKey;
- (NSData *)keyWithLength:(NSUInteger)keyLength;

View File

@@ -8,67 +8,48 @@
#import "MPConfig.h"
#import "MPAppDelegate_Key.h"
#import "MPElementEntity.h"
@implementation MPAppDelegate_Shared (Key)
static NSDictionary *keyQuery() {
static NSDictionary *keyQuery(MPUserEntity *user) {
static NSDictionary *MPKeyQuery = nil;
if (!MPKeyQuery)
MPKeyQuery = [PearlKeyChain createQueryForClass:kSecClassGenericPassword
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
@"Saved Master Password", (__bridge id)kSecAttrService,
@"default", (__bridge id)kSecAttrAccount,
nil]
matches:nil];
return MPKeyQuery;
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
@"Saved Master Password", (__bridge id)kSecAttrService,
user.name, (__bridge id)kSecAttrAccount,
nil]
matches:nil];
}
static NSDictionary *keyIDQuery() {
- (void)forgetSavedKey {
static NSDictionary *MPKeyIDQuery = nil;
if (!MPKeyIDQuery)
MPKeyIDQuery = [PearlKeyChain createQueryForClass:kSecClassGenericPassword
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
@"Master Password Check", (__bridge id)kSecAttrService,
@"default", (__bridge id)kSecAttrAccount,
nil]
matches:nil];
return MPKeyIDQuery;
}
- (void)forgetKey {
inf(@"Deleting key and ID from keychain.");
if ([PearlKeyChain deleteItemForQuery:keyQuery()] != errSecItemNotFound)
if ([PearlKeyChain deleteItemForQuery:keyQuery(self.activeUser)] != errSecItemNotFound) {
inf(@"Removed key from keychain.");
if ([PearlKeyChain deleteItemForQuery:keyIDQuery()] != errSecItemNotFound)
inf(@"Removed key ID from keychain.");
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeyForgotten object:self];
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeyForgotten object:self];
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPTestFlightCheckpointMPForgotten];
[TestFlight passCheckpoint:MPTestFlightCheckpointMPForgotten];
#endif
}
}
- (IBAction)signOut:(id)sender {
[MPConfig get].saveKey = [NSNumber numberWithBool:NO];
[self updateKey:nil];
[self forgetSavedKey];
[self unsetKey];
}
- (void)loadStoredKey {
- (void)loadSavedKey {
if ([[MPConfig get].saveKey boolValue]) {
// Key is stored in keychain. Load it.
[self updateKey:[PearlKeyChain dataOfItemForQuery:keyQuery()]];
if ([self.activeUser.saveKey boolValue]) {
// Key should be saved in keychain. Load it.
self.key = [PearlKeyChain dataOfItemForQuery:keyQuery(self.activeUser)];
inf(@"Looking for key in keychain: %@.", self.key? @"found": @"missing");
if (self.key)
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeySet object:self];
} else {
// Key should not be stored in keychain. Delete it.
if ([PearlKeyChain deleteItemForQuery:keyQuery()] != errSecItemNotFound)
if ([PearlKeyChain deleteItemForQuery:keyQuery(self.activeUser)] != errSecItemNotFound)
inf(@"Removed key from keychain.");
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPTestFlightCheckpointMPUnstored];
@@ -76,20 +57,19 @@ static NSDictionary *keyIDQuery() {
}
}
- (BOOL)tryMasterPassword:(NSString *)tryPassword {
- (BOOL)tryMasterPassword:(NSString *)tryPassword forUser:(MPUserEntity *)user {
if (![tryPassword length])
return NO;
NSData *tryKey = keyForPassword(tryPassword);
NSData *tryKeyID = keyIDForKey(tryKey);
NSData *keyID = [PearlKeyChain dataOfItemForQuery:keyIDQuery()];
inf(@"Key ID known? %@.", keyID? @"YES": @"NO");
if (keyID)
inf(@"Key ID known? %@.", user.keyID? @"YES": @"NO");
if (user.keyID)
// A key ID is known -> a password is set.
// Make sure the user's entered password matches it.
if (![keyID isEqual:tryKeyID]) {
wrn(@"Key ID mismatch. Expected: %@, answer: %@.", [keyID encodeHex], [tryKeyID encodeHex]);
if (![user.keyID isEqual:tryKeyID]) {
wrn(@"Key ID mismatch. Expected: %@, answer: %@.", [user.keyID encodeHex], [tryKeyID encodeHex]);
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPTestFlightCheckpointMPMismatch];
@@ -101,55 +81,45 @@ static NSDictionary *keyIDQuery() {
[TestFlight passCheckpoint:MPTestFlightCheckpointMPEntered];
#endif
[self updateKey:tryKey];
if (self.key != tryKey) {
self.key = tryKey;
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeySet object:self];
}
self.activeUser = user;
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPTestFlightCheckpointSetKey];
#endif
return YES;
}
- (void)updateKey:(NSData *)key {
- (void)storeSavedKey {
if (self.key != key) {
self.key = key;
if ([self.activeUser.saveKey boolValue]) {
NSData *existingKey = [PearlKeyChain dataOfItemForQuery:keyQuery(self.activeUser)];
if (key)
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeySet object:self];
else
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeyUnset object:self];
}
if (self.key) {
self.keyID = keyIDForKey(self.key);
NSData *existingKeyID = [PearlKeyChain dataOfItemForQuery:keyIDQuery()];
if (![existingKeyID isEqualToData:self.keyID]) {
inf(@"Updating key ID in keychain.");
[PearlKeyChain addOrUpdateItemForQuery:keyIDQuery()
if (![existingKey isEqualToData:self.key]) {
inf(@"Updating key in keychain.");
[PearlKeyChain addOrUpdateItemForQuery:keyQuery(self.activeUser)
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
self.keyID, (__bridge id)kSecValueData,
self.key, (__bridge id)kSecValueData,
#if TARGET_OS_IPHONE
kSecAttrAccessibleWhenUnlocked, (__bridge id)kSecAttrAccessible,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly, (__bridge id)kSecAttrAccessible,
#endif
nil]];
}
if ([[MPConfig get].saveKey boolValue]) {
NSData *existingKey = [PearlKeyChain dataOfItemForQuery:keyQuery()];
if (![existingKey isEqualToData:self.key]) {
inf(@"Updating key in keychain.");
[PearlKeyChain addOrUpdateItemForQuery:keyQuery()
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
self.key, (__bridge id)kSecValueData,
#if TARGET_OS_IPHONE
kSecAttrAccessibleWhenUnlocked, (__bridge id)kSecAttrAccessible,
#endif
nil]];
}
}
#ifdef TESTFLIGHT_SDK_VERSION
[TestFlight passCheckpoint:MPTestFlightCheckpointSetKey];
#endif
}
}
- (void)unsetKey {
self.key = nil;
self.activeUser = nil;
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeyUnset object:self];
}
- (NSData *)keyWithLength:(NSUInteger)keyLength {
return [self.key subdataWithRange:NSMakeRange(0, MIN(keyLength, self.key.length))];

View File

@@ -6,14 +6,16 @@
// 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) NSData *key;
@property (strong, nonatomic) NSData *keyID;
+ (MPAppDelegate_Shared *)get;

View File

@@ -11,7 +11,7 @@
@implementation MPAppDelegate_Shared
@synthesize key;
@synthesize keyID;
@synthesize activeUser;
+ (MPAppDelegate_Shared *)get {

View File

@@ -27,7 +27,6 @@ typedef enum {
- (UbiquityStoreManager *)storeManager;
- (void)saveContext;
- (void)printStore;
- (MPImportResult)importSites:(NSString *)importedSitesString withPassword:(NSString *)password
askConfirmation:(BOOL(^)(NSUInteger importCount, NSUInteger deleteCount))confirmation;

View File

@@ -7,7 +7,7 @@
//
#import "MPAppDelegate_Store.h"
#import "MPElementEntity.h"
#import "MPEntities.h"
#import "MPConfig.h"
@implementation MPAppDelegate_Shared (Store)
@@ -57,7 +57,7 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
}
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
// Start loading the store.
[self storeManager];
@@ -69,7 +69,7 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
isReady = [self storeManager].isReady;
});
}
assert([self storeManager].isReady);
return [self storeManager].persistentStoreCoordinator;
}];
@@ -134,51 +134,6 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
}];
}
- (void)printStore {
if (![self managedObjectModel] || ![self managedObjectContext]) {
trc(@"Not printing store: store not initialized.");
return;
}
[self.managedObjectContext performBlock:^{
trc(@"=== All entities ===");
for(NSEntityDescription *entity in [[self managedObjectModel] entities]) {
NSFetchRequest *request = [NSFetchRequest new];
[request setEntity:entity];
NSError *error;
NSArray *results = [[self managedObjectContext] executeFetchRequest:request error:&error];
for(NSManagedObject *o in results) {
if ([o isKindOfClass:[MPElementEntity class]]) {
MPElementEntity *e = (MPElementEntity *)o;
trc(@"For descriptor: %@, found: %@: %@ (%@)", entity.name, [o class], e.name, e.keyID);
} else {
trc(@"For descriptor: %@, found: %@", entity.name, [o class]);
}
}
}
trc(@"---");
if ([MPAppDelegate_Shared get].keyID) {
trc(@"=== Known sites ===");
NSFetchRequest *fetchRequest = [[self managedObjectModel]
fetchRequestFromTemplateWithName:@"MPElements"
substitutionVariables:[NSDictionary dictionaryWithObjectsAndKeys:
@"", @"query",
[MPAppDelegate_Shared get].keyID, @"keyID",
nil]];
[fetchRequest setSortDescriptors:
[NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]]];
NSError *error = nil;
for (MPElementEntity *e in [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error]) {
trc(@"Found site: %@ (%@): %@", e.name, e.keyID, e);
}
trc(@"---");
} else
trc(@"Not printing sites: master password not set.");
}];
}
#pragma mark - UbiquityStoreManagerDelegate
- (NSManagedObjectContext *)managedObjectContextForUbiquityStoreManager:(UbiquityStoreManager *)usm {
@@ -278,7 +233,8 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
if (!headerPattern || !sitePattern)
return MPImportResultInternalError;
NSString *keyIDHex = nil;
NSString *keyIDHex = nil, *userName = nil;
MPUserEntity *user = nil;
BOOL headerStarted = NO, headerEnded = NO;
NSArray *importedSiteLines = [importedSitesString componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
NSMutableSet *elementsToDelete = [NSMutableSet set];
@@ -307,6 +263,13 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
NSTextCheckingResult *headerElements = [[headerPattern matchesInString:importedSiteLine options:0 range:NSMakeRange(0, [importedSiteLine length])] lastObject];
NSString *key = [importedSiteLine substringWithRange:[headerElements rangeAtIndex:1]];
NSString *value = [importedSiteLine substringWithRange:[headerElements rangeAtIndex:2]];
if ([key isEqualToString:@"User Name"]) {
userName = value;
NSFetchRequest *userFetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPUserEntity class])];
userFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@", userName];
user = [[self.managedObjectContext executeFetchRequest:fetchRequest error:&error] lastObject];
}
if ([key isEqualToString:@"Key ID"]) {
if (![(keyIDHex = value) isEqualToString:[keyIDForPassword(password) encodeHex]])
return MPImportResultInvalidPassword;
@@ -316,7 +279,7 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
}
if (!headerEnded)
continue;
if (!keyIDHex)
if (!keyIDHex || ![userName length])
return MPImportResultMalformedInput;
if (![importedSiteLine length])
continue;
@@ -334,15 +297,17 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
NSString *exportContent = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:5]];
// Find existing site.
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@ AND keyID == %@", name, keyIDHex];
NSArray *existingSites = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
if (error)
err(@"Couldn't search existing sites: %@", error);
if (!existingSites)
return MPImportResultInternalError;
[elementsToDelete addObjectsFromArray:existingSites];
[importedSiteElements addObject:[NSArray arrayWithObjects:lastUsed, uses, type, name, exportContent, nil]];
if (user) {
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@ AND user == %@", name, user];
NSArray *existingSites = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
if (error)
err(@"Couldn't search existing sites: %@", error);
if (!existingSites)
return MPImportResultInternalError;
[elementsToDelete addObjectsFromArray:existingSites];
[importedSiteElements addObject:[NSArray arrayWithObjects:lastUsed, uses, type, name, exportContent, nil]];
}
}
// Ask for confirmation to import these sites.
@@ -357,6 +322,11 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
[self saveContext];
// Import new sites.
if (!user) {
user = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPUserEntity class]) inManagedObjectContext:self.managedObjectContext];
user.name = userName;
user.keyID = [keyIDHex decodeHex];
}
for (NSArray *siteElements in importedSiteElements) {
NSDate *lastUsed = [rfc3339DateFormatter dateFromString:[siteElements objectAtIndex:0]];
NSInteger uses = [[siteElements objectAtIndex:1] integerValue];
@@ -365,14 +335,13 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
NSString *exportContent = [siteElements objectAtIndex:4];
// Create new site.
inf(@"Importing site: name=%@, lastUsed=%@, uses=%d, type=%u, keyID=%@", name, lastUsed, uses, type, keyIDHex);
MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:ClassNameFromMPElementType(type)
inManagedObjectContext:self.managedObjectContext];
element.name = name;
element.keyID = [keyIDHex decodeHex];
element.type = type;
element.uses = uses;
element.lastUsed = [lastUsed timeIntervalSinceReferenceDate];
element.user = user;
element.type = [NSNumber numberWithUnsignedInteger:type];
element.uses = [NSNumber numberWithUnsignedInteger:uses];
element.lastUsed = lastUsed;
if ([exportContent length])
[element importContent:exportContent];
}
@@ -399,7 +368,8 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
[export appendFormat:@"# \n"];
[export appendFormat:@"##\n"];
[export appendFormat:@"# Version: %@\n", [PearlInfoPlist get].CFBundleVersion];
[export appendFormat:@"# Key ID: %@\n", [self.keyID encodeHex]];
[export appendFormat:@"# User Name: %@\n", self.activeUser.name];
[export appendFormat:@"# Key ID: %@\n", [self.activeUser.keyID encodeHex]];
[export appendFormat:@"# Date: %@\n", [rfc3339DateFormatter stringFromDate:[NSDate date]]];
if (showPasswords)
[export appendFormat:@"# Passwords: VISIBLE\n"];
@@ -411,17 +381,9 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
[export appendFormat:@"# used used type name\tpassword\n"];
// Sites.
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"keyID == %@", self.keyID];
__autoreleasing NSError *error = nil;
NSArray *elements = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
if (error)
err(@"Error fetching sites for export: %@", error);
for (MPElementEntity *element in elements) {
NSTimeInterval lastUsed = element.lastUsed;
int16_t uses = element.uses;
for (MPElementEntity *element in self.activeUser.elements) {
NSDate *lastUsed = element.lastUsed;
NSNumber *uses = element.uses;
MPElementType type = (unsigned)element.type;
NSString *name = element.name;
NSString *content = nil;
@@ -435,7 +397,7 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
}
[export appendFormat:@"%@ %8d %8d %20s\t%@\n",
[rfc3339DateFormatter stringFromDate:[NSDate dateWithTimeIntervalSinceReferenceDate:lastUsed]], uses, type, [name cStringUsingEncoding:NSUTF8StringEncoding], content? content: @""];
[rfc3339DateFormatter stringFromDate:lastUsed], uses, type, [name cStringUsingEncoding:NSUTF8StringEncoding], content? content: @""];
}
#ifdef TESTFLIGHT_SDK_VERSION

View File

@@ -8,8 +8,7 @@
@interface MPConfig : PearlConfig
@property (nonatomic, retain) NSNumber *saveKey;
@property (nonatomic, retain) NSNumber *rememberKey;
@property (nonatomic, retain) NSNumber *rememberLogin;
@property (nonatomic, retain) NSNumber *iCloud;
@property (nonatomic, retain) NSNumber *iCloudDecided;

View File

@@ -10,7 +10,7 @@
#import "MPAppDelegate.h"
@implementation MPConfig
@dynamic saveKey, rememberKey, iCloud, iCloudDecided;
@dynamic rememberLogin, iCloud, iCloudDecided;
- (id)init {
@@ -20,8 +20,7 @@
[self.defaults registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(askForReviews)),
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(saveKey)),
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(rememberKey)),
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(rememberLogin)),
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(iCloud)),
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(iCloudDecided)),
nil]];

View File

@@ -1,27 +1,23 @@
//
// MPElementEntity.h
// MasterPassword
// MasterPassword-iOS
//
// Created by Maarten Billemont on 02/01/12.
// Created by Maarten Billemont on 04/06/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
@class MPUserEntity;
@interface MPElementEntity : NSManagedObject
@property (nonatomic, retain) NSString *name;
@property (nonatomic, retain) NSData *keyID;
@property (nonatomic, assign) int16_t type;
@property (nonatomic, assign) int16_t uses;
@property (nonatomic, assign) NSTimeInterval lastUsed;
@property (nonatomic, retain, readonly) id content;
- (int16_t)use;
- (NSString *)exportContent;
- (void)importContent:(NSString *)content;
@property (nonatomic, retain) id content;
@property (nonatomic, retain) NSDate * lastUsed;
@property (nonatomic, retain) NSString * name;
@property (nonatomic, retain) NSNumber * type;
@property (nonatomic, retain) NSNumber * uses;
@property (nonatomic, retain) MPUserEntity *user;
@end

View File

@@ -1,51 +1,22 @@
//
// MPElementEntity.m
// MasterPassword
// MasterPassword-iOS
//
// Created by Maarten Billemont on 02/01/12.
// Created by Maarten Billemont on 04/06/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "MPElementEntity.h"
#import "MPUserEntity.h"
@implementation MPElementEntity
@dynamic content;
@dynamic lastUsed;
@dynamic name;
@dynamic keyID;
@dynamic type;
@dynamic uses;
@dynamic lastUsed;
- (int16_t)use {
self.lastUsed = [[NSDate date] timeIntervalSinceReferenceDate];
return ++self.uses;
}
- (id)content {
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Content implementation missing." userInfo:nil];
}
- (NSString *)exportContent {
return nil;
}
- (void)importContent:(NSString *)content {
}
- (NSString *)description {
return PearlString(@"%@:%@", [self class], [self name]);
}
- (NSString *)debugDescription {
return PearlString(@"{%@: name=%@, keyID=%@, type=%d, uses=%d, lastUsed=%@}",
NSStringFromClass([self class]), self.name, self.keyID, self.type, self.uses, self.lastUsed);
}
@dynamic user;
@end

View File

@@ -1,8 +1,8 @@
//
// MPElementGeneratedEntity.h
// MasterPassword
// MasterPassword-iOS
//
// Created by Maarten Billemont on 16/01/12.
// Created by Maarten Billemont on 04/06/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
@@ -13,6 +13,6 @@
@interface MPElementGeneratedEntity : MPElementEntity
@property (nonatomic, assign) int32_t counter;
@property (nonatomic, retain) NSNumber * counter;
@end

View File

@@ -1,31 +1,16 @@
//
// MPElementGeneratedEntity.m
// MasterPassword
// MasterPassword-iOS
//
// Created by Maarten Billemont on 16/01/12.
// Created by Maarten Billemont on 04/06/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "MPElementGeneratedEntity.h"
#import "MPAppDelegate.h"
#import "MPAppDelegate_Key.h"
@implementation MPElementGeneratedEntity
@dynamic counter;
- (id)content {
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 MPCalculateContent((unsigned)self.type, self.name, [MPAppDelegate get].key, self.counter);
}
@end

View File

@@ -1,8 +1,8 @@
//
// MPElementStoredEntity.h
// MasterPassword
// MasterPassword-iOS
//
// Created by Maarten Billemont on 02/01/12.
// Created by Maarten Billemont on 04/06/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
@@ -13,6 +13,6 @@
@interface MPElementStoredEntity : MPElementEntity
@property (nonatomic, retain, readwrite) id content;
@property (nonatomic, retain) id contentObject;
@end

View File

@@ -1,76 +1,16 @@
//
// MPElementStoredEntity.m
// MasterPassword
// MasterPassword-iOS
//
// Created by Maarten Billemont on 02/01/12.
// Created by Maarten Billemont on 04/06/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "MPElementStoredEntity.h"
#import "MPAppDelegate.h"
#import "MPAppDelegate_Key.h"
@interface MPElementStoredEntity ()
@property (nonatomic, retain, readwrite) id contentObject;
@end
@implementation MPElementStoredEntity
@dynamic contentObject;
+ (NSDictionary *)queryForDevicePrivateElementNamed:(NSString *)name {
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
@"DevicePrivate", (__bridge id)kSecAttrService,
name, (__bridge id)kSecAttrAccount,
nil]
matches:nil];
}
- (id)content {
assert(self.type & MPElementTypeClassStored);
NSData *encryptedContent;
if (self.type & MPElementFeatureDevicePrivate)
encryptedContent = [PearlKeyChain dataOfItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]];
else
encryptedContent = self.contentObject;
NSData *decryptedContent = [encryptedContent decryptWithSymmetricKey:[[MPAppDelegate get] keyWithLength:PearlCryptKeySize]
padding:YES];
return [[NSString alloc] initWithBytes:decryptedContent.bytes length:decryptedContent.length encoding:NSUTF8StringEncoding];
}
- (void)setContent:(id)content {
NSData *encryptedContent = [[content description] encryptWithSymmetricKey:[[MPAppDelegate get] keyWithLength:PearlCryptKeySize]
padding:YES];
if (self.type & MPElementFeatureDevicePrivate) {
[PearlKeyChain addOrUpdateItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
encryptedContent, (__bridge id)kSecValueData,
#if TARGET_OS_IPHONE
kSecAttrAccessibleWhenUnlockedThisDeviceOnly, (__bridge id)kSecAttrAccessible,
#endif
nil]];
self.contentObject = nil;
} else
self.contentObject = encryptedContent;
}
- (NSString *)exportContent {
return [self.contentObject encodeBase64];
}
- (void)importContent:(NSString *)content {
self.contentObject = [content decodeBase64];
}
@end

View File

@@ -0,0 +1,21 @@
//
// 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"
@interface MPElementEntity (MP)
- (NSNumber *)use;
- (NSString *)exportContent;
- (void)importContent:(NSString *)content;
@end

122
MasterPassword/MPEntities.m Normal file
View File

@@ -0,0 +1,122 @@
//
// 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"
#import "MPAppDelegate_Key.h"
@implementation MPElementEntity (MP)
- (NSNumber *)use {
self.lastUsed = [NSDate date];
self.uses = [NSNumber numberWithUnsignedInteger:[self.uses unsignedIntegerValue] + 1];
return self.uses;
}
- (id)content {
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Content implementation missing." userInfo:nil];
}
- (NSString *)exportContent {
return nil;
}
- (void)importContent:(NSString *)content {
}
- (NSString *)description {
return PearlString(@"%@:%@", [self class], [self name]);
}
- (NSString *)debugDescription {
return PearlString(@"{%@: name=%@, user=%@, type=%d, uses=%d, lastUsed=%@}",
NSStringFromClass([self class]), self.name, self.user.name, self.type, self.uses, self.lastUsed);
}
@end
@implementation MPElementGeneratedEntity (MP)
- (id)content {
if (!([self.type unsignedIntegerValue] & MPElementTypeClassGenerated)) {
err(@"Corrupt element: %@, type: %d is not in MPElementTypeClassGenerated", self.name, self.type);
return nil;
}
if (![self.name length])
return nil;
return MPCalculateContent([self.type unsignedIntegerValue], self.name, [MPAppDelegate get].key, [self.counter unsignedIntegerValue]);
}
@end
@implementation MPElementStoredEntity (MP)
+ (NSDictionary *)queryForDevicePrivateElementNamed:(NSString *)name {
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
@"DevicePrivate", (__bridge id)kSecAttrService,
name, (__bridge id)kSecAttrAccount,
nil]
matches:nil];
}
- (id)content {
assert([self.type unsignedIntegerValue] & MPElementTypeClassStored);
NSData *encryptedContent;
if ([self.type unsignedIntegerValue] & MPElementFeatureDevicePrivate)
encryptedContent = [PearlKeyChain dataOfItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]];
else
encryptedContent = self.contentObject;
NSData *decryptedContent = [encryptedContent decryptWithSymmetricKey:[[MPAppDelegate get] keyWithLength:PearlCryptKeySize]
padding:YES];
return [[NSString alloc] initWithBytes:decryptedContent.bytes length:decryptedContent.length encoding:NSUTF8StringEncoding];
}
- (void)setContent:(id)content {
NSData *encryptedContent = [[content description] encryptWithSymmetricKey:[[MPAppDelegate get] keyWithLength:PearlCryptKeySize]
padding:YES];
if ([self.type unsignedIntegerValue] & MPElementFeatureDevicePrivate) {
[PearlKeyChain addOrUpdateItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
encryptedContent, (__bridge id)kSecValueData,
#if TARGET_OS_IPHONE
kSecAttrAccessibleWhenUnlockedThisDeviceOnly, (__bridge id)kSecAttrAccessible,
#endif
nil]];
self.contentObject = nil;
} else
self.contentObject = encryptedContent;
}
- (NSString *)exportContent {
return [self.contentObject encodeBase64];
}
- (void)importContent:(NSString *)content {
self.contentObject = [content decodeBase64];
}
@end

View File

@@ -83,4 +83,4 @@ NSData *keyIDForKey(NSData *key);
NSString *NSStringFromMPElementType(MPElementType type);
NSString *ClassNameFromMPElementType(MPElementType type);
Class ClassFromMPElementType(MPElementType type);
NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, int32_t counter);
NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, uint32_t counter);

View File

@@ -104,7 +104,7 @@ NSString *ClassNameFromMPElementType(MPElementType type) {
}
static NSDictionary *MPTypes_ciphers = nil;
NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, int32_t counter) {
NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, uint32_t counter) {
if (!(type & MPElementTypeClassGenerated)) {
err(@"Incorrect type (is not MPElementTypeClassGenerated): %d, for: %@", type, name);
@@ -118,7 +118,7 @@ NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, in
err(@"Key not set.");
return nil;
}
uint32_t salt = (unsigned)counter;
uint32_t salt = counter;
if (!counter)
// Counter unset, go into OTP mode.
// Get the UNIX timestamp of the start of the interval of 5 minutes that the current time is in.

View File

@@ -0,0 +1,31 @@
//
// MPUserEntity.h
// MasterPassword-iOS
//
// Created by Maarten Billemont on 04/06/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
@class MPElementEntity;
@interface MPUserEntity : NSManagedObject
@property (nonatomic, retain) NSData * keyID;
@property (nonatomic, retain) NSDate * lastUsed;
@property (nonatomic, retain) NSString * name;
@property (nonatomic, retain) NSNumber * saveKey;
@property (nonatomic, retain) NSNumber * avatar;
@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,22 @@
//
// MPUserEntity.m
// MasterPassword-iOS
//
// Created by Maarten Billemont on 04/06/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "MPUserEntity.h"
#import "MPElementEntity.h"
@implementation MPUserEntity
@dynamic keyID;
@dynamic lastUsed;
@dynamic name;
@dynamic saveKey;
@dynamic avatar;
@dynamic elements;
@end

View File

@@ -115,8 +115,8 @@
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name BEGINSWITH[cd] %@) AND keyID == %@",
query, query, [MPAppDelegate get].keyID];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name BEGINSWITH[cd] %@) AND user == %@",
query, query, [MPAppDelegate get].activeUser];
NSError *error = nil;
self.siteResults = [[MPAppDelegate managedObjectContext] executeFetchRequest:fetchRequest error:&error];

View File

@@ -1,11 +1,12 @@
<?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="keyID" attributeType="Binary" indexed="YES" syncable="YES" isSyncIdentityProperty="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" isSyncIdentityProperty="YES"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="16" 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"/>
@@ -13,9 +14,18 @@
<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" optional="YES" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
<attribute name="keyID" optional="YES" attributeType="Binary" syncable="YES"/>
<attribute name="lastUsed" optional="YES" attributeType="Date" syncable="YES"/>
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="saveKey" optional="YES" attributeType="Boolean" syncable="YES"/>
<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="120"/>
<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="120"/>
</elements>
</model>

View File

@@ -62,20 +62,15 @@
if (!self.key)
// Try and load the key from the keychain.
[self loadStoredKey];
[self loadSavedKey];
if (!self.key)
// Ask the user to set the key through his master password.
if ([NSThread isMainThread])
PearlMainThread(^{
[self.navigationController presentViewController:
[self.navigationController.storyboard instantiateViewControllerWithIdentifier:@"MPUnlockViewController"]
animated:animated completion:nil];
else
dispatch_async(dispatch_get_main_queue(), ^{
[self.navigationController presentViewController:
[self.navigationController.storyboard instantiateViewControllerWithIdentifier:@"MPUnlockViewController"]
animated:animated completion:nil];
});
});
}
- (void)export {
@@ -91,7 +86,7 @@
@"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 tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
if (buttonIndex == [alert firstOtherButtonIndex] + 0)
// Safe Export
[self exportShowPasswords:NO];
@@ -132,12 +127,6 @@
- (void)checkConfig {
if ([[MPConfig get].saveKey boolValue]) {
if (self.key)
[self updateKey:self.key];
} else
[self loadStoredKey];
if ([[MPConfig get].iCloud boolValue] != [self.storeManager iCloudEnabled])
[self.storeManager useiCloudStore:[[MPConfig get].iCloud boolValue] alertUser:YES];
}
@@ -315,7 +304,7 @@
NSString *importedSitesString = [[NSString alloc] initWithData:importedSitesData encoding:NSUTF8StringEncoding];
[PearlAlert showAlertWithTitle:@"Import Password" message:
@"Enter the master password for this export:"
viewStyle:UIAlertViewStyleSecureTextInput tappedButtonBlock:
viewStyle:UIAlertViewStyleSecureTextInput initAlert:nil tappedButtonBlock:
^(UIAlertView *alert, NSInteger buttonIndex) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
MPImportResult result = [self importSites:importedSitesString withPassword:[alert textFieldAtIndex:0].text
@@ -328,6 +317,7 @@
[PearlAlert showAlertWithTitle:@"Import Sites?"
message:PearlLocalize(@"Import %d sites, overwriting %d existing sites?", importCount, deleteCount)
viewStyle:UIAlertViewStyleDefault
initAlert:nil
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
if (buttonIndex != [alert cancelButtonIndex])
confirmation = YES;
@@ -409,8 +399,8 @@
[self saveContext];
if (![[MPiOSConfig get].rememberKey boolValue]) {
[self updateKey:nil];
if (![[MPiOSConfig get].rememberLogin boolValue]) {
[self unsetKey];
[self loadKey:NO];
}
@@ -455,7 +445,7 @@
message:
@"iCloud is now disabled.\n\n"
@"It is highly recommended you enable iCloud."
viewStyle:UIAlertViewStyleDefault tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
if (buttonIndex == [alert firstOtherButtonIndex] + 0) {
[PearlAlert showAlertWithTitle:@"About iCloud"
message:
@@ -471,6 +461,7 @@
@"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 didSwitchToiCloud:iCloudEnabled];
}

View File

@@ -9,9 +9,8 @@
#import "MPTypeViewController.h"
#import "MPElementEntity.h"
#import "MPSearchDelegate.h"
#import "IASKAppSettingsViewController.h"
@interface MPMainViewController : UIViewController <MPTypeDelegate, UITextFieldDelegate, MPSearchResultsDelegate, UIWebViewDelegate, IASKSettingsDelegate>
@interface MPMainViewController : UIViewController <MPTypeDelegate, UITextFieldDelegate, MPSearchResultsDelegate, UIWebViewDelegate>
@property (strong, nonatomic) MPElementEntity *activeElement;
@property (strong, nonatomic) IBOutlet MPSearchDelegate *searchResultsController;

View File

@@ -10,8 +10,7 @@
#import "MPAppDelegate.h"
#import "MPAppDelegate_Key.h"
#import "MPAppDelegate_Store.h"
#import "MPElementGeneratedEntity.h"
#import "MPElementStoredEntity.h"
#import "MPEntities.h"
#import "IASKAppSettingsViewController.h"
#import "ATConnect.h"
@@ -74,7 +73,7 @@
[super viewWillAppear:animated];
if (![self.activeElement.keyID isEqualToData:[MPAppDelegate get].keyID])
if (self.activeElement.user != [MPAppDelegate get].activeUser)
self.activeElement = nil;
self.searchDisplayController.searchBar.text = nil;
@@ -157,9 +156,9 @@
[self setHelpChapter:self.activeElement? @"2": @"1"];
self.siteName.text = self.activeElement.name;
self.passwordCounter.alpha = self.activeElement.type & MPElementTypeClassGenerated? 0.5f: 0;
self.passwordIncrementer.alpha = self.activeElement.type & MPElementTypeClassGenerated? 0.5f: 0;
self.passwordEdit.alpha = self.activeElement.type & MPElementTypeClassStored? 0.5f: 0;
self.passwordCounter.alpha = [self.activeElement.type unsignedIntegerValue] & MPElementTypeClassGenerated? 0.5f: 0;
self.passwordIncrementer.alpha = [self.activeElement.type unsignedIntegerValue] & MPElementTypeClassGenerated? 0.5f: 0;
self.passwordEdit.alpha = [self.activeElement.type unsignedIntegerValue] & MPElementTypeClassStored? 0.5f: 0;
[self.typeButton setTitle:NSStringFromMPElementType((unsigned)self.activeElement.type)
forState:UIControlStateNormal];
@@ -300,7 +299,7 @@
@"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:^{
++((MPElementGeneratedEntity *) self.activeElement).counter;
PearlUnsignedIntegerOp([((MPElementGeneratedEntity *) self.activeElement) counter], +1);
}];
[TestFlight passCheckpoint:MPTestFlightCheckpointIncrementPasswordCounter];
@@ -314,7 +313,7 @@
if (![self.activeElement isKindOfClass:[MPElementGeneratedEntity class]])
// Not of a type that supports a password counter.
return;
if (((MPElementGeneratedEntity *)self.activeElement).counter == 1)
if ([((MPElementGeneratedEntity *)self.activeElement).counter unsignedIntegerValue] == 1)
// Counter has initial value, no point resetting.
return;
@@ -323,7 +322,7 @@
@"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:^{
((MPElementGeneratedEntity *) self.activeElement).counter = 1;
((MPElementGeneratedEntity *) self.activeElement).counter = PearlUnsignedInteger(1);
}];
[TestFlight passCheckpoint:MPTestFlightCheckpointResetPasswordCounter];
@@ -332,6 +331,7 @@
- (void)changeElementWithWarning:(NSString *)warning do:(void (^)(void))task; {
[PearlAlert showAlertWithTitle:@"Password Change" message:warning viewStyle:UIAlertViewStyleDefault
initAlert:nil
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
if (buttonIndex == [alert cancelButtonIndex])
return;
@@ -361,7 +361,7 @@
- (IBAction)editPassword {
if (self.activeElement.type & MPElementTypeClassStored) {
if ([self.activeElement.type unsignedIntegerValue] & MPElementTypeClassStored) {
self.contentField.enabled = YES;
[self.contentField becomeFirstResponder];
}
@@ -403,9 +403,7 @@
break;
}
case 3: {
IASKAppSettingsViewController *settingsVC = [IASKAppSettingsViewController new];
settingsVC.delegate = self;
[self.navigationController pushViewController:settingsVC animated:YES];
[self performSegueWithIdentifier:@"UserProfile" sender:self];
break;
}
case 4: {
@@ -436,7 +434,7 @@
[TestFlight passCheckpoint:MPTestFlightCheckpointAction];
}
cancelTitle:[PearlStrings get].commonButtonCancel destructiveTitle:nil otherTitles:
[self isHelpVisible]? @"Hide Help": @"Show Help", @"FAQ", @"Tutorial", @"Settings", @"Export", @"Feedback", @"Sign Out", nil];
[self isHelpVisible]? @"Hide Help": @"Show Help", @"FAQ", @"Tutorial", @"Preferences", @"Feedback", @"Sign Out", nil];
}
- (MPElementType)selectedType {
@@ -458,7 +456,7 @@
MPElementEntity *newElement = [NSEntityDescription insertNewObjectForEntityForName:ClassNameFromMPElementType(type)
inManagedObjectContext:[MPAppDelegate managedObjectContext]];
newElement.name = self.activeElement.name;
newElement.keyID = self.activeElement.keyID;
newElement.user = self.activeElement.user;
newElement.uses = self.activeElement.uses;
newElement.lastUsed = self.activeElement.lastUsed;
@@ -466,7 +464,7 @@
self.activeElement = newElement;
}];
self.activeElement.type = type;
self.activeElement.type = PearlUnsignedInteger(type);
[TestFlight passCheckpoint:[NSString stringWithFormat:MPTestFlightCheckpointSelectType, NSStringFromMPElementType(type)]];
@@ -481,7 +479,7 @@
if (element) {
self.activeElement = element;
if ([self.activeElement use] == 1)
if ([[self.activeElement use] unsignedIntegerValue] == 1)
[self showAlertWithTitle:@"New Site" message:
PearlLocalize(@"You've just created a password for %@.\n\n"
@"IMPORTANT:\n"
@@ -552,10 +550,4 @@
return YES;
}
- (void)settingsViewControllerDidEnd:(IASKAppSettingsViewController *)sender {
while ([self.navigationController.viewControllers containsObject:sender])
[self.navigationController popViewControllerAnimated:YES];
}
@end

View File

@@ -0,0 +1,16 @@
//
// 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"
@interface MPPreferencesViewController : UITableViewController <IASKSettingsDelegate>
@property (weak, nonatomic) IBOutlet UIScrollView *avatarScrollView;
@end

View File

@@ -0,0 +1,63 @@
//
// MPPreferencesViewController.m
// MasterPassword-iOS
//
// Created by Maarten Billemont on 04/06/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "MPPreferencesViewController.h"
#import "MPAppDelegate.h"
@interface MPPreferencesViewController ()
@end
@implementation MPPreferencesViewController
@synthesize avatarScrollView;
- (void)viewDidLoad {
__block NSInteger avatarIndex = 0;
[self.avatarScrollView enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
UIButton *avatar = (UIButton *)subview;
avatar.toggleSelectionWhenTouchedInside = YES;
avatar.tag = avatarIndex++;
[avatar onSelect:^(BOOL selected) {
[MPAppDelegate get].activeUser.avatar = PearlInteger(avatar.tag);
[self.avatarScrollView enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
UIButton *avatar = (UIButton *)subview;
avatar.selected = ([[MPAppDelegate get].activeUser.avatar integerValue] == avatar.tag);
} recurse:NO];
} options:0];
} recurse:NO];
[super viewDidLoad];
}
- (void)viewWillAppear:(BOOL)animated {
[PearlUIUtils autoSizeContent:self.avatarScrollView];
[super viewWillAppear:animated];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
#pragma mark -
- (void)settingsViewControllerDidEnd:(IASKAppSettingsViewController *)sender {
while ([self.navigationController.viewControllers containsObject:sender])
[self.navigationController popViewControllerAnimated:YES];
}
- (void)viewDidUnload {
[self setAvatarScrollView:nil];
[super viewDidUnload];
}
@end

View File

@@ -131,8 +131,8 @@
assert(self.query);
self.fetchedResultsController.fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name BEGINSWITH[cd] %@) AND keyID == %@",
self.query, self.query, NilToNull([MPAppDelegate get].keyID)];
self.fetchedResultsController.fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name BEGINSWITH[cd] %@) AND user == %@",
self.query, self.query, NilToNull([MPAppDelegate get].activeUser)];
NSError *error;
if (![self.fetchedResultsController performFetch:&error])
@@ -147,7 +147,7 @@
[self.tipView removeFromSuperview];
[overlay addSubview:self.tipView];
}
return YES;
}
@@ -280,7 +280,7 @@
cell.textLabel.text = element.name;
cell.detailTextLabel.text = [NSString stringWithFormat:@"Used %d times, last on %@",
element.uses, [self.dateFormatter stringFromDate:[NSDate dateWithTimeIntervalSinceReferenceDate:element.lastUsed]]];
element.uses, [self.dateFormatter stringFromDate:element.lastUsed]];
} else {
// "New" section
cell.textLabel.text = self.query;
@@ -299,6 +299,7 @@
[PearlAlert showAlertWithTitle:@"New Site"
message:PearlLocalize(@"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];
@@ -309,10 +310,10 @@
MPElementGeneratedEntity *element = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPElementGeneratedEntity class])
inManagedObjectContext:self.fetchedResultsController.managedObjectContext];
assert([element isKindOfClass:ClassFromMPElementType((unsigned)element.type)]);
assert([MPAppDelegate get].keyID);
assert([MPAppDelegate get].activeUser.keyID);
element.name = siteName;
element.keyID = [MPAppDelegate get].keyID;
element.user = [MPAppDelegate get].activeUser;
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate didSelectElement:element];

View File

@@ -62,17 +62,15 @@
if ([delegate respondsToSelector:@selector(selectedType)])
if ([delegate selectedType] == [self typeAtIndexPath:indexPath])
[cell iterateSubviewsContinueAfter:^BOOL(UIView *subview) {
[cell enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
if ([subview isKindOfClass:[UIImageView class]]) {
UIImageView *imageView = ((UIImageView *)subview);
if (!imageView.highlightedImage)
imageView.highlightedImage = [imageView.image highlightedImage];
imageView.highlighted = YES;
return NO;
*stop = YES;
}
return YES;
}];
} recurse:NO];
return cell;
}

View File

@@ -8,14 +8,17 @@
#import <UIKit/UIKit.h>
@interface MPUnlockViewController : UIViewController <UITextFieldDelegate>
@interface MPUnlockViewController : UIViewController <UITextFieldDelegate, UIScrollViewDelegate>
@property (weak, nonatomic) IBOutlet UIImageView *lock;
@property (weak, nonatomic) IBOutlet UIImageView *spinner;
@property (weak, nonatomic) IBOutlet UITextField *field;
@property (weak, nonatomic) IBOutlet UILabel *messageLabel;
@property (weak, nonatomic) IBOutlet UIView *changeMPView;
@property (weak, nonatomic) IBOutlet UITextField *passwordField;
@property (weak, nonatomic) IBOutlet UIView *passwordView;
@property (weak, nonatomic) IBOutlet UIScrollView *usersView;
@property (weak, nonatomic) IBOutlet UILabel *usernameLabel;
@property (weak, nonatomic) IBOutlet UILabel *oldUsernameLabel;
@property (weak, nonatomic) IBOutlet UIButton *userButtonTemplate;
@property (weak, nonatomic) IBOutlet UILabel *deleteTip;
- (IBAction)changeMP;
- (IBAction)deleteTargetedUser:(UILongPressGestureRecognizer *)sender;
@end

View File

@@ -12,78 +12,29 @@
#import "MPAppDelegate.h"
#import "MPAppDelegate_Key.h"
#import "MPAppDelegate_Store.h"
#import "MPElementEntity.h"
typedef enum {
MPLockscreenIdle,
MPLockscreenError,
MPLockscreenSuccess,
MPLockscreenProgress,
} MPLockscreen;
#import "MPEntities.h"
@interface MPUnlockViewController ()
@property (strong, nonatomic) MPUserEntity *selectedUser;
@property (strong, nonatomic) NSMutableDictionary *avatarToUser;
@end
@implementation MPUnlockViewController
@synthesize lock;
@synthesize selectedUser;
@synthesize avatarToUser;
@synthesize spinner;
@synthesize field;
@synthesize messageLabel;
@synthesize changeMPView;
@synthesize passwordField;
@synthesize passwordView;
@synthesize usersView;
@synthesize usernameLabel, oldUsernameLabel;
@synthesize userButtonTemplate;
@synthesize deleteTip;
- (void)showMessage:(NSString *)message state:(MPLockscreen)state {
__block void(^showMessageAnimation)(void) = ^{
self.lock.alpha = 0.0f;
switch (state) {
case MPLockscreenIdle:
[self.lock setImage:[UIImage imageNamed:@"lock_idle"]];
break;
case MPLockscreenError:
[self.lock setImage:[UIImage imageNamed:@"lock_red"]];
break;
case MPLockscreenSuccess:
[self.lock setImage:[UIImage imageNamed:@"lock_green"]];
break;
case MPLockscreenProgress:
[self.lock setImage:[UIImage imageNamed:@"lock_blue"]];
break;
}
self.lock.alpha = 0.0f;
[UIView animateWithDuration:1.0f animations:^{
self.lock.alpha = 1.0f;
} completion:^(BOOL finished) {
if (finished)
[UIView animateWithDuration:1.0f delay:0 options:UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse animations:^{
self.lock.alpha = 0.5f;
} completion:nil];
}];
[UIView animateWithDuration:0.5f animations:^{
self.messageLabel.alpha = 1.0f;
self.messageLabel.text = message;
}];
};
if (self.messageLabel.alpha)
[UIView animateWithDuration:0.3f animations:^{
self.messageLabel.alpha = 0.0f;
} completion:^(BOOL finished) {
if (finished)
showMessageAnimation();
}];
else
showMessageAnimation();
}
- (void)hideMessage {
[UIView animateWithDuration:0.5f animations:^{
self.messageLabel.alpha = 0.0f;
}];
}
// [UIView animateWithDuration:1.0f delay:0 options:UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse animations:^{
// self.lock.alpha = 0.5f;
// } completion:nil];
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
@@ -92,16 +43,16 @@ typedef enum {
- (void)viewDidLoad {
self.messageLabel.text = nil;
self.messageLabel.alpha = 0;
self.changeMPView.alpha = 0;
self.spinner.alpha = 0;
self.field.text = nil;
self.avatarToUser = [NSMutableDictionary dictionaryWithCapacity:3];
[[NSNotificationCenter defaultCenter] addObserverForName:MPNotificationKeyForgotten
object:nil queue:nil usingBlock:^(NSNotification *note) {
[self.field becomeFirstResponder];
}];
self.spinner.alpha = 0;
self.passwordField.text = nil;
self.usersView.decelerationRate = UIScrollViewDecelerationRateFast;
self.usersView.clipsToBounds = NO;
self.usernameLabel.layer.cornerRadius = 5;
self.userButtonTemplate.hidden = YES;
[self updateLayoutAnimated:NO allowScroll:YES completion:nil];
[super viewDidLoad];
}
@@ -109,20 +60,29 @@ typedef enum {
- (void)viewDidUnload {
[self setSpinner:nil];
[self setField:nil];
[self setMessageLabel:nil];
[self setLock:nil];
[self setChangeMPView:nil];
[self setPasswordField:nil];
[self setPasswordView:nil];
[self setUsersView:nil];
[self setUsernameLabel:nil];
[self setUserButtonTemplate:nil];
[self setDeleteTip:nil];
[super viewDidUnload];
}
- (void)viewWillAppear:(BOOL)animated {
self.selectedUser = nil;
[self updateUsers];
[super viewWillAppear:animated];
}
- (void)viewDidAppear:(BOOL)animated {
[[UIApplication sharedApplication] setStatusBarHidden:YES
withAnimation:animated? UIStatusBarAnimationSlide: UIStatusBarAnimationNone];
[super viewWillAppear:animated];
[super viewDidAppear:animated];
}
- (void)viewWillDisappear:(BOOL)animated {
@@ -133,24 +93,247 @@ typedef enum {
[super viewWillDisappear:animated];
}
- (void)viewDidAppear:(BOOL)animated {
[self.field becomeFirstResponder];
[super viewDidAppear:animated];
- (void)updateUsers {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPUserEntity class])];
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"lastUsed" ascending:NO]];
NSArray *users = [[MPAppDelegate managedObjectContext] executeFetchRequest:fetchRequest error:nil];
// Clean up avatars.
for (UIView *view in [self.usersView subviews])
if (view != self.userButtonTemplate)
[view removeFromSuperview];
[self.avatarToUser removeAllObjects];
// Create avatars.
for (MPUserEntity *user in users)
[self setupAvatar:[PearlUIUtils copyOf:self.userButtonTemplate] forUser:user];
[self setupAvatar:[PearlUIUtils copyOf:self.userButtonTemplate] forUser:nil];
// Scroll view's content changed, update its content size.
[PearlUIUtils autoSizeContent:self.usersView ignoreHidden:YES ignoreInvisible:YES limitPadding:NO ignoreSubviews:nil];
[self updateLayoutAnimated:YES allowScroll:YES completion:nil];
self.deleteTip.alpha = 0;
if ([users count] > 1)
[UIView animateWithDuration:0.5f animations:^{
self.deleteTip.alpha = 1;
}];
}
- (UIButton *)setupAvatar:(UIButton *)avatar forUser:(MPUserEntity *)user {
[avatar onHighlightOrSelect:^(BOOL highlighted, BOOL selected) {
if (highlighted || selected)
avatar.backgroundColor = self.userButtonTemplate.backgroundColor;
else
avatar.backgroundColor = [UIColor clearColor];
} options:0];
[avatar onSelect:^(BOOL selected) {
self.selectedUser = selected? user: nil;
if (user)
[self didToggleUserSelection];
else if (selected)
[self didSelectNewUserAvatar:avatar];
} options:0];
avatar.toggleSelectionWhenTouchedInside = YES;
avatar.center = CGPointMake(avatar.center.x + [self.avatarToUser count] * 160, avatar.center.y);
avatar.hidden = NO;
avatar.layer.cornerRadius = 5;
avatar.layer.shadowColor = [UIColor blackColor].CGColor;
avatar.layer.shadowOpacity = 1;
avatar.layer.shadowRadius = 20;
avatar.layer.masksToBounds = NO;
avatar.backgroundColor = [UIColor clearColor];
if (user)
[self.avatarToUser setObject:user forKey:[NSValue valueWithNonretainedObject:avatar]];
if (self.selectedUser && user == self.selectedUser)
avatar.selected = YES;
return avatar;
}
- (void)didToggleUserSelection {
if (!self.selectedUser)
[self.passwordField resignFirstResponder];
[self updateLayoutAnimated:YES allowScroll:YES completion:^(BOOL finished) {
if (finished)
if (self.selectedUser)
[self.passwordField becomeFirstResponder];
}];
}
- (void)didSelectNewUserAvatar:(UIButton *)newUserAvatar {
[PearlAlert showAlertWithTitle:@"New User"
message:@"Enter your name:" viewStyle:UIAlertViewStylePlainTextInput
initAlert:^(UIAlertView *alert, UITextField *firstField) {
firstField.autocapitalizationType = UITextAutocapitalizationTypeWords;
firstField.autocorrectionType = UITextAutocorrectionTypeYes;
firstField.spellCheckingType = UITextSpellCheckingTypeYes;
firstField.keyboardType = UIKeyboardTypeAlphabet;
}
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
newUserAvatar.selected = NO;
if (buttonIndex == [alert cancelButtonIndex])
return;
MPUserEntity *newUser = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPUserEntity class])
inManagedObjectContext:[MPAppDelegate managedObjectContext]];
newUser.name = [alert textFieldAtIndex:0].text;
self.selectedUser = newUser;
[self updateUsers];
}
cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:[PearlStrings get].commonButtonSave, nil];
}
- (void)updateLayoutAnimated:(BOOL)animated allowScroll:(BOOL)allowScroll completion:(void (^)(BOOL finished))completion {
if (animated) {
self.oldUsernameLabel.text = self.usernameLabel.text;
self.oldUsernameLabel.alpha = 1;
self.usernameLabel.alpha = 0;
[UIView animateWithDuration:0.5f animations:^{
[self updateLayoutAnimated:NO allowScroll:allowScroll completion:nil];
self.oldUsernameLabel.alpha = 0;
self.usernameLabel.alpha = 1;
} completion:^(BOOL finished) {
if (completion)
completion(finished);
}];
return;
}
if (self.selectedUser && !self.passwordView.alpha) {
self.passwordView.alpha = 1;
self.usersView.center = CGPointMake(160, 100);
self.usersView.scrollEnabled = NO;
self.usernameLabel.center = CGPointMake(160, 84);
self.usernameLabel.backgroundColor = [UIColor blackColor];
self.oldUsernameLabel.center = self.usernameLabel.center;
} else if (self.passwordView.alpha == 1) {
self.passwordView.alpha = 0;
self.usersView.center = CGPointMake(160, 240);
self.usersView.scrollEnabled = YES;
self.usernameLabel.center = CGPointMake(160, 296);
self.usernameLabel.backgroundColor = [UIColor clearColor];
self.oldUsernameLabel.center = self.usernameLabel.center;
}
MPUserEntity *targetedUser = self.selectedUser;
UIButton *selectedAvatar = [self avatarForUser:self.selectedUser];
UIButton *targetedAvatar = selectedAvatar;
if (!targetedAvatar) {
targetedAvatar = [self findTargetedAvatar];
targetedUser = [self userForAvatar:targetedAvatar];
}
[self.usersView enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
const BOOL isTargeted = subview == targetedAvatar;
subview.userInteractionEnabled = isTargeted;
subview.alpha = isTargeted ? 1: self.selectedUser? 0.1: 0.4;
if (!isTargeted && [subview.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 = [NSArray arrayWithObjects:toShadowColorAnimation, toShadowOpacityAnimation, nil];
group.duration = 0.5f;
[subview.layer removeAnimationForKey:@"targetedShadow"];
[subview.layer addAnimation:group forKey:@"inactiveShadow"];
}
} recurse:NO];
if (![targetedAvatar.layer animationForKey:@"targetedShadow"]) {
CABasicAnimation *toShadowColorAnimation = [CABasicAnimation animationWithKeyPath:@"shadowColor"];
toShadowColorAnimation.toValue = (__bridge id)[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 = NSIntegerMax;
CAAnimationGroup *group = [[CAAnimationGroup alloc] init];
group.animations = [NSArray arrayWithObjects:toShadowColorAnimation, toShadowOpacityAnimation, pulseShadowOpacityAnimation, nil];
group.duration = CGFLOAT_MAX;
[targetedAvatar.layer removeAnimationForKey:@"inactiveShadow"];
[targetedAvatar.layer addAnimation:group forKey:@"targetedShadow"];
}
if (allowScroll) {
CGPoint targetContentOffset = CGPointMake(targetedAvatar.center.x - self.usersView.bounds.size.width / 2, self.usersView.contentOffset.y);
if (!CGPointEqualToPoint(self.usersView.contentOffset, targetContentOffset))
[self.usersView setContentOffset:targetContentOffset animated:animated];
}
self.usernameLabel.text = targetedUser? targetedUser.name: @"New User";
self.usernameLabel.bounds = CGRectSetHeight(self.usernameLabel.bounds,
[self.usernameLabel.text sizeWithFont:self.usernameLabel.font
constrainedToSize:CGSizeMake(self.usernameLabel.bounds.size.width - 10, 100)
lineBreakMode:self.usernameLabel.lineBreakMode].height);
self.oldUsernameLabel.bounds = self.usernameLabel.bounds;
if (completion)
completion(YES);
}
- (UIButton *)findTargetedAvatar {
CGFloat xOfMiddle = self.usersView.contentOffset.x + self.usersView.bounds.size.width / 2;
return (UIButton *)[PearlUIUtils viewClosestTo:CGPointMake(xOfMiddle, self.usersView.contentOffset.y) ofArray:self.usersView.subviews];
}
- (UIButton *)avatarForUser:(MPUserEntity *)user {
__block UIButton *avatar = nil;
if (user)
[self.avatarToUser enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if (obj == user)
avatar = [key nonretainedObjectValue];
}];
return avatar;
}
- (MPUserEntity *)userForAvatar:(UIButton *)avatar {
return NullToNil([self.avatarToUser objectForKey:[NSValue valueWithNonretainedObject:avatar]]);
}
#pragma mark - UITextFieldDelegate
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
if ([textField.text length]) {
[textField resignFirstResponder];
return YES;
}
if (![textField.text length])
return NO;
return NO;
}
- (void)textFieldDidEndEditing:(UITextField *)textField {
[textField resignFirstResponder];
CABasicAnimation *rotate = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
@@ -166,52 +349,53 @@ typedef enum {
self.spinner.alpha = 1.0f;
}];
[self showMessage:@"Checking password..." state:MPLockscreenProgress];
// [self showMessage:@"Checking password..." state:MPLockscreenProgress];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
BOOL unlocked = [[MPAppDelegate get] tryMasterPassword:textField.text];
BOOL unlocked = [[MPAppDelegate get] tryMasterPassword:textField.text forUser:self.selectedUser];
dispatch_async(dispatch_get_main_queue(), ^{
if (unlocked) {
[self showMessage:@"Success!" state:MPLockscreenSuccess];
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"keyID == %@", [MPAppDelegate get].keyID];
fetchRequest.fetchLimit = 1;
BOOL keyIDHasElements = [[[MPAppDelegate managedObjectContext] executeFetchRequest:fetchRequest error:nil] count] > 0;
if (keyIDHasElements)
// [self showMessage:@"Success!" state:MPLockscreenSuccess];
if ([selectedUser.keyID length])
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (long)(NSEC_PER_SEC * 1.5f)), dispatch_get_main_queue(), ^{
[self dismissModalViewControllerAnimated:YES];
});
else {
[PearlAlert showAlertWithTitle:@"New Master Password"
message:
@"Please confirm the spelling of this new master password."
message:@"Please confirm the spelling of this new master password."
viewStyle:UIAlertViewStyleSecureTextInput
initAlert:nil
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
if (buttonIndex == [alert cancelButtonIndex]) {
[[MPAppDelegate get] updateKey:nil];
[[MPAppDelegate get] unsetKey];
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 tappedButtonBlock:nil
viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:nil
cancelTitle:[PearlStrings get].commonButtonOkay otherTitles:nil];
return;
}
self.selectedUser.keyID = [MPAppDelegate get].activeUser.keyID;
[[MPAppDelegate get] saveContext];
[self dismissModalViewControllerAnimated:YES];
}
cancelTitle:[PearlStrings get].commonButtonCancel
otherTitles:[PearlStrings get].commonButtonContinue, nil];
}
} else {
[self showMessage:@"Not valid." state:MPLockscreenError];
// [self showMessage:@"Not valid." state:MPLockscreenError];
[UIView animateWithDuration:0.5f animations:^{
self.changeMPView.alpha = 1.0f;
// self.changeMPView.alpha = 1.0f;
}];
}
@@ -224,8 +408,39 @@ typedef enum {
});
});
});
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];
// [self scrollToAvatar:middleAvatar animated:YES];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[self updateLayoutAnimated:YES allowScroll:YES completion:nil];
// [self scrollToAvatar:middleAvatar animated:YES];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// CGFloat xOfMiddle = scrollView.contentOffset.x + scrollView.bounds.size.width / 2;
// UIButton *middleAvatar = (UIButton *)[PearlUIUtils viewClosestTo:CGPointMake(xOfMiddle, scrollView.contentOffset.y) ofArray:scrollView.subviews];
//
[self updateLayoutAnimated:NO allowScroll:NO completion:nil];
// [self scrollToAvatar:middleAvatar animated:NO];
}
#pragma mark - IBActions
- (IBAction)changeMP {
[PearlAlert showAlertWithTitle:@"Changing Master Password"
@@ -236,11 +451,12 @@ typedef enum {
@"You can always change back to your current master password later.\n"
@"Your current sites and passwords will then become available again."
viewStyle:UIAlertViewStyleDefault
initAlert:nil
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
if (buttonIndex == [alert cancelButtonIndex])
return;
[[MPAppDelegate get] forgetKey];
[[MPAppDelegate get] forgetSavedKey];
[[MPAppDelegate get] loadKey:YES];
[TestFlight passCheckpoint:MPTestFlightCheckpointMPChanged];
@@ -249,4 +465,30 @@ typedef enum {
otherTitles:[PearlStrings get].commonButtonContinue, nil];
}
- (IBAction)deleteTargetedUser:(UILongPressGestureRecognizer *)sender {
if (sender.state != UIGestureRecognizerStateBegan)
return;
if (self.selectedUser)
return;
MPUserEntity *targetedUser = [self userForAvatar:[self findTargetedAvatar]];
if (!targetedUser)
return;
[PearlAlert showAlertWithTitle:@"Delete User" message:
PearlString(@"Do you want to delete all record of the following user?\n\n%@", targetedUser.name)
viewStyle:UIAlertViewStyleDefault
initAlert:nil
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
if (buttonIndex == [alert cancelButtonIndex])
return;
[[MPAppDelegate get].managedObjectContext deleteObject:targetedUser];
[[MPAppDelegate get] saveContext];
[self updateUsers];
} cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Delete", nil];
}
@end

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="1.1" toolsVersion="2182" systemVersion="11E53" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" initialViewController="KZF-fe-y9n">
<dependencies>
<deployment defaultVersion="1296" identifier="iOS"/>
<development defaultVersion="4200" identifier="xcode"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="1181"/>
</dependencies>
@@ -659,6 +658,7 @@ L4m3P4sSw0rD</string>
<outlet property="siteName" destination="gSK-aB-wNI" id="IIe-z8-zy8"/>
<outlet property="typeButton" destination="Cei-5z-uWE" id="4M1-d7-5Bh"/>
<outlet property="typeTipContainer" destination="g55-0m-WjS" id="KZ9-KV-NMh"/>
<segue destination="oLN-6u-GLb" kind="push" identifier="UserProfile" id="tKN-Sw-S5J"/>
</connections>
</viewController>
<pongPressGestureRecognizer allowableMovement="10" minimumPressDuration="0.5" id="cZr-Fj-eBw">
@@ -735,7 +735,7 @@ L4m3P4sSw0rD</string>
<scene sceneID="HkH-JR-Fhy">
<objects>
<placeholder placeholderIdentifier="IBFirstResponder" id="OGA-5j-IcQ" userLabel="First Responder" sceneMemberID="firstResponder"/>
<viewController storyboardIdentifier="MPUnlockViewController" modalTransitionStyle="flipHorizontal" id="Nbn-Rv-sP1" customClass="MPUnlockViewController" sceneMemberID="viewController">
<viewController storyboardIdentifier="MPUnlockViewController" wantsFullScreenLayout="YES" modalTransitionStyle="flipHorizontal" id="Nbn-Rv-sP1" customClass="MPUnlockViewController" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="PHH-XC-9QQ">
<rect key="frame" x="0.0" y="0.0" width="320" height="480"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
@@ -744,96 +744,111 @@ L4m3P4sSw0rD</string>
<rect key="frame" x="0.0" y="0.0" width="320" height="480"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</imageView>
<view contentMode="scaleToFill" id="OP6-72-eij">
<rect key="frame" x="0.0" y="157" width="320" height="130"/>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" alpha="0.5" contentMode="left" text="Tap and hold to delete." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="10" id="DBJ-Qi-ZcF">
<rect key="frame" x="32" y="460" width="256" height="20"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" name="Copperplate" family="Copperplate" pointSize="12"/>
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
<color key="shadowColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
</label>
<view contentMode="scaleToFill" id="7cc-yu-i0m">
<rect key="frame" x="20" y="168" width="280" height="88"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="center" image="lock_idle.png" id="tyv-qJ-bKR">
<rect key="frame" x="110" y="15" width="100" height="100"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
</imageView>
<imageView userInteractionEnabled="NO" alpha="0.0" contentMode="center" image="lock_idle.png" id="Lpf-KA-3CV">
<rect key="frame" x="110" y="15" width="100" height="100"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
</imageView>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" image="ui_spinner.png" id="27q-lX-0vy">
<rect key="frame" x="122" y="28" width="75" height="75"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
</imageView>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Checking password..." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="10" id="oU9-lf-nnJ">
<rect key="frame" x="20" y="115" width="280" height="15"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<fontDescription key="fontDescription" name="Copperplate-Bold" family="Copperplate" pointSize="13"/>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Enter your master password:" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="10" id="RhX-bA-EhC">
<rect key="frame" x="12" y="0.0" width="256" height="20"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" name="Copperplate" family="Copperplate" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" image="ui_textfield.png" id="ivR-Xl-NrT">
<rect key="frame" x="0.0" y="28" width="280" height="60"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<rect key="contentStretch" x="0.25" y="0.25" width="0.49999999999999961" height="0.49999999999999961"/>
</imageView>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" textAlignment="center" clearsOnBeginEditing="YES" minimumFontSize="17" id="rTR-7Q-X8o">
<rect key="frame" x="0.0" y="28" width="280" height="60"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<fontDescription key="fontDescription" name="Copperplate" family="Copperplate" pointSize="36"/>
<textInputTraits key="textInputTraits" enablesReturnKeyAutomatically="YES" secureTextEntry="YES"/>
<connections>
<outlet property="delegate" destination="Nbn-Rv-sP1" id="Y0T-cI-gF1"/>
</connections>
</textField>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" image="ui_textfield.png" id="ivR-Xl-NrT">
<rect key="frame" x="20" y="89" width="280" height="60"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<rect key="contentStretch" x="0.25" y="0.25" width="0.49999999999999961" height="0.49999999999999961"/>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" image="ui_spinner.png" id="27q-lX-0vy">
<rect key="frame" x="105" y="29" width="110" height="110"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
</imageView>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" textAlignment="center" clearsOnBeginEditing="YES" minimumFontSize="17" id="rTR-7Q-X8o">
<rect key="frame" x="20" y="89" width="280" height="60"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<fontDescription key="fontDescription" name="Copperplate" family="Copperplate" pointSize="36"/>
<textInputTraits key="textInputTraits" secureTextEntry="YES"/>
<connections>
<outlet property="delegate" destination="Nbn-Rv-sP1" id="Y0T-cI-gF1"/>
</connections>
</textField>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Enter your master password:" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="10" id="RhX-bA-EhC">
<rect key="frame" x="32" y="61" width="256" height="20"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" name="Copperplate" family="Copperplate" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" id="Wu7-Mg-9SD">
<rect key="frame" x="0.0" y="391" width="320" height="89"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" delaysContentTouches="NO" id="Blg-F1-9NA">
<rect key="frame" x="0.0" y="20" width="320" height="160"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Trying to log in with another master password?" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="10" id="vnS-n6-NZI">
<rect key="frame" x="0.0" y="0.0" width="320" height="15"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<fontDescription key="fontDescription" name="Copperplate-Bold" family="Copperplate" pointSize="11"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" reversesTitleShadowWhenHighlighted="YES" lineBreakMode="middleTruncation" id="wad-V1-K3f">
<rect key="frame" x="10" y="23" width="300" height="46"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="top" showsTouchWhenHighlighted="YES" lineBreakMode="middleTruncation" id="Ten-ig-gog">
<rect key="frame" x="105" y="10" width="110" height="110"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" red="0.40000000596046448" green="0.80000001192092896" blue="1" alpha="1" colorSpace="calibratedRGB"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<size key="titleShadowOffset" width="0.0" height="1"/>
<state key="normal" title="Change master password" backgroundImage="ui_button_standard_large.png">
<color key="titleColor" cocoaTouchSystemColor="lightTextColor"/>
<color key="titleShadowColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
<state key="normal" backgroundImage="avatar-male-1.png">
<color key="titleColor" red="0.19607843459999999" green="0.30980393290000002" blue="0.52156865600000002" alpha="1" colorSpace="calibratedRGB"/>
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<state key="highlighted">
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</state>
<connections>
<action selector="changeMP" destination="Nbn-Rv-sP1" eventType="touchUpInside" id="7RI-hu-SiS"/>
</connections>
</button>
</subviews>
<gestureRecognizers/>
<connections>
<outlet property="delegate" destination="Nbn-Rv-sP1" id="E1h-WA-PYV"/>
<outletCollection property="gestureRecognizers" destination="9WS-yS-aqQ" appends="YES" id="B9k-bg-gqA"/>
</connections>
</scrollView>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" alpha="0.0" contentMode="left" text="Maarten Billemont" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumFontSize="10" id="8s0-nT-Aoq">
<rect key="frame" x="90" y="289" width="140" height="15"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
<fontDescription key="fontDescription" name="Copperplate-Bold" family="Copperplate" pointSize="13"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Maarten Billemont" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumFontSize="10" id="0NM-NI-7UR">
<rect key="frame" x="90" y="76.5" width="140" height="15"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
<fontDescription key="fontDescription" name="Copperplate-Bold" family="Copperplate" pointSize="13"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
<imageView hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="keypad.png" id="4tz-l4-4Kj">
<rect key="frame" x="0.0" y="264" width="320" height="216"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</imageView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
<nil key="simulatedStatusBarMetrics"/>
<connections>
<outlet property="changeMPView" destination="Wu7-Mg-9SD" id="84H-HT-5ml"/>
<outlet property="field" destination="rTR-7Q-X8o" id="DHg-gg-MVD"/>
<outlet property="lock" destination="Lpf-KA-3CV" id="6cE-2g-4XQ"/>
<outlet property="messageLabel" destination="oU9-lf-nnJ" id="Ahc-hl-KnJ"/>
<outlet property="deleteTip" destination="DBJ-Qi-ZcF" id="VXD-Zc-UYi"/>
<outlet property="oldUsernameLabel" destination="8s0-nT-Aoq" id="mXR-VG-2js"/>
<outlet property="passwordField" destination="rTR-7Q-X8o" id="CDA-iP-kCm"/>
<outlet property="passwordView" destination="7cc-yu-i0m" id="WoF-Ab-PPC"/>
<outlet property="spinner" destination="27q-lX-0vy" id="jUx-GK-Lgf"/>
<outlet property="userButtonTemplate" destination="Ten-ig-gog" id="hSM-hh-ofy"/>
<outlet property="usernameLabel" destination="0NM-NI-7UR" id="Sa4-pY-87O"/>
<outlet property="usersView" destination="Blg-F1-9NA" id="SsC-1H-fIB"/>
</connections>
</viewController>
<pongPressGestureRecognizer allowableMovement="10" minimumPressDuration="0.5" id="9WS-yS-aqQ">
<connections>
<action selector="deleteTargetedUser:" destination="Nbn-Rv-sP1" id="cBQ-oQ-c7g"/>
</connections>
</pongPressGestureRecognizer>
</objects>
<point key="canvasLocation" x="455" y="1425"/>
</scene>
@@ -857,9 +872,319 @@ L4m3P4sSw0rD</string>
</objects>
<point key="canvasLocation" x="-85" y="182"/>
</scene>
<!--App Settings View Controller-->
<scene sceneID="4E1-FA-zP1">
<objects>
<placeholder placeholderIdentifier="IBFirstResponder" id="Xkt-gh-nAq" userLabel="First Responder" sceneMemberID="firstResponder"/>
<viewController id="Ypi-Ct-X6S" customClass="IASKAppSettingsViewController" sceneMemberID="viewController">
<navigationItem key="navigationItem" id="u18-ao-KIy"/>
</viewController>
</objects>
<point key="canvasLocation" x="1537" y="785"/>
</scene>
<!--Preferences View Controller - User Profile-->
<scene sceneID="cMu-Lq-D10">
<objects>
<placeholder placeholderIdentifier="IBFirstResponder" id="CEl-jQ-l9k" userLabel="First Responder" sceneMemberID="firstResponder"/>
<tableViewController id="oLN-6u-GLb" customClass="MPPreferencesViewController" sceneMemberID="viewController">
<tableView key="view" opaque="NO" clipsSubviews="YES" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="plain" separatorStyle="none" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" id="oSh-Ap-kLt">
<rect key="frame" x="0.0" y="64" width="320" height="416"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.12549020350000001" green="0.1411764771" blue="0.14901961389999999" alpha="1" colorSpace="calibratedRGB"/>
<view key="tableFooterView" contentMode="scaleToFill" id="63M-7L-M7o">
<rect key="frame" x="0.0" y="508" width="320" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
<sections>
<tableViewSection id="OJv-8V-W2l">
<cells>
<tableViewCell contentMode="scaleToFill" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" rowHeight="226" id="9gC-Vq-Ta8">
<rect key="frame" x="0.0" y="0.0" width="320" height="226"/>
<autoresizingMask key="autoresizingMask"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="320" height="225"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Avatar" lineBreakMode="tailTruncation" minimumFontSize="10" id="0Wd-wn-t6z">
<rect key="frame" x="20" y="20" width="280" height="21"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" name="GillSans-Bold" family="Gill Sans" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
<color key="shadowColor" cocoaTouchSystemColor="darkTextColor"/>
<size key="shadowOffset" width="0.0" height="1"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="This is the icon that represents you on the Master Password unlock screen." lineBreakMode="tailTruncation" numberOfLines="0" minimumFontSize="10" id="DgX-yc-sKT">
<rect key="frame" x="20" y="49" width="280" height="38"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" name="GillSans-LightItalic" family="Gill Sans" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
<color key="shadowColor" cocoaTouchSystemColor="darkTextColor"/>
<size key="shadowOffset" width="0.0" height="1"/>
</label>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" id="TMH-Xt-EnD">
<rect key="frame" x="0.0" y="95" width="320" height="130"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" id="jEL-UA-da6">
<rect key="frame" x="0.0" y="0.0" width="614" height="130"/>
<autoresizingMask key="autoresizingMask" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="top" lineBreakMode="middleTruncation" id="6ap-Xw-Ubd">
<rect key="frame" x="20" y="10" width="110" height="110"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<state key="normal" image="avatar-male-1.png">
<color key="titleColor" red="0.19607843459999999" green="0.30980393290000002" blue="0.52156865600000002" alpha="1" colorSpace="calibratedRGB"/>
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<state key="highlighted">
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</state>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="top" lineBreakMode="middleTruncation" id="DwC-Fa-yaI">
<rect key="frame" x="138" y="10" width="110" height="110"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<state key="normal" image="avatar-female-1.png">
<color key="titleColor" red="0.19607843459999999" green="0.30980393290000002" blue="0.52156865600000002" alpha="1" colorSpace="calibratedRGB"/>
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<state key="highlighted">
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</state>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="top" lineBreakMode="middleTruncation" id="TF1-Zi-jZK">
<rect key="frame" x="256" y="10" width="110" height="110"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<state key="normal" image="avatar-male-2.png">
<color key="titleColor" red="0.19607843459999999" green="0.30980393290000002" blue="0.52156865600000002" alpha="1" colorSpace="calibratedRGB"/>
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<state key="highlighted">
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</state>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="top" lineBreakMode="middleTruncation" id="32d-Yo-SGt">
<rect key="frame" x="366" y="10" width="110" height="110"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<state key="normal" image="avatar-female-2.png">
<color key="titleColor" red="0.19607843459999999" green="0.30980393290000002" blue="0.52156865600000002" alpha="1" colorSpace="calibratedRGB"/>
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<state key="highlighted">
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</state>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="top" lineBreakMode="middleTruncation" id="LTD-gI-8bH">
<rect key="frame" x="484" y="10" width="110" height="110"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<state key="normal" image="avatar-male-3.png">
<color key="titleColor" red="0.19607843459999999" green="0.30980393290000002" blue="0.52156865600000002" alpha="1" colorSpace="calibratedRGB"/>
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<state key="highlighted">
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</state>
</button>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
</subviews>
</scrollView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection id="TRa-Gy-tG5">
<cells>
<tableViewCell contentMode="scaleToFill" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" rowHeight="119" id="n1H-sG-HDc">
<rect key="frame" x="0.0" y="226" width="320" height="119"/>
<autoresizingMask key="autoresizingMask"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="320" height="118"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Preferences" lineBreakMode="tailTruncation" minimumFontSize="10" id="ylz-i5-tem">
<rect key="frame" x="20" y="20" width="280" height="21"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" name="GillSans-Bold" family="Gill Sans" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
<color key="shadowColor" cocoaTouchSystemColor="darkTextColor"/>
<size key="shadowOffset" width="0.0" height="1"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="These are user-specific configuration settings." lineBreakMode="tailTruncation" numberOfLines="0" minimumFontSize="10" id="WCm-A3-a1a">
<rect key="frame" x="20" y="49" width="280" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" name="GillSans-LightItalic" family="Gill Sans" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
<color key="shadowColor" cocoaTouchSystemColor="darkTextColor"/>
<size key="shadowOffset" width="0.0" height="1"/>
</label>
<imageView userInteractionEnabled="NO" contentMode="top" image="ui_list_first.png" id="tK4-bL-QK1">
<rect key="frame" x="10" y="75" width="300" height="50"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<rect key="contentStretch" x="0.10000000000000001" y="0.20000000000000001" width="0.79999999999999982" height="0.59999999999999964"/>
</imageView>
<imageView userInteractionEnabled="NO" contentMode="bottom" image="ui_list_last.png" id="fAY-fK-Uzn">
<rect key="frame" x="10" y="109" width="300" height="10"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<rect key="contentStretch" x="0.10000000000000001" y="0.20000000000000001" width="0.79999999999999982" height="0.59999999999999964"/>
</imageView>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Save Password" lineBreakMode="tailTruncation" minimumFontSize="10" id="mmP-r2-iNF">
<rect key="frame" x="20" y="75" width="280" height="23"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<fontDescription key="fontDescription" name="GillSans" family="Gill Sans" pointSize="18"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
<color key="shadowColor" cocoaTouchSystemColor="darkTextColor"/>
<size key="shadowOffset" width="0.0" height="1"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Stores your password on the device." lineBreakMode="tailTruncation" minimumFontSize="10" id="LRv-ac-sdH">
<rect key="frame" x="20" y="98" width="280" height="20"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<fontDescription key="fontDescription" name="GillSans-LightItalic" family="Gill Sans" pointSize="14"/>
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" id="seh-qE-k6P">
<rect key="frame" x="221" y="83" width="79" height="27"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="onTintColor" red="0.12549020350000001" green="0.1411764771" blue="0.14901961389999999" alpha="1" colorSpace="calibratedRGB"/>
</switch>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection id="dN3-cJ-9WA">
<cells>
<tableViewCell contentMode="scaleToFill" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" rowHeight="119" id="xBt-OT-BYA">
<rect key="frame" x="0.0" y="345" width="320" height="119"/>
<autoresizingMask key="autoresizingMask"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="320" height="118"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Operations" lineBreakMode="tailTruncation" minimumFontSize="10" id="VI9-uT-nrJ">
<rect key="frame" x="20" y="20" width="280" height="21"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" name="GillSans-Bold" family="Gill Sans" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
<color key="shadowColor" cocoaTouchSystemColor="darkTextColor"/>
<size key="shadowOffset" width="0.0" height="1"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Advanced operations for managing your passwords." lineBreakMode="tailTruncation" numberOfLines="0" minimumFontSize="10" id="blC-Mc-59X">
<rect key="frame" x="20" y="49" width="280" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" name="GillSans-LightItalic" family="Gill Sans" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
<color key="shadowColor" cocoaTouchSystemColor="darkTextColor"/>
<size key="shadowOffset" width="0.0" height="1"/>
</label>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" image="ui_list_first.png" id="zeK-qR-5wK">
<rect key="frame" x="10" y="75" width="300" height="44"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<rect key="contentStretch" x="0.10000000000000001" y="0.20000000000000001" width="0.79999999999999982" height="0.59999999999999964"/>
</imageView>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Export" lineBreakMode="tailTruncation" minimumFontSize="10" id="DTP-qu-VqT">
<rect key="frame" x="20" y="75" width="280" height="23"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<fontDescription key="fontDescription" name="GillSans" family="Gill Sans" pointSize="18"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
<color key="shadowColor" cocoaTouchSystemColor="darkTextColor"/>
<size key="shadowOffset" width="0.0" height="1"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Save all your sites to a file." lineBreakMode="tailTruncation" minimumFontSize="10" id="oi9-2Y-2KM">
<rect key="frame" x="20" y="98" width="280" height="20"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<fontDescription key="fontDescription" name="GillSans-LightItalic" family="Gill Sans" pointSize="14"/>
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</tableViewCell>
<tableViewCell contentMode="scaleToFill" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" id="glr-eJ-qKq">
<rect key="frame" x="0.0" y="464" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="320" height="43"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" image="ui_list_last.png" id="rbd-L8-fSm">
<rect key="frame" x="10" y="0.0" width="300" height="44"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<rect key="contentStretch" x="0.10000000000000001" y="0.20000000000000001" width="0.79999999999999982" height="0.59999999999999964"/>
</imageView>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Change Master Password" lineBreakMode="tailTruncation" minimumFontSize="10" id="M38-26-Zzv">
<rect key="frame" x="20" y="0.0" width="280" height="24"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<fontDescription key="fontDescription" name="GillSans" family="Gill Sans" pointSize="18"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
<color key="shadowColor" cocoaTouchSystemColor="darkTextColor"/>
<size key="shadowOffset" width="0.0" height="1"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Change your user's login master password." lineBreakMode="tailTruncation" minimumFontSize="10" id="tUk-yd-cyq">
<rect key="frame" x="20" y="23" width="280" height="20"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<fontDescription key="fontDescription" name="GillSans-LightItalic" family="Gill Sans" pointSize="14"/>
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="oLN-6u-GLb" id="a6n-fI-h3l"/>
<outlet property="delegate" destination="oLN-6u-GLb" id="MM9-S2-8rf"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="User Profile" id="reg-hh-9Ra">
<barButtonItem key="rightBarButtonItem" title="Settings" id="Bac-IA-e0Z">
<connections>
<segue destination="Ypi-Ct-X6S" kind="push" id="vLK-dS-gos"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="avatarScrollView" destination="TMH-Xt-EnD" id="zdf-xt-piZ"/>
</connections>
</tableViewController>
</objects>
<point key="canvasLocation" x="996" y="785"/>
</scene>
</scenes>
<resources>
<image name="Square-bottom.png" width="551" height="58"/>
<image name="avatar-female-1.png" width="110" height="110"/>
<image name="avatar-female-2.png" width="110" height="110"/>
<image name="avatar-male-1.png" width="110" height="110"/>
<image name="avatar-male-2.png" width="110" height="110"/>
<image name="avatar-male-3.png" width="110" height="110"/>
<image name="background.png" width="480" height="480"/>
<image name="guide_page_0.png" width="320" height="480"/>
<image name="guide_page_1.png" width="320" height="480"/>
@@ -872,7 +1197,7 @@ L4m3P4sSw0rD</string>
<image name="icon_cancel.png" width="32" height="32"/>
<image name="icon_edit.png" width="32" height="32"/>
<image name="icon_plus.png" width="32" height="32"/>
<image name="lock_idle.png" width="100" height="100"/>
<image name="keypad.png" width="320" height="216"/>
<image name="tip_alert_black.png" width="235" height="81"/>
<image name="tip_basic_black.png" width="210" height="60"/>
<image name="tip_basic_black_top.png" width="210" height="60"/>
@@ -887,6 +1212,86 @@ L4m3P4sSw0rD</string>
<image name="ui_spinner.png" width="75" height="75"/>
<image name="ui_textfield.png" width="158" height="34"/>
</resources>
<classes>
<class className="IASKAppSettingsViewController" superclassName="UITableViewController">
<source key="sourceIdentifier" type="project" relativePath="./Classes/IASKAppSettingsViewController.h"/>
<relationships>
<relationship kind="action" name="dismiss:"/>
<relationship kind="outlet" name="delegate"/>
</relationships>
</class>
<class className="MPGuideViewController" superclassName="UIViewController">
<source key="sourceIdentifier" type="project" relativePath="./Classes/MPGuideViewController.h"/>
<relationships>
<relationship kind="action" name="close"/>
<relationship kind="outlet" name="scrollView" candidateClass="UIScrollView"/>
</relationships>
</class>
<class className="MPMainViewController" superclassName="UIViewController">
<source key="sourceIdentifier" type="project" relativePath="./Classes/MPMainViewController.h"/>
<relationships>
<relationship kind="action" name="action:" candidateClass="UIBarButtonItem"/>
<relationship kind="action" name="closeAlert"/>
<relationship kind="action" name="copyContent"/>
<relationship kind="action" name="editPassword"/>
<relationship kind="action" name="incrementPasswordCounter"/>
<relationship kind="action" name="resetPasswordCounter:" candidateClass="UILongPressGestureRecognizer"/>
<relationship kind="outlet" name="actionsTipContainer" candidateClass="UIView"/>
<relationship kind="outlet" name="alertBody" candidateClass="UITextView"/>
<relationship kind="outlet" name="alertContainer" candidateClass="UIView"/>
<relationship kind="outlet" name="alertTitle" candidateClass="UILabel"/>
<relationship kind="outlet" name="contentContainer" candidateClass="UIView"/>
<relationship kind="outlet" name="contentField" candidateClass="UITextField"/>
<relationship kind="outlet" name="contentTipBody" candidateClass="UILabel"/>
<relationship kind="outlet" name="contentTipContainer" candidateClass="UIView"/>
<relationship kind="outlet" name="contentTipEditIcon" candidateClass="UIImageView"/>
<relationship kind="outlet" name="helpContainer" candidateClass="UIView"/>
<relationship kind="outlet" name="helpView" candidateClass="UIWebView"/>
<relationship kind="outlet" name="passwordCounter" candidateClass="UILabel"/>
<relationship kind="outlet" name="passwordEdit" candidateClass="UIButton"/>
<relationship kind="outlet" name="passwordIncrementer" candidateClass="UIButton"/>
<relationship kind="outlet" name="searchResultsController" candidateClass="MPSearchDelegate"/>
<relationship kind="outlet" name="searchTipContainer" candidateClass="UIView"/>
<relationship kind="outlet" name="siteName" candidateClass="UILabel"/>
<relationship kind="outlet" name="typeButton" candidateClass="UIButton"/>
<relationship kind="outlet" name="typeTipContainer" candidateClass="UIView"/>
</relationships>
</class>
<class className="MPPreferencesViewController" superclassName="UITableViewController">
<source key="sourceIdentifier" type="project" relativePath="./Classes/MPPreferencesViewController.h"/>
<relationships>
<relationship kind="outlet" name="avatarScrollView" candidateClass="UIScrollView"/>
</relationships>
</class>
<class className="MPSearchDelegate" superclassName="NSObject">
<source key="sourceIdentifier" type="project" relativePath="./Classes/MPSearchDelegate.h"/>
<relationships>
<relationship kind="outlet" name="delegate"/>
<relationship kind="outlet" name="searchDisplayController" candidateClass="UISearchDisplayController"/>
<relationship kind="outlet" name="searchTipContainer" candidateClass="UIView"/>
</relationships>
</class>
<class className="MPTypeViewController" superclassName="UITableViewController">
<source key="sourceIdentifier" type="project" relativePath="./Classes/MPTypeViewController.h"/>
<relationships>
<relationship kind="outlet" name="recommendedTipContainer" candidateClass="UIView"/>
</relationships>
</class>
<class className="MPUnlockViewController" superclassName="UIViewController">
<source key="sourceIdentifier" type="project" relativePath="./Classes/MPUnlockViewController.h"/>
<relationships>
<relationship kind="action" name="deleteTargetedUser:" candidateClass="UILongPressGestureRecognizer"/>
<relationship kind="outlet" name="deleteTip" candidateClass="UILabel"/>
<relationship kind="outlet" name="oldUsernameLabel" candidateClass="UILabel"/>
<relationship kind="outlet" name="passwordField" candidateClass="UITextField"/>
<relationship kind="outlet" name="passwordView" candidateClass="UIView"/>
<relationship kind="outlet" name="spinner" candidateClass="UIImageView"/>
<relationship kind="outlet" name="userButtonTemplate" candidateClass="UIButton"/>
<relationship kind="outlet" name="usernameLabel" candidateClass="UILabel"/>
<relationship kind="outlet" name="usersView" candidateClass="UIScrollView"/>
</relationships>
</class>
</classes>
<simulatedMetricsContainer key="defaultSimulatedMetrics">
<nil key="statusBar"/>
<simulatedOrientationMetrics key="orientation"/>