Files
rmbgame 44ee8a0047 v1版本令牌导入修复
布局性能调优
令牌交易确认实现
2021-03-21 02:37:22 +08:00

1538 lines
54 KiB
C#

/*
* 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
}
}