744 lines
26 KiB
Objective-C
744 lines
26 KiB
Objective-C
//==============================================================================
|
|
// 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 <http://www.gnu.org/licenses/>.
|
|
//==============================================================================
|
|
|
|
#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 MPAlgorithmVersionV0;
|
|
}
|
|
|
|
- (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<MPAlgorithm>)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_master_key( 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:keyData], -[start timeIntervalSinceNow] );
|
|
mpw_free( &masterKey, MPMasterKeySize );
|
|
}
|
|
}];
|
|
|
|
return keyData;
|
|
}
|
|
|
|
- (NSData *)keyIDForKey:(NSData *)masterKey {
|
|
|
|
return [masterKey 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";
|
|
|
|
case MPResultTypeDeriveKey:
|
|
return @"Crypto Key";
|
|
}
|
|
|
|
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";
|
|
|
|
case MPResultTypeDeriveKey:
|
|
return @"Key";
|
|
}
|
|
|
|
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];
|
|
|
|
case MPResultTypeDeriveKey:
|
|
break;
|
|
}
|
|
|
|
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;
|
|
case MPResultTypeDeriveKey:
|
|
break;
|
|
}
|
|
|
|
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:MPCounterValueInitial
|
|
variant:MPKeyPurposeIdentification context:nil usingKey:key];
|
|
}
|
|
|
|
- (NSString *)mpwTemplateForSiteNamed:(NSString *)name ofType:(MPResultType)type
|
|
withCounter:(MPCounterValue)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:MPCounterValueInitial
|
|
variant:MPKeyPurposeRecovery context:question usingKey:key];
|
|
}
|
|
|
|
- (NSString *)mpwResultForSiteNamed:(NSString *)name ofType:(MPResultType)type parameter:(NSString *)parameter
|
|
withCounter:(MPCounterValue)counter variant:(MPKeyPurpose)purpose context:(NSString *)context
|
|
usingKey:(MPKey *)key {
|
|
|
|
__block NSString *result = nil;
|
|
[self mpw_perform:^{
|
|
NSData *masterKey = [key keyForAlgorithm:self];
|
|
char const *resultBytes = mpw_site_result( masterKey.bytes, name.UTF8String,
|
|
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:^{
|
|
NSData *masterKey = [key keyForAlgorithm:self];
|
|
char const *stateBytes = mpw_site_state( masterKey.bytes, 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<MPAlgorithm> 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<MPAlgorithm> 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] );
|
|
return;
|
|
}
|
|
|
|
MPCounterValue counter = ((MPGeneratedSiteEntity *)site).counter;
|
|
|
|
PearlNotMainQueue( ^{
|
|
resultBlock( [algorithm mpwTemplateForSiteNamed:name ofType:type withCounter:counter usingKey:key] );
|
|
} );
|
|
return;
|
|
}
|
|
|
|
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] );
|
|
return;
|
|
}
|
|
|
|
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] );
|
|
} );
|
|
return;
|
|
}
|
|
|
|
case MPResultTypeDeriveKey:
|
|
return;
|
|
}
|
|
|
|
Throw( @"Type not supported: %lu", (long)type );
|
|
}
|
|
|
|
- (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<MPAlgorithm> 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<MPAlgorithm> 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_type_templates( 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_class_characters( 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_class_characters( 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
|