Click here to Skip to main content
Click here to Skip to main content
Add your own
alternative version

A Smart Card Framework for .NET

, 20 Feb 2013
Describes a framework to use the PCSC Smart Card API with .NET.
smartcardapi_demo.zip
smartcardapi_doc.zip
smartcardapi_doc
banner.jpg
darkcorner.jpg
GemCard
gradleft.jpg
gradtop.jpg
graycorner.jpg
minus.jpg
plus.jpg
titletile.jpg
smartcardapi_source.zip
Smartcard_API
DemoSCFmwk
Properties
GemCardEx
GemCardEx.aps
GemCardEx.def
GemCardEx.rgs
GemCardEx.suo
GemCardEx.vcproj.CORUSCANT.han.user
GemCardEx.vcproj.vspscc
GemCardExps.def
Release
GemCardEx.dll
SCardDatabaseEx.rgs
Smartcard Framework 2005.suo
GemCard
GemCard.csproj.user
SmartcardFrameworkWithWCFSCardService.zip
SmartcardFramework
DemoSCardService
Properties
Service References
SCardNPService
configuration.svcinfo
configuration91.svcinfo
DemoSCardService.SCardNPService.APDUResponse.datasource
Reference.svcmap
service.wsdl
SCardService
configuration.svcinfo
configuration91.svcinfo
DemoSCardService.SCardService.APDUResponse.datasource
Reference.svcmap
service.wsdl
SCardServiceHost
Properties
Settings.settings
Smartcard Framework 2010.suo
Smartcard Framework 2010.v11.suo
Smartcard_API
DemoSCFmwk
Properties
GemCard
GemCard.csproj.user
SmartCardPlayer
Properties
Smartcard_Test
ApduExchange
ApduExchange.csproj.user
App.ico
Thumbs.db
GSMHelper
Properties
ReadPhonebook
Properties
Settings.settings
ReadPhonebook.csproj.user
SmartcardService
Properties
SmartcardService.csproj.user
Smartcard_Framework_2010.zip
Smartcard_Framework_2010
Smartcard Framework 2010.suo
Smartcard_API
DemoSCFmwk
DemoSCFmwk.csproj.user
Properties
GemCard
GemCard.csproj.user
SmartCardPlayer
Properties
Smartcard_Test
ApduExchange
ApduExchange.csproj.user
App.ico
GSMHelper
Properties
ReadPhonebook
Properties
Settings.settings
ReadPhonebook.csproj.user
Smartcard_Framework_64.zip
Smartcard_Framework
Smartcard_API
DemoSCFmwk
Properties
GemCard
GemCard.csproj.user
SmartCardPlayer
Properties
Smartcard_Test
ApduExchange
ApduExchange.csproj.user
App.ico
GSMHelper
Properties
ReadPhonebook
Properties
Settings.settings
ReadPhonebook.csproj.user
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Xml;

using GemCard;

namespace SmartCardPlayer
{
    /// <summary>
    /// List of sequence name. It is just a Dictionary of string indexed by string
    /// </summary>
    public class SequenceParameter : Dictionary<string, string> {
    }


	/// <summary>
	/// This class provides a set of functions to process APDU commands described in
	/// an XML format
	/// 
	/// The format is the following
	/// 
	/// <CommandList>
	///		<Apdu Name="VerifyCHV" Class="A0" Ins="20" P1="0" P2="1" P3="8" Data="31323334FFFFFFFF" />
	///		<Apdu Name="Get Response" Class="A0" Ins="C0" P1="0" P2="0" P3="SW2" Data="" />
	///		<Apdu Name="Select 6F3A" Class="A0" Ins="A4" P1="0" P2="0" P3="2" Data="6F3A" />
	///		<Apdu Name="Read Record" Class="A0" Ins="B2" P1="1" P2="4" P3="D15" Data="" />
	///	</CommandList>
	/// </summary>
	public class APDUPlayer
	{
		private const	string	
			xmlNodeApdu = "Apdu",
            xmlNodeSequence = "Sequence",
            xmlNodeCommand = "Command",
            xmlAttrApdu = "Apdu",
            xmlAttrSequence = "Sequence",
			xmlAttrName = "Name",
			xmlAttrClass = "Class",
			xmlAttrIns = "Ins",
			xmlAttrP1 = "P1",
			xmlAttrP2 = "P2",
			xmAttrLe = "Le",
			xmlAttrLc = "Lc",
			xmlAttrData = "Data";

		private	const	string
			paramSW1 = "SW1",
			paramSW2 = "SW2";

		private	bool	m_bLeSW2 = false;
        private bool    m_bLeData = false;
        private short   m_nDataId = -1;
		private	byte	m_bSW1Cond = 0;
		private	bool	m_bCheckSW1 = false;
        private bool    m_bReplay = false;

        private XmlNodeList m_xmlApduList = null;
        private XmlNodeList m_xmlSequenceList = null;
		private	ICard		m_iCard = null;
		private	APDUResponse	m_apduResp = null;

        private APDULogList m_logList = new APDULogList();

		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="apduFileName">XML file name with the description of the commands</param>
		/// <param name="iCard">Card interface to process the APDUs</param>
		public APDUPlayer(string apduFileName, ICard iCard)
		{
			XmlDocument	apduDoc = new XmlDocument();

			try
			{
				// Load the document
				apduDoc.Load(apduFileName);

				// Get the list of APDUs
				m_xmlApduList = apduDoc.GetElementsByTagName(xmlNodeApdu);	

                // Get the list of sequences
                m_xmlSequenceList = apduDoc.GetElementsByTagName(xmlNodeSequence);

				m_iCard = iCard;
			}
			catch
			{
                throw new ApduCommandException(ApduCommandException.NotValidDocument);
			}
		}

        public APDULogList Log
        {
            get
            {
                return m_logList;
            }
        }


        /// <summary>
        /// Constructs an APDU player
        /// </summary>
        /// <param name="iCard">ICard interface to the Smartcard</param>
        public APDUPlayer(ICard iCard)
        {
            m_iCard = iCard;
        }


        /// <summary>
        /// Constructs an APDU player
        /// </summary>
        /// <param name="apduFileName">APDU file name</param>
        /// <param name="seqFileName">Sequence file name</param>
        /// <param name="iCard">ICard interface to the Smartcard</param>
        public APDUPlayer(string apduFileName, string seqFileName, ICard iCard)
        {
            LoadAPDUFile(apduFileName);
            LoadSequenceFile(seqFileName);

            m_iCard = iCard;
        }


        /// <summary>
        /// Loads a Sequence file
        /// </summary>
        /// <param name="fileName">Sequence file name</param>
        public void LoadAPDUFile(string fileName)
        {
            XmlDocument apduDoc = new XmlDocument();

            try
            {
                // Load the document
                apduDoc.Load(fileName);

                // Get the list of sequences
                m_xmlApduList = apduDoc.GetElementsByTagName(xmlNodeApdu);
            }
            catch
            {
                throw new ApduCommandException(ApduCommandException.NotValidDocument);
            }
        }


        /// <summary>
        /// Loads an APDU file
        /// </summary>
        /// <param name="fileName">APDU file name</param>
        public void LoadSequenceFile(string fileName)
        {
            XmlDocument apduDoc = new XmlDocument();

            try
            {
                // Load the document
                apduDoc.Load(fileName);

                // Get the list of sequences
                m_xmlSequenceList = apduDoc.GetElementsByTagName(xmlNodeSequence);
            }
            catch
            {
                throw new ApduCommandException(ApduCommandException.NotValidDocument);
            }
        }


		/// <summary>
		/// APDUNames property, gets a list of the APDU Names
		/// </summary>
		public	string[] APDUNames
		{
			get
			{
				string[] apduNames = null;
				int	nCount = m_xmlApduList.Count;

				if (nCount != 0)
				{
					apduNames = new string[nCount];
					for (int nI = 0; nI < nCount; nI++)
					{
						XmlNode apdu = m_xmlApduList.Item(nI);

						apduNames[nI] = apdu.Attributes[xmlAttrName].Value;
					}
				}

				return apduNames;
			}
		}


        /// <summary>
        /// Process a simple APDU command, Parameters can be provided in the APDUParam object
        /// </summary>
        /// <param name="command">APDU command name</param>
        /// <param name="apduParam">Parameters for the command</param>
        /// <returns>An APDUResponse object with the response of the card </returns>
        public APDUResponse ProcessCommand(string apduName, APDUParam apduParam)
		{
			APDUCommand		apduCmd = null;
		
			// Get the base APDU
			apduCmd = APDUByName(apduName);
			if (apduCmd == null)
                throw new ApduCommandException(ApduCommandException.NoSuchCommand);

			apduCmd.Update(apduParam);
			
			return ExecuteApduCommand(apduCmd);
		}


		/// <summary>
		/// Process a simple APDU command, all parameters are included in the 
		/// XML description
		/// </summary>
		/// <param name="command">APDU command name</param>
		/// <returns>An APDUResponse object with the response of the card </returns>
		public	APDUResponse	ProcessCommand(string apduName)
		{
			APDUCommand		apduCmd = null;
		
			apduCmd = APDUByName(apduName);
			if (apduCmd == null)
                throw new ApduCommandException(ApduCommandException.NoSuchCommand);

			return ExecuteApduCommand(apduCmd);
		}


        /// <summary>
        /// Process an APDU sequence and execute each of its commands in the sequence order
        /// </summary>
        /// <param name="apduSequenceName">Name of the sequence to play</param>
        /// <returns>APDUResponse object of the last commad executed</returns>
        public APDUResponse ProcessSequence(string apduSequenceName)
        {
            return ProcessSequence(apduSequenceName, null);
        }


        /// <summary>
        /// Process an APDU sequence and execute each of its commands in the sequence order
        /// </summary>
        /// <param name="apduSequenceName">Name of the sequence to play</param>
        /// <param name="seqParam">An array of SequenceParam object used as parameters for the sequence</param>
        /// <returns>APDUResponse object of the last command executed</returns>
        //public APDUResponse ProcessSequence(string apduSequenceName,  Dictionary<string, string> seqParam)
        public APDUResponse ProcessSequence(string apduSequenceName, SequenceParameter seqParam)
        {
            APDUResponse apduResp = null;
            //Dictionary<string, string> l_seqParam = null;
            SequenceParameter l_seqParam = null;

            // Get the sequence
            XmlNode apduSeq = SequenceByName(apduSequenceName);

            if (apduSeq == null)
                throw new ApduCommandException(ApduCommandException.NoSuchSequence);

            // Process the params of the sequence
            l_seqParam = ProcessParams(seqParam, apduSeq.Attributes);

            // Get the list of commands to execute
            XmlNodeList xmlCmdList = apduSeq.ChildNodes;
            for (int nI = 0; nI < xmlCmdList.Count; nI++)
                apduResp = ProcessSeqCmd(xmlCmdList.Item(nI), l_seqParam);

            return apduResp;
        }


        /// <summary>
        /// Gets an APDU command by name
        /// </summary>
        /// <param name="apduName">APDU name</param>
        /// <returns>An APDUCommand object, null if the command was not found</returns>
        public APDUCommand APDUByName(string apduName)
        {
            APDUCommand apduCmd = null;

            if (m_xmlApduList != null)
            {
                for (int nI = 0; nI < m_xmlApduList.Count; nI++)
                {
                    XmlNode apdu = m_xmlApduList.Item(nI);

                    string sName = apdu.Attributes[xmlAttrName].Value;
                    if (sName == apduName)
                    {
                        apduCmd = APDUFromXml(apdu);
                        break;
                    }
                }
            }

            return apduCmd;
        }

        #region Private methods


		/// <summary>
		/// Builds an APDUCommand object from an XmlNode representing the command.
		/// 
		/// This command uses the result of a previous command to fill some paramaters
        /// P3 in this case represents the length expected by the command
		/// P3 = "R,0:SW1?xx"
        ///     m_bReplay is set to true
		///		m_bSW1Cond is set with the value xx
		///		m_bCheckSW1 is set to true
		///		When the command is called, if SW1 == xx, the command is played a 
		///		second time with Le = resp.SW2
		///
        /// P3 = "R,xx:DRyy"
        ///     m_bReplay is set to true
        ///     m_bLeData is set to true
        ///     m_nDataId is set to yy
        ///     Le is set to xx for the first call of the command
        ///     Then the command is replayed with Le = Le + resp.Data[m_nDataId - 1]
        /// 
		///	P3 = "SW2"
		///		if SW1 == 0x9F on the previous call to a command, m_bLeSW2 is set to true
		///		if m_bLeSW2 if true, Le is replaced with resp.SW2
		///		
		///	P3 = "Dxx"
		///		if resp.Data id not null from the previous command,
		///		xx is used as the index of the byte that gives Le in the response data
		///		Le = Data[xx]
		/// </summary>
		/// <param name="xmlApdu">XML representation of the command</param>
		/// <returns>An APDUCommand object build from the XML data</returns>
        private APDUCommand APDUFromXml(XmlNode xmlApdu)
        {
            XmlElement apduElt = (XmlElement)xmlApdu;

            m_bLeData = false;
            m_bCheckSW1 = false;
            m_bReplay = false;

            // Get command detail
            string sClass = (string)xmlApdu.Attributes["Class"].Value;
            string sIns = (string)xmlApdu.Attributes["Ins"].Value;
            string sP1 = (string)xmlApdu.Attributes["P1"].Value;
            string sP2 = (string)xmlApdu.Attributes["P2"].Value;
            string sP3 = (string)xmlApdu.Attributes["P3"].Value;

            sP3 = sP3.ToUpper();
            string sData = apduElt.GetAttribute("Data");

            byte bP1 = 0;
            byte bP2 = 0;
            byte bP3 = 0;
            byte bLe = 0;
            byte bClass = byte.Parse(sClass, NumberStyles.AllowHexSpecifier);
            byte bIns = byte.Parse(sIns, NumberStyles.AllowHexSpecifier);
            if (sP1 != "" && sP1 != "@")
                bP1 = byte.Parse(sP1, NumberStyles.AllowHexSpecifier);
            if (sP2 != "" && sP2 != "@")
                bP2 = byte.Parse(sP2, NumberStyles.AllowHexSpecifier);

            int nId = 0;
            int nId2 = 0;

            // Process P3 parameter
            if (sP3.IndexOf("DR") != -1)
            {
                // Use data byte of previous command, index value follows D
                try
                {
                    int nIdx = int.Parse(sP3.Substring(sP3.IndexOf("DR") + 2));
                    if ((m_apduResp != null) && (m_apduResp.Data != null))
                        bLe = m_apduResp.Data[nIdx - 1];
                    else
                        bLe = 0;
                }
                catch
                {
                    throw new ApduCommandException(ApduCommandException.ParamP3Format);
                }
            }
            else if (sP3.IndexOf("DL") != -1)
            {
                bP3 = (byte)(sData.Length / 2);
                bLe = 0;
            }
            else if (sP3.IndexOf(paramSW2) != -1)
            {
                if (m_bLeSW2)
                {
                    // Use SW2 parameter of previous command
                    bLe = m_apduResp.SW2;
                }
                else
                    bLe = 0;
            }
            else if ((nId = sP3.IndexOf('R')) == 0)
            {
                m_bReplay = true;
                if (sP3[++nId] == ',')
                {
                    nId2 = sP3.IndexOf(':');
                    if (nId2 != -1)
                    {
                        bLe = byte.Parse(sP3.Substring(nId + 1, nId2 - nId - 1));
                    }
                    else
                        throw new ApduCommandException(ApduCommandException.ParamP3Format);
                }
                else if (sP3[nId] == ':')
                {
                    bLe = 0;
                }

                if (sP3.IndexOf(paramSW1, nId) != -1)
                {
                    if ((nId2 = sP3.IndexOf('?', nId)) != -1)
                    {
                        m_bCheckSW1 = true;
                        m_bSW1Cond = byte.Parse(sP3.Substring(nId2 + 1), NumberStyles.AllowHexSpecifier);
                    }
                    else
                        throw new ApduCommandException(ApduCommandException.ParamP3Format);
                }
                else if ((nId2 = sP3.IndexOf("DR", nId)) != -1)
                {
                    m_bLeData = true;
                    m_nDataId = short.Parse(sP3.Substring(nId2 + 2));
                }
                else
                    throw new ApduCommandException(ApduCommandException.ParamP3Format);
            }
            else if (sP3 != "")
            {
                bP3 = byte.Parse(sP3);
                if (sData.Length == 0)
                    bLe = bP3;
            }

            byte[] baData = null;
            if (bP3 != 0 && sData.Length != 0)
            {
                baData = new byte[bP3];
                for (int nJ = 0; nJ < sData.Length; nJ += 2)
                    baData[nJ / 2] = byte.Parse(sData.Substring(nJ, 2), NumberStyles.AllowHexSpecifier);
                bLe = 0;
            }

            return new APDUCommand(bClass, bIns, bP1, bP2, baData, bLe);
        }


        /// <summary>
        /// Executes an APDU command
        /// </summary>
        /// <param name="apduCmd">APDUCommand object to execute</param>
        /// <returns>APDUResponse object of the response</returns>
        private APDUResponse ExecuteApduCommand(APDUCommand apduCmd)
        {
            byte bLe = 0;

            // Send the command
            m_apduResp = m_iCard.Transmit(apduCmd);
            AddLog(new APDULog(apduCmd, m_apduResp));

            // Check if SW2 can be used as Le for the next call
            if (m_apduResp.SW1 == 0x9F)
                m_bLeSW2 = true;
            else
                m_bLeSW2 = false;

            if (m_bReplay)
            {
                if (m_bCheckSW1 && (m_apduResp.SW1 == m_bSW1Cond))
                {
                    // Replay the command with Le = SW2 of response
                    bLe = m_apduResp.SW2;
                    m_bCheckSW1 = false;
                }
                else if (m_bLeData)
                {
                    // Replay the command with Le = Le + Data[m_nDataId - 1] of response
                    bLe = (byte)(m_apduResp.Data[m_nDataId - 1] + apduCmd.Le);
                    m_bLeData = false;
                }

                // Replay the command
                apduCmd = new APDUCommand(
                    apduCmd.Class,
                    apduCmd.Ins,
                    apduCmd.P1,
                    apduCmd.P2,
                    apduCmd.Data,
                    bLe);

                m_apduResp = m_iCard.Transmit(apduCmd);
                AddLog(new APDULog(apduCmd, m_apduResp));

                m_bReplay = false;
            }

            return m_apduResp;
        }


        /// <summary>
        /// Gets the XML node for a Sequence of APDUs
        /// </summary>
        /// <param name="name">Name of the sequence</param>
        /// <returns>XmlNode of the sequence</returns>
        private XmlNode SequenceByName(string name)
        {
            XmlNode apduSeq = null;

            if (m_xmlSequenceList != null)
            {
                for (int nI = 0; nI < m_xmlSequenceList.Count; nI++)
                {
                    apduSeq = m_xmlSequenceList.Item(nI);

                    string sName = apduSeq.Attributes[xmlAttrName].Value;
                    if (sName == name)
                        break;
                    else
                        apduSeq = null;
                }
            }

            return apduSeq;
        }


        /// <summary>
        /// Process the parameters of a Sequence. If a parameter of the same name is found in the list of parameters
        /// it overrides the XML parameter of the sequence
        /// </summary>
        /// <param name="seqParam">List of parameters</param>
        /// <param name="xmlSeqParam">Parameters of the XML sequence</param>
        /// <returns>The list of parameters to used to process the sequence of APDU commands</returns>
//        private Dictionary<string, string> ProcessParams(Dictionary<string, string> seqParam, XmlAttributeCollection xmlSeqParam)
        private SequenceParameter ProcessParams(SequenceParameter seqParam, XmlAttributeCollection xmlSeqParam)
        {
            //Dictionary<string, string> l_seqParam = new Dictionary<string, string>();
            SequenceParameter l_seqParam = new SequenceParameter();

            int nNbParam = xmlSeqParam.Count;

            for (int nI = 0; nI < nNbParam; nI++)
            {
                XmlNode xNode = xmlSeqParam.Item(nI);

                string name = xNode.Name;
                string val = xNode.Value;

                // Check if a val overrides the XML parameter of Sequence
                if (seqParam != null)
                {
                    try
                    {
                        val = seqParam[name];
                    }
                    catch
                    {
                    }
                }

                l_seqParam.Add(name, val);
            }

            return l_seqParam;
        }


        /// <summary>
        /// Builds an APDUParam object from the parameters of a command and a set of parameter for a sequence
        /// </summary>
        /// <param name="xmlAttrs">List of parameters of the APDU</param>
        /// <param name="seqParam">List of parameters of the sequence</param>
        /// <returns>APDUParam object</returns>
//        private APDUParam BuildCommandParam(XmlAttributeCollection xmlAttrs, Dictionary<string, string> seqParam)
        private APDUParam BuildCommandParam(XmlAttributeCollection xmlAttrs, SequenceParameter seqParam)
        {
            APDUParam apduParam = null;
            byte[] baData = null;
            string sVal = null;

            apduParam = new APDUParam();

            for (int nI = 0; nI < xmlAttrs.Count; nI++)
            {
                XmlNode xmlParam = xmlAttrs.Item(nI);
                switch (xmlParam.Name)
                {
                    case xmlAttrP1:
                    {
                        try
                        {
                            sVal = seqParam[xmlParam.Value];
                        }
                        catch
                        {
                            sVal = xmlParam.Value;
                        }
                        finally
                        {
                            apduParam.P1 = byte.Parse(sVal, NumberStyles.AllowHexSpecifier);
                        }
                        break;
                    }

                    case xmlAttrP2:
                    {
                        try
                        {
                            sVal = seqParam[xmlParam.Value];
                        }
                        catch
                        {
                            sVal = xmlParam.Value;
                        }
                        finally
                        {
                            apduParam.P2 = byte.Parse(sVal, NumberStyles.AllowHexSpecifier);
                        }
                        break;
                    }

                    case xmlAttrData:
                    {
                        try
                        {
                            sVal = seqParam[xmlParam.Value];
                        }
                        catch
                        {
                            sVal = xmlParam.Value;
                        }
                        finally
                        {
                            int nLen = sVal.Length / 2;
                            if (nLen != 0)
                            {
                                baData = new byte[nLen];
                                for (int nJ = 0; nJ < nLen; nJ++)
                                    baData[nJ] = byte.Parse(sVal.Substring(nJ * 2, 2), NumberStyles.AllowHexSpecifier);

                                apduParam.Data = baData;
                            }
                        }
                        break;
                    }
                }
            }

            return apduParam;
        }


        /// <summary>
        /// Process a command of a sequence of APDU
        /// </summary>
        /// <param name="xmlCmd">XML node representing the command</param>
        /// <param name="seqParam">List of parameters of the sequence</param>
        /// <returns>APDUResponse object of command executed</returns>
//        private APDUResponse ProcessSeqCmd(XmlNode xmlCmd, Dictionary<string, string> seqParam)
        private APDUResponse ProcessSeqCmd(XmlNode xmlCmd, SequenceParameter seqParam)
        {
            bool bApdu = false;
            bool bSeq = false;
            string sApduName = null;
            string sSeqName = null;
            APDUResponse apduResp = null;

            // Get the APDU or Sequence name
            try
            {
                // Get the Apdu name to run
                sApduName = xmlCmd.Attributes[xmlAttrApdu].Value;
                bApdu = true;
            }
            catch
            {
            }
            finally
            {
                try
                {
                    sSeqName = xmlCmd.Attributes[xmlAttrSequence].Value;
                    bSeq = true;
                }
                catch
                {
                }

                if ((bSeq | bApdu) == false)
                    throw new ApduCommandException(ApduCommandException.MissingApduOrCommand);
            }

            if (bApdu)
            {
                APDUParam apduParams = BuildCommandParam(xmlCmd.Attributes, seqParam);
                apduResp = ProcessCommand(sApduName, apduParams);
            }

            if (bSeq)
            {
                // Process a sub sequence
                apduResp = ProcessSequence(sSeqName, seqParam);
            }

            return apduResp;
        }

        private void AddLog(APDULog apduLog)
        {
            m_logList.Add(apduLog);
        }
        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

orouit
Architect Consistel - Singapore
Singapore Singapore
Software Architect, COM, .NET and Smartcard based security specialist.
 
I've been working in the software industry since I graduated in Electrical and Electronics Engineering. I chose software because I preferred digital to analog.
 
I started to program with 6802 machine code and evolved to the current .NET technologies... that was a long way.
 
For more than 20 years I have always worked in technical positions as I simply like to get my hands dirty and crack my brain when things don't go right!
 
After 12 years in the smart card industry I can claim a strong knowledge in security solutions based on those really small computers! I'm currently back in the business to design the licensing system for the enterprise solution I'm currenly working on, using a .NET smart card (yes they can run .NET CLR!)
 
View my profile on LinkedIn
 
You can contact me for professional consulting by using the forum.

| Advertise | Privacy | Mobile
Web01 | 2.8.140827.1 | Last Updated 20 Feb 2013
Article Copyright 2006 by orouit
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid