2
0

Prepare key handling logic for sharing with OS X.

[MOVED]     Key logic now in a common class extension on MPAppDelegate
            so it can be shared between iOS and OS X apps.
[MOVED]     MPConfig for sharing between iOS and OS X apps.
[CHANGED]   keyphrase -> key.
This commit is contained in:
Maarten Billemont 2012-03-05 22:19:05 +01:00
parent 6bda70920b
commit 02ffa9611a
23 changed files with 420 additions and 251 deletions

View File

@ -13,6 +13,8 @@
DA5BFA4B147E415C00F98B1E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA5BFA4A147E415C00F98B1E /* Foundation.framework */; };
DA5BFA4D147E415C00F98B1E /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA5BFA4C147E415C00F98B1E /* CoreGraphics.framework */; };
DA5BFA4F147E415C00F98B1E /* CoreData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA5BFA4E147E415C00F98B1E /* CoreData.framework */; };
DA600C2515054F3A008E9AB6 /* MPAppDelegate_Key.m in Sources */ = {isa = PBXBuildFile; fileRef = DA600C2315054F3A008E9AB6 /* MPAppDelegate_Key.m */; };
DA600C2815056428008E9AB6 /* MPConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = DA600C2715056427008E9AB6 /* MPConfig.m */; };
DA672D2F14F92C6B004A189C /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = DA672D2E14F92C6B004A189C /* libz.dylib */; };
DA672D3014F9413D004A189C /* libPearl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DAC77CAD148291A600BCF976 /* libPearl.a */; };
DA95D59D14DF063C008D1B94 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA5BFA4A147E415C00F98B1E /* Foundation.framework */; };
@ -61,7 +63,7 @@
DAB8D46015036BCF00CED3BC /* MainStoryboard_iPhone.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DAB8D44215036BCF00CED3BC /* MainStoryboard_iPhone.storyboard */; };
DAB8D46215036BCF00CED3BC /* MasterPassword.entitlements in Resources */ = {isa = PBXBuildFile; fileRef = DAB8D44515036BCF00CED3BC /* MasterPassword.entitlements */; };
DAB8D46315036BCF00CED3BC /* MPAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = DAB8D44715036BCF00CED3BC /* MPAppDelegate.m */; };
DAB8D46415036BCF00CED3BC /* MPConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = DAB8D44915036BCF00CED3BC /* MPConfig.m */; };
DAB8D46415036BCF00CED3BC /* MPiOSConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = DAB8D44915036BCF00CED3BC /* MPiOSConfig.m */; };
DAB8D46515036BCF00CED3BC /* MPGuideViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DAB8D44B15036BCF00CED3BC /* MPGuideViewController.m */; };
DAB8D46615036BCF00CED3BC /* MPMainViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DAB8D44D15036BCF00CED3BC /* MPMainViewController.m */; };
DAB8D46715036BCF00CED3BC /* MPSearchDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = DAB8D44F15036BCF00CED3BC /* MPSearchDelegate.m */; };
@ -841,6 +843,10 @@
DA5BFA4A147E415C00F98B1E /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
DA5BFA4C147E415C00F98B1E /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
DA5BFA4E147E415C00F98B1E /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; };
DA600C2315054F3A008E9AB6 /* MPAppDelegate_Key.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPAppDelegate_Key.m; sourceTree = "<group>"; };
DA600C2415054F3A008E9AB6 /* MPAppDelegate_Key.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPAppDelegate_Key.h; sourceTree = "<group>"; };
DA600C2615056427008E9AB6 /* MPConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MPConfig.h; path = MasterPassword/MPConfig.h; sourceTree = SOURCE_ROOT; };
DA600C2715056427008E9AB6 /* MPConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MPConfig.m; path = MasterPassword/MPConfig.m; sourceTree = SOURCE_ROOT; };
DA672D2E14F92C6B004A189C /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; };
DA95D59C14DF063C008D1B94 /* libInAppSettingsKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libInAppSettingsKit.a; sourceTree = BUILT_PRODUCTS_DIR; };
DA95D5A814DF0691008D1B94 /* IASKAppSettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IASKAppSettingsViewController.h; sourceTree = "<group>"; };
@ -891,8 +897,8 @@
DAB8D44515036BCF00CED3BC /* MasterPassword.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = MasterPassword.entitlements; sourceTree = "<group>"; };
DAB8D44615036BCF00CED3BC /* MPAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPAppDelegate.h; sourceTree = "<group>"; };
DAB8D44715036BCF00CED3BC /* MPAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPAppDelegate.m; sourceTree = "<group>"; };
DAB8D44815036BCF00CED3BC /* MPConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPConfig.h; sourceTree = "<group>"; };
DAB8D44915036BCF00CED3BC /* MPConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPConfig.m; sourceTree = "<group>"; };
DAB8D44815036BCF00CED3BC /* MPiOSConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPiOSConfig.h; sourceTree = "<group>"; };
DAB8D44915036BCF00CED3BC /* MPiOSConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPiOSConfig.m; sourceTree = "<group>"; };
DAB8D44A15036BCF00CED3BC /* MPGuideViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPGuideViewController.h; sourceTree = "<group>"; };
DAB8D44B15036BCF00CED3BC /* MPGuideViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPGuideViewController.m; sourceTree = "<group>"; };
DAB8D44C15036BCF00CED3BC /* MPMainViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPMainViewController.h; sourceTree = "<group>"; };
@ -1754,6 +1760,10 @@
children = (
DAB8D43C15036BCF00CED3BC /* MasterPassword.xcdatamodeld */,
DAB8D43E15036BCF00CED3BC /* iOS */,
DA600C2415054F3A008E9AB6 /* MPAppDelegate_Key.h */,
DA600C2315054F3A008E9AB6 /* MPAppDelegate_Key.m */,
DA600C2615056427008E9AB6 /* MPConfig.h */,
DA600C2715056427008E9AB6 /* MPConfig.m */,
DAB8D45515036BCF00CED3BC /* MPElementStoredEntity.m */,
DAB8D45615036BCF00CED3BC /* MPTypes.m */,
DAB8D45715036BCF00CED3BC /* MPElementEntity.h */,
@ -1862,8 +1872,8 @@
DAB8D44215036BCF00CED3BC /* MainStoryboard_iPhone.storyboard */,
DAB8D44615036BCF00CED3BC /* MPAppDelegate.h */,
DAB8D44715036BCF00CED3BC /* MPAppDelegate.m */,
DAB8D44815036BCF00CED3BC /* MPConfig.h */,
DAB8D44915036BCF00CED3BC /* MPConfig.m */,
DAB8D44815036BCF00CED3BC /* MPiOSConfig.h */,
DAB8D44915036BCF00CED3BC /* MPiOSConfig.m */,
DAB8D44A15036BCF00CED3BC /* MPGuideViewController.h */,
DAB8D44B15036BCF00CED3BC /* MPGuideViewController.m */,
DAB8D44C15036BCF00CED3BC /* MPMainViewController.h */,
@ -3675,7 +3685,7 @@
DAB8D45D15036BCF00CED3BC /* MasterPassword.xcdatamodeld in Sources */,
DAB8D45F15036BCF00CED3BC /* main.m in Sources */,
DAB8D46315036BCF00CED3BC /* MPAppDelegate.m in Sources */,
DAB8D46415036BCF00CED3BC /* MPConfig.m in Sources */,
DAB8D46415036BCF00CED3BC /* MPiOSConfig.m in Sources */,
DAB8D46515036BCF00CED3BC /* MPGuideViewController.m in Sources */,
DAB8D46615036BCF00CED3BC /* MPMainViewController.m in Sources */,
DAB8D46715036BCF00CED3BC /* MPSearchDelegate.m in Sources */,
@ -3685,6 +3695,8 @@
DAB8D46C15036BCF00CED3BC /* MPTypes.m in Sources */,
DAB8D46D15036BCF00CED3BC /* MPElementEntity.m in Sources */,
DAB8D46E15036BCF00CED3BC /* MPElementGeneratedEntity.m in Sources */,
DA600C2515054F3A008E9AB6 /* MPAppDelegate_Key.m in Sources */,
DA600C2815056428008E9AB6 /* MPConfig.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -0,0 +1,33 @@
//
// MPAppDelegate_Key.h
// MasterPassword
//
// Created by Maarten Billemont on 24/11/11.
// Copyright (c) 2011 Lyndir. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "MPAppDelegate.h"
@interface MPAppDelegate ()
@property (strong, nonatomic) NSData *key;
@property (strong, nonatomic) NSData *keyHash;
@property (strong, nonatomic) NSString *keyHashHex;
@end
@interface MPAppDelegate (Key)
+ (MPAppDelegate *)get;
- (void)loadStoredKey;
- (void)signOut;
- (BOOL)tryMasterPassword:(NSString *)tryPassword;
- (void)updateKey:(NSData *)key;
- (void)forgetKey;
- (NSData *)keyWithLength:(NSUInteger)keyLength;
@end

View File

@ -0,0 +1,148 @@
//
// MPAppDelegate.m
// MasterPassword
//
// Created by Maarten Billemont on 24/11/11.
// Copyright (c) 2011 Lyndir. All rights reserved.
//
#import "MPAppDelegate_Key.h"
#import "MPMainViewController.h"
#import "IASKSettingsReader.h"
@implementation MPAppDelegate (Key)
static NSDictionary *keyQuery() {
static NSDictionary *MPKeyQuery = nil;
if (!MPKeyQuery)
MPKeyQuery = [PearlKeyChain createQueryForClass:kSecClassGenericPassword
attributes:[NSDictionary dictionaryWithObject:@"Master Password Key"
forKey:(__bridge id)kSecAttrService]
matches:nil];
return MPKeyQuery;
}
static NSDictionary *keyHashQuery() {
static NSDictionary *MPKeyHashQuery = nil;
if (!MPKeyHashQuery)
MPKeyHashQuery = [PearlKeyChain createQueryForClass:kSecClassGenericPassword
attributes:[NSDictionary dictionaryWithObject:@"Master Password Key Hash"
forKey:(__bridge id)kSecAttrService]
matches:nil];
return MPKeyHashQuery;
}
- (void)forgetKey {
dbg(@"Deleting master key and hash from key chain.");
[PearlKeyChain deleteItemForQuery:keyQuery()];
[PearlKeyChain deleteItemForQuery:keyHashQuery()];
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeyForgotten object:self];
#ifndef PRODUCTION
[TestFlight passCheckpoint:MPTestFlightCheckpointMPForgotten];
#endif
}
- (void)signOut {
[self updateKey:nil];
}
- (void)loadStoredKey {
if ([[MPiOSConfig get].storeKey boolValue]) {
// Key is stored in keychain. Load it.
dbg(@"Loading key from key chain.");
[self updateKey:[PearlKeyChain dataOfItemForQuery:keyQuery()]];
dbg(@" -> Key %@.", self.key? @"found": @"NOT found");
} else {
// Key should not be stored in keychain. Delete it.
dbg(@"Deleting key from key chain.");
[PearlKeyChain deleteItemForQuery:keyQuery()];
#ifndef PRODUCTION
[TestFlight passCheckpoint:MPTestFlightCheckpointMPUnstored];
#endif
}
}
+ (MPAppDelegate *)get {
return (MPAppDelegate *)[super get];
}
- (BOOL)tryMasterPassword:(NSString *)tryPassword {
NSData *keyHash = [PearlKeyChain dataOfItemForQuery:keyHashQuery()];
dbg(@"Key hash %@.", keyHash? @"known": @"NOT known");
if (![tryPassword length])
return NO;
NSData *tryKey = keyForPassword(tryPassword);
NSData *tryKeyHash = keyHashForKey(tryKey);
if (keyHash)
// A key hash is known -> a key is set.
// Make sure the user's entered key matches it.
if (![keyHash isEqual:tryKeyHash]) {
dbg(@"Key phrase hash mismatch. Expected: %@, answer: %@.", keyHash, tryKeyHash);
#ifndef PRODUCTION
[TestFlight passCheckpoint:MPTestFlightCheckpointMPMismatch];
#endif
return NO;
}
#ifndef PRODUCTION
[TestFlight passCheckpoint:MPTestFlightCheckpointMPAsked];
#endif
[self updateKey:tryKey];
return YES;
}
- (void)updateKey:(NSData *)key {
self.key = key;
if (key)
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeySet object:self];
else
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeyUnset object:self];
if (key) {
self.keyHash = keyHashForKey(key);
self.keyHashHex = [self.keyHash encodeHex];
dbg(@"Updating key hash to: %@.", self.keyHashHex);
[PearlKeyChain addOrUpdateItemForQuery:keyHashQuery()
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
self.keyHash, (__bridge id)kSecValueData,
kSecAttrAccessibleWhenUnlocked, (__bridge id)kSecAttrAccessible,
nil]];
if ([[MPiOSConfig get].storeKey boolValue]) {
dbg(@"Storing key in key chain.");
[PearlKeyChain addOrUpdateItemForQuery:keyQuery()
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
key, (__bridge id)kSecValueData,
kSecAttrAccessibleWhenUnlocked, (__bridge id)kSecAttrAccessible,
nil]];
}
#ifndef PRODUCTION
[TestFlight passCheckpoint:[NSString stringWithFormat:MPTestFlightCheckpointSetKeyphraseLength, key.length]];
#endif
}
}
- (NSData *)keyWithLength:(NSUInteger)keyLength {
return [self.key subdataWithRange:NSMakeRange(0, MIN(keyLength, self.key.length))];
}
@end

View File

@ -9,10 +9,8 @@
@interface MPConfig : PearlConfig
@property (nonatomic, retain) NSNumber *dataStoreError;
@property (nonatomic, retain) NSNumber *storeKeyPhrase;
@property (nonatomic, retain) NSNumber *rememberKeyPhrase;
@property (nonatomic, retain) NSNumber *helpHidden;
@property (nonatomic, retain) NSNumber *showQuickStart;
@property (nonatomic, retain) NSNumber *storeKey;
@property (nonatomic, retain) NSNumber *rememberKey;
+ (MPConfig *)get;

View File

@ -9,7 +9,7 @@
#import "MPConfig.h"
@implementation MPConfig
@dynamic dataStoreError, storeKeyPhrase, rememberKeyPhrase, helpHidden, showQuickStart;
@dynamic dataStoreError, storeKey, rememberKey;
- (id)init {
@ -18,10 +18,8 @@
[self.defaults registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(dataStoreError)),
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(storeKeyPhrase)),
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(rememberKeyPhrase)),
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(helpHidden)),
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(showQuickStart)),
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(storeKey)),
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(rememberKey)),
nil]];
return self;

View File

@ -7,7 +7,7 @@
//
#import "MPElementGeneratedEntity.h"
#import "MPAppDelegate.h"
#import "MPAppDelegate_Key.h"
@implementation MPElementGeneratedEntity
@ -22,7 +22,7 @@
return nil;
if (self.type & MPElementTypeClassCalculated)
return MPCalculateContent(self.type, self.name, [MPAppDelegate get].keyPhrase, self.counter);
return MPCalculateContent(self.type, self.name, [MPAppDelegate get].key, self.counter);
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:@"Unsupported type: %d", self.type] userInfo:nil];

View File

@ -7,7 +7,7 @@
//
#import "MPElementStoredEntity.h"
#import "MPAppDelegate.h"
#import "MPAppDelegate_Key.h"
@interface MPElementStoredEntity ()
@ -39,14 +39,14 @@
else
encryptedContent = self.contentObject;
NSData *decryptedContent = [encryptedContent decryptWithSymmetricKey:[[MPAppDelegate get] keyPhraseWithLength:PearlCryptKeySize]
NSData *decryptedContent = [encryptedContent decryptWithSymmetricKey:[[MPAppDelegate get] keyWithLength:PearlCryptKeySize]
usePadding:YES];
return [[NSString alloc] initWithBytes:decryptedContent.bytes length:decryptedContent.length encoding:NSUTF8StringEncoding];
}
- (void)setContent:(id)content {
NSData *encryptedContent = [[content description] encryptWithSymmetricKey:[[MPAppDelegate get] keyPhraseWithLength:PearlCryptKeySize]
NSData *encryptedContent = [[content description] encryptWithSymmetricKey:[[MPAppDelegate get] keyWithLength:PearlCryptKeySize]
usePadding:YES];
if (self.type == MPElementTypeStoredDevicePrivate) {

View File

@ -63,10 +63,10 @@ typedef enum {
#define MPNotificationKeyUnset @"MPNotificationKeyUnset"
#define MPNotificationKeyForgotten @"MPNotificationKeyForgotten"
NSData *keyPhraseForPassword(NSString *password);
NSData *keyPhraseHashForPassword(NSString *password);
NSData *keyPhraseHashForKeyPhrase(NSData *keyPhrase);
NSData *keyForPassword(NSString *password);
NSData *keyHashForPassword(NSString *password);
NSData *keyHashForKey(NSData *key);
NSString *NSStringFromMPElementType(MPElementType type);
NSString *ClassNameFromMPElementType(MPElementType type);
Class ClassFromMPElementType(MPElementType type);
NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *keyPhrase, uint16_t counter);
NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, uint16_t counter);

View File

@ -17,18 +17,18 @@
#define MP_dkLen 64
#define MP_hash PearlDigestSHA256
NSData *keyPhraseForPassword(NSString *password) {
NSData *keyForPassword(NSString *password) {
return [PearlSCrypt deriveKeyWithLength:MP_dkLen fromPassword:[password dataUsingEncoding:NSUTF8StringEncoding]
usingSalt:MP_salt N:MP_N r:MP_r p:MP_p];
}
NSData *keyPhraseHashForPassword(NSString *password) {
NSData *keyHashForPassword(NSString *password) {
return keyPhraseHashForKeyPhrase(keyPhraseForPassword(password));
return keyHashForKey(keyForPassword(password));
}
NSData *keyPhraseHashForKeyPhrase(NSData *keyPhrase) {
NSData *keyHashForKey(NSData *key) {
return [keyPhrase hashWith:MP_hash];
return [key hashWith:MP_hash];
}
NSString *NSStringFromMPElementType(MPElementType type) {
@ -100,7 +100,7 @@ NSString *ClassNameFromMPElementType(MPElementType type) {
}
static NSDictionary *MPTypes_ciphers = nil;
NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *keyPhrase, uint16_t counter) {
NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, uint16_t counter) {
assert(type & MPElementTypeClassCalculated);
@ -108,12 +108,12 @@ NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *keyPhra
MPTypes_ciphers = [NSDictionary dictionaryWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"ciphers"
withExtension:@"plist"]];
// Determine the hash whose bytes will be used for calculating a password: md4(name-keyPhrase)
assert(name && keyPhrase);
// Determine the hash whose bytes will be used for calculating a password: md4(name-key)
assert(name && key);
uint16_t ncounter = htons(counter);
NSData *keyHash = [[NSData dataByConcatenatingWithDelimitor:'-' datas:
[name dataUsingEncoding:NSUTF8StringEncoding],
keyPhrase,
key,
[NSData dataWithBytes:&ncounter length:sizeof(ncounter)],
nil] hashWith:PearlDigestSHA1];
const char *keyBytes = keyHash.bytes;

View File

@ -7,6 +7,7 @@
//
#import <Cocoa/Cocoa.h>
#import "MPPasswordWindowController.h"
@interface MPAppDelegate : NSObject <NSApplicationDelegate>
@ -16,9 +17,13 @@
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (readonly, strong, nonatomic) MPPasswordWindowController *passwordWindow;
@property (readonly, strong, nonatomic) NSData *keyPhrase;
@property (readonly, strong, nonatomic) NSString *keyPhraseHashHex;
+ (MPAppDelegate *)get;
+ (NSManagedObjectModel *)managedObjectModel;
+ (NSManagedObjectContext *)managedObjectContext;
- (IBAction)saveAction:(id)sender;
- (NSData *)keyPhraseWithLength:(NSUInteger)keyLength;

View File

@ -8,12 +8,18 @@
#import "MPAppDelegate.h"
@implementation MPAppDelegate
@interface MPAppDelegate ()
@property (readwrite, strong, nonatomic) MPPasswordWindowController *passwordWindow;
@end
@implementation MPAppDelegate
@synthesize window = _window;
@synthesize persistentStoreCoordinator = __persistentStoreCoordinator;
@synthesize managedObjectModel = __managedObjectModel;
@synthesize managedObjectContext = __managedObjectContext;
@synthesize passwordWindow;
@synthesize keyPhrase;
+ (MPAppDelegate *)get {
@ -21,9 +27,25 @@
return (MPAppDelegate *)[NSApplication sharedApplication].delegate;
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Insert code here to initialize your application
+ (NSManagedObjectContext *)managedObjectContext {
return [[self get] managedObjectContext];
}
+ (NSManagedObjectModel *)managedObjectModel {
return [[self get] managedObjectModel];
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
}
- (void)applicationDidBecomeActive:(NSNotification *)notification {
if (!self.passwordWindow)
self.passwordWindow = [[MPPasswordWindowController alloc] initWithWindowNibName:@"MPPasswordWindowController"];
[self.passwordWindow showWindow:self];
}
- (NSURL *)applicationFilesDirectory {

View File

@ -8,7 +8,7 @@
#import <Cocoa/Cocoa.h>
@interface MPPasswordWindowController : NSWindowController
@interface MPPasswordWindowController : NSWindowController <NSTextFieldDelegate>
@property (weak) IBOutlet NSTextField *siteField;
@property (weak) IBOutlet NSTextField *contentField;

View File

@ -7,20 +7,58 @@
//
#import "MPPasswordWindowController.h"
#import "MPAppDelegate.h"
@interface MPPasswordWindowController ()
@property (nonatomic, assign) BOOL completingSiteName;
@end
@implementation MPPasswordWindowController
@synthesize completingSiteName;
@synthesize siteField;
@synthesize contentField;
- (void)windowDidLoad {
[super windowDidLoad];
[self.contentField setStringValue:@""];
[[NSNotificationCenter defaultCenter] addObserverForName:NSWindowWillCloseNotification object:self.window queue:nil
usingBlock:^(NSNotification *note) {
[[NSApplication sharedApplication] hide:self];
}];
[[NSNotificationCenter defaultCenter] addObserverForName:NSControlTextDidChangeNotification object:self.siteField queue:nil
usingBlock:^(NSNotification *note) {
if (!self.completingSiteName) {
self.completingSiteName = YES;
[[[note userInfo] objectForKey:@"NSFieldEditor"] complete:nil];
self.completingSiteName = NO;
}
}];
[super windowDidLoad];
}
- (NSArray *)control:(NSControl *)control textView:(NSTextView *)textView completions:(NSArray *)words forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index {
NSString *query = [[control stringValue] substringWithRange:charRange];
NSFetchRequest *fetchRequest = [MPAppDelegate.managedObjectModel
fetchRequestFromTemplateWithName:@"MPElements"
substitutionVariables:[NSDictionary dictionaryWithObjectsAndKeys:
query, @"query",
[MPAppDelegate get].keyPhraseHashHex, @"mpHashHex",
nil]];
return [NSArray arrayWithObjects:@"cow", @"milk", @"hippopotamus", nil];
}
- (void)controlTextDidEndEditing:(NSNotification *)obj {
if (obj.object == self.siteField) {
// NSString *siteName = [self.siteField stringValue];
// [self.contentField setStringValue:];
}
}
@end

View File

@ -36,7 +36,7 @@
<string key="NSClassName">NSApplication</string>
</object>
<object class="NSWindowTemplate" id="45434518">
<int key="NSWindowStyleMask">8223</int>
<int key="NSWindowStyleMask">8215</int>
<int key="NSWindowBacking">2</int>
<string key="NSWindowRect">{{600, 530}, {480, 134}}</string>
<int key="NSWTFlags">611845120</int>
@ -68,7 +68,7 @@
<double key="NSSize">13</double>
<int key="NSfFlags">1044</int>
</object>
<string key="NSPlaceholderString">Enter site name</string>
<string key="NSPlaceholderString">Enter site name X</string>
<string key="NSCellIdentifier">_NS:9</string>
<reference key="NSControlView" ref="291791585"/>
<bool key="NSDrawsBackground">YES</bool>
@ -98,7 +98,6 @@
<int key="NSvFlags">268</int>
<string key="NSFrame">{{17, 20}, {446, 64}}</string>
<reference key="NSSuperview" ref="258451033"/>
<reference key="NSNextKeyView"/>
<int key="NSViewLayerContentsRedrawPolicy">2</int>
<string key="NSReuseIdentifierKey">_NS:9</string>
<string key="NSAntiCompressionPriority">{250, 750}</string>
@ -177,6 +176,14 @@
</object>
<int key="connectionID">39</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">delegate</string>
<reference key="source" ref="291791585"/>
<reference key="destination" ref="1001"/>
</object>
<int key="connectionID">43</int>
</object>
</array>
<object class="IBMutableOrderedSet" key="objectRecords">
<array key="orderedObjects">
@ -526,7 +533,7 @@
<nil key="activeLocalization"/>
<dictionary class="NSMutableDictionary" key="localizations"/>
<nil key="sourceID"/>
<int key="maxID">42</int>
<int key="maxID">43</int>
</object>
<object class="IBClassDescriber" key="IBDocument.Classes"/>
<int key="IBDocument.localizationMode">0</int>

View File

@ -13,11 +13,7 @@
@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;
@property (readonly, strong, nonatomic) NSData *keyPhrase;
@property (readonly, strong, nonatomic) NSData *keyPhraseHash;
@property (readonly, strong, nonatomic) NSString *keyPhraseHashHex;
+ (MPAppDelegate *)get;
+ (NSManagedObjectModel *)managedObjectModel;
+ (NSManagedObjectContext *)managedObjectContext;
@ -25,10 +21,6 @@
- (NSURL *)applicationDocumentsDirectory;
- (void)showGuide;
- (void)loadKeyPhrase:(BOOL)animated;
- (void)signOut;
- (void)forgetKeyPhrase;
- (NSData *)keyPhraseWithLength:(NSUInteger)keyLength;
- (BOOL)tryMasterPassword:(NSString *)tryPassword;
- (void)loadKey:(BOOL)animated;
@end

View File

@ -7,21 +7,14 @@
//
#import "MPAppDelegate.h"
#import "MPAppDelegate_Key.h"
#import "MPMainViewController.h"
#import "IASKSettingsReader.h"
@interface MPAppDelegate ()
@property (strong, nonatomic) NSData *keyPhrase;
@property (strong, nonatomic) NSData *keyPhraseHash;
@property (strong, nonatomic) NSString *keyPhraseHashHex;
+ (NSDictionary *)keyPhraseQuery;
+ (NSDictionary *)keyPhraseHashQuery;
- (void)loadStoredKeyPhrase;
- (void)askKeyPhrase:(BOOL)animated;
- (void)askKey:(BOOL)animated;
@end
@ -31,13 +24,13 @@
@synthesize managedObjectContext = __managedObjectContext;
@synthesize persistentStoreCoordinator = __persistentStoreCoordinator;
@synthesize keyPhrase = _keyPhrase;
@synthesize keyPhraseHash = _keyPhraseHash;
@synthesize keyPhraseHashHex = _keyPhraseHashHex;
@synthesize key;
@synthesize keyHash;
@synthesize keyHashHex;
+ (void)initialize {
[MPConfig get];
[MPiOSConfig get];
#ifdef DEBUG
[PearlLogger get].autoprintLevel = PearlLogLevelTrace;
@ -45,30 +38,6 @@
#endif
}
+ (NSDictionary *)keyPhraseQuery {
static NSDictionary *MPKeyPhraseQuery = nil;
if (!MPKeyPhraseQuery)
MPKeyPhraseQuery = [PearlKeyChain createQueryForClass:kSecClassGenericPassword
attributes:[NSDictionary dictionaryWithObject:@"MasterPassword"
forKey:(__bridge id)kSecAttrService]
matches:nil];
return MPKeyPhraseQuery;
}
+ (NSDictionary *)keyPhraseHashQuery {
static NSDictionary *MPKeyPhraseHashQuery = nil;
if (!MPKeyPhraseHashQuery)
MPKeyPhraseHashQuery = [PearlKeyChain createQueryForClass:kSecClassGenericPassword
attributes:[NSDictionary dictionaryWithObject:@"MasterPasswordHash"
forKey:(__bridge id)kSecAttrService]
matches:nil];
return MPKeyPhraseHashQuery;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
#ifndef PRODUCTION
@ -145,14 +114,14 @@
[[NSNotificationCenter defaultCenter] addObserverForName:kIASKAppSettingChanged object:nil queue:nil
usingBlock:^(NSNotification *note) {
if ([NSStringFromSelector(@selector(storeKeyPhrase))
if ([NSStringFromSelector(@selector(storeKey))
isEqualToString:[note.object description]]) {
self.keyPhrase = self.keyPhrase;
[self loadKeyPhrase:YES];
[self updateKey:self.key];
[self loadKey:YES];
}
if ([NSStringFromSelector(@selector(forgetKeyPhrase))
if ([NSStringFromSelector(@selector(forgetKey))
isEqualToString:[note.object description]])
[self loadKeyPhrase:YES];
[self loadKey:YES];
}];
#ifndef PRODUCTION
@ -175,10 +144,10 @@
- (void)applicationDidBecomeActive:(UIApplication *)application {
if ([[MPConfig get].showQuickStart boolValue])
if ([[MPiOSConfig get].showQuickStart boolValue])
[self showGuide];
else
[self loadKeyPhrase:NO];
[self loadKey:NO];
#ifndef PRODUCTION
[TestFlight passCheckpoint:MPTestFlightCheckpointActivated];
@ -194,78 +163,21 @@
#endif
}
- (void)loadKeyPhrase:(BOOL)animated {
- (void)loadKey:(BOOL)animated {
if (self.keyPhrase)
if (self.key)
return;
[self loadStoredKeyPhrase];
if (!self.keyPhrase) {
// Key phrase is not known. Ask user to set/specify it.
dbg(@"Key phrase not known. Will ask user.");
[self askKeyPhrase:animated];
[self loadStoredKey];
if (!self.key) {
// Key is not known. Ask user to set/specify it.
dbg(@"Key not known. Will ask user.");
[self askKey:animated];
return;
}
}
- (void)forgetKeyPhrase {
dbg(@"Forgetting key phrase.");
[PearlAlert showAlertWithTitle:@"Changing Master Password"
message:
@"This will allow you to log in with a different master password.\n\n"
@"Note that you will only see the sites and passwords for the master password you log in with.\n"
@"If you log in with a different master password, your current sites will be unavailable.\n\n"
@"You can always change back to your current master password later.\n"
@"Your current sites and passwords will then become available again."
viewStyle:UIAlertViewStyleDefault
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
if (buttonIndex != [alert cancelButtonIndex]) {
// Key phrase reset. Delete it.
dbg(@"Deleting master key phrase and hash from key chain.");
[PearlKeyChain deleteItemForQuery:[MPAppDelegate keyPhraseQuery]];
[PearlKeyChain deleteItemForQuery:[MPAppDelegate keyPhraseHashQuery]];
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeyForgotten object:self];
#ifndef PRODUCTION
[TestFlight passCheckpoint:MPTestFlightCheckpointMPForgotten];
#endif
}
[self loadKeyPhrase:YES];
#ifndef PRODUCTION
[TestFlight passCheckpoint:MPTestFlightCheckpointMPChanged];
#endif
}
cancelTitle:[PearlStrings get].commonButtonAbort
otherTitles:[PearlStrings get].commonButtonContinue, nil];
}
- (void)signOut {
self.keyPhrase = nil;
[self loadKeyPhrase:YES];
}
- (void)loadStoredKeyPhrase {
if ([[MPConfig get].storeKeyPhrase boolValue]) {
// Key phrase is stored in keychain. Load it.
dbg(@"Loading master key phrase from key chain.");
self.keyPhrase = [PearlKeyChain dataOfItemForQuery:[MPAppDelegate keyPhraseQuery]];
dbg(@" -> Master key phrase %@.", self.keyPhrase? @"found": @"NOT found");
} else {
// Key phrase should not be stored in keychain. Delete it.
dbg(@"Deleting master key phrase from key chain.");
[PearlKeyChain deleteItemForQuery:[MPAppDelegate keyPhraseQuery]];
#ifndef PRODUCTION
[TestFlight passCheckpoint:MPTestFlightCheckpointMPUnstored];
#endif
}
}
- (void)askKeyPhrase:(BOOL)animated {
- (void)askKey:(BOOL)animated {
dispatch_async(dispatch_get_main_queue(), ^{
[self.navigationController presentViewController:
@ -278,8 +190,8 @@
[self saveContext];
if (![[MPConfig get].rememberKeyPhrase boolValue])
self.keyPhrase = nil;
if (![[MPiOSConfig get].rememberKey boolValue])
[self updateKey:nil];
#ifndef PRODUCTION
[TestFlight passCheckpoint:MPTestFlightCheckpointDeactivated];
@ -302,12 +214,12 @@
+ (NSManagedObjectContext *)managedObjectContext {
return [(MPAppDelegate *)[UIApplication sharedApplication].delegate managedObjectContext];
return [[self get] managedObjectContext];
}
+ (NSManagedObjectModel *)managedObjectModel {
return [(MPAppDelegate *)[UIApplication sharedApplication].delegate managedObjectModel];
return [[self get] managedObjectModel];
}
- (void)saveContext {
@ -319,75 +231,6 @@
}];
}
- (BOOL)tryMasterPassword:(NSString *)tryPassword {
NSData *keyPhraseHash = [PearlKeyChain dataOfItemForQuery:[MPAppDelegate keyPhraseHashQuery]];
dbg(@"Key phrase hash %@.", keyPhraseHash? @"known": @"NOT known");
if (![tryPassword length])
return NO;
NSData *tryKeyPhrase = keyPhraseForPassword(tryPassword);
NSData *tryKeyPhraseHash = keyPhraseHashForKeyPhrase(tryKeyPhrase);
if (keyPhraseHash)
// A key phrase hash is known -> a key phrase is set.
// Make sure the user's entered key phrase matches it.
if (![keyPhraseHash isEqual:tryKeyPhraseHash]) {
dbg(@"Key phrase hash mismatch. Expected: %@, answer: %@.", keyPhraseHash, tryKeyPhraseHash);
#ifndef PRODUCTION
[TestFlight passCheckpoint:MPTestFlightCheckpointMPMismatch];
#endif
return NO;
}
#ifndef PRODUCTION
[TestFlight passCheckpoint:MPTestFlightCheckpointMPAsked];
#endif
self.keyPhrase = tryKeyPhrase;
return YES;
}
- (void)setKeyPhrase:(NSData *)keyPhrase {
_keyPhrase = keyPhrase;
if (keyPhrase)
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeySet object:self];
else
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeyUnset object:self];
if (keyPhrase) {
self.keyPhraseHash = keyPhraseHashForKeyPhrase(keyPhrase);
self.keyPhraseHashHex = [self.keyPhraseHash encodeHex];
dbg(@"Updating master key phrase hash to: %@.", self.keyPhraseHashHex);
[PearlKeyChain addOrUpdateItemForQuery:[MPAppDelegate keyPhraseHashQuery]
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
self.keyPhraseHash, (__bridge id)kSecValueData,
kSecAttrAccessibleWhenUnlocked, (__bridge id)kSecAttrAccessible,
nil]];
if ([[MPConfig get].storeKeyPhrase boolValue]) {
dbg(@"Storing master key phrase in key chain.");
[PearlKeyChain addOrUpdateItemForQuery:[MPAppDelegate keyPhraseQuery]
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
keyPhrase, (__bridge id)kSecValueData,
kSecAttrAccessibleWhenUnlocked, (__bridge id)kSecAttrAccessible,
nil]];
}
#ifndef PRODUCTION
[TestFlight passCheckpoint:[NSString stringWithFormat:MPTestFlightCheckpointSetKeyphraseLength, _keyPhrase.length]];
#endif
}
}
- (NSData *)keyPhraseWithLength:(NSUInteger)keyLength {
return [self.keyPhrase subdataWithRange:NSMakeRange(0, MIN(keyLength, self.keyPhrase.length))];
}
#pragma mark - Core Data stack
- (NSManagedObjectModel *)managedObjectModel {

View File

@ -7,7 +7,7 @@
//
#import "MPGuideViewController.h"
#import "MPAppDelegate.h"
#import "MPAppDelegate_Key.h"
@implementation MPGuideViewController
@synthesize scrollView;
@ -28,14 +28,14 @@
[super viewWillDisappear:animated];
[MPConfig get].showQuickStart = [NSNumber numberWithBool:NO];
[MPiOSConfig get].showQuickStart = [NSNumber numberWithBool:NO];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[[MPAppDelegate get] loadKeyPhrase:animated];
[[MPAppDelegate get] loadKey:animated];
}

View File

@ -7,7 +7,7 @@
//
#import "MPMainViewController.h"
#import "MPAppDelegate.h"
#import "MPAppDelegate_Key.h"
#import "MPElementGeneratedEntity.h"
#import "MPElementStoredEntity.h"
#import "IASKAppSettingsViewController.h"
@ -75,7 +75,7 @@
self.searchTipContainer.alpha = 1;
}];
[self setHelpHidden:[[MPConfig get].helpHidden boolValue] animated:animated];
[self setHelpHidden:[[MPiOSConfig get].helpHidden boolValue] animated:animated];
[self updateAnimated:animated];
}
@ -109,14 +109,14 @@
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillResignActiveNotification object:nil queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *note) {
if (![MPAppDelegate get].keyPhrase) {
if (![MPAppDelegate get].key) {
self.activeElement = nil;
[self updateAnimated:NO];
}
}];
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification object:nil queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *note) {
if (![MPAppDelegate get].keyPhrase) {
if (![MPAppDelegate get].key) {
self.activeElement = nil;
[self updateAnimated:NO];
}
@ -205,11 +205,11 @@
if (hidden) {
self.contentContainer.frame = CGRectSetHeight(self.contentContainer.frame, self.view.bounds.size.height - 44);
self.helpContainer.frame = CGRectSetY(self.helpContainer.frame, self.view.bounds.size.height);
[MPConfig get].helpHidden = [NSNumber numberWithBool:YES];
[MPiOSConfig get].helpHidden = [NSNumber numberWithBool:YES];
} else {
self.contentContainer.frame = CGRectSetHeight(self.contentContainer.frame, 175);
self.helpContainer.frame = CGRectSetY(self.helpContainer.frame, 216);
[MPConfig get].helpHidden = [NSNumber numberWithBool:NO];
[MPiOSConfig get].helpHidden = [NSNumber numberWithBool:NO];
}
}];
}
@ -373,9 +373,12 @@
#else
case 4:
#endif
{
[[MPAppDelegate get] signOut];
[[MPAppDelegate get] loadKey:YES];
break;
}
}
#ifndef PRODUCTION
[TestFlight passCheckpoint:MPTestFlightCheckpointAction];

View File

@ -7,7 +7,7 @@
//
#import "MPSearchDelegate.h"
#import "MPAppDelegate.h"
#import "MPAppDelegate_Key.h"
#import "MPElementGeneratedEntity.h"
@interface MPSearchDelegate (Private)
@ -109,12 +109,12 @@
- (void)update {
assert(self.query);
assert([MPAppDelegate get].keyPhraseHashHex);
assert([MPAppDelegate get].keyHashHex);
NSFetchRequest *fetchRequest = [[MPAppDelegate get].managedObjectModel
fetchRequestFromTemplateWithName:@"MPElements"
substitutionVariables:[NSDictionary dictionaryWithObjectsAndKeys:
self.query, @"query",
[MPAppDelegate get].keyPhraseHashHex, @"mpHashHex",
[MPAppDelegate get].keyHashHex, @"mpHashHex",
nil]];
[fetchRequest setSortDescriptors:
[NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]]];
@ -266,7 +266,7 @@
assert([element isKindOfClass:ClassFromMPElementType(element.type)]);
element.name = siteName;
element.mpHashHex = [MPAppDelegate get].keyPhraseHashHex;
element.mpHashHex = [MPAppDelegate get].keyHashHex;
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate didSelectElement:element];

View File

@ -9,7 +9,7 @@
#import <QuartzCore/QuartzCore.h>
#import "MPUnlockViewController.h"
#import "MPAppDelegate.h"
#import "MPAppDelegate_Key.h"
typedef enum {
MPLockscreenIdle,
@ -199,7 +199,27 @@ typedef enum {
- (IBAction)changeMP {
[[MPAppDelegate get] forgetKeyPhrase];
dbg(@"Forgetting key phrase.");
[PearlAlert showAlertWithTitle:@"Changing Master Password"
message:
@"This will allow you to log in with a different master password.\n\n"
@"Note that you will only see the sites and passwords for the master password you log in with.\n"
@"If you log in with a different master password, your current sites will be unavailable.\n\n"
@"You can always change back to your current master password later.\n"
@"Your current sites and passwords will then become available again."
viewStyle:UIAlertViewStyleDefault
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
if (buttonIndex != [alert cancelButtonIndex])
[[MPAppDelegate get] forgetKey];
[[MPAppDelegate get] loadKey:YES];
#ifndef PRODUCTION
[TestFlight passCheckpoint:MPTestFlightCheckpointMPChanged];
#endif
}
cancelTitle:[PearlStrings get].commonButtonAbort
otherTitles:[PearlStrings get].commonButtonContinue, nil];
}
@end

View File

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

View File

@ -0,0 +1,32 @@
//
// MPConfig.m
// MasterPassword
//
// Created by Maarten Billemont on 02/01/12.
// Copyright (c) 2012 Lyndir. All rights reserved.
//
#import "MPiOSConfig.h"
@implementation MPiOSConfig
@dynamic helpHidden, showQuickStart;
- (id)init {
if(!(self = [super init]))
return self;
[self.defaults registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(helpHidden)),
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(showQuickStart)),
nil]];
return self;
}
+ (MPiOSConfig *)get {
return (MPiOSConfig *)[super get];
}
@end

View File

@ -21,4 +21,4 @@
#import "Pearl-Prefix.pch"
#import "MPTypes.h"
#import "MPConfig.h"
#import "MPiOSConfig.h"