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