{"id":26800548,"url":"https://github.com/ktsu-dev/credentialcache","last_synced_at":"2026-05-23T01:06:42.832Z","repository":{"id":259012122,"uuid":"871429155","full_name":"ktsu-dev/CredentialCache","owner":"ktsu-dev","description":null,"archived":false,"fork":false,"pushed_at":"2025-03-25T06:04:10.000Z","size":277,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-25T07:19:38.947Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PowerShell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ktsu-dev.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":"AUTHORS.md","dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-10-12T00:32:57.000Z","updated_at":"2025-03-25T06:01:58.000Z","dependencies_parsed_at":"2025-01-04T15:19:39.368Z","dependency_job_id":"7b550f84-eedd-4bbf-8880-9ac802db087c","html_url":"https://github.com/ktsu-dev/CredentialCache","commit_stats":null,"previous_names":["ktsu-dev/credentialcache"],"tags_count":83,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ktsu-dev%2FCredentialCache","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ktsu-dev%2FCredentialCache/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ktsu-dev%2FCredentialCache/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ktsu-dev%2FCredentialCache/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ktsu-dev","download_url":"https://codeload.github.com/ktsu-dev/CredentialCache/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246237434,"owners_count":20745348,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-03-29T20:17:48.527Z","updated_at":"2026-05-19T01:03:01.297Z","avatar_url":"https://github.com/ktsu-dev.png","language":"PowerShell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ktsu.CredentialCache\n\n\u003e A cross-platform credential cache for .NET that stores secrets in the host's native keyring.\n\n[![License](https://img.shields.io/github/license/ktsu-dev/CredentialCache)](https://github.com/ktsu-dev/CredentialCache/blob/main/LICENSE.md)\n[![NuGet](https://img.shields.io/nuget/v/ktsu.CredentialCache.svg)](https://www.nuget.org/packages/ktsu.CredentialCache/)\n[![NuGet Downloads](https://img.shields.io/nuget/dt/ktsu.CredentialCache.svg)](https://www.nuget.org/packages/ktsu.CredentialCache/)\n[![Build Status](https://github.com/ktsu-dev/CredentialCache/workflows/build/badge.svg)](https://github.com/ktsu-dev/CredentialCache/actions)\n\n## Overview\n\nCredentialCache 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:\n\n| Platform | Backing store | Native API |\n|----------|--------------|-----|\n| Windows  | Credential Manager | `advapi32` \u0026mdash; `CredReadW` / `CredWriteW` / `CredDeleteW` / `CredEnumerateW` |\n| macOS    | Keychain Services | `Security.framework` \u0026mdash; `SecKeychainAddGenericPassword` and friends |\n| Linux    | freedesktop.org Secret Service | `libsecret-1.so.0` \u0026mdash; `secret_password_store_sync` / `lookup_sync` / `clear_sync` |\n| Other / opt-out | None | `InMemoryCredentialStore` |\n\nEach persona's credential is stored as its own entry in the OS keyring scoped by a `service` name \u0026mdash; the library never writes a plaintext blob to disk.\n\n## Installation\n\n```bash\ndotnet add package ktsu.CredentialCache\n```\n\nRequires .NET 9 or .NET 10.\n\n### Linux runtime prerequisites\n\nThe Linux store requires `libsecret` and a running Secret Service implementation (gnome-keyring, KWallet's secret-service bridge, KeePassXC, \u0026hellip;). On Debian/Ubuntu:\n\n```bash\nsudo apt-get install libsecret-1-0 gnome-keyring\n```\n\nOn headless or CI hosts you'll typically need a session bus and an unlocked keyring \u0026mdash; 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`).\n\n## Quick start\n\n```csharp\nusing ktsu.CredentialCache;\nusing ktsu.CredentialCache.Storage;\nusing ktsu.Semantics.Strings;\n\n// Pick the platform-native store explicitly...\nICredentialStore store = CredentialStoreFactory.CreateDefault(\"MyApp\");\nusing CredentialCache cache = new(store);\n\n// ...or just use the singleton, which calls CreateDefault() on first access.\nCredentialCache singleton = CredentialCache.Instance;\n\nPersonaGUID persona = CredentialCache.CreatePersonaGUID();\n\ncache.AddOrReplace(persona, new CredentialWithUsernamePassword\n{\n    Username = SemanticString\u003cCredentialUsername\u003e.Create(\"alice\"),\n    Password = SemanticString\u003cCredentialPassword\u003e.Create(\"hunter2\"),\n});\n\nif (cache.TryGet(persona, out Credential? stored)\n    \u0026\u0026 stored is CredentialWithUsernamePassword creds)\n{\n    Console.WriteLine($\"Hello, {creds.Username}\");\n}\n\ncache.Remove(persona);\n```\n\n### Pick your own service name\n\n`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.\n\n## Credential types\n\nThe library ships with three concrete `Credential` subclasses:\n\n| Type | Use it for |\n|------|-----------|\n| `CredentialWithNothing` | Sentinel for \"no credential required\" |\n| `CredentialWithToken` | Opaque bearer or API token |\n| `CredentialWithUsernamePassword` | Classic username + password pair |\n\n### Adding your own credential type\n\n`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:\n\n```csharp\n// 1. Add the subclass.\npublic sealed class CredentialWithCertificate : Credential\n{\n    public string Thumbprint { get; init; } = \"\";\n}\n\n// 2. Register it on the base in Credential.cs.\n[JsonDerivedType(typeof(CredentialWithCertificate), nameof(CredentialWithCertificate))]\npublic abstract class Credential { /* ... */ }\n\n// 3. Optional: register a factory so TryCreate\u003cT\u003e works.\npublic sealed class CertificateFactory : ICredentialFactory\u003cCredentialWithCertificate\u003e\n{\n    public CredentialWithCertificate Create() =\u003e new();\n}\ncache.RegisterCredentialFactory(new CertificateFactory());\n```\n\nIf a subclass uses `SemanticString\u003cT\u003e` properties, they round-trip through `ktsu.RoundTripStringJsonConverter` automatically.\n\n## Customising the backing store\n\n`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):\n\n```csharp\npublic sealed class MyCustomStore : ICredentialStore { /* ... */ }\n\nICredentialStore store = new MyCustomStore();\nCredentialCache.ConfigureStore(store); // must be called before first Instance access\n```\n\nFor unit tests, use the in-memory store and skip the singleton entirely:\n\n```csharp\nusing CredentialCache cache = new(new InMemoryCredentialStore());\n```\n\n### Enumerating stored personas\n\n`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:\n\n```csharp\nif (cache.Store is ISearchableCredentialStore searchable)\n{\n    foreach (PersonaGUID key in searchable.EnumerateKeys())\n    {\n        // ...\n    }\n}\nelse\n{\n    // Track persona GUIDs yourself on macOS / Linux.\n}\n```\n\n## Platform notes\n\n- **Windows Credential Manager** caps the credential blob at 2560 bytes (`5 * 512`). Tokens larger than that throw `CredentialStoreException` \u0026mdash; split or compress before storing.\n- **macOS** uses the user's default login keychain. The first access from an application prompts the user for permission, as with any keychain client.\n- **Linux** requires `libsecret-1` plus an active Secret Service. Headless CI agents typically have neither \u0026mdash; use `InMemoryCredentialStore` there, or set up `dbus-run-session` + `gnome-keyring-daemon` as the `cross-platform.yml` workflow does.\n- 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 \u0026mdash; treat `Save` / `Remove` as I/O, not as cheap accessors.\n\n## API summary\n\n### `CredentialCache`\n\n| Member | Description |\n|--------|-------------|\n| `CredentialCache(ICredentialStore store)` | Construct an instance with an explicit store. |\n| `static Instance` | Process-wide singleton (lazy, thread-safe). |\n| `Store` | The backing store passed to the constructor. |\n| `static ConfigureStore(ICredentialStore)` | Override the singleton's store. Must precede first `Instance` access. |\n| `static ResetSingletonForTesting()` | Dispose the singleton and clear configuration. Tests only. |\n| `static CreatePersonaGUID()` | Allocates a new `PersonaGUID`. |\n| `TryGet(persona, out cred)` | Memory-cache lookup with fall-through to the backing store. |\n| `AddOrReplace(persona, cred)` | Persists eagerly through the store. |\n| `Remove(persona)` | Deletes from both the in-memory cache and the store. |\n| `RegisterCredentialFactory\u003cT\u003e(factory)` | Optional factory hook used by `TryCreate\u003cT\u003e`. |\n| `TryCreate\u003cT\u003e(out cred)` | Constructs a credential via a registered factory. |\n| `Dispose()` | Releases in-memory state. The OS store is left untouched. |\n\n### `ICredentialStore`\n\n| Member | Description |\n|--------|-------------|\n| `Name` | Diagnostic identifier (e.g. `\"Windows Credential Manager\"`, `\"macOS Keychain\"`, `\"Linux libsecret (Secret Service)\"`, `\"InMemory\"`). |\n| `TryLoad(persona, out cred)` | Load a single credential. |\n| `Save(persona, cred)` | Persist or overwrite a single credential. |\n| `Remove(persona)` | Delete a single credential. |\n\n### `ISearchableCredentialStore : ICredentialStore`\n\n| Member | Description |\n|--------|-------------|\n| `EnumerateKeys()` | Enumerate every persona key currently persisted (Windows, in-memory). |\n\n## Don't dispose the singleton\n\n`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)`.\n\n## License\n\nMIT \u0026mdash; see [LICENSE.md](LICENSE.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fktsu-dev%2Fcredentialcache","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fktsu-dev%2Fcredentialcache","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fktsu-dev%2Fcredentialcache/lists"}