在 Windows 应用商店应用中使用智能卡
本文介绍了一种访问 WinRT 中不可用的 API 和资源的方法。
介绍
不久前,我曾尝试将我的 .NET 智能卡框架移植到 C++ 的 WinRT 组件。由于我在智能卡技术和 COM 组件方面都有一些经验,因此我认为这是一个有趣的练习。
因此,我使用 C++/CX 创建了一个简单的 WinRT 组件项目,以允许 Windows 应用商店应用程序访问智能卡功能。在 C++ 应用程序中,您可以包含 winscard.h,这样就可以访问 PC/SC API 与卡进行通信。不幸的是,如果您在 Windows 应用商店项目中使用此方法,所有智能卡 API 定义都会被 Microsoft 禁用。
我从 Microsoft 得到了关于我在 .h 源文件中发现的内容的确认。出于我并不真正理解的原因,在 Windows 应用商店应用程序或库中,根本无法使用标准的 PC/SC API 访问智能卡。
但是,我有点固执,在尝试做某事时不会轻易放弃。Windows 8 仍然可以在所谓的 Windows 桌面模式下运行旧版 Windows 应用程序。我完全可以在 Windows 8 的桌面模式下运行我的 .NET 智能卡框架应用程序,因此我有一个简单的想法。如果我可以在桌面模式下运行一个 WCF 服务,并通过 WinRT 组件使用这个 WCF 服务,那么我就有了一个解决方案来构建 Windows 应用商店应用的智能卡组件……到目前为止,Microsoft 还没有禁用 WinRT 中的 WCF,因为访问云仍然非常需要它!
在之前的一篇文章中,我介绍了我 .NET 智能卡框架 的 WCF 包装器。这个练习对于 Windows 7 或 XP 来说意义不大,因为 PC/SC 支持智能卡,但它目前是从 Windows 应用商店应用访问智能卡的唯一方法。
连接到 WCF 智能卡服务的 WinRT 组件
智能卡是一种相对慢的设备,因此 WCF 服务引入的开销几乎无法察觉,特别是使用 Net TCP 或 Named Pipe 等二进制绑定时。WCF 智能卡服务在之前的文章中已有介绍。它托管在一个需要系统启动时才能启动的 Windows 窗体应用程序中。Windows 7 和 Windows 8 在 Windows 服务中运行时对智能卡访问有限制,因此需要将其托管在 Windows 应用程序中,最终也可以是控制台应用程序。
该服务有一个 **MEX** 端点,因此很容易创建客户端组件。对于 WinRT 组件,我选择了 NetTcp 绑定,因为在导入引用时,它会创建一切,您只需调用一个无参数的构造函数即可实例化 WCF 客户端。如果您想使用 NamedPipe
绑定,则需要使用绑定和终结点地址来实例化 WCF 客户端。我在控制台测试应用程序中测试了这两种绑定,未能注意到这两种绑定之间存在任何性能差异。
WinRT 包装器的代码如下:
/// <summary> /// This is a simple component to manage a smartcard /// /// Method are made synchronous because the service on the local machine and a smartcard /// although not very fast is a quite responsive device.. /// </summary> public sealed class Smartcard { private SCardService.RemoteCardClient cardClient = null; private const int TIMEOUT = 10000; public Smartcard() { // Create an instance of the cardClient = new SCardService.RemoteCardClient(); } /// <summary> /// Gets the list of readers /// /// REM: This method is not really at its place and should be in a seperate /// component. Maybe later if I have some time /// </summary> /// <returns>A string array of the readers</returns> public string[] ListReaders() { Task<ObservableCollection<string>> readers = cardClient.ListReadersAsync(); try { return readers.Result.ToArray(); } catch (AggregateException ax) { throw new Exception(ProcessAggregateException(ax)); } } /// <summary> /// Connects to a card. Establishes a card session /// </summary> /// <param name="Reader">Reader string</param> /// <param name="ShareMode">Session share mode</param> /// <param name="PreferredProtocols">Session preferred protocol</param> public void Connect(string reader, SHARE shareMode, PROTOCOL preferredProtocols) { try { cardClient.ConnectAsync(reader, (SCardService.SHARE)shareMode, (SCardService.PROTOCOL)preferredProtocols).Wait(TIMEOUT); } catch (AggregateException ax) { throw new Exception(ProcessAggregateException(ax)); } } /// <summary> /// Disconnect the current session /// </summary> /// <param name="Disposition">Action when disconnecting from the card</param> public void Disconnect(DISCONNECT disposition) { try { cardClient.DisconnectAsync((SCardService.DISCONNECT)disposition).Wait(TIMEOUT); } catch (AggregateException ax) { throw new Exception(ProcessAggregateException(ax)); } } /// <summary> /// Transmit an APDU command to the card /// </summary> /// <param name="ApduCmd">APDU Command to send to the card</param> /// <returns>An APDU Response from the card</returns> public APDUResponse Transmit(APDUCommand apduCmd) { Task<SCardService.APDUResponse> task = cardClient.TransmitAsync( new SCardService.APDUCommand() { Class = apduCmd.Class, Ins = apduCmd.Ins, P1 = apduCmd.P1, P2 = apduCmd.P2, Le = apduCmd.Le, Data = apduCmd.Data }); try { SCardService.APDUResponse resp = task.Result; return new APDUResponse() { SW1 = resp.SW1, SW2 = resp.SW2, Data = resp.Data }; } catch (AggregateException ax) { throw new Exception(ProcessAggregateException(ax)); } } /// <summary> /// Begins a card transaction /// </summary> public void BeginTransaction() { try { cardClient.BeginTransactionAsync().Wait(TIMEOUT); } catch (AggregateException ax) { throw new Exception(ProcessAggregateException(ax)); } } /// <summary> /// Ends a card transaction /// </summary> public void EndTransaction(DISCONNECT disposition) { try { cardClient.EndTransactionAsync((SCardService.DISCONNECT)disposition).Wait(TIMEOUT); } catch (AggregateException ax) { throw new Exception(ProcessAggregateException(ax)); } } /// <summary> /// Gets the attributes of the card /// /// This command can be used to get the Answer to reset /// </summary> /// <param name="AttribId">Identifier for the Attribute to get</param> /// <returns>Attribute content</returns> public byte[] GetAttribute(UInt32 attribId) { Task<byte[]> task = cardClient.GetAttributeAsync(attribId); try { return task.Result; } catch (AggregateException ax) { throw new Exception(ProcessAggregateException(ax)); } } /// <summary> /// This method extract the error message carried by the WCF fault /// /// Supports SmarcardFault and GeneralFault /// </summary> /// <param name="ax">AggregateException object</param> /// <returns>Extracted message</returns> private static string ProcessAggregateException(AggregateException ax) { string msg = "Unknown fault"; FaultException<SCardService.SmartcardFault> scFault = ax.InnerException as FaultException<SCardService.SmartcardFault>; if (scFault != null) { msg = scFault.Detail.Message; } else { FaultException<SCardService.GeneralFault> exFault = ax.InnerException as FaultException<SCardService.GeneralFault>; if (exFault != null) { msg = exFault.Detail.Message; } } return msg; } }
Smartcard
类是一个用 C# 编写的 WinRT 组件,它只是包装了 WCF 智能卡服务客户端。要创建生成的包装器,我只是在 Windows 桌面模式下运行了 WCF 服务,因为它公开了一个 MEX 端点,我可以让 Visual Studio 生成代理。我也可以直接在 Windows 应用商店应用程序中使用 WCF 服务,但创建一个执行工作的组件会更好,因为它可以在不同应用程序中重用。
当您将 WCF 服务导入 WinRT 组件或 Windows 应用商店应用时,您只会得到方法的异步版本。在这个第一个版本中,我只提供了同步方法,因为服务运行在本地机器上,而智能卡是一个响应速度很快的设备。但是,如果我有时间,并且作为一项练习,我将实现一些方法的异步版本(这应该相当直接)。
如果您查看最复杂的 Transmit
方法,它基本上有两个操作:
- 调用异步方法
TransmitAsync
- 调用正在运行服务方法的生成的 Task<> 上的 Result 方法
由于方法调用在单独的线程中运行,如果您的服务可以抛出 FaultException
,则无法简单地通过 catch(FaultException<SmartcardException> ex)
等方式捕获此异常。幸运的是,WinRT 提供了 AggregateException
,可以通过在异步调用周围使用 catch
来捕获,并且它包含对线程中可能发生的异常的引用。
因此,具有异常处理的异步调用代码如下所示:
public byte[] GetAttribute(UInt32 attribId) { Task<byte[]> task = cardClient.GetAttributeAsync(attribId); try { return task.Result; } catch (AggregateException ax) { throw new Exception(ProcessAggregateException(ax)); } }
AggregateException
实例包含一个 InnerException
成员,该成员包含中断线程的主异常。在我们的例子中,它应该包含服务实现所支持的 FaultException
之一。ProcessAggregateException
方法提取预期的 FaultException
并返回一条消息,说明发生了什么。这是一个简化的实现,我的目标只是避免在服务调用中发生异常时出现应用程序意外终止。
真正的应用程序应该区分可恢复的异常和终止与 WCF 服务通信的异常,后者需要应用程序重新连接到 WCF 服务。
演示应用程序:一个 Windows 应用商店 ExchangeAPDU 应用
在 .NET 智能卡框架文章中,我发布了一个简单的 ExchangeAPDU 演示应用程序,用于向智能卡发送低级命令。我选择将该应用程序简单地移植到 Windows 应用商店。然而,这是一个更简单的版本,因为 WCF 服务尚不支持卡事件。
我使用 C# 编写了此应用程序,因为此类演示应用程序没有性能问题需要担心。C# 中的 Windows 应用商店应用使用类似于 WPF 的技术,因此我的简单应用程序 UI 包含一个模型,即 Smartcard
组件及其 APDUCommand
和 APDUResponse
组件,一个视图(即主页),以及一个用于管理视图的 ViewModel。视图没有代码隐藏。
为了支持命令,我从一个不错的 MVVM 框架中提取了一些类,该框架可以在 Github 上获得。
数据字节文本框的筛选器
Windows 应用商店应用对 XAML 的支持似乎不像最新版本的 WPF 那样完整。为了给不同的 TextBox
控件添加字符过滤功能,我无法使用标准的 WPF 功能。
- 不支持
PreviewTextInput
- 不支持
UpdateSourceTrigger
这些是支持过滤的两个问题。没有 UpdateSourceTrigger
,就无法选择何时在 TextBox
控件中输入字符时更新源数据。没有 PreviewTextInput
,就无法在显示的文本之前获取输入的文本。
我能找到的唯一事件是 KeyDown
,但它获取输入 VirtualKey
的值,并且它返回的值对于数字不精确。例如,在法式键盘上,如果您不按 Shift 键,按下“6”会得到“-”,但对于这两个字符,您都会得到 VirtualKey.Number6
。结果是某些不必要的字符未被过滤。
没有 UpdateSourceTrigger
,就无法更改源的更新方式,因此默认情况下,它会在控件失去焦点时更新。由于我希望在输入文本时更新数据,因此我通常会使用 UpdateSourceTrigger=PropertyChanged
。
我在论坛上搜索了一下,找到了一个解决方法来弥补 UpdateSourceTrigger
的不足。它是一个自定义行为,可以应用于 TextBox
,以便在输入字符时更新源。
public class TextBoxUpdateSourceBehaviour { private static Dictionary<TextBox, PropertyInfo> _boundProperties = new Dictionary<TextBox, PropertyInfo>(); public static readonly DependencyProperty BindingSourceProperty = DependencyProperty.RegisterAttached( "BindingSource", typeof(string), typeof(TextBoxUpdateSourceBehaviour), new PropertyMetadata(default(string), OnBindingChanged)); public static void SetBindingSource(TextBox element, string value) { element.SetValue(BindingSourceProperty, value); } public static string GetBindingSource(TextBox element) { return (string)element.GetValue(BindingSourceProperty); } private static void OnBindingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var txtBox = d as TextBox; if (txtBox != null) { txtBox.Loaded += OnLoaded; txtBox.TextChanged += OnTextChanged; } } static void OnLoaded(object sender, RoutedEventArgs e) { var txtBox = sender as TextBox; if (txtBox != null) { // Reflect the datacontext of the textbox to find the field to bind to. if (txtBox.DataContext != null) // DataContext is null in the designer { var dataContextType = txtBox.DataContext.GetType(); AddToBoundPropertyDictionary(txtBox, dataContextType.GetRuntimeProperty(GetBindingSource(txtBox))); } } } static void AddToBoundPropertyDictionary(TextBox txtBox, PropertyInfo boundProperty) { PropertyInfo propInfo; if (!_boundProperties.TryGetValue(txtBox, out propInfo)) { _boundProperties.Add(txtBox, boundProperty); } } static void OnTextChanged(object sender, TextChangedEventArgs e) { var txtBox = sender as TextBox; if (txtBox != null) { _boundProperties[txtBox].SetValue(txtBox.DataContext, txtBox.Text); } } }
要使用此行为,您需要在 XAML 中设置命名空间,例如 xmlns:util="using:Core.Wpf"
,然后在 TextBox XAML 中必须添加以下属性: util:TextBoxUpdateSourceBehaviour.BindingSource="Class"
获取源
您可以从本文附加的 ZIP 文件中获取这些项目的源代码,或者在 Github 上关注这些项目,因为它们将作为公共存储库定期更新。
.NET 智能卡库:https://github.com/orouit/SmartcardFramework.git
WinRT 组件和演示应用:https://github.com/orouit/SmartcardWithWindowsStore.git
兴趣点
正如我在上一篇文章中所提到的,WCF 服务是从 Windows 应用商店应用访问智能卡资源的解决方案。这个演示应用程序证明了这一点!
实际上,这种技术可以应用于任何 Windows API 不再可用于 Windows 应用商店应用程序的情况。但是,我只建议在您的应用程序不是通过 Windows 应用商店分发的企业环境中使用此技术。我没有检查应用提交工具包,但我怀疑使用 WCF 服务访问 Windows 不受支持的功能的应用程序可能不会被接受。
历史
- 2013/10/2:UI 已更新为十六进制过滤。代码已在 Github 上更新。