mirror of
https://github.com/BeyondDimension/SteamTools.git
synced 2026-10-18 14:41:26 +08:00
44ee8a0047
布局性能调优 令牌交易确认实现
1538 lines
54 KiB
C#
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
|
|
|
|
}
|
|
}
|