Importing of mpsites + renames + fixes.
[ADDED] Importing mpsites exports. [RENAMED] keyHashHex -> keyID. [RENAMED] Calculated -> Generated. [FIXED] iOS: Dismiss mail VC when done with it. [FIXED] iOS: Properly hide content tip icons when a content tip is shown while one with an icon is still active. [FIXED] iOS: Settings bundle was using old keys.
This commit is contained in:
parent
04bc7a497c
commit
21c0565619
@ -111,9 +111,9 @@ static NSDictionary *keyHashQuery() {
|
|||||||
|
|
||||||
if (key) {
|
if (key) {
|
||||||
self.keyHash = keyHashForKey(key);
|
self.keyHash = keyHashForKey(key);
|
||||||
self.keyHashHex = [self.keyHash encodeHex];
|
self.keyID = [self.keyHash encodeHex];
|
||||||
|
|
||||||
dbg(@"Updating key hash to: %@.", self.keyHashHex);
|
dbg(@"Updating key ID to: %@.", self.keyID);
|
||||||
[PearlKeyChain addOrUpdateItemForQuery:keyHashQuery()
|
[PearlKeyChain addOrUpdateItemForQuery:keyHashQuery()
|
||||||
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
self.keyHash, (__bridge id)kSecValueData,
|
self.keyHash, (__bridge id)kSecValueData,
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
@property (strong, nonatomic) NSData *key;
|
@property (strong, nonatomic) NSData *key;
|
||||||
@property (strong, nonatomic) NSData *keyHash;
|
@property (strong, nonatomic) NSData *keyHash;
|
||||||
@property (strong, nonatomic) NSString *keyHashHex;
|
@property (strong, nonatomic) NSString *keyID;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
@ -10,6 +10,14 @@
|
|||||||
|
|
||||||
#import "UbiquityStoreManager.h"
|
#import "UbiquityStoreManager.h"
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
MPImportResultSuccess,
|
||||||
|
MPImportResultCancelled,
|
||||||
|
MPImportResultInvalidPassword,
|
||||||
|
MPImportResultMalformedInput,
|
||||||
|
MPImportResultInternalError,
|
||||||
|
} MPImportResult;
|
||||||
|
|
||||||
@interface MPAppDelegate (Store) <UbiquityStoreManagerDelegate>
|
@interface MPAppDelegate (Store) <UbiquityStoreManagerDelegate>
|
||||||
|
|
||||||
+ (NSManagedObjectContext *)managedObjectContext;
|
+ (NSManagedObjectContext *)managedObjectContext;
|
||||||
@ -21,6 +29,8 @@
|
|||||||
- (void)saveContext;
|
- (void)saveContext;
|
||||||
- (void)printStore;
|
- (void)printStore;
|
||||||
|
|
||||||
|
- (MPImportResult)importSites:(NSString *)importedSitesString withPassword:(NSString *)password
|
||||||
|
askConfirmation:(BOOL(^)(NSUInteger importCount, NSUInteger deleteCount))confirmation;
|
||||||
- (NSString *)exportSitesShowingPasswords:(BOOL)showPasswords;
|
- (NSString *)exportSitesShowingPasswords:(BOOL)showPasswords;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -11,6 +11,20 @@
|
|||||||
|
|
||||||
@implementation MPAppDelegate (Store)
|
@implementation MPAppDelegate (Store)
|
||||||
|
|
||||||
|
static NSDateFormatter *rfc3339DateFormatter = nil;
|
||||||
|
|
||||||
|
- (void)loadRFC3339DateFormatter {
|
||||||
|
|
||||||
|
if (rfc3339DateFormatter)
|
||||||
|
return;
|
||||||
|
|
||||||
|
rfc3339DateFormatter = [NSDateFormatter new];
|
||||||
|
NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
|
||||||
|
[rfc3339DateFormatter setLocale:enUSPOSIXLocale];
|
||||||
|
[rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"];
|
||||||
|
[rfc3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
|
||||||
|
}
|
||||||
|
|
||||||
+ (NSManagedObjectContext *)managedObjectContext {
|
+ (NSManagedObjectContext *)managedObjectContext {
|
||||||
|
|
||||||
return [[self get] managedObjectContext];
|
return [[self get] managedObjectContext];
|
||||||
@ -83,23 +97,29 @@
|
|||||||
#if TARGET_OS_IPHONE
|
#if TARGET_OS_IPHONE
|
||||||
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillEnterForegroundNotification
|
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillEnterForegroundNotification
|
||||||
object:[UIApplication sharedApplication] queue:nil
|
object:[UIApplication sharedApplication] queue:nil
|
||||||
#else
|
|
||||||
[[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillBecomeActiveNotification
|
|
||||||
object:[NSApplication sharedApplication] queue:nil
|
|
||||||
#endif
|
|
||||||
usingBlock:^(NSNotification *note) {
|
usingBlock:^(NSNotification *note) {
|
||||||
[storeManager checkiCloudStatus];
|
[storeManager checkiCloudStatus];
|
||||||
}];
|
}];
|
||||||
|
#else
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillBecomeActiveNotification
|
||||||
|
object:[NSApplication sharedApplication] queue:nil
|
||||||
|
usingBlock:^(NSNotification *note) {
|
||||||
|
[storeManager checkiCloudStatus];
|
||||||
|
}];
|
||||||
|
#endif
|
||||||
#if TARGET_OS_IPHONE
|
#if TARGET_OS_IPHONE
|
||||||
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillTerminateNotification
|
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillTerminateNotification
|
||||||
object:[UIApplication sharedApplication] queue:nil
|
object:[UIApplication sharedApplication] queue:nil
|
||||||
#else
|
|
||||||
[[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillTerminateNotification
|
|
||||||
object:[NSApplication sharedApplication] queue:nil
|
|
||||||
#endif
|
|
||||||
usingBlock:^(NSNotification *note) {
|
usingBlock:^(NSNotification *note) {
|
||||||
[self saveContext];
|
[self saveContext];
|
||||||
}];
|
}];
|
||||||
|
#else
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillTerminateNotification
|
||||||
|
object:[NSApplication sharedApplication] queue:nil
|
||||||
|
usingBlock:^(NSNotification *note) {
|
||||||
|
[self saveContext];
|
||||||
|
}];
|
||||||
|
#endif
|
||||||
|
|
||||||
return storeManager;
|
return storeManager;
|
||||||
}
|
}
|
||||||
@ -136,27 +156,27 @@
|
|||||||
for(NSManagedObject *o in results) {
|
for(NSManagedObject *o in results) {
|
||||||
if ([o isKindOfClass:[MPElementEntity class]]) {
|
if ([o isKindOfClass:[MPElementEntity class]]) {
|
||||||
MPElementEntity *e = (MPElementEntity *)o;
|
MPElementEntity *e = (MPElementEntity *)o;
|
||||||
trc(@"For descriptor: %@, found: %@: %@ (%@)", entity.name, [o class], e.name, e.mpHashHex);
|
trc(@"For descriptor: %@, found: %@: %@ (%@)", entity.name, [o class], e.name, e.keyID);
|
||||||
} else {
|
} else {
|
||||||
trc(@"For descriptor: %@, found: %@", entity.name, [o class]);
|
trc(@"For descriptor: %@, found: %@", entity.name, [o class]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
trc(@"---");
|
trc(@"---");
|
||||||
if ([MPAppDelegate get].keyHashHex) {
|
if ([MPAppDelegate get].keyID) {
|
||||||
trc(@"=== Known sites ===");
|
trc(@"=== Known sites ===");
|
||||||
NSFetchRequest *fetchRequest = [[self managedObjectModel]
|
NSFetchRequest *fetchRequest = [[self managedObjectModel]
|
||||||
fetchRequestFromTemplateWithName:@"MPElements"
|
fetchRequestFromTemplateWithName:@"MPElements"
|
||||||
substitutionVariables:[NSDictionary dictionaryWithObjectsAndKeys:
|
substitutionVariables:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
@"", @"query",
|
@"", @"query",
|
||||||
[MPAppDelegate get].keyHashHex, @"mpHashHex",
|
[MPAppDelegate get].keyID, @"keyID",
|
||||||
nil]];
|
nil]];
|
||||||
[fetchRequest setSortDescriptors:
|
[fetchRequest setSortDescriptors:
|
||||||
[NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]]];
|
[NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]]];
|
||||||
|
|
||||||
NSError *error = nil;
|
NSError *error = nil;
|
||||||
for (MPElementEntity *e in [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error]) {
|
for (MPElementEntity *e in [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error]) {
|
||||||
trc(@"Found site: %@ (%@): %@", e.name, e.mpHashHex, e);
|
trc(@"Found site: %@ (%@): %@", e.name, e.keyID, e);
|
||||||
}
|
}
|
||||||
trc(@"---");
|
trc(@"---");
|
||||||
} else
|
} else
|
||||||
@ -164,47 +184,167 @@
|
|||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSString *)exportSitesShowingPasswords:(BOOL)showPasswords {
|
- (MPImportResult)importSites:(NSString *)importedSitesString withPassword:(NSString *)password
|
||||||
|
askConfirmation:(BOOL(^)(NSUInteger importCount, NSUInteger deleteCount))confirmation {
|
||||||
|
|
||||||
static NSDateFormatter *rfc3339DateFormatter = nil;
|
[self loadRFC3339DateFormatter];
|
||||||
if (!rfc3339DateFormatter) {
|
|
||||||
rfc3339DateFormatter = [NSDateFormatter new];
|
static NSRegularExpression *headerPattern, *sitePattern;
|
||||||
NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
|
__autoreleasing NSError *error;
|
||||||
[rfc3339DateFormatter setLocale:enUSPOSIXLocale];
|
if (!headerPattern) {
|
||||||
[rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"];
|
headerPattern = [[NSRegularExpression alloc]
|
||||||
[rfc3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
|
initWithPattern:@"^#[[:space:]]*([^:]+): (.*)"
|
||||||
|
options:0 error:&error];
|
||||||
|
if (error)
|
||||||
|
err(@"Error loading the header pattern: %@", error);
|
||||||
}
|
}
|
||||||
|
if (!sitePattern) {
|
||||||
|
sitePattern = [[NSRegularExpression alloc]
|
||||||
|
initWithPattern:@"^([^[:space:]]+)[[:space:]]+([[:digit:]]+)[[:space:]]+([[:digit:]]+)[[:space:]]+([^\t]+)\t(.*)"
|
||||||
|
options:0 error:&error];
|
||||||
|
if (error)
|
||||||
|
err(@"Error loading the site pattern: %@", error);
|
||||||
|
}
|
||||||
|
if (!headerPattern || !sitePattern)
|
||||||
|
return MPImportResultInternalError;
|
||||||
|
|
||||||
|
NSString *keyID = nil;
|
||||||
|
BOOL headerStarted = NO, headerEnded = NO;
|
||||||
|
NSArray *importedSiteLines = [importedSitesString componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
|
||||||
|
NSMutableSet *elementsToDelete = [NSMutableSet set];
|
||||||
|
NSMutableArray *importedSiteElements = [NSMutableArray arrayWithCapacity:[importedSiteLines count]];
|
||||||
|
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
|
||||||
|
for(NSString *importedSiteLine in importedSiteLines) {
|
||||||
|
if ([importedSiteLine hasPrefix:@"#"]) {
|
||||||
|
// Comment or header
|
||||||
|
if (!headerStarted) {
|
||||||
|
if ([importedSiteLine isEqualToString:@"##"])
|
||||||
|
headerStarted = YES;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (headerEnded)
|
||||||
|
continue;
|
||||||
|
if ([importedSiteLine isEqualToString:@"##"]) {
|
||||||
|
headerEnded = YES;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header
|
||||||
|
if ([headerPattern numberOfMatchesInString:importedSiteLine options:0 range:NSRangeFromString(importedSiteLine)] != 2) {
|
||||||
|
err(@"Invalid header format in line: %@", importedSiteLine);
|
||||||
|
return MPImportResultMalformedInput;
|
||||||
|
}
|
||||||
|
NSArray *headerElements = [headerPattern matchesInString:importedSiteLine options:0 range:NSRangeFromString(importedSiteLine)];
|
||||||
|
NSString *key = [importedSiteLine substringWithRange:[[headerElements objectAtIndex:0] range]];
|
||||||
|
NSString *value = [importedSiteLine substringWithRange:[[headerElements objectAtIndex:1] range]];
|
||||||
|
if ([key isEqualToString:@"Key ID"]) {
|
||||||
|
if (![(keyID = value) isEqualToString:[keyHashForPassword(password) encodeHex]])
|
||||||
|
return MPImportResultInvalidPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!headerEnded)
|
||||||
|
continue;
|
||||||
|
if (!keyID)
|
||||||
|
return MPImportResultMalformedInput;
|
||||||
|
if (![importedSiteLine length])
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Site
|
||||||
|
if ([sitePattern numberOfMatchesInString:importedSiteLine options:0 range:NSRangeFromString(importedSiteLine)] != 2) {
|
||||||
|
err(@"Invalid site format in line: %@", importedSiteLine);
|
||||||
|
return MPImportResultMalformedInput;
|
||||||
|
}
|
||||||
|
NSArray *siteElements = [headerPattern matchesInString:importedSiteLine options:0 range:NSRangeFromString(importedSiteLine)];
|
||||||
|
NSString *lastUsed = [importedSiteLine substringWithRange:[[siteElements objectAtIndex:0] range]];
|
||||||
|
NSString *uses = [importedSiteLine substringWithRange:[[siteElements objectAtIndex:1] range]];
|
||||||
|
NSString *type = [importedSiteLine substringWithRange:[[siteElements objectAtIndex:2] range]];
|
||||||
|
NSString *name = [importedSiteLine substringWithRange:[[siteElements objectAtIndex:3] range]];
|
||||||
|
NSString *exportContent = [importedSiteLine substringWithRange:[[siteElements objectAtIndex:4] range]];
|
||||||
|
|
||||||
|
// Find existing site.
|
||||||
|
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@ AND keyID == %@", name, keyID];
|
||||||
|
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.
|
||||||
|
if (!confirmation([importedSiteElements count], [elementsToDelete count]))
|
||||||
|
return MPImportResultCancelled;
|
||||||
|
|
||||||
|
// Delete existing sites.
|
||||||
|
[elementsToDelete enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {
|
||||||
|
[self.managedObjectContext deleteObject:obj];
|
||||||
|
}];
|
||||||
|
|
||||||
|
// Import new sites.
|
||||||
|
for (NSArray *siteElements in importedSiteElements) {
|
||||||
|
NSDate *lastUsed = [rfc3339DateFormatter dateFromString:[siteElements objectAtIndex:0]];
|
||||||
|
NSInteger uses = [[siteElements objectAtIndex:1] integerValue];
|
||||||
|
MPElementType type = (unsigned)[[siteElements objectAtIndex:2] integerValue];
|
||||||
|
NSString *name = [siteElements objectAtIndex:3];
|
||||||
|
NSString *exportContent = [siteElements objectAtIndex:4];
|
||||||
|
|
||||||
|
// Create new site.
|
||||||
|
MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:ClassNameFromMPElementType(type)
|
||||||
|
inManagedObjectContext:self.managedObjectContext];
|
||||||
|
element.lastUsed = [lastUsed timeIntervalSinceReferenceDate];
|
||||||
|
element.uses = uses;
|
||||||
|
element.type = type;
|
||||||
|
element.name = name;
|
||||||
|
if ([exportContent length])
|
||||||
|
[element importContent:exportContent];
|
||||||
|
}
|
||||||
|
|
||||||
|
return MPImportResultSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)exportSitesShowingPasswords:(BOOL)showPasswords {
|
||||||
|
|
||||||
|
[self loadRFC3339DateFormatter];
|
||||||
|
|
||||||
// Header.
|
// Header.
|
||||||
NSMutableString *export = [NSMutableString new];
|
NSMutableString *export = [NSMutableString new];
|
||||||
[export appendFormat:@"# MasterPassword %@\n", [PearlInfoPlist get].CFBundleVersion];
|
[export appendFormat:@"# Master Password site export\n"];
|
||||||
if (showPasswords)
|
if (showPasswords)
|
||||||
[export appendFormat:@"# Export of site names and passwords in clear-text.\n"];
|
[export appendFormat:@"# Export of site names and passwords in clear-text.\n"];
|
||||||
else
|
else
|
||||||
[export appendFormat:@"# Export of site names and stored passwords (unless device-private) encrypted with the master key.\n"];
|
[export appendFormat:@"# Export of site names and stored passwords (unless device-private) encrypted with the master key.\n"];
|
||||||
[export appendFormat:@"\n"];
|
[export appendFormat:@"# \n"];
|
||||||
[export appendFormat:@"# Key ID: %@\n", self.keyHashHex];
|
[export appendFormat:@"##\n"];
|
||||||
|
[export appendFormat:@"# Version: %@\n", [PearlInfoPlist get].CFBundleVersion];
|
||||||
|
[export appendFormat:@"# Key ID: %@\n", self.keyID];
|
||||||
[export appendFormat:@"# Date: %@\n", [rfc3339DateFormatter stringFromDate:[NSDate date]]];
|
[export appendFormat:@"# Date: %@\n", [rfc3339DateFormatter stringFromDate:[NSDate date]]];
|
||||||
if (showPasswords)
|
if (showPasswords)
|
||||||
[export appendFormat:@"# Passwords: VISIBLE\n"];
|
[export appendFormat:@"# Passwords: VISIBLE\n"];
|
||||||
else
|
else
|
||||||
[export appendFormat:@"# Passwords: PROTECTED\n"];
|
[export appendFormat:@"# Passwords: PROTECTED\n"];
|
||||||
[export appendFormat:@"\n"];
|
[export appendFormat:@"##\n"];
|
||||||
|
[export appendFormat:@"#\n"];
|
||||||
|
[export appendFormat:@"# Last Times Password Site\tSite\n"];
|
||||||
|
[export appendFormat:@"# used used type name\tpassword\n"];
|
||||||
|
|
||||||
// Sites.
|
// Sites.
|
||||||
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
|
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
|
||||||
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]];
|
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]];
|
||||||
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"mpHashHex == %@", self.keyHashHex];
|
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"keyID == %@", self.keyID];
|
||||||
__autoreleasing NSError *error = nil;
|
__autoreleasing NSError *error = nil;
|
||||||
NSArray *elements = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
|
NSArray *elements = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
|
||||||
if (error)
|
if (error)
|
||||||
err(@"Error fetching sites for export: %@", error);
|
err(@"Error fetching sites for export: %@", error);
|
||||||
|
|
||||||
for (MPElementEntity *element in elements) {
|
for (MPElementEntity *element in elements) {
|
||||||
NSString *name = element.name;
|
|
||||||
MPElementType type = (unsigned)element.type;
|
|
||||||
int16_t uses = element.uses;
|
|
||||||
NSTimeInterval lastUsed = element.lastUsed;
|
NSTimeInterval lastUsed = element.lastUsed;
|
||||||
|
int16_t uses = element.uses;
|
||||||
|
MPElementType type = (unsigned)element.type;
|
||||||
|
NSString *name = element.name;
|
||||||
NSString *content = nil;
|
NSString *content = nil;
|
||||||
|
|
||||||
// Determine the content to export.
|
// Determine the content to export.
|
||||||
@ -215,12 +355,11 @@
|
|||||||
content = element.exportContent;
|
content = element.exportContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
[export appendFormat:@"%@\t%d\t%d\t%@\t%@\n",
|
[export appendFormat:@"%@ %8d %8d %20s\t%@\n",
|
||||||
name, type, uses, [rfc3339DateFormatter stringFromDate:[NSDate dateWithTimeIntervalSinceReferenceDate:lastUsed]], content];
|
[rfc3339DateFormatter stringFromDate:[NSDate dateWithTimeIntervalSinceReferenceDate:lastUsed]], uses, type, [name cStringUsingEncoding:NSUTF8StringEncoding], content? content: @""];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return export;
|
return export;
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -13,14 +13,15 @@
|
|||||||
@interface MPElementEntity : NSManagedObject
|
@interface MPElementEntity : NSManagedObject
|
||||||
|
|
||||||
@property (nonatomic, retain) NSString *name;
|
@property (nonatomic, retain) NSString *name;
|
||||||
@property (nonatomic, retain) NSString *mpHashHex;
|
@property (nonatomic, retain) NSString *keyID;
|
||||||
@property (nonatomic, assign) int16_t type;
|
@property (nonatomic, assign) int16_t type;
|
||||||
@property (nonatomic, assign) int16_t uses;
|
@property (nonatomic, assign) int16_t uses;
|
||||||
@property (nonatomic, assign) NSTimeInterval lastUsed;
|
@property (nonatomic, assign) NSTimeInterval lastUsed;
|
||||||
|
|
||||||
@property (nonatomic, retain, readonly) id content;
|
@property (nonatomic, retain, readonly) id content;
|
||||||
@property (nonatomic, retain, readonly) NSString *exportContent;
|
|
||||||
|
|
||||||
- (int16_t)use;
|
- (int16_t)use;
|
||||||
|
- (NSString *)exportContent;
|
||||||
|
- (void)importContent:(NSString *)content;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
@implementation MPElementEntity
|
@implementation MPElementEntity
|
||||||
|
|
||||||
@dynamic name;
|
@dynamic name;
|
||||||
@dynamic mpHashHex;
|
@dynamic keyID;
|
||||||
@dynamic type;
|
@dynamic type;
|
||||||
@dynamic uses;
|
@dynamic uses;
|
||||||
@dynamic lastUsed;
|
@dynamic lastUsed;
|
||||||
@ -33,6 +33,10 @@
|
|||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)importContent:(NSString *)content {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
- (NSString *)description {
|
- (NSString *)description {
|
||||||
|
|
||||||
return str(@"%@:%@", [self class], [self name]);
|
return str(@"%@:%@", [self class], [self name]);
|
||||||
@ -40,8 +44,8 @@
|
|||||||
|
|
||||||
- (NSString *)debugDescription {
|
- (NSString *)debugDescription {
|
||||||
|
|
||||||
return str(@"{%@: name=%@, mpHashHex=%@, type=%d, uses=%d, lastUsed=%@}",
|
return str(@"{%@: name=%@, keyID=%@, type=%d, uses=%d, lastUsed=%@}",
|
||||||
NSStringFromClass([self class]), self.name, self.mpHashHex, self.type, self.uses, self.lastUsed);
|
NSStringFromClass([self class]), self.name, self.keyID, self.type, self.uses, self.lastUsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
- (id)content {
|
- (id)content {
|
||||||
|
|
||||||
if (!(self.type & MPElementTypeClassCalculated)) {
|
if (!(self.type & MPElementTypeClassGenerated)) {
|
||||||
err(@"Corrupt element: %@, type: %d, does not match class: %@", self.name, self.type, [self class]);
|
err(@"Corrupt element: %@, type: %d, does not match class: %@", self.name, self.type, [self class]);
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
@ -67,4 +67,9 @@
|
|||||||
return [self.contentObject encodeBase64];
|
return [self.contentObject encodeBase64];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)importContent:(NSString *)content {
|
||||||
|
|
||||||
|
self.contentObject = [content decodeBase64];
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -37,8 +37,8 @@ typedef enum {
|
|||||||
MPElementTypeGeneratedBasic = 0x3 | MPElementTypeClassGenerated | 0x0,
|
MPElementTypeGeneratedBasic = 0x3 | MPElementTypeClassGenerated | 0x0,
|
||||||
MPElementTypeGeneratedPIN = 0x4 | MPElementTypeClassGenerated | 0x0,
|
MPElementTypeGeneratedPIN = 0x4 | MPElementTypeClassGenerated | 0x0,
|
||||||
|
|
||||||
MPElementTypeStoredPersonal = 0x0 | MPElementTypeClassStored | MPElementFeatureExportContent | MPElementFeatureDevicePrivate,
|
MPElementTypeStoredPersonal = 0x0 | MPElementTypeClassStored | MPElementFeatureExportContent,
|
||||||
MPElementTypeStoredDevicePrivate = 0x1 | MPElementTypeClassStored | 0x0,
|
MPElementTypeStoredDevicePrivate = 0x1 | MPElementTypeClassStored | MPElementFeatureDevicePrivate,
|
||||||
} MPElementType;
|
} MPElementType;
|
||||||
|
|
||||||
#define MPTestFlightCheckpointAction @"MPTestFlightCheckpointAction"
|
#define MPTestFlightCheckpointAction @"MPTestFlightCheckpointAction"
|
||||||
|
@ -38,19 +38,19 @@ NSString *NSStringFromMPElementType(MPElementType type) {
|
|||||||
return nil;
|
return nil;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case MPElementTypeCalculatedLong:
|
case MPElementTypeGeneratedLong:
|
||||||
return @"Long Password";
|
return @"Long Password";
|
||||||
|
|
||||||
case MPElementTypeCalculatedMedium:
|
case MPElementTypeGeneratedMedium:
|
||||||
return @"Medium Password";
|
return @"Medium Password";
|
||||||
|
|
||||||
case MPElementTypeCalculatedShort:
|
case MPElementTypeGeneratedShort:
|
||||||
return @"Short Password";
|
return @"Short Password";
|
||||||
|
|
||||||
case MPElementTypeCalculatedBasic:
|
case MPElementTypeGeneratedBasic:
|
||||||
return @"Basic Password";
|
return @"Basic Password";
|
||||||
|
|
||||||
case MPElementTypeCalculatedPIN:
|
case MPElementTypeGeneratedPIN:
|
||||||
return @"PIN";
|
return @"PIN";
|
||||||
|
|
||||||
case MPElementTypeStoredPersonal:
|
case MPElementTypeStoredPersonal:
|
||||||
@ -70,19 +70,19 @@ Class ClassFromMPElementType(MPElementType type) {
|
|||||||
return nil;
|
return nil;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case MPElementTypeCalculatedLong:
|
case MPElementTypeGeneratedLong:
|
||||||
return [MPElementGeneratedEntity class];
|
return [MPElementGeneratedEntity class];
|
||||||
|
|
||||||
case MPElementTypeCalculatedMedium:
|
case MPElementTypeGeneratedMedium:
|
||||||
return [MPElementGeneratedEntity class];
|
return [MPElementGeneratedEntity class];
|
||||||
|
|
||||||
case MPElementTypeCalculatedShort:
|
case MPElementTypeGeneratedShort:
|
||||||
return [MPElementGeneratedEntity class];
|
return [MPElementGeneratedEntity class];
|
||||||
|
|
||||||
case MPElementTypeCalculatedBasic:
|
case MPElementTypeGeneratedBasic:
|
||||||
return [MPElementGeneratedEntity class];
|
return [MPElementGeneratedEntity class];
|
||||||
|
|
||||||
case MPElementTypeCalculatedPIN:
|
case MPElementTypeGeneratedPIN:
|
||||||
return [MPElementGeneratedEntity class];
|
return [MPElementGeneratedEntity class];
|
||||||
|
|
||||||
case MPElementTypeStoredPersonal:
|
case MPElementTypeStoredPersonal:
|
||||||
@ -108,8 +108,8 @@ NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, in
|
|||||||
err(@"Missing name.");
|
err(@"Missing name.");
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
if (!(type & MPElementTypeClassCalculated)) {
|
if (!(type & MPElementTypeClassGenerated)) {
|
||||||
err(@"Incorrect type (is not MPElementTypeClassCalculated): %d, for: %@", type, name);
|
err(@"Incorrect type (is not MPElementTypeClassGenerated): %d, for: %@", type, name);
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
if (!key) {
|
if (!key) {
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
@synthesize key;
|
@synthesize key;
|
||||||
@synthesize keyHash;
|
@synthesize keyHash;
|
||||||
@synthesize keyHashHex;
|
@synthesize keyID;
|
||||||
|
|
||||||
#pragma GCC diagnostic ignored "-Wfour-char-constants"
|
#pragma GCC diagnostic ignored "-Wfour-char-constants"
|
||||||
static EventHotKeyID MPShowHotKey = { .signature = 'show', .id = 1 };
|
static EventHotKeyID MPShowHotKey = { .signature = 'show', .id = 1 };
|
||||||
|
@ -110,14 +110,14 @@
|
|||||||
- (NSArray *)control:(NSControl *)control textView:(NSTextView *)textView completions:(NSArray *)words forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index {
|
- (NSArray *)control:(NSControl *)control textView:(NSTextView *)textView completions:(NSArray *)words forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index {
|
||||||
|
|
||||||
NSString *query = [[control stringValue] substringWithRange:charRange];
|
NSString *query = [[control stringValue] substringWithRange:charRange];
|
||||||
if (![query length] || ![MPAppDelegate get].keyHashHex)
|
if (![query length] || ![MPAppDelegate get].keyID)
|
||||||
return nil;
|
return nil;
|
||||||
|
|
||||||
NSFetchRequest *fetchRequest = [MPAppDelegate.managedObjectModel
|
NSFetchRequest *fetchRequest = [MPAppDelegate.managedObjectModel
|
||||||
fetchRequestFromTemplateWithName:@"MPElements"
|
fetchRequestFromTemplateWithName:@"MPElements"
|
||||||
substitutionVariables:[NSDictionary dictionaryWithObjectsAndKeys:
|
substitutionVariables:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
query, @"query",
|
query, @"query",
|
||||||
[MPAppDelegate get].keyHashHex, @"mpHashHex",
|
[MPAppDelegate get].keyID, @"keyID",
|
||||||
nil]];
|
nil]];
|
||||||
[fetchRequest setSortDescriptors:
|
[fetchRequest setSortDescriptors:
|
||||||
[NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]]];
|
[NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]]];
|
||||||
@ -222,10 +222,10 @@
|
|||||||
MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPElementGeneratedEntity class])
|
MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPElementGeneratedEntity class])
|
||||||
inManagedObjectContext:[MPAppDelegate get].managedObjectContext];
|
inManagedObjectContext:[MPAppDelegate get].managedObjectContext];
|
||||||
assert([element isKindOfClass:ClassFromMPElementType(element.type)]);
|
assert([element isKindOfClass:ClassFromMPElementType(element.type)]);
|
||||||
assert([MPAppDelegate get].keyHashHex);
|
assert([MPAppDelegate get].keyID);
|
||||||
|
|
||||||
element.name = siteName;
|
element.name = siteName;
|
||||||
element.mpHashHex = [MPAppDelegate get].keyHashHex;
|
element.keyID = [MPAppDelegate get].keyID;
|
||||||
|
|
||||||
NSString *description = [element.content description];
|
NSString *description = [element.content description];
|
||||||
[element use];
|
[element use];
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<model name="" userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1170" systemVersion="11D50" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
|
<model name="" userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1171" systemVersion="11D50" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
|
||||||
<entity name="MPElementEntity" representedClassName="MPElementEntity" isAbstract="YES" syncable="YES">
|
<entity name="MPElementEntity" representedClassName="MPElementEntity" isAbstract="YES" syncable="YES">
|
||||||
|
<attribute name="keyID" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
<attribute name="lastUsed" attributeType="Date" syncable="YES"/>
|
<attribute name="lastUsed" attributeType="Date" syncable="YES"/>
|
||||||
<attribute name="mpHashHex" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" indexed="YES" syncable="YES"/>
|
<attribute name="name" attributeType="String" indexed="YES" syncable="YES"/>
|
||||||
<attribute name="type" attributeType="Integer 16" defaultValueString="257" syncable="YES"/>
|
<attribute name="type" attributeType="Integer 16" defaultValueString="16" syncable="YES"/>
|
||||||
<attribute name="uses" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
|
<attribute name="uses" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="MPElementGeneratedEntity" representedClassName="MPElementGeneratedEntity" parentEntity="MPElementEntity" syncable="YES">
|
<entity name="MPElementGeneratedEntity" representedClassName="MPElementGeneratedEntity" parentEntity="MPElementEntity" syncable="YES">
|
||||||
@ -13,9 +13,8 @@
|
|||||||
<entity name="MPElementStoredEntity" representedClassName="MPElementStoredEntity" parentEntity="MPElementEntity" syncable="YES">
|
<entity name="MPElementStoredEntity" representedClassName="MPElementStoredEntity" parentEntity="MPElementEntity" syncable="YES">
|
||||||
<attribute name="contentObject" optional="YES" attributeType="Transformable" storedInTruthFile="YES" syncable="YES"/>
|
<attribute name="contentObject" optional="YES" attributeType="Transformable" storedInTruthFile="YES" syncable="YES"/>
|
||||||
</entity>
|
</entity>
|
||||||
<fetchRequest name="MPElements" entity="MPElementEntity" predicateString="($query == '' OR name BEGINSWITH[cd] $query) AND mpHashHex == $mpHashHex"/>
|
|
||||||
<elements>
|
<elements>
|
||||||
<element name="MPElementEntity" positionX="160" positionY="192" width="128" height="135"/>
|
<element name="MPElementEntity" positionX="160" positionY="192" width="128" height="120"/>
|
||||||
<element name="MPElementGeneratedEntity" positionX="160" positionY="192" width="128" height="60"/>
|
<element name="MPElementGeneratedEntity" positionX="160" positionY="192" width="128" height="60"/>
|
||||||
<element name="MPElementStoredEntity" positionX="160" positionY="192" width="128" height="60"/>
|
<element name="MPElementStoredEntity" positionX="160" positionY="192" width="128" height="60"/>
|
||||||
</elements>
|
</elements>
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
@synthesize key;
|
@synthesize key;
|
||||||
@synthesize keyHash;
|
@synthesize keyHash;
|
||||||
@synthesize keyHashHex;
|
@synthesize keyID;
|
||||||
|
|
||||||
+ (void)initialize {
|
+ (void)initialize {
|
||||||
|
|
||||||
@ -43,6 +43,75 @@
|
|||||||
//[NSClassFromString(@"WebView") performSelector:NSSelectorFromString(@"_enableRemoteInspector")];
|
//[NSClassFromString(@"WebView") performSelector:NSSelectorFromString(@"_enableRemoteInspector")];
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
- (void)showGuide {
|
||||||
|
|
||||||
|
[self.navigationController performSegueWithIdentifier:@"MP_Guide" sender:self];
|
||||||
|
|
||||||
|
[TestFlight passCheckpoint:MPTestFlightCheckpointShowGuide];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)loadKey:(BOOL)animated {
|
||||||
|
|
||||||
|
if (!self.key)
|
||||||
|
// Try and load the key from the keychain.
|
||||||
|
[self loadStoredKey];
|
||||||
|
|
||||||
|
if (!self.key)
|
||||||
|
// Ask the user to set the key through his master password.
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[self.navigationController presentViewController:
|
||||||
|
[self.navigationController.storyboard instantiateViewControllerWithIdentifier:@"MPUnlockViewController"]
|
||||||
|
animated:animated completion:nil];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)export {
|
||||||
|
|
||||||
|
[PearlAlert showNotice:
|
||||||
|
@"This will export all your site names.\n\n"
|
||||||
|
@"You can open the export with a text editor to get an overview of all your sites.\n\n"
|
||||||
|
@"The file also acts as a personal backup of your site list in case you don't sync with iCloud/iTunes."
|
||||||
|
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||||
|
[PearlAlert showAlertWithTitle:@"Reveal Passwords?" message:
|
||||||
|
@"Would you like to make all your passwords visible in the export?\n\n"
|
||||||
|
@"A safe export will only include your stored passwords, in an encrypted manner, "
|
||||||
|
@"making the result safe from falling in the wrong hands.\n\n"
|
||||||
|
@"If all your passwords are shown and somebody else finds the export, "
|
||||||
|
@"they could gain access to all your sites!"
|
||||||
|
viewStyle:UIAlertViewStyleDefault tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||||
|
if (buttonIndex == [alert firstOtherButtonIndex] + 0)
|
||||||
|
// Safe Export
|
||||||
|
[self exportShowPasswords:NO];
|
||||||
|
if (buttonIndex == [alert firstOtherButtonIndex] + 1)
|
||||||
|
// Safe Export
|
||||||
|
[self exportShowPasswords:YES];
|
||||||
|
} cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Safe Export", @"Show Passwords", nil];
|
||||||
|
} otherTitles:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)exportShowPasswords:(BOOL)showPasswords {
|
||||||
|
|
||||||
|
NSString *exportedSites = [self exportSitesShowingPasswords:showPasswords];
|
||||||
|
NSString *message;
|
||||||
|
if (showPasswords)
|
||||||
|
message = @"Export of my Master Password sites with passwords visible.\n\nREMINDER: Make sure nobody else sees this file!\n";
|
||||||
|
else
|
||||||
|
message = @"Backup of my Master Password sites.\n";
|
||||||
|
|
||||||
|
NSDateFormatter *exportDateFormatter = [NSDateFormatter new];
|
||||||
|
[exportDateFormatter setDateFormat:@"'Master Password sites ('yyyy'-'MM'-'DD').mpsites'"];
|
||||||
|
|
||||||
|
MFMailComposeViewController *composer = [[MFMailComposeViewController alloc] init];
|
||||||
|
[composer setMailComposeDelegate:self];
|
||||||
|
[composer setSubject:@"Master Password site export"];
|
||||||
|
[composer setMessageBody:message isHTML:NO];
|
||||||
|
[composer addAttachmentData:[exportedSites dataUsingEncoding:NSUTF8StringEncoding] mimeType:@"text/plain"
|
||||||
|
fileName:[exportDateFormatter stringFromDate:[NSDate date]]];
|
||||||
|
[self.window.rootViewController presentModalViewController:composer animated:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - UIApplicationDelegate
|
||||||
|
|
||||||
|
|
||||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
||||||
|
|
||||||
@ -188,6 +257,69 @@
|
|||||||
return [super application:application didFinishLaunchingWithOptions:launchOptions];
|
return [super application:application didFinishLaunchingWithOptions:launchOptions];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
|
||||||
|
sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
|
||||||
|
|
||||||
|
__autoreleasing NSError *error;
|
||||||
|
__autoreleasing NSURLResponse *response;
|
||||||
|
NSData *importedSitesData = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:url]
|
||||||
|
returningResponse:&response error:&error];
|
||||||
|
if (error)
|
||||||
|
err(@"While reading imported sites from %@: %@", url, error);
|
||||||
|
if (!importedSitesData)
|
||||||
|
return NO;
|
||||||
|
|
||||||
|
NSString *importedSitesString = [[NSString alloc] initWithData:importedSitesData encoding:NSUTF8StringEncoding];
|
||||||
|
[PearlAlert showAlertWithTitle:@"Import Password" message:
|
||||||
|
@"Enter the master password for this export:"
|
||||||
|
viewStyle:UIAlertViewStyleSecureTextInput 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
|
||||||
|
askConfirmation:^BOOL(NSUInteger importCount, NSUInteger deleteCount) {
|
||||||
|
__block BOOL confirmation = NO;
|
||||||
|
|
||||||
|
dispatch_group_t confirmationGroup = dispatch_group_create();
|
||||||
|
dispatch_group_enter(confirmationGroup);
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[PearlAlert showAlertWithTitle:@"Import Sites?"
|
||||||
|
message:l(@"Import %d sites, overwriting %d existing sites?", importCount, deleteCount)
|
||||||
|
viewStyle:UIAlertViewStyleDefault
|
||||||
|
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||||
|
if (buttonIndex != [alert cancelButtonIndex])
|
||||||
|
confirmation = YES;
|
||||||
|
|
||||||
|
dispatch_group_leave(confirmationGroup);
|
||||||
|
}
|
||||||
|
cancelTitle:[PearlStrings get].commonButtonCancel
|
||||||
|
otherTitles:@"Import", nil];
|
||||||
|
});
|
||||||
|
dispatch_group_wait(confirmationGroup, DISPATCH_TIME_FOREVER);
|
||||||
|
|
||||||
|
return confirmation;
|
||||||
|
}];
|
||||||
|
|
||||||
|
switch (result) {
|
||||||
|
case MPImportResultSuccess:
|
||||||
|
case MPImportResultCancelled:
|
||||||
|
break;
|
||||||
|
case MPImportResultInternalError:
|
||||||
|
[PearlAlert showError:@"Import failed because of an internal error."];
|
||||||
|
break;
|
||||||
|
case MPImportResultMalformedInput:
|
||||||
|
[PearlAlert showError:@"The import doesn't look like a Master Password export."];
|
||||||
|
break;
|
||||||
|
case MPImportResultInvalidPassword:
|
||||||
|
[PearlAlert showError:@"Incorrect master password for the import sites."];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Unlock File", nil];
|
||||||
|
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
- (void)applicationDidBecomeActive:(UIApplication *)application {
|
- (void)applicationDidBecomeActive:(UIApplication *)application {
|
||||||
|
|
||||||
if ([[MPiOSConfig get].showQuickStart boolValue])
|
if ([[MPiOSConfig get].showQuickStart boolValue])
|
||||||
@ -198,70 +330,6 @@
|
|||||||
[TestFlight passCheckpoint:MPTestFlightCheckpointActivated];
|
[TestFlight passCheckpoint:MPTestFlightCheckpointActivated];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)showGuide {
|
|
||||||
|
|
||||||
[self.navigationController performSegueWithIdentifier:@"MP_Guide" sender:self];
|
|
||||||
|
|
||||||
[TestFlight passCheckpoint:MPTestFlightCheckpointShowGuide];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)loadKey:(BOOL)animated {
|
|
||||||
|
|
||||||
if (!self.key)
|
|
||||||
// Try and load the key from the keychain.
|
|
||||||
[self loadStoredKey];
|
|
||||||
|
|
||||||
if (!self.key)
|
|
||||||
// Ask the user to set the key through his master password.
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
[self.navigationController presentViewController:
|
|
||||||
[self.navigationController.storyboard instantiateViewControllerWithIdentifier:@"MPUnlockViewController"]
|
|
||||||
animated:animated completion:nil];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)export {
|
|
||||||
|
|
||||||
[PearlAlert showNotice:
|
|
||||||
@"This export contains the names of all your sites. "
|
|
||||||
@"You can even open it in a text editor to view its contents.\n\n"
|
|
||||||
@"If you ever loose your device and don't have iCloud enabled or sync with iTunes, "
|
|
||||||
@"this will help you remember what sites you had an account with.\n"
|
|
||||||
@"Don't worry: Even if you don't have an export of your sites, "
|
|
||||||
@"loosing your device never means loosing your generated passwords."];
|
|
||||||
[PearlAlert showAlertWithTitle:@"Reveal Passwords?" message:
|
|
||||||
@"Would you like to make all your passwords visible in the export?\n\n"
|
|
||||||
@"By default, only your stored passwords are exported, in an encrypted manner, "
|
|
||||||
@"making it safe from falling in the wrong hands.\n"
|
|
||||||
@"If you make all your passwords visible and somebody else finds the file, "
|
|
||||||
@"they can gain access to all your sites with it!"
|
|
||||||
viewStyle:UIAlertViewStyleDefault tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
|
||||||
if (buttonIndex == [alert firstOtherButtonIndex] + 0)
|
|
||||||
// Safe Export
|
|
||||||
[self exportShowPasswords:NO];
|
|
||||||
if (buttonIndex == [alert firstOtherButtonIndex] + 1)
|
|
||||||
// Safe Export
|
|
||||||
[self exportShowPasswords:YES];
|
|
||||||
} cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Safe Export", @"Show Passwords", nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)exportShowPasswords:(BOOL)showPasswords {
|
|
||||||
|
|
||||||
NSString *exportedSites = [self exportSitesShowingPasswords:showPasswords];
|
|
||||||
|
|
||||||
NSDateFormatter *exportDateFormatter = [NSDateFormatter new];
|
|
||||||
[exportDateFormatter setDateFormat:@"yyyy'-'MM'-'DD'T'HH':'mm'.mpsites'"];
|
|
||||||
|
|
||||||
MFMailComposeViewController *composer = [[MFMailComposeViewController alloc] init];
|
|
||||||
[composer setMailComposeDelegate:self];
|
|
||||||
[composer setSubject:@"Master Password site export"];
|
|
||||||
[composer addAttachmentData:[exportedSites dataUsingEncoding:NSUTF8StringEncoding] mimeType:@"text/plain"
|
|
||||||
fileName:[exportDateFormatter stringFromDate:[NSDate date]]];
|
|
||||||
[self.window.rootViewController presentModalViewController:composer animated:YES];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - UIApplicationDelegate
|
|
||||||
|
|
||||||
- (void)applicationDidEnterBackground:(UIApplication *)application {
|
- (void)applicationDidEnterBackground:(UIApplication *)application {
|
||||||
|
|
||||||
[[LocalyticsSession sharedLocalyticsSession] close];
|
[[LocalyticsSession sharedLocalyticsSession] close];
|
||||||
@ -314,10 +382,16 @@
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case MFMailComposeResultFailed:
|
case MFMailComposeResultFailed:
|
||||||
break;
|
[PearlAlert showError:@"A problem occurred while sending the message." tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||||
|
if (buttonIndex == [alert firstOtherButtonIndex])
|
||||||
|
return;
|
||||||
|
} otherTitles:@"Retry", nil];
|
||||||
|
return;
|
||||||
case MFMailComposeResultCancelled:
|
case MFMailComposeResultCancelled:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[controller dismissModalViewControllerAnimated:YES];
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - TestFlight
|
#pragma mark - TestFlight
|
||||||
|
@ -32,6 +32,8 @@
|
|||||||
@property (weak, nonatomic) IBOutlet UIImageView *contentTipEditIcon;
|
@property (weak, nonatomic) IBOutlet UIImageView *contentTipEditIcon;
|
||||||
@property (weak, nonatomic) IBOutlet UIView *searchTipContainer;
|
@property (weak, nonatomic) IBOutlet UIView *searchTipContainer;
|
||||||
|
|
||||||
|
@property (copy) void (^contentTipCleanup)(BOOL finished);
|
||||||
|
|
||||||
- (IBAction)copyContent;
|
- (IBAction)copyContent;
|
||||||
- (IBAction)incrementPasswordCounter;
|
- (IBAction)incrementPasswordCounter;
|
||||||
- (IBAction)resetPasswordCounter:(UILongPressGestureRecognizer *)sender;
|
- (IBAction)resetPasswordCounter:(UILongPressGestureRecognizer *)sender;
|
||||||
|
@ -45,6 +45,7 @@
|
|||||||
@synthesize contentTipEditIcon = _contentTipEditIcon;
|
@synthesize contentTipEditIcon = _contentTipEditIcon;
|
||||||
@synthesize searchTipContainer = _searchTip;
|
@synthesize searchTipContainer = _searchTip;
|
||||||
@synthesize contentField = _contentField;
|
@synthesize contentField = _contentField;
|
||||||
|
@synthesize contentTipCleanup;
|
||||||
|
|
||||||
#pragma mark - View lifecycle
|
#pragma mark - View lifecycle
|
||||||
|
|
||||||
@ -161,8 +162,8 @@
|
|||||||
[self setHelpChapter:self.activeElement? @"2": @"1"];
|
[self setHelpChapter:self.activeElement? @"2": @"1"];
|
||||||
self.siteName.text = self.activeElement.name;
|
self.siteName.text = self.activeElement.name;
|
||||||
|
|
||||||
self.passwordCounter.alpha = self.activeElement.type & MPElementTypeClassCalculated? 0.5f: 0;
|
self.passwordCounter.alpha = self.activeElement.type & MPElementTypeClassGenerated? 0.5f: 0;
|
||||||
self.passwordIncrementer.alpha = self.activeElement.type & MPElementTypeClassCalculated? 0.5f: 0;
|
self.passwordIncrementer.alpha = self.activeElement.type & MPElementTypeClassGenerated? 0.5f: 0;
|
||||||
self.passwordEdit.alpha = self.activeElement.type & MPElementTypeClassStored? 0.5f: 0;
|
self.passwordEdit.alpha = self.activeElement.type & MPElementTypeClassStored? 0.5f: 0;
|
||||||
|
|
||||||
[self.typeButton setTitle:NSStringFromMPElementType((unsigned)self.activeElement.type)
|
[self.typeButton setTitle:NSStringFromMPElementType((unsigned)self.activeElement.type)
|
||||||
@ -235,7 +236,14 @@
|
|||||||
- (void)showContentTip:(NSString *)message withIcon:(UIImageView *)icon {
|
- (void)showContentTip:(NSString *)message withIcon:(UIImageView *)icon {
|
||||||
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
if (self.contentTipCleanup)
|
||||||
|
self.contentTipCleanup(NO);
|
||||||
|
|
||||||
self.contentTipBody.text = message;
|
self.contentTipBody.text = message;
|
||||||
|
self.contentTipCleanup = ^(BOOL finished) {
|
||||||
|
icon.hidden = YES;
|
||||||
|
self.contentTipCleanup = nil;
|
||||||
|
};
|
||||||
|
|
||||||
icon.hidden = NO;
|
icon.hidden = NO;
|
||||||
[UIView animateWithDuration:0.2f animations:^{
|
[UIView animateWithDuration:0.2f animations:^{
|
||||||
@ -246,10 +254,7 @@
|
|||||||
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
|
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
|
||||||
[UIView animateWithDuration:0.2f animations:^{
|
[UIView animateWithDuration:0.2f animations:^{
|
||||||
self.contentTipContainer.alpha = 0;
|
self.contentTipContainer.alpha = 0;
|
||||||
} completion:^(BOOL finished) {
|
} completion:self.contentTipCleanup];
|
||||||
if (finished)
|
|
||||||
icon.hidden = YES;
|
|
||||||
}];
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
@ -441,7 +446,7 @@
|
|||||||
[TestFlight passCheckpoint:MPTestFlightCheckpointAction];
|
[TestFlight passCheckpoint:MPTestFlightCheckpointAction];
|
||||||
} cancelTitle:[PearlStrings get].commonButtonCancel destructiveTitle:nil
|
} cancelTitle:[PearlStrings get].commonButtonCancel destructiveTitle:nil
|
||||||
otherTitles:
|
otherTitles:
|
||||||
[self isHelpVisible]? @"Hide Help": @"Show Help", @"FAQ", @"Tutorial", @"Settings", @"Export", @"Import",
|
[self isHelpVisible]? @"Hide Help": @"Show Help", @"FAQ", @"Tutorial", @"Settings", @"Export",
|
||||||
#ifdef ADHOC
|
#ifdef ADHOC
|
||||||
@"Feedback",
|
@"Feedback",
|
||||||
#endif
|
#endif
|
||||||
@ -472,7 +477,7 @@
|
|||||||
MPElementEntity *newElement = [NSEntityDescription insertNewObjectForEntityForName:ClassNameFromMPElementType(type)
|
MPElementEntity *newElement = [NSEntityDescription insertNewObjectForEntityForName:ClassNameFromMPElementType(type)
|
||||||
inManagedObjectContext:[MPAppDelegate managedObjectContext]];
|
inManagedObjectContext:[MPAppDelegate managedObjectContext]];
|
||||||
newElement.name = self.activeElement.name;
|
newElement.name = self.activeElement.name;
|
||||||
newElement.mpHashHex = self.activeElement.mpHashHex;
|
newElement.keyID = self.activeElement.keyID;
|
||||||
newElement.uses = self.activeElement.uses;
|
newElement.uses = self.activeElement.uses;
|
||||||
newElement.lastUsed = self.activeElement.lastUsed;
|
newElement.lastUsed = self.activeElement.lastUsed;
|
||||||
|
|
||||||
|
@ -113,10 +113,10 @@
|
|||||||
- (void)update {
|
- (void)update {
|
||||||
|
|
||||||
assert(self.query);
|
assert(self.query);
|
||||||
assert([MPAppDelegate get].keyHashHex);
|
assert([MPAppDelegate get].keyID);
|
||||||
|
|
||||||
self.fetchedResultsController.fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name BEGINSWITH[cd] %@) AND mpHashHex == %@",
|
self.fetchedResultsController.fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name BEGINSWITH[cd] %@) AND keyID == %@",
|
||||||
self.query, self.query, [MPAppDelegate get].keyHashHex];
|
self.query, self.query, [MPAppDelegate get].keyID];
|
||||||
|
|
||||||
NSError *error;
|
NSError *error;
|
||||||
if (![self.fetchedResultsController performFetch:&error])
|
if (![self.fetchedResultsController performFetch:&error])
|
||||||
@ -259,10 +259,10 @@
|
|||||||
MPElementGeneratedEntity *element = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPElementGeneratedEntity class])
|
MPElementGeneratedEntity *element = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPElementGeneratedEntity class])
|
||||||
inManagedObjectContext:self.fetchedResultsController.managedObjectContext];
|
inManagedObjectContext:self.fetchedResultsController.managedObjectContext];
|
||||||
assert([element isKindOfClass:ClassFromMPElementType((unsigned)element.type)]);
|
assert([element isKindOfClass:ClassFromMPElementType((unsigned)element.type)]);
|
||||||
assert([MPAppDelegate get].keyHashHex);
|
assert([MPAppDelegate get].keyID);
|
||||||
|
|
||||||
element.name = siteName;
|
element.name = siteName;
|
||||||
element.mpHashHex = [MPAppDelegate get].keyHashHex;
|
element.keyID = [MPAppDelegate get].keyID;
|
||||||
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[self.delegate didSelectElement:element];
|
[self.delegate didSelectElement:element];
|
||||||
|
@ -65,22 +65,22 @@
|
|||||||
|
|
||||||
switch (indexPath.section) {
|
switch (indexPath.section) {
|
||||||
case 0: {
|
case 0: {
|
||||||
// Calculated
|
// Generated
|
||||||
switch (indexPath.row) {
|
switch (indexPath.row) {
|
||||||
case 0:
|
case 0:
|
||||||
return MPElementTypeCalculatedLong;
|
return MPElementTypeGeneratedLong;
|
||||||
case 1:
|
case 1:
|
||||||
return MPElementTypeCalculatedMedium;
|
return MPElementTypeGeneratedMedium;
|
||||||
case 2:
|
case 2:
|
||||||
return MPElementTypeCalculatedShort;
|
return MPElementTypeGeneratedShort;
|
||||||
case 3:
|
case 3:
|
||||||
return MPElementTypeCalculatedBasic;
|
return MPElementTypeGeneratedBasic;
|
||||||
case 4:
|
case 4:
|
||||||
return MPElementTypeCalculatedPIN;
|
return MPElementTypeGeneratedPIN;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
[NSException raise:NSInternalInconsistencyException
|
[NSException raise:NSInternalInconsistencyException
|
||||||
format:@"Unsupported row: %d, when selecting calculated element type.", indexPath.row];
|
format:@"Unsupported row: %d, when selecting generated element type.", indexPath.row];
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,25 @@
|
|||||||
<string>en</string>
|
<string>en</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Passwords</string>
|
<string>Passwords</string>
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>mpsites</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeIconFiles</key>
|
||||||
|
<array/>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Master Password sites</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Owner</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>com.lyndir.lhunath.MasterPassword.sites</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>${EXECUTABLE_NAME}</string>
|
<string>${EXECUTABLE_NAME}</string>
|
||||||
<key>CFBundleIconFiles</key>
|
<key>CFBundleIconFiles</key>
|
||||||
@ -85,5 +104,25 @@
|
|||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UTExportedTypeDeclarations</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UTTypeDescription</key>
|
||||||
|
<string>Master Password sites</string>
|
||||||
|
<key>UTTypeIdentifier</key>
|
||||||
|
<string>com.lyndir.lhunath.MasterPassword.sites</string>
|
||||||
|
<key>UTTypeSize320IconFile</key>
|
||||||
|
<string></string>
|
||||||
|
<key>UTTypeSize64IconFile</key>
|
||||||
|
<string></string>
|
||||||
|
<key>UTTypeTagSpecification</key>
|
||||||
|
<dict>
|
||||||
|
<key>public.filename-extension</key>
|
||||||
|
<array>
|
||||||
|
<string>mpsites</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -54,7 +54,7 @@
|
|||||||
<key>DefaultValue</key>
|
<key>DefaultValue</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>Key</key>
|
<key>Key</key>
|
||||||
<string>rememberKeyPhrase</string>
|
<string>rememberKey</string>
|
||||||
<key>Title</key>
|
<key>Title</key>
|
||||||
<string>Remember my password</string>
|
<string>Remember my password</string>
|
||||||
<key>Type</key>
|
<key>Type</key>
|
||||||
@ -72,9 +72,9 @@
|
|||||||
<key>DefaultValue</key>
|
<key>DefaultValue</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>Key</key>
|
<key>Key</key>
|
||||||
<string>storeKeyPhrase</string>
|
<string>saveKey</string>
|
||||||
<key>Title</key>
|
<key>Title</key>
|
||||||
<string>Store my password</string>
|
<string>Save my password</string>
|
||||||
<key>Type</key>
|
<key>Type</key>
|
||||||
<string>PSToggleSwitchSpecifier</string>
|
<string>PSToggleSwitchSpecifier</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
Loading…
Reference in New Issue
Block a user