/// <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>
        /// Deletes an existing OATH entry from the device.
        /// </summary>
        /// <param name="name">Name of the entry to delete</param>
        /// <exception cref="UnexpectedResponseException">Thrown on a non-success status code.</exception>
        public void Delete(string name)
        {
            TagLengthValue tlvName = new TagLengthValue(TagLengthValue.YKTag.NAME, Encoding.UTF8.GetBytes(name));

            SendAPDU(new APDU
            {
                CLA  = 0x00,
                INS  = (APDU.Instruction)Instruction.DELETE,
                P1   = 0x00,
                P2   = 0x00,
                Data = tlvName.Data
            });
        }
        /// <summary>
        /// Configures device authentication.
        /// </summary>
        /// <param name="password">New device password</param>
        /// <exception cref="UnexpectedResponseException">Thrown on a non-success status code.</exception>
        public void SetCode(string password)
        {
            byte[]   key  = DeriveKey(password);
            HMACSHA1 hmac = new HMACSHA1(key);

            byte[] tlvKeyData = new byte[1 + key.Length];
            Buffer.BlockCopy(key, 0, tlvKeyData, 1, key.Length);
            tlvKeyData[0] = (byte)Type.TOTP | (byte)Algo.HMAC_SHA1;
            byte[] tlvChallengeData = new byte[8];
            Random random           = new Random();

            random.NextBytes(tlvChallengeData);
            TagLengthValue tlvKey       = new TagLengthValue(TagLengthValue.YKTag.KEY, tlvKeyData);
            TagLengthValue tlvChallenge = new TagLengthValue(TagLengthValue.YKTag.CHALLENGE, tlvChallengeData);
            TagLengthValue tlvResponse  = new TagLengthValue(TagLengthValue.YKTag.RESPONSE, hmac.ComputeHash(tlvChallengeData));

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

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

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

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

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

            // Send APDU to device
            SendAPDU(new APDU
            {
                CLA  = 0x00,
                INS  = (APDU.Instruction)Instruction.SET_CODE,
                P1   = 0x00,
                P2   = 0x00,
                Data = apduData
            });
        }
        /// <summary>
        /// Parses raw APDU data into a list of several <see cref="TagLengthValue"/> instances.
        /// </summary>
        /// <param name="data">Raw APDU data</param>
        /// <returns>Returns a list of <see cref="TagLengthValue"/> instances.</returns>
        public static List <TagLengthValue> FromData(byte[] data)
        {
            List <TagLengthValue> tags = new List <TagLengthValue>();

            int offset = 0;

            while (offset < data.Length)
            {
                byte[] offsetData = new byte[data.Length - offset];
                Buffer.BlockCopy(data, offset, offsetData, 0, data.Length - offset);
                TagLengthValue tlv = new TagLengthValue(offsetData);
                tags.Add(tlv);
                offset += tlv.Data.Length;
            }

            return(tags);
        }
        /// <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)
            });
        }
        /// <summary>
        /// Adds a new raw secret as an OATH entry on the device.
        /// </summary>
        /// <param name="key">Base-32 encoded secret</param>
        /// <param name="name">Name of the entry</param>
        /// <param name="type">Entry type (HOTP/TOTP)</param>
        /// <param name="digits">Output code digits</param>
        /// <param name="algo">Entry algorithm (SHA1/SHA256/SHA512)</param>
        /// <param name="counter">HOTP counter</param>
        /// <param name="requireTouch">Whether the new entry should require touch when calling <see cref="Calculate(Credential, DateTime?)"/></param>
        /// <exception cref="KeyExistsException">Thrown when an entry by that name already exists.</exception>
        /// <exception cref="UnexpectedResponseException">Thrown on a non-success status code.</exception>
        public void Put(byte[] secret, string name, Type type = Type.TOTP, byte digits = 6, Algo algo = Algo.HMAC_SHA1, uint counter = 0, bool requireTouch = false)
        {
            if (List().Exists(item => item.Name.Equals(name)))
            {
                throw new KeyExistsException("A key with that name already exists.");
            }

            TagLengthValue tlvName = new TagLengthValue(TagLengthValue.YKTag.NAME, Encoding.UTF8.GetBytes(name));

            byte[]         tlvSecretData = null;
            TagLengthValue tlvSecret     = null;

            byte[]         tvProperties = null;
            TagLengthValue tlvIMF       = null;

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

            // hmac_shorten_key
            switch (algo)
            {
            case Algo.HMAC_SHA1:
                using (SHA1 sha = new SHA1CryptoServiceProvider())
                {
                    if (secret.Length > 64)
                    {
                        int shaBytes = sha.HashSize / 8;
                        tlvSecretData = new byte[2 + Math.Max(MinKeySize, shaBytes)];
                        Buffer.BlockCopy(sha.ComputeHash(secret), 0, tlvSecretData, 2, shaBytes);
                    }
                    else
                    {
                        tlvSecretData = new byte[2 + Math.Max(MinKeySize, secret.Length)];
                        Buffer.BlockCopy(secret, 0, tlvSecretData, 2, secret.Length);
                    }
                }
                break;

            case Algo.HMAC_SHA256:
                using (SHA256 sha = new SHA256CryptoServiceProvider())
                {
                    if (secret.Length > 64)
                    {
                        int shaBytes = sha.HashSize / 8;
                        tlvSecretData = new byte[2 + Math.Max(MinKeySize, shaBytes)];
                        Buffer.BlockCopy(sha.ComputeHash(secret), 0, tlvSecretData, 2, shaBytes);
                    }
                    else
                    {
                        tlvSecretData = new byte[2 + Math.Max(MinKeySize, secret.Length)];
                        Buffer.BlockCopy(secret, 0, tlvSecretData, 2, secret.Length);
                    }
                }
                break;

            case Algo.HMAC_SHA512:
                using (SHA512 sha = new SHA512CryptoServiceProvider())
                {
                    if (secret.Length > 128)
                    {
                        int shaBytes = sha.HashSize / 8;
                        tlvSecretData = new byte[2 + Math.Max(MinKeySize, shaBytes)];
                        Buffer.BlockCopy(sha.ComputeHash(secret), 0, tlvSecretData, 2, shaBytes);
                    }
                    else
                    {
                        tlvSecretData = new byte[2 + Math.Max(MinKeySize, secret.Length)];
                        Buffer.BlockCopy(secret, 0, tlvSecretData, 2, secret.Length);
                    }
                }
                break;
            }

            tlvSecretData[0] = (byte)((byte)type | (byte)algo);
            tlvSecretData[1] = digits;
            tlvSecret        = new TagLengthValue(TagLengthValue.YKTag.KEY, tlvSecretData);

            // Calculate correct length of apduData
            apduDataLen += tlvName.Data.Length + tlvSecret.Data.Length;
            if (requireTouch)
            {
                tvProperties = new byte[2] {
                    (byte)TagLengthValue.YKTag.PROPERTY, (byte)Property.REQUIRE_TOUCH
                };
                apduDataLen += tvProperties.Length;
            }
            if (counter > 0)
            {
                tlvIMFData = BitConverter.GetBytes(counter);
                if (BitConverter.IsLittleEndian)
                {
                    Array.Reverse(tlvIMFData);
                }
                tlvIMF       = new TagLengthValue(TagLengthValue.YKTag.IMF, tlvIMFData);
                apduDataLen += tlvIMF.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(tlvSecret.Data, 0, apduData, offset, tlvSecret.Data.Length);
            offset += tlvSecret.Data.Length;
            if (tvProperties != null)
            {
                Buffer.BlockCopy(tvProperties, 0, apduData, offset, tvProperties.Length);
                offset += tvProperties.Length;
            }
            if (tlvIMF != null)
            {
                Buffer.BlockCopy(tlvIMF.Data, 0, apduData, offset, tlvIMF.Data.Length);
                offset += tlvIMF.Data.Length;
            }

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