CredManUtils.cs
using
System;
using
System.Collections.Generic;
using
System.Text;
using
System.Runtime.InteropServices;
using
Microsoft.Win32;
using
System.Security.Permissions;
using
Microsoft.Win32.SafeHandles;
using
System.Web;
using
System.Runtime.CompilerServices;
namespace
CredMan
{
/// <summary>
/// Library exposes 2 friendly API frontends for the crypt'ic ReadCred and WriteCred crypto apis.
/// </summary>
internal class CredManUtils
{
#region
Structs and Enums
private enum CRED_TYPE : int
{
GENERIC = 1,
DOMAIN_PASSWORD = 2,
DOMAIN_CERTIFICATE = 3,
DOMAIN_VISIBLE_PASSWORD = 4,
MAXIMUM = 5
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct Credential
{
public UInt32 Flags;
public CRED_TYPE Type;
public string TargetName;
public string Comment;
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
public UInt32 CredentialBlobSize;
public string CredentialBlob;
public Persistance Persist;
public UInt32 AttributeCount;
public IntPtr Attributes;
public string TargetAlias;
public string UserName;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct NativeCredential
{
public UInt32 Flags;
public CRED_TYPE Type;
public IntPtr TargetName;
public IntPtr Comment;
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
public UInt32 CredentialBlobSize;
public IntPtr CredentialBlob;
public UInt32 Persist;
public UInt32 AttributeCount;
public IntPtr Attributes;
public IntPtr TargetAlias;
public IntPtr UserName;
/// <summary>
/// This method derives a NativeCredential instance from a given Credential instance.
/// </summary>
/// <param name="cred">The managed Credential counterpart containing data to be stored.</param>
/// <returns>A NativeCredential instance that is derived from the given Credential instance.</returns>
internal static NativeCredential GetNativeCredential(Credential cred)
{
NativeCredential ncred = new NativeCredential();
ncred.AttributeCount = 0;
ncred.Attributes = IntPtr.Zero;
ncred.Comment = IntPtr.Zero;
ncred.TargetAlias = IntPtr.Zero;
ncred.Type = CRED_TYPE.GENERIC;
ncred.Persist = (UInt32)Persistance.CRED_PERSIST_SESSION;
ncred.CredentialBlobSize = (UInt32)cred.CredentialBlobSize;
ncred.TargetName = Marshal.StringToCoTaskMemUni(cred.TargetName);
ncred.CredentialBlob = Marshal.StringToCoTaskMemUni(cred.CredentialBlob);
ncred.UserName = Marshal.StringToCoTaskMemUni(System.Environment.UserName);
return ncred;
}
}
private enum Persistance : int
{
CRED_PERSIST_SESSION = 1,
CRED_PERSIST_LOCAL_MACHINE = 2,
CRED_PERSIST_ENTERPRISE = 3
}
#endregion
#region
P/Invoke API signatures
[DllImport("advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool CredRead(string target, CRED_TYPE type, int reservedFlag, out IntPtr CredentialPtr);
[DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool CredWrite([In] ref NativeCredential userCredential, [In] UInt32 flags);
[DllImport("Advapi32.dll", EntryPoint = "CredFree", SetLastError = true)]
static extern bool CredFree([In] IntPtr cred);
#endregion
#region
Critical Handle Type definition
sealed class CriticalCredentialHandle : CriticalHandleZeroOrMinusOneIsInvalid
{
// Set the handle.
internal CriticalCredentialHandle(IntPtr preexistingHandle)
{
SetHandle(preexistingHandle);
}
internal Credential GetCredential()
{
if (!IsInvalid)
{
// Get the Credential from the mem location
NativeCredential ncred = (NativeCredential)Marshal.PtrToStructure(handle, typeof(NativeCredential));
// Create a managed Credential type and fill it with data from the native counterpart.
Credential cred = new Credential();
cred.CredentialBlobSize = ncred.CredentialBlobSize;
cred.CredentialBlob = Marshal.PtrToStringUni(ncred.CredentialBlob, (int)ncred.CredentialBlobSize / 2);
cred.UserName = Marshal.PtrToStringUni(ncred.UserName);
cred.TargetName = Marshal.PtrToStringUni(ncred.TargetName);
cred.TargetAlias = Marshal.PtrToStringUni(ncred.TargetAlias);
cred.Type = ncred.Type;
cred.Flags = ncred.Flags;
cred.Persist = (Persistance)ncred.Persist;
return cred;
}
else
{
throw new InvalidOperationException("Invalid CriticalHandle!");
}
}
// Perform any specific actions to release the handle in the ReleaseHandle method.
// Often, you need to use Pinvoke to make a call into the Win32 API to release the
// handle. In this case, however, we can use the Marshal class to release the unmanaged memory.
override protected bool ReleaseHandle()
{
// If the handle was set, free it. Return success.
if (!IsInvalid)
{
// TODO: We should also ZERO out the memory allocated to the handle, before free'ing it.
// Freedom!!!
CredFree(handle);
// Mark the handle as invalid for future users.
SetHandleAsInvalid();
return true;
}
// Return false.
return false;
}
}
#endregion
#region
The WriteCred library method
/// <summary>
/// Writes a given secret string under a given 'target' name.
/// ALL exceptions are bubbled up to the caller.
/// </summary>
/// <param name="name">Name</param>
/// <param name="secret">The secret message, must be less than 512 bytes.</param>
/// <returns>Zero on success, API error code if the WriteCred API fails.</returns>
public static int WriteCred(string key, string secret)
{
// Validations.
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException("name");
if (null == secret)
throw new ArgumentNullException("secret");
byte[] byteArray = Encoding.Unicode.GetBytes(secret);
if (byteArray.Length > 512)
throw new ArgumentOutOfRangeException("The secret message has exceeded 512 bytes.");
// Go ahead with what we have are stuff it into the CredMan.
Credential cred = new Credential();
cred.TargetName = key;
cred.CredentialBlob = secret;
cred.CredentialBlobSize = (UInt32)Encoding.Unicode.GetBytes(secret).Length;
cred.AttributeCount = 0;
cred.Attributes = IntPtr.Zero;
cred.Comment = null;
cred.TargetAlias = null;
cred.Type = CRED_TYPE.GENERIC;
cred.Persist = Persistance.CRED_PERSIST_SESSION;
NativeCredential ncred = NativeCredential.GetNativeCredential(cred);
bool written = CredWrite(ref ncred, 0);
int lastError = Marshal.GetLastWin32Error();
if (written)
{
return 0;
}
else
{
string message = string.Format("CredWrite failed with the error code {0}.", lastError);
throw new Exception (message);
}
}
#endregion
#region
The ReadCred library method
/// <summary>
/// Calls the ReadCred API that actually reads the secret stored against a given name.
/// </summary>
/// <param name="name">Name containing the secret to retrieve</param>
/// <returns>The secret if it was successfully retrieved.
/// An exception is thrown if an API failure occurs.</returns>
public static string ReadCred(string key)
{
// Validations.
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException("name");
IntPtr nCredPtr;
string readPasswordText = null;
// Since we will *have* to call CredFree upon a successful CredRead operation, we will
// employ the CER (Constrained Execution Region) feature of the .Net 2.0 runtime which
// will ensure that the operations are carried out atomically.
RuntimeHelpers.PrepareConstrainedRegions();
try
{
}
finally
{
// Make the API call using the P/Invoke signature
bool read = CredRead(key, CRED_TYPE.GENERIC, 0, out nCredPtr);
int lastError = Marshal.GetLastWin32Error();
// If the API was successful then...
if (read)
{
using (CriticalCredentialHandle critCred = new CriticalCredentialHandle(nCredPtr))
{
Credential cred = critCred.GetCredential();
readPasswordText = cred.CredentialBlob;
}
}
else
{
string message = string.Format("ReadCred failed with the error code {0}.", lastError);
throw new Exception(message);
}
}
return readPasswordText;
}
#endregion
#region
GeneratePassword library method.
internal static string GetRandomPassword()
{
int length = 15; int numberOfNonAlphanumericCharacters = 5;
string password = System.Web.Security.Membership.GeneratePassword(length, numberOfNonAlphanumericCharacters);
return password;
}
#endregion
#region
ValidatePassword library method.
internal static bool IsPasswordStrong (string password)
{
// So what we mean by 'strong' here is that the password satisfy the following rules -
// a. Have atleast 15 characters
// b. Have atleast 2 digits
// c. Have atleast 2 special chars.
System.Text.RegularExpressions.Regex strong = new System.Text.RegularExpressions.Regex(@"(?=.{15,})(?=(.*\d){2,})(?=(.*\W){2,})");
return strong.IsMatch(password);
}
#endregion
}
}
SecureChat.cs
// Copyright (c) Microsoft Corporation. All Rights Reserved.
using
System;
using
System.Configuration;
using
System.Security;
using
System.Security.Cryptography;
using
System.Security.Cryptography.X509Certificates;
using
System.ServiceModel;
using
System.ServiceModel.Channels;
using
System.Collections;
using
System.Collections.Specialized;
using
System.Text.RegularExpressions;
using
CredMan;
// Multi-party chat application using Peer Channel (a multi-party channel)
// If you are unfamiliar with new concepts used in this sample, refer to the Indigo Basic\GettingStarted sample.
namespace
Microsoft.ServiceModel.Samples
{
// Chat service contract
// Applying [PeerBehavior] attribute on the service contract enables retrieval of PeerNode from IClientChannel.
[ServiceContract(Namespace = "http://Microsoft.ServiceModel.Samples", CallbackContract = typeof(IChat))]
[PeerBehavior]
public interface IChat
{
[OperationContract(IsOneWay = true)]
void Join(string member);
[OperationContract(IsOneWay = true)]
void Chat(string member, string msg);
[OperationContract(IsOneWay = true)]
void Leave(string member);
}
public interface IChatChannel : IChat, IClientChannel
{
}
public class ChatApp : IChat
{
static string password, meshName, memberName;
#region
Command line processing madness
static bool ProcessCommandLineInput(string[] args)
{
NameValueCollection parameters = new NameValueCollection(args.Length);
bool unknownParam = true;
bool appCannotContinue = false;
#region
CLI processing
Regex cmd = new Regex(@"(?<prefix>/)(?<param>\w*)([:,=])*(?<value>\w*)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
// We need to load all of the command line switches up into a collection to check if some
// of the switches are conflicting.
foreach (string arg in args)
{
Match m = cmd.Match(arg);
string param = m.Groups["param"].Value;
string value = m.Groups["value"].Value;
parameters.Add(param, value);
}
for (int i = 0; i < parameters.Count; i++)
{
string param = parameters.Keys[i];
string value = parameters[param];
// We assume we know the param unless otherwise discovered.
unknownParam = false;
switch (param.ToLower())
{
case "generatepassword":
// Check if a password was provided in the command line.
if (null != parameters.Get("password"))
{
Console.WriteLine("Conflicting switches! Cannot autogenerate password when a password is specified. The user specified password will be used.");
}
else
{
password = CredManUtils.GetRandomPassword();
Console.WriteLine("The randomly generated password is '{0}'", password);
}
break;
case "password":
password = value;
bool strongPassword = CredManUtils.IsPasswordStrong(password);
Console.WriteLine("Validating user provided password.... The password is deemed: {0}", (strongPassword ? "Strong" : "Weak"));
if (!strongPassword)
{
Console.WriteLine("Cannot continue with an insecure password. Please re-try with a strong password. Pattern: blah blah blah");
appCannotContinue = true;
}
break;
case "storepassword":
break;
case "readpassword":
break;
case "meshname":
meshName = value;
Console.WriteLine("The meshname we want to join now is {0}", meshName);
break;
default:
unknownParam = true;
#region
if DEBUG
#if
DEBUG
Console.WriteLine("Unknown switch /{0}:{1}", param, value);
#endif
#endregion
break;
}
// No point processing the remainder of switches as we will have to quit.
if (appCannotContinue)
{
return false;
}
if (!unknownParam)
{
parameters.Add(param, value);
}
}
// At this time we have either a user given password, or a system generated password
// and if the storepassword switch is provided, then store the pwd in the registry.
if (null != parameters.Get("storepassword"))
{
Console.WriteLine("Saving password '{0}' information for the current mesh to CredMan...", password);
CredManUtils.WriteCred(meshName, password);
Console.WriteLine("Password saved successfully.");
}
if (null != parameters.Get("readpassword"))
{
Console.WriteLine("Reading password from the CredMan");
string storedPassword = CredManUtils.ReadCred(meshName);
Console.WriteLine("Password '{0}' read successfully.", storedPassword);
password = storedPassword;
}
return true;
#endregion
}
#endregion
// Host the chat instance within this EXE console application.
public static void Main(string[] args)
{
// Get the memberId from configuration
memberName = ConfigurationManager.AppSettings["member"];
// Get the memberId from configuration
meshName = ConfigurationManager.AppSettings["meshName"];
#region
Process the command line params and exit if necessary.
// Process the command line switches. Command inputs overrides any config-loaded app settings.
bool success = ProcessCommandLineInput(args);
if (!success)
{
System.Environment.Exit(-1);
}
#endregion
// Construct InstanceContext to handle messages on callback interface.
// An instance of ChatApp is created and passed to the InstanceContext.
InstanceContext site = new InstanceContext(new ChatApp());
// Create the participant with the given endpoint configuration
// Each participant opens a duplex channel to the mesh
// participant is an instance of the chat application that has opened a channel to the mesh
NetPeerTcpBinding binding = new NetPeerTcpBinding("SecureChatBinding");
//create a PeerSecurityBehavior instance to pass PeerChannel-specific credentials.
//for PeerAuthenticationMode.Password, you need to specify a certificate and a password
PeerSecurityBehavior security = new PeerSecurityBehavior();
security.Password = password;
//if the certificate has a different name than the member, override it here.
X509Certificate2 certificate = GetCertificate(StoreName.My, StoreLocation.CurrentUser, "CN="+memberName, X509FindType.FindBySubjectDistinguishedName);
security.SetSelfCertificate(certificate);
using (ChannelFactory<IChatChannel> cf = new ChannelFactory<IChatChannel>("SecureChatEndpoint"))
{
//add the behavior to the channel factory.
cf.Description.Behaviors.Add(security);
using (IChatChannel participant = cf.CreateDuplexChannel(site))
{
// Retrieve the PeerNode associated with the participant and register for online/offline events
// PeerNode represents a node in the mesh. Mesh is the named collection of connected nodes.
PeerNode node = (participant as IClientChannel).Extensions.Find<PeerNode>();
node.Online += new EventHandler(OnOnline);
node.Offline += new EventHandler(OnOffline);
Console.WriteLine("{0} is ready", memberName);
Console.WriteLine("Type chat messages after going Online");
Console.WriteLine("Press q<ENTER> to terminate this instance.");
// Announce self to other participants
participant.Join(memberName);
// loop until the user quits
while (true)
{
string line = Console.ReadLine();
if (line == "q") break;
participant.Chat(memberName, line);
}
// Leave the mesh and close the proxy
participant.Leave(memberName);
}
}
}
// IChat implementation
public void Join(string member)
{
Console.WriteLine("[{0} joined]", member);
}
public void Chat(string member, string msg)
{
Console.WriteLine("[{0}] {1}", member, msg);
}
public void Leave(string member)
{
Console.WriteLine("[{0} left]", member);
}
// PeerNode event handlers
static void OnOnline(object sender, EventArgs e)
{
Console.WriteLine("** Online");
}
static void OnOffline(object sender, EventArgs e)
{
Console.WriteLine("** Offline");
}
static internal X509Certificate2 GetCertificate(StoreName storeName, StoreLocation storeLocation, string key, X509FindType findType)
{
X509Certificate2 result;
X509Store store = new X509Store(storeName, storeLocation);
store.Open(OpenFlags.ReadOnly);
try
{
X509Certificate2Collection matches;
matches = store.Certificates.Find(findType, key, false);
if (matches.Count > 1)
throw new InvalidOperationException(String.Format("More than one certificate with key '{0}' found in the store.", key));
if (matches.Count == 0)
throw new InvalidOperationException(String.Format("No certificates with key '{0}' found in the store.", key));
result = matches[0];
}
finally
{
store.Close();
}
return result;
}
}
}
App.config
<?
xml version="1.0" encoding="utf-8" ?>
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
<appSettings>
<!-- use appSetting to configure the Member Id -->
<add key="member" value="peer1" />
</appSettings>
<system.serviceModel>
<client>
<!-- chat instance participating in the mesh -->
<endpoint name="SecureChatEndpoint"address="net.p2p://SecureChatMesh/servicemodelsamples/chat"
binding="netPeerTcpBinding"
bindingConfiguration="SecureChatBinding"
contract="Microsoft.ServiceModel.Samples.IChat">
</endpoint>
</client>
<bindings>
<netPeerTcpBinding>
<binding name="SecureChatBinding" port="5290" resolverType="" />
</netPeerTcpBinding>
</bindings>
</system.serviceModel>
</configuration>