🙆

Windows Hello で認証しないと使えない Win32 ネイティブアプリ

に公開

動機

本人認証しないと使えない Windows アプリを作りたかったので実装方法を調べてみました。
↓ 成果物。Windows Hello で認証しないとメインウィンドウをアクティブにできません。
verify.png

環境

Windows 25H2
rustc 1.91.1
windows crate 0.62.2

解説

RequestVerificationForWindowAsync

Windows Hello を使うには UserConsentVerifier クラスを使います。Microsoft 公式ドキュメントによると、C# ではRequestVerificationAsync関数を使いますが、C++/WinRT ではRequestVerificationForWindowAsyncを使う必要があるとのこと。windows crate の feature search で調べるIUserConsentVerifierInteropインターフェースで定義されていることが分かりました。Iで始まるのはインターフェースなので直接インスタンス化できません。なのでwindows::core::factor関数を使ってオブジェクトにアクセスします。(COM のことは詳しくないのですが、たぶん内部的にはどこかの DLL にある関数がメモリに読み込まれてて、その関数へのポインタを持ってるテーブル(このテーブルもメモリ上のどこかにある)にアクセスする感じ...なのかな?🤔)Windows Hello で認証する関数は以下のようになりました。

use anyhow::{Result, ensure};

// hwnd はメインウィンドウのウィンドウハンドル
fn verify(hwnd: HWND) -> Result<()> {
    // 認証デバイスが使えるかどうかのチェック
    let availability = UserConsentVerifier::CheckAvailabilityAsync()?.join()?;
    ensure!(
        availability == UserConsentVerifierAvailability::Available,
        "verfier is not available"
    );

    // IUserConsentVerifierInterop にアクセスする
    let verifier = factory::<UserConsentVerifier, IUserConsentVerifierInterop>()?;

    // 認証デバイスを使用して検証を実行する
    let result = unsafe {
        verifier
            .RequestVerificationForWindowAsync::<IAsyncOperation<UserConsentVerificationResult>>(
                hwnd,
                &"Please verify your identity".into(),
            )?
            .join()?
    };

    // 検証結果の確認
    ensure!(
        result == UserConsentVerificationResult::Verified,
        "failed to verify"
    );
    Ok(())
}

別スレッドで認証

上記のverify関数は別スレッドで呼び出す必要があるようです。メインウィンドウと同じスレッドで実行したら、アプリがデッドロックみたいな状況になったので別スレッドで実行することにしました。

fn main() -> Result<()> {
    // メインウィンドウの生成
    let hwnd = unsafe { CreateWindowExW(/* 引数は省略 */)? };

    // 別スレッドで verify を実行。
    thread::spawn(|| verify(hwnd));

    // ここにメッセージループ
    loop {
        // 省略
    }
}

そしたら、HWNDはスレッドセーフじゃないと怒られてビルドできませんでした。なので、HWNDをラップするHwnd構造体を定義して、「この構造体はスレッドセーフですよ」ということをマークしてコンパイルを通します。まあメインウィンドウのウィンドウハンドルなんで大丈夫でしょう。

struct Hwnd(HWND);

unsafe impl Send for Hwnd {}
unsafe impl Sync for Hwnd {}

fn main() -> Result<()> {
    // 省略

    let h = Hwnd(hwnd);
    thread::spawn(move || verify(h));

    // 省略
}

まとめ

エラーメッセージの表示等追加して全体のソースコードは以下のようになりました。

コピペ用サンプルコード
Cargo.toml
[package]
name = "verify"
version = "0.1.0"
edition = "2024"

[dependencies]
anyhow = "1.0"
windows-future = "0.3"

[dependencies.windows]
version = "0.62"
features = [
  "Security_Credentials_UI",
  "Win32_System_WinRT",
  "Win32_UI_WindowsAndMessaging",
  "Win32_Graphics_Gdi",
]
src/main.rs
#![cfg_attr(not(debug_assertions), windows_subsytem = "windows")]

use anyhow::{Result, ensure};
use std::fmt::Display;
use std::thread;
use windows::{
    Security::Credentials::UI::{
        UserConsentVerificationResult, UserConsentVerifier, UserConsentVerifierAvailability,
    },
    Win32::{
        Foundation::{HWND, LPARAM, LRESULT, WPARAM},
        System::WinRT::IUserConsentVerifierInterop,
        UI::WindowsAndMessaging::{
            CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DispatchMessageW, GetMessageW,
            IDI_APPLICATION, LoadCursorW, MB_ICONERROR, MSG, MessageBoxW, PostQuitMessage,
            RegisterClassW, SW_SHOW, SendMessageW, ShowWindow, TranslateMessage, WINDOW_EX_STYLE,
            WM_APP, WM_DESTROY, WNDCLASSW, WS_CAPTION, WS_OVERLAPPED, WS_SYSMENU, WS_VISIBLE,
        },
    },
    core::{HSTRING, PCWSTR, factory, w},
};
use windows_future::IAsyncOperation;

const CLASS_NAME: PCWSTR = w!("verify_window_class");
const WM_VERIFIED: u32 = WM_APP + 1;
const WM_REJECTED: u32 = WM_APP + 2;

struct Hwnd(HWND);

unsafe impl Send for Hwnd {}
unsafe impl Sync for Hwnd {}

fn verify_impl(hwnd: HWND) -> Result<()> {
    let availability = UserConsentVerifier::CheckAvailabilityAsync()?.join()?;
    ensure!(
        availability == UserConsentVerifierAvailability::Available,
        "verfier is not available"
    );

    let verifier = factory::<UserConsentVerifier, IUserConsentVerifierInterop>()?;
    let result = unsafe {
        verifier
            .RequestVerificationForWindowAsync::<IAsyncOperation<UserConsentVerificationResult>>(
                hwnd,
                &"Please verify your identity".into(),
            )?
            .join()?
    };
    ensure!(
        result == UserConsentVerificationResult::Verified,
        "failed to verify"
    );
    Ok(())
}

fn verify(hwnd: Hwnd) {
    match verify_impl(hwnd.0) {
        Ok(_) => unsafe {
            SendMessageW(hwnd.0, WM_VERIFIED, None, None);
        },
        Err(e) => unsafe {
            msg_box(hwnd.0, e);
            SendMessageW(hwnd.0, WM_REJECTED, None, None);
        },
    }
}

fn msg_box(hwnd: HWND, e: impl Display) {
    unsafe {
        MessageBoxW(
            Some(hwnd),
            &HSTRING::from(format!("{e}")),
            w!("Error"),
            MB_ICONERROR,
        );
    };
}

unsafe extern "system" fn wnd_proc(
    hwnd: HWND,
    msg: u32,
    wparam: WPARAM,
    lparam: LPARAM,
) -> LRESULT {
    match msg {
        WM_VERIFIED => {
            dbg!("verified");
        }
        WM_REJECTED => {
            dbg!("rejected");
            unsafe { PostQuitMessage(1) };
        }
        WM_DESTROY => {
            unsafe { PostQuitMessage(0) };
        }
        _ => return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) },
    }
    LRESULT::default()
}

fn main() -> Result<()> {
    let wc = WNDCLASSW {
        lpfnWndProc: Some(wnd_proc),
        lpszClassName: CLASS_NAME,
        hCursor: unsafe { LoadCursorW(None, IDI_APPLICATION)? },
        ..Default::default()
    };
    unsafe { RegisterClassW(&wc) };
    let hwnd = unsafe {
        CreateWindowExW(
            WINDOW_EX_STYLE::default(),
            CLASS_NAME,
            w!("verify"),
            WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_VISIBLE,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            300,
            200,
            None,
            None,
            None,
            None,
        )?
    };

    let h = Hwnd(hwnd);
    thread::spawn(move || verify(h));

    _ = unsafe { ShowWindow(hwnd, SW_SHOW) };

    let mut msg = MSG::default();
    loop {
        if unsafe { !GetMessageW(&mut msg, None, 0, 0).as_bool() } {
            break;
        }
        _ = unsafe { TranslateMessage(&msg) };
        unsafe { DispatchMessageW(&msg) };
    }
    Ok(())
}

以上。

Discussion