You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Updates the docs to match the post-#61 reality:
- Documents ISearchableCredentialStore and shows the runtime cast pattern
consumers need on macOS/Linux when they want enumeration.
- Adds a Linux runtime prerequisites section (libsecret + Secret Service,
with a pointer to the dbus-run-session + gnome-keyring-daemon workflow
used in CI).
- Walks through adding a custom Credential subclass: [JsonDerivedType] on
the base, optional ICredentialFactory<T> registration, and the note
that SemanticString<T> properties round-trip via the RoundTrip converter
automatically.
- Adds a "don't dispose the singleton" warning -- calling Dispose() on
CredentialCache.Instance breaks it process-wide.
- Adds the Store property and ISearchableCredentialStore rows to the API
summary table; native API column in the platform table is now accurate.
https://claude.ai/code/session_017B9mN9F7C3pWGZRyoKBd6R
Copy file name to clipboardExpand all lines: README.md
+93-19Lines changed: 93 additions & 19 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -11,29 +11,42 @@
11
11
12
12
CredentialCache keeps credentials in memory for fast lookup during the lifetime of a process and persists each one through an `ICredentialStore` whose default implementation delegates to the platform-native secret manager:
13
13
14
-
| Platform | Backing store | API |
14
+
| Platform | Backing store |Native API |
15
15
|----------|--------------|-----|
16
-
| Windows |Windows Credential Manager |`advapi32`(`CredRead`/`CredWrite`/`CredDelete`)|
| macOS | Keychain Services |`Security.framework`—`SecKeychainAddGenericPassword` and friends|
18
+
| Linux | freedesktop.org Secret Service |`libsecret-1.so.0`—`secret_password_store_sync` / `lookup_sync` / `clear_sync`|
19
19
| Other / opt-out | None |`InMemoryCredentialStore`|
20
20
21
-
Each persona's credential is stored as its own entry in the OS keyring — the library never writes a plaintext blob to disk.
21
+
Each persona's credential is stored as its own entry in the OS keyring scoped by a `service` name — the library never writes a plaintext blob to disk.
22
22
23
23
## Installation
24
24
25
25
```bash
26
26
dotnet add package ktsu.CredentialCache
27
27
```
28
28
29
+
Requires .NET 9 or .NET 10.
30
+
31
+
### Linux runtime prerequisites
32
+
33
+
The Linux store requires `libsecret` and a running Secret Service implementation (gnome-keyring, KWallet's secret-service bridge, KeePassXC, …). On Debian/Ubuntu:
34
+
35
+
```bash
36
+
sudo apt-get install libsecret-1-0 gnome-keyring
37
+
```
38
+
39
+
On headless or CI hosts you'll typically need a session bus and an unlocked keyring — see the `cross-platform.yml` workflow in this repo for the `dbus-run-session` + `gnome-keyring-daemon` incantation. If a Secret Service isn't available in your deployment, fall back to `InMemoryCredentialStore` (or roll your own `ICredentialStore`).
@@ -56,19 +69,52 @@ if (cache.TryGet(persona, out Credential? stored)
56
69
cache.Remove(persona);
57
70
```
58
71
72
+
### Pick your own service name
73
+
74
+
`CredentialStoreFactory.CreateDefault(serviceName)` scopes entries by a logical service name (defaults to `"ktsu.CredentialCache"`). If two applications share a host, pass per-app names so their keyring entries don't collide.
75
+
59
76
## Credential types
60
77
61
-
-`CredentialWithNothing`— sentinel for "no credential required".
62
-
-`CredentialWithToken`— opaque bearer / API token.
New credential types must derive from `Credential` and be registered with a `[JsonDerivedType]` attribute on `Credential` so polymorphic serialization round-trips through the keyring entry.
86
+
### Adding your own credential type
87
+
88
+
`Credential` is a polymorphic record class round-tripped through `System.Text.Json`. New subclasses need a `[JsonDerivedType]` on the base so deserialization can resolve them:
If a subclass uses `SemanticString<T>` properties, they round-trip through `ktsu.RoundTripStringJsonConverter` automatically.
66
110
67
111
## Customising the backing store
68
112
69
-
`ICredentialStore` is a small CRUD interface (`TryLoad`/`Save`/`Remove`/`EnumerateKeys`). Bring your own implementation when you need a different backend (HashiCorp Vault, an encrypted file, a test double):
113
+
`ICredentialStore` is a small CRUD interface (`TryLoad` / `Save` / `Remove`). Bring your own implementation when you need a different backend (HashiCorp Vault, an encrypted file, a test double):
`ICredentialStore` deliberately has no `EnumerateKeys` method, because macOS Keychain and libsecret require substantially more native marshalling for enumeration than the simple key-value ops. The optional `ISearchableCredentialStore` interface adds it, and only the Windows and in-memory stores implement it:
131
+
132
+
```csharp
133
+
if (cache.StoreisISearchableCredentialStoresearchable)
-**Windows Credential Manager** caps the credential blob at 2560 bytes. Tokens larger than that will throw `CredentialStoreException`— split or compress before storing.
85
-
-**Linux** requires `libsecret-1` (e.g. `apt install libsecret-1-0`) plus a running Secret Service implementation (gnome-keyring, KWallet's secret-service bridge, KeePassXC, …). Headless CI agents typically have neither — use `InMemoryCredentialStore` there.
148
+
-**Windows Credential Manager** caps the credential blob at 2560 bytes (`5 * 512`). Tokens larger than that throw `CredentialStoreException`— split or compress before storing.
86
149
-**macOS** uses the user's default login keychain. The first access from an application prompts the user for permission, as with any keychain client.
87
-
-`EnumerateKeys()` is fully implemented on Windows. On macOS and Linux it currently returns an empty sequence (implementing it would require substantially more native marshalling for a use-case most consumers can satisfy by tracking persona GUIDs themselves).
150
+
-**Linux** requires `libsecret-1` plus an active Secret Service. Headless CI agents typically have neither — use `InMemoryCredentialStore` there, or set up `dbus-run-session` + `gnome-keyring-daemon` as the `cross-platform.yml` workflow does.
151
+
- All native calls happen on the thread the API is invoked from. The library's in-memory cache is thread-safe (`ConcurrentDictionary`); the native APIs themselves are documented as thread-safe by their respective platform owners, but blocking calls (especially libsecret) are not cheap — treat `Save` / `Remove` as I/O, not as cheap accessors.
88
152
89
153
## API summary
90
154
@@ -94,10 +158,11 @@ using CredentialCache cache = new(new InMemoryCredentialStore());
94
158
|--------|-------------|
95
159
|`CredentialCache(ICredentialStore store)`| Construct an instance with an explicit store. |
|`EnumerateKeys()`| Enumerate every persona key currently persisted (Windows, in-memory). |
186
+
187
+
## Don't dispose the singleton
188
+
189
+
`CredentialCache.Instance` returns a process-wide singleton. Calling `Dispose()` on it (e.g. via `using var c = CredentialCache.Instance;`) puts the singleton in a disposed state and the next consumer in the process gets `ObjectDisposedException`. If you need disposal semantics, construct your own instance with `new CredentialCache(store)`.
0 commit comments