/// <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>
        /// Sends an APDU to the device connected to the underlying driver.
        /// </summary>
        /// <param name="apdu">APDU to send to the device</param>
        /// <returns>Returns an <see cref="APDUResponse"/> containing response status code and any data.</returns>
        /// <exception cref="ArgumentNullException">Thrown when <paramref name="apdu"/> is null.</exception>
        /// <exception cref="UnexpectedResponseException">Thrown on a non-success status code.</exception>
        public APDUResponse SendAPDU(APDU apdu)
        {
            // Send APDU and parse response
            APDUResponse res = Driver.SendAPDU(apdu, null);

            // Read remaining data
            while (res.SW1 == (byte)APDUResponse.StatusWord.MORE_DATA_AVAILABLE)
            {
                APDUResponse _res = Driver.SendAPDU(new APDU
                {
                    CLA = 0x00,
                    INS = (APDU.Instruction)Instruction.SEND_REMAINING,
                    P1  = 0x00,
                    P2  = 0x00
                }, null);

                // Extend res.Data with remaining data
                int offset = res.Data.Length;
                Array.Resize(ref res.Data, offset + _res.Data.Length);
                Buffer.BlockCopy(_res.Data, 0, res.Data, offset, _res.Data.Length);

                // Overwrite status words
                res.SW = _res.SW;
            }

            // Validate final status
            if (res.SW != APDUResponse.StatusWord.SUCCESS)
            {
                throw new UnexpectedResponseException("Unexpected response from device.", res.SW);
            }

            // Return full response
            return(res);
        }
Beispiel #3
0
        /// <summary>
        /// Reads the version from the Yubico OTP applet into the class.
        /// </summary>
        /// <exception cref="UnexpectedResponseException">Thrown if unsuccessful.</exception>
        private void ReadVersion()
        {
            // Apparently Yubico OTP applet is capable of delivering Yubikey firmware version
            // who knew
            APDUResponse res = SelectApplet(Applets.All[Applets.Type.YUBICO_OTP].AID);

            Version = new Version(res.Data[0], res.Data[1], res.Data[2]);
        }
        /// <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;
        }
Beispiel #5
0
        /// <summary>
        /// Reads the serial from the Yubico OTP applet into the class.
        /// </summary>
        /// <exception cref="UnexpectedResponseException">Thrown if unsuccessful.</exception>
        private void ReadSerial()
        {
            APDUResponse res = SendAPDU(new APDU
            {
                CLA = 0x00,
                INS = APDU.Instruction.YK2_REQ,
                P1  = (byte)YKSlotCode.DEVICE_SERIAL,
                P2  = 0x00
            });

            if (res.Data != null && res.Data.Length == 4)
            {
                if (BitConverter.IsLittleEndian)
                {
                    Array.Reverse(res.Data);
                }

                Serial = BitConverter.ToUInt32(res.Data, 0);
            }
        }
        /// <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);
        }
Beispiel #7
0
        /// <summary>
        /// Transmits an <see cref="APDU"/> to the device and receives an <see cref="APDUResponse"/> back from it.
        /// </summary>
        /// <param name="apdu">APDU to send to the device</param>
        /// <param name="check">Status code to check for</param>
        /// <returns>Returns the parsed <see cref="APDUResponse"/> from the device response.</returns>
        /// <exception cref="UnexpectedResponseException">Thrown if write is unsuccessful or device returns a status code not matching <paramref name="check"/>.</exception>
        public APDUResponse SendAPDU(APDU apdu, APDUResponse.StatusWord?check = APDUResponse.StatusWord.SUCCESS)
        {
            if (apdu == null)
            {
                throw new ArgumentNullException();
            }

            byte[]     buffer   = new byte[256];
            SCardError writeres = Reader.Transmit(apdu.ToBytes(), ref buffer);

            if (writeres != SCardError.Success)
            {
                throw new UnexpectedResponseException("Device returned a non-success status code.");
            }

            APDUResponse res = new APDUResponse(buffer);

            if (check != null && check != res.SW)
            {
                throw new UnexpectedResponseException("Unexpected response from device.");
            }

            return(res);
        }
        /// <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)
            });
        }