資格情報マネージャをマネージドコードから使用する。
正しいやり方がマジで分らない。誰か教えて下さい。
まず、前提条件として、既に読んでいるけれどもイマイチ意味が分らないと言うか、分った気がしない情報源等。
- Credentials Management
- ローカルストレージに保存するデータの暗号化 ― Windows の場合
- Windows XP および Windows Server 2003 での資格情報管理の使用方法
- Authentication Functions
- 説明がアッサリし過ぎててどうしたら良いか全然分らない…orz
- Credential Management with the .NET Framework 2.0
- Windows Data Protection
Windowsで動作するクライアントアプリケーションを実装しているます。
ある程度出来たら多分きっと間違いなくOSSとして公開するんだけど、今の所どこにも公開してませぬ。
ユーザIDとパスワード(以後Credential)を扱うソフトウェアになるので、比較的真面目に実装しようかな…と思ったり。
想定する動作環境は、
- .NET Framework 4.0 Client Profile
- WindowsXP sp3, Windows7, (Windows Vista)
まず、Credentialを一体全体どこに保管するのが正しいのさ?という事。
考えられるのは、
- ファイルを勝手な所に作る。
- 実行バイナリの隣
- Document and Settingsの下辺りのユーザディレクトリのどこか
- レジストリに格納する。
- オレオレエントリをどこかHKEY\CURRENT_USER\SOFTWAREの下辺りに作る
- 資格情報マネージャを使う
かな、と考える訳です。
資格情報マネージャは、WindowsXPやWindowsVistaのHomeエディション以下は、何かあったり無かったりするみたいなんだけどもさ。
で、とりあえずOSが抑えてる場所に格納するのが、相応に妥当だろうと考えるわけで。
じゃあ、System.Security辺りの名前空間に資格情報マネージャをハンドリング出来る様なマネージドAPIがある筈だと考えるじゃん?普通に。
無い。と言うか見つけられない。何でどうしてほわい?どこにあるの?
致し方ないので、DllImportする等して、自分でWin32APIを叩くじゃん?
いや、格納出来たよ、うん。
Windows7だと、コントロールパネルの中に資格情報マネージャってあるのさ。
汎用資格情報って所に、Credentialをエントリするとさ、以前のパスワード聞かずにガンガン変更出来ちゃう。
え?ナニソレ?
もっと絶望的なのは、僕が書いたコードでエントリしたデータを、Credential Management with the .NET Framework 2.0にリンクのあるサンプルコードでゴッソリ読めちゃうし変えられる。
マズくね?
しょうがないので、DPAPIを使ってパスワードを暗号化するじゃん?
そしたら、資格情報マネージャや、そのサンプルコードから値を変えると、複合化出来なくなる。
そりゃそうだよね、僕が書いたコードを彼らは知らない訳だからさ。
それ、不便じゃね?アレ?どうしよう?
と言うのが、今の状態。知りたいのは、
- Credentialをストレージする為の妥当な場所ややり方を考える為の基準。
- 資格情報マネージャを使うとして、
- エントリを消せるけど、パスワードは変えられない様にする方法。
- パスワードを変えた時に、コールバックを貰う、もしくは割り込む方法。
参考の為に今僕が書いたコードを張っておきます。
ドメインがどうこう言う部分は、僕とは関係ないので、全く実装してませぬ。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Security.Cryptography; using System.Runtime.InteropServices; using DWORD = System.UInt32; using LPTSTR = System.String; using LPBYTE = System.String; using LPCTSTR = System.String; using BOOL = System.Boolean; using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME; namespace System.Security.Authentication { public enum CRED_TYPE { GENERIC = 1, DOMAIN_PASSWORD, DOMAIN_CERTIFICATE, DOMAIN_VISIBLE_PASSWORD, DOMAIN_EXTENDED } [Flags] public enum CRED_FLAGS { PROMPT_NOW = 0x2, USERNAME_TARGET = 0x4 } public enum CRED_PERSIST { SESSION = 1, LOCAL_MACHINE, ENTERPRISE } #region internal use [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] internal struct PCREDENTIAL { internal DWORD Flags; internal DWORD Type; internal LPTSTR TargetName; internal LPTSTR Comment; internal FILETIME LastWritten; internal DWORD CredentialBlobSize; internal IntPtr CredentialBlob; internal DWORD Persist; internal DWORD AttributeCount; internal IntPtr Attributes; internal LPTSTR TargetAlias; internal LPTSTR UserName; } static class Natives { [DllImport("Advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern BOOL CredDelete(LPCTSTR TargetName, DWORD Type, DWORD Flags); [DllImport("Advapi32.dll", EntryPoint = "CredFree", SetLastError = true)] internal static extern void CredFree(ref IntPtr cred); [DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern BOOL CredRead(LPCTSTR TargetName, DWORD Type, DWORD Flags, out IntPtr Credential); [DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern BOOL CredWrite(ref PCREDENTIAL Credential, DWORD Flags); [DllImport("kernel32.dll", EntryPoint = "FormatMessage", CharSet = CharSet.Unicode)] internal static extern DWORD FormatMessage(DWORD dwFlags, IntPtr lpSource, DWORD dwMessageId, DWORD dwLanguageId, out LPTSTR lpBuffer, DWORD nSize, IntPtr Arguments); } #endregion public static class CredentialsManagement { public struct Credential { public string UserName; public string Password; } public static Credential Read(string targetname, CRED_TYPE type = CRED_TYPE.GENERIC) { IntPtr handle; Credential result = new Credential(); if (Natives.CredRead(targetname, (DWORD)type, 0, out handle)) { PCREDENTIAL cred = (PCREDENTIAL)Marshal.PtrToStructure(handle, typeof(PCREDENTIAL)); // NativeCallから返ってきたポインタがそのまま使われる環境が存在する為コピーする。 result.UserName = String.Copy(cred.UserName); // 自動的にマーシャルさせると、 // サイズを考慮せずに連続領域を全て持ってきてしまうらしく、末尾にtargetnameが付いてくる。 var s = Marshal.PtrToStringUni(cred.CredentialBlob, (int)cred.CredentialBlobSize / 2); result.Password = Decrypt(s); Natives.CredFree(ref handle); } else { ThrowWin32Exception(); } return result; } public static void Write(string targetname, Credential c) { PCREDENTIAL cred = new PCREDENTIAL(); cred.TargetName = targetname; cred.UserName = c.UserName; // 資格情報マネージャに格納されたパスワードはある種のツールで中身を見る事が出来るので暗号化する。 var s = Encrypt(c.Password); cred.CredentialBlob = Marshal.StringToCoTaskMemUni(s); cred.CredentialBlobSize = (DWORD)Encoding.Unicode.GetBytes(s).Length; cred.Type = (DWORD) CRED_TYPE.GENERIC; cred.Persist = (DWORD)CRED_PERSIST.LOCAL_MACHINE; cred.AttributeCount = 0; cred.Attributes = IntPtr.Zero; cred.Comment = null; cred.LastWritten = DateTime.Today.ToFILETIME(); if (Natives.CredWrite(ref cred, 0) == false) { ThrowWin32Exception(); } } public static void Delete(string targetname, CRED_TYPE type = CRED_TYPE.GENERIC) { if (Natives.CredDelete(targetname, (DWORD)type, 0) == false) { ThrowWin32Exception(); } } public static string Encrypt(string s) { var bytes = Encoding.Unicode.GetBytes(s); var b = ProtectedData.Protect(bytes, null, DataProtectionScope.CurrentUser); return Convert.ToBase64String(b); } public static string Decrypt(string s) { try { var bytes = Convert.FromBase64String(s); var b = ProtectedData.Unprotect(bytes, null, DataProtectionScope.CurrentUser); return Encoding.Unicode.GetString(b); } catch (CryptographicException) { // 資格情報マネージャで値を変更している等、複合化出来ない要因があるので、 // 値が無効であるとみなす。 return null; } } internal static void ThrowWin32Exception() { int code = Marshal.GetLastWin32Error(); const DWORD FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000; string msg; Natives.FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, IntPtr.Zero, (uint)code, 0, out msg, 0, IntPtr.Zero); throw new Exception(msg); } internal static FILETIME ToFILETIME(this DateTime datetime) { var value = datetime.ToFileTime(); return new FILETIME() { dwHighDateTime = unchecked((int)(value >> 32)), dwLowDateTime = unchecked((int)value) }; } } }