Generating security question answers for sites.
This commit is contained in:
parent
d5a5cd7de4
commit
58156be793
@ -63,8 +63,9 @@ NSString *NSStringFromTimeToCrack(TimeToCrack timeToCrack);
|
||||
- (NSString *)generateLoginForSiteNamed:(NSString *)name usingKey:(MPKey *)key;
|
||||
- (NSString *)generatePasswordForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter
|
||||
usingKey:(MPKey *)key;
|
||||
- (NSString *)generateAnswerForSiteNamed:(NSString *)name onQuestion:(NSString *)question usingKey:(MPKey *)key;
|
||||
- (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 *)storedPasswordForSite:(MPStoredSiteEntity *)site usingKey:(MPKey *)key;
|
||||
@ -73,11 +74,17 @@ NSString *NSStringFromTimeToCrack(TimeToCrack timeToCrack);
|
||||
|
||||
- (NSString *)resolveLoginForSite:(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
|
||||
result:(void ( ^ )(NSString *result))resultBlock;
|
||||
- (void)resolvePasswordForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey
|
||||
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
|
||||
intoSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey;
|
||||
|
@ -19,6 +19,7 @@
|
||||
#import "MPEntities.h"
|
||||
#import "MPAppDelegate_Shared.h"
|
||||
#import "MPAppDelegate_InApp.h"
|
||||
#import "MPSiteQuestionEntity.h"
|
||||
#include <openssl/bn.h>
|
||||
#include <openssl/err.h>
|
||||
|
||||
@ -145,6 +146,8 @@
|
||||
return @"com.lyndir.masterpassword";
|
||||
case MPSiteVariantLogin:
|
||||
return @"com.lyndir.masterpassword.login";
|
||||
case MPSiteVariantAnswer:
|
||||
return @"com.lyndir.masterpassword.answer";
|
||||
}
|
||||
|
||||
Throw( @"Unsupported variant: %ld", (long)variant );
|
||||
@ -361,31 +364,40 @@
|
||||
- (NSString *)generateLoginForSiteNamed:(NSString *)name usingKey:(MPKey *)key {
|
||||
|
||||
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
|
||||
usingKey:(MPKey *)key {
|
||||
|
||||
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
|
||||
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
|
||||
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 *nameLengthBytes = [NSData dataWithBytes:&nnameLength length:sizeof( nnameLength )];
|
||||
NSData *contextLengthBytes = [NSData dataWithBytes:&ncontextLength length:sizeof( ncontextLength )];
|
||||
NSString *scope = [self scopeForVariant:variant];
|
||||
trc( @"seed from: hmac-sha256(%@, %@ | %@ | %@ | %@)",
|
||||
[[key keyID] encodeHex], scope, [nameLengthBytes encodeHex], name, [counterBytes encodeHex] );
|
||||
trc( @"seed from: hmac-sha256(%@, %@ | %@ | %@ | %@ | %@)",
|
||||
[[key keyID] encodeHex], scope, [nameLengthBytes encodeHex], name, [counterBytes encodeHex], context );
|
||||
NSData *seed = [[NSData dataByConcatenatingDatas:
|
||||
[scope dataUsingEncoding:NSUTF8StringEncoding],
|
||||
nameLengthBytes,
|
||||
[name dataUsingEncoding:NSUTF8StringEncoding],
|
||||
counterBytes,
|
||||
context? contextLengthBytes: nil,
|
||||
[context dataUsingEncoding:NSUTF8StringEncoding],
|
||||
nil]
|
||||
hmacWith:PearlHashSHA256 key:key.keyData];
|
||||
trc( @"seed is: %@", [seed encodeHex] );
|
||||
@ -509,6 +521,34 @@
|
||||
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 {
|
||||
|
||||
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
|
||||
intoSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey {
|
||||
|
||||
|
@ -28,7 +28,7 @@ PearlAssociatedObjectProperty( NSArray*, PaymentTransactions, paymentTransaction
|
||||
|
||||
- (BOOL)isPurchased:(NSString *)productIdentifier {
|
||||
|
||||
return [[NSUserDefaults standardUserDefaults] objectForKey:productIdentifier] != nil;
|
||||
return YES; //[[NSUserDefaults standardUserDefaults] objectForKey:productIdentifier] != nil;
|
||||
}
|
||||
|
||||
#pragma mark - SKProductsRequestDelegate
|
||||
|
@ -14,6 +14,5 @@
|
||||
@interface MPSiteQuestionEntity : NSManagedObject
|
||||
|
||||
@property (nonatomic, retain) NSString * keyword;
|
||||
@property (nonatomic, retain) MPSiteEntity *site;
|
||||
|
||||
@end
|
||||
|
@ -13,6 +13,5 @@
|
||||
@implementation MPSiteQuestionEntity
|
||||
|
||||
@dynamic keyword;
|
||||
@dynamic site;
|
||||
|
||||
@end
|
||||
|
@ -20,6 +20,8 @@ typedef NS_ENUM( NSUInteger, MPSiteVariant ) {
|
||||
MPSiteVariantPassword,
|
||||
/** Generate the login name. */
|
||||
MPSiteVariantLogin,
|
||||
/** Generate a security answer. */
|
||||
MPSiteVariantAnswer,
|
||||
};
|
||||
|
||||
typedef NS_ENUM( NSUInteger, MPSiteFeature ) {
|
||||
|
@ -15,12 +15,11 @@
|
||||
<attribute name="type_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
|
||||
<attribute name="uses_" attributeType="Integer 16" defaultValueString="0" indexed="YES" syncable="YES"/>
|
||||
<attribute name="version_" attributeType="Integer 16" minValueString="0" defaultValueString="0" syncable="YES"/>
|
||||
<relationship name="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"/>
|
||||
</entity>
|
||||
<entity name="MPSiteQuestionEntity" representedClassName="MPSiteQuestionEntity" 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 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"/>
|
||||
@ -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"/>
|
||||
</entity>
|
||||
<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="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="MPUserEntity" positionX="-218" positionY="-288" width="128" height="148"/>
|
||||
</elements>
|
||||
|
@ -8,9 +8,13 @@
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "MPTypeViewController.h"
|
||||
#import "MPSiteQuestionEntity.h"
|
||||
|
||||
@interface MPAnswersViewController : UITableViewController
|
||||
|
||||
- (void)setSite:(MPSiteEntity *)site;
|
||||
- (MPSiteEntity *)siteInContext:(NSManagedObjectContext *)context;
|
||||
|
||||
@end
|
||||
|
||||
@interface MPGlobalAnswersCell : UITableViewCell
|
||||
@ -18,13 +22,15 @@
|
||||
@property (nonatomic) IBOutlet UILabel *titleLabel;
|
||||
@property (nonatomic) IBOutlet UITextField *answerField;
|
||||
|
||||
- (void)setSite:(MPSiteEntity *)site;
|
||||
|
||||
@end
|
||||
|
||||
@interface MPSendAnswersCell : UITableViewCell
|
||||
|
||||
@end
|
||||
|
||||
@interface MPMultipleAnswersCell : UITableViewCell
|
||||
@interface MPMultipleAnswersCell : UITableViewCell <UITextFieldDelegate>
|
||||
|
||||
@end
|
||||
|
||||
@ -33,4 +39,6 @@
|
||||
@property(nonatomic) IBOutlet UITextField *questionField;
|
||||
@property(nonatomic) IBOutlet UITextField *answerField;
|
||||
|
||||
- (void)setQuestion:(MPSiteQuestionEntity *)question forSite:(MPSiteEntity *)site;
|
||||
|
||||
@end
|
||||
|
@ -13,50 +13,235 @@
|
||||
#import "UIColor+Expanded.h"
|
||||
#import "MPPasswordsViewController.h"
|
||||
#import "MPCoachmarkViewController.h"
|
||||
#import "MPSiteQuestionEntity.h"
|
||||
|
||||
@interface MPAnswersViewController()
|
||||
|
||||
@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
|
||||
|
||||
- (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 {
|
||||
|
||||
if (indexPath.item == 0)
|
||||
return [MPGlobalAnswersCell dequeueCellFromTableView:tableView indexPath:indexPath];
|
||||
MPSiteEntity *site = [self siteInContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]];
|
||||
if (indexPath.section == 0) {
|
||||
if (indexPath.item == 0) {
|
||||
MPGlobalAnswersCell *cell = [MPGlobalAnswersCell dequeueCellFromTableView:tableView indexPath:indexPath];
|
||||
[cell setSite:site];
|
||||
return cell;
|
||||
}
|
||||
if (indexPath.item == 1)
|
||||
return [MPSendAnswersCell dequeueCellFromTableView:tableView indexPath:indexPath];
|
||||
if (indexPath.item == 2)
|
||||
return [MPMultipleAnswersCell 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];
|
||||
MPSiteQuestionEntity *question = nil;
|
||||
if ([site.questions count] > indexPath.item)
|
||||
question = site.questions[indexPath.item];
|
||||
[cell setQuestion:question forSite:site];
|
||||
|
||||
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 {
|
||||
|
||||
// UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];
|
||||
//
|
||||
// [tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
MPSiteEntity *site = [self siteInContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]];
|
||||
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
|
||||
|
||||
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
|
||||
|
||||
@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
|
||||
|
||||
@implementation MPSendAnswersCell : UITableViewCell
|
||||
@implementation MPSendAnswersCell
|
||||
|
||||
@end
|
||||
|
||||
@implementation MPMultipleAnswersCell : UITableViewCell
|
||||
@implementation MPMultipleAnswersCell
|
||||
|
||||
@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
|
||||
|
@ -30,5 +30,6 @@ typedef NS_ENUM ( NSUInteger, MPPasswordCellMode ) {
|
||||
- (void)setSite:(MPSiteEntity *)site animated:(BOOL)animated;
|
||||
- (void)setTransientSite:(NSString *)siteName animated:(BOOL)animated;
|
||||
- (void)setMode:(MPPasswordCellMode)mode animated:(BOOL)animated;
|
||||
- (MPSiteEntity *)siteInContext:(NSManagedObjectContext *)context;
|
||||
|
||||
@end
|
||||
|
@ -32,6 +32,7 @@
|
||||
@property(nonatomic, strong) IBOutlet UILabel *counterLabel;
|
||||
@property(nonatomic, strong) IBOutlet UIButton *counterButton;
|
||||
@property(nonatomic, strong) IBOutlet UIButton *upgradeButton;
|
||||
@property(nonatomic, strong) IBOutlet UIButton *answersButton;
|
||||
@property(nonatomic, strong) IBOutlet UIButton *modeButton;
|
||||
@property(nonatomic, strong) IBOutlet UIButton *editButton;
|
||||
@property(nonatomic, strong) IBOutlet UIScrollView *modeScrollView;
|
||||
@ -464,7 +465,8 @@
|
||||
MPSiteEntity *mainSite = [self siteInContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]];
|
||||
|
||||
// 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;
|
||||
self.loginNameContainer.alpha = settingsMode || mainSite.loginGenerated || [mainSite.loginName length]? 0.7f: 0;
|
||||
self.loginNameField.textColor = [UIColor colorWithHexString:mainSite.loginGenerated? @"5E636D": @"6D5E63"];
|
||||
|
@ -23,6 +23,7 @@
|
||||
#import "MPAppDelegate_Key.h"
|
||||
#import "MPPasswordCell.h"
|
||||
#import "UICollectionView+PearlReloadFromArray.h"
|
||||
#import "MPAnswersViewController.h"
|
||||
|
||||
@interface MPPasswordsViewController()<NSFetchedResultsControllerDelegate>
|
||||
|
||||
@ -85,6 +86,9 @@
|
||||
|
||||
if ([segue.identifier isEqualToString:@"popdown"])
|
||||
_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 {
|
||||
|
@ -16,11 +16,8 @@
|
||||
|
||||
@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;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MPiOSAppDelegate
|
||||
|
@ -117,6 +117,8 @@
|
||||
DA32CFF319CF1C8F004F3F0E /* MPSiteEntity.m in Sources */ = {isa = PBXBuildFile; fileRef = DA32CFEC19CF1C8F004F3F0E /* MPSiteEntity.m */; };
|
||||
DA32CFF419CF1C8F004F3F0E /* MPGeneratedSiteEntity.m in Sources */ = {isa = PBXBuildFile; fileRef = DA32CFEE19CF1C8F004F3F0E /* MPGeneratedSiteEntity.m */; };
|
||||
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 */; };
|
||||
DA3509FF15F101A500C14A8E /* PearlQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = DA3509FD15F101A500C14A8E /* PearlQueue.m */; };
|
||||
DA38D6A318CCB5BF009AEB3E /* Storyboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DA38D6A218CCB5BF009AEB3E /* Storyboard.storyboard */; };
|
||||
@ -3071,11 +3073,13 @@
|
||||
DA25C5FA197CCAE00046CDCF /* icon_delete.png in Resources */,
|
||||
DA25C601197DBF260046CDCF /* icon_trash@2x.png in Resources */,
|
||||
DABD39551711E29700CF925C /* avatar-6.png in Resources */,
|
||||
DA32D00919CF5C55004F3F0E /* icon_question.png in Resources */,
|
||||
DABD39561711E29700CF925C /* avatar-6@2x.png in Resources */,
|
||||
DABD39571711E29700CF925C /* avatar-7.png in Resources */,
|
||||
DABD39581711E29700CF925C /* avatar-7@2x.png in Resources */,
|
||||
DABD39591711E29700CF925C /* avatar-8.png in Resources */,
|
||||
DA250A0D1956484D00AC23F1 /* image-2@2x.png in Resources */,
|
||||
DA32D00A19CF5C55004F3F0E /* icon_question@2x.png in Resources */,
|
||||
DA250A051956484D00AC23F1 /* image-6@2x.png in Resources */,
|
||||
DABD395A1711E29700CF925C /* avatar-8@2x.png in Resources */,
|
||||
DABD395B1711E29700CF925C /* avatar-9.png in Resources */,
|
||||
|
@ -1203,12 +1203,12 @@
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LvK-28-fbm" userLabel="Settings Container">
|
||||
<rect key="frame" x="300" y="0.0" width="300" height="100"/>
|
||||
<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">
|
||||
<rect key="frame" x="29" y="56" width="44" height="44"/>
|
||||
<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="-15" 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"/>
|
||||
<constraint firstAttribute="width" constant="44" id="5DT-m6-RYu"/>
|
||||
<constraint firstAttribute="height" constant="44" id="kyZ-o1-ntZ"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
|
||||
<inset key="contentEdgeInsets" minX="6" minY="6" maxX="6" maxY="6"/>
|
||||
@ -1220,7 +1220,27 @@
|
||||
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</state>
|
||||
<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>
|
||||
</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">
|
||||
@ -1316,9 +1336,11 @@
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
|
||||
<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="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="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="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"/>
|
||||
@ -1366,6 +1388,7 @@
|
||||
<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="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="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"/>
|
||||
@ -1417,6 +1440,7 @@
|
||||
<constraint firstAttribute="bottom" secondItem="xph-TW-9QO" secondAttribute="bottom" id="W1r-47-bqe"/>
|
||||
</constraints>
|
||||
<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="counterButton" destination="uZi-FT-Fe8" id="oaF-bB-Sgc"/>
|
||||
<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="siteNameLabel" destination="OwP-sb-Wxl" id="6GN-Ou-K0F"/>
|
||||
<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>
|
||||
</collectionViewCell>
|
||||
</cells>
|
||||
@ -1578,7 +1602,6 @@
|
||||
<outlet property="popdownToTopConstraint" destination="BdD-Kc-eHl" id="59Y-ap-Yn4"/>
|
||||
<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="koB-V2-GYf" kind="modal" identifier="answers" id="PbR-2r-Ebm"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<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"/>
|
||||
<textInputTraits key="textInputTraits" keyboardType="namePhonePad" keyboardAppearance="alert" returnKeyType="done"/>
|
||||
<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>
|
||||
</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">
|
||||
@ -2901,6 +2925,7 @@ See </string>
|
||||
<image name="icon_list-names.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_question.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_tools.png" width="32" height="32"/>
|
||||
|
Loading…
Reference in New Issue
Block a user