Skip to content
WindowsDeep Dive Published Updated 6 min readViews unavailable

Windows Credential Manager: Generic Secrets, Target Names, and Persistence

Store app-owned Windows secrets with generic Credential Manager records, explicit persistence scope, exact byte lengths, and reliable cleanup.

Windows Credential Manager gives desktop applications a supported way to save and retrieve credentials through the current user’s credential set. For application-defined secrets, the relevant Win32 path is a generic credential written with CredWriteW and read with CredReadW. The system manages the stored record, but the application still chooses the target name, persistence scope, payload format, and when plaintext enters process memory.

This is not an automatically roaming password service or a general-purpose enterprise vault. Generic credentials have no authentication package that consumes them for the application: your code must read the value and use it for its own protocol. The record’s scope is tied to the logon identity and persistence value, so it is a poor substitute for a managed identity, service vault, or server-side authorization design.

Choose the generic credential type deliberately

CREDENTIALW describes the record written by CredWriteW. Its Type field selects the credential class; CRED_TYPE_GENERIC is for application-defined data. Microsoft describes the generic credential’s target as a service identifier plus its actual target and recommends a publisher or product prefix to reduce naming collisions. Target names are case-insensitive, and a write using an existing target and type replaces that record.

Use a stable, namespaced target such as ExampleCo.SyncClient/api.example.invalid, not a username, email address, or arbitrary string copied from an untrusted request. Treat the target name as a lookup key, not as a secret. The CredentialBlob is a byte sequence, not an implicitly encoded string; CredentialBlobSize is its exact byte count and must not include an assumed NUL terminator. If the application’s own blob format deliberately includes a trailing NUL byte, include that byte in the count. The documented maximum generic credential blob is 5 × 512 bytes, or 2,560 bytes.

For data larger than that limit, define a separate protected-storage design instead of silently truncating or splitting an opaque token. For CRED_TYPE_GENERIC, Windows ignores UserName; do not rely on that member to preserve account metadata. If the application needs an associated account identifier, include it in a documented, versioned payload only when the reader needs it. Never copy the secret into Comment or diagnostic fields.

Persist only as long as the workflow needs

The Persist member is a security and availability decision:

Value Documented scope
CRED_PERSIST_SESSION The current logon session only; not visible to another logon session for the same user and removed after sign-out.
CRED_PERSIST_LOCAL_MACHINE Subsequent logon sessions for the same user on this computer; not other users or the same user on another computer.
CRED_PERSIST_ENTERPRISE Can roam with the user’s account profile; if the account has no roaming state, it can remain local.

Do not read “local machine” as “all users on the machine.” Microsoft documents these persistence values in the credential structure and explicitly bounds the visible identity. Start with session persistence for short-lived tasks. Use longer persistence only when the product needs it and can explain how revocation, account removal, and device transfer work. Enterprise persistence is not a promise that every organization’s profile configuration will roam the record.

Both read and write operate on the credential set associated with the current token’s logon session. A network logon session may have no associated credential set; the API can return ERROR_NO_SUCH_LOGON_SESSION. That is a context limitation, not proof that the username or target is wrong. A background service should use a service-appropriate identity and secret-delivery design rather than borrowing an interactive user’s vault.

Write a bounded test record

The following is a Win32 C++ fragment using a synthetic payload and a test-only target. Replace neither with a real production token when validating the code path:

#include <windows.h>
#include <wincred.h>

bool write_test_credential()
{
    wchar_t target[] = L"ExampleCo.ArticleTest/api.example.invalid";
    BYTE testSecret[] = { 0x74, 0x65, 0x73, 0x74 }; // "test", not a token

    CREDENTIALW credential{};
    credential.Type = CRED_TYPE_GENERIC;
    credential.TargetName = target;
    credential.CredentialBlob = testSecret;
    credential.CredentialBlobSize = static_cast<DWORD>(sizeof(testSecret));
    credential.Persist = CRED_PERSIST_SESSION;

    if (!CredWriteW(&credential, 0)) {
        const DWORD error = GetLastError();
        // Report the numeric status and operation, never the payload.
        (void)error;
        return false;
    }
    return true;
}

The byte array has no string terminator because its recorded length is exactly four. CredWriteW returns a Boolean; when it fails, capture GetLastError() before another API call overwrites the thread’s last-error value. A successful write to the same generic target replaces the prior record, so test with a unique target and remove only that exact test entry.

Read and release the API-owned buffer

CredReadW returns a pointer to one allocated block containing the credential structure and its pointed-to fields. The pointers inside the structure refer into that same allocation. Free the returned block with CredFree, not free, delete, or LocalFree:

PCREDENTIALW credential = nullptr;
if (!CredReadW(
        L"ExampleCo.ArticleTest/api.example.invalid",
        CRED_TYPE_GENERIC,
        0,
        &credential)) {
    const DWORD error = GetLastError();
    (void)error;
    return false; // ERROR_NOT_FOUND is a normal first-run state
}

consume_test_bytes(credential->CredentialBlob,
                   credential->CredentialBlobSize);
CredFree(credential);

Production code should wrap the returned pointer in a small RAII deleter so exceptions and early returns still call CredFree. Use the blob length rather than assuming it is a NUL-terminated string. If the application converts it to text, define the encoding and validate the byte sequence explicitly. The API’s generic blob is application-defined, so another program cannot infer its format.

Reading puts the secret in the caller’s address space. Keep that exposure brief, avoid copies where practical, never log the blob or a formatted credential structure, and do not put secrets in command-line arguments or process environment variables. CredFree releases the API allocation; the documentation does not promise that it securely erases every copy. If your application creates a mutable plaintext copy, clear that owned buffer with an appropriate secure-zeroing primitive after use.

Use CredDeleteW(target, CRED_TYPE_GENERIC, 0) for an explicitly authorized cleanup path. Check its return value and error immediately. Do not build cleanup that enumerates or deletes every record under a broad prefix: records may belong to other app versions or user workflows. During a test, write and delete a unique synthetic entry, then verify that the exact target is absent.

Keep the protection boundary honest

Credential Manager is preferable to saving a password in a plaintext configuration file, but it cannot make plaintext safe after the application reads it. A user or process that can run as the same identity may often obtain the same credential; the API is not protection from malware already executing with that user’s authority. It does not validate a remote service, rotate a token, or guarantee a service can read an interactive user’s record.

For arbitrary local data that your application must protect itself, DPAPI is a separate interface with user- or machine-context semantics. For cloud-hosted services, prefer managed identity or a remote secret vault instead of persisting an operator’s credential in a desktop user’s set. For authentication where passwords can be removed entirely, use a supported passwordless method. Choose the storage mechanism from the actual reader identity, process lifetime, and revocation model.

Test the record lifecycle under the same token type and logon context the shipped application will use. Cover first-run ERROR_NOT_FOUND, create, read with an exact byte count, overwrite behavior, deletion, logoff or process restart as relevant to the chosen persistence value, and failure when no credential set is associated with the token. Use synthetic credentials in a disposable account. The credential vault is a local API boundary, not a substitute for server-side authorization or a tested identity design.

Related:

Sources:

Comments