Reputation: 31
I currently have a program written in c# invoking CredRead/CredWrite from Advapi32.dll to make it easy for users that are not joined to the domain to save terminal server/WebDAV server credentials. (http://msdn.microsoft.com/en-us/library/aa374788%28v=vs.85%29.aspx for credential structure information)
This is all working fine, but I ran into an issue the other day that persists on all XP operating systems; I cannot save the terminal server credentials. The native CredWrite method is returning an error code 87 (ERROR_INVALID_PARAMETER).
After trying numerous different combinations of credential persistence and types, I realized the problem was the TargetName itself. To save terminal servers in the windows key ring, the TargetName is TERMSRV/server.domain.com. On Windows Vista or higher, this works fine with my code, but on Windows XP this does not.
Oddly enough, when you run the Remote Desktop Application on Windows XP and have it save the credentials, it does so without fuss. When I created a method to enumerate saved credentials after using RDP (obviously no password blobs were returned), I could see that the target was identical to the one my program attempted to write.
To confirm the problem was in the TargetName, I removed the "/" and it worked fine.
Below is the relevant portion of my code:
var writeInt = NativeCredMan.WriteCred("TERMSRV/host.server.com", samAccountName, password, CRED_TYPE.DOMAIN_PASSWORD, CRED_PERSIST.LOCAL_MACHINE);
... ...
public enum CRED_TYPE : uint
{
GENERIC = 1,
DOMAIN_PASSWORD = 2,
DOMAIN_CERTIFICATE = 3,
DOMAIN_VISIBLE_PASSWORD = 4,
GENERIC_CERTIFICATE = 5,
DOMAIN_EXTENDED = 6,
MAXIMUM = 7, // Maximum supported cred type
MAXIMUM_EX = (MAXIMUM + 1000), // Allow new applications to run on old OSes
}
public enum CRED_PERSIST : uint
{
SESSION = 1,
LOCAL_MACHINE = 2,
ENTERPRISE = 3,
}
... ...
[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);
[DllImport("Advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode)]
static extern bool CredDelete(string target, CRED_TYPE type, int flags);
//[DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)]
//static extern bool CredEnumerateold(string filter, int flag, out int count, out IntPtr pCredentials);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool CredEnumerate(string filter, uint flag, out uint count, out IntPtr pCredentials);
[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;
internal static NativeCredential GetNativeCredential(Credential cred)
{
var ncred = new NativeCredential
{
AttributeCount = 0,
Attributes = IntPtr.Zero,
Comment = IntPtr.Zero,
TargetAlias = IntPtr.Zero,
Type = CRED_TYPE.DOMAIN_PASSWORD,
Persist = (UInt32) cred.Persist,
CredentialBlobSize = (UInt32) cred.CredentialBlobSize,
TargetName = Marshal.StringToCoTaskMemUni(cred.TargetName),
CredentialBlob = Marshal.StringToCoTaskMemUni(cred.CredentialBlob),
UserName = Marshal.StringToCoTaskMemUni(cred.UserName)
};
return ncred;
}
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public 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 CRED_PERSIST Persist;
public UInt32 AttributeCount;
public IntPtr Attributes;
public string TargetAlias;
public string UserName;
}
... ...
public static int WriteCred(string key, string userName, string secret, CRED_TYPE type, CRED_PERSIST credPersist)
{
var byteArray = Encoding.Unicode.GetBytes(secret);
if (byteArray.Length > 512)
throw new ArgumentOutOfRangeException("The secret message has exceeded 512 bytes.");
var cred = new Credential
{
TargetName = key,
CredentialBlob = secret,
CredentialBlobSize = (UInt32) Encoding.Unicode.GetBytes(secret).Length,
AttributeCount = 0,
Attributes = IntPtr.Zero,
UserName = userName,
Comment = null,
TargetAlias = null,
Type = type,
Persist = credPersist
};
var ncred = NativeCredential.GetNativeCredential(cred);
var written = CredWrite(ref ncred, 0);
var lastError = Marshal.GetLastWin32Error();
if (written)
{
return 0;
}
var message = "";
if (lastError == 1312)
{
message = (string.Format("Failed to save " + key + " with error code {0}.", lastError) + " This error typically occurrs on home editions of Windows XP and Vista. Verify the version of Windows is Pro/Business or higher.");
}
else
{
message = string.Format("Failed to save " + key + " with error code {0}.", lastError);
}
MessageBox.Show(message);
return 1;
}
Can any one point in the right direction as to how I can get these types of credentials to save?
Thanks
Upvotes: 1
Views: 2104
Reputation: 31
Nevermind. I found the problem. After reviewing my question i noticed that the managed credential counter part here:
internal static NativeCredential GetNativeCredential(Credential cred)
{
var ncred = new NativeCredential
{
AttributeCount = 0,
Attributes = IntPtr.Zero,
Comment = IntPtr.Zero,
TargetAlias = IntPtr.Zero,
Type = CRED_TYPE.DOMAIN_PASSWORD,
Persist = (UInt32) cred.Persist,
CredentialBlobSize = (UInt32) cred.CredentialBlobSize,
TargetName = Marshal.StringToCoTaskMemUni(cred.TargetName),
CredentialBlob = Marshal.StringToCoTaskMemUni(cred.CredentialBlob),
UserName = Marshal.StringToCoTaskMemUni(cred.UserName)
};
return ncred;
}
had the TYPE predefined to CRED.TYPE.DOMAIN_PASSWORD instead of just decalring the type.
Changing to:
internal static NativeCredential GetNativeCredential(Credential cred)
{
var ncred = new NativeCredential
{
AttributeCount = 0,
Attributes = IntPtr.Zero,
Comment = IntPtr.Zero,
TargetAlias = IntPtr.Zero,
Type = cred.type,
Persist = (UInt32) cred.Persist,
CredentialBlobSize = (UInt32) cred.CredentialBlobSize,
TargetName = Marshal.StringToCoTaskMemUni(cred.TargetName),
CredentialBlob = Marshal.StringToCoTaskMemUni(cred.CredentialBlob),
UserName = Marshal.StringToCoTaskMemUni(cred.UserName)
};
return ncred;
}
and sending the credential as a GENERIC resolved the problem. Apparently, Windows Vista/7 was allowing the "/" on a DOMAIN_PASSWORD and GENERIC persistence password, but XP only allows this for GENERIC.
Upvotes: 1