sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

WPF PasswordBox のバインドとその対策 (Windows Hello)

f:id:shikaku_sh:20210823173353p:plain:w300

PasswordBox コントロールの Password プロパティは仕様のため、バインディングをすることができません。まずもって、バインディングをすることはコンセプト的に非推奨です。

セキュリティ対策の一環のため、バインディングを禁止したのはパスワードの平文をメモリー内に持ちたくありませんでした。PasswordBox は SecurePassword プロパティを追加した経緯があります。SecurePassword は暗号化したパスワードをメモリー内に持つことが目的の機能でした。

なので、SecurePassword とバインディングすればいいのか、となりそうですが、現在は SecurePassword のプロパティ自体、非推奨になっています。

We don't recommend that you use the SecureString class for new development. For more information, see SecureString shouldn't be used on GitHub.

SecurePassword の実体は SecureString で実装されていますが、結局以下の問題となります:

  • .NET はどこかで文字列をプレーンテキストに変換する必要があるため、完全に防ぐことはできなかった
  • .NET Framework 以外では暗号化されません
  • .NET Core では、もうモリー内で暗号化されない

そんなわけで SecurePassword は死んだ仕様となり、簡単にパスワードを使うなら結局 Password プロパティでいいや、ってことになるかどうかは後述のとおりで、今は別の選択ができています。

非推奨でも Password プロパティを Binding する

実際のところ、平文でパスワードを扱わざるをえないなら、もうバインディングしてもいいやってときは、こんな感じでバインディングできると思います。

<PasswordBox local:AttachedProperty.BindablePassword="{Binding Text2}"/>
class AttachedProperty
{
    private static readonly DependencyProperty IsAttachedProperty =
        DependencyProperty.RegisterAttached(
            "IsAttached",
            typeof(bool),
            typeof(AttachedProperty),
            new FrameworkPropertyMetadata(
                false,
                FrameworkPropertyMetadataOptions.None,
                (s, e) =>
                {
                    if (s is PasswordBox passwordBox)
                    {
                        if (passwordBox == null)
                        {
                            return;
                        }

                        if ((bool)e.OldValue)
                        {
                            passwordBox.PasswordChanged -= PasswordBox_PasswordChanged;
                        }

                        if ((bool)e.NewValue)
                        {
                            passwordBox.PasswordChanged += PasswordBox_PasswordChanged;
                        }
                    }
                })
            );

    private static readonly DependencyProperty BindablePasswordProperty =
        DependencyProperty.RegisterAttached(
            "BindablePassword",
            typeof(string),
            typeof(AttachedProperty),
            new FrameworkPropertyMetadata(
                "",
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                (s, e) =>
                {
                    if (s is PasswordBox passwordBox)
                    {
                        var newPassword = (string)e.NewValue;

                        if (GetIsAttached(passwordBox) == false)
                        {
                            SetIsAttached(passwordBox, true);
                        }

                        // 例外
                        if (string.IsNullOrEmpty(passwordBox.Password) && string.IsNullOrEmpty(newPassword) ||
                            passwordBox.Password == newPassword)
                        {
                            return;
                        }

                        passwordBox.PasswordChanged -= PasswordBox_PasswordChanged;
                        passwordBox.Password = newPassword;
                        passwordBox.PasswordChanged += PasswordBox_PasswordChanged;
                    }
                })
            );

    public static bool GetIsAttached(DependencyObject d)
    {
        return (bool)d.GetValue(IsAttachedProperty);
    }

    public static void SetIsAttached(DependencyObject d, bool value)
    {
        d.SetValue(IsAttachedProperty, value);
    }

    public static string GetBindablePassword(DependencyObject d)
    {
        return (string)d.GetValue(BindablePasswordProperty);
    }

    public static void SetBindablePassword(DependencyObject d, string value)
    {
        d.SetValue(BindablePasswordProperty, value);
    }

    private static void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
    {
        var passwordBox = sender as PasswordBox;

        if (passwordBox == null)
        {
            return;
        }

        SetBindablePassword(passwordBox, passwordBox.Password);
    }
}

Windows Hello を利用した認証

パスワードの認証から Web アカウントマネージャー のような GitHubTwitter アカウントを利用して認証を受ける方法もあります。(ストア連携が必要になります)

Web アカウントマネージャーよりも楽に使える認証パターンとして、Windows Hello を利用した PIN や指紋認証によるログインを実装することも検討したいです。ログイン設定を OS 管理にできるため、省力化にも繋がると思います。

.NET5 の場合は、UWP (Win RT) から機能をつかうことになります。Visual Studio のプロジェクトファイル .csproj を開いて以下のように編集します:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net5.0-windows10.0.19041.0</TargetFramework>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Prism.Unity" Version="8.1.97" />
    <PackageReference Include="Prism.Wpf" Version="8.1.97" />
  </ItemGroup>

</Project>

TargetFramework のところを編集しただけです。デフォルトだとプロパティ値が net5.0-windows のようになっていると思います。対応を始める Windows10 のバージョンを明記することで指紋認証に使うクラスが利用できるようになります。

実際に使ってみたサンプルコードはつぎのとおり:

var ucvAvailability = await UserConsentVerifier.CheckAvailabilityAsync();

if (ucvAvailability == UserConsentVerifierAvailability.Available)
{
    var consentResult = await UserConsentVerifier.RequestVerificationAsync("Please provide fingerprint verification.");

    if (consentResult == UserConsentVerificationResult.Verified)
    {
        Debug.WriteLine("OK");
    }
}
  • CheckAvailabilityAsync()
    WindowsHello に対応しているかチェックする
  • RequestVerificationAsync(string)
    認証のウィンドウを表示する

非同期を使うので、Prism なんかのコマンドを利用すると楽:

TestFingerPrintCommand = new DelegateCommand(async () =>
{
    var ucvAvailability = await UserConsentVerifier.CheckAvailabilityAsync();
    ...
});

実際に動かしてみた感じはサンプルを参照。

エラー発生するとき

ちなみに、.NET5 で Microsoft.Windows.SDK.Contracts NuGet パッケージをインストールするとエラーになります。

f:id:shikaku_sh:20211104161336p:plain:w500

.NET 5 以上のターゲットを設定する場合、Windows Metadata コンポーネントを直接参照することはできません。 詳細については、 「https://aka.ms/netsdk1130」をご参照ください。

or

SupportedOSPlatformVersion 10.0.xxxxxx.0 を TargetPlatformVersion 7.0 より大きくすることはできません。

上述のとおり、.csproj を編集してください。

サンプル

f:id:shikaku_sh:20211104161243g:plain:w500

GitHub にコードのサンプルを公開しています。

参考