From a383d0eee7359736e91fb9f66369ed3199872b04 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Thu, 5 Mar 2015 17:28:04 -0500 Subject: [PATCH 1/3] Make new site creation on Mac same as iOS. [FIXED] Unable to create a site that is a substring of an existing site. --- MasterPassword/C/mpw-util.c | 40 +++--- .../ObjC/Mac/MPPasswordWindowController.m | 44 ++++--- MasterPassword/ObjC/Mac/MPSiteModel.h | 6 +- MasterPassword/ObjC/Mac/MPSiteModel.m | 114 +++++++++++++----- 4 files changed, 139 insertions(+), 65 deletions(-) diff --git a/MasterPassword/C/mpw-util.c b/MasterPassword/C/mpw-util.c index b50ae37b..347afe70 100644 --- a/MasterPassword/C/mpw-util.c +++ b/MasterPassword/C/mpw-util.c @@ -80,7 +80,7 @@ uint8_t const *mpw_scrypt(const size_t keySize, const char *secret, const uint8_ uint8_t const *mpw_hmac_sha256(const uint8_t *key, const size_t keySize, const uint8_t *salt, const size_t saltSize) { - uint8_t *const buffer = malloc(32); + uint8_t *const buffer = malloc( 32 ); if (!buffer) return NULL; @@ -96,24 +96,29 @@ const char *mpw_idForBuf(const void *buf, size_t length) { return mpw_hex( hash, 32 ); } -static char **mpw_hex_buf = NULL; -static unsigned int mpw_hex_buf_i = 0; +//static char **mpw_hex_buf = NULL; +//static unsigned int mpw_hex_buf_i = 0; + const char *mpw_hex(const void *buf, size_t length) { - if (!mpw_hex_buf) { - mpw_hex_buf = malloc( 10 * sizeof( char* ) ); - for (uint8_t i = 0; i < 10; ++i) - mpw_hex_buf[i] = NULL; - } - mpw_hex_buf_i = (mpw_hex_buf_i + 1) % 10; + // FIXME +// if (!mpw_hex_buf) { +// mpw_hex_buf = malloc( 10 * sizeof( char * ) ); +// for (uint8_t i = 0; i < 10; ++i) +// mpw_hex_buf[i] = NULL; +// } +// mpw_hex_buf_i = (mpw_hex_buf_i + 1) % 10; +// +// mpw_hex_buf[mpw_hex_buf_i] = realloc( mpw_hex_buf[mpw_hex_buf_i], length * 2 + 1 ); +// for (size_t kH = 0; kH < length; kH++) +// sprintf( &(mpw_hex_buf[mpw_hex_buf_i][kH * 2]), "%02X", ((const uint8_t *)buf)[kH] ); - mpw_hex_buf[mpw_hex_buf_i] = realloc( mpw_hex_buf[mpw_hex_buf_i], length * 2 + 1 ); - for (size_t kH = 0; kH < length; kH++) - sprintf( &(mpw_hex_buf[mpw_hex_buf_i][kH * 2]), "%02X", ((const uint8_t *)buf)[kH] ); - - return mpw_hex_buf[mpw_hex_buf_i]; +// return mpw_hex_buf[mpw_hex_buf_i]; + return NULL; } + const char *mpw_hex_l(uint32_t number) { + return mpw_hex( &number, sizeof( number ) ); } @@ -144,7 +149,8 @@ const char *mpw_identicon(const char *fullName, const char *masterPassword) { const char *accessory[] = { "◈", "◎", "◐", "◑", "◒", "◓", "☀", "☁", "☂", "☃", "☄", "★", "☆", "☎", "☏", "⎈", "⌂", "☘", "☢", "☣", "☕", "⌚", "⌛", "⏰", "⚡", "⛄", "⛅", "☔", "♔", "♕", "♖", "♗", "♘", "♙", "♚", "♛", "♜", "♝", "♞", "♟", - "♨", "♩", "♪", "♫", "⚐", "⚑", "⚔", "⚖", "⚙", "⚠", "⌘", "⏎", "✄", "✆", "✈", "✉", "✌" }; + "♨", "♩", "♪", "♫", "⚐", "⚑", "⚔", "⚖", "⚙", "⚠", "⌘", "⏎", "✄", "✆", "✈", "✉", "✌" + }; uint8_t identiconSeed[32]; HMAC_SHA256_Buf( masterPassword, strlen( masterPassword ), fullName, strlen( fullName ), identiconSeed ); @@ -206,8 +212,8 @@ const size_t mpw_charlen(const char *utf8String) { size_t charlen = 0; char *remainingString = (char *)utf8String; - for (int charByteSize; (charByteSize = mpw_charByteSize( *remainingString )); remainingString += charByteSize) + for (int charByteSize; (charByteSize = mpw_charByteSize( (unsigned char)*remainingString )); remainingString += charByteSize) ++charlen; - return charlen; + return charlen; } diff --git a/MasterPassword/ObjC/Mac/MPPasswordWindowController.m b/MasterPassword/ObjC/Mac/MPPasswordWindowController.m index ae733511..3fd217c9 100644 --- a/MasterPassword/ObjC/Mac/MPPasswordWindowController.m +++ b/MasterPassword/ObjC/Mac/MPPasswordWindowController.m @@ -463,26 +463,26 @@ - (void)useSite { MPSiteModel *selectedSite = [self selectedSite]; - if (selectedSite) { - // Performing action while content is available. Copy it. - [self copyContent:selectedSite.content]; + if (!selectedSite) + return; - [self fadeOut]; + if (selectedSite.transient) { + [self createNewSite:selectedSite.name]; + return; + } - NSUserNotification *notification = [NSUserNotification new]; - notification.title = @"Password Copied"; - if (selectedSite.loginName.length) - notification.subtitle = strf( @"%@ at %@", selectedSite.loginName, selectedSite.name ); - else - notification.subtitle = selectedSite.name; - [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification]; - } - else { - NSString *siteName = [self.siteField stringValue]; - if ([siteName length]) - // Performing action without content but a site name is written. - [self createNewSite:siteName]; - } + // Performing action while content is available. Copy it. + [self copyContent:selectedSite.content]; + + [self fadeOut]; + + NSUserNotification *notification = [NSUserNotification new]; + notification.title = @"Password Copied"; + if (selectedSite.loginName.length) + notification.subtitle = strf( @"%@ at %@", selectedSite.loginName, selectedSite.name ); + else + notification.subtitle = selectedSite.name; + [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification]; } - (void)updateUser { @@ -549,9 +549,15 @@ return; } + BOOL exact = NO; NSMutableArray *newSites = [NSMutableArray arrayWithCapacity:[siteResults count]]; - for (MPSiteEntity *site in siteResults) + for (MPSiteEntity *site in siteResults) { [newSites addObject:[[MPSiteModel alloc] initWithEntity:site fuzzyGroups:fuzzyGroups]]; + exact |= [site.name isEqualToString:queryString]; + } + if (!exact && [queryString length]) + [newSites addObject:[[MPSiteModel alloc] initWithName:queryString]]; + dbg( @"newSites: %@", newSites ); self.sites = newSites; }]; } diff --git a/MasterPassword/ObjC/Mac/MPSiteModel.h b/MasterPassword/ObjC/Mac/MPSiteModel.h index e052fdbc..2581dc19 100644 --- a/MasterPassword/ObjC/Mac/MPSiteModel.h +++ b/MasterPassword/ObjC/Mac/MPSiteModel.h @@ -35,10 +35,12 @@ @property (nonatomic) NSUInteger counter; @property (nonatomic) NSDate *lastUsed; @property (nonatomic) id algorithm; -@property (nonatomic) BOOL generated; -@property (nonatomic) BOOL stored; +@property (nonatomic, readonly) BOOL generated; +@property (nonatomic, readonly) BOOL stored; +@property (nonatomic, readonly) BOOL transient; - (instancetype)initWithEntity:(MPSiteEntity *)entity fuzzyGroups:(NSArray *)fuzzyGroups; +- (instancetype)initWithName:(NSString *)siteName; - (MPSiteEntity *)entityInContext:(NSManagedObjectContext *)moc; - (void)updateContent; diff --git a/MasterPassword/ObjC/Mac/MPSiteModel.m b/MasterPassword/ObjC/Mac/MPSiteModel.m index c3e71205..56c7e6df 100644 --- a/MasterPassword/ObjC/Mac/MPSiteModel.m +++ b/MasterPassword/ObjC/Mac/MPSiteModel.m @@ -28,7 +28,7 @@ BOOL _initialized; } -- (id)initWithEntity:(MPSiteEntity *)entity fuzzyGroups:(NSArray *)fuzzyGroups { +- (instancetype)initWithEntity:(MPSiteEntity *)entity fuzzyGroups:(NSArray *)fuzzyGroups { if (!(self = [super init])) return nil; @@ -39,6 +39,17 @@ return self; } +- (instancetype)initWithName:(NSString *)siteName { + + if (!(self = [super init])) + return nil; + + [self setTransientSiteName:siteName]; + _initialized = YES; + + return self; +} + - (void)setEntity:(MPSiteEntity *)entity fuzzyGroups:(NSArray *)fuzzyGroups { if ([_entityOID isEqual:entity.objectID]) @@ -59,7 +70,7 @@ NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; paragraphStyle.alignment = NSCenterTextAlignment; [attributedSiteName addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange( 0, [siteName length] )]; - + self.displayedName = attributedSiteName; self.name = siteName; self.algorithm = entity.algorithm; @@ -73,6 +84,28 @@ [self updateContent:entity]; } +- (void)setTransientSiteName:(NSString *)siteName { + + _entityOID = nil; + + NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; + paragraphStyle.alignment = NSCenterTextAlignment; + self.displayedName = stra( siteName, @{ + NSBackgroundColorAttributeName : [NSColor alternateSelectedControlColor], + NSParagraphStyleAttributeName : paragraphStyle, + } ); + self.name = siteName; + self.algorithm = MPAlgorithmDefault; + self.lastUsed = nil; + self.type = [MPAppDelegate_Shared get].activeUserForMainThread.defaultType; + self.typeName = [self.algorithm nameOfType:self.type]; + self.uses = @0; + self.counter = 1; + + // Find all password types and the index of the current type amongst them. + [self updateContent]; +} + - (MPSiteEntity *)entityInContext:(NSManagedObjectContext *)moc { if (!_entityOID) @@ -96,15 +129,18 @@ // This wasn't a change to the entity. return; - [MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { - MPSiteEntity *entity = [self entityInContext:context]; - if ([entity isKindOfClass:[MPGeneratedSiteEntity class]]) { - ((MPGeneratedSiteEntity *)entity).counter = counter; - [context saveToStore]; + if (_entityOID) + [MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { + MPSiteEntity *entity = [self entityInContext:context]; + if ([entity isKindOfClass:[MPGeneratedSiteEntity class]]) { + ((MPGeneratedSiteEntity *)entity).counter = counter; + [context saveToStore]; - [self updateContent:entity]; - } - }]; + [self updateContent:entity]; + } + }]; + else + [self updateContent]; } - (BOOL)generated { @@ -117,36 +153,60 @@ return self.type & MPSiteTypeClassStored; } +- (BOOL)transient { + + return _entityOID == nil; +} + - (void)updateContent { - [MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { - [self updateContent:[MPSiteEntity existingObjectWithID:_entityOID inContext:context]]; - }]; + if (_entityOID) + [MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { + [self updateContent:[MPSiteEntity existingObjectWithID:_entityOID inContext:context]]; + }]; + else + PearlNotMainQueue( ^{ + NSString *password = [self.algorithm generatePasswordForSiteNamed:self.name ofType:self.type withCounter:self.counter + usingKey:[MPAppDelegate_Shared get].key]; + NSString *loginName = [self.algorithm generateLoginForSiteNamed:self.name usingKey:[MPAppDelegate_Shared get].key]; + [self updatePasswordWithResult:password]; + [self updateLoginNameWithResult:loginName]; + } ); } - (void)updateContent:(MPSiteEntity *)entity { + [entity resolvePasswordUsingKey:[MPAppDelegate_Shared get].key result:^(NSString *result) { + [self updatePasswordWithResult:result]; + }]; + [entity resolveLoginUsingKey:[MPAppDelegate_Shared get].key result:^(NSString *result) { + [self updateLoginNameWithResult:result]; + }]; +} + +- (void)updatePasswordWithResult:(NSString *)result { + static NSRegularExpression *re_anyChar; static dispatch_once_t once = 0; dispatch_once( &once, ^{ re_anyChar = [NSRegularExpression regularExpressionWithPattern:@"." options:0 error:nil]; } ); - [entity resolvePasswordUsingKey:[MPAppDelegate_Shared get].key result:^(NSString *result) { - NSString *displayResult = result; - if ([[MPConfig get].hidePasswords boolValue] && !([NSEvent modifierFlags] & NSAlternateKeyMask)) - displayResult = [displayResult stringByReplacingMatchesOfExpression:re_anyChar withTemplate:@"●"]; + NSString *displayResult = result; + if ([[MPConfig get].hidePasswords boolValue] && !([NSEvent modifierFlags] & NSAlternateKeyMask)) + displayResult = [displayResult stringByReplacingMatchesOfExpression:re_anyChar withTemplate:@"●"]; - PearlMainQueue( ^{ - self.content = result; - self.displayedContent = displayResult; - } ); - }]; - [entity resolveLoginUsingKey:[MPAppDelegate_Shared get].key result:^(NSString *result) { - PearlMainQueue( ^{ - self.loginName = result; - } ); - }]; + PearlMainQueue( ^{ + self.content = result; + self.displayedContent = displayResult; + } ); +} + +- (void)updateLoginNameWithResult:(NSString *)loginName { + + PearlMainQueue( ^{ + self.loginName = loginName; + } ); } @end From fd35fea8cfb1cc1273e5abce69c97e4175ac7fb1 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Wed, 11 Mar 2015 17:30:34 -0400 Subject: [PATCH 2/3] mpw C code is not thread-safe + bad performance long site queries. --- External/Pearl | 2 +- MasterPassword/C/mpw-util.c | 27 ++++---- MasterPassword/ObjC/MPAlgorithmV0.m | 64 ++++++++++++++----- MasterPassword/ObjC/MPAppDelegate_Store.m | 31 +++++---- .../ObjC/Mac/MPPasswordWindowController.m | 23 +++++-- MasterPassword/ObjC/Mac/MPSiteModel.h | 3 +- MasterPassword/ObjC/Mac/MPSiteModel.m | 8 +-- .../ObjC/iOS/MPPasswordsViewController.m | 7 +- Site/mpw-js/js/mpw-js | 2 +- 9 files changed, 111 insertions(+), 56 deletions(-) diff --git a/External/Pearl b/External/Pearl index 08c42ba0..65b4e3d9 160000 --- a/External/Pearl +++ b/External/Pearl @@ -1 +1 @@ -Subproject commit 08c42ba0f96a95703ec67cbf5846bfe6680dd0a6 +Subproject commit 65b4e3d9984d077f66e6ab15f2ffcc4965d07825 diff --git a/MasterPassword/C/mpw-util.c b/MasterPassword/C/mpw-util.c index 347afe70..49236a39 100644 --- a/MasterPassword/C/mpw-util.c +++ b/MasterPassword/C/mpw-util.c @@ -96,25 +96,24 @@ const char *mpw_idForBuf(const void *buf, size_t length) { return mpw_hex( hash, 32 ); } -//static char **mpw_hex_buf = NULL; -//static unsigned int mpw_hex_buf_i = 0; +static char **mpw_hex_buf = NULL; +static unsigned int mpw_hex_buf_i = 0; const char *mpw_hex(const void *buf, size_t length) { // FIXME -// if (!mpw_hex_buf) { -// mpw_hex_buf = malloc( 10 * sizeof( char * ) ); -// for (uint8_t i = 0; i < 10; ++i) -// mpw_hex_buf[i] = NULL; -// } -// mpw_hex_buf_i = (mpw_hex_buf_i + 1) % 10; -// -// mpw_hex_buf[mpw_hex_buf_i] = realloc( mpw_hex_buf[mpw_hex_buf_i], length * 2 + 1 ); -// for (size_t kH = 0; kH < length; kH++) -// sprintf( &(mpw_hex_buf[mpw_hex_buf_i][kH * 2]), "%02X", ((const uint8_t *)buf)[kH] ); + if (!mpw_hex_buf) { + mpw_hex_buf = malloc( 10 * sizeof( char * ) ); + for (uint8_t i = 0; i < 10; ++i) + mpw_hex_buf[i] = NULL; + } + mpw_hex_buf_i = (mpw_hex_buf_i + 1) % 10; -// return mpw_hex_buf[mpw_hex_buf_i]; - return NULL; + mpw_hex_buf[mpw_hex_buf_i] = realloc( mpw_hex_buf[mpw_hex_buf_i], length * 2 + 1 ); + for (size_t kH = 0; kH < length; kH++) + sprintf( &(mpw_hex_buf[mpw_hex_buf_i][kH * 2]), "%02X", ((const uint8_t *)buf)[kH] ); + + return mpw_hex_buf[mpw_hex_buf_i]; } const char *mpw_hex_l(uint32_t number) { diff --git a/MasterPassword/ObjC/MPAlgorithmV0.m b/MasterPassword/ObjC/MPAlgorithmV0.m index bf5a7f46..f25c2c93 100644 --- a/MasterPassword/ObjC/MPAlgorithmV0.m +++ b/MasterPassword/ObjC/MPAlgorithmV0.m @@ -28,8 +28,10 @@ #define CRACKING_PER_SECOND 2495000000UL #define CRACKING_PRICE 350 +NSOperationQueue *_mpwQueue = nil; + @implementation MPAlgorithmV0 { - BN_CTX *ctx; + BN_CTX *_ctx; } - (id)init { @@ -37,15 +39,22 @@ if (!(self = [super init])) return nil; - ctx = BN_CTX_new(); + _ctx = BN_CTX_new(); + + static dispatch_once_t once = 0; + dispatch_once( &once, ^{ + _mpwQueue = [NSOperationQueue new]; + _mpwQueue.maxConcurrentOperationCount = 1; + _mpwQueue.name = @"mpw queue"; + } ); return self; } - (void)dealloc { - BN_CTX_free( ctx ); - ctx = NULL; + BN_CTX_free( _ctx ); + _ctx = NULL; } - (MPAlgorithmVersion)version { @@ -68,6 +77,19 @@ return [(id)other version] == [self version]; } +- (void)mpw_perform:(void ( ^ )(void))operationBlock { + + if ([NSOperationQueue currentQueue] == _mpwQueue) { + operationBlock(); + return; + } + + NSOperation *operation = [NSBlockOperation blockOperationWithBlock:operationBlock]; + if ([operation respondsToSelector:@selector( qualityOfService )]) + operation.qualityOfService = NSQualityOfServiceUserInitiated; + [_mpwQueue addOperations:@[ operation ] waitUntilFinished:YES]; +} + - (BOOL)tryMigrateUser:(MPUserEntity *)user inContext:(NSManagedObjectContext *)moc { NSError *error = nil; @@ -107,12 +129,16 @@ - (NSData *)keyDataForFullName:(NSString *)fullName withMasterPassword:(NSString *)masterPassword { - NSDate *start = [NSDate date]; - uint8_t const *masterKeyBytes = mpw_masterKeyForUser( fullName.UTF8String, masterPassword.UTF8String, [self version] ); - NSData *keyData = [NSData dataWithBytes:masterKeyBytes length:MP_dkLen]; - trc( @"User: %@, password: %@ derives to key ID: %@ (took %0.2fs)", // - fullName, masterPassword, [self keyIDForKeyData:keyData], -[start timeIntervalSinceNow] ); - mpw_free( masterKeyBytes, MP_dkLen ); + __block NSData *keyData; + [self mpw_perform:^{ + NSDate *start = [NSDate date]; + uint8_t const *masterKeyBytes = mpw_masterKeyForUser( fullName.UTF8String, masterPassword.UTF8String, [self version] ); + keyData = [NSData dataWithBytes:masterKeyBytes length:MP_dkLen]; + trc( @"User: %@, password: %@ derives to key ID: %@ (took %0.2fs)", // + fullName, masterPassword, [self keyIDForKeyData:keyData], -[start timeIntervalSinceNow] ); + mpw_free( masterKeyBytes, MP_dkLen ); + }]; + return keyData; } @@ -317,10 +343,13 @@ - (NSString *)generateContentForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter variant:(MPSiteVariant)variant context:(NSString *)context usingKey:(MPKey *)key { - char const *contentBytes = mpw_passwordForSite( [key keyDataForAlgorithm:self].bytes, - name.UTF8String, type, (uint32_t)counter, variant, context.UTF8String, [self version] ); - NSString *content = [NSString stringWithCString:contentBytes encoding:NSUTF8StringEncoding]; - mpw_freeString( contentBytes ); + __block NSString *content; + [self mpw_perform:^{ + char const *contentBytes = mpw_passwordForSite( [key keyDataForAlgorithm:self].bytes, + name.UTF8String, type, (uint32_t)counter, variant, context.UTF8String, [self version] ); + content = [NSString stringWithCString:contentBytes encoding:NSUTF8StringEncoding]; + mpw_freeString( contentBytes ); + }]; return content; } @@ -382,7 +411,7 @@ [PearlKeyChain deleteItemForQuery:siteQuery]; else [PearlKeyChain addOrUpdateItemForQuery:siteQuery withAttributes:@{ - (__bridge id)kSecValueData : encryptedContent, + (__bridge id)kSecValueData : encryptedContent, #if TARGET_OS_IPHONE (__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly, #endif @@ -562,7 +591,8 @@ - (void)resolveAnswerForQuestion:(MPSiteQuestionEntity *)question usingKey:(MPKey *)siteKey result:(void ( ^ )(NSString *result))resultBlock { - NSAssert( [[siteKey keyIDForAlgorithm:question.site.user.algorithm] isEqualToData:question.site.user.keyID], @"Site does not belong to current user." ); + NSAssert( [[siteKey keyIDForAlgorithm:question.site.user.algorithm] isEqualToData:question.site.user.keyID], + @"Site does not belong to current user." ); NSString *name = question.site.name; NSString *keyword = question.keyword; id algorithm = nil; @@ -748,7 +778,7 @@ if (strchr( charactersForClass, passwordCharacter )) { // Found class for password character. - characterEntropy = (BN_ULONG)strlen(charactersForClass); + characterEntropy = (BN_ULONG)strlen( charactersForClass ); break; } } diff --git a/MasterPassword/ObjC/MPAppDelegate_Store.m b/MasterPassword/ObjC/MPAppDelegate_Store.m index 730f8622..8eba5670 100644 --- a/MasterPassword/ObjC/MPAppDelegate_Store.m +++ b/MasterPassword/ObjC/MPAppDelegate_Store.m @@ -190,14 +190,14 @@ PearlAssociatedObjectProperty( NSNumber*, StoreCorrupted, storeCorrupted ); // When privateManagedObjectContext is saved, import the changes into mainManagedObjectContext. PearlAddNotificationObserverTo( self.mainManagedObjectContext, NSManagedObjectContextDidSaveNotification, self.privateManagedObjectContext, nil, ^(NSManagedObjectContext *mainManagedObjectContext, NSNotification *note) { - [mainManagedObjectContext performBlock:^{ - @try { - [mainManagedObjectContext mergeChangesFromContextDidSaveNotification:note]; - } - @catch (NSException *exception) { - err( @"While merging changes:\n%@",[exception fullDescription] ); - } - }]; + [mainManagedObjectContext performBlock:^{ + @try { + [mainManagedObjectContext mergeChangesFromContextDidSaveNotification:note]; + } + @catch (NSException *exception) { + err( @"While merging changes:\n%@", [exception fullDescription] ); + } + }]; } ); @@ -821,10 +821,17 @@ PearlAssociatedObjectProperty( NSNumber*, StoreCorrupted, storeCorrupted ); content = [site.algorithm exportPasswordForSite:site usingKey:self.key]; } - [export appendFormat:@"%@ %8ld %8s %25s\t%25s\t%@\n", - [[NSDateFormatter rfc3339DateFormatter] stringFromDate:lastUsed], (long)uses, - [strf( @"%lu:%lu:%lu", (long)type, (long)[algorithm version], (long)counter ) UTF8String], - [(loginName?: @"") UTF8String], [siteName UTF8String], content?: @""]; + NSString *lastUsedExport = [[NSDateFormatter rfc3339DateFormatter] stringFromDate:lastUsed]; + long usesExport = (long)uses; + NSString *typeExport = strf( @"%lu:%lu:%lu", (long)type, (long)[algorithm version], (long)counter ); + NSString *loginNameExport = loginName?: @""; + NSString *contentExport = content?: @""; + [export appendFormat:@"%@ %8ld %8S %25S\t%25S\t%@\n", + lastUsedExport, usesExport, + (const unichar *)[typeExport cStringUsingEncoding:NSUTF16StringEncoding], + (const unichar *)[loginNameExport cStringUsingEncoding:NSUTF16StringEncoding], + (const unichar *)[siteName cStringUsingEncoding:NSUTF16StringEncoding], + contentExport]; } return export; diff --git a/MasterPassword/ObjC/Mac/MPPasswordWindowController.m b/MasterPassword/ObjC/Mac/MPPasswordWindowController.m index 3fd217c9..d80f8cb0 100644 --- a/MasterPassword/ObjC/Mac/MPPasswordWindowController.m +++ b/MasterPassword/ObjC/Mac/MPPasswordWindowController.m @@ -17,6 +17,7 @@ // #import +#import #import "MPPasswordWindowController.h" #import "MPMacAppDelegate.h" #import "MPAppDelegate_Store.h" @@ -518,6 +519,7 @@ - (void)updateSites { + NSAssert( [NSOperationQueue currentQueue] == [NSOperationQueue mainQueue], @"updateSites should be called on the main queue." ); if (![MPMacAppDelegate get].key) { self.sites = nil; return; @@ -530,13 +532,18 @@ } ); NSString *queryString = self.siteField.stringValue; - NSString *queryPattern = [queryString stringByReplacingMatchesOfExpression:fuzzyRE withTemplate:@"*$1*"]; + NSString *queryPattern; + if ([queryString length] < 13) + queryPattern = [queryString stringByReplacingMatchesOfExpression:fuzzyRE withTemplate:@"*$1*"]; + else + // If query is too long, a wildcard per character makes the CoreData fetch take excessively long. + queryPattern = strf( @"*%@*", queryString ); NSMutableArray *fuzzyGroups = [NSMutableArray new]; [fuzzyRE enumerateMatchesInString:queryString options:0 range:NSMakeRange( 0, queryString.length ) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { [fuzzyGroups addObject:[queryString substringWithRange:result.range]]; }]; - [MPMacAppDelegate managedObjectContextPerformBlockAndWait:^(NSManagedObjectContext *context) { + [MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPSiteEntity class] )]; fetchRequest.sortDescriptors = @[ [[NSSortDescriptor alloc] initWithKey:@"lastUsed" ascending:NO] ]; fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name LIKE[cd] %@) AND user == %@", @@ -555,10 +562,16 @@ [newSites addObject:[[MPSiteModel alloc] initWithEntity:site fuzzyGroups:fuzzyGroups]]; exact |= [site.name isEqualToString:queryString]; } - if (!exact && [queryString length]) - [newSites addObject:[[MPSiteModel alloc] initWithName:queryString]]; + if (!exact && [queryString length]) { + MPUserEntity *activeUser = [[MPAppDelegate_Shared get] activeUserInContext:context]; + [newSites addObject:[[MPSiteModel alloc] initWithName:queryString forUser:activeUser]]; + } + dbg( @"newSites: %@", newSites ); - self.sites = newSites; + if (![newSites isEqualToArray:self.sites]) + PearlMainQueue( ^{ + self.sites = newSites; + } ); }]; } diff --git a/MasterPassword/ObjC/Mac/MPSiteModel.h b/MasterPassword/ObjC/Mac/MPSiteModel.h index 2581dc19..ae29dc04 100644 --- a/MasterPassword/ObjC/Mac/MPSiteModel.h +++ b/MasterPassword/ObjC/Mac/MPSiteModel.h @@ -19,6 +19,7 @@ #import #import "MPSiteEntity.h" #import "MPAlgorithm.h" +#import "MPUserEntity.h" @class MPSiteEntity; @@ -40,7 +41,7 @@ @property (nonatomic, readonly) BOOL transient; - (instancetype)initWithEntity:(MPSiteEntity *)entity fuzzyGroups:(NSArray *)fuzzyGroups; -- (instancetype)initWithName:(NSString *)siteName; +- (instancetype)initWithName:(NSString *)siteName forUser:(MPUserEntity *)user; - (MPSiteEntity *)entityInContext:(NSManagedObjectContext *)moc; - (void)updateContent; diff --git a/MasterPassword/ObjC/Mac/MPSiteModel.m b/MasterPassword/ObjC/Mac/MPSiteModel.m index 56c7e6df..ba9f8ace 100644 --- a/MasterPassword/ObjC/Mac/MPSiteModel.m +++ b/MasterPassword/ObjC/Mac/MPSiteModel.m @@ -39,12 +39,12 @@ return self; } -- (instancetype)initWithName:(NSString *)siteName { +- (instancetype)initWithName:(NSString *)siteName forUser:(MPUserEntity *)user { if (!(self = [super init])) return nil; - [self setTransientSiteName:siteName]; + [self setTransientSiteName:siteName forUser:user]; _initialized = YES; return self; @@ -84,7 +84,7 @@ [self updateContent:entity]; } -- (void)setTransientSiteName:(NSString *)siteName { +- (void)setTransientSiteName:(NSString *)siteName forUser:(MPUserEntity *)user { _entityOID = nil; @@ -97,7 +97,7 @@ self.name = siteName; self.algorithm = MPAlgorithmDefault; self.lastUsed = nil; - self.type = [MPAppDelegate_Shared get].activeUserForMainThread.defaultType; + self.type = user.defaultType; self.typeName = [self.algorithm nameOfType:self.type]; self.uses = @0; self.counter = 1; diff --git a/MasterPassword/ObjC/iOS/MPPasswordsViewController.m b/MasterPassword/ObjC/iOS/MPPasswordsViewController.m index 3a75b112..9929e94a 100644 --- a/MasterPassword/ObjC/iOS/MPPasswordsViewController.m +++ b/MasterPassword/ObjC/iOS/MPPasswordsViewController.m @@ -392,7 +392,12 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { } ); NSString *queryString = self.query; - NSString *queryPattern = [queryString stringByReplacingMatchesOfExpression:fuzzyRE withTemplate:@"*$1*"]; + NSString *queryPattern; + if ([queryString length] < 13) + queryPattern = [queryString stringByReplacingMatchesOfExpression:fuzzyRE withTemplate:@"*$1*"]; + else + // If query is too long, a wildcard per character makes the CoreData fetch take excessively long. + queryPattern = strf( @"*%@*", queryString ); NSMutableArray *fuzzyGroups = [NSMutableArray arrayWithCapacity:[queryString length]]; [fuzzyRE enumerateMatchesInString:queryString options:0 range:NSMakeRange( 0, queryString.length ) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { diff --git a/Site/mpw-js/js/mpw-js b/Site/mpw-js/js/mpw-js index 442e4189..28a988eb 160000 --- a/Site/mpw-js/js/mpw-js +++ b/Site/mpw-js/js/mpw-js @@ -1 +1 @@ -Subproject commit 442e41896998e06e850b42f9f8ea4b33bb237bf1 +Subproject commit 28a988ebe6d1b8052ecc5190b5f9a1fd658b6cf8 From 9cffe53993a42dca8fb50308a49e80024184518a Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Thu, 12 Mar 2015 01:03:02 -0400 Subject: [PATCH 3/3] A bash completion script for mpw. --- MasterPassword/C/bashcomplib | 61 ++++++++++++++++++++++++++++ MasterPassword/C/mpw-algorithm.h | 1 + MasterPassword/C/mpw.completion.bash | 56 +++++++++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 MasterPassword/C/bashcomplib create mode 100644 MasterPassword/C/mpw.completion.bash diff --git a/MasterPassword/C/bashcomplib b/MasterPassword/C/bashcomplib new file mode 100644 index 00000000..367e415f --- /dev/null +++ b/MasterPassword/C/bashcomplib @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +# FIXME +# partials are currently readline words, but these can't be reliably compared against literal data. We need to make them literal first.. in a safe way. Currently using xargs' quote parser as a hack. + +# Process literal completion options in COMPREPLY +# +# 1. Filter COMPREPLY by excluding the options that do not match the word that is being completed. +# 2. Shell-escape the COMPREPLY words so they remain syntactical words when injected into the completed command. +# 3. Add a space after the words so successful completions advance to the next word +# (we disabled this default behavior with -o nospace so we can do completions that don't want this, eg. directory names) +_comp_finish_completions() { + local partial=$(xargs <<< "${COMP_WORDS[COMP_CWORD]}") # FIXME + local word words=( "${COMPREPLY[@]}" ) + + COMPREPLY=() + for word in "${words[@]}"; do + ( shopt -s nocasematch; [[ $word = $partial* ]] ) && COMPREPLY+=( "$(printf '%q ' "$word")" ) + done + + if (( ${#COMPREPLY[@]} > 1 )) && [[ $_comp_title ]]; then + printf '\n%s:' "$_comp_title" + unset _comp_title + fi +} + +# Perform pathname completion. +# +# 1. Populate COMPREPLY with pathnames. +# 2. Shell-escape the COMPREPLY words so they remain syntactical words when injected into the completed command. +# 3. Add a space after file names so successful completions advance to the next word. +# Directory names are suffixed with a / instead so we can keep completing the files inside. +_comp_complete_path() { + local partial=$(xargs <<< "${COMP_WORDS[COMP_CWORD]}") + local path + + COMPREPLY=() + for path in "$partial"*; do + if [[ -d $path ]]; then + COMPREPLY+=( "$(printf '%q/' "$path")" ) + + elif [[ -e $path ]]; then + COMPREPLY+=( "$(printf '%q ' "$path")" ) + + fi + done +} + +_show_args() { + echo + local i=0 + for arg; do + printf "arg %d: %s\n" "$((i++))" "$arg" + done + + i=0 + for word in "${COMP_WORDS[@]}"; do + printf "word %d: %s -> %s %s\n" "$i" "$word" "$(xargs <<< "$word")" "$( ((i == $COMP_CWORD)) && echo '' )" + let i++ + done +} diff --git a/MasterPassword/C/mpw-algorithm.h b/MasterPassword/C/mpw-algorithm.h index 71fb0a01..dab72e7b 100644 --- a/MasterPassword/C/mpw-algorithm.h +++ b/MasterPassword/C/mpw-algorithm.h @@ -6,6 +6,7 @@ // Copyright (c) 2014 Lyndir. All rights reserved. // +// NOTE: mpw is currently NOT thread-safe. #include "mpw-types.h" typedef enum(unsigned int, MPAlgorithmVersion) { diff --git a/MasterPassword/C/mpw.completion.bash b/MasterPassword/C/mpw.completion.bash new file mode 100644 index 00000000..38943a8b --- /dev/null +++ b/MasterPassword/C/mpw.completion.bash @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +source bashcomplib + +# completing the 'mpw' command. +_comp_mpw() { + local optarg= cword=${COMP_WORDS[COMP_CWORD]} pcword + + if (( COMP_CWORD > 0 )); then + pcword=${COMP_WORDS[COMP_CWORD - 1]} + + case $pcword in + -u) optarg=user ;; + -t) optarg=type ;; + -c) optarg=counter ;; + -V) optarg=version ;; + -v) optarg=variant ;; + -C) optarg=context ;; + esac + fi + + case $optarg in + user) # complete full names. + COMPREPLY=( ~/.mpw.d/*.mpsites ) COMPREPLY=( "${COMPREPLY[@]##*/}" ) COMPREPLY=( "${COMPREPLY[@]%.mpsites}" ) + ;; + type) # complete types. + COMPREPLY=( maximum long medium basic short pin name phrase ) + ;; + counter) # complete counter. + COMPREPLY=( 1 ) + ;; + version) # complete versions. + COMPREPLY=( 0 1 2 3 ) + ;; + variant) # complete variants. + COMPREPLY=( password login answer ) + ;; + context) # complete context. + ;; + *) + # previous word is not an option we can complete, complete site name (or option if leading -) + if [[ $cword = -* ]]; then + COMPREPLY=( -u -t -c -V -v -C ) + else + local w fullName=$MP_FULLNAME + for (( w = 0; w < ${#COMP_WORDS[@]}; ++w )); do + [[ ${COMP_WORDS[w]} = -u ]] && fullName=$(xargs <<< "${COMP_WORDS[w + 1]}") && break + done + IFS=$'\n' read -d '' -ra COMPREPLY < <(awk -F$'\t' '!/^ *#/{sub(/^ */, "", $2); print $2}' ~/.mpw.d/"$fullName.mpsites") + printf -v _comp_title 'Sites for %s' "$fullName" + fi ;; + esac + _comp_finish_completions +} + +#complete -F _show_args mpw +complete -o nospace -F _comp_mpw mpw