project-imas / encrypted-core-data

v2.0 - iOS Core Data encrypted SQLite store using SQLCipher
Other
785 stars 236 forks source link

Core Data Unique Constraints do not work #289

Open shorbenko opened 7 years ago

shorbenko commented 7 years ago

Uniqueness Constraints https://developer.apple.com/documentation/coredata/nsentitydescription/1425095-uniquenessconstraints?language=objc do not work with Encrypted Core Data SQLite Store. Instead of replacing the record with the same identifier for example I have multiple objects with the same identifier in my tables.

gistya commented 6 years ago

Seems likely this might be because the underlying API does not decrypt the unique IDs before comparing them. If it’s an Apppe private API doing it, swizzling that could get you in trouble on the App Store but not for Enterprise deploys. Hopefully it’s not.

Izulle commented 6 years ago

That can be fixed by adding table-constraint while create table

patrickhartling commented 6 years ago

Would that work to add uniqueness constraints as part of a managed object model migration?

Izulle commented 6 years ago

As far as I can see migration calls createTableForEntity so it should work. But

  1. it might not detect the need of migration if old table was created without contstrains but constraints existed in old model
  2. i yet have no any tests for migration
vruzeda commented 4 years ago

To help anyone wanting to try this out, here's the code from @Izulle in an easier way to copy. This wasn't enough for me, as this does not cause Core Data to merge the conflicting objects.

EncryptedStore.h

typedef NS_ENUM(NSInteger, EncryptedStoreError) {
    EncryptedStoreErrorIncorrectPasscode = 6000,
    EncryptedStoreErrorMigrationFailed,
    EncryptedStoreErrorUniquenessConstraintsMalformed,
};

EncryptedStore.m

- (BOOL)createTableForEntity:(NSEntityDescription *)entity error:(NSError **)error {
    // Skip sub-entities since the super-entities should handle
    // creating columns for all their children.
    if (entity.superentity) {
        return YES;
    }

    // prepare columns
    NSMutableArray *columns = [NSMutableArray arrayWithObject:@"'__objectid' integer primary key"];
    if (entity.subentities.count > 0) {
        // NOTE: Will use '-[NSString hash]' to determine the entity type so we can use
        //       faster integer-indexed queries.  Any string greater than 96-chars is
        //       not guaranteed to produce a unique hash value, but for entity names that
        //       shouldn't be a problem.
        [columns addObject:@"'__entityType' integer"];
    }

    [columns addObjectsFromArray:[self columnNamesForEntity:entity indexedOnly:NO quotedNames:YES]];

    NSArray<NSString *> *constraints = [self constraintsQueryFragmentForEntity:entity error:error];
    if (constraints == nil) {
        return NO;
    }

    // create table
    NSString *string = nil;
    if (constraints.count) {
        string = [NSString stringWithFormat:
                               @"CREATE TABLE %@ (%@, %@);",
                               [self tableNameForEntity:entity],
                               [columns componentsJoinedByString:@", "],
                               [constraints componentsJoinedByString:@", "]];
    } else {
        string = [NSString stringWithFormat:
                               @"CREATE TABLE %@ (%@);",
                               [self tableNameForEntity:entity],
                               [columns componentsJoinedByString:@", "]];
    }

    NSLog(@"[VUZEDA-DEBUG] Create table query: %@", string);

    sqlite3_stmt *statement = [self preparedStatementForQuery:string];
    sqlite3_step(statement);

    BOOL result = (statement != NULL && sqlite3_finalize(statement) == SQLITE_OK);
    if (!result && error) {
        *error = [self databaseError];
        return result;
    }

    return [self createIndicesForEntity:entity error:error];
}

- (void)reportUniquenessConstraintsMalformed:(NSEntityDescription *)entity error:(NSError **)error {
    if (error) {
        *error = [NSError errorWithDomain:EncryptedStoreErrorDomain
                                     code:EncryptedStoreErrorUniquenessConstraintsMalformed
                                 userInfo:@{EncryptedStoreErrorMessageKey: [NSString stringWithFormat:@"Cannot parse uniqueness constraints for entity %@", entity.name]}];
    }
}

- (NSString *)constraintsQueryFragmentForUniquenessConstraint:(NSArray *)contsraintColumns tableName:(NSString *)tableName error:(NSError **)error {
    NSMutableArray<NSString *> *constraintElems = [[NSMutableArray<NSString *> alloc] init];
    for (id element in contsraintColumns) {
        if ([element isKindOfClass:[NSString class]]) {
            [constraintElems addObject:(NSString *)element];
        } else {
            return nil;
        }
    }

    NSString *joined = [constraintElems componentsJoinedByString:@"_"];
    NSString *escaped = [NSString stringWithFormat:@"'%@'", [constraintElems componentsJoinedByString:@"','"]];

    return [NSString stringWithFormat:@"CONSTRAINT %@_%@ UNIQUE (%@)", tableName, joined, escaped];
}

- (NSArray<NSString *> *)constraintsQueryFragmentForEntity:(NSEntityDescription *)entity error:(NSError **)error {
    // Returns nil on error, returning an empty array means that there are no constraints

    NSMutableArray<NSString *> *parts = [[NSMutableArray<NSString *> alloc] init];

    if (entity.superentity) {
        return parts;
    }

    NSArray<NSArray *> *uniquenessConstraints = [entity uniquenessConstraints];
    if (!uniquenessConstraints.count) {
        return parts;
    }

    for (id constraint in uniquenessConstraints) {
        if ([constraint isKindOfClass:[NSArray class]]) {
            NSString *part = [self constraintsQueryFragmentForUniquenessConstraint:(NSArray *)constraint tableName:[self tableNameForEntity:entity] error:error];
            if (part == nil) {
                [self reportUniquenessConstraintsMalformed:entity error:error];
                return nil;
            }
            [parts addObject:part];
        } else {
            [self reportUniquenessConstraintsMalformed:entity error:error];
            return nil;
        }
    }

    return parts;
}