附带 Windows Hello 配套 (IoT) 设备的 Windows 解锁

Windows Hello 配套设备是一种可与 Windows 10 桌面版一起使用从而增强用户身份验证体验的设备。 通过使用 Windows Hello 配套设备框架,即使是在生物识别不可用时(例如,在 Windows 10 桌面版缺少相机进行面部身份验证或缺少指纹读取器设备时),配套设备也能提供丰富的 Windows Hello 体验。

注意

Windows 10 版本 2004 中已弃用 Windows Hello 配套设备框架的 API。

介绍

有关代码示例,请参阅 Windows Hello 配套设备框架 GitHub 存储库

用例

有多个方法可通过 Windows Hello 配套设备框架并借助配套设备来构建出色的 Windows 解锁体验。 例如,用户可:

  • 通过 USB 将配套设备附加到电脑,触摸配套设备上的按钮,然后自动解锁电脑。
  • 在口袋里装上一个已通过蓝牙与电脑进行配对的手机。 在电脑上点击空格键后,他们的手机便会收到通知。 批准此通知,电脑即可解锁。
  • 点击其连接到 NFC 读卡器的配套设备,可快速解锁其电脑。
  • 佩戴已对佩戴者进行身份验证的健身手环。 接近电脑时,通过做出特殊手势(如鼓掌),电脑即可解锁。

已启用生物识别的 Windows Hello 配套设备

如果配套设备支持生物识别,则在某些情况下,Windows 生物识别框架可能是一种优于 Windows Hello 配套设备框架的解决方案。

解决方案组件

下图描绘了该解决方案的组件,以及负责生成这些组件的人员。

框架概述

Windows Hello 配套设备框架会作为在 Windows 上运行的一项服务(在本文中被称为“配套身份验证服务”)来实现。 此服务负责生成一个解锁令牌,而该令牌需由存储在 Windows Hello 配套设备上的 HMAC 密钥提供保护。 此举可保证访问解锁令牌时需存在 Windows Hello 配套设备。 每个(电脑、Windows 用户)元组均有一个唯一的解锁令牌。

与 Windows Hello 配套设备框架集成时,需有:

  • 从 Windows 应用商店下载的、适用于配套设备的通用 Windows 平台 (UWP) Windows Hello 配套设备应用。
  • 可在 Windows Hello 配套设备上创建两个 256 位 HMAC 密钥,并使用它们(并借助 SHA-256)来生成 HMAC。
  • 已正确配置 Windows 10 桌面版上的安全设置。 配套身份验证服务需先设置此 PIN,才能接入任一 Windows Hello 配套设备。 用户必须通过“设置”>“帐户”>“登录”选项来设置 PIN。

除上述要求外,Windows Hello 配套设备应用还负责:

  • Windows Hello 配套设备的初始注册与取消注册的相关用户体验和品牌打造。
  • 在后台运行、发现 Windows Hello 配套设备、与 Windows Hello 配套设备进行通信,以及提供“配套身份验证服务”。
  • 错误处理

通常,配套设备会随某一应用一起提供以便进行初始设置;例如,首次设置健身手环。 本文档中所述的功能可能为该应用的其中一部分,而无需使用单独的应用。

用户信号

每个 Windows Hello 配套设备均应与支持三个用户信号的应用搭配使用。 这些信号可能会采用操作或手势的形式。

  • 意向信号:允许用户表明其解锁意图;例如,点击 Windows Hello 配套设备上的某一按钮。 此意向信号必须在 Windows Hello 配套设备端进行收集。
  • 用户存在信号:证明用户确实存在。 例如,Windows Hello 配套设备可能需要 PIN 才能用于解锁电脑(而不应与电脑 PIN 相混淆),或是可能要求按下某一按钮。
  • 消除歧义信号:有多个选项可供 Windows Hello 配套设备使用时,用于消除有关用户要解锁的实际 Windows 10 桌面版的歧义。

任意数量的此类用户信号均可组合成一个信号。 每次使用时,均须有用户存在与意向信号。

电脑与 Windows Hello 配套设备之间的注册和未来通信

必须先向 Windows Hello 配套设备框架注册 Windows Hello 配套设备,然后才能将此配套设备接入该框架。 注册体验完全由 Windows Hello 配套设备应用负责。

Windows Hello 配套设备与 Windows 10 桌面设备之间的关系可为“一对多”(即,一个配套设备可用于多个 Windows 10 桌面设备)。 但是,每个 Windows Hello 配套设备只能用于每个 Windows 10 桌面设备上的一个用户。

在 Windows Hello 配套设备可与电脑进行通信之前,二者需就要使用的传输方法达成一致。 此类选择需由 Windows Hello 配套设备应用来决定;Windows Hello 配套设备框架不会对 Windows Hello 配套设备与 Windows 10 桌面设备端的 Windows Hello 配套设备应用之间所用的传输类型(USB、NFC、Wi-Fi、BT、BLE 等)或协议施加任何限制。 但是,它确实会推荐针对传输层的某些安全注意事项,如本文档的“安全要求”一节所述。 设备提供商需负责提供这些要求。 此框架不提供相关要求。

用户交互模型

Windows Hello 配套设备应用的发现、安装和首次注册

典型的用户工作流如下所示:

  • 用户在要使用该 Windows Hello 配套设备来解锁的每个目标 Windows 10 桌面设备上设置 PIN。
  • 用户在其 Windows 10 桌面设备上运行 Windows Hello 配套设备应用,从而将 Windows Hello 配套设备注册到 Windows 10 桌面版。

注意:

  • 我们建议简化和自动化(如果可能)Windows Hello 配套设备应用的发现、下载和启动(例如当点击 Windows 10 桌面版设备端的 NFC 读卡器上的 Windows Hello 配套设备时,即可下载应用)。 但是,此操作需由 Windows Hello 配套设备和 Windows Hello 配套设备应用负责。
  • 在企业环境中,可通过 MDM 部署 Windows Hello 配套设备应用。
  • Windows Hello 配套设备应用需负责向用户显示注册过程中出现的所有错误消息。

注册和取消注册协议

下图演示了注册期间 Windows Hello 配套设备如何与配套身份验证服务进行交互。

注册流程的示意图。

我们的协议使用了两个密钥:

  • 设备密钥 (devicekey):用于保护电脑解锁 Windows 所需的解锁令牌。
  • 身份验证密钥 (authkey):用于对 Windows Hello 配套设备和配套身份验证服务进行相互验证。

注册时,设备密钥和身份验证密钥会在 Windows Hello 配套设备应用与 Windows Hello 配套设备之间进行交换。 因此,Windows Hello 配套设备应用和 Windows Hello 配套设备必须使用一种安全的传输方法来保护这些密钥。

此外请注意,虽然上图显示了在 Windows Hello 配套设备上生成的两个 HMAC 密钥,但此应用还可生成这些密钥并将其发送到 Windows Hello 配套设备以便进行存储。

启动身份验证流程

用户可通过两种方式并使用 Windows Hello 配套设备框架(即,提供意向信号)来启动登录到 Windows 10 桌面版的流程:

  • 打开笔记本电脑上的盖子,或是点击空格键或在电脑上向上轻扫。
  • 在 Windows Hello 配套设备端执行某一手势或操作。

由 Windows Hello 配套设备选择将哪个设备作为起点。 当选项 1 出现时,Windows Hello 配套设备框架会通知配套设备应用。 对于选项 2,Windows Hello 配套设备应用应查询配套设备,以确认是否已捕获该事件。 此举可确保 Windows Hello 配套设备在解锁成功之前能收集到意向信号。

Windows Hello 配套设备凭据提供程序

Windows 10 中有一个新的凭据提供程序,它可处理所有 Windows Hello 配套设备。

Windows Hello 配套设备凭据提供程序负责通过激活触发器来启动配套设备后台任务。 当电脑唤醒并显示锁屏时,会首次设置该触发器。 当电脑进入登录 UI 且 Windows Hello 配套设备凭据提供程序为所选磁贴时,会第二次设置该触发器。

Windows Hello 配套设备应用的帮助程序库会侦听锁屏状态变化,并发送对应于 Windows Hello 配套设备后台任务的事件。

如果存在多个 Windows Hello 配套设备后台任务,则完成身份验证流程的第一个后台任务会解锁电脑。 配套设备身份验证服务会忽略所有其余身份验证调用。

Windows Hello 配套设备端的体验由 Windows Hello 配套设备应用负责并进行相应的管理。 Windows Hello 配套设备框架无法控制此部分的用户体验。 更具体地说,配套身份验证提供程序通知 Windows Hello 配套设备应用(通过后台应用)有关登录 UI 的状态更改情况(例如锁屏界面刚应用,或者用户刚点击了空格键而消除了锁屏界面),而围绕状态更改来生成体验(例如当用户点击空格键和消除锁屏界面时,开始通过 USB 寻找设备)则是 Windows Hello 配套设备应用的责任。

Windows Hello 配套设备框架提供可供 Windows Hello 配套设备应用从中选择的一系列(本地化后的)文本和错误消息。 这些内容会显示在锁屏界面的顶部(或登录 UI 中)。 有关更多详细信息,请参阅“处理消息和错误”一节。

身份验证协议

启动与 Windows Hello 配套设备应用关联的后台任务后,该服务便会负责要求 Windows Hello 配套设备验证配套身份验证服务计算出的 HMAC 值,并帮助计算两个 HMAC 值:

  • 验证服务 HMAC = HMAC(身份验证密钥、服务 nonce || 设备 nonce || 会话 nonce)。
  • 使用 nonce 来计算设备密钥的 HMAC。
  • 计算身份验证密钥的 HMAC;其中,第一个 HMAC 值会与配套身份验证服务生成的 nonce 进行连接。

该服务会使用第二个计算出的值对设备进行身份验证,同时防止传输通道中的重放攻击。

更新后的注册流程的示意图。

生命周期管理

注册一次即可随心使用

如果没有后端服务器,用户则须单独向每个 Windows 10 桌面设备注册其 Windows Hello 配套设备。

配套设备供应商或 OEM 可实现 Web 服务,从而跨用户 Windows 10 桌面版或移动设备来漫游注册状态。 有关更多详细信息,请参阅“漫游、吊销和筛选服务”一节。

PIN 管理

在使用配套设备之前,需在 Windows 10 桌面设备上设置 PIN。 此举可确保用户留有备份,以免其 Windows Hello 配套设备无效。 PIN 是一种由 Windows 进行管理而该应用永远无法看到的内容。 若要更改它,用户应导航到“设置”>“帐户”>“登录”选项。

管理和策略

用户可通过在该桌面设备上运行 Windows Hello 配套设备应用,从而从 Windows 10 桌面版中删除 Windows Hello 配套设备。

企业有两个选项可用于控制 Windows Hello 配套设备框架:

  • 打开或关闭此功能
  • 使用 Windows 应用保险箱定义允许的 Windows Hello 配套设备允许列表

Windows Hello 配套设备框架不支持任何集中方式来保留可用配套设备的清单,也不支持通过某一方法来进一步筛选允许使用哪些 Windows Hello 配套设备类型的实例(例如,只允许使用其序列号介于 X 与 Y 之间的配套设备)。 但是,应用开发人员可构建一项服务来提供此类功能。 有关更多详细信息,请参阅“漫游、吊销和筛选服务”一节。

撤销

Windows Hello 配套设备框架不支持从特定 Windows 10 桌面设备中远程删除配套设备。 相反,用户可通过在该 Windows 10 桌面版上运行的 Windows Hello 配套设备应用来删除 Windows Hello 配套设备。

但是,配套设备供应商可构建一个服务来提供远程吊销功能。 有关更多详细信息,请参阅“漫游、吊销和筛选服务”一节。

漫游与筛选服务

配套设备供应商可实现一个可用于以下场景的 Web 服务:

  • 针对企业的筛选服务:企业可将一组可在其环境中运行的 Windows Hello 配套设备限制为来自特定供应商的少数设备。 例如,Contoso 公司可从供应商 X 订购 10,000 个型号 Y 的配套设备,并确保仅这些设备(而不是来自供应商 X 的任何其他设备型号)可在 Contoso 域中工作。
  • 清单:企业可确定在企业环境中使用的现有配套设备的列表。
  • 实时吊销:如果员工报告其配套设备已丢失或被盗,此 Web 服务则可用于撤销该设备。
  • 漫游:用户只需注册配套设备一次,即可在其所有 Windows 10 桌面版和移动版上使用该配套设备。

实现这些功能要求 Windows Hello 配套设备应用在注册和使用时与该 Web 服务进行协商。 Windows Hello 配套设备应用可针对缓存式登录场景进行优化,例如每天只需与 Web 服务协商一次(代价则是将吊销时间延长到最长一天)。

Windows Hello 配套设备框架 API 模型

概述

Windows Hello 配套设备应用应包含两个组件:一个负责注册和注销设备的 UI,以及一个用于处理身份验证的后台任务。

总体 API 流程如下:

  1. 注册 Windows Hello 配套设备
    • 确保该设备位于附近并查询其功能(如果需要)
    • 生成两个 HMAC 密钥(位于配套设备端或应用端)
    • 调用 RequestStartRegisteringDeviceAsync
    • 调用 FinishRegisteringDeviceAsync
    • 确保 Windows Hello 配套设备应用存储了这些 HMAC 密钥(如果受支持),同时 Windows Hello 配套设备应用会放弃其副本
  2. 注册后台任务
  3. 等待后台任务中出现正确的事件
    • WaitingForUserConfirmation:如果需要 Windows Hello 配套设备端的用户操作/手势才能启动身份验证流程,则等待此事件出现
    • CollectingCredential:如果电脑端的 Windows Hello 配套设备依靠用户操作/手势启动身份验证流程(例如通过点击空格键),请等待此事件
    • 其他触发器(例如,智能卡):请务必查询当前身份验证状态以调用正确的 API。
  4. 通过调用 ShowNotificationMessageAsync 使用户始终了解错误消息或所需完成的后续步骤。 收集到意向信号后,仅调用此 API 一次
  5. 解锁
    • 确保意向与用户存在信号均已收集到
    • 调用 StartAuthenticationAsync
    • 与配套设备进行通信,从而执行必要的 HMAC 操作
    • 调用 FinishAuthenticationAsync
  6. 当用户请求 Windows Hello 配套设备时,注销该设备(例如,如果用户已丢失其配套设备)
    • 通过 FindAllRegisteredDeviceInfoAsync 枚举已登录用户的 Windows Hello 配套设备
    • 使用 UnregisterDeviceAsync 注销此配套设备

注册和取消注册

注册时需对配套身份验证服务进行两次 API 调用:RequestStartRegisteringDeviceAsync 和 FinishRegisteringDeviceAsync。

在执行上述任意调用之前,Windows Hello 配套设备应用均须确保 Windows Hello 配套设备可用。 如果 Windows Hello 配套设备负责生成 HMAC 密钥(身份验证密钥和设备密钥),Windows Hello 配套设备应用则还应要求配套设备生成这些密钥,然后再执行上述任一调用。 如果 Windows Hello 配套设备应用负责生成 HMAC 密钥,则它应在调用上述两个调用之前执行此操作。

此外,作为第一个 API 调用 (RequestStartRegisteringDeviceAsync) 的一部分,Windows Hello 配套设备应用必须确定设备功能,并准备好将其作为此 API 调用的一部分来传递;例如,该 Windows Hello 配套设备是否支持 HMAC 密钥的安全存储。 如果同一 Windows Hello 配套设备应用还用于管理同一配套设备的多个版本以及此类功能更改(且需通过设备查询来确定),则建议在执行第一个 API 调用之前完成此查询。

第一个 API (RequestStartRegisteringDeviceAsync) 将返回第二个 API (FinishRegisteringDeviceAsync) 使用的句柄。 注册的第一个调用会启动 PIN 提示,从而确保用户存在。 如果未设置 PIN,此调用则会失败。 Windows Hello 配套设备应用还可通过 KeyCredentialManager.IsSupportedAsync 调用来查询 PIN 是否已设置。 如果策略已禁用对 Windows Hello 配套设备进行使用,RequestStartRegisteringDeviceAsync 调用也可能会失败。

第一个调用的结果会通过 SecondaryAuthenticationFactorRegistrationStatus 枚举返回:

{
	Failed = 0, 		// Something went wrong in the underlying components
	Started,     		// First call succeeded
	CanceledByUser,  	// User cancelled PIN prompt
	PinSetupRequired,	// PIN is not set up
	DisabledByPolicy,	// Companion device framework or this app is disabled
}

第二个调用 (FinishRegisteringDeviceAsync) 会完成此注册。 注册过程中,Windows Hello 配套设备应用可使用配套身份验证服务来存储配套设备配置数据。 此数据的大小限制为 4 KB。 此数据会在身份验证时提供给 Windows Hello 配套设备应用。 例如,可使用此数据连接到 Windows Hello 配套设备(如 MAC 地址);如果 Windows Hello 配套设备没有存储功能而配套设备想使用电脑进行存储,则可使用配置数据。 请注意,作为配置数据的一部分而存储的任意敏感数据均须通过仅 Windows Hello 配套设备知晓的密钥进行加密。 此外,由于配置数据会由 Windows 服务进行存储,因而该数据可通过不同的用户配置文件供 Windows Hello 配套设备应用使用。

Windows Hello 配套设备应用可调用 AbortRegisteringDeviceAsync 来取消注册并传入错误代码。 配套身份验证服务会在遥测数据中记录该错误。 执行此调用很好的示例是 Windows Hello 配套设备出错并且无法完成注册(例如无法存储 HMAC 密钥或者丢失了 BT 连接)。

Windows Hello 配套设备应用必须让用户可以选择从 Windows 10 桌面版注销 Windows Hello 配套设备(例如,在他们丢失了配套设备或购买了较新的版本时)。 当用户选择该选项时,Windows Hello 配套设备应用必须调用 UnregisterDeviceAsync。 Windows Hello 配套设备应用执行的此调用将触发配套设备身份验证服务,以从电脑端删除与调用方应用的特定设备 ID 和 AppId 相对应的所有数据(包括 HMAC 密钥)。 此 API 调用不会尝试从 Windows Hello 配套设备应用或配套设备端删除 HMAC 密钥。 此操作需由 Windows Hello 配套设备应用来完成。

Windows Hello 配套设备应用负责显示注册与取消注册阶段出现的所有错误消息。

using System;
using Windows.Security.Authentication.Identity.Provider;
using Windows.Storage.Streams;
using Windows.Security.Cryptography;
using Windows.UI.Popups;

namespace SecondaryAuthFactorSample
{
	public class DeviceRegistration
	{

		public void async OnRegisterButtonClick()
		{
			//
			// Pseudo function, the deviceId should be retrieved by the application from the device
			//
			string deviceId = await ReadSerialNumberFromDevice();

			IBuffer deviceKey = CryptographicBuffer.GenerateRandom(256/8);
			IBuffer mutualAuthenticationKey = CryptographicBuffer.GenerateRandom(256/8);

			SecondaryAuthenticationFactorRegistration registrationResult =
				await SecondaryAuthenticationFactorRegistration.RequestStartRegisteringDeviceAsync(
					deviceId,  // deviceId: max 40 wide characters. For example, serial number of the device
					SecondaryAuthenticationFactorDeviceCapabilities.SecureStorage |
						SecondaryAuthenticationFactorDeviceCapabilities.HMacSha256 |
						SecondaryAuthenticationFactorDeviceCapabilities.StoreKeys,
					"My test device 1", // deviceFriendlyName: max 64 wide characters. For example: John's card
					"SAMPLE-001", // deviceModelNumber: max 32 wide characters. The app should read the model number from device.
					deviceKey,
					mutualAuthenticationKey);

			switch(registerResult.Status)
			{
			case SecondaryAuthenticationFactorRegistrationStatus.Started:
				//
				// Pseudo function:
				// The app needs to retrieve the value from device and set into opaqueBlob
				//
				IBuffer deviceConfigData = ReadConfigurationDataFromDevice();

				if (deviceConfigData != null)
				{
					await registrationResult.Registration.FinishRegisteringDeviceAsync(deviceConfigData); //config data limited to 4096 bytes
					MessageDialog dialog = new MessageDialog("The device is registered correctly.");
					await dialog.ShowAsync();
				}
				else
				{
					await registrationResult.Registration.AbortRegisteringDeviceAsync("Failed to connect to the device");
					MessageDialog dialog = new MessageDialog("Failed to connect to the device.");
					await dialog.ShowAsync();
				}
				break;

			case SecondaryAuthenticationFactorRegistrationStatus.CanceledByUser:
				MessageDialog dialog = new MessageDialog("You didn't enter your PIN.");
				await dialog.ShowAsync();
				break;

			case SecondaryAuthenticationFactorRegistrationStatus.PinSetupRequired:
				MessageDialog dialog = new MessageDialog("Please setup PIN in settings.");
				await dialog.ShowAsync();
				break;

			case SecondaryAuthenticationFactorRegistrationStatus.DisabledByPolicy:
				MessageDialog dialog = new MessageDialog("Your enterprise prevents using this device to sign in.");
				await dialog.ShowAsync();
				break;
			}
		}

		public void async UpdateDeviceList()
		{
			IReadOnlyList<SecondaryAuthenticationFactorInfo> deviceInfoList =
				await SecondaryAuthenticationFactorRegistration.FindAllRegisteredDeviceInfoAsync(
					SecondaryAuthenticationFactorDeviceFindScope.User);

			if (deviceInfoList.Count > 0)
			{
				foreach (SecondaryAuthenticationFactorInfo deviceInfo in deviceInfoList)
				{
					//
					// Add deviceInfo.FriendlyName and deviceInfo.DeviceId into a combo box
					//
				}
			}
		}

		public void async OnUnregisterButtonClick()
		{
			string deviceId;
			//
			// Read the deviceId from the selected item in the combo box
			//
			await SecondaryAuthenticationFactorRegistration.UnregisterDeviceAsync(deviceId);
		}
	}
}

身份验证

身份验证时需对配套身份验证服务进行两次 API 调用:StartAuthenticationAsync 和 FinishAuthencationAsync。

第一个启动 API 将返回第二个 API 使用的句柄。 第一个调用会返回一个 nonce;而一旦与其他内容进行连接,则需使用存储在 Windows Hello 配套设备上的设备密钥对此 nonce 进行 HMAC 转换。 第二个调用会返回附带设备密钥的 HMAC 的结果,且可能以成功的身份验证(即,用户会看到其桌面)结束。

如果策略在初始注册后便已禁用 Windows Hello 配套设备,则第一个启动 API (StartAuthenticationAsync) 可能会失败。 如果该 API 调用是在 WaitingForUserConfirmation 或 CollectingCredential 状态(本节稍后将对此进行详细介绍)之外执行的,则该调用也可能会失败。 如果已注销的配套设备应用对其进行调用,它也可能会失败。 SecondaryAuthenticationFactorAuthenticationStatus 枚举会对可能的结果进行总结:

{
	Failed = 0, 					// Something went wrong in the underlying components
	Started,
	UnknownDevice,    				// Companion device app is not registered with framework
	DisabledByPolicy, 				// Policy disabled this device after registration
	InvalidAuthenticationStage,		// Companion device framework is not currently accepting
									// incoming authentication requests
}

如果第一个调用中提供的 nonce 已过期(20 秒),则第二个 API 调用 (FinishAuthencationAsync) 可能会失败。 SecondaryAuthenticationFactorFinishAuthenticationStatus 枚举会捕获可能的结果。

{
	Failed = 0, 	// Something went wrong in the underlying components
	Completed,   	// Success
	NonceExpired,   // Nonce is expired
}

执行这两个 API 调用(StartAuthenticationAsync 和 FinishAuthencationAsync)的时机均需与 Windows Hello 配套设备收集意向、用户存在于消除歧义信号的方式保持一致(有关更多详细信息,请参阅“用户信号”)。 例如,在意向信号可用之前,不得提交第二个调用。 换言之,如果用户没有表明其意图,则电脑不应解锁。 为进一步说明此问题,假设蓝牙邻近感应被用于电脑解锁,则须收集明确的意向信号;否则,一旦用户在其前往厨房时路过其电脑,电脑就会解锁。 此外,第一个调用返回的 nonce 存在时间限制的(20 秒),且会在特定时段后过期。 因此,仅应在 Windows Hello 配套设备应用可以清楚指示配套设备存在(例如,配套设备已插入 USB 端口或者已点击 NFC 读卡器)时,才能执行第一个调用。 使用蓝牙时,必须注意避免影响电脑端的电池,或在检查是否存在 Windows Hello 配套设备时影响正在进行的其他蓝牙活动。 另外,如果需要提供用户存在信号(例如通过键入 PIN),建议在收集完该信号后执行第一个身份验证调用。

通过提供用户在身份验证流程中所处阶段的完整概况,Windows Hello 配套设备框架可帮助 Windows Hello 配套设备应用就何时执行上述两个调用做出明智决策。 Windows Hello 配套设备框架通过向应用后台任务提供锁定状态更改通知来提供此功能。

配套设备流程

每种此类状态的详细信息如下:

状态 说明
WaitingForUserConfirmation 当锁屏界面应用(例如用户已按下 Windows + L)时,将触发此状态更改通知事件。 对于与难以发现处于错误状态的设备有关的错误消息,建议不请求此类错误消息。 一般情况下,建议仅在有意向信号时显示消息。 如果配套设备收集意图信号(例如点击 NFC 读卡器、在配套设备上按某个按钮,或者是诸如鼓掌等特定手势),Windows Hello 配套设备应用应在此状态中将第一个 API 调用用于身份验证,并且 Windows Hello 配套设备应用后台任务会从配套设备接收已检测到意图信号的指示。 否则,如果 Windows Hello 配套设备应用依赖于电脑来启动身份验证流程(具体方法是:让用户向上轻扫解锁屏幕或点击空格栏),Windows Hello 配套设备应用则需等待下一个状态 (CollectingCredential) 的出现。
CollectingCredential 当用户打开笔记本电脑盖板、点击键盘上的任意键或向上轻扫解锁屏幕时,均会触发此状态更改通知事件。 如果 Windows Hello 配套设备依赖上述操作来开始收集意图信号,则 Windows Hello 配套设备应用应开始收集该信号(例如,通过配套设备上询问用户是否想要解锁电脑的弹出窗口)。 如果 Windows Hello 配套设备应用需要用户在配套设备上提供用户存在信号(例如,在 Windows Hello 配套设备上键入 PIN),此时则是提供错误案例的好时机。
SuspendingAuthentication 当 Windows Hello 配套设备应用收到此状态时,则意味着配套身份验证服务已停止接受身份验证请求。
CredentialCollected 这意味着,另一个 Windows Hello 配套设备应用已调用第二个 API,且配套身份验证服务正在验证提交的内容。 此时,配套身份验证服务不接受任何其他身份验证请求,除非当前提交的身份验证请求未通过验证。 Windows Hello 配套设备应用应保持侦听状态,直到出现下一个状态。
CredentialAuthenticated 这意味着提交的凭据有效。 credentialAuthenticated 具有已成功 Windows Hello 配套设备的设备 ID。 Windows Hello 配套设备应用应确保检查此事项,从而了解其关联的设备是否已成功。 否则,Windows Hello 配套设备应用应避免显示任何后续身份验证流程(例如,配套设备上的成功消息或该设备上可能出现振动)。 请注意,如果提交的凭据无效,此状态则会变为 CollectingCredential 状态。
StoppingAuthentication 身份验证成功,且用户已看到桌面。 终止后台任务的时间。 退出后台任务之前,请显式注销 StageEvent 处理程序。 此举有助于后台任务快速退出。

Windows Hello 配套设备应用应仅调用处于前两种状态的两个身份验证 API。 Windows Hello 配套设备应用应检查触发此事件的对应场景。 有两种可能性:解锁或解锁后。 目前仅支持解锁。 在即将发布的版本中,可能会支持解锁后场景。 SecondaryAuthenticationFactorAuthenticationScenario 枚举会捕获以下两个选项:

{
	SignIn = 0,      	// Running under lock screen mode
	CredentialPrompt, 	// Running post unlock
}

完整代码示例:

using System;
using Windows.Security.Authentication.Identity.Provider;
using Windows.Storage.Streams;
using Windows.Security.Cryptography;
using System.Threading;
using Windows.ApplicationModel.Background;

namespace SecondaryAuthFactorSample
{
	public sealed class AuthenticationTask : IBackgroundTask
	{
		private string _deviceId;
		private static AutoResetEvent _exitTaskEvent = new AutoResetEvent(false);
		private static IBackgroundTaskInstance _taskInstance;
		private BackgroundTaskDeferral _deferral;

		private void Authenticate()
		{
			int retryCount = 0;

			while (retryCount < 3)
			{
				//
				// Pseudo code, the svcAuthNonce should be passed to device or generated from device
				//
				IBuffer svcAuthNonce = CryptographicBuffer.GenerateRandom(256/8);

				SecondaryAuthenticationFactorAuthenticationResult authResult = await
					SecondaryAuthenticationFactorAuthentication.StartAuthenticationAsync(
						_deviceId,
						svcAuthNonce);
				if (authResult.Status != SecondaryAuthenticationFactorAuthenticationStatus.Started)
				{
					SecondaryAuthenticationFactorAuthenticationMessage message;
					switch (authResult.Status)
					{
						case SecondaryAuthenticationFactorAuthenticationStatus.DisabledByPolicy:
							message = SecondaryAuthenticationFactorAuthenticationMessage.DisabledByPolicy;
							break;
						case SecondaryAuthenticationFactorAuthenticationStatus.InvalidAuthenticationStage:
							// The task might need to wait for a SecondaryAuthenticationFactorAuthenticationStageChangedEvent
							break;
						default:
							return;
					}

					// Show error message. Limited to 512 characters wide
					await SecondaryAuthenticationFactorAuthentication.ShowNotificationMessageAsync(null, message);
					return;
				}

				//
				// Pseudo function:
				// The device calculates and returns sessionHmac and deviceHmac
				//
				await GetHmacsFromDevice(
					authResult.Authentication.ServiceAuthenticationHmac,
					authResult.Authentication.DeviceNonce,
					authResult.Authentication.SessionNonce,
					out deviceHmac,
					out sessionHmac);
				if (sessionHmac == null ||
					deviceHmac == null)
				{
					await authResult.Authentication.AbortAuthenticationAsync(
						"Failed to read data from device");
					return;
				}

				SecondaryAuthenticationFactorFinishAuthenticationStatus status =
					await authResult.Authentication.FinishAuthencationAsync(deviceHmac, sessionHmac);
				if (status == SecondaryAuthenticationFactorFinishAuthenticationStatus.NonceExpired)
				{
					retryCount++;
					continue;
				}
				else if (status == SecondaryAuthenticationFactorFinishAuthenticationStatus.Completed)
				{
					// The credential data is collected and ready for unlock
					return;
				}
			}
		}

		public void OnAuthenticationStageChanged(
			object sender,
			SecondaryAuthenticationFactorAuthenticationStageChangedEventArgs args)
		{
			// The application should check the args.StageInfo.Stage to determine what to do in next. Note that args.StageInfo.Scenario will have the scenario information (SignIn vs CredentialPrompt).

			switch(args.StageInfo.Stage)
			{
			case SecondaryAuthenticationFactorAuthenticationStage.WaitingForUserConfirmation:
				// Show welcome message
				await SecondaryAuthenticationFactorAuthentication.ShowNotificationMessageAsync(
					null,
					SecondaryAuthenticationFactorAuthenticationMessage.WelcomeMessageSwipeUp);
				break;

			case SecondaryAuthenticationFactorAuthenticationStage.CollectingCredential:
				// Authenticate device
				Authenticate();
				break;

			case SecondaryAuthenticationFactorAuthenticationStage.CredentialAuthenticated:
				if (args.StageInfo.DeviceId = _deviceId)
				{
					// Show notification on device about PC unlock
				}
				break;

			case SecondaryAuthenticationFactorAuthenticationStage.StoppingAuthentication:
				// Quit from background task
				_exitTaskEvent.Set();
				break;
			}

			Debug.WriteLine("Authentication Stage = " + args.StageInfo.AuthenticationStage.ToString());
		}

		//
		// The Run method is the entry point of a background task.
		//
		public void Run(IBackgroundTaskInstance taskInstance)
		{
			_taskInstance = taskInstance;
			_deferral = taskInstance.GetDeferral();

			// Register canceled event for this task
			taskInstance.Canceled += TaskInstanceCanceled;

			// Find all device registred by this application
			IReadOnlyList<SecondaryAuthenticationFactorInfo> deviceInfoList =
				await SecondaryAuthenticationFactorRegistration.FindAllRegisteredDeviceInfoAsync(
					SecondaryAuthenticationFactorDeviceFindScope.AllUsers);

			if (deviceInfoList.Count == 0)
			{
				// Quit the task silently
				return;
			}
			_deviceId = deviceInfoList[0].DeviceId;
			Debug.WriteLine("Use first device '" + _deviceId + "' in the list to signin");

			// Register AuthenticationStageChanged event
			SecondaryAuthenticationFactorRegistration.AuthenticationStageChanged += OnAuthenticationStageChanged;

			// Wait the task exit event
			_exitTaskEvent.WaitOne();

			_deferral.Complete();
		}

		void TaskInstanceCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
		{
			_exitTaskEvent.Set();
		}
	}
}

注册后台任务

当 Windows Hello 配套设备应用注册第一个配套设备时,它还应注册其后台任务组件,而该组件可在设备与配套设备身份验证服务之间传递身份验证信息。

using System;
using Windows.Security.Authentication.Identity.Provider;
using Windows.Storage.Streams;
using Windows.ApplicationModel.Background;

namespace SecondaryAuthFactorSample
{
	public class BackgroundTaskManager
	{
		// Register background task
		public static async Task<IBackgroundTaskRegistration> GetOrRegisterBackgroundTaskAsync(
			string bgTaskName,
			string taskEntryPoint)
		{
			// Check if there's an existing background task already registered
			var bgTask = (from t in BackgroundTaskRegistration.AllTasks
						  where t.Value.Name.Equals(bgTaskName)
						  select t.Value).SingleOrDefault();
			if (bgTask == null)
			{
				BackgroundAccessStatus status =
					BackgroundExecutionManager.RequestAccessAsync().AsTask().GetAwaiter().GetResult();

				if (status == BackgroundAccessStatus.Denied)
				{
					Debug.WriteLine("Background Execution is denied.");
					return null;
				}

				var taskBuilder = new BackgroundTaskBuilder();
				taskBuilder.Name = bgTaskName;
				taskBuilder.TaskEntryPoint = taskEntryPoint;
				taskBuilder.SetTrigger(new SecondaryAuthenticationFactorAuthenticationTrigger());
				bgTask = taskBuilder.Register();
				// Background task is registered
			}

			bgTask.Completed += BgTask_Completed;
			bgTask.Progress += BgTask_Progress;

			return bgTask;
		}
	}
}

错误和消息

Windows Hello 配套设备框架负责向用户提供有关登录成功或失败的反馈。 Windows Hello 配套设备框架提供可供 Windows Hello 配套设备应用从中选择的一系列(本地化后的)文本和错误消息。 这些内容会显示在登录 UI 中。

配套设备错误

Windows Hello 配套设备应用可使用 ShowNotificationMessageAsync 向用户显示消息,以作为登录 UI 的一部分。 有意向信号时调用此 API。 请注意,必须始终在 Windows Hello 配套设备端收集意向信号。

有两种类型的消息:指导和错误。

指导消息旨在向用户展示如何启动解锁流程。 此类消息仅会在首次设备注册时在锁屏界面上向用户显示一次,此后便不再显示。 此类消息会继续显示在锁屏下。

错误消息始终会显示,并在提供意向信号后显示。 由于在向用户显示消息之前必须收集意向信号,且用户仅会使用其中一个 Windows Hello 配套设备来提供该意向,因此不存在多个 Windows Hello 配套设备争着显示错误消息的情况。 因此,Windows Hello 配套设备框架不会维护任何队列。 当调用方请求错误消息时,此消息将显示 5 秒钟,而在该 5 秒内请求显示错误消息的所有其他请求均会被删除。 5 秒后,其他调用方可请求显示错误消息。 我们禁止任何调用方干扰错误通道。

指导与错误消息如下所示。 设备名称是配套设备应用作为 ShowNotificationMessageAsync 的一部分所传递的参数。

指南

  • “向上轻扫或按空格键以使用 设备名称 登录。”
  • “正在设置配套设备。 请等待或使用其他登录选项。”
  • “点击连接到 NFC 读取器的 设备名称 以登录。”
  • “正在查找 设备名称...”
  • “将 设备名称 插入 USB 端口以登录。”

错误

  • “有关登录说明,请参阅 设备名称。”
  • “打开蓝牙以使用 设备名称 登录。”
  • “打开 NFC 以使用 设备名称 登录。”
  • “连接 Wi-Fi 网络以使用 设备名称 登录。”
  • “请再次点击 设备名称。”
  • “你的企业阻止使用 设备名称 登录。” 请使用其他登录选项。”
  • “点击 设备名称 以登录。”
  • “将手指放在 设备名称 上以登录。”
  • “在 设备名称 上轻摇手指以登录。”
  • “无法使用 设备名称 登录。 请使用其他登录选项。”
  • “出现错误。 请使用其他登录选项,然后再次设置 设备名称。“
  • ”请重试。“
  • “请向 设备名称 念出口头通行短语。“
  • “已可使用 设备名称 登录。”
  • “首先使用其他登录选项,然后便可使用 设备名称 登录。“

枚举已注册的设备

Windows Hello 配套设备应用可通过 FindAllRegisteredDeviceInfoAsync 调用来枚举已注册配套设备的列表。 此 API 支持通过枚举 SecondaryAuthenticationFactorDeviceFindScope 来定义的两种查询类型:

{
	User = 0,
	AllUsers,
}

第一个范围会返回已登录用户的设备列表。 第二个会返回该电脑上所有用户的列表。 第一个范围必须在注销时使用,以免注销其他用户的 Windows Hello 配套设备。 第二个则必须在身份验证或注册时使用:注册时,此枚举可帮助应用避免尝试两次注册同一 Windows Hello 配套设备。

请注意,即使应用不执行此检查,电脑也会执行该检查并拒绝多次注册同一 Windows Hello 配套设备。 身份验证时,使用 AllUsers 范围有助于 Windows Hello 配套设备应用支持切换用户流程:用户 B 已登录后,让用户 A 登录(它要求这两个用户均已安装 Windows Hello 配套设备应用,且用户 A 已在电脑中注册其配套设备,同时电脑正显示锁屏界面(或登录屏幕))。

安全要求

配套身份验证服务提供以下安全保护措施。

  • 作为中等用户或应用容器而运行的 Windows 10 桌面设备上的恶意软件无法使用 Windows Hello 配套设备并以无提示方式访问电脑上的用户凭据密钥(作为 Windows Hello 的一部分进行存储)。
  • Windows 10 桌面设备上的恶意用户无法使用属于该 Windows 10 桌面设备上的其他用户的 Windows Hello 配套设备并以无提示方式访问其用户凭据密钥(位于同一 Windows 10 桌面设备上)。
  • Windows Hello 配套设备上的恶意软件无法以无提示方式访问 Windows 10 桌面设备上的用户凭据密钥,其中包括利用专为 Windows Hello 配套设备框架而开发的功能或代码。
  • 恶意用户无法通过捕获 Windows Hello 配套设备与 Windows 10 桌面设备之间的流量并在后续重放这些流量来解锁 Windows 10 桌面设备。 在我们的协议中使用 nonce、authkey 和 HMAC 可保证防止重放攻击。
  • 异常电脑上的恶意软件或恶意用户无法使用 Windows Hello 配套设备访问无恶意的用户电脑。 通过在我们的协议中使用 authkey 和 HMAC 以便在配套身份验证服务与 Windows Hello 配套设备之间进行相互身份验证,即可实现此目的。

实现上述安全保护措施的关键在于,保护 HMAC 密钥免受未经授权的访问以及验证用户是否存在。 更具体而言,它必须满足以下要求:

  • 提供保护,以免克隆 Windows Hello 配套设备
  • 注册期间,向电脑发送 HMAC 密钥时提供保护以免窃听
  • 确保用户存在信号可用