/
diev
/
winauth
Обзор
Документация
Войти
/
diev
/
winauth
Код
Запросы
0
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
Authenticator/Authenticator.cs
1 537 строк
48 KB
WinAuth
#524 Fix use of SHA256 and MD5 to downgrade to SHA1 when FIPS enabled
10 авг 2017, 08:05
10 авг 2017, 08:05
4139786
Код
Авторство
О чём код?
/* * 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 <http://www.gnu.org/licenses/>. */ 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 { /// <summary> /// Class that implements base RFC 4226 an RFC 6238 authenticator /// </summary> public abstract class Authenticator : ICloneable { /// <summary> /// Number of bytes making up the salt /// </summary> private const int SALT_LENGTH = 8; /// <summary> /// Number of iterations in PBKDF2 key generation /// </summary> private const int PBKDF2_ITERATIONS = 2000; /// <summary> /// Size of derived PBKDF2 key /// </summary> private const int PBKDF2_KEYSIZE = 256; /// <summary> /// Version for encrpytion changes /// </summary> private static string ENCRYPTION_HEADER = Authenticator.ByteArrayToString(Encoding.UTF8.GetBytes("WINAUTH3")); /// <summary> /// Default number of digits in code /// </summary> public const int DEFAULT_CODE_DIGITS = 6; /// <summary> /// Default period of 30s /// </summary> public const int DEFAULT_PERIOD = 30; /// <summary> /// Type of password to use to encrypt secret data /// </summary> public enum PasswordTypes { None = 0, Explicit = 1, User = 2, Machine = 4, YubiKeySlot1 = 8, YubiKeySlot2 = 16 } /// <summary> /// HMAC hashing algorithm types /// </summary> public enum HMACTypes { SHA1 = 0, SHA256 = 1, SHA512 = 2 } public const HMACTypes DEFAULT_HMAC_TYPE = HMACTypes.SHA1; #region Authenticator data /// <summary> /// Serial number of authenticator /// </summary> //public virtual string Serial { get; set; } /// <summary> /// Secret key used for Authenticator /// </summary> public byte[] SecretKey { get; set; } /// <summary> /// Time difference in milliseconds of our machine and server /// </summary> public long ServerTimeDiff { get; set; } /// <summary> /// Time of last synced /// </summary> public long LastServerTime { get; set; } /// <summary> /// Type of password used to encrypt secretdata /// </summary> public PasswordTypes PasswordType { get; private set; } /// <summary> /// Password used to encrypt secretdata (if PasswordType == Explict) /// </summary> protected string Password { get; set; } /// <summary> /// Hash of secret data to detect changes /// </summary> protected byte[] SecretHash { get; private set; } public bool RequiresPassword { get; private set; } /// <summary> /// The data current saved with the current encryption and/or password (might be none) /// </summary> protected string EncryptedData { get; private set; } /// <summary> /// Number of digits returned in code (default is 6) /// </summary> public int CodeDigits { get; set; } /// <summary> /// Hashing algorithm used for OTP generation (default is SHA1) /// </summary> public HMACTypes HMACType { get; set; } /// <summary> /// Period in seconds for next code /// </summary> public int Period { get; set; } /// <summary> /// Name of issuer /// </summary> public virtual string Issuer { get; set; } /// <summary> /// Get/set the combined secret data value /// </summary> 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; } } } /// <summary> /// Advanced script saved with authenticator so it is also encrypted /// </summary> //public string Script {get; set;} /// <summary> /// Get the server time since 1/1/70 /// </summary> public long ServerTime { get { return CurrentTime + ServerTimeDiff; } } /// <summary> /// Calculate the code interval based on the calculated server time /// </summary> public long CodeInterval { get { // calculate the code interval; the server's time div 30,000 return (CurrentTime + ServerTimeDiff) / ((long)this.Period * 1000L); } } /// <summary> /// Get the current code for the authenticator. /// </summary> /// <returns>authenticator code</returns> public string CurrentCode { get { if (this.SecretKey == null && this.EncryptedData != null) { throw new EncryptedSecretDataException(); } return CalculateCode(false); } } #endregion /// <summary> /// Static initializer /// </summary> static Authenticator() { // Issue#71: remove the default .net expect header, which can cause issues (http://stackoverflow.com/questions/566437/) System.Net.ServicePointManager.Expect100Continue = false; } /// <summary> /// Create a new Authenticator object /// </summary> public Authenticator(int codeDigits = DEFAULT_CODE_DIGITS, HMACTypes hmacType = HMACTypes.SHA1, int period = DEFAULT_PERIOD) { CodeDigits = codeDigits; HMACType = hmacType; Period = period; } /// <summary> /// Calculate the current code for the authenticator. /// </summary> /// <param name="resyncTime">flag to resync time</param> /// <returns>authenticator code</returns> 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; } /// <summary> /// Synchorise this authenticator's time with server time. We update our data record with the difference from our UTC time. /// </summary> 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; } /// <summary> /// Convert the string password types into the PasswordTypes type /// </summary> /// <param name="passwordTypes">string version of password types</param> /// <returns>PasswordTypes value</returns> 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; } /// <summary> /// Encode the PasswordTypes type into a string for storing in config /// </summary> /// <param name="passwordType">PasswordTypes value</param> /// <returns>string version</returns> 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; } } /// <summary> /// Write this authenticator into an XmlWriter /// </summary> /// <param name="writer">XmlWriter to receive authenticator</param> 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(); } /* /// <summary> /// Write this authenticator into an XmlWriter /// </summary> /// <param name="writer">XmlWriter to receive authenticator</param> 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); } } */ /// <summary> /// Virtual function to write any class specific xml nodes into the writer /// </summary> /// <param name="writer">XmlWriter to write data</param> protected virtual void WriteExtraXml(XmlWriter writer) { } #endregion #region Utility functions /// <summary> /// Create a one-time pad by generating a random block and then taking a hash of that block as many times as needed. /// </summary> /// <param name="length">desired pad length</param> /// <returns>array of bytes conatining random data</returns> 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; } /// <summary> /// Get the milliseconds since 1/1/70 (same as Java currentTimeMillis) /// </summary> public static long CurrentTime { get { return Convert.ToInt64((DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalMilliseconds); } } /// <summary> /// Convert a hex string into a byte array. E.g. "001f406a" -> byte[] {0x00, 0x1f, 0x40, 0x6a} /// </summary> /// <param name="hex">hex string to convert</param> /// <returns>byte[] of hex string</returns> 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; } /// <summary> /// Convert a byte array into a ascii hex string, e.g. byte[]{0x00,0x1f,0x40,ox6a} -> "001f406a" /// </summary> /// <param name="bytes">byte array to convert</param> /// <returns>string version of byte array</returns> public static string ByteArrayToString(byte[] bytes) { // Use BitConverter, but it sticks dashes in the string return BitConverter.ToString(bytes).Replace("-", string.Empty); } /// <summary> /// Decrypt a string sequence using the selected encryption types /// </summary> /// <param name="data">hex coded string sequence to decrypt</param> /// <param name="encryptedTypes">Encryption types</param> /// <param name="password">optional password</param> /// <param name="yubidata">optional yubi data</param> /// <param name="decode"></param> /// <returns>decrypted string sequence</returns> 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; } /// <summary> /// Decrypt a string sequence using the selected encryption types /// </summary> /// <param name="data">hex coded string sequence to decrypt</param> /// <param name="encryptedTypes">Encryption types</param> /// <param name="password">optional password</param> /// <param name="yubidata">optional yubi data</param> /// <param name="decode"></param> /// <returns>decrypted string sequence</returns> 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; } /// <summary> /// Downgrade SHA256 or MD5 to SHA1 to be FIPS compliant /// </summary> 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; } /// <summary> /// Encrypt a string with a given key /// </summary> /// <param name="plain">data to encrypt - hex representation of byte array</param> /// <param name="password">key to use to encrypt</param> /// <returns>hex coded encrypted string</returns> 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); } /// <summary> /// Encrypt a string with a byte array key /// </summary> /// <param name="plain">data to encrypt - hex representation of byte array</param> /// <param name="passwordBytes">key to use to encrypt</param> /// <returns>hex coded encrypted string</returns> 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); } /// <summary> /// Decrypt a hex-coded string using our MD5 or PBKDF2 generated key /// </summary> /// <param name="data">data string to be decrypted</param> /// <param name="key">decryption key</param> /// <param name="PBKDF2">flag to indicate we are using PBKDF2 to generate derived key</param> /// <returns>hex coded decrypted string</returns> 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); } /// <summary> /// Decrypt a hex-encoded string with a byte array key /// </summary> /// <param name="data">hex-encoded string</param> /// <param name="key">key for decryption</param> /// <returns>hex-encoded plain text</returns> 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); } /// <summary> /// Wrapped TryParse for compatibility with NETCF35 to simulate long.TryParse /// </summary> /// <param name="s">string of value to parse</param> /// <param name="val">out long value</param> /// <returns>true if value was parsed</returns> 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 /// <summary> /// Clone the current object /// </summary> /// <returns>return clone</returns> public object Clone() { // we only need to do shallow copy return this.MemberwiseClone(); } #endregion #if NETCF /// <summary> /// Private class that implements PBKDF2 needed for older NETCF. Implemented from http://en.wikipedia.org/wiki/PBKDF2. /// </summary> private class PBKDF2 { /// <summary> /// Our digest /// </summary> private HMac m_mac; /// <summary> /// Digest length /// </summary> private int m_hlen; /// <summary> /// Base password /// </summary> private byte[] m_password; /// <summary> /// Salt /// </summary> private byte[] m_salt; /// <summary> /// Number of iterations /// </summary> private int m_iterations; /// <summary> /// Create a new PBKDF2 object /// </summary> 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(); } /// <summary> /// 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: /// </summary> /// <param name="P"></param> /// <param name="S"></param> /// <param name="c"></param> /// <param name="i"></param> /// <param name="DK"></param> /// <param name="DKoffset"></param> 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]; } } } /// <summary> /// Calculate a derived key of dkLen bytes long from our initial password and salt /// </summary> /// <param name="dkLen">Length of desired key to be returned</param> /// <returns>derived key of dkLen bytes</returns> 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 } }