/* * Copyright (C) 2011 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.Linq; using System.Net; using System.Security.Cryptography; using System.Reflection; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Xml; using System.Xml.Serialization; 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; using System.Collections; using System.Security; #if NUNIT using NUnit.Framework; #endif #if NETCF using OpenNETCF.Security.Cryptography; #endif namespace WinAuth { /// /// Class that implements base RFC 4226 an RFC 6238 authenticator /// public abstract class Authenticator : ICloneable { /// /// Number of bytes making up the salt /// private const int SALT_LENGTH = 8; /// /// Number of iterations in PBKDF2 key generation /// private const int PBKDF2_ITERATIONS = 2000; /// /// Size of derived PBKDF2 key /// private const int PBKDF2_KEYSIZE = 256; /// /// Version for encrpytion changes /// private static string ENCRYPTION_HEADER = Authenticator.ByteArrayToString(Encoding.UTF8.GetBytes("WINAUTH3")); /// /// Default number of digits in code /// public const int DEFAULT_CODE_DIGITS = 6; /// /// Default period of 30s /// public const int DEFAULT_PERIOD = 30; /// /// Type of password to use to encrypt secret data /// public enum PasswordTypes { None = 0, Explicit = 1, User = 2, Machine = 4, YubiKeySlot1 = 8, YubiKeySlot2 = 16 } /// /// HMAC hashing algorithm types /// public enum HMACTypes { SHA1 = 0, SHA256 = 1, SHA512 = 2 } public const HMACTypes DEFAULT_HMAC_TYPE = HMACTypes.SHA1; #region Authenticator data /// /// Serial number of authenticator /// //public virtual string Serial { get; set; } /// /// Secret key used for Authenticator /// public byte[] SecretKey { get; set; } /// /// Time difference in milliseconds of our machine and server /// public long ServerTimeDiff { get; set; } /// /// Time of last synced /// public long LastServerTime { get; set; } /// /// Type of password used to encrypt secretdata /// public PasswordTypes PasswordType { get; private set; } /// /// Password used to encrypt secretdata (if PasswordType == Explict) /// protected string Password { get; set; } /// /// Hash of secret data to detect changes /// protected byte[] SecretHash { get; private set; } public bool RequiresPassword { get; private set; } /// /// The data current saved with the current encryption and/or password (might be none) /// protected string EncryptedData { get; private set; } /// /// Number of digits returned in code (default is 6) /// public int CodeDigits { get; set; } /// /// Hashing algorithm used for OTP generation (default is SHA1) /// public HMACTypes HMACType { get; set; } /// /// Period in seconds for next code /// public int Period { get; set; } /// /// Name of issuer /// public virtual string Issuer { get; set; } /// /// Get/set the combined secret data value /// public virtual string SecretData { get { // this is the secretkey return Authenticator.ByteArrayToString(SecretKey) + "\t" + this.CodeDigits.ToString() + "\t" + this.HMACType.ToString() + "\t" + this.Period.ToString(); } set { if (string.IsNullOrEmpty(value) == false) { string[] parts = value.Split('|')[0].Split('\t'); SecretKey = Authenticator.StringToByteArray(parts[0]); if (parts.Length > 1) { int digits; if (int.TryParse(parts[1], out digits) == true) { CodeDigits = digits; } } if (parts.Length > 2) { HMACType = (HMACTypes)Enum.Parse(typeof(HMACTypes), parts[2]); } if (parts.Length > 3) { int period; if (int.TryParse(parts[3], out period) == true) { Period = period; } } } else { SecretKey = null; } } } /// /// Advanced script saved with authenticator so it is also encrypted /// //public string Script {get; set;} /// /// Get the server time since 1/1/70 /// public long ServerTime { get { return CurrentTime + ServerTimeDiff; } } /// /// Calculate the code interval based on the calculated server time /// public long CodeInterval { get { // calculate the code interval; the server's time div 30,000 return (CurrentTime + ServerTimeDiff) / ((long)this.Period * 1000L); } } /// /// Get the current code for the authenticator. /// /// authenticator code public string CurrentCode { get { if (this.SecretKey == null && this.EncryptedData != null) { throw new EncryptedSecretDataException(); } return CalculateCode(false); } } #endregion /// /// Static initializer /// static Authenticator() { // Issue#71: remove the default .net expect header, which can cause issues (http://stackoverflow.com/questions/566437/) System.Net.ServicePointManager.Expect100Continue = false; } /// /// Create a new Authenticator object /// public Authenticator(int codeDigits = DEFAULT_CODE_DIGITS, HMACTypes hmacType = HMACTypes.SHA1, int period = DEFAULT_PERIOD) { CodeDigits = codeDigits; HMACType = hmacType; Period = period; } /// /// Calculate the current code for the authenticator. /// /// flag to resync time /// authenticator code protected virtual string CalculateCode(bool resync = false, long interval = -1) { // sync time if required if (resync == true || ServerTimeDiff == 0) { if (interval > 0) { ServerTimeDiff = (interval * ((long)this.Period * 1000L)) - CurrentTime; } else { Sync(); } } HMac hmac; switch (HMACType) { case HMACTypes.SHA1: hmac = new HMac(new Sha1Digest()); break; case HMACTypes.SHA256: hmac = new HMac(new Sha256Digest()); break; case HMACTypes.SHA512: hmac = new HMac(new Sha512Digest()); break; default: throw new InvalidHMACAlgorithmException(); } 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.Last() & 0x0f; // extract those 4 bytes byte[] bytes = new byte[4]; Array.Copy(mac, start, bytes, 0, 4); if (BitConverter.IsLittleEndian) { Array.Reverse(bytes); } uint fullcode = BitConverter.ToUInt32(bytes, 0) & 0x7fffffff; // 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); return code; } /// /// Synchorise this authenticator's time with server time. We update our data record with the difference from our UTC time. /// public abstract void Sync(); #region Load / Save public static Authenticator ReadXmlv2(XmlReader reader, string password = null) { Authenticator authenticator = null; string authenticatorType = reader.GetAttribute("type"); if (string.IsNullOrEmpty(authenticatorType) == false) { authenticatorType = authenticatorType.Replace("WindowsAuthenticator.", "WinAuth."); Type type = System.Reflection.Assembly.GetExecutingAssembly().GetType(authenticatorType, false, true); authenticator = Activator.CreateInstance(type) as Authenticator; } if (authenticator == null) { authenticator = new BattleNetAuthenticator(); } reader.MoveToContent(); if (reader.IsEmptyElement) { reader.Read(); return null; } reader.Read(); while (reader.EOF == false) { if (reader.IsStartElement()) { switch (reader.Name) { case "servertimediff": authenticator.ServerTimeDiff = reader.ReadElementContentAsLong(); break; //case "restorecodeverified": // authenticator.RestoreCodeVerified = reader.ReadElementContentAsBoolean(); // break; case "secretdata": string encrypted = reader.GetAttribute("encrypted"); string data = reader.ReadElementContentAsString(); PasswordTypes passwordType = DecodePasswordTypes(encrypted); if (passwordType != PasswordTypes.None) { // this is an old version so there is no hash data = DecryptSequence(data, passwordType, password, null); } authenticator.PasswordType = PasswordTypes.None; authenticator.SecretData = data; break; default: if (authenticator.ReadExtraXml(reader, reader.Name) == false) { reader.Skip(); } break; } } else { reader.Read(); break; } } return authenticator; } public virtual bool ReadExtraXml(XmlReader reader, string name) { return false; } /// /// Convert the string password types into the PasswordTypes type /// /// string version of password types /// PasswordTypes value public static PasswordTypes DecodePasswordTypes(string passwordTypes) { PasswordTypes passwordType = PasswordTypes.None; if (string.IsNullOrEmpty(passwordTypes) == true) { return passwordType; } char[] types = passwordTypes.ToCharArray(); for (int i = types.Length - 1; i >= 0; i--) { char type = types[i]; switch (type) { case 'u': passwordType |= PasswordTypes.User; break; case 'm': passwordType |= PasswordTypes.Machine; break; case 'y': passwordType |= PasswordTypes.Explicit; break; case 'a': passwordType |= PasswordTypes.YubiKeySlot1; break; case 'b': passwordType |= PasswordTypes.YubiKeySlot2; break; default: break; } } return passwordType; } /// /// Encode the PasswordTypes type into a string for storing in config /// /// PasswordTypes value /// string version public static string EncodePasswordTypes(PasswordTypes passwordType) { StringBuilder encryptedTypes = new StringBuilder(); if ((passwordType & PasswordTypes.Explicit) != 0) { encryptedTypes.Append("y"); } if ((passwordType & PasswordTypes.User) != 0) { encryptedTypes.Append("u"); } if ((passwordType & PasswordTypes.Machine) != 0) { encryptedTypes.Append("m"); } return encryptedTypes.ToString(); } public void SetEncryption(PasswordTypes passwordType, string password = null) { // check if still encrpyted if (this.RequiresPassword == true) { // have to decrypt to be able to re-encrypt throw new EncryptedSecretDataException(); } if (passwordType == PasswordTypes.None) { this.RequiresPassword = false; this.EncryptedData = null; this.PasswordType = passwordType; } else { using (MemoryStream ms = new MemoryStream()) { // get the plain version XmlWriterSettings settings = new XmlWriterSettings(); settings.Indent = true; settings.Encoding = Encoding.UTF8; using (XmlWriter encryptedwriter = XmlWriter.Create(ms, settings)) { string encrpytedData = this.EncryptedData; Authenticator.PasswordTypes savedpasswordType = PasswordType; try { PasswordType = Authenticator.PasswordTypes.None; EncryptedData = null; WriteToWriter(encryptedwriter); } finally { this.PasswordType = savedpasswordType; this.EncryptedData = encrpytedData; } } string data = Authenticator.ByteArrayToString(ms.ToArray()); // update secret hash using (SHA1 sha1 = SHA1.Create()) { this.SecretHash = sha1.ComputeHash(Encoding.UTF8.GetBytes(this.SecretData)); } // encrypt this.EncryptedData = Authenticator.EncryptSequence(data, passwordType, password, null); this.PasswordType = passwordType; if (this.PasswordType == PasswordTypes.Explicit) { this.SecretData = null; this.RequiresPassword = true; } } } } public void Protect() { if (this.PasswordType != PasswordTypes.None) { // check if the data has changed //if (this.SecretData != null) //{ // using (SHA1 sha1 = SHA1.Create()) // { // byte[] secretHash = sha1.ComputeHash(Encoding.UTF8.GetBytes(this.SecretData)); // if (this.SecretHash == null || secretHash.SequenceEqual(this.SecretHash) == false) // { // // we need to encrypt changed secret data // SetEncryption(this.PasswordType, this.Password); // } // } //} this.SecretData = null; this.RequiresPassword = true; this.Password = null; } } public bool Unprotect(string password) { PasswordTypes passwordType = this.PasswordType; if (passwordType == PasswordTypes.None) { throw new InvalidOperationException("Cannot Unprotect a non-encrypted authenticator"); } // decrypt bool changed = false; try { string data = Authenticator.DecryptSequence(this.EncryptedData, this.PasswordType, password, null); using (MemoryStream ms = new MemoryStream(Authenticator.StringToByteArray(data))) { XmlReader reader = XmlReader.Create(ms); changed = this.ReadXml(reader, password) || changed; } this.RequiresPassword = false; // calculate hash of current secretdata using (SHA1 sha1 = SHA1.Create()) { this.SecretHash = sha1.ComputeHash(Encoding.UTF8.GetBytes(this.SecretData)); } // keep the password until we reprotect in case data changes this.Password = password; if (changed == true) { // we need to encrypt changed secret data using (MemoryStream ms = new MemoryStream()) { // get the plain version XmlWriterSettings settings = new XmlWriterSettings(); settings.Indent = true; settings.Encoding = Encoding.UTF8; using (XmlWriter encryptedwriter = XmlWriter.Create(ms, settings)) { WriteToWriter(encryptedwriter); } string encrypteddata = Authenticator.ByteArrayToString(ms.ToArray()); // update secret hash using (SHA1 sha1 = SHA1.Create()) { this.SecretHash = sha1.ComputeHash(Encoding.UTF8.GetBytes(this.SecretData)); } // encrypt this.EncryptedData = Authenticator.EncryptSequence(encrypteddata, passwordType, password, null); } } return changed; } catch (EncryptedSecretDataException) { this.RequiresPassword = true; throw; } finally { this.PasswordType = passwordType; } } public bool ReadXml(XmlReader reader, string password = null) { // decode the password type string encrypted = reader.GetAttribute("encrypted"); PasswordTypes passwordType = DecodePasswordTypes(encrypted); this.PasswordType = passwordType; if (passwordType != PasswordTypes.None) { // read the encrypted text from the node this.EncryptedData = reader.ReadElementContentAsString(); return Unprotect(password); //// decrypt //try //{ // string data = Authenticator.DecryptSequence(this.EncryptedData, passwordType, password); // using (MemoryStream ms = new MemoryStream(Authenticator.StringToByteArray(data))) // { // reader = XmlReader.Create(ms); // this.ReadXml(reader, password); // } //} //catch (EncryptedSecretDataException) //{ // this.RequiresPassword = true; // throw; //} //finally //{ // this.PasswordType = passwordType; //} } reader.MoveToContent(); if (reader.IsEmptyElement) { reader.Read(); return false; } reader.Read(); while (reader.EOF == false) { if (reader.IsStartElement()) { switch (reader.Name) { case "lastservertime": LastServerTime = reader.ReadElementContentAsLong(); break; case "servertimediff": ServerTimeDiff = reader.ReadElementContentAsLong(); break; case "secretdata": SecretData = reader.ReadElementContentAsString(); break; default: if (ReadExtraXml(reader, reader.Name) == false) { reader.Skip(); } break; } } else { reader.Read(); break; } } // check if we need to sync, or if it's been a day if (this is HOTPAuthenticator) { // no time sync return true; } else if (ServerTimeDiff == 0 || LastServerTime == 0 || LastServerTime < DateTime.Now.AddHours(-24).Ticks) { Sync(); return true; } else { return false; } } /// /// Write this authenticator into an XmlWriter /// /// XmlWriter to receive authenticator public void WriteToWriter(XmlWriter writer) { writer.WriteStartElement("authenticatordata"); //writer.WriteAttributeString("type", this.GetType().FullName); string encrypted = EncodePasswordTypes(this.PasswordType); if (string.IsNullOrEmpty(encrypted) == false) { writer.WriteAttributeString("encrypted", encrypted); } if (this.PasswordType != PasswordTypes.None) { writer.WriteRaw(this.EncryptedData); } else { writer.WriteStartElement("servertimediff"); writer.WriteString(ServerTimeDiff.ToString()); writer.WriteEndElement(); // writer.WriteStartElement("lastservertime"); writer.WriteString(LastServerTime.ToString()); writer.WriteEndElement(); // writer.WriteStartElement("secretdata"); writer.WriteString(SecretData); writer.WriteEndElement(); WriteExtraXml(writer); } /* if (passwordType != Authenticator.PasswordTypes.None) { //string data = this.EncryptedData; //if (data == null) //{ // using (MemoryStream ms = new MemoryStream()) // { // XmlWriterSettings settings = new XmlWriterSettings(); // settings.Indent = true; // settings.Encoding = Encoding.UTF8; // using (XmlWriter encryptedwriter = XmlWriter.Create(ms, settings)) // { // Authenticator.PasswordTypes savedpasswordType = PasswordType; // PasswordType = Authenticator.PasswordTypes.None; // WriteToWriter(encryptedwriter); // PasswordType = savedpasswordType; // } // data = Authenticator.ByteArrayToString(ms.ToArray()); // } // data = Authenticator.EncryptSequence(data, PasswordType, Password); //} writer.WriteString(this.EncryptedData); writer.WriteEndElement(); return; } // writer.WriteStartElement("servertimediff"); writer.WriteString(ServerTimeDiff.ToString()); writer.WriteEndElement(); // writer.WriteStartElement("secretdata"); writer.WriteString(SecretData); writer.WriteEndElement(); WriteExtraXml(writer); */ writer.WriteEndElement(); } /* /// /// Write this authenticator into an XmlWriter /// /// XmlWriter to receive authenticator protected void WriteToWriter(XmlWriter writer, PasswordTypes passwordType) { if (passwordType != Authenticator.PasswordTypes.None) { writer.WriteStartElement("authenticatordata"); writer.WriteAttributeString("encrypted", EncodePasswordTypes(this.PasswordType)); writer.WriteString(this.EncryptedData); writer.WriteEndElement(); } else { writer.WriteStartElement("servertimediff"); writer.WriteString(ServerTimeDiff.ToString()); writer.WriteEndElement(); // writer.WriteStartElement("secretdata"); writer.WriteString(SecretData); writer.WriteEndElement(); WriteExtraXml(writer); } } */ /// /// Virtual function to write any class specific xml nodes into the writer /// /// XmlWriter to write data protected virtual void WriteExtraXml(XmlWriter writer) { } #endregion #region Utility functions /// /// Create a one-time pad by generating a random block and then taking a hash of that block as many times as needed. /// /// desired pad length /// array of bytes conatining random data protected internal static byte[] CreateOneTimePad(int length) { // There is a MITM vulnerability from using the standard Random call // see https://docs.google.com/document/edit?id=1pf-YCgUnxR4duE8tr-xulE3rJ1Hw-Bm5aMk5tNOGU3E&hl=en // in http://code.google.com/p/winauth/issues/detail?id=2 // so we switch out to use RNGCryptoServiceProvider instead of Random RNGCryptoServiceProvider random = new RNGCryptoServiceProvider(); byte[] randomblock = new byte[length]; SHA1 sha1 = SHA1.Create(); int i = 0; do { byte[] hashBlock = new byte[128]; random.GetBytes(hashBlock); byte[] key = sha1.ComputeHash(hashBlock, 0, hashBlock.Length); if (key.Length >= randomblock.Length) { Array.Copy(key, 0, randomblock, i, randomblock.Length); break; } Array.Copy(key, 0, randomblock, i, key.Length); i += key.Length; } while (true); return randomblock; } /// /// Get the milliseconds since 1/1/70 (same as Java currentTimeMillis) /// public static long CurrentTime { get { return Convert.ToInt64((DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalMilliseconds); } } /// /// Convert a hex string into a byte array. E.g. "001f406a" -> byte[] {0x00, 0x1f, 0x40, 0x6a} /// /// hex string to convert /// byte[] of hex string public static byte[] StringToByteArray(string hex) { int len = hex.Length; byte[] bytes = new byte[len / 2]; for (int i = 0; i < len; i += 2) { bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); } return bytes; } /// /// Convert a byte array into a ascii hex string, e.g. byte[]{0x00,0x1f,0x40,ox6a} -> "001f406a" /// /// byte array to convert /// string version of byte array public static string ByteArrayToString(byte[] bytes) { // Use BitConverter, but it sticks dashes in the string return BitConverter.ToString(bytes).Replace("-", string.Empty); } /// /// Decrypt a string sequence using the selected encryption types /// /// hex coded string sequence to decrypt /// Encryption types /// optional password /// optional yubi data /// /// decrypted string sequence public static string DecryptSequence(string data, PasswordTypes encryptedTypes, string password, YubiKey yubi, bool decode = false) { // check for encrpytion header if (data.Length < ENCRYPTION_HEADER.Length || data.IndexOf(ENCRYPTION_HEADER) != 0) { return DecryptSequenceNoHash(data, encryptedTypes, password, yubi, decode); } // extract salt and hash //using (var sha = new SHA256Managed()) using (var sha = SafeHasher("SHA256")) { // jump header int datastart = ENCRYPTION_HEADER.Length; string salt = data.Substring(datastart, Math.Min(SALT_LENGTH * 2, data.Length - datastart)); datastart += salt.Length; string hash = data.Substring(datastart, Math.Min(sha.HashSize / 8 * 2, data.Length - datastart)); datastart += hash.Length; data = data.Substring(datastart); data = DecryptSequenceNoHash(data, encryptedTypes, password, yubi); // check the hash byte[] compareplain = StringToByteArray(salt + data); string comparehash = ByteArrayToString(sha.ComputeHash(compareplain)); if (string.Compare(comparehash, hash) != 0) { throw new BadPasswordException(); } } return data; } /// /// Decrypt a string sequence using the selected encryption types /// /// hex coded string sequence to decrypt /// Encryption types /// optional password /// optional yubi data /// /// decrypted string sequence private static string DecryptSequenceNoHash(string data, PasswordTypes encryptedTypes, string password, YubiKey yubi, bool decode = false) { try { // reverse order they were encrypted if ((encryptedTypes & PasswordTypes.Machine) != 0) { // we are going to decrypt with the Windows local machine key byte[] cipher = Authenticator.StringToByteArray(data); byte[] plain = ProtectedData.Unprotect(cipher, null, DataProtectionScope.LocalMachine); if (decode == true) { data = Encoding.UTF8.GetString(plain, 0, plain.Length); } else { data = ByteArrayToString(plain); } } if ((encryptedTypes & PasswordTypes.User) != 0) { // we are going to decrypt with the Windows User account key byte[] cipher = StringToByteArray(data); byte[] plain = ProtectedData.Unprotect(cipher, null, DataProtectionScope.CurrentUser); if (decode == true) { data = Encoding.UTF8.GetString(plain, 0, plain.Length); } else { data = ByteArrayToString(plain); } } if ((encryptedTypes & PasswordTypes.Explicit) != 0) { // we use an explicit password to encrypt data if (string.IsNullOrEmpty(password) == true) { throw new EncryptedSecretDataException(); } data = Authenticator.Decrypt(data, password, true); if (decode == true) { byte[] plain = Authenticator.StringToByteArray(data); data = Encoding.UTF8.GetString(plain, 0, plain.Length); } } if ((encryptedTypes & PasswordTypes.YubiKeySlot1) != 0 || (encryptedTypes & PasswordTypes.YubiKeySlot2) != 0) { if (string.IsNullOrEmpty(yubi.Info.Error) == false) { throw new BadYubiKeyException("Unable to detect YubiKey"); } if (yubi.Info.Status.VersionMajor == 0) { throw new BadYubiKeyException("Please insert your YubiKey"); } int slot = ((encryptedTypes & PasswordTypes.YubiKeySlot1) != 0 ? 1 : 2); string seed = data.Substring(0, SALT_LENGTH * 2); data = data.Substring(seed.Length); byte[] key = yubi.ChallengeResponse(slot, StringToByteArray(seed)); data = Authenticator.Decrypt(data, key); if (decode == true) { byte[] plain = Authenticator.StringToByteArray(data); data = Encoding.UTF8.GetString(plain, 0, plain.Length); } yubi.YubiData.Seed = seed; yubi.YubiData.Data = key; } } catch (EncryptedSecretDataException) { throw; } catch (BadYubiKeyException) { throw; } catch (ChallengeResponseException ex) { throw new BadYubiKeyException("Please check your YubiKey or touch the flashing button", ex); } catch (Exception ex) { throw new BadPasswordException(ex.Message, ex); } return data; } /// /// Downgrade SHA256 or MD5 to SHA1 to be FIPS compliant /// public static HashAlgorithm SafeHasher(string name) { try { if (name == "SHA512") { return SHA512.Create(); } if (name == "SHA256") { return SHA256.Create(); } if (name == "MD5") { return MD5.Create(); } return SHA1.Create(); } catch (Exception) { // FIPS only allows SHA1 before Windows 10 return SHA1.Create(); } } public static string EncryptSequence(string data, PasswordTypes passwordType, string password, YubiKey yubi) { // get hash of original var random = new RNGCryptoServiceProvider(); byte[] saltbytes = new byte[SALT_LENGTH]; random.GetBytes(saltbytes); string salt = ByteArrayToString(saltbytes); string hash; //using (var sha = new SHA256Managed()) using (var sha = SafeHasher("SHA256")) { byte[] plain = StringToByteArray(salt + data); hash = ByteArrayToString(sha.ComputeHash(plain)); } if ((passwordType & PasswordTypes.YubiKeySlot1) != 0 || (passwordType & PasswordTypes.YubiKeySlot2) != 0) { if (yubi.YubiData.Length == 0) { byte[] seed = new byte[SALT_LENGTH]; random = new RNGCryptoServiceProvider(); random.GetBytes(seed); // we encrypt the data using the hash of a random string from the YubiKey int slot = ((passwordType & PasswordTypes.YubiKeySlot1) != 0 ? 1 : 2); yubi.YubiData.Data = yubi.ChallengeResponse(slot, seed); yubi.YubiData.Seed = Authenticator.ByteArrayToString(seed); } byte[] key = yubi.YubiData.Data; string encrypted = Encrypt(data, key); // test the encryption string decrypted = Decrypt(encrypted, key); if (string.Compare(data, decrypted) != 0) { throw new InvalidEncryptionException(data, password, encrypted, decrypted); } data = yubi.YubiData.Seed + encrypted; } if ((passwordType & PasswordTypes.Explicit) != 0) { string encrypted = Encrypt(data, password); // test the encryption string decrypted = Decrypt(encrypted, password, true); if (string.Compare(data, decrypted) != 0) { throw new InvalidEncryptionException(data, password, encrypted, decrypted); } data = encrypted; } if ((passwordType & PasswordTypes.User) != 0) { // we encrypt the data using the Windows User account key byte[] plain = StringToByteArray(data); byte[] cipher = ProtectedData.Protect(plain, null, DataProtectionScope.CurrentUser); data = ByteArrayToString(cipher); } if ((passwordType & PasswordTypes.Machine) != 0) { // we encrypt the data using the Local Machine account key byte[] plain = StringToByteArray(data); byte[] cipher = ProtectedData.Protect(plain, null, DataProtectionScope.LocalMachine); data = ByteArrayToString(cipher); } // prepend the salt + hash return ENCRYPTION_HEADER + salt + hash + data; } /// /// Encrypt a string with a given key /// /// data to encrypt - hex representation of byte array /// key to use to encrypt /// hex coded encrypted string public static string Encrypt(string plain, string password) { byte[] passwordBytes = Encoding.UTF8.GetBytes(password); // build a new salt RNGCryptoServiceProvider rg = new RNGCryptoServiceProvider(); byte[] saltbytes = new byte[SALT_LENGTH]; rg.GetBytes(saltbytes); string salt = Authenticator.ByteArrayToString(saltbytes); // build our PBKDF2 key #if NETCF PBKDF2 kg = new PBKDF2(passwordBytes, saltbytes, PBKDF2_ITERATIONS); #else Rfc2898DeriveBytes kg = new Rfc2898DeriveBytes(passwordBytes, saltbytes, PBKDF2_ITERATIONS); #endif byte[] key = kg.GetBytes(PBKDF2_KEYSIZE); return salt + Encrypt(plain, key); } /// /// Encrypt a string with a byte array key /// /// data to encrypt - hex representation of byte array /// key to use to encrypt /// hex coded encrypted string public static string Encrypt(string plain, byte[] key) { byte[] inBytes = Authenticator.StringToByteArray(plain); // get our cipher BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new BlowfishEngine(), new ISO10126d2Padding()); cipher.Init(true, new KeyParameter(key)); // encrypt data int osize = cipher.GetOutputSize(inBytes.Length); byte[] outBytes = new byte[osize]; int olen = cipher.ProcessBytes(inBytes, 0, inBytes.Length, outBytes, 0); olen += cipher.DoFinal(outBytes, olen); if (olen < osize) { byte[] t = new byte[olen]; Array.Copy(outBytes, 0, t, 0, olen); outBytes = t; } // return encoded byte->hex string return Authenticator.ByteArrayToString(outBytes); } /// /// Decrypt a hex-coded string using our MD5 or PBKDF2 generated key /// /// data string to be decrypted /// decryption key /// flag to indicate we are using PBKDF2 to generate derived key /// hex coded decrypted string public static string Decrypt(string data, string password, bool PBKDF2) { byte[] key; byte[] saltBytes = Authenticator.StringToByteArray(data.Substring(0, SALT_LENGTH * 2)); if (PBKDF2 == true) { // extract the salt from the data byte[] passwordBytes = Encoding.UTF8.GetBytes(password); // build our PBKDF2 key #if NETCF PBKDF2 kg = new PBKDF2(passwordBytes, saltbytes, 2000); #else Rfc2898DeriveBytes kg = new Rfc2898DeriveBytes(passwordBytes, saltBytes, PBKDF2_ITERATIONS); #endif key = kg.GetBytes(PBKDF2_KEYSIZE); } else { // extract the salt from the data byte[] passwordBytes = Encoding.Default.GetBytes(password); key = new byte[saltBytes.Length + passwordBytes.Length]; Array.Copy(saltBytes, key, saltBytes.Length); Array.Copy(passwordBytes, 0, key, saltBytes.Length, passwordBytes.Length); // build out combined key MD5 md5 = MD5.Create(); key = md5.ComputeHash(key); } // extract the actual data to be decrypted byte[] inBytes = Authenticator.StringToByteArray(data.Substring(SALT_LENGTH * 2)); // get cipher BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new BlowfishEngine(), new ISO10126d2Padding()); cipher.Init(false, new KeyParameter(key)); // decrypt the data int osize = cipher.GetOutputSize(inBytes.Length); byte[] outBytes = new byte[osize]; try { int olen = cipher.ProcessBytes(inBytes, 0, inBytes.Length, outBytes, 0); olen += cipher.DoFinal(outBytes, olen); if (olen < osize) { byte[] t = new byte[olen]; Array.Copy(outBytes, 0, t, 0, olen); outBytes = t; } } catch (Exception) { // an exception is due to bad password throw new BadPasswordException(); } // return encoded string return Authenticator.ByteArrayToString(outBytes); } /// /// Decrypt a hex-encoded string with a byte array key /// /// hex-encoded string /// key for decryption /// hex-encoded plain text public static string Decrypt(string data, byte[] key) { // the actual data to be decrypted byte[] inBytes = Authenticator.StringToByteArray(data); // get cipher BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new BlowfishEngine(), new ISO10126d2Padding()); cipher.Init(false, new KeyParameter(key)); // decrypt the data int osize = cipher.GetOutputSize(inBytes.Length); byte[] outBytes = new byte[osize]; try { int olen = cipher.ProcessBytes(inBytes, 0, inBytes.Length, outBytes, 0); olen += cipher.DoFinal(outBytes, olen); if (olen < osize) { byte[] t = new byte[olen]; Array.Copy(outBytes, 0, t, 0, olen); outBytes = t; } } catch (Exception) { // an exception is due to bad password throw new BadPasswordException(); } // return encoded string return Authenticator.ByteArrayToString(outBytes); } /// /// Wrapped TryParse for compatibility with NETCF35 to simulate long.TryParse /// /// string of value to parse /// out long value /// true if value was parsed protected internal static bool LongTryParse(string s, out long val) { #if NETCF try { val = long.Parse(s); return true; } catch (Exception ) { val = 0; return false; } #else return long.TryParse(s, out val); #endif } #endregion #region ICloneable /// /// Clone the current object /// /// return clone public object Clone() { // we only need to do shallow copy return this.MemberwiseClone(); } #endregion #if NETCF /// /// Private class that implements PBKDF2 needed for older NETCF. Implemented from http://en.wikipedia.org/wiki/PBKDF2. /// private class PBKDF2 { /// /// Our digest /// private HMac m_mac; /// /// Digest length /// private int m_hlen; /// /// Base password /// private byte[] m_password; /// /// Salt /// private byte[] m_salt; /// /// Number of iterations /// private int m_iterations; /// /// Create a new PBKDF2 object /// public PBKDF2(byte[] password, byte[] salt, int iterations) { m_password = password; m_salt = salt; m_iterations = iterations; m_mac = new HMac(new Sha1Digest()); m_hlen = m_mac.GetMacSize(); } /// /// Calculate F. /// F(P,S,c,i) = U1 ^ U2 ^ ... ^ Uc /// Where F is an xor of c iterations of chained PRF. First iteration of PRF uses master password P as PRF key and salt concatenated to i. Second and greater PRF uses P and output of previous PRF computation: /// /// /// /// /// /// /// private void F(byte[] P, byte[] S, int c, byte[] i, byte[] DK, int DKoffset) { // first iteration (ses master password P as PRF key and salt concatenated to i) byte[] buf = new byte[m_hlen]; ICipherParameters param = new KeyParameter(P); m_mac.Init(param); m_mac.BlockUpdate(S, 0, S.Length); m_mac.BlockUpdate(i, 0, i.Length); m_mac.DoFinal(buf, 0); Array.Copy(buf, 0, DK, DKoffset, buf.Length); // remaining iterations (uses P and output of previous PRF computation) for (int iter = 1; iter < c; iter++) { m_mac.Init(param); m_mac.BlockUpdate(buf, 0, buf.Length); m_mac.DoFinal(buf, 0); for (int j=buf.Length-1; j >= 0; j--) { DK[DKoffset + j] ^= buf[j]; } } } /// /// Calculate a derived key of dkLen bytes long from our initial password and salt /// /// Length of desired key to be returned /// derived key of dkLen bytes public byte[] GetBytes(int dkLen) { // For each hLen-bit block Ti of derived key DK, computing is as follows: // DK = T1 || T2 || ... || Tdklen/hlen // Ti = F(P,S,c,i) int chunks = (dkLen + m_hlen - 1) / m_hlen; byte[] DK = new byte[chunks * m_hlen]; byte[] idata = new byte[4]; for (int i = 1; i <= chunks; i++) { idata[0] = (byte)((uint)i >> 24); idata[1] = (byte)((uint)i >> 16); idata[2] = (byte)((uint)i >> 8); idata[3] = (byte)i; F(m_password, m_salt, m_iterations, idata, DK, (i-1) * m_hlen); } if (DK.Length > dkLen) { byte[] reduced = new byte[dkLen]; Array.Copy(DK, 0, reduced, 0, dkLen); DK = reduced; } return DK; } } #if NUNIT /// Test against standard test vectors and one of our own of the correct iterations. /// http://tools.ietf.org/html/draft-josefsson-pbkdf2-test-vectors-00 [Test] public static void TestPBKDF2() { PBKDF2 kg; byte[] DK; byte[] tv1 = new byte[] { 0x0c, 0x60, 0xc8, 0x0f, 0x96, 0x1f, 0x0e, 0x71, 0xf3, 0xa9, 0xb5, 0x24, 0xaf, 0x60, 0x12, 0x06, 0x2f, 0xe0, 0x37, 0xa6 }; kg = new PBKDF2(Encoding.Default.GetBytes("password"), Encoding.Default.GetBytes("salt"), 1); DK = kg.GetBytes(20); Assert.AreEqual(DK, tv1); byte[] tv2 = new byte[] { 0xea, 0x6c, 0x01, 0x4d, 0xc7, 0x2d, 0x6f, 0x8c, 0xcd, 0x1e, 0xd9, 0x2a, 0xce, 0x1d, 0x41, 0xf0, 0xd8, 0xde, 0x89, 0x57 }; kg = new PBKDF2(Encoding.Default.GetBytes("password"), Encoding.Default.GetBytes("salt"), 2); DK = kg.GetBytes(20); Assert.AreEqual(DK, tv2); byte[] tv3 = new byte[] { 0x4b, 0x00, 0x79, 0x01, 0xb7, 0x65, 0x48, 0x9a, 0xbe, 0xad, 0x49, 0xd9, 0x26, 0xf7, 0x21, 0xd0, 0x65, 0xa4, 0x29, 0xc1 }; kg = new PBKDF2(Encoding.Default.GetBytes("password"), Encoding.Default.GetBytes("salt"), 4096); DK = kg.GetBytes(20); Assert.AreEqual(DK, tv3); byte[] tv4 = new byte[] { 0x2f, 0x25, 0x5b, 0x3a, 0x95, 0x46, 0x3c, 0x76, 0x62, 0x1f, 0x06, 0x80, 0xa2, 0xb3, 0x35, 0xad, 0x90, 0x3b, 0x85, 0xde }; kg = new PBKDF2(Encoding.Default.GetBytes("VXFr[24c=6(D8He"), Encoding.Default.GetBytes("salt"), 1000); DK = kg.GetBytes(20); Assert.AreEqual(DK, tv4); } #endif #endif } }