読者です 読者をやめる 読者になる 読者になる

【C#】Windows資格情報を操作する

以前C#でWindows資格情報を列挙すると言う記事で試しに作成したソリューションをGitHubに公開したんですが、相当手を加えて列挙だけでなく操作も全部できるようにしたのでした。

したんですが、それを紹介する記事を書いてなかったので書きます。とは言え、作ってから大分時間が経ってしまったのでかなり適当な説明になりそうですが…。

Windows資格情報を操作するAPI

まぁ以前も書いたんですが、Win32APIをC#でガリガリとラップしていくしかないです。

前回はCredEnumerateしか紹介しませんでしたが、一応事前にすべて紹介しましょう。どんなものがあるかはこのサイトを参考にさせていただきました。

今ソース見てたらCredFreeだけ実装してないですね…。ま、いっか。(よくない)

CREDENTIAL structure

基本的に資格情報はCREDENTIALと言う構造体にデータを詰め込んだり詰め込んでもらったりして操作します。

C#で書くと、こんな感じです。

/// <summary>
/// アンマネージドなCredential
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct UnmanagedCredential
{
    public uint Flags;
    public uint Type;
    public string TargetName;
    public string Comment;
    public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
    public uint CredentialBlobSize;
    public IntPtr CredentialBlob;
    public uint Persist;
    public uint AttributeCount;
    public IntPtr Attributes;
    public string TargetAlias;
    public string UserName;
}

こいつをこのまま使うのは流石に面倒なので、以下のマネージドなクラスに変換できるようにします。

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_EX = (MAXIMUM + 1000),
}

public enum CRED_PERSIST : uint
{
    SESSION = 1,
    LOCAL_MACHINE = 2,
    ENTERPRISE = 3,
}

public enum CRED_FLAGS : uint
{
    PROMPT_NOW = 0x2,
    USERNAME_TARGET = 0x4
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct CREDENTIAL_ATTRIBUTE
{
    string Keyword;
    uint Flags;
    uint ValueSize;
    IntPtr Value;
}

/// <summary>
/// マネージドコードに置き換えたCredential
/// </summary>
public class ManagedCredential
{
    public CRED_FLAGS Flags { get; set; }
    public CRED_TYPE Type { get; set; }
    public string TargetName { get; set; }
    public string Comment { get; set; }
    public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten { get; set; }
    public byte[] CredentialBlob { get; set; }
    public CRED_PERSIST Persist { get; set; }
    public CREDENTIAL_ATTRIBUTE[] Attributes { get; set; }
    public string TargetAlias { get; set; }
    public string UserName { get; set; }

    public string GetPassword()
    {
        if (CredentialBlob.Length > 0)
        {
            return Encoding.UTF8.GetString(CredentialBlob);
        }
        else
        {
            return string.Empty;
        }
    }

    public void SetPassword(string password)
    {
        if (!string.IsNullOrEmpty(password))
        {
            CredentialBlob = Encoding.UTF8.GetBytes(password);
        }
    }
}

CREDENTIAL_ATTRIBUTE[] Attributesだけアンマネージドなんですが、特に使うことはないからですかね…。(うろ覚え)

また、アンマネージド⇔マネージドの相互変換ができると後で便利なので、そんなメソッドを作っておきます。

/// <summary>
/// アンマネージドな資格情報(ポインタ)をマネージドなEnumに変換する
/// </summary>
/// <param name="ptr"></param>
/// <returns></returns>
private static ManagedCredential ConvertToManagedCredential(IntPtr ptr)
{
    var unmanagedCred = (UnmanagedCredential)Marshal.PtrToStructure(ptr, typeof(UnmanagedCredential));


    // 特殊な操作が必要ないものを詰め込んだManagedCredentialを作成する
    // CredentialBlobは別途変換する必要があるため、一旦サイズだけを持った配列を作成する
    var managedCred = new ManagedCredential
    {
        Flags = (CRED_FLAGS)unmanagedCred.Flags,
        Type = (CRED_TYPE)unmanagedCred.Type,
        TargetName = unmanagedCred.TargetName,
        Comment = unmanagedCred.Comment,
        LastWritten = unmanagedCred.LastWritten,
        CredentialBlob = new byte[unmanagedCred.CredentialBlobSize],
        Persist = (CRED_PERSIST)unmanagedCred.Persist,
        Attributes = Enumerable.Range(0, (int)unmanagedCred.AttributeCount)
                                        .Select(x => Marshal.ReadIntPtr(unmanagedCred.Attributes, x * Marshal.SizeOf(typeof(CREDENTIAL_ATTRIBUTE))))
                                        .Select(x => (CREDENTIAL_ATTRIBUTE)Marshal.PtrToStructure(x, typeof(CREDENTIAL_ATTRIBUTE)))
                                        .ToArray(),
        TargetAlias = unmanagedCred.TargetAlias,
        UserName = unmanagedCred.UserName,
    };

    if (unmanagedCred.CredentialBlobSize != 0)
    {
        Marshal.Copy(unmanagedCred.CredentialBlob, managedCred.CredentialBlob, 0, (int)unmanagedCred.CredentialBlobSize);
    }

    return managedCred;
}

/// <summary>
/// マネージドな資格情報をアンマネージドなポインタに変換する
/// </summary>
/// <param name="managedCred"></param>
/// <returns></returns>
private static UnmanagedCredential ConvertToUnmanagedCredential(ManagedCredential managedCred)
{
    var unmanagedCred = new UnmanagedCredential()
    {
        Flags = (uint)managedCred.Flags,
        Type = (uint)managedCred.Type,
        TargetName = managedCred.TargetName,
        Comment = managedCred.Comment,
        LastWritten = managedCred.LastWritten,
        CredentialBlobSize = managedCred.CredentialBlob != null ? (uint)managedCred.CredentialBlob.Length : 0,
        Persist = (uint)managedCred.Persist,
        TargetAlias = managedCred.TargetAlias,
        UserName = managedCred.UserName,
        AttributeCount = managedCred.Attributes != null ? (uint)managedCred.Attributes.Length : 0,
        Attributes = IntPtr.Zero,
        CredentialBlob = IntPtr.Zero,
    };


    if (unmanagedCred.AttributeCount != 0)
    {
        Marshal.Copy(managedCred.Attributes.Cast<int>().ToArray(), 0, unmanagedCred.Attributes, managedCred.Attributes.Length);
    }

    if (unmanagedCred.CredentialBlobSize != 0)
    {
        var p = Marshal.AllocHGlobal(managedCred.CredentialBlob.Length);
        Marshal.Copy(managedCred.CredentialBlob, 0, p, managedCred.CredentialBlob.Length);
        unmanagedCred.CredentialBlob = p;
        Marshal.FreeHGlobal(p);
    }

    return unmanagedCred;
}

CredentialBlobだけはMarshal.Copyメソッドを使って取得する必要があります。

これには資格情報に紐付くパスワードが入っています。ManagedCredentialのGetPassword / SetPasswordメソッドを見ればわかる通り、BOMつきUTF-8で簡単にエンコード / デコードできてしまいます。悪用しないようにね!フリじゃないよ!

FormatMessage function

それじゃあ早速資格情報を操作して…と言いたいところなんですが、FormatMessageと言うWin32 APIの関数をマネージドコードでラップしておきます。

と言うのも、資格情報を操作するAPIは大体みんなboolを返してくれるんですが、「何で失敗したか」を取得するにはこれが必要だからです。

これがまた取得方法が面倒で…。とりあえずソースだけはっておきます。

[DllImport("kernel32.dll")]
private static extern uint FormatMessage(uint dwFlags, IntPtr lpSource, uint dwMessageId
    , uint dwLanguageId, StringBuilder lpBuffer, int nSize, IntPtr Arguments);

private static string GetErrorMessage()
{
    const uint FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000;
    var win32ErrorCode = Marshal.GetLastWin32Error();
    var message = new StringBuilder(255);
    FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, IntPtr.Zero, (uint)win32ErrorCode, 0, message, message.Capacity, IntPtr.Zero);

    return message.ToString();
}

資格情報の列挙

下準備がやっと(ある程度)終わりました。早速前回もやった資格情報の列挙をやってみましょう。CredEnumerateがそれに該当します。

[DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool CredEnumerate(string filter, CRED_FLAGS flag, out int count, out IntPtr pCredentials);

/// <summary>
/// 資格情報の列挙
/// </summary>
/// <returns></returns>
public static List<ManagedCredential> Enumerate()
{
    var count = 0;
    var pCredentials = IntPtr.Zero;

    //CredEnumerate呼び出し
    if (!CredEnumerate(null, 0, out count, out pCredentials))
    {
        Console.WriteLine(GetErrorMessage());
        throw new ApplicationException("資格情報の列挙に失敗しました。");
    }

    return Enumerable.Range(0, count)
                     .Select(n => Marshal.ReadIntPtr(pCredentials, n * Marshal.SizeOf(typeof(IntPtr))))
                     .Select(ptr => ConvertToManagedCredential(ptr))
                     .ToList();
}

pCredentialsはポインタの配列です。21行目で一旦これをIEnumerable<IntPtr>に変換してしまいます。22行目で先ほどのアンマネージド→マネージドに変換するメソッドを噛ませ、ToListで返してあげます。いい感じですね。

資格情報の読み込み

他の操作も大体こんな感じでやっていきます。次はCredReadですね。

[DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool CredRead(string targetName, CRED_TYPE type, CRED_FLAGS flags, out IntPtr pCredential);

/// <summary>
/// 資格情報の読み込み
/// </summary>
/// <param name="targetName"></param>
/// <param name="type"></param>
/// <returns></returns>
public static ManagedCredential Read(string targetName, CRED_TYPE type)
{
    var credential = IntPtr.Zero;

    if (!CredRead(targetName, type, 0, out credential))
    {
        Console.WriteLine(GetErrorMessage());
        throw new ApplicationException("資格情報の取得に失敗しました。");
    }

    return ConvertToManagedCredential(credential);
}

「targetNameってなんじゃい」とか「CRED_TYPEってなんじゃい」と言う質問に対する回答はCREDENTIAL構造体のドキュメントに載っているので読んでおいてください。(丸投げ)

資格情報の作成

CredWriteです。当然作成するのにマネージドコードのままじゃアレなので変換します。

[DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool CredWrite(ref UnmanagedCredential credential, CRED_FLAGS flags);

/// <summary>
/// 資格情報の登録
/// </summary>
/// <param name="managedCred"></param>
/// <param name="flags"></param>
public static void Write(ManagedCredential managedCred, CRED_FLAGS flags)
{
    Write(ConvertToUnmanagedCredential(managedCred), flags);
}

/// <summary>
/// 資格情報の登録
/// </summary>
/// <param name="unmanagedCred"></param>
/// <param name="flags"></param>
private static void Write(UnmanagedCredential unmanagedCred, CRED_FLAGS flags)
{
    if (!CredWrite(ref unmanagedCred, flags))
    {
        Console.WriteLine(GetErrorMessage());
        throw new ApplicationException("資格情報の書き込みに失敗しました。");
    }

    Console.WriteLine("ok");
}

資格情報の削除

CredDeleteです。

[DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool CredDelete(string targetName, CRED_TYPE type, CRED_FLAGS flags);

/// <summary>
/// 資格情報の削除
/// </summary>
/// <param name="targetName"></param>
/// <param name="type"></param>
/// <param name="flags"></param>
private static void Delete(string targetName, CRED_TYPE type, CRED_FLAGS flags)
{
    if (!CredDelete(targetName, type, flags))
    {
        Console.WriteLine(GetErrorMessage());
        throw new ApplicationException("資格情報の削除に失敗しました。");
    }
}

まとめ

他にもう一つ、資格情報の入力プロンプトを表示する機能があるんですが、微妙に私がこれを理解しきれていないのと、文字数がいっぱいいっぱいなので別途記事を立てようと思います。

ソリューションはここにあるので、使いたい人は勝手にどうぞ。

参考