65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Windows 应用商店应用中使用智能卡

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2013 年 2 月 5 日

CPOL

8分钟阅读

viewsIcon

33635

downloadIcon

1753

本文介绍了一种访问 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 应用商店应用访问智能卡的唯一方法。

图 1:组件交互图

连接到 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 组件及其 APDUCommandAPDUResponse 组件,一个视图(即主页),以及一个用于管理视图的 ViewModel。视图没有代码隐藏。

为了支持命令,我从一个不错的 MVVM 框架中提取了一些类,该框架可以在 Github 上获得。

图 2:ExchangeAPDU 应用,在 SIM 卡上选择 MF (3F00) 

数据字节文本框的筛选器 

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 上更新。

© . All rights reserved.