/// <summary>
        /// Selects the OATH application for use and initializes the class with device data.
        /// </summary>
        /// <exception cref="UnexpectedResponseException">Thrown on a non-success status code or when the response data is invalid.</exception>
        public void Select()
        {
            APDUResponse res = Driver.SelectApplet(Applets.All[Applets.Type.YUBICO_OATH].AID);

            if (res.Data == null)
            {
                throw new UnexpectedResponseException("Unexpected response from device.");
            }

            var tags = TagLengthValue.FromData(res.Data);

            if (!tags.Exists(tag => tag.Tag == TagLengthValue.YKTag.VERSION) ||
                !tags.Exists(tag => tag.Tag == TagLengthValue.YKTag.NAME))
            {
                throw new UnexpectedResponseException("Unexpected response from device.");
            }

            byte[] version = tags.Find(tag => tag.Tag == TagLengthValue.YKTag.VERSION).Value;

            Version = new Version(version[0], version[1], version[2]);
            Salt    = tags.Find(tag => tag.Tag == TagLengthValue.YKTag.NAME).Value;
            ID      = GetDeviceID(Salt);

            if (tags.Exists(tag => tag.Tag == TagLengthValue.YKTag.CHALLENGE))
            {
                Challenge = tags.Find(tag => tag.Tag == TagLengthValue.YKTag.CHALLENGE).Value;
            }

            // Yubico ignores this in their ykman Python SDK...
            //if (tags.Exists(tag => tag.Tag == TagLengthValue.YKTag.ALGORITHM)
            //    ChallengeAlgo = tags.Find(tag => tag.Tag == TagLengthValue.YKTag.ALGORITHM).Value;
        }
        /// <summary>
        /// Validates authentication (mutually).
        /// </summary>
        /// <param name="password">Password to unlock the device with</param>
        public void Validate(string password)
        {
            HMACSHA1       hmac        = new HMACSHA1(DeriveKey(password));
            TagLengthValue tlvResponse = new TagLengthValue(TagLengthValue.YKTag.RESPONSE, hmac.ComputeHash(Challenge));

            byte[] tlvChallengeData = new byte[8];
            Random random           = new Random();

            random.NextBytes(tlvChallengeData);
            byte[]         expectedResponse = hmac.ComputeHash(tlvChallengeData);
            TagLengthValue tlvChallenge     = new TagLengthValue(TagLengthValue.YKTag.CHALLENGE, tlvChallengeData);

            byte[] apduData    = null;
            int    apduDataLen = 0;

            // Calculate correct length of apduData
            apduDataLen += tlvResponse.Data.Length;
            apduDataLen += tlvChallenge.Data.Length;

            // Initialize apduData with correct length
            apduData = new byte[apduDataLen];

            // Fill apduData with previously constructed TLV data
            int offset = 0;

            Buffer.BlockCopy(tlvResponse.Data, 0, apduData, offset, tlvResponse.Data.Length);
            offset += tlvResponse.Data.Length;
            Buffer.BlockCopy(tlvChallenge.Data, 0, apduData, offset, tlvChallenge.Data.Length);
            offset += tlvChallenge.Data.Length;

            // Send APDU to device
            APDUResponse res = SendAPDU(new APDU {
                CLA  = 0x00,
                INS  = (APDU.Instruction)Instruction.VALIDATE,
                P1   = 0x00,
                P2   = 0x00,
                Data = apduData
            });

            if (res.Data == null)
            {
                throw new UnexpectedResponseException("No response from card.");
            }

            var tags = TagLengthValue.FromData(res.Data);

            if (!tags.Exists(tag => tag.Tag == TagLengthValue.YKTag.RESPONSE))
            {
                throw new UnexpectedResponseException("Unexpected response from device.");
            }

            if (!tags.Find(tag => tag.Tag == TagLengthValue.YKTag.RESPONSE).ValueEquals(expectedResponse))
            {
                throw new UnexpectedResponseException("Incorrect response from device.", APDUResponse.StatusWord.INCORRECT_RESPONSE);
            }

            Challenge = null;
        }
        /// <summary>
        /// Lists configured credentials.
        /// </summary>
        /// <returns>Returns a list of configured credentials.</returns>
        /// <exception cref="UnexpectedResponseException">Thrown on a non-success status code.</exception>
        public List <Credential> List()
        {
            APDUResponse res = SendAPDU(new APDU
            {
                CLA = 0x00,
                INS = (APDU.Instruction)Instruction.LIST,
                P1  = 0x00,
                P2  = 0x00
            });

            List <Credential> list = new List <Credential>();
            var tags = TagLengthValue.FromData(res.Data);

            foreach (var tag in tags)
            {
                if (tag.Tag != TagLengthValue.YKTag.NAME_LIST)
                {
                    continue;
                }

                Algo algo = (Algo)((byte)Mask.ALGO & tag.Value[0]);
                if (!Enum.IsDefined(typeof(Algo), algo))
                {
                    throw new UnexpectedResponseException("Device returned item with unexpected algorithm.");
                }

                Type type = (Type)((byte)Mask.TYPE & tag.Value[0]);
                if (!Enum.IsDefined(typeof(Type), type))
                {
                    throw new UnexpectedResponseException("Device returned item with unexpected type.");
                }

                list.Add(new Credential {
                    Name      = Encoding.UTF8.GetString(tag.Value, 1, tag.Value.Length - 1),
                    Algorithm = algo,
                    Type      = type
                });
            }

            return(list);
        }
        /// <summary>
        /// Performs <see cref="Calculate(Credential, DateTime?)"/> for all available TOTP credentials that do not require touch.
        /// </summary>
        /// <param name="time">The <see cref="DateTime"/> to generate a TOTP code at</param>
        /// <returns>Returns codes for all TOTP credentials that do not require touch.</returns>
        public List <Code> CalculateAll(DateTime?time = null)
        {
            if (time == null)
            {
                time = DateTime.UtcNow;
            }

            Int32 timestamp = (Int32)(time.Value.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;

            byte[] challenge     = new byte[8];
            byte[] totpChallenge = BitConverter.GetBytes(timestamp / 30);
            if (BitConverter.IsLittleEndian)
            {
                Array.Reverse(totpChallenge);
            }
            Buffer.BlockCopy(totpChallenge, 0, challenge, 4, 4);
            TagLengthValue tlvChallenge = new TagLengthValue(TagLengthValue.YKTag.CHALLENGE, challenge);

            APDUResponse res = SendAPDU(new APDU {
                CLA  = 0x00,
                INS  = (APDU.Instruction)Instruction.CALCULATE_ALL,
                P1   = 0x00,
                P2   = 0x01,
                Data = tlvChallenge.Data
            });

            List <Code> codes = new List <Code>();
            var         tags  = TagLengthValue.FromData(res.Data);

            if (tags.Count % 2 != 0)
            {
                throw new UnexpectedResponseException("Unexpected tag count from device.");
            }

            for (int i = 0; i < tags.Count; i += 2)
            {
                if (tags[i].Tag != TagLengthValue.YKTag.NAME)
                {
                    throw new UnexpectedResponseException("Unexpected tag order from device.");
                }

                Credential cred = new Credential
                {
                    Name = Encoding.UTF8.GetString(tags[i].Value),
                };
                Int32 validFrom = timestamp - (timestamp % cred.Period);
                Int32 validTo   = validFrom + cred.Period;
                Code  code;

                switch (tags[i + 1].Tag)
                {
                case TagLengthValue.YKTag.TOUCH:
                    cred.Touch = true;
                    code       = new Code
                    {
                        Credential = cred
                    };
                    break;

                case TagLengthValue.YKTag.HOTP:
                    code = new Code {
                        Credential = cred
                    };
                    break;

                case TagLengthValue.YKTag.TRUNCATED_RESPONSE:
                    if (cred.Period != 30 || cred.IsSteam)
                    {
                        code = Calculate(cred);
                    }
                    else
                    {
                        code = new Code
                        {
                            Credential = cred,
                            ValidFrom  = validFrom,
                            ValidTo    = validTo,
                            Value      = FormatCode(tags[i + 1].Value)
                        };
                    }
                    break;

                case TagLengthValue.YKTag.RESPONSE:
                default:
                    throw new UnexpectedResponseException("Unexpected tag from device.");
                }

                codes.Add(code);
            }

            return(codes);
        }
        /// <summary>
        /// Calculates a HOTP or TOTP code for one <see cref="Credential"/>.
        /// </summary>
        /// <param name="cred">The <see cref="Credential"/> to generate a code for</param>
        /// <param name="time">The <see cref="DateTime"/> to generate a TOTP code at</param>
        /// <returns>Returns a new <see cref="Code"/></returns>
        /// <exception cref="ArgumentException">Thrown when an invalid credential type is passed.</exception>
        /// <exception cref="UnexpectedResponseException">Thrown on a non-success status code or when response data is invalid.</exception>
        public Code Calculate(Credential cred, DateTime?time = null)
        {
            // The 4.2.0-4.2.6 firmwares have a known issue with credentials that
            // require touch: If this action is performed within 2 seconds of a
            // command resulting in a long response (over 54 bytes),
            // the command will hang. A workaround is to send an invalid command
            // (resulting in a short reply) prior to the "calculate" command.
            if (cred.Touch && Version.CompareTo(new Version(4, 2, 0)) >= 0 && Version.CompareTo(new Version(4, 2, 6)) <= 0)
            {
                Driver.SendAPDU(new APDU
                {
                    CLA  = 0x00,
                    INS  = 0x00,
                    P1   = 0x00,
                    P2   = 0x00,
                    Data = new byte[0]
                }, null);
            }

            if (time == null)
            {
                time = DateTime.UtcNow;
            }

            Int32 timestamp = (Int32)(time.Value.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;
            Int32 validFrom = 0;
            Int32 validTo   = 0;

            byte[] challenge = new byte[8];

            switch (cred.Type)
            {
            case Type.TOTP:
                validFrom = timestamp - (timestamp % cred.Period);
                validTo   = validFrom + cred.Period;
                byte[] totpChallenge = BitConverter.GetBytes(timestamp / cred.Period);
                if (BitConverter.IsLittleEndian)
                {
                    Array.Reverse(totpChallenge);
                }
                Buffer.BlockCopy(totpChallenge, 0, challenge, 4, 4);
                break;

            case Type.HOTP:
                validFrom = timestamp;
                break;

            default:
                throw new ArgumentException("Invalid credential type.", "cred");
            }

            TagLengthValue tlvName      = new TagLengthValue(TagLengthValue.YKTag.NAME, Encoding.UTF8.GetBytes(cred.Name));
            TagLengthValue tlvChallenge = new TagLengthValue(TagLengthValue.YKTag.CHALLENGE, challenge);

            byte[] apduData    = null;
            int    apduDataLen = 0;

            // Calculate correct length of apduData
            apduDataLen += tlvName.Data.Length;
            apduDataLen += tlvChallenge.Data.Length;

            // Initialize apduData with correct length
            apduData = new byte[apduDataLen];

            // Fill apduData with previously constructed TLV data
            int offset = 0;

            Buffer.BlockCopy(tlvName.Data, 0, apduData, offset, tlvName.Data.Length);
            offset += tlvName.Data.Length;
            Buffer.BlockCopy(tlvChallenge.Data, 0, apduData, offset, tlvChallenge.Data.Length);
            offset += tlvChallenge.Data.Length;

            // Send APDU to device
            APDUResponse res = SendAPDU(new APDU
            {
                CLA  = 0x00,
                INS  = (APDU.Instruction)Instruction.CALCULATE,
                P1   = 0x00,
                P2   = 0x01,
                Data = apduData
            });

            var tags = TagLengthValue.FromData(res.Data);

            if (!tags.Exists(tag => tag.Tag == TagLengthValue.YKTag.TRUNCATED_RESPONSE))
            {
                throw new UnexpectedResponseException("Unexpected response from device.");
            }

            byte[] resValue = tags.Find(tag => tag.Tag == TagLengthValue.YKTag.TRUNCATED_RESPONSE).Value;
            return(new Code
            {
                Credential = cred,
                ValidFrom = validFrom,
                ValidTo = validTo,
                Value = FormatCode(resValue)
            });
        }