Clarifying Details on MacOS Keychain (+ possible bug?)

Here are some details on working with duplicacy and the MacOS keychain that I’ve now clarified for myself. Parts of the below pieces of info are scattered across the forum, but I have not previously read an overall synopsis of how duplicacy works with keychain.

  1. When you init the repo it creates an entry in keychain, but doesn’t save the password.
  2. When you run your first backup it asks for the password and then saves it in keychain.
  3. As indicated elsewhere, if you do not give the storage a unique name, the storage will be given the name “default” in Duplicacy’s preference file.
  4. Duplicacy saves all items in keychain with the service name (-s flag in MacOS security command) of “duplicacy.” This shows up in both the Name and Where fields of they keychain access app.
  5. If storage name = default, duplicacy will assign the account name (-a flag in security; Account field in keychain access) as “password”.
  6. If a distinct storage name is given, duplicacy will assign the account name as “StorageName_password”.
  7. If you init another storage on the same Mac without a unique storage name, duplicacy will overwrite the first one (with account name “password” and service name “duplicacy”).
  8. If you have a unique storage name with the corresponding unique account name in keychain, “StorageName_password”, duplicacy will successfully retrieve that information for all commands except the info command.
  9. It works this way: if you enter any of those commands with the debug (-d) flag, duplicacy reports that it is reading an environment variable with this distinct name. For example, if you name your storage BOB; save your password with keychain (such that duplicacy saves it with the account name of BOB_password); and then run duplicacy -d check, duplicacy will report Reading the environment variable DUPLICACY_BOB_PASSWORD and it will succeed.
  10. But if you try to run info -e duplicacy will look for the default password; it will report Reading the environment variable DUPLICACY_PASSWORD and then it will fail. That is, the way the init command functions, it assumes a storage name of default, unless told otherwise. In contrast, other commands can get the proper storageName_password on their own.
  11. You fix this by giving the info command the -storage-name flag and the correct storage name. So with a storage named “BOB” you need this: duplicacy -d info -e -storage-name=BOB /path/to/storage Duplicacy then looks for DUPLICACY_BOB_PASSWORD and it works fine.

#10 may not be a bug, but it is an idiosyncrasy that could be better documented. It seems to me that the CLI help on the info command should specify that if your storage name is not default, then you must provide it in the command. Right now the flag is described this way:
-storage-name <name> the storage name to be assigned to the storage url

But that’s what the flag does in the init command. In this listing under info perhaps it should say something like
this flag is required for any storage with a unique name

#7 strikes me as a bug.
Only when trying to figure this out did a realize that I ran into this when I first tried out duplicacy 5 years ago and at that time @gchen helpfully explained to me the interaction between storage names and stored keychain passwords (I, of course, completely forgot this). Still, understanding that this is how it works doesn’t seem to be enough: it feels to me like overwriting a user’s encryption password simply shouldn’t be this easy to do. It wouldn’t be odd at all for a user to init a repo on their Mac (default storage) and assume their encryption password is safely saved in the Mac keychain. Then later they decide to backup some other directory, so they init a completely different repo (again, default storage) – and suddenly their earlier encryption password is just gone (I, at least, don’t know how to recover it).

1 Like

@gchen

There seems to be a fundamental issue in how duplicacy does not distinguish between different storages that are referred by the same name when dealing with a Keychain. Perhaps the key shall include some hash of [some data in] config file. Because users can init many repositories with different names all over the disk pointing to the same storage; storage encryption password is a property of a storage, not repository, and hence shall be tied to the former.

This looks like a separate bug too, if you ask me.

Updating this earlier post to simplify to two things:

  1. My best reading of the code (and I could totally be wrong) is that behavior detailed in pt. 7 above, is found in the code for keyring_darwin.go. That code first checks if the item is in the keyring. If it’s not, the program adds it. But if it does find the item already in the keychain, it calls the update function directly and overwrites it.

  2. To at least avoid overwriting data, the Objective-C portion of that program could be rewritten as follows:

int Set(char *service, char *username, char *password) {
    NSMutableDictionary *keychainItem = [NSMutableDictionary dictionary];
    keychainItem[(__bridge id)kSecClass] = (__bridge id)kSecClassGenericPassword;
    keychainItem[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleWhenUnlocked;
    keychainItem[(__bridge id)kSecAttrAccount] = [NSString stringWithUTF8String:username];
    keychainItem[(__bridge id)kSecAttrService] = [NSString stringWithUTF8String:service];
    
    // Check if the item already exists
    if (SecItemCopyMatching((__bridge CFDictionaryRef)keychainItem, NULL) == noErr) {
        // Return an error code indicating the item already exists
        return (int)errSecDuplicateItem;
    } else {
        // If the item does not exist, add it to the keychain
        keychainItem[(__bridge id)kSecValueData] = [@(password) dataUsingEncoding:NSUTF8StringEncoding];
        OSStatus sts = SecItemAdd((__bridge CFDictionaryRef)keychainItem, NULL);
        return (int)sts;
    }
}

I think the above code would allow you to add any new items, but would fail with an error if you gave it a password that could only be placed into the keychain by overwriting something already there.

Perhaps the better solution would be to modify the code so that when it detects a duplicate it prompts the user for input “update the previous item or cancel?” But I don’t know how you would implement that, because any interaction with the user would happen at the Go layer, but the actual update to the keychain happens at the Objective-C level. So I think there would need to be a new function that either achieves what the original Objective-C code did with “attributesToUpdate,” or hands things back to that same earlier function.