hwchen / keyring-rs

Cross-platform library and utility to manage passwords
Apache License 2.0
485 stars 52 forks source link

Invalid `compile_error` condition #214

Open soywod opened 1 week ago

soywod commented 1 week ago

https://github.com/hwchen/keyring-rs/blob/27a7b3514ee7535374637955f682cf16eeaa25fa/src/lib.rs#L173-L181

I think the condition is not correct, at least for Linux. It prevents Linux users to combine both the secret service based keystore (for persistent storage, on disk) and the keyutils (for cache, in memory). I actually have a combination of both with your v2, and this compile error prevents me to upgrade to the v3:

keyring_native = { version = "3", package = "keyring", default-features = false, features = ["linux-native", "apple-native", "windows-native", "async-secret-service"] }
error[E0252]: the name `default` is defined multiple times
   --> /home/soywod/.cargo/registry/src/index.crates.io-6f17d22bba15001f/keyring-3.3.0/src/lib.rs:208:9
    |
197 | pub use keyutils as default;
    |         ------------------- previous import of the module `default` here
...
208 | pub use secret_service as default;
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^ `default` reimported here
    |
    = note: `default` must be defined only once in the type namespace of this module
help: you can use `as` to change the binding name of the import
    |
208 | pub use secret_service as other_default;
    |         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

error: You can enable at most one keystore per target architecture
   --> /home/soywod/.cargo/registry/src/index.crates.io-6f17d22bba15001f/keyring-3.3.0/src/lib.rs:181:1
    |
181 | compile_error!("You can enable at most one keystore per target architecture");
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
brotskydotcom commented 1 week ago

I completely agree that this is a bug, Clément, and should be fixed. But I have some question about how to fix it.

When I first did v3, I figured I would just have the two secret-service modules be conflicting, and let you combine them with the keyutils store. But that raised the question of which of the two services (secret-service or keyutils) should be the default. I really didn't like the v2 features that let you set the default, so instead I made them all conflict, figuring I would force people to choose which one they wanted. But that eliminates the ability to combine them, as you discovered :).

So... shall I just make the mock keystore be the default if both secret-service and keyutils are included, which forces clients to be explicit about which keystore they want? Shall I arbitrarily pick one or the other? I don't think I'm willing to bring back a feature to choose the default, because it feels way too clumsy.

Unless you (or other clients) can make a good argument for handling it some other way, I think I will just have the mock keystore be the default when multiple keystores on the same platform are included. Please let me know what you think.

soywod commented 1 week ago

Why do you need a default keystore? It makes things way more complicated than it should be. You cannot know in advance how people will use your lib. There are too many edge cases: someone could work an a legacy code refactor and may need the sync AND async secret service keystores at the same time, someone may need both async secret service AND keyutils, tomorrow you may integrate a new keystore for MacOS which would bring the same default question etc.

The only useful case I see for default keystores is about OS-related ones (Linux keyutils, MacOS Keychain and Windows Credential Manager), but even there I doubt. When I first used your lib, I did not know about the Linux keyutils and its memory-only property. I thought it was similar to the MacOS and Windows keystores until I rebooted my computer. I faced it in production, so did my users at that time. In reality, keyutils does not really compete with the MacOS nor Windows keystores. It even looks like keyutils is the only keystore acting on memory?

In my opinion (with my actual understanding of the lib, which may be wrong), forcing users to select keystore(s) they want is the simplest way to go:

let keyutils_entry = EntryBuilder::new().with_credential(keyring::macos).build(service, user);
let secret_service_entry = EntryBuilder::new().with_credential(keyring::secret_service).build(service, user);

Multiple credentials could be given, acting like fallback/cache/proxy/replicate (a nice API could be designed):

let entry = EntryBuilder::new()
    .with_strategy(CredentialStrategy::Fallback) // Switch to the next credential in case of failure
    .with_credential(keyring::keyutils)
    .with_credential(keyring::secret_service)
    .build(service, user);

Better to propose an API that pushes users to compose their keystores rather than restricting them to use only one default that may not match / be enough.

brotskydotcom commented 1 week ago

Thanks for the feedback!

What I have learned from client feedback is that using more than one keystore on a particular platform is very rare (even on Linux, where two are available). With v3, now that clients have to explicitly specify which keystore they want to use, you are the only one I've heard from who has noticed that you can't use more than one together.

In cases where a client is using only one keystore per platform, the notion of a default keystore is incredibly helpful: it gets rid of any need for explicit specification of which keystore one intends to use (already chosen via feature). That's why the original version of keyring only had the default keystore, and had no way of specifying you wanted to use some other one. It was only with the advent of v2 and the ability to bring your own keystore that the notion of a default keystore had to be made explicit, so that someone could specify a different keystore to be used by Entry::new (and the crate itself would know which keystore to use in the absence of that).

While I am intrigued by your EntryBuilder idea, I can't really see the utility of it, and it's not platform independent. (For example the keyutils and secret_service modules don't even exist in a Mac build.) Can you tell me what use case you have that can't be addressed by using set_default_credential_builder to set the desired default and using new_with_credential to build a non-default credential where needed?

Once again, thanks for your thoughts!

soywod commented 6 days ago

While I am intrigued by your EntryBuilder idea, I can't really see the utility of it, and it's not platform independent. (For example the keyutils and secret_service modules don't even exist in a Mac build.)

This is the point, every platform enables its own modules, and Credential implementations would need to be explicitly enabled. They are not platform independent because they provide different features. A Linux keyutils cannot be put at the same level with a MacOS Keychain, because they do not persist data the same way.

Can you tell me what use case you have that can't be addressed by using set_default_credential_builder to set the desired default and using new_with_credential to build a non-default credential where needed?

Managing 2 credentials at the same time, for example the Linux keyutils + secret service.

My main points are:

What about putting the default thing behind a cargo feature, enabled by default? If disabled, there is no more keystore as default. But then the whole build_default_credential would need a refactored. Maybe I could propose a draft PR? It would be simpler to explain what I have in mind.


Side note: do you have any feedback of users using only the Linux keyutils? If so, for which purpose? As I said earlier, my use-case is to have a secret service keystore backed up by a keyutils for cache. Which implies:

soywod commented 6 days ago

After sleeping on it, my thoughts became slightly more precise. Trying to propose a common credential API and a default one matching the current OS is what makes things messy. IMO, what misses is an intermediate crate:

It does not mean to be necessary a dedicated crate: cargo features could be enough. I think a default-keystore feature could work as well. Would you like me to propose a PR, so we can discuss on it? It should not break anything and could even remain in the actual v3.