2
0

Generating security question answers for sites.

This commit is contained in:
Maarten Billemont 2014-09-21 22:45:21 -04:00
parent d5a5cd7de4
commit 58156be793
15 changed files with 355 additions and 45 deletions

View File

@ -63,8 +63,9 @@ NSString *NSStringFromTimeToCrack(TimeToCrack timeToCrack);
- (NSString *)generateLoginForSiteNamed:(NSString *)name usingKey:(MPKey *)key; - (NSString *)generateLoginForSiteNamed:(NSString *)name usingKey:(MPKey *)key;
- (NSString *)generatePasswordForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter - (NSString *)generatePasswordForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter
usingKey:(MPKey *)key; usingKey:(MPKey *)key;
- (NSString *)generateAnswerForSiteNamed:(NSString *)name onQuestion:(NSString *)question usingKey:(MPKey *)key;
- (NSString *)generateContentForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter - (NSString *)generateContentForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter
variant:(MPSiteVariant)variant usingKey:(MPKey *)key; variant:(MPSiteVariant)variant context:(NSString *)context usingKey:(MPKey *)key;
- (NSString *)storedLoginForSite:(MPStoredSiteEntity *)site usingKey:(MPKey *)key; - (NSString *)storedLoginForSite:(MPStoredSiteEntity *)site usingKey:(MPKey *)key;
- (NSString *)storedPasswordForSite:(MPStoredSiteEntity *)site usingKey:(MPKey *)key; - (NSString *)storedPasswordForSite:(MPStoredSiteEntity *)site usingKey:(MPKey *)key;
@ -73,11 +74,17 @@ NSString *NSStringFromTimeToCrack(TimeToCrack timeToCrack);
- (NSString *)resolveLoginForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey; - (NSString *)resolveLoginForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey;
- (NSString *)resolvePasswordForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey; - (NSString *)resolvePasswordForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey;
- (NSString *)resolveAnswerForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey;
- (NSString *)resolveAnswerForQuestion:(MPSiteQuestionEntity *)question ofSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey;
- (void)resolveLoginForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey - (void)resolveLoginForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey
result:(void ( ^ )(NSString *result))resultBlock; result:(void ( ^ )(NSString *result))resultBlock;
- (void)resolvePasswordForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey - (void)resolvePasswordForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey
result:(void ( ^ )(NSString *result))resultBlock; result:(void ( ^ )(NSString *result))resultBlock;
- (void)resolveAnswerForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey
result:(void ( ^ )(NSString *result))resultBlock;
- (void)resolveAnswerForQuestion:(MPSiteQuestionEntity *)question ofSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey
result:(void ( ^ )(NSString *result))resultBlock;
- (void)importProtectedPassword:(NSString *)protectedPassword protectedByKey:(MPKey *)importKey - (void)importProtectedPassword:(NSString *)protectedPassword protectedByKey:(MPKey *)importKey
intoSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey; intoSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey;

View File

@ -19,6 +19,7 @@
#import "MPEntities.h" #import "MPEntities.h"
#import "MPAppDelegate_Shared.h" #import "MPAppDelegate_Shared.h"
#import "MPAppDelegate_InApp.h" #import "MPAppDelegate_InApp.h"
#import "MPSiteQuestionEntity.h"
#include <openssl/bn.h> #include <openssl/bn.h>
#include <openssl/err.h> #include <openssl/err.h>
@ -145,6 +146,8 @@
return @"com.lyndir.masterpassword"; return @"com.lyndir.masterpassword";
case MPSiteVariantLogin: case MPSiteVariantLogin:
return @"com.lyndir.masterpassword.login"; return @"com.lyndir.masterpassword.login";
case MPSiteVariantAnswer:
return @"com.lyndir.masterpassword.answer";
} }
Throw( @"Unsupported variant: %ld", (long)variant ); Throw( @"Unsupported variant: %ld", (long)variant );
@ -361,32 +364,41 @@
- (NSString *)generateLoginForSiteNamed:(NSString *)name usingKey:(MPKey *)key { - (NSString *)generateLoginForSiteNamed:(NSString *)name usingKey:(MPKey *)key {
return [self generateContentForSiteNamed:name ofType:MPSiteTypeGeneratedName withCounter:1 return [self generateContentForSiteNamed:name ofType:MPSiteTypeGeneratedName withCounter:1
variant:MPSiteVariantLogin usingKey:key]; variant:MPSiteVariantLogin context:nil usingKey:key];
} }
- (NSString *)generatePasswordForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter - (NSString *)generatePasswordForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter
usingKey:(MPKey *)key { usingKey:(MPKey *)key {
return [self generateContentForSiteNamed:name ofType:type withCounter:counter return [self generateContentForSiteNamed:name ofType:type withCounter:counter
variant:MPSiteVariantPassword usingKey:key]; variant:MPSiteVariantPassword context:nil usingKey:key];
}
- (NSString *)generateAnswerForSiteNamed:(NSString *)name onQuestion:(NSString *)question usingKey:(MPKey *)key {
return [self generateContentForSiteNamed:name ofType:MPSiteTypeGeneratedPhrase withCounter:1
variant:MPSiteVariantAnswer context:question usingKey:key];
} }
- (NSString *)generateContentForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter - (NSString *)generateContentForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter
variant:(MPSiteVariant)variant usingKey:(MPKey *)key { variant:(MPSiteVariant)variant context:(NSString *)context usingKey:(MPKey *)key {
// Determine the seed whose bytes will be used for calculating a password // Determine the seed whose bytes will be used for calculating a password
uint32_t ncounter = htonl( counter ), nnameLength = htonl( name.length ); uint32_t ncounter = htonl( counter ), nnameLength = htonl( name.length ), ncontextLength = htonl( context.length );
NSData *counterBytes = [NSData dataWithBytes:&ncounter length:sizeof( ncounter )]; NSData *counterBytes = [NSData dataWithBytes:&ncounter length:sizeof( ncounter )];
NSData *nameLengthBytes = [NSData dataWithBytes:&nnameLength length:sizeof( nnameLength )]; NSData *nameLengthBytes = [NSData dataWithBytes:&nnameLength length:sizeof( nnameLength )];
NSData *contextLengthBytes = [NSData dataWithBytes:&ncontextLength length:sizeof( ncontextLength )];
NSString *scope = [self scopeForVariant:variant]; NSString *scope = [self scopeForVariant:variant];
trc( @"seed from: hmac-sha256(%@, %@ | %@ | %@ | %@)", trc( @"seed from: hmac-sha256(%@, %@ | %@ | %@ | %@ | %@)",
[[key keyID] encodeHex], scope, [nameLengthBytes encodeHex], name, [counterBytes encodeHex] ); [[key keyID] encodeHex], scope, [nameLengthBytes encodeHex], name, [counterBytes encodeHex], context );
NSData *seed = [[NSData dataByConcatenatingDatas: NSData *seed = [[NSData dataByConcatenatingDatas:
[scope dataUsingEncoding:NSUTF8StringEncoding], [scope dataUsingEncoding:NSUTF8StringEncoding],
nameLengthBytes, nameLengthBytes,
[name dataUsingEncoding:NSUTF8StringEncoding], [name dataUsingEncoding:NSUTF8StringEncoding],
counterBytes, counterBytes,
nil] context? contextLengthBytes: nil,
[context dataUsingEncoding:NSUTF8StringEncoding],
nil]
hmacWith:PearlHashSHA256 key:key.keyData]; hmacWith:PearlHashSHA256 key:key.keyData];
trc( @"seed is: %@", [seed encodeHex] ); trc( @"seed is: %@", [seed encodeHex] );
const char *seedBytes = seed.bytes; const char *seedBytes = seed.bytes;
@ -509,6 +521,34 @@
return result; return result;
} }
- (NSString *)resolveAnswerForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey {
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter( group );
__block NSString *result = nil;
[self resolveAnswerForSite:site usingKey:siteKey result:^(NSString *result_) {
result = result_;
dispatch_group_leave( group );
}];
dispatch_group_wait( group, DISPATCH_TIME_FOREVER );
return result;
}
- (NSString *)resolveAnswerForQuestion:(MPSiteQuestionEntity *)question ofSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey {
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter( group );
__block NSString *result = nil;
[self resolveAnswerForQuestion:question ofSite:site usingKey:siteKey result:^(NSString *result_) {
result = result_;
dispatch_group_leave( group );
}];
dispatch_group_wait( group, DISPATCH_TIME_FOREVER );
return result;
}
- (void)resolveLoginForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey result:(void ( ^ )(NSString *result))resultBlock { - (void)resolveLoginForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey result:(void ( ^ )(NSString *result))resultBlock {
NSAssert( [siteKey.keyID isEqualToData:site.user.keyID], @"Site does not belong to current user." ); NSAssert( [siteKey.keyID isEqualToData:site.user.keyID], @"Site does not belong to current user." );
@ -599,6 +639,44 @@
} }
} }
- (void)resolveAnswerForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey result:(void ( ^ )(NSString *result))resultBlock {
NSAssert( [siteKey.keyID isEqualToData:site.user.keyID], @"Site does not belong to current user." );
NSString *name = site.name;
id<MPAlgorithm> algorithm = nil;
if (!site.name.length)
err( @"Missing name." );
else if (!siteKey.keyData.length)
err( @"Missing key." );
else
algorithm = site.algorithm;
dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
NSString *result = [algorithm generateAnswerForSiteNamed:name onQuestion:nil usingKey:siteKey];
resultBlock( result );
} );
}
- (void)resolveAnswerForQuestion:(MPSiteQuestionEntity *)question ofSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey
result:(void ( ^ )(NSString *result))resultBlock {
NSAssert( [siteKey.keyID isEqualToData:site.user.keyID], @"Site does not belong to current user." );
NSString *name = site.name;
NSString *keyword = question.keyword;
id<MPAlgorithm> algorithm = nil;
if (!site.name.length)
err( @"Missing name." );
else if (!siteKey.keyData.length)
err( @"Missing key." );
else
algorithm = site.algorithm;
dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
NSString *result = [algorithm generateAnswerForSiteNamed:name onQuestion:keyword usingKey:siteKey];
resultBlock( result );
} );
}
- (void)importProtectedPassword:(NSString *)protectedContent protectedByKey:(MPKey *)importKey - (void)importProtectedPassword:(NSString *)protectedContent protectedByKey:(MPKey *)importKey
intoSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey { intoSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey {

View File

@ -28,7 +28,7 @@ PearlAssociatedObjectProperty( NSArray*, PaymentTransactions, paymentTransaction
- (BOOL)isPurchased:(NSString *)productIdentifier { - (BOOL)isPurchased:(NSString *)productIdentifier {
return [[NSUserDefaults standardUserDefaults] objectForKey:productIdentifier] != nil; return YES; //[[NSUserDefaults standardUserDefaults] objectForKey:productIdentifier] != nil;
} }
#pragma mark - SKProductsRequestDelegate #pragma mark - SKProductsRequestDelegate

View File

@ -14,6 +14,5 @@
@interface MPSiteQuestionEntity : NSManagedObject @interface MPSiteQuestionEntity : NSManagedObject
@property (nonatomic, retain) NSString * keyword; @property (nonatomic, retain) NSString * keyword;
@property (nonatomic, retain) MPSiteEntity *site;
@end @end

View File

@ -13,6 +13,5 @@
@implementation MPSiteQuestionEntity @implementation MPSiteQuestionEntity
@dynamic keyword; @dynamic keyword;
@dynamic site;
@end @end

View File

@ -20,6 +20,8 @@ typedef NS_ENUM( NSUInteger, MPSiteVariant ) {
MPSiteVariantPassword, MPSiteVariantPassword,
/** Generate the login name. */ /** Generate the login name. */
MPSiteVariantLogin, MPSiteVariantLogin,
/** Generate a security answer. */
MPSiteVariantAnswer,
}; };
typedef NS_ENUM( NSUInteger, MPSiteFeature ) { typedef NS_ENUM( NSUInteger, MPSiteFeature ) {

View File

@ -15,12 +15,11 @@
<attribute name="type_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/> <attribute name="type_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
<attribute name="uses_" attributeType="Integer 16" defaultValueString="0" indexed="YES" syncable="YES"/> <attribute name="uses_" attributeType="Integer 16" defaultValueString="0" indexed="YES" syncable="YES"/>
<attribute name="version_" attributeType="Integer 16" minValueString="0" defaultValueString="0" syncable="YES"/> <attribute name="version_" attributeType="Integer 16" minValueString="0" defaultValueString="0" syncable="YES"/>
<relationship name="questions" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="MPSiteQuestionEntity" inverseName="site" inverseEntity="MPSiteQuestionEntity" syncable="YES"/> <relationship name="questions" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="MPSiteQuestionEntity" syncable="YES"/>
<relationship name="user" minCount="1" maxCount="1" deletionRule="Nullify" destinationEntity="MPUserEntity" inverseName="sites" inverseEntity="MPUserEntity" elementID="FC8AE32E-7BE3-4FA6-8611-B7DC0DB063EF" syncable="YES"/> <relationship name="user" minCount="1" maxCount="1" deletionRule="Nullify" destinationEntity="MPUserEntity" inverseName="sites" inverseEntity="MPUserEntity" elementID="FC8AE32E-7BE3-4FA6-8611-B7DC0DB063EF" syncable="YES"/>
</entity> </entity>
<entity name="MPSiteQuestionEntity" representedClassName="MPSiteQuestionEntity" syncable="YES"> <entity name="MPSiteQuestionEntity" representedClassName="MPSiteQuestionEntity" syncable="YES">
<attribute name="keyword" attributeType="String" syncable="YES"/> <attribute name="keyword" attributeType="String" syncable="YES"/>
<relationship name="site" maxCount="1" deletionRule="Nullify" destinationEntity="MPSiteEntity" inverseName="questions" inverseEntity="MPSiteEntity" syncable="YES"/>
</entity> </entity>
<entity name="MPStoredSiteEntity" representedClassName="MPStoredSiteEntity" parentEntity="MPSiteEntity" elementID="BEEF1688-0CCD-4699-A86A-4D860FE2CEB8" syncable="YES"> <entity name="MPStoredSiteEntity" representedClassName="MPStoredSiteEntity" parentEntity="MPSiteEntity" elementID="BEEF1688-0CCD-4699-A86A-4D860FE2CEB8" syncable="YES">
<attribute name="contentObject" optional="YES" attributeType="Transformable" storedInTruthFile="YES" syncable="YES"/> <attribute name="contentObject" optional="YES" attributeType="Transformable" storedInTruthFile="YES" syncable="YES"/>
@ -37,9 +36,9 @@
<relationship name="sites" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="MPSiteEntity" inverseName="user" inverseEntity="MPSiteEntity" elementID="D18D6772-040E-4CFE-8F32-A34B08E9E9BC" syncable="YES"/> <relationship name="sites" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="MPSiteEntity" inverseName="user" inverseEntity="MPSiteEntity" elementID="D18D6772-040E-4CFE-8F32-A34B08E9E9BC" syncable="YES"/>
</entity> </entity>
<elements> <elements>
<element name="MPSiteEntity" positionX="-0" positionY="-286" width="128" height="208"/>
<element name="MPGeneratedSiteEntity" positionX="216" positionY="-288" width="128" height="58"/> <element name="MPGeneratedSiteEntity" positionX="216" positionY="-288" width="128" height="58"/>
<element name="MPSiteQuestionEntity" positionX="-2" positionY="-9" width="128" height="73"/> <element name="MPSiteEntity" positionX="-0" positionY="-286" width="128" height="208"/>
<element name="MPSiteQuestionEntity" positionX="-2" positionY="-9" width="128" height="58"/>
<element name="MPStoredSiteEntity" positionX="214" positionY="-171" width="128" height="58"/> <element name="MPStoredSiteEntity" positionX="214" positionY="-171" width="128" height="58"/>
<element name="MPUserEntity" positionX="-218" positionY="-288" width="128" height="148"/> <element name="MPUserEntity" positionX="-218" positionY="-288" width="128" height="148"/>
</elements> </elements>

View File

@ -8,9 +8,13 @@
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#import "MPTypeViewController.h" #import "MPTypeViewController.h"
#import "MPSiteQuestionEntity.h"
@interface MPAnswersViewController : UITableViewController @interface MPAnswersViewController : UITableViewController
- (void)setSite:(MPSiteEntity *)site;
- (MPSiteEntity *)siteInContext:(NSManagedObjectContext *)context;
@end @end
@interface MPGlobalAnswersCell : UITableViewCell @interface MPGlobalAnswersCell : UITableViewCell
@ -18,13 +22,15 @@
@property (nonatomic) IBOutlet UILabel *titleLabel; @property (nonatomic) IBOutlet UILabel *titleLabel;
@property (nonatomic) IBOutlet UITextField *answerField; @property (nonatomic) IBOutlet UITextField *answerField;
- (void)setSite:(MPSiteEntity *)site;
@end @end
@interface MPSendAnswersCell : UITableViewCell @interface MPSendAnswersCell : UITableViewCell
@end @end
@interface MPMultipleAnswersCell : UITableViewCell @interface MPMultipleAnswersCell : UITableViewCell <UITextFieldDelegate>
@end @end
@ -33,4 +39,6 @@
@property(nonatomic) IBOutlet UITextField *questionField; @property(nonatomic) IBOutlet UITextField *questionField;
@property(nonatomic) IBOutlet UITextField *answerField; @property(nonatomic) IBOutlet UITextField *answerField;
- (void)setQuestion:(MPSiteQuestionEntity *)question forSite:(MPSiteEntity *)site;
@end @end

View File

@ -13,50 +13,235 @@
#import "UIColor+Expanded.h" #import "UIColor+Expanded.h"
#import "MPPasswordsViewController.h" #import "MPPasswordsViewController.h"
#import "MPCoachmarkViewController.h" #import "MPCoachmarkViewController.h"
#import "MPSiteQuestionEntity.h"
@interface MPAnswersViewController() @interface MPAnswersViewController()
@end @end
@implementation MPAnswersViewController @implementation MPAnswersViewController {
NSManagedObjectID *_siteOID;
BOOL _multiple;
}
#pragma mark - Life
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.tableHeaderView = [UIView new];
self.tableView.tableFooterView = [UIView new];
}
#pragma mark - State
- (void)setSite:(MPSiteEntity *)site {
_siteOID = [site objectID];
_multiple = [site.questions count] > 0;
[self.tableView reloadData];
}
- (MPSiteEntity *)siteInContext:(NSManagedObjectContext *)context {
return [MPSiteEntity existingObjectWithID:_siteOID inContext:context];
}
#pragma mark - UITableViewDelegate #pragma mark - UITableViewDelegate
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 2;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (section == 0)
return 3;
if (!_multiple)
return 0;
return MAX( 2, [[self siteInContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]].questions count] );
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.item == 0) MPSiteEntity *site = [self siteInContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]];
return [MPGlobalAnswersCell dequeueCellFromTableView:tableView indexPath:indexPath]; if (indexPath.section == 0) {
if (indexPath.item == 1) if (indexPath.item == 0) {
return [MPSendAnswersCell dequeueCellFromTableView:tableView indexPath:indexPath]; MPGlobalAnswersCell *cell = [MPGlobalAnswersCell dequeueCellFromTableView:tableView indexPath:indexPath];
if (indexPath.item == 2) [cell setSite:site];
return [MPMultipleAnswersCell dequeueCellFromTableView:tableView indexPath:indexPath]; return cell;
}
if (indexPath.item == 1)
return [MPSendAnswersCell dequeueCellFromTableView:tableView indexPath:indexPath];
if (indexPath.item == 2) {
MPMultipleAnswersCell *cell = [MPMultipleAnswersCell dequeueCellFromTableView:tableView indexPath:indexPath];
cell.accessoryType = _multiple? UITableViewCellAccessoryCheckmark: UITableViewCellAccessoryNone;
return cell;
}
Throw( @"Unsupported row index: %@", indexPath );
}
MPAnswersQuestionCell *cell = [MPAnswersQuestionCell dequeueCellFromTableView:tableView indexPath:indexPath]; MPAnswersQuestionCell *cell = [MPAnswersQuestionCell dequeueCellFromTableView:tableView indexPath:indexPath];
MPSiteQuestionEntity *question = nil;
if ([site.questions count] > indexPath.item)
question = site.questions[indexPath.item];
[cell setQuestion:question forSite:site];
return cell; return cell;
} }
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.section == 0) {
if (indexPath.item == 0)
return 133;
return 44;
}
return 130;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath]; MPSiteEntity *site = [self siteInContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]];
// UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
// [tableView deselectRowAtIndexPath:indexPath animated:YES];
if ([cell isKindOfClass:[MPGlobalAnswersCell class]]) {
[PearlOverlay showTemporaryOverlayWithTitle:strl( @"Answer Copied" ) dismissAfter:2];
[UIPasteboard generalPasteboard].string = ((MPGlobalAnswersCell *)cell).answerField.text;
}
else if ([cell isKindOfClass:[MPMultipleAnswersCell class]]) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
_multiple = !_multiple;
[tableView reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationAutomatic];
}];
}
else if ([cell isKindOfClass:[MPSendAnswersCell class]]) {
NSString *body;
if (!_multiple) {
NSObject *answer = [site.algorithm resolveAnswerForSite:site usingKey:[MPiOSAppDelegate get].key];
body = strf( @"Master Password generated the following security answer for your site: %@\n\n"
@"%@\n"
@"\n\nYou should use this as the answer to each security question the site asks you.\n"
@"Do not share this answer with others!", site.name, answer );
}
else {
NSMutableString *bodyBuilder = [NSMutableString string];
[bodyBuilder appendFormat:@"Master Password generated the following security answers for your site: %@\n\n", site.name];
for (MPSiteQuestionEntity *question in site.questions) {
NSObject *answer = [site.algorithm resolveAnswerForQuestion:question ofSite:site usingKey:[MPiOSAppDelegate get].key];
[bodyBuilder appendFormat:@"For question: '%@', use answer: %@\n", question.keyword, answer];
}
[bodyBuilder appendFormat:@"\n\nUse the answer for the matching security question.\n"
@"Do not share this answer with others!"];
body = bodyBuilder;
}
[PearlEMail sendEMailTo:nil fromVC:self subject:strf( @"Master Password security answers for %@", site.name ) body:body];
}
else if ([cell isKindOfClass:[MPAnswersQuestionCell class]]) {
[PearlOverlay showTemporaryOverlayWithTitle:strl( @"Answer Copied" ) dismissAfter:2];
[UIPasteboard generalPasteboard].string = ((MPAnswersQuestionCell *)cell).answerField.text;
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
} }
@end @end
@implementation MPGlobalAnswersCell : UITableViewCell @implementation MPGlobalAnswersCell
#pragma mark - State
- (void)setSite:(MPSiteEntity *)site {
self.answerField.text = @"...";
[site.algorithm resolveAnswerForSite:site usingKey:[MPiOSAppDelegate get].key result:^(NSString *result) {
PearlMainQueue( ^{
self.answerField.text = result;
} );
}];
}
@end @end
@implementation MPSendAnswersCell : UITableViewCell @implementation MPSendAnswersCell
@end @end
@implementation MPMultipleAnswersCell : UITableViewCell @implementation MPMultipleAnswersCell
@end @end
@implementation MPAnswersQuestionCell : UITableViewCell @implementation MPAnswersQuestionCell {
NSManagedObjectID *_siteOID;
NSManagedObjectID *_questionOID;
}
#pragma mark - State
- (void)setQuestion:(MPSiteQuestionEntity *)question forSite:(MPSiteEntity *)site {
_siteOID = site.objectID;
_questionOID = question.objectID;
[self updateAnswerForQuestion:question ofSite:site];
}
#pragma mark - UITextFieldDelegate
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[textField resignFirstResponder];
return NO;
}
- (IBAction)textFieldDidChange:(UITextField *)textField {
NSString *keyword = textField.text;
[MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
MPSiteEntity *site = [MPSiteEntity existingObjectWithID:_siteOID inContext:context];
MPSiteQuestionEntity *question = [MPSiteQuestionEntity existingObjectWithID:_questionOID inContext:context];
if (!question)
[site addQuestionsObject:question = [MPSiteQuestionEntity insertNewObjectInContext:context]];
question.keyword = keyword;
if ([context saveToStore]) {
_questionOID = question.objectID;
[self updateAnswerForQuestion:question ofSite:site];
}
}];
}
#pragma mark - Private
- (void)updateAnswerForQuestion:(MPSiteQuestionEntity *)question ofSite:(MPSiteEntity *)site {
if (!question)
PearlMainQueue( ^{
self.questionField.text = self.answerField.text = nil;
} );
else {
NSString *keyword = question.keyword;
PearlMainQueue( ^{
self.answerField.text = @"...";
} );
[site.algorithm resolveAnswerForQuestion:question ofSite:site usingKey:[MPiOSAppDelegate get].key result:^(NSString *result) {
PearlMainQueue( ^{
self.questionField.text = keyword;
self.answerField.text = result;
} );
}];
}
}
@end @end

View File

@ -30,5 +30,6 @@ typedef NS_ENUM ( NSUInteger, MPPasswordCellMode ) {
- (void)setSite:(MPSiteEntity *)site animated:(BOOL)animated; - (void)setSite:(MPSiteEntity *)site animated:(BOOL)animated;
- (void)setTransientSite:(NSString *)siteName animated:(BOOL)animated; - (void)setTransientSite:(NSString *)siteName animated:(BOOL)animated;
- (void)setMode:(MPPasswordCellMode)mode animated:(BOOL)animated; - (void)setMode:(MPPasswordCellMode)mode animated:(BOOL)animated;
- (MPSiteEntity *)siteInContext:(NSManagedObjectContext *)context;
@end @end

View File

@ -32,6 +32,7 @@
@property(nonatomic, strong) IBOutlet UILabel *counterLabel; @property(nonatomic, strong) IBOutlet UILabel *counterLabel;
@property(nonatomic, strong) IBOutlet UIButton *counterButton; @property(nonatomic, strong) IBOutlet UIButton *counterButton;
@property(nonatomic, strong) IBOutlet UIButton *upgradeButton; @property(nonatomic, strong) IBOutlet UIButton *upgradeButton;
@property(nonatomic, strong) IBOutlet UIButton *answersButton;
@property(nonatomic, strong) IBOutlet UIButton *modeButton; @property(nonatomic, strong) IBOutlet UIButton *modeButton;
@property(nonatomic, strong) IBOutlet UIButton *editButton; @property(nonatomic, strong) IBOutlet UIButton *editButton;
@property(nonatomic, strong) IBOutlet UIScrollView *modeScrollView; @property(nonatomic, strong) IBOutlet UIScrollView *modeScrollView;
@ -464,7 +465,8 @@
MPSiteEntity *mainSite = [self siteInContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]]; MPSiteEntity *mainSite = [self siteInContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]];
// UI // UI
self.upgradeButton.alpha = mainSite.requiresExplicitMigration? 1: 0; self.upgradeButton.gone = !mainSite.requiresExplicitMigration;
self.answersButton.gone = ![[MPiOSAppDelegate get] isPurchased:MPProductGenerateAnswers];
BOOL settingsMode = self.mode == MPPasswordCellModeSettings; BOOL settingsMode = self.mode == MPPasswordCellModeSettings;
self.loginNameContainer.alpha = settingsMode || mainSite.loginGenerated || [mainSite.loginName length]? 0.7f: 0; self.loginNameContainer.alpha = settingsMode || mainSite.loginGenerated || [mainSite.loginName length]? 0.7f: 0;
self.loginNameField.textColor = [UIColor colorWithHexString:mainSite.loginGenerated? @"5E636D": @"6D5E63"]; self.loginNameField.textColor = [UIColor colorWithHexString:mainSite.loginGenerated? @"5E636D": @"6D5E63"];

View File

@ -23,6 +23,7 @@
#import "MPAppDelegate_Key.h" #import "MPAppDelegate_Key.h"
#import "MPPasswordCell.h" #import "MPPasswordCell.h"
#import "UICollectionView+PearlReloadFromArray.h" #import "UICollectionView+PearlReloadFromArray.h"
#import "MPAnswersViewController.h"
@interface MPPasswordsViewController()<NSFetchedResultsControllerDelegate> @interface MPPasswordsViewController()<NSFetchedResultsControllerDelegate>
@ -85,6 +86,9 @@
if ([segue.identifier isEqualToString:@"popdown"]) if ([segue.identifier isEqualToString:@"popdown"])
_popdownVC = segue.destinationViewController; _popdownVC = segue.destinationViewController;
if ([segue.identifier isEqualToString:@"answers"])
((MPAnswersViewController *)segue.destinationViewController).site =
[[MPPasswordCell findAsSuperviewOf:sender] siteInContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]];
} }
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
@ -298,8 +302,8 @@ referenceSizeForHeaderInSection:(NSInteger)section {
[[NSNotificationCenter defaultCenter] [[NSNotificationCenter defaultCenter]
addObserverForName:MPCheckConfigNotification object:nil addObserverForName:MPCheckConfigNotification object:nil
queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
[self updateConfigKey:note.object]; [self updateConfigKey:note.object];
}], }],
]; ];
} }

View File

@ -16,11 +16,8 @@
@interface MPiOSAppDelegate()<UIDocumentInteractionControllerDelegate> @interface MPiOSAppDelegate()<UIDocumentInteractionControllerDelegate>
@property(nonatomic, weak) PearlAlert *handleCloudDisabledAlert;
@property(nonatomic, weak) PearlAlert *handleCloudContentAlert;
@property(nonatomic, weak) PearlAlert *fixCloudContentAlert;
@property(nonatomic, weak) PearlOverlay *storeLoadingOverlay;
@property(nonatomic, strong) UIDocumentInteractionController *interactionController; @property(nonatomic, strong) UIDocumentInteractionController *interactionController;
@end @end
@implementation MPiOSAppDelegate @implementation MPiOSAppDelegate

View File

@ -117,6 +117,8 @@
DA32CFF319CF1C8F004F3F0E /* MPSiteEntity.m in Sources */ = {isa = PBXBuildFile; fileRef = DA32CFEC19CF1C8F004F3F0E /* MPSiteEntity.m */; }; DA32CFF319CF1C8F004F3F0E /* MPSiteEntity.m in Sources */ = {isa = PBXBuildFile; fileRef = DA32CFEC19CF1C8F004F3F0E /* MPSiteEntity.m */; };
DA32CFF419CF1C8F004F3F0E /* MPGeneratedSiteEntity.m in Sources */ = {isa = PBXBuildFile; fileRef = DA32CFEE19CF1C8F004F3F0E /* MPGeneratedSiteEntity.m */; }; DA32CFF419CF1C8F004F3F0E /* MPGeneratedSiteEntity.m in Sources */ = {isa = PBXBuildFile; fileRef = DA32CFEE19CF1C8F004F3F0E /* MPGeneratedSiteEntity.m */; };
DA32D00819CF4735004F3F0E /* MasterPassword.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DA32D00119CF4735004F3F0E /* MasterPassword.xcdatamodeld */; }; DA32D00819CF4735004F3F0E /* MasterPassword.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DA32D00119CF4735004F3F0E /* MasterPassword.xcdatamodeld */; };
DA32D00919CF5C55004F3F0E /* icon_question.png in Resources */ = {isa = PBXBuildFile; fileRef = DABD37FE1711E29600CF925C /* icon_question.png */; };
DA32D00A19CF5C55004F3F0E /* icon_question@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = DABD37FF1711E29600CF925C /* icon_question@2x.png */; };
DA3509FE15F101A500C14A8E /* PearlQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = DA3509FC15F101A500C14A8E /* PearlQueue.h */; }; DA3509FE15F101A500C14A8E /* PearlQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = DA3509FC15F101A500C14A8E /* PearlQueue.h */; };
DA3509FF15F101A500C14A8E /* PearlQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = DA3509FD15F101A500C14A8E /* PearlQueue.m */; }; DA3509FF15F101A500C14A8E /* PearlQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = DA3509FD15F101A500C14A8E /* PearlQueue.m */; };
DA38D6A318CCB5BF009AEB3E /* Storyboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DA38D6A218CCB5BF009AEB3E /* Storyboard.storyboard */; }; DA38D6A318CCB5BF009AEB3E /* Storyboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DA38D6A218CCB5BF009AEB3E /* Storyboard.storyboard */; };
@ -3071,11 +3073,13 @@
DA25C5FA197CCAE00046CDCF /* icon_delete.png in Resources */, DA25C5FA197CCAE00046CDCF /* icon_delete.png in Resources */,
DA25C601197DBF260046CDCF /* icon_trash@2x.png in Resources */, DA25C601197DBF260046CDCF /* icon_trash@2x.png in Resources */,
DABD39551711E29700CF925C /* avatar-6.png in Resources */, DABD39551711E29700CF925C /* avatar-6.png in Resources */,
DA32D00919CF5C55004F3F0E /* icon_question.png in Resources */,
DABD39561711E29700CF925C /* avatar-6@2x.png in Resources */, DABD39561711E29700CF925C /* avatar-6@2x.png in Resources */,
DABD39571711E29700CF925C /* avatar-7.png in Resources */, DABD39571711E29700CF925C /* avatar-7.png in Resources */,
DABD39581711E29700CF925C /* avatar-7@2x.png in Resources */, DABD39581711E29700CF925C /* avatar-7@2x.png in Resources */,
DABD39591711E29700CF925C /* avatar-8.png in Resources */, DABD39591711E29700CF925C /* avatar-8.png in Resources */,
DA250A0D1956484D00AC23F1 /* image-2@2x.png in Resources */, DA250A0D1956484D00AC23F1 /* image-2@2x.png in Resources */,
DA32D00A19CF5C55004F3F0E /* icon_question@2x.png in Resources */,
DA250A051956484D00AC23F1 /* image-6@2x.png in Resources */, DA250A051956484D00AC23F1 /* image-6@2x.png in Resources */,
DABD395A1711E29700CF925C /* avatar-8@2x.png in Resources */, DABD395A1711E29700CF925C /* avatar-8@2x.png in Resources */,
DABD395B1711E29700CF925C /* avatar-9.png in Resources */, DABD395B1711E29700CF925C /* avatar-9.png in Resources */,

View File

@ -1203,12 +1203,12 @@
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LvK-28-fbm" userLabel="Settings Container"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LvK-28-fbm" userLabel="Settings Container">
<rect key="frame" x="300" y="0.0" width="300" height="100"/> <rect key="frame" x="300" y="0.0" width="300" height="100"/>
<subviews> <subviews>
<button opaque="NO" alpha="0.5" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" showsTouchWhenHighlighted="YES" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="vGk-t6-hZn" userLabel="Upgrade"> <button opaque="NO" alpha="0.5" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" showsTouchWhenHighlighted="YES" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="IKd-Ot-0n4" userLabel="Upgrade">
<rect key="frame" x="29" y="56" width="44" height="44"/> <rect key="frame" x="-15" y="56" width="44" height="44"/>
<accessibility key="accessibilityConfiguration" hint="Upgrades the password."/> <accessibility key="accessibilityConfiguration" hint="Upgrades the password."/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="44" id="8gK-8v-Q0K"/> <constraint firstAttribute="width" constant="44" id="5DT-m6-RYu"/>
<constraint firstAttribute="height" constant="44" id="hQh-jZ-41x"/> <constraint firstAttribute="height" constant="44" id="kyZ-o1-ntZ"/>
</constraints> </constraints>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<inset key="contentEdgeInsets" minX="6" minY="6" maxX="6" maxY="6"/> <inset key="contentEdgeInsets" minX="6" minY="6" maxX="6" maxY="6"/>
@ -1220,7 +1220,27 @@
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/> <color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</state> </state>
<connections> <connections>
<action selector="doUpgrade:" destination="W2g-yv-V3V" eventType="touchUpInside" id="H3Y-gA-0MY"/> <action selector="doUpgrade:" destination="W2g-yv-V3V" eventType="touchUpInside" id="kTZ-AM-qGa"/>
</connections>
</button>
<button opaque="NO" alpha="0.5" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" showsTouchWhenHighlighted="YES" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="vGk-t6-hZn" userLabel="Answers">
<rect key="frame" x="29" y="56" width="44" height="44"/>
<accessibility key="accessibilityConfiguration" hint="Upgrades the password."/>
<constraints>
<constraint firstAttribute="width" constant="44" id="8gK-8v-Q0K"/>
<constraint firstAttribute="height" constant="44" id="hQh-jZ-41x"/>
</constraints>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<inset key="contentEdgeInsets" minX="6" minY="6" maxX="6" maxY="6"/>
<state key="normal" image="icon_question.png">
<color key="titleColor" red="0.19607843459999999" green="0.30980393290000002" blue="0.52156865600000002" alpha="1" colorSpace="calibratedRGB"/>
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<state key="highlighted">
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</state>
<connections>
<segue destination="koB-V2-GYf" kind="modal" identifier="answers" id="gQR-Xm-ATO"/>
</connections> </connections>
</button> </button>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" alpha="0.5" contentMode="left" text="1" textAlignment="right" lineBreakMode="tailTruncation" minimumFontSize="10" translatesAutoresizingMaskIntoConstraints="NO" id="PKP-M9-T8E" userLabel="Counter"> <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" alpha="0.5" contentMode="left" text="1" textAlignment="right" lineBreakMode="tailTruncation" minimumFontSize="10" translatesAutoresizingMaskIntoConstraints="NO" id="PKP-M9-T8E" userLabel="Counter">
@ -1316,9 +1336,11 @@
</subviews> </subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<constraints> <constraints>
<constraint firstItem="PKP-M9-T8E" firstAttribute="leading" secondItem="IKd-Ot-0n4" secondAttribute="trailing" priority="750" id="3sN-hc-iUd"/>
<constraint firstItem="PKP-M9-T8E" firstAttribute="leading" secondItem="vGk-t6-hZn" secondAttribute="trailing" id="47y-gh-Ibs"/> <constraint firstItem="PKP-M9-T8E" firstAttribute="leading" secondItem="vGk-t6-hZn" secondAttribute="trailing" id="47y-gh-Ibs"/>
<constraint firstItem="PKP-M9-T8E" firstAttribute="centerY" secondItem="uZi-FT-Fe8" secondAttribute="centerY" id="KEK-0r-cob"/> <constraint firstItem="PKP-M9-T8E" firstAttribute="centerY" secondItem="uZi-FT-Fe8" secondAttribute="centerY" id="KEK-0r-cob"/>
<constraint firstItem="qBo-Kw-vN9" firstAttribute="leading" secondItem="uZi-FT-Fe8" secondAttribute="trailing" id="LLk-5B-1g9"/> <constraint firstItem="qBo-Kw-vN9" firstAttribute="leading" secondItem="uZi-FT-Fe8" secondAttribute="trailing" id="LLk-5B-1g9"/>
<constraint firstItem="vGk-t6-hZn" firstAttribute="leading" secondItem="IKd-Ot-0n4" secondAttribute="trailing" id="e2D-Rg-OQz"/>
<constraint firstItem="K8q-bM-tzh" firstAttribute="leading" secondItem="I0v-ou-hDb" secondAttribute="trailing" id="il2-7H-Hgk"/> <constraint firstItem="K8q-bM-tzh" firstAttribute="leading" secondItem="I0v-ou-hDb" secondAttribute="trailing" id="il2-7H-Hgk"/>
<constraint firstItem="I0v-ou-hDb" firstAttribute="leading" secondItem="qBo-Kw-vN9" secondAttribute="trailing" id="myx-31-6tz"/> <constraint firstItem="I0v-ou-hDb" firstAttribute="leading" secondItem="qBo-Kw-vN9" secondAttribute="trailing" id="myx-31-6tz"/>
<constraint firstAttribute="trailing" secondItem="K8q-bM-tzh" secondAttribute="trailing" constant="44" id="qQK-ts-seh"/> <constraint firstAttribute="trailing" secondItem="K8q-bM-tzh" secondAttribute="trailing" constant="44" id="qQK-ts-seh"/>
@ -1366,6 +1388,7 @@
<constraint firstItem="b5f-wN-2xb" firstAttribute="centerY" secondItem="vGk-t6-hZn" secondAttribute="centerY" id="fGB-8g-u5b"/> <constraint firstItem="b5f-wN-2xb" firstAttribute="centerY" secondItem="vGk-t6-hZn" secondAttribute="centerY" id="fGB-8g-u5b"/>
<constraint firstItem="tl3-hd-x35" firstAttribute="leading" secondItem="bff-RU-OcY" secondAttribute="leading" id="fx5-KQ-LSM"/> <constraint firstItem="tl3-hd-x35" firstAttribute="leading" secondItem="bff-RU-OcY" secondAttribute="leading" id="fx5-KQ-LSM"/>
<constraint firstItem="tl3-hd-x35" firstAttribute="top" secondItem="bff-RU-OcY" secondAttribute="top" id="jc9-39-FY3"/> <constraint firstItem="tl3-hd-x35" firstAttribute="top" secondItem="bff-RU-OcY" secondAttribute="top" id="jc9-39-FY3"/>
<constraint firstItem="b5f-wN-2xb" firstAttribute="centerY" secondItem="IKd-Ot-0n4" secondAttribute="centerY" id="mCJ-ug-mmv"/>
<constraint firstItem="K8q-bM-tzh" firstAttribute="centerY" secondItem="b5f-wN-2xb" secondAttribute="centerY" id="ofO-r3-bjz"/> <constraint firstItem="K8q-bM-tzh" firstAttribute="centerY" secondItem="b5f-wN-2xb" secondAttribute="centerY" id="ofO-r3-bjz"/>
<constraint firstItem="b5f-wN-2xb" firstAttribute="centerX" secondItem="tuh-Au-J9k" secondAttribute="centerX" id="pYG-bz-kd8"/> <constraint firstItem="b5f-wN-2xb" firstAttribute="centerX" secondItem="tuh-Au-J9k" secondAttribute="centerX" id="pYG-bz-kd8"/>
<constraint firstAttribute="trailing" secondItem="LvK-28-fbm" secondAttribute="trailing" id="r9d-ym-Frs"/> <constraint firstAttribute="trailing" secondItem="LvK-28-fbm" secondAttribute="trailing" id="r9d-ym-Frs"/>
@ -1417,6 +1440,7 @@
<constraint firstAttribute="bottom" secondItem="xph-TW-9QO" secondAttribute="bottom" id="W1r-47-bqe"/> <constraint firstAttribute="bottom" secondItem="xph-TW-9QO" secondAttribute="bottom" id="W1r-47-bqe"/>
</constraints> </constraints>
<connections> <connections>
<outlet property="answersButton" destination="vGk-t6-hZn" id="0oi-2F-M1S"/>
<outlet property="contentButton" destination="aDw-qY-VjU" id="R3R-kq-XMd"/> <outlet property="contentButton" destination="aDw-qY-VjU" id="R3R-kq-XMd"/>
<outlet property="counterButton" destination="uZi-FT-Fe8" id="oaF-bB-Sgc"/> <outlet property="counterButton" destination="uZi-FT-Fe8" id="oaF-bB-Sgc"/>
<outlet property="counterLabel" destination="PKP-M9-T8E" id="m9u-Qx-Z9N"/> <outlet property="counterLabel" destination="PKP-M9-T8E" id="m9u-Qx-Z9N"/>
@ -1430,7 +1454,7 @@
<outlet property="passwordField" destination="blw-Ou-8I8" id="bov-At-Wpd"/> <outlet property="passwordField" destination="blw-Ou-8I8" id="bov-At-Wpd"/>
<outlet property="siteNameLabel" destination="OwP-sb-Wxl" id="6GN-Ou-K0F"/> <outlet property="siteNameLabel" destination="OwP-sb-Wxl" id="6GN-Ou-K0F"/>
<outlet property="strengthLabel" destination="wfM-xz-roR" id="dzk-dB-OfP"/> <outlet property="strengthLabel" destination="wfM-xz-roR" id="dzk-dB-OfP"/>
<outlet property="upgradeButton" destination="vGk-t6-hZn" id="m2T-CA-HmJ"/> <outlet property="upgradeButton" destination="IKd-Ot-0n4" id="c1P-UY-odx"/>
</connections> </connections>
</collectionViewCell> </collectionViewCell>
</cells> </cells>
@ -1578,7 +1602,6 @@
<outlet property="popdownToTopConstraint" destination="BdD-Kc-eHl" id="59Y-ap-Yn4"/> <outlet property="popdownToTopConstraint" destination="BdD-Kc-eHl" id="59Y-ap-Yn4"/>
<outlet property="popdownView" destination="XNM-XQ-rMe" id="FaW-4m-Fff"/> <outlet property="popdownView" destination="XNM-XQ-rMe" id="FaW-4m-Fff"/>
<segue destination="z9O-w0-6oR" kind="modal" identifier="guide" id="Ql4-wf-T8u"/> <segue destination="z9O-w0-6oR" kind="modal" identifier="guide" id="Ql4-wf-T8u"/>
<segue destination="koB-V2-GYf" kind="modal" identifier="answers" id="PbR-2r-Ebm"/>
</connections> </connections>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="APh-u5-vFI" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="APh-u5-vFI" userLabel="First Responder" sceneMemberID="firstResponder"/>
@ -2842,7 +2865,8 @@ See </string>
<fontDescription key="fontDescription" name="Exo2.0-Regular" family="Exo 2.0" pointSize="28"/> <fontDescription key="fontDescription" name="Exo2.0-Regular" family="Exo 2.0" pointSize="28"/>
<textInputTraits key="textInputTraits" keyboardType="namePhonePad" keyboardAppearance="alert" returnKeyType="done"/> <textInputTraits key="textInputTraits" keyboardType="namePhonePad" keyboardAppearance="alert" returnKeyType="done"/>
<connections> <connections>
<outlet property="delegate" destination="S8q-YF-Kt9" id="TtZ-rC-HTB"/> <action selector="textFieldDidChange:" destination="iFm-3w-hOv" eventType="editingChanged" id="V9g-YU-rIb"/>
<outlet property="delegate" destination="iFm-3w-hOv" id="olS-Rx-56Y"/>
</connections> </connections>
</textField> </textField>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="left" contentVerticalAlignment="center" text="pifm gup balvabi yiz" textAlignment="center" minimumFontSize="17" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="3xA-ez-efa" userLabel="Answer Field"> <textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="left" contentVerticalAlignment="center" text="pifm gup balvabi yiz" textAlignment="center" minimumFontSize="17" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="3xA-ez-efa" userLabel="Answer Field">
@ -2901,6 +2925,7 @@ See </string>
<image name="icon_list-names.png" width="32" height="32"/> <image name="icon_list-names.png" width="32" height="32"/>
<image name="icon_person.png" width="32" height="32"/> <image name="icon_person.png" width="32" height="32"/>
<image name="icon_plus.png" width="32" height="32"/> <image name="icon_plus.png" width="32" height="32"/>
<image name="icon_question.png" width="32" height="32"/>
<image name="icon_star-hollow.png" width="32" height="32"/> <image name="icon_star-hollow.png" width="32" height="32"/>
<image name="icon_thumbs-up.png" width="32" height="32"/> <image name="icon_thumbs-up.png" width="32" height="32"/>
<image name="icon_tools.png" width="32" height="32"/> <image name="icon_tools.png" width="32" height="32"/>