/* * Copyright (C) 2013 Colin Mackie. * This software is distributed under the terms of the GNU General Public License. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Security.Cryptography; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Xml; using System.Xml.Serialization; using System.Xml.XPath; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Engines; using Org.BouncyCastle.Crypto.Macs; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Crypto.Paddings; using Org.BouncyCastle.Crypto.Digests; using Org.BouncyCastle.Crypto.Generators; #if NUNIT using NUnit.Framework; #endif #if NETCF using OpenNETCF.Security.Cryptography; #endif namespace WinAuth { /// /// Class that implements Trion's version of the RFC6238 Authenticator /// public class TrionAuthenticator : Authenticator { /// /// Size of model string /// private const int MODEL_SIZE = 15; /// /// String of possible chars we use in our random model string /// private const string MODEL_CHARS = "1234567890ABCDEF"; /// /// Number of digits in code /// private const int CODE_DIGITS = 8; /// /// Name of issuer for export /// private const string TRION_ISSUER = "Trion"; /// /// URLs for all mobile services /// private static string ENROLL_URL = "https://rift.trionworlds.com/external/create-device-key"; private static string SYNC_URL = "https://auth.trionworlds.com/time"; private static string SECURITYQUESTIONS_URL = "https://rift.trionworlds.com/external/get-account-security-questions.action"; private static string RESTORE_URL = "https://rift.trionworlds.com/external/retrieve-device-key.action"; /// /// Number of minutes to ignore syncing if network error /// private const int SYNC_ERROR_MINUTES = 60; /// /// Time of last Sync error /// private static DateTime _lastSyncError = DateTime.MinValue; #region Authenticator data /// /// Get/set the combined secret data value /// public override string SecretData { get { // this is the key | serial | deviceid return base.SecretData + "|" + Authenticator.ByteArrayToString(Encoding.UTF8.GetBytes(Serial)) + "|" + Authenticator.ByteArrayToString(Encoding.UTF8.GetBytes(DeviceId)); } set { // extract key + serial + deviceid if (string.IsNullOrEmpty(value) == false) { string[] parts = value.Split('|'); if (parts.Length == 3 && parts[1].IndexOf("-") != -1) { // alpha 3.0.2 version SecretKey = Authenticator.StringToByteArray(parts[0]); Serial = parts[1]; DeviceId = parts[2]; } else if (parts.Length == 4) { // alpha 3.0.6 version SecretKey = Authenticator.StringToByteArray(parts[0]); Serial = (parts.Length > 2 ? Encoding.UTF8.GetString(Authenticator.StringToByteArray(parts[2])) : null); DeviceId = (parts.Length > 3 ? Encoding.UTF8.GetString(Authenticator.StringToByteArray(parts[3])) : null); } else { base.SecretData = value; Serial = (parts.Length > 1 ? Encoding.UTF8.GetString(Authenticator.StringToByteArray(parts[1])) : null); DeviceId = (parts.Length > 2 ? Encoding.UTF8.GetString(Authenticator.StringToByteArray(parts[2])) : null); } } else { SecretKey = null; Serial = null; DeviceId = null; } } } public string Serial { get; set; } public string DeviceId {get; set;} #endregion /// /// Create a new Authenticator object /// public TrionAuthenticator() : base(CODE_DIGITS) { Issuer = TRION_ISSUER; } /// /// Enroll the authenticator with the server. /// public void Enroll() { // generate model name string deviceId = GeneralRandomModel(); string postdata = "deviceId=" + deviceId; // call the enroll server HttpWebRequest request = (HttpWebRequest)WebRequest.Create(ENROLL_URL); request.Method = "POST"; request.ContentType = "application/x-www-form-urlencoded"; request.ContentLength = postdata.Length; StreamWriter requestStream = new StreamWriter(request.GetRequestStream()); requestStream.Write(postdata); requestStream.Close(); string responseData; using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) { // OK? if (response.StatusCode != HttpStatusCode.OK) { throw new InvalidEnrollResponseException(string.Format("{0}: {1}", (int)response.StatusCode, response.StatusDescription)); } // load the response using (StreamReader responseStream = new StreamReader(response.GetResponseStream())) { responseData = responseStream.ReadToEnd(); } } // return data: // // // // // only exists if an error // XmlDocument doc = new XmlDocument(); doc.LoadXml(responseData); XmlNode node = doc.SelectSingleNode("//ErrorCode"); if (node != null && string.IsNullOrEmpty(node.InnerText) == false) { // an error occured throw new InvalidEnrollResponseException(node.InnerText); } // get the secret key SecretKey = Encoding.UTF8.GetBytes(doc.SelectSingleNode("//SecretKey").InnerText); // get the serial number string serial = doc.SelectSingleNode("//SerialKey").InnerText; Serial = Regex.Replace(serial, @"(.{4})", "$1 ").Trim().Replace(" ", "-"); // save the device DeviceId = doc.SelectSingleNode("//DeviceId").InnerText; } #if DEBUG /// /// Debug version of enroll that just returns a known test authenticator /// /// public void Enroll(bool testmode) { if (!testmode) { Enroll(); } else { //string responseData = "zarTM0v5ko0BwrOYQV1HhsE4Q0stqgbFFJP7H9DG3T67DP7FFJZKLG6ZNCJTNNMT"; string responseData = "19897B57952648559364352F7FE9B8A8HM3ZMQ233FPZ6MYNGRGYX7XZQNL6T2M6"; // // // // only exists if an error // XmlDocument doc = new XmlDocument(); doc.LoadXml(responseData); XmlNode node = doc.SelectSingleNode("//ErrorCode"); if (node != null && string.IsNullOrEmpty(node.InnerText) == false) { // an error occured throw new InvalidEnrollResponseException(node.InnerText); } // get the secret key SecretKey = Encoding.UTF8.GetBytes(doc.SelectSingleNode("//SecretKey").InnerText); // get the serial number string serial = doc.SelectSingleNode("//SerialKey").InnerText; Serial = Regex.Replace(serial, @"(.{4})", "$1 ").Trim().Replace(" ", "-"); DeviceId = doc.SelectSingleNode("//DeviceId").InnerText; } } #endif /// /// Synchorise this authenticator's time with server time. We update our data record with the difference from our UTC time. /// public override void Sync() { // check if data is protected if (this.SecretKey == null && this.EncryptedData != null) { throw new EncryptedSecretDataException(); } // don't retry for 5 minutes if (_lastSyncError >= DateTime.Now.AddMinutes(0 - SYNC_ERROR_MINUTES)) { return; } try { // create a connection to time sync server HttpWebRequest request = (HttpWebRequest)WebRequest.Create(SYNC_URL); request.Method = "GET"; // get response string responseData; using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) { // OK? if (response.StatusCode != HttpStatusCode.OK) { throw new ApplicationException(string.Format("{0}: {1}", (int)response.StatusCode, response.StatusDescription)); } // load the response using (StreamReader responseStream = new StreamReader(response.GetResponseStream())) { responseData = responseStream.ReadToEnd(); } } // return data is string version of time in milliseconds since epoch // get the difference between the server time and our current time long serverTimeDiff = long.Parse(responseData) - CurrentTime; // update the Data object ServerTimeDiff = serverTimeDiff; LastServerTime = DateTime.Now.Ticks; // clear any sync error _lastSyncError = DateTime.MinValue; } catch (Exception) { // don't retry for a while after error _lastSyncError = DateTime.Now; } } /// /// Calculate the current code for the authenticator. /// Trion's implementation is broken in that they don't built the signed integer correctly from the 4-byte array, so we have to override /// the proper method /// /// flag to resync time /// authenticator code protected override string CalculateCode(bool resyncTime = false, long interval = -1) { // sync time if required if (resyncTime == true || ServerTimeDiff == 0) { if (interval > 0) { ServerTimeDiff = (interval * ((long)this.Period * 1000L)) - CurrentTime; } else { Sync(); } } HMac hmac = new HMac(new Sha1Digest()); hmac.Init(new KeyParameter(SecretKey)); byte[] codeIntervalArray = BitConverter.GetBytes(CodeInterval); if (BitConverter.IsLittleEndian) { Array.Reverse(codeIntervalArray); } hmac.BlockUpdate(codeIntervalArray, 0, codeIntervalArray.Length); byte[] mac = new byte[hmac.GetMacSize()]; hmac.DoFinal(mac, 0); // the last 4 bits of the mac say where the code starts (e.g. if last 4 bit are 1100, we start at byte 12) int start = mac[19] & 0x0f; // extract those 4 bytes byte[] bytes = new byte[4]; Array.Copy(mac, start, bytes, 0, 4); if (BitConverter.IsLittleEndian) { Array.Reverse(bytes); } // this is where Trion is broken and their version uses all 32bits //uint fullcode = BitConverter.ToUInt32(bytes, 0) & 0x7fffffff; uint fullcode = BitConverter.ToUInt32(bytes, 0); // we use the last 8 digits of this code in radix 10 uint codemask = (uint)Math.Pow(10, CodeDigits); string format = new string('0', CodeDigits); string code = (fullcode % codemask).ToString(format); // New glyph authenticator now uses 6, but takes the first 6 of 8 rather the proper last 6, so again we override the standard implementation code = code.Substring(0, 6); return code; } /// /// Get the secret questions for an account /// /// user's account email /// user's account password /// returned secret question 1 /// returned secret question 2 public static void SecurityQuestions(string email, string password, out string question1, out string question2) { string postdata = "emailAddress=" + email + "&password=" + password; // call the enroll server HttpWebRequest request = (HttpWebRequest)WebRequest.Create(SECURITYQUESTIONS_URL); request.Method = "POST"; request.ContentType = "application/x-www-form-urlencoded"; request.ContentLength = postdata.Length; StreamWriter requestStream = new StreamWriter(request.GetRequestStream()); requestStream.Write(postdata); requestStream.Close(); string responseData; using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) { // OK? if (response.StatusCode != HttpStatusCode.OK) { throw new InvalidRestoreResponseException(string.Format("{0}: {1}", (int)response.StatusCode, response.StatusDescription)); } // load the response using (StreamReader responseStream = new StreamReader(response.GetResponseStream())) { responseData = responseStream.ReadToEnd(); } } // return data: // // // // // only exists if an error // XmlDocument doc = new XmlDocument(); doc.LoadXml(responseData); XmlNode node = doc.SelectSingleNode("//ErrorCode"); if (node != null && string.IsNullOrEmpty(node.InnerText) == false) { // an error occured throw new InvalidRestoreResponseException(node.InnerText); } // get the questions question1 = doc.SelectSingleNode("//FirstQuestion").InnerText; question2 = doc.SelectSingleNode("//SecondQuestion").InnerText; } /// /// Restore an authenticator using the account details and security questions /// /// user's account email /// user's account password /// register authenticator deviceid /// answer to secret question 1 /// answer to secret question 2 public void Restore(string email, string password, string deviceId, string answer1, string answer2) { string postdata = "emailAddress=" + email + "&password=" + password + "&deviceId=" + deviceId + "&securityAnswer=" + answer1 + "&secondSecurityAnswer=" + answer2; // call the enroll server HttpWebRequest request = (HttpWebRequest)WebRequest.Create(RESTORE_URL); request.Method = "POST"; request.ContentType = "application/x-www-form-urlencoded"; request.ContentLength = postdata.Length; StreamWriter requestStream = new StreamWriter(request.GetRequestStream()); requestStream.Write(postdata); requestStream.Close(); string responseData; using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) { // OK? if (response.StatusCode != HttpStatusCode.OK) { throw new InvalidRestoreResponseException(string.Format("{0}: {1}", (int)response.StatusCode, response.StatusDescription)); } // load the response using (StreamReader responseStream = new StreamReader(response.GetResponseStream())) { responseData = responseStream.ReadToEnd(); } } // return data: // // // // // only exists if an error // XmlDocument doc = new XmlDocument(); doc.LoadXml(responseData); XmlNode node = doc.SelectSingleNode("//ErrorCode"); if (node != null && string.IsNullOrEmpty(node.InnerText) == false) { // an error occured throw new InvalidRestoreResponseException(node.InnerText); } // get the secret key SecretKey = Encoding.UTF8.GetBytes(doc.SelectSingleNode("//SecretKey").InnerText); // get the serial number string serial = doc.SelectSingleNode("//SerialKey").InnerText; Serial = Regex.Replace(serial, @"(.{4})", "$1 ").Trim().Replace(" ", "-"); // save the device DeviceId = doc.SelectSingleNode("//DeviceId").InnerText; } /// /// Create a random Model string for initialization to armor the init string sent over the wire /// /// Random model string private static string GeneralRandomModel() { // seed a new RNG RNGCryptoServiceProvider randomSeedGenerator = new RNGCryptoServiceProvider(); byte[] seedBuffer = new byte[4]; randomSeedGenerator.GetBytes(seedBuffer); Random random = new Random(BitConverter.ToInt32(seedBuffer, 0)); // create a model string with available characters StringBuilder model = new StringBuilder(MODEL_SIZE); for (int i = MODEL_SIZE; i > 0; i--) { model.Append(MODEL_CHARS[random.Next(MODEL_CHARS.Length)]); } return model.ToString(); } } }