Search code examples
iosobjective-crealm

RLMException after key change - already opened with different encryption key aft


I have an app that after a logout, for compliance reasons, need to erase all Realm data and change the encryption key.

It's not quite the same situation of RLMException "Realm at path ' ' already opened with different encryption key" after writeCopy(toFile:,encryptionKey:)

I'm deleting all files after ensuring all of the Realm objects have been deallocated.

+ (void)deleteRealm:(BOOL)emptyDatabase numRetries:(NSUInteger)numRetries
{
    [[NSNotificationCenter defaultCenter] postNotificationName:@"AppDataStoreWillFlushDatabaseNotification" object:self];

    if (emptyDatabase)
    {
        @autoreleasepool {
            RLMRealm *realm = [RLMRealm defaultRealm];
            [realm transactionWithBlock:^{
                [realm deleteAllObjects];
            }];
            [realm invalidate];
        }
    }

    NSFileManager *manager = [NSFileManager defaultManager];
    RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
    NSArray<NSURL *> *realmFileURLs = @[
        config.fileURL,
        [config.fileURL URLByAppendingPathExtension:@"lock"],
        [config.fileURL URLByAppendingPathExtension:@"log_a"],
        [config.fileURL URLByAppendingPathExtension:@"log_b"],
        [config.fileURL URLByAppendingPathExtension:@"note"],
        [[config.fileURL URLByDeletingLastPathComponent] URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.realm.management", [[config.fileURL URLByDeletingPathExtension] lastPathComponent]]]
    ];
    for (NSURL *URL in realmFileURLs)
    {
        NSError *error = nil;
        [manager removeItemAtURL:URL error:&error];
        if (error)
        {
            // handle error
            DDLogError(@"Error deleting realm file - %@", error);
        }
    }

    // Remove old password
    [CHPasswordManager wipeKeyFromSecurityEnclave];

    [self configureRealm:(numRetries + 1)];
}

And recreating with:

+ (void)configureRealm:(NSUInteger)numRetries
{
    // Setup the encryption key
    NSString *encryptionKey = [CHPasswordManager encryptedStorePassphrase];

    // If encryption key is not valid anymore, generate a new one (CoreData used a 32 chars string while Realm uses a 64 chars string)
    if (encryptionKey.length == 32)
    {
        [CHPasswordManager wipeKeyFromSecurityEnclave];
        encryptionKey = [CHPasswordManager encryptedStorePassphrase];
    }

    NSData *key = [encryptionKey dataUsingEncoding:NSUTF8StringEncoding];
    RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];

    config.encryptionKey = key;

    // Set this as the configuration used for the default Realm
    [RLMRealmConfiguration setDefaultConfiguration:config];

    @autoreleasepool {
        @try {
            [RealmUser allObjects];
        }@catch (NSException *e) {
            if ([e.name isEqual:@"RLMException"])
            {
                // Something went wrong with encryption key - delete database and recreate again
                if (numRetries >= kRetriesThreshold)
                {
                    [Bugsnag notify:e];
                } else {
                    [self deleteRealm:NO numRetries:numRetries];
                }
            }
        }
    }
}

However, I'm often getting a RLMException:

"reason: 'Realm at path '/Users/myname/Library/Developer/CoreSimulator/Devices/152AC3D5-FD24-40DD-AFD7-5A3C4F6EE282/data/Containers/Data/Application/2F0140CF-68E4-4D0E-8AC0-BB869BEE9BF8/Documents/default.realm' already opened with different encryption key'"

Funny thing is that:

'/Users/myname/Library/Developer/CoreSimulator/Devices/152AC3D5-FD24-40DD-AFD7-5A3C4F6EE282/data/Containers/Data/Application/2F0140CF-68E4-4D0E-8AC0-BB869BEE9BF8/Documents/default.realm'

Isn't even on the filesystem.

Any hints to fix the problem?


Solution

  • Like @bdash outlined in his other issue, when a RLMRealm instance is created and wasn't explicitly contained in an @autoreleasepool, Realm internally caches references to that Realm. This is done by design in order to ensure that Realm runs as efficiently as possible.

    While an obvious solution would be to simply enclose literally every Realm operation in your app in an @autoreleasepool, this would be detrimental to performance since you'll be then forcing Realm to open a new reference to disk each and every time you call it.

    I have one potential solution that no one has mentioned directly in here: Don't recycle default.realm.

    The problem here is that you are creating a fresh copy of default.realm while there are lingering references to the old Realm of the same file name still in memory.

    A reasonable compromise would be that instead of recreating default.realm, you instead create a new Realm file with a unique file name each time you do this log-out operation, and then set that as your default Realm. For example, instead of default.realm, you generate a UUID string (e.g 7742e4bc-9f2b-44ae-acf1-7fb424438816.realm), store a reference to its file name somewhere else (NSUserDefaults is probably fine), and use that as the filename in RLMRealmConfiguration.

    That way, whenever you need to delete the old Realm and create a new one, the new one will be considered a wholly new entity in your app, and the remnants of cache will not interfere, and will be gone by the time the app next launches. :)