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