sharepoint 修改windows 凭据管理器

控制面板\所有控制面板项\凭据管理器

Credential Managementwith the .NET Framework 2.0

Kenny Kerr

January 2006

Applies to:
   Microsoft .NET Framework 2.0
   .NET Framework Security
   Windows XP

Summary: Get an introduction to the Credential Management API that includes functions for user interface handling and lesser-known functions for managing a user's credential set. Also see a .NET class library that dramatically simplifies the task of credential management, for languages such as C# and Visual Basic .NET, and provides a more elegant and robust approach to credential management for C++ developers. (26 printed pages)

Download the associated KerrCredentialsSample.exe code sample.

Download the credential management for C++ developers. (26 printed pages)

Contents

Introduction
Prompting For Credentials
Prompting for Credentials from Managed Code
Tool: PromptForCredentials Builder
A SecureString Primer
The Credential Set
Using the Credential Set from Managed Code
Tool: Credential Set Manager
Conclusion

Introduction

Managing user credentials is hard work. Ideally your Windows domain credentials would be sufficient to grant access to all the resources you might need. Is it ever that simple? Inevitably you need to deal with different security authorities including Windows domains, Microsoft Passport, and application-specific authentication schemes. As if that weren't challenging enough, credentials now come in different forms, including smart cards, certificates, and good old passwords.

Since the release of Windows XP in 2001, Windows has included a Credential Management API for managing user credentials. This API is specifically designed to simplify the task of managing user credentials from within applications, as well as to provide a consistent and secure method for associating credentials with your user profile. It can also be used to prompt for credentials that are not persisted, or credentials that your application may persist in an application-specific way, such as by using the Data Protection API.

In this article I will introduce you to the Credential Management API that includes functions for user-interface handling and lesser-known functions for managing a user's credential set. One challenge presented by this API, depending on your background and the particular project you may want to use it in, is that it is a C-style API suited for applications written with Visual C++ or any other C++ compiler for Windows. With the popularity of the .NET Framework, an ideal solution would include a .NET Framework assembly that exposes the functionality directly to managed code without resorting to error-prone Platform Invoke directly. To address this need I also present a .NET class library in this article that dramatically simplifies the task of credential management for languages like C# and Visual Basic .NET and also provides a more elegant and robust approach to credential management for C++ developers.

One final note before we begin: as I describe the various functions and structures used for credential management I include descriptions and tables of relevant options, flags, and properties. Although many of the options I describe are documented in the Platform SDK (and the MSDN Library), I have not simply copied the descriptions from the documentation. Instead I have tried to provide you with the details that you cannot readily gather from the documentation to convey the knowledge that I have gained based on my experience writing credential management code.

Let's get started!

Prompting For Credentials

If all you needed was your Windows domain credentials, then prompting for credentials would not be necessary since the logon session created by the operating system on your behalf when you logged into your computer could be used to identify you to all the resources you might access. Clearly this is not realistic in practice so prompting a user for credentials becomes inevitable. In the past, different applications created different user interfaces for prompting users for credentials, but with the widespread adoption of Windows XP, applications can rely on the built-in user interface building blocks to provide the user with a familiar and safe mechanism for entering credentials.

Windows provides the CredUIPromptForCredentials function to display a configurable dialog box to accept credential information from the user.

DWORD WINAPI CredUIPromptForCredentialsW(PCREDUI_INFOW info,
                                         PCWSTR targetName,
                                         PCtxtHandle reserved,
                                         DWORD errorCode,
                                         PWSTR userName,
                                         ULONG userNameBufferSize,
                                         PWSTR password,
                                           ULONG passwordBufferSize,
                                         PBOOL saveChecked,
                                         DWORD flags);

Since this article is not about managing credentials in C or native C++, I will not go into too much detail describing this function. It is however useful to have an idea of the functionality it exposes and the way in which it does that.

The first parameter points to a CREDUI_INFO structure allowing you to specify the parent window of the dialog box as well as override the default banner bitmap, caption, and message text.

The targetName parameter is used to identify the credential when storing and retrieving and is also used as part of the caption and message text on the dialog box if this is not overridden in the CREDUI_INFO structure. The full scope of the target name will become clear when we discuss persisting credentials later in this article.

The errorCode parameter allows the dialog box to accommodate certain error codes by modifying the prompt in some way to match the error. Unfortunately the Platform SDK does not provide any hint as to which error codes are supported, so this parameter is of little use.

The userName and userNameBufferSize parameters are used to specify an initial user name to be presented and to retrieve the user name entered by the user. Similarly thepassword and passwordBufferSize parameters specify the initial password and retrieve the password entered by the user. This is the kind of thing that makes programming in C and C++ hard. You need to be very careful when managing these buffers to avoid buffer overruns and other errors related to incorrectly matching the character arrays with their purported sizes. As you will see in the next section, this is one of the biggest areas where managed code can help you write robust code more easily.

The dialog box can display a check box providing the user with the option of saving the credential. The saveChecked parameter is used to set the initial check state of the check box as well as to determine the user's preference when the dialog box is closed. Keep in mind that this parameter is ignored for all but generic credentials.

Finally, the flags parameter allows you to specify any combination of flags to control the behavior of the dialog box and how the resulting credential is managed. The challenge is that many of these flags are mutually exclusive and different combinations of flags can also lead to errors.

Rather than explore this function in more detail directly, I will present you with a few solutions for simplifying the task of prompting for credentials and specifically with the use of managed code. Before we get to that, let's take a look at an example of calling the CredUIPromptForCredentials function from C++ to understand the productivity and safety issues we need to address. This example assumes you're developing an MFC application but the code can just as easily be written in a classic Win32-style application.

CREDUI_INFO info = { sizeof (CREDUI_INFO) };
info.hwndParent = GetSafeHwnd();
 
info.pszCaptionText = L"Title";
info.pszMessageText = L"Message";
 
CBitmap bitmap;
VERIFY(bitmap.LoadBitmap(IDB_PROMPT_BITMAP));
info.hbmBanner = bitmap;
 
CString target;
const DWORD errorCode = 0;
 
wchar_t userName[CREDUI_MAX_USERNAME_LENGTH + 1] = { 0 };
 
wcscpy_s(userName,
         _countof(userName),
         L"initial user name");
 
wchar_t password[CREDUI_MAX_PASSWORD_LENGTH + 1] = { 0 };
 
BOOL saveChecked = false;
 
Const DWORD flags = CREDUI_FLAGS_DO_NOT_PERSIST | 
                    CREDUI_FLAGS_SHOW_SAVE_CHECK_BOX ;
 
DWORD result = ::CredUIPromptForCredentials(&info,
                                            target,
                                            0, // reserved
                                            errorCode,
                                            userName,
                                            _countof(userName)
                                            password,
                                            _countof(password)
                                            &saveChecked,
                                            flags);
 
switch (result)
{
    case NO_ERROR:
    {
        // User chose OK...
        break;
    }
    case ERROR_CANCELLED:
    {
        // User chose Cancel...
        break;
    }
    default:
    {
        // Handle all other errors...
    }
}
 
::SecureZeroMemory(password,
                   sizeof (password));

The code will result in the following dialog box being displayed.

As you can see, there's quite a bit of code to write in order to call this function correctly. First you need to populate a CREDUI_INFO structure to control the various UI aspects of the dialog box. The next step is to prepare the buffers that will receive the user name and password and optionally specify the initial values to display. The CREDUI_MAX_USERNAME_LENGTH constant indicates the maximum length of a user name in characters. Likewise, CREDUI_MAX_PASSWORD_LENGTH indicates the maximum for a password. A value of 1 is added to both the userName and password buffers to accommodate the terminating null character. The call to the wcscpy_s function is optional but illustrates how you can prefill the dialog box with initial values for the user name and password text boxes. And finally, I specify two flags indicating that I do not want the dialog box to persist the credential but I do want the save check box to be displayed. The saveChecked variable will contain the user's preference for saving the credential.

Clearly there is a lot that you can get wrong when calling this function. On the other hand it's not impossible. With a little bit of careful coding you can write very secure code in native C++. Consider for example the use of the new wcscpy_s function, which forces you to consider the length of the destination buffer, and the use of the _countof macro to easily get the length of the statically-allocated array. Also notice that I use the SecureZeroMemory function to fill the password buffer with zeros. The SecureZeroMemory function is preferred over the traditional ZeroMemory function, as it thwarts the Visual C++ compiler's optimizations that can often remove calls to ZeroMemory entirely. If you will be using exceptions to report errors returned by the CredUIPromptForCredentials function, then be sure to wrap the buffer in a class and place the call to SecureZeroMemory in the destructor to ensure that it's called reliably.

Now on to managed code!

Prompting for Credentials from Managed Code

Since the CredUIPromptForCredentials function really represents a dialog box, it would be ideal to be able to use it from managed code just like all the other common dialog box classes and in the same way that you would show a modal form, using a ShowDialog method. The following example illustrates how you can use the PromptForCredentials class, which is available in the download for this article. These examples also illustrate the managed equivalent of the native C++ code in the previous example.

[C++]
ref class MainWindow : Windows::Forms::Form
{
public:

    void Prompt()
    {
        PromptForCredentials dialog;

        dialog.Title = "Title";
        dialog.Message = "Message";
        dialog.Banner = Images::Banner;
        dialog.UserName = "initial user name";

        dialog.DoNotPersist = true;
        dialog.ShowSaveCheckBox = true;

        if (Windows::Forms::DialogResult::OK == dialog.ShowDialog(this))
        {
            // Use credentials wisely...
        }
    }
};
[C#]
class MainWindow : Form
{
    public void Prompt()
    {
        using (PromptForCredentials dialog = new PromptForCredentials())
        {
            dialog.Title = "Title";
            dialog.Message = "Message";
            dialog.Banner = Images.Banner;
            dialog.UserName = "initial user name";

            dialog.DoNotPersist = true;
            dialog.ShowSaveCheckBox = true;

            if (DialogResult.OK == dialog.ShowDialog(this))
            {
                // Use credentials wisely...
            }
        }
    }
}

The PromptForCredentials class owns and manages the lifetime of the various resources used with the CredUIPromptForCredentials function. This includes the banner bitmap and secured password. To ensure that these resources are disposed of promptly, be sure to employ a using statement in C# to call the PromptForCredentials object's Dispose method automatically and in the face of exceptions. In C++, stack semantics take care of this for you automatically. Notice that you no longer need to provide a CREDUI_INFO structure. The class takes care of populating one "under the covers" while providing you with simple properties to populate as desired. There is also no need to bitwise-OR together all the various flags since the class exposes the supported flags through simple bool properties that you can set and clear as necessary.

ShowDialog method is provided to display the dialog box. Instead of writing a switch statement to process the various result codes, the ShowDialog method returns a familiarDialogResult value indicating whether the user chose the OK or Cancel button and errors are reported with managed exceptions.

The following table lists the basic properties of the PromptForCredentials class.

Property Description
[C++]String^ TargetName

[C#]string TargetName

Gets or sets the target name that is used to identify the credential when storing and retrieving it. It is also used as part of the title and message text on the dialog box if these are not overridden.
[C++]int ErrorCode

[C#]int ErrorCode

Gets or sets the error code to allow the dialog box to accommodate certain errors.
[C++]String^ UserName

[C#]string UserName

Gets or sets the user name entered by the user. The dialog box will be prefilled with the initial value.
[C++]SecureString^ Password

[C#]SecureString Password

Gets or sets the password entered by the user. The dialog box will be prefilled with the initial value.
[C++]bool SaveChecked

[C#]bool SaveChecked

Gets or sets a value indicating whether the save check box is checked. This value is ignored unless the ShowSaveCheckBox property is set to true.
[C++]String^ Message

[C#]string Message

Gets or sets the message displayed on the dialog box. If the message is an empty string, the dialog box contains a default message including the target name.
[C++]String^ Title

[C#]string Title

Gets or sets the dialog box title. If the title is an empty string, the dialog box uses a default title including the target name.
[C++]Bitmap^ Banner

[C#]Bitmap Banner

Gets or sets a banner bitmap to display in the dialog box. If a bitmap is not provided, the dialog box displays a default bitmap. The bitmap size is limited to 320 x 60 pixels.

The following table lists the properties of the PromptForCredentials class that represent the flags that control its behavior. One of the challenges with using the CredUIPromptForCredentials function correctly is figuring out which combinations of flags are permissible and give the desired behavior. The table points out the main interdependencies and incompatibilities between the various flags. The description column also indicates which related properties need to be set in order for a particular property to work. For some combinations, failure to adhere to the described usage will result in an error. In other cases it will simply ignore the incorrectly used properties. Remember, the PromptForCredentials class is just a wrapper for the CredUIPromptForCredentials function.

Name Description
AlwaysShowUI The dialog box should be displayed even if a matching credential exists in the user's credential set.

GenericCredentials should be true.

UserNameTargetCredentials should be false.

CompleteUserName The dialog box will automatically add the target name as the authority in the user name if the user doesn't specify an authority. This property is only used with generic credentials as user name completion is always used for Windows credentials.

GenericCredentials should be true.

DoNotPersist The dialog box should not store the credential in the user's credential set. The Save check box is not displayed unless theShowSaveCheckBox property is set to true.

ExpectConfirmation should be false.

Persist should be false.

ExcludeCertificates Certificate or smart card credentials will not be displayed in the User name combo box. Only generic and password credentials will be present.

RequireCertificate should be false.

RequireSmartCard should be false.

ExpectConfirmation The credential manager expects that you will validate the credentials before it stores them. This avoids invalid credentials from being added to the user's credential set.

ShowSaveCheckBox should be false.

GenericCredentials The entered credentials are considered application-specific.

UserNameTargetCredentials should be false.

IncorrectPassword The dialog box displays a balloon tip indicating that a logon attempt was unsuccessful, suggesting that the password may be incorrect.
Persist The dialog box will not display the Save check box but will behave as though it were shown and checked.

DoNotPersist should be false.

ShowSaveCheckBox should be false.

RequestAdministrator The user name combo box is populated with the names of the local administrator accounts. If this property is not set, the dialog box populates the combo box with the user names from the user's credential set.
RequireCertificate The user name combo box is populated with available certificates and the user is not able to enter a user name.

ExcludeCertificates should be false.

RequireSmartCard should be false.

RequireSmartCard The user name combo box is populated with available smart cards and the user is not able to enter a user name.

ExcludeCertificates should be false.

RequireCertificate should be false.

ShowSaveCheckBox The dialog box will display the check box despite the fact that it will not actually persist the credential. This is useful for applications that need to manage credential storage manually.

DoNotPersist should be true.

ExpectConfirmation should be false.

Persist should be false.

UserNameReadOnly The user name field is read-only, allowing only a password to be entered.
ValidateUserName The dialog box will ensure that the entered user name uses a valid format. This property is only used with generic credentials as user name validation is always used for Windows credentials.

GenericCredentials should be true.

There are two properties in particular that deserve further explanation.

The ExpectConfirmation property informs the credential manager that you will confirm the entered credential is in fact valid before it is persisted. Here is a simple example to illustrate this process.

[C++]
ref class MainWindow : Windows::Forms::Form
{
public:

    bool IsValidCredential(String^ userName,
                           Security::SecureString^ password)
    {
        // Validate credentials here...
    }

    void Prompt()
    {
        PromptForCredentials dialog;

        dialog.TargetName = "Target";
        dialog.ExpectConfirmation = true;

        if (Windows::Forms::DialogResult::OK == dialog.ShowDialog(this))
        {
            if (dialog.SaveChecked &&
                IsValidCredential(dialog.UserName,
                                    dialog.Password))
            {
                dialog.ConfirmCredentials();

                // Credentials are now persisted.
            }
        }
    }
};
[C#]
class MainWindow : Form
{
    private bool IsValidCredential(string userName,
                                   SecureString password)
    {
        // Validate credentials here...
    }

    public void Prompt()
    {
        using (PromptForCredentials dialog = new PromptForCredentials())
        {
            dialog.TargetName = "Target";
            dialog.ExpectConfirmation = true;

            if (DialogResult.OK == dialog.ShowDialog(this))
            {
                if (dialog.SaveChecked &&
                    IsValidCredential(dialog.UserName,
                                      dialog.Password))
                {
                    dialog.ConfirmCredentials();

                    // Credentials are now persisted.
                }
            }
        }
    }
}

As you can see from the example, the PromptForCredentials class provides the ConfirmCredentials method to indicate to the credentials manager that the credential is valid and should be persisted. Notice that the code is careful to check that the user actually opted to save the credential. If you fail to check for this condition, the call to ConfirmCredentialsmay fail unexpectedly. The credential manager maintains a list of pending credentials while waiting for applications to confirm them. The PromptForCredentials destructor takes care of notifying the credentials manager that the credential can be discarded if you do not call the ConfirmCredentials method.

The other property that is worth mentioning is IncorrectPassword. In order for this property to have any effect you need to specify a value for the TargetName and UserNameproperties. At this point you should be able to call ShowDialog and have something like this displayed to the user.

Of course there's a catch. Support for this property was introduced after Windows XP shipped. Specifically, it is only supported on Windows Server 2003 and later versions of Windows, such as the upcoming Windows Vista.

There is an inherent risk in the whole notion of prompting users for credentials. The Windows single sign-on concept along with impersonation is meant to avoid continually prompting users for credentials. Besides it being terribly irritating to have to continualy enter credentials, it is also a security risk. If users get used to regularly having to provide credentials, they often stop thinking about it and this leads to a vulnerability where an attacker that manages to get code running on their computer, even under least privilege, can display a prompt to fool the user into disclosing their credentials. Perhaps you think this is overly paranoid. Consider the following dialog box. Where have you seen this before?

Why, it's the sign in dialog box for MSN Messenger, or is it? It has all the same user names in the combo box, leaving no clue as to the dialog box's true origin. Although it looks exactly like the sign-in dialog box for MSN Messenger, it was acutally created with the following code using the PromptForCredentials class. All I had to do was extract the correct bitmap from the MSN Messenger executable to get the correct look and copy the title and message strings. The CredUIPromptForCredentials modifies the dialog box's appearance if you specify "passport.net" as the target name.

[C++]
PromptForCredentials dialog;
dialog.TargetName = "passport.net";
dialog.Title = ".NET Messenger Service";

dialog.Message = "Please sign in with your Microsoft Passport to see your "
                 "online contacts, have online conversations, and receive "
                 "alerts.";

dialog.Banner = gcnew Drawing::Bitmap("C:\\msn.bmp");

if (Windows::Forms::DialogResult::OK == dialog.ShowDialog())
{
    String^ password = SecureStringToString(dialog.Password);
    
    // The password variable now contains the user's cleartext password!
}
[C#]
using (PromptForCredentials dialog = new PromptForCredentials())
{
    dialog.TargetName = "passport.net";
    dialog.Title = ".NET Messenger Service";

    dialog.Message = "Please sign in with your Microsoft Passport to see your " +
                     "online contacts, have online conversations, and receive " +
                     "alerts.";

    dialog.Banner = new Bitmap("C:\\msn.bmp");

    if (DialogResult.OK == dialog.ShowDialog())
    {
        string password = SecureStringToString(dialog.Password);
        
        // The password variable now contains the user's cleartext password!
    }
}

The bottom line is that although Windows provides a lot of support for credential management, you still need to be careful about where and when you employ it in your applications. And of course you need to trust the applications you choose to install on your computer.

Tool: PromptForCredentials Builder

As a developer, I really appreciate the value of visual tools to help me test functionally or validate my assumptions. To help test the PromptForCredentials class and help you in adopting this class I wrote the PromptForCredentials Builder application. The application executable and the full source code are available with the download for this article.

Although not exhaustive, the application does attempt to guide you in choosing flags by enforcing the usage rules for combining flags as described earlier in this article. The Test Prompt button will display the dialog box and closing the dialog box will populate the user name and password fields with the values entered by the user. Once you're happy that the behavior is what you require for your application, click the appropriate button to generate the code for using the PromptForCredentials class with all the properties set according to your specification.

A SecureString Primer

One of the most notable members of the PromptForCredentials class is the Password property that uses the new SecureString class to represent passwords. SecureString is a welcome addition to the .NET Framework, as it provides a secure way of managing secrets in memory without resorting to unmanaged buffers and encryption directly. Since SecureString is not in any way connected to the .NET Framework's standard immutable string class, it is helpful to take a closer look at this class to understand how it can be used, especially since it plays such an important part in writing secure credential management code with the .NET Framework.

The main problem with System.String for the purpose of storing secrets is that, since it is an immutable type, there is no way to zero the memory containing the string characters. Zeroing out passwords in memory is a very important part of keeping passwords secure. One alternative is to use a managed array of characters. Although this does allow the memory to be zeroed out, managed arrays can still be moved around in memory, as the CLR compacts the managed heap leaving any number of copies of the password in memory. Taking this further, you could use an unmanaged array or buffer to store your passwords, but this still leaves you with the job of encrypting the data while it is stored in memory.

The SecureString class makes all this a lot easier. SecureString is mutable, meaning you can change the characters and the length of the string. It also stores the string in unmanaged memory to avoid leaving traces of it behind in the managed heap and encrypts the contents of the unmanaged memory using the Data Protection API.

One of the challenges with adopting the SecureString class is that few other parts of the .NET Framework provide it with any support. For example, the widely usedNetworkCredential class from the System.Net namespace uses System.String to represent passwords. Granted, it stores the password internally in an encrypted buffer, but the class' methods and properties still rely on passwords being communicated as simple strings. The good news is that SecureString is finally part of the .NET Framework and future versions of the Framework will undoubtedly offer far wider adoption of this class for managing passwords and other text-based secrets.

If you're familiar with the System.String type you may be a bit perplexed at first as to how exactly to use the SecureString class. After all, it provides none of the familiar methods for manipulating strings. To begin with, the SecureString class provides two constructors. The default constructor creates an empty string that you can then add characters to one by one. The second constructor creates a copy of a native character array and uses that as the initial value of the string. The second option is useful for C++ projects where you can freely mix native and managed code. This is also the constructor used by the PromptForCredentials class to copy the password returned by the CredUIPromptForCredentials function to a SecureString object. Once constructed, various methods can be used to append, insert, and remove characters from the string.

Let's take a look at a simple example of reading a password from the console to get a feel for some of the functionality provided by the SecureString class.

[C++]
void Backspace()
{
    Console::SetCursorPosition(Console::CursorLeft - 1,
                               Console::CursorTop);
}

void main()
{
    Security::SecureString password;

    for (ConsoleKeyInfo keyInfo = Console::ReadKey(true);
         ConsoleKey::Enter != keyInfo.Key;
         keyInfo = Console::ReadKey(true))
    {
        if (ConsoleKey::Backspace == keyInfo.Key)
        {
            if (0 < password.Length)
            {
                password.RemoveAt(password.Length - 1);
                Backspace();
                Console::Write(" ");
                Backspace();
            }
        }
        else
        {
            password.AppendChar(keyInfo.KeyChar);
            Console::Write("*");
        }
    }
}
[C#]
class Program
{
    static void Backspace()
    {
        Console.SetCursorPosition(Console.CursorLeft - 1,
                                  Console.CursorTop);
    }

    static void Main()
    {
        using (SecureString password = new SecureString())
        {
            for (ConsoleKeyInfo keyInfo = Console.ReadKey(true);
                 ConsoleKey.Enter != keyInfo.Key;
                 keyInfo = Console.ReadKey(true))
            {
                if (ConsoleKey.Backspace == keyInfo.Key)
                {
                    if (0 < password.Length)
                    {
                        password.RemoveAt(password.Length - 1);
                        Backspace();
                        Console.Write(" ");
                        Backspace();
                    }
                }
                else
                {
                    password.AppendChar(keyInfo.KeyChar);
                    Console.Write("*");
                }
            }

            // Use the secure password here...

            Console.WriteLine(SecureStringToString(password));
        }           
    }
}

If you haven't spent any time with the Console class in version 2.0 of the .NET Framework, you should be pleasantly surprised by the power it now affords. Being able to intercept individual key presses and controlling the cursor position are just some the many features the new version of the Console class provides. It also makes it very simple to build a relatively rich prompt for entering passwords at the console. SecureString's AppendChar method is used to append characters as they are read from the console and the RemoveAtmethod removes characters when the user presses the Backspace key.

While debugging, it can often be useful to trace out the contents of a SecureString object. Unfortunately the SecureString class does not provide a single method or property to read any part of the string other than its length. Looking at the SecureString class in a disassembler reveals a few internal methods for copying the contents of the string using various native memory allocators. Fortunately for us, the Marshal class provides a few static methods to allow us to get at the contents of a SecureString. The following examples illustrate how to use one of these methods to copy a SecureString to a BSTR and then convert the BSTR to a cleartext string.

[C++]
String^ SecureStringToString(Security::SecureString^ value)
{
    using Runtime::InteropServices::Marshal;

    IntPtr bstr = Marshal::SecureStringToBSTR(value);

    try
    {
        return Marshal::PtrToStringBSTR(bstr);
    }
    finally
    {
        Marshal::FreeBSTR(bstr);
    }
}
[C#]
static String SecureStringToString(SecureString value)
{
    IntPtr bstr = Marshal.SecureStringToBSTR(value);

    try
    {
        return Marshal.PtrToStringBSTR(bstr);
    }
    finally
    {
        Marshal.FreeBSTR(bstr);
    }
}

Just remember, you should never include code like this in release builds—or any build for that matter—that will be used in a production environment.

The Credential Set

Up till now I have hinted at something I call the credential set, or more specifically the user's credential set. Whenever you allow the CredUIPromptForCredentials function to persist a credential, it stores it in the current user's credential set. As complex and powerful as CredUIPromptForCredentials is, it is in some ways a high-level function that hides the details of interacting directly with the credential set. It is also very useful when debugging this function to examine the credential set to determine what exactly is being persisted.

The credential set is basically a table of credential records that the operating system manages for every user account and logon session. The records in the table are keyed by target name and type. If you're familiar with SQL, then think of the target name and type as the credential set's primary key. The following table lists the main fields in a credential record.

Name Description
Target Name (key) The target name along with the credential type uniquely identifies a credential.
Type (key) Along with uniquely identifying a particular credential, the type indicates whether the credential represents a generic or Windows credential.
User Name The name of the user account represented by the credential. Although this is usually in the form AUTHORITY\principal or the newer principal@authority.com, it can also be a marshaled certificate reference for use with certificate-based authentication.
Password Although this field is typically used to store the plaintext password for the user account identified by the UserName field, it may also contain any application-specific binary data.
Persistence Defines how the credential is persisted and its availability in time and space.
Description A string description that applications can use to describe the purpose of a credential. This is for informational purposes only.
Last Write Time The UTC date and time that the credential was last modified.

Although this list is not exhaustive, it does represent the most common fields stored for a credential in the user's credential set.

So how can I access the credential set? Once again we need to return to native code and a few more functions that were introduced with Windows XP.

To add or modify a credential in the credential set you can use the CredWrite function.

BOOL WINAPI CredWriteW(PCREDENTIALW credential,
                       DWORD flags);

The CREDENTIAL structure is used to communicate credential information. Simply pass the address of a populated CREDENTIAL object to the CredWrite function. The function currently supports just one flag, namely CRED_PRESERVE_CREDENTIAL_BLOB, which indicates that the existing password stored for the credential should be preserved. This is useful when you want to update an existing credential without modifying the stored password and you don't want to load the password just to set it again.

CREDENTIAL credential = { 0 };

credential.TargetName = L"Server";
credential.Type = CRED_TYPE_GENERIC;
credential.Persist = CRED_PERSIST_SESSION;

credential.UserName = L"User name";

wchar_t password[CREDUI_MAX_PASSWORD_LENGTH + 1] = { 0 };

DWORD length = ReadPassword(password,
                            _countof(password));

credential.CredentialBlob = reinterpret_cast<PBYTE>(password);
credential.CredentialBlobSize = length * sizeof (wchar_t);

if (!::CredWrite(&credential,
                 0))
{
    DWORD result = ::GetLastError();

    // Handle or report error appropriately...
}

::SecureZeroMemory(credential.CredentialBlob,
                   credential.CredentialBlobSize);

As I mentioned, the target name and type uniquely identify a credential in the user's credential set. In the example the type is set to CRED_TYPE_GENERIC. This type is used for application-specific credentials, and more specifically is not used by any of the Windows authentication packages such as NTLM or Kerberos. The following table lists the available types and the constraints they place on the stored credential.

Name Description
CRED_TYPE_GENERIC Generic credentials are not used by any of the Windows authentication packages such as the NTLM or Kerberos security service providers (SSP). The text password or secret data can be read by your code.
CRED_TYPE_DOMAIN_PASSWORD The credential stores a SAM (AUTHORITY\principal) or UPN (principal@authority) user name and a Unicode password that can be written by your code but can only be read by an SSP.
CRED_TYPE_DOMAIN_CERTIFICATE The credential stores a marshaled certificate reference as the user name. Instead of a password, the credential stores a Unicode text pin for the certificate. This value can also only be read by an SSP.
CRED_TYPE_DOMAIN_VISIBLE_PASSWORD Like CRED_TYPE_DOMAIN_PASSWORD, a textual user name and password are stored with the credential. Unlike CRED_TYPE_DOMAIN_PASSWORD, however, you code can both read and write the password.

Another option that the CREDENTIAL structure provides is to allow you to control the lifetime and reach of the credential. The example above uses CRED_PERSIST_SESSION to indicate that the credential should persist for the duration of the user's current logon session. Once the user logs off or the final user token is released, the credential will be discarded. If you want to share the credential with other logon sessions for the same user and on the same computer, then you must specify CRED_PERSIST_LOCAL_MACHINE. Other logon sessions will be able to use the persisted credential and it will also survive a reboot of the computer. However, the user's credential set will not include the credential if a logon session is created on a different computer. If you want the credential to be available for a roaming user profile on any computer on the network, then the CRED_PERSIST_ENTERPRISE value is what you need.

The rest of the code in the example above just takes care of all the little details around correctly managing buffers, errors, and securely zeroing out the password when it's all done. As with the CredUIPromptForCredentials function, there is a lot of code you need to write to correctly manage memory and secure secrets.

Along with CredWrite, Windows provides CredRead to get credential information given a target name and type. You can also use CredEnumerate to list the credentials in the user's credential set. The download for this article includes sample code that illustrates how these functions can be used. The remainder of this article looks at the managed classes that use these and other functions internally to provide a simpler and more robust library for managing credentials.

Using the Credential Set from Managed Code

To provide a better developer experience around the credential set management functions, I wrote the Credential and CredentialSet classes that are also available with the download for this article. The Credential class models a single credential, and the CredentialSet class allows you to query and enumerate the credentials in the user's credential set.

The previous section included an example of saving a credential using the CredWrite function. The following example achieves the same result using the Credential class in managed code.

[C++]
Credential credential("Server",
                      CredentialType::Generic);

credential.UserName = "User name";
credential.Password = ReadPassword();
credential.Persistence = CredentialPersistence::Session;

credential.Save();
[C#]
using (Credential credential = new Credential("Server2",
                                              CredentialType.Generic))
{
    credential.UserName = "User name";
    credential.Password = ReadPassword();
    credential.Persistence = CredentialPersistence.Session;

    credential.Save();
} 

Since the Credential object stores a password, be sure to dispose of the object promptly so that the password can be removed from memory. Fortunately the Credential class uses the trusty SecureString class to represent passwords, so handling passwords is simpler and more secure to begin with. The other useful feature of the Credential class is that it uses managed enums to declare the constants used to express the credential type and its persistence scheme. The CredentialType enumeration defines the type of the credential and is defined as follows in my Visual C++ 2005 CLR library project.

public enum class CredentialType
{
    Generic = CRED_TYPE_GENERIC,
    DomainPassword = CRED_TYPE_DOMAIN_PASSWORD,
    DomainCertificate = CRED_TYPE_DOMAIN_CERTIFICATE,
    DomainVisiblePassword = CRED_TYPE_DOMAIN_VISIBLE_PASSWORD
};

As you can see, the enum constants simply map to the appropriate Platform SDK constants, giving you a strongly typed alternative with all the bells and whistles, such as IntelliSense support during development. Similarly the CredentialPersistence enumeration is defined as follows:

public enum class CredentialPersistence
{
    Session = CRED_PERSIST_SESSION,
    LocalComputer = CRED_PERSIST_LOCAL_MACHINE,
    Enterprise = CRED_PERSIST_ENTERPRISE
};

The CredentialSet class can be used to determine the credentials that are currently available in the user's credential set. Although the native CredEnumerate function simply returns an array of CREDENTIAL structures, I use a class to model the credential set to automatically dispose of the Credential objects that are created and owned by the CredentialSet object. This simplifies resource management a great deal. The following example shows how you can easily print some of the properties for every credential to the console.

[C++]
CredentialSet credentials;

for each (Credential^ credential in credentials)
{
    Console::WriteLine(credential->TargetName);
    Console::WriteLine(credential->Type);
    Console::WriteLine(credential->UserName);
    Console::WriteLine(credential->Persistence);
    Console::WriteLine(credential->Description);
    Console::WriteLine(credential->LastWriteTime);
}
[C#]
using (CredentialSet credentials = new CredentialSet())
{
    foreach (Credential credential in credentials)
    {
        Console.WriteLine(credential.TargetName);
        Console.WriteLine(credential.Type);
        Console.WriteLine(credential.UserName);
        Console.WriteLine(credential.Persistence);
        Console.WriteLine(credential.Description);
        Console.WriteLine(credential.LastWriteTime);
    }
}

The CredentialSet class also provides a Count property to get the number of credentials that were loaded, as well as a default indexed property to return a Credential object using a zero-based index. As you can see in the example above, the Credential class provides a host of properties for reading different parts of a credential. A few of the properties deserve some explanation.

The TargetName property is read-only since it represents part of the key used to identify the credential. Windows does, however, provide a special function to rename the target name of an existing credential. The function is CredRename and accepts the existing and new target names, as well as the credential type. The credential type can never be changed. To expose this functionality to managed code, the Credential class provides the ChangeTargetName method.

[C++]
Credential credential("Target name",
                      CredentialType::Generic);

credential.ChangeTargetName("New target name");
[C#]
using (Credential credential = new Credential("Target name",
                                              CredentialType.Generic))
{
    credential.ChangeTargetName("New target name");
}

The ChangeTargetName method expects a credential with the old target name and type to exist. If one is not found, then it will throw a CredentialNotFoundException exception. Use the static Exists method to determine whether a credential exists.

The Credential class also provides a Load method to retrieve the credential information for an existing credential and a Delete method to remove the credential from the user's credential set.

Tool: Credential Set Manager

As nice as the managed Credential and CredentialSet classes are, it helps to be able to visually inspect the contents of the credential set during development or while diagnosing a problem. To that end I wrote the Credential Set Manager class using the .NET Framework, as well as the Credential and CredentialSet classes introduced in the previous section.

The Credential Set Manager lists the contents of the user's credential set, and groups credentials by type. It can be quite revealing to see what applications make use of the credential set. The Credential Set Manager also provides commands for adding your own credentials, as well as modifying or removing existing credentials.

Conclusion

The Credential Management API introduced with Windows XP provides a significant amount of functionality to allow applications to manage credentials more securely. Using the managed classes presented in this article will allow you to quickly add this functionality to your own .NET applications, just be conscious of when and where you prompt the user for credentials, as excessive prompts can lead users to accidentally divulge their passwords to malicious code should such code make its way onto their computers.

Top of pageTop of page

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值