//============================================================================== // This file is part of Master Password. // Copyright (c) 2011-2017, Maarten Billemont. // // Master Password is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Master Password is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You can find a copy of the GNU General Public License in the // LICENSE file. Alternatively, see . //============================================================================== #ifndef trc #error error #endif #import "MPAlgorithmV0.h" #import "MPEntities.h" #import "MPAppDelegate_Shared.h" #import "MPAppDelegate_InApp.h" #import "mpw-util.h" /* An AMD HD 7970 calculates 2495M SHA-1 hashes per second at a cost of ~350$ per GPU */ #define CRACKING_PER_SECOND 2495000000UL #define CRACKING_PRICE 350 static NSOperationQueue *_mpwQueue = nil; @implementation MPAlgorithmV0 - (id)init { if (!(self = [super init])) return nil; static dispatch_once_t once = 0; dispatch_once( &once, ^{ _mpwQueue = [NSOperationQueue new]; _mpwQueue.maxConcurrentOperationCount = 1; _mpwQueue.name = @"mpw queue"; } ); return self; } - (MPAlgorithmVersion)version { return MPAlgorithmVersion0; } - (NSString *)description { return strf( @"V%lu", (unsigned long)self.version ); } - (NSString *)debugDescription { return strf( @"<%@: version=%lu>", NSStringFromClass( [self class] ), (unsigned long)self.version ); } - (BOOL)isEqual:(id)other { if (other == self) return YES; if (!other || ![other conformsToProtocol:@protocol(MPAlgorithm)]) return NO; 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; NSFetchRequest *migrationRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPSiteEntity class] )]; migrationRequest.predicate = [NSPredicate predicateWithFormat:@"version_ < %d AND user == %@", self.version, user]; NSArray *migrationSites = [moc executeFetchRequest:migrationRequest error:&error]; if (!migrationSites) { MPError( error, @"While looking for sites to migrate." ); return NO; } BOOL success = YES; for (MPSiteEntity *migrationSite in migrationSites) if (![migrationSite tryMigrateExplicitly:NO]) success = NO; return success; } - (BOOL)tryMigrateSite:(MPSiteEntity *)site explicit:(BOOL)explicit { if ([site.algorithm version] != [self version] - 1) // Only migrate from previous version. return NO; if (!explicit) { // This migration requires explicit permission. site.requiresExplicitMigration = YES; return NO; } // Apply migration. site.requiresExplicitMigration = NO; site.algorithm = self; return YES; } - (NSData *)keyDataForFullName:(NSString *)fullName withMasterPassword:(NSString *)masterPassword { __block NSData *keyData; [self mpw_perform:^{ NSDate *start = [NSDate date]; MPMasterKey masterKey = mpw_masterKey( fullName.UTF8String, masterPassword.UTF8String, [self version] ); if (masterKey) { keyData = [NSData dataWithBytes:masterKey length:MPMasterKeySize]; trc( @"User: %@, password: %@ derives to key ID: %@ (took %0.2fs)", // fullName, masterPassword, [self keyIDForKey:masterKey], -[start timeIntervalSinceNow] ); mpw_free( masterKey, MPMasterKeySize ); } }]; return keyData; } - (NSData *)keyIDForKey:(MPMasterKey)masterKey { return [[NSData dataWithBytesNoCopy:(void *)masterKey length:MPMasterKeySize] hashWith:PearlHashSHA256]; } - (NSString *)nameOfType:(MPResultType)type { if (!type) return nil; switch (type) { case MPResultTypeTemplateMaximum: return @"Maximum Security Password"; case MPResultTypeTemplateLong: return @"Long Password"; case MPResultTypeTemplateMedium: return @"Medium Password"; case MPResultTypeTemplateBasic: return @"Basic Password"; case MPResultTypeTemplateShort: return @"Short Password"; case MPResultTypeTemplatePIN: return @"PIN"; case MPResultTypeTemplateName: return @"Name"; case MPResultTypeTemplatePhrase: return @"Phrase"; case MPResultTypeStatefulPersonal: return @"Personal Password"; case MPResultTypeStatefulDevice: return @"Device Private Password"; } Throw( @"Type not supported: %lu", (long)type ); } - (NSString *)shortNameOfType:(MPResultType)type { if (!type) return nil; switch (type) { case MPResultTypeTemplateMaximum: return @"Maximum"; case MPResultTypeTemplateLong: return @"Long"; case MPResultTypeTemplateMedium: return @"Medium"; case MPResultTypeTemplateBasic: return @"Basic"; case MPResultTypeTemplateShort: return @"Short"; case MPResultTypeTemplatePIN: return @"PIN"; case MPResultTypeTemplateName: return @"Name"; case MPResultTypeTemplatePhrase: return @"Phrase"; case MPResultTypeStatefulPersonal: return @"Personal"; case MPResultTypeStatefulDevice: return @"Device"; } Throw( @"Type not supported: %lu", (long)type ); } - (NSString *)classNameOfType:(MPResultType)type { return NSStringFromClass( [self classOfType:type] ); } - (Class)classOfType:(MPResultType)type { if (!type) Throw( @"No type given." ); switch (type) { case MPResultTypeTemplateMaximum: return [MPGeneratedSiteEntity class]; case MPResultTypeTemplateLong: return [MPGeneratedSiteEntity class]; case MPResultTypeTemplateMedium: return [MPGeneratedSiteEntity class]; case MPResultTypeTemplateBasic: return [MPGeneratedSiteEntity class]; case MPResultTypeTemplateShort: return [MPGeneratedSiteEntity class]; case MPResultTypeTemplatePIN: return [MPGeneratedSiteEntity class]; case MPResultTypeTemplateName: return [MPGeneratedSiteEntity class]; case MPResultTypeTemplatePhrase: return [MPGeneratedSiteEntity class]; case MPResultTypeStatefulPersonal: return [MPStoredSiteEntity class]; case MPResultTypeStatefulDevice: return [MPStoredSiteEntity class]; } Throw( @"Type not supported: %lu", (long)type ); } - (NSArray *)allTypes { return [self allTypesStartingWith:MPResultTypeTemplatePhrase]; } - (NSArray *)allTypesStartingWith:(MPResultType)startingType { NSMutableArray *allTypes = [[NSMutableArray alloc] initWithCapacity:8]; MPResultType currentType = startingType; do { [allTypes addObject:@(currentType)]; } while ((currentType = [self nextType:currentType]) != startingType); return allTypes; } - (MPResultType)defaultType { return MPResultTypeTemplateLong; } - (MPResultType)nextType:(MPResultType)type { switch (type) { case MPResultTypeTemplatePhrase: return MPResultTypeTemplateName; case MPResultTypeTemplateName: return MPResultTypeTemplateMaximum; case MPResultTypeTemplateMaximum: return MPResultTypeTemplateLong; case MPResultTypeTemplateLong: return MPResultTypeTemplateMedium; case MPResultTypeTemplateMedium: return MPResultTypeTemplateBasic; case MPResultTypeTemplateBasic: return MPResultTypeTemplateShort; case MPResultTypeTemplateShort: return MPResultTypeTemplatePIN; case MPResultTypeTemplatePIN: return MPResultTypeStatefulPersonal; case MPResultTypeStatefulPersonal: return MPResultTypeStatefulDevice; case MPResultTypeStatefulDevice: return MPResultTypeTemplatePhrase; } return [self defaultType]; } - (MPResultType)previousType:(MPResultType)type { MPResultType previousType = type, nextType = type; while ((nextType = [self nextType:nextType]) != type) previousType = nextType; return previousType; } - (NSString *)mpwLoginForSiteNamed:(NSString *)name usingKey:(MPKey *)key { return [self mpwResultForSiteNamed:name ofType:MPResultTypeTemplateName parameter:nil withCounter:1 variant:MPKeyPurposeIdentification context:nil usingKey:key]; } - (NSString *)mpwTemplateForSiteNamed:(NSString *)name ofType:(MPResultType)type withCounter:(NSUInteger)counter usingKey:(MPKey *)key { return [self mpwResultForSiteNamed:name ofType:type parameter:nil withCounter:counter variant:MPKeyPurposeAuthentication context:nil usingKey:key]; } - (NSString *)mpwAnswerForSiteNamed:(NSString *)name onQuestion:(NSString *)question usingKey:(MPKey *)key { return [self mpwResultForSiteNamed:name ofType:MPResultTypeTemplatePhrase parameter:nil withCounter:1 variant:MPKeyPurposeRecovery context:question usingKey:key]; } - (NSString *)mpwResultForSiteNamed:(NSString *)name ofType:(MPResultType)type parameter:(NSString *)parameter withCounter:(NSUInteger)counter variant:(MPKeyPurpose)purpose context:(NSString *)context usingKey:(MPKey *)key { __block NSString *result = nil; [self mpw_perform:^{ char const *resultBytes = mpw_siteResult( [key keyForAlgorithm:self], name.UTF8String, (uint32_t)counter, purpose, context.UTF8String, type, parameter.UTF8String, [self version] ); if (resultBytes) { result = [NSString stringWithCString:resultBytes encoding:NSUTF8StringEncoding]; mpw_free_string( resultBytes ); } }]; return result; } - (BOOL)savePassword:(NSString *)plainText toSite:(MPSiteEntity *)site usingKey:(MPKey *)key { if (!(site.type & MPResultTypeClassStateful)) { wrn( @"Can only save content to site with a stateful type: %lu.", (long)site.type ); return NO; } NSAssert( [[key keyIDForAlgorithm:site.user.algorithm] isEqualToData:site.user.keyID], @"Site does not belong to current user." ); if (![site isKindOfClass:[MPStoredSiteEntity class]]) { wrn( @"Site with stored type %lu is not an MPStoredSiteEntity, but a %@.", (long)site.type, [site class] ); return NO; } __block NSData *state = nil; if (plainText) [self mpw_perform:^{ char const *stateBytes = mpw_siteState( [key keyForAlgorithm:self], site.name.UTF8String, MPCounterValueInitial, MPKeyPurposeAuthentication, NULL, site.type, plainText.UTF8String, [self version] ); if (stateBytes) { state = [[NSString stringWithCString:stateBytes encoding:NSUTF8StringEncoding] decodeBase64]; mpw_free_string( stateBytes ); } }]; NSDictionary *siteQuery = [self queryForSite:site]; if (!state) [PearlKeyChain deleteItemForQuery:siteQuery]; else [PearlKeyChain addOrUpdateItemForQuery:siteQuery withAttributes:@{ (__bridge id)kSecValueData : state, #if TARGET_OS_IPHONE (__bridge id)kSecAttrAccessible: site.type & MPSiteFeatureDevicePrivate? (__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly : (__bridge id)kSecAttrAccessibleWhenUnlocked, #endif }]; ((MPStoredSiteEntity *)site).contentObject = nil; return YES; } - (NSString *)resolveLoginForSite:(MPSiteEntity *)site usingKey:(MPKey *)key { return PearlAwait( ^(void (^setResult)(id)) { [self resolveLoginForSite:site usingKey:key result:^(NSString *result_) { setResult( result_ ); }]; } ); } - (NSString *)resolvePasswordForSite:(MPSiteEntity *)site usingKey:(MPKey *)key { return PearlAwait( ^(void (^setResult)(id)) { [self resolvePasswordForSite:site usingKey:key result:^(NSString *result_) { setResult( result_ ); }]; } ); } - (NSString *)resolveAnswerForSite:(MPSiteEntity *)site usingKey:(MPKey *)key { return PearlAwait( ^(void (^setResult)(id)) { [self resolveAnswerForSite:site usingKey:key result:^(NSString *result_) { setResult( result_ ); }]; } ); } - (NSString *)resolveAnswerForQuestion:(MPSiteQuestionEntity *)question usingKey:(MPKey *)key { return PearlAwait( ^(void (^setResult)(id)) { [self resolveAnswerForQuestion:question usingKey:key result:^(NSString *result_) { setResult( result_ ); }]; } ); } - (void)resolveLoginForSite:(MPSiteEntity *)site usingKey:(MPKey *)key result:(void ( ^ )(NSString *result))resultBlock { NSAssert( [[key keyIDForAlgorithm:site.user.algorithm] isEqualToData:site.user.keyID], @"Site does not belong to current user." ); NSString *name = site.name; BOOL loginGenerated = site.loginGenerated && [[MPAppDelegate_Shared get] isFeatureUnlocked:MPProductGenerateLogins]; NSString *loginName = site.loginName; id algorithm = nil; if (!name.length) err( @"Missing name." ); else if (!key) err( @"Missing key." ); else algorithm = site.algorithm; if (!loginGenerated || [loginName length]) resultBlock( loginName ); else PearlNotMainQueue( ^{ resultBlock( [algorithm mpwLoginForSiteNamed:name usingKey:key] ); } ); } - (void)resolvePasswordForSite:(MPSiteEntity *)site usingKey:(MPKey *)key result:(void ( ^ )(NSString *result))resultBlock { NSAssert( [[key keyIDForAlgorithm:site.user.algorithm] isEqualToData:site.user.keyID], @"Site does not belong to current user." ); NSString *name = site.name; MPResultType type = site.type; id algorithm = nil; if (!site.name.length) err( @"Missing name." ); else if (!key) err( @"Missing key." ); else algorithm = site.algorithm; switch (site.type) { case MPResultTypeTemplateMaximum: case MPResultTypeTemplateLong: case MPResultTypeTemplateMedium: case MPResultTypeTemplateBasic: case MPResultTypeTemplateShort: case MPResultTypeTemplatePIN: case MPResultTypeTemplateName: case MPResultTypeTemplatePhrase: { if (![site isKindOfClass:[MPGeneratedSiteEntity class]]) { wrn( @"Site with generated type %lu is not an MPGeneratedSiteEntity, but a %@.", (long)site.type, [site class] ); break; } NSUInteger counter = ((MPGeneratedSiteEntity *)site).counter; PearlNotMainQueue( ^{ resultBlock( [algorithm mpwTemplateForSiteNamed:name ofType:type withCounter:counter usingKey:key] ); } ); break; } case MPResultTypeStatefulPersonal: case MPResultTypeStatefulDevice: { if (![site isKindOfClass:[MPStoredSiteEntity class]]) { wrn( @"Site with stored type %lu is not an MPStoredSiteEntity, but a %@.", (long)site.type, [site class] ); break; } NSDictionary *siteQuery = [self queryForSite:site]; NSData *state = [PearlKeyChain dataOfItemForQuery:siteQuery]; state = state?: ((MPStoredSiteEntity *)site).contentObject; PearlNotMainQueue( ^{ resultBlock( [algorithm mpwResultForSiteNamed:name ofType:type parameter:[state encodeBase64] withCounter:MPCounterValueInitial variant:MPKeyPurposeAuthentication context:nil usingKey:key] ); } ); break; } } } - (void)resolveAnswerForSite:(MPSiteEntity *)site usingKey:(MPKey *)key result:(void ( ^ )(NSString *result))resultBlock { NSAssert( [[key keyIDForAlgorithm:site.user.algorithm] isEqualToData:site.user.keyID], @"Site does not belong to current user." ); NSString *name = site.name; id algorithm = nil; if (!site.name.length) err( @"Missing name." ); else if (!key) err( @"Missing key." ); else algorithm = site.algorithm; PearlNotMainQueue( ^{ resultBlock( [algorithm mpwAnswerForSiteNamed:name onQuestion:nil usingKey:key] ); } ); } - (void)resolveAnswerForQuestion:(MPSiteQuestionEntity *)question usingKey:(MPKey *)key result:(void ( ^ )(NSString *result))resultBlock { NSAssert( [[key 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; if (!name.length) err( @"Missing name." ); else if (!key) err( @"Missing key." ); else if ([[MPAppDelegate_Shared get] isFeatureUnlocked:MPProductGenerateAnswers]) algorithm = question.site.algorithm; PearlNotMainQueue( ^{ resultBlock( [algorithm mpwAnswerForSiteNamed:name onQuestion:keyword usingKey:key] ); } ); } - (void)importPassword:(NSString *)cipherText protectedByKey:(MPKey *)importKey intoSite:(MPSiteEntity *)site usingKey:(MPKey *)key { NSAssert( [[key keyIDForAlgorithm:site.user.algorithm] isEqualToData:site.user.keyID], @"Site does not belong to current user." ); if (cipherText && cipherText.length && site.type & MPResultTypeClassStateful) { NSString *plainText = [self mpwResultForSiteNamed:site.name ofType:site.type parameter:cipherText withCounter:MPCounterValueInitial variant:MPKeyPurposeAuthentication context:nil usingKey:importKey]; if (plainText) [self savePassword:plainText toSite:site usingKey:key]; } } - (NSDictionary *)queryForSite:(MPSiteEntity *)site { return [PearlKeyChain createQueryForClass:kSecClassGenericPassword attributes:@{ (__bridge id)kSecAttrService: site.type & MPSiteFeatureDevicePrivate? @"DevicePrivate": @"Private", (__bridge id)kSecAttrAccount: site.name } matches:nil]; } - (NSString *)exportPasswordForSite:(MPSiteEntity *)site usingKey:(MPKey *)key { if (!(site.type & MPSiteFeatureExportContent)) return nil; NSDictionary *siteQuery = [self queryForSite:site]; NSData *state = [PearlKeyChain dataOfItemForQuery:siteQuery]; return [state?: ((MPStoredSiteEntity *)site).contentObject encodeBase64]; } - (BOOL)timeToCrack:(out TimeToCrack *)timeToCrack passwordOfType:(MPResultType)type byAttacker:(MPAttacker)attacker { if (!(type & MPResultTypeClassTemplate)) return NO; size_t count = 0; const char **templates = mpw_templatesForType( type, &count ); if (!templates) return NO; NSDecimalNumber *permutations = [NSDecimalNumber zero], *templatePermutations; for (size_t t = 0; t < count; ++t) { const char *template = templates[t]; templatePermutations = [NSDecimalNumber one]; for (NSUInteger c = 0; c < strlen( template ); ++c) templatePermutations = [templatePermutations decimalNumberByMultiplyingBy: (id)[[NSDecimalNumber alloc] initWithUnsignedLong:strlen( mpw_charactersInClass( template[c] ) )]]; permutations = [permutations decimalNumberByAdding:templatePermutations]; } free( templates ); return [self timeToCrack:timeToCrack permutations:permutations forAttacker:attacker]; } - (BOOL)timeToCrack:(out TimeToCrack *)timeToCrack passwordString:(NSString *)password byAttacker:(MPAttacker)attacker { NSDecimalNumber *permutations = [NSDecimalNumber one]; for (NSUInteger c = 0; c < [password length]; ++c) { const char passwordCharacter = [password substringWithRange:NSMakeRange( c, 1 )].UTF8String[0]; unsigned long characterEntropy = 0; for (NSString *characterClass in @[ @"v", @"c", @"a", @"x" ]) { char const *charactersForClass = mpw_charactersInClass( characterClass.UTF8String[0] ); if (strchr( charactersForClass, passwordCharacter )) { // Found class for password character. characterEntropy = strlen( charactersForClass ); break; } } if (!characterEntropy) characterEntropy = 256 /* a byte */; permutations = [permutations decimalNumberByMultiplyingBy: (id)[[NSDecimalNumber alloc] initWithUnsignedLong:characterEntropy]]; } return [self timeToCrack:timeToCrack permutations:permutations forAttacker:attacker]; } - (BOOL)timeToCrack:(out TimeToCrack *)timeToCrack permutations:(NSDecimalNumber *)permutations forAttacker:(MPAttacker)attacker { // Determine base seconds needed to calculate the permutations. NSDecimalNumber *secondsToCrack = [permutations decimalNumberByDividingBy: (id)[[NSDecimalNumber alloc] initWithUnsignedLong:CRACKING_PER_SECOND]]; // Modify seconds needed by applying our hardware budget. switch (attacker) { case MPAttacker1: break; case MPAttacker5K: secondsToCrack = [secondsToCrack decimalNumberByMultiplyingBy: (id)[[NSDecimalNumber alloc] initWithUnsignedLong:CRACKING_PRICE]]; secondsToCrack = [secondsToCrack decimalNumberByDividingBy: (id)[[NSDecimalNumber alloc] initWithUnsignedLong:5000]]; break; case MPAttacker20M: secondsToCrack = [secondsToCrack decimalNumberByMultiplyingBy: (id)[[NSDecimalNumber alloc] initWithUnsignedLong:CRACKING_PRICE]]; secondsToCrack = [secondsToCrack decimalNumberByDividingBy: (id)[[NSDecimalNumber alloc] initWithUnsignedLong:20000000]]; break; case MPAttacker5B: secondsToCrack = [secondsToCrack decimalNumberByMultiplyingBy: (id)[[NSDecimalNumber alloc] initWithUnsignedLong:CRACKING_PRICE]]; secondsToCrack = [secondsToCrack decimalNumberByDividingBy: (id)[[NSDecimalNumber alloc] initWithUnsignedLong:5000]]; secondsToCrack = [secondsToCrack decimalNumberByDividingBy: (id)[[NSDecimalNumber alloc] initWithUnsignedLong:1000000]]; break; } NSDecimalNumber *ulong_max = (id)[[NSDecimalNumber alloc] initWithUnsignedLong:ULONG_MAX]; NSDecimalNumber *hoursToCrack = [secondsToCrack decimalNumberByDividingBy: (id)[[NSDecimalNumber alloc] initWithUnsignedLong:3600L]]; if ([hoursToCrack compare:ulong_max] == NSOrderedAscending) timeToCrack->hours = (unsigned long long)[hoursToCrack doubleValue]; else timeToCrack->hours = ULONG_MAX; NSDecimalNumber *daysToCrack = [hoursToCrack decimalNumberByDividingBy: (id)[[NSDecimalNumber alloc] initWithUnsignedLong:24L]]; if ([daysToCrack compare:ulong_max] == NSOrderedAscending) timeToCrack->days = (unsigned long long)[daysToCrack doubleValue]; else timeToCrack->days = ULONG_MAX; NSDecimalNumber *weeksToCrack = [daysToCrack decimalNumberByDividingBy: (id)[[NSDecimalNumber alloc] initWithUnsignedLong:7L]]; if ([weeksToCrack compare:ulong_max] == NSOrderedAscending) timeToCrack->weeks = (unsigned long long)[weeksToCrack doubleValue]; else timeToCrack->weeks = ULONG_MAX; NSDecimalNumber *monthsToCrack = [daysToCrack decimalNumberByDividingBy: (id)[[NSDecimalNumber alloc] initWithUnsignedLong:31L]]; if ([monthsToCrack compare:ulong_max] == NSOrderedAscending) timeToCrack->months = (unsigned long long)[monthsToCrack doubleValue]; else timeToCrack->months = ULONG_MAX; NSDecimalNumber *yearsToCrack = [daysToCrack decimalNumberByDividingBy: (id)[[NSDecimalNumber alloc] initWithUnsignedLong:356L]]; if ([yearsToCrack compare:ulong_max] == NSOrderedAscending) timeToCrack->years = (unsigned long long)[yearsToCrack doubleValue]; else timeToCrack->years = ULONG_MAX; NSDecimalNumber *universesToCrack = [yearsToCrack decimalNumberByDividingBy: (id)[[NSDecimalNumber alloc] initWithUnsignedLong:14000L]]; universesToCrack = [universesToCrack decimalNumberByDividingBy: (id)[[NSDecimalNumber alloc] initWithUnsignedLong:1000000L]]; if ([universesToCrack compare:ulong_max] == NSOrderedAscending) timeToCrack->universes = (unsigned long long)[universesToCrack doubleValue]; else timeToCrack->universes = ULONG_MAX; return YES; } @end