2
0
MasterPassword/MasterPassword/ObjC/MPAlgorithmV0.m

525 lines
19 KiB
Mathematica
Raw Normal View History

/**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
*
* See the enclosed file LICENSE for license information (LGPLv3). If you did
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
//
// MPAlgorithmV0
//
// Created by Maarten Billemont on 16/07/12.
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
//
#import "MPAlgorithmV0.h"
#import "MPEntities.h"
#define MP_N 32768
#define MP_r 8
#define MP_p 2
#define MP_dkLen 64
#define MP_hash PearlHashSHA256
@implementation MPAlgorithmV0
- (NSUInteger)version {
2012-06-08 21:46:13 +00:00
return 0;
}
2012-06-08 21:46:13 +00:00
- (BOOL)migrateUser:(MPUserEntity *)user inContext:(NSManagedObjectContext *)moc {
2013-04-20 18:11:19 +00:00
NSError *error = nil;
NSFetchRequest *migrationRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPElementEntity class] )];
migrationRequest.predicate = [NSPredicate predicateWithFormat:@"version_ < %d AND user == %@", self.version, user];
NSArray *migrationElements = [moc executeFetchRequest:migrationRequest error:&error];
if (!migrationElements) {
err(@"While looking for elements to migrate: %@", error);
return NO;
}
2013-04-20 18:11:19 +00:00
BOOL requiresExplicitMigration = NO;
for (MPElementEntity *migrationElement in migrationElements)
if (![migrationElement migrateExplicitly:NO])
requiresExplicitMigration = YES;
return requiresExplicitMigration;
}
- (BOOL)migrateElement:(MPElementEntity *)element explicit:(BOOL)explicit {
if (element.version != [self version] - 1)
2013-04-20 18:11:19 +00:00
// Only migrate from previous version.
return NO;
if (!explicit) {
// This migration requires explicit permission.
element.requiresExplicitMigration = YES;
return NO;
}
// Apply migration.
element.requiresExplicitMigration = NO;
2013-04-20 18:11:19 +00:00
element.version = [self version];
return YES;
}
- (MPKey *)keyForPassword:(NSString *)password ofUserNamed:(NSString *)userName {
uint32_t nuserNameLength = htonl(userName.length);
2013-04-20 18:11:19 +00:00
NSDate *start = [NSDate date];
NSData *keyData = [PearlSCrypt deriveKeyWithLength:MP_dkLen fromPassword:[password dataUsingEncoding:NSUTF8StringEncoding]
2013-04-20 18:11:19 +00:00
usingSalt:[NSData dataByConcatenatingDatas:
[@"com.lyndir.masterpassword" dataUsingEncoding:NSUTF8StringEncoding],
[NSData dataWithBytes:&nuserNameLength
length:sizeof(nuserNameLength)],
[userName dataUsingEncoding:NSUTF8StringEncoding],
nil] N:MP_N r:MP_r p:MP_p];
MPKey *key = [self keyFromKeyData:keyData];
trc(@"User: %@, password: %@ derives to key ID: %@ (took %0.2fs)", userName, password, [key.keyID encodeHex], -[start timeIntervalSinceNow]);
2012-06-08 21:46:13 +00:00
return key;
}
2012-06-08 21:46:13 +00:00
- (MPKey *)keyFromKeyData:(NSData *)keyData {
2012-06-08 21:46:13 +00:00
return [[MPKey alloc] initWithKeyData:keyData algorithm:self];
}
2012-06-08 21:46:13 +00:00
- (NSData *)keyIDForKeyData:(NSData *)keyData {
return [keyData hashWith:MP_hash];
}
- (NSString *)nameOfType:(MPElementType)type {
2012-06-08 21:46:13 +00:00
2012-01-16 08:51:08 +00:00
if (!type)
return nil;
2012-06-08 21:46:13 +00:00
switch (type) {
case MPElementTypeGeneratedMaximum:
return @"Maximum Security Password";
2012-06-08 21:46:13 +00:00
case MPElementTypeGeneratedLong:
2012-01-24 23:30:43 +00:00
return @"Long Password";
case MPElementTypeGeneratedMedium:
2012-01-24 23:30:43 +00:00
return @"Medium Password";
2012-06-08 21:46:13 +00:00
case MPElementTypeGeneratedBasic:
2012-01-24 23:30:43 +00:00
return @"Basic Password";
2012-06-08 21:46:13 +00:00
case MPElementTypeGeneratedShort:
return @"Short Password";
case MPElementTypeGeneratedPIN:
return @"PIN";
2012-06-08 21:46:13 +00:00
2012-02-05 21:18:38 +00:00
case MPElementTypeStoredPersonal:
2012-01-24 23:30:43 +00:00
return @"Personal Password";
2012-06-08 21:46:13 +00:00
2012-02-05 21:18:38 +00:00
case MPElementTypeStoredDevicePrivate:
2012-01-24 23:30:43 +00:00
return @"Device Private Password";
}
Throw(@"Type not supported: %lu", (long)type);
}
- (NSString *)shortNameOfType:(MPElementType)type {
if (!type)
return nil;
switch (type) {
case MPElementTypeGeneratedMaximum:
return @"Maximum";
case MPElementTypeGeneratedLong:
return @"Long";
case MPElementTypeGeneratedMedium:
return @"Medium";
case MPElementTypeGeneratedBasic:
return @"Basic";
case MPElementTypeGeneratedShort:
return @"Short";
case MPElementTypeGeneratedPIN:
return @"PIN";
case MPElementTypeStoredPersonal:
return @"Personal";
case MPElementTypeStoredDevicePrivate:
return @"Device";
}
Throw(@"Type not supported: %lu", (long)type);
}
- (NSString *)classNameOfType:(MPElementType)type {
2013-04-20 18:11:19 +00:00
return NSStringFromClass( [self classOfType:type] );
}
- (Class)classOfType:(MPElementType)type {
2012-06-08 21:46:13 +00:00
if (!type)
2013-04-20 18:11:19 +00:00
Throw(@"No type given.");
2012-06-08 21:46:13 +00:00
switch (type) {
case MPElementTypeGeneratedMaximum:
2012-02-05 21:18:38 +00:00
return [MPElementGeneratedEntity class];
2012-06-08 21:46:13 +00:00
case MPElementTypeGeneratedLong:
return [MPElementGeneratedEntity class];
case MPElementTypeGeneratedMedium:
2012-02-05 21:18:38 +00:00
return [MPElementGeneratedEntity class];
2012-06-08 21:46:13 +00:00
case MPElementTypeGeneratedBasic:
2012-02-05 21:18:38 +00:00
return [MPElementGeneratedEntity class];
2012-06-08 21:46:13 +00:00
case MPElementTypeGeneratedShort:
2012-02-05 21:18:38 +00:00
return [MPElementGeneratedEntity class];
2012-06-08 21:46:13 +00:00
case MPElementTypeGeneratedPIN:
2012-02-05 21:18:38 +00:00
return [MPElementGeneratedEntity class];
2012-06-08 21:46:13 +00:00
2012-02-05 21:18:38 +00:00
case MPElementTypeStoredPersonal:
return [MPElementStoredEntity class];
2012-06-08 21:46:13 +00:00
2012-02-05 21:18:38 +00:00
case MPElementTypeStoredDevicePrivate:
return [MPElementStoredEntity class];
}
Throw(@"Type not supported: %lu", (long)type);
}
- (MPElementType)nextType:(MPElementType)type {
if (!type)
Throw(@"No type given.");
switch (type) {
case MPElementTypeGeneratedMaximum:
return MPElementTypeStoredDevicePrivate;
case MPElementTypeGeneratedLong:
return MPElementTypeGeneratedMaximum;
case MPElementTypeGeneratedMedium:
return MPElementTypeGeneratedLong;
case MPElementTypeGeneratedBasic:
return MPElementTypeGeneratedMedium;
case MPElementTypeGeneratedShort:
return MPElementTypeGeneratedBasic;
case MPElementTypeGeneratedPIN:
return MPElementTypeGeneratedShort;
case MPElementTypeStoredPersonal:
return MPElementTypeGeneratedPIN;
case MPElementTypeStoredDevicePrivate:
return MPElementTypeStoredPersonal;
}
Throw(@"Type not supported: %lu", (long)type);
}
- (MPElementType)previousType:(MPElementType)type {
MPElementType previousType = type, nextType = type;
while ((nextType = [self nextType:nextType]) != type)
previousType = nextType;
return previousType;
2012-01-24 23:30:43 +00:00
}
- (NSString *)generateContentNamed:(NSString *)name ofType:(MPElementType)type withCounter:(NSUInteger)counter usingKey:(MPKey *)key {
static NSDictionary *MPTypes_ciphers = nil;
2012-02-05 21:18:38 +00:00
if (MPTypes_ciphers == nil)
MPTypes_ciphers = [NSDictionary dictionaryWithContentsOfURL:
[[NSBundle mainBundle] URLForResource:@"ciphers" withExtension:@"plist"]];
2012-06-08 21:46:13 +00:00
// Determine the seed whose bytes will be used for calculating a password
uint32_t ncounter = htonl(counter), nnameLength = htonl(name.length);
2013-04-20 18:11:19 +00:00
NSData *counterBytes = [NSData dataWithBytes:&ncounter length:sizeof(ncounter)];
NSData *nameLengthBytes = [NSData dataWithBytes:&nnameLength length:sizeof(nnameLength)];
trc(@"seed from: hmac-sha256(%@, 'com.lyndir.masterpassword' | %@ | %@ | %@)", [key.keyData encodeBase64], [nameLengthBytes encodeHex], name, [counterBytes encodeHex]);
NSData *seed = [[NSData dataByConcatenatingDatas:
2013-04-20 18:11:19 +00:00
[@"com.lyndir.masterpassword" dataUsingEncoding:NSUTF8StringEncoding],
nameLengthBytes,
[name dataUsingEncoding:NSUTF8StringEncoding],
counterBytes,
nil]
hmacWith:PearlHashSHA256 key:key.keyData];
trc(@"seed is: %@", [seed encodeBase64]);
2012-05-12 17:24:29 +00:00
const char *seedBytes = seed.bytes;
2012-06-08 21:46:13 +00:00
2012-05-12 17:24:29 +00:00
// Determine the cipher from the first seed byte.
NSAssert([seed length], @"Missing seed.");
2013-04-20 18:11:19 +00:00
NSArray *typeCiphers = [[MPTypes_ciphers valueForKey:[self classNameOfType:type]]
valueForKey:[self nameOfType:type]];
NSString *cipher = typeCiphers[htons(seedBytes[0]) % [typeCiphers count]];
trc(@"type %@, ciphers: %@, selected: %@", [self nameOfType:type], typeCiphers, cipher);
2012-06-08 21:46:13 +00:00
2012-05-12 17:24:29 +00:00
// Encode the content, character by character, using subsequent seed bytes and the cipher.
NSAssert([seed length] >= [cipher length] + 1, @"Insufficient seed bytes to encode cipher.");
NSMutableString *content = [NSMutableString stringWithCapacity:[cipher length]];
for (NSUInteger c = 0; c < [cipher length]; ++c) {
2012-05-12 17:24:29 +00:00
uint16_t keyByte = htons(seedBytes[c + 1]);
2013-04-20 18:11:19 +00:00
NSString *cipherClass = [cipher substringWithRange:NSMakeRange( c, 1 )];
2012-02-05 21:18:38 +00:00
NSString *cipherClassCharacters = [[MPTypes_ciphers valueForKey:@"MPCharacterClasses"] valueForKey:cipherClass];
2013-04-20 18:11:19 +00:00
NSString *character = [cipherClassCharacters substringWithRange:NSMakeRange( keyByte % [cipherClassCharacters length],
1 )];
2012-06-08 21:46:13 +00:00
trc(@"class %@ has characters: %@, index: %u, selected: %@", cipherClass, cipherClassCharacters, keyByte, character);
[content appendString:character];
}
2012-06-08 21:46:13 +00:00
return content;
}
- (NSString *)storedContentForElement:(MPElementStoredEntity *)element usingKey:(MPKey *)key {
return [self decryptContent:element.contentObject usingKey:key];
}
- (void)saveContent:(NSString *)clearContent toElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey {
NSAssert([elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user.");
switch (element.type) {
case MPElementTypeGeneratedMaximum:
case MPElementTypeGeneratedLong:
case MPElementTypeGeneratedMedium:
case MPElementTypeGeneratedBasic:
case MPElementTypeGeneratedShort:
case MPElementTypeGeneratedPIN: {
NSAssert(NO, @"Cannot save content to element with generated type %lu.", (long)element.type);
break;
}
case MPElementTypeStoredPersonal: {
NSAssert([element isKindOfClass:[MPElementStoredEntity class]],
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]);
NSData *encryptedContent = [[clearContent dataUsingEncoding:NSUTF8StringEncoding]
encryptWithSymmetricKey:[elementKey subKeyOfLength:PearlCryptKeySize].keyData padding:YES];
((MPElementStoredEntity *)element).contentObject = encryptedContent;
break;
}
case MPElementTypeStoredDevicePrivate: {
NSAssert([element isKindOfClass:[MPElementStoredEntity class]],
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]);
NSData *encryptedContent = [[clearContent dataUsingEncoding:NSUTF8StringEncoding]
encryptWithSymmetricKey:[elementKey subKeyOfLength:PearlCryptKeySize].keyData padding:YES];
NSDictionary *elementQuery = [self queryForDevicePrivateElementNamed:element.name];
if (!encryptedContent)
[PearlKeyChain deleteItemForQuery:elementQuery];
else
[PearlKeyChain addOrUpdateItemForQuery:elementQuery withAttributes:@{
(__bridge id)kSecValueData : encryptedContent,
#if TARGET_OS_IPHONE
(__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
#endif
}];
((MPElementStoredEntity *)element).contentObject = nil;
break;
}
}
}
- (NSString *)resolveContentForElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey {
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter( group );
__block NSString *result = nil;
[self resolveContentForElement:element usingKey:elementKey result:^(NSString *result_) {
result = result_;
dispatch_group_leave( group );
}];
dispatch_group_wait( group, DISPATCH_TIME_FOREVER );
return result;
}
- (void)resolveContentForElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey result:(void (^)(NSString *result))resultBlock {
NSAssert([elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user.");
switch (element.type) {
case MPElementTypeGeneratedMaximum:
case MPElementTypeGeneratedLong:
case MPElementTypeGeneratedMedium:
case MPElementTypeGeneratedBasic:
case MPElementTypeGeneratedShort:
case MPElementTypeGeneratedPIN: {
NSAssert([element isKindOfClass:[MPElementGeneratedEntity class]],
@"Element with generated type %lu is not an MPElementGeneratedEntity, but a %@.", (long)element.type, [element class]);
NSString *name = element.name;
MPElementType type = element.type;
NSUInteger counter = ((MPElementGeneratedEntity *)element).counter;
id<MPAlgorithm> algorithm = nil;
if (!element.name.length)
err(@"Missing name.");
else if (!elementKey.keyData.length)
err(@"Missing key.");
else
algorithm = element.algorithm;
dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
NSString *result = [algorithm generateContentNamed:name ofType:type withCounter:counter usingKey:elementKey];
resultBlock( result );
} );
break;
}
case MPElementTypeStoredPersonal: {
NSAssert([element isKindOfClass:[MPElementStoredEntity class]],
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]);
NSData *encryptedContent = ((MPElementStoredEntity *)element).contentObject;
dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
NSString *result = [self decryptContent:encryptedContent usingKey:elementKey];
resultBlock( result );
} );
break;
}
case MPElementTypeStoredDevicePrivate: {
NSAssert([element isKindOfClass:[MPElementStoredEntity class]],
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]);
NSDictionary *elementQuery = [self queryForDevicePrivateElementNamed:element.name];
NSData *encryptedContent = [PearlKeyChain dataOfItemForQuery:elementQuery];
dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
NSString *result = [self decryptContent:encryptedContent usingKey:elementKey];
resultBlock( result );
} );
break;
}
}
}
- (void)importProtectedContent:(NSString *)protectedContent protectedByKey:(MPKey *)importKey
intoElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey {
NSAssert([elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user.");
switch (element.type) {
case MPElementTypeGeneratedMaximum:
case MPElementTypeGeneratedLong:
case MPElementTypeGeneratedMedium:
case MPElementTypeGeneratedBasic:
case MPElementTypeGeneratedShort:
case MPElementTypeGeneratedPIN:
break;
case MPElementTypeStoredPersonal: {
NSAssert([element isKindOfClass:[MPElementStoredEntity class]],
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]);
if ([importKey.keyID isEqualToData:elementKey.keyID])
((MPElementStoredEntity *)element).contentObject = [protectedContent decodeBase64];
else {
NSString *clearContent = [self decryptContent:[protectedContent decodeBase64] usingKey:importKey];
[self importClearTextContent:clearContent intoElement:element usingKey:elementKey];
}
break;
}
case MPElementTypeStoredDevicePrivate:
break;
}
}
- (void)importClearTextContent:(NSString *)clearContent intoElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey {
NSAssert([elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user.");
switch (element.type) {
case MPElementTypeGeneratedMaximum:
case MPElementTypeGeneratedLong:
case MPElementTypeGeneratedMedium:
case MPElementTypeGeneratedBasic:
case MPElementTypeGeneratedShort:
case MPElementTypeGeneratedPIN:
break;
case MPElementTypeStoredPersonal: {
[self saveContent:clearContent toElement:element usingKey:elementKey];
break;
}
case MPElementTypeStoredDevicePrivate:
break;
}
}
- (NSString *)exportContentForElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey {
NSAssert([elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user.");
if (!(element.type & MPElementFeatureExportContent))
return nil;
NSString *result = nil;
switch (element.type) {
case MPElementTypeGeneratedMaximum:
case MPElementTypeGeneratedLong:
case MPElementTypeGeneratedMedium:
case MPElementTypeGeneratedBasic:
case MPElementTypeGeneratedShort:
case MPElementTypeGeneratedPIN: {
result = nil;
break;
}
case MPElementTypeStoredPersonal: {
NSAssert([element isKindOfClass:[MPElementStoredEntity class]],
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]);
result = [((MPElementStoredEntity *)element).contentObject encodeBase64];
break;
}
case MPElementTypeStoredDevicePrivate: {
result = nil;
break;
}
}
return result;
}
- (BOOL)migrateExplicitly:(BOOL)explicit {
return NO;
}
- (NSDictionary *)queryForDevicePrivateElementNamed:(NSString *)name {
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
attributes:@{
(__bridge id)kSecAttrService : @"DevicePrivate",
(__bridge id)kSecAttrAccount : name
}
matches:nil];
}
- (NSString *)decryptContent:(NSData *)encryptedContent usingKey:(MPKey *)key {
NSData *decryptedContent = nil;
if ([encryptedContent length])
decryptedContent = [encryptedContent decryptWithSymmetricKey:[key subKeyOfLength:PearlCryptKeySize].keyData padding:YES];
if (!decryptedContent)
return nil;
return [[NSString alloc] initWithBytes:decryptedContent.bytes length:decryptedContent.length encoding:NSUTF8StringEncoding];
}
@end