support for Keyfiles like in Keepass 2.47

This commit is contained in:
Philipp Crocoll
2021-01-12 07:31:18 +01:00
parent 533021da3f
commit d9bcac600c
5 changed files with 637 additions and 40 deletions

View File

@@ -72,6 +72,7 @@
<Compile Include="Cryptography\KeyDerivation\KdfParameters.cs" />
<Compile Include="Cryptography\KeyDerivation\KdfPool.cs" />
<Compile Include="IDatabaseFormat.cs" />
<Compile Include="Keys\KcpKeyFile.Xml.cs" />
<Compile Include="Kp2aLog.cs" />
<Compile Include="Resources\Resource.designer.cs" />
<Compile Include="Resources\KLRes.Generated.cs" />
@@ -161,6 +162,7 @@
<Compile Include="Utility\UrlUtil.cs" />
<Compile Include="Utility\TimeUtil.cs" />
<Compile Include="Delegates\Handlers.cs" />
<Compile Include="Utility\XmlUtilEx.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

View File

@@ -0,0 +1,281 @@
/*
KeePass Password Safe - The Open-Source Password Manager
Copyright (C) 2003-2021 Dominik Reichl <dominik.reichl@t-online.de>
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 2 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, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Xml.Serialization;
using KeePassLib.Cryptography;
using KeePassLib.Resources;
using KeePassLib.Serialization;
using KeePassLib.Utility;
namespace KeePassLib.Keys
{
[XmlType("KeyFile")]
public sealed class KfxFile
{
private const ulong KfxVersionCriticalMask = 0xFFFF000000000000UL;
private const int KfxDataHashLength = 4;
private KfxMeta m_meta = new KfxMeta();
public KfxMeta Meta
{
get { return m_meta; }
set
{
if(value == null) throw new ArgumentNullException("value");
m_meta = value;
}
}
private KfxKey m_key = new KfxKey();
public KfxKey Key
{
get { return m_key; }
set
{
if(value == null) throw new ArgumentNullException("value");
m_key = value;
}
}
public static KfxFile Create(ulong uVersion, byte[] pbKey, byte[] pbHash)
{
if(pbKey == null) throw new ArgumentNullException("pbKey");
if(pbKey.Length == 0) throw new ArgumentOutOfRangeException("pbKey");
if(uVersion == 0) uVersion = 0x0002000000000000;
// Null hash: generate one, empty hash: store no hash
if(pbHash == null) pbHash = HashData(pbKey);
VerifyHash(pbKey, pbHash);
KfxFile kf = new KfxFile();
if(uVersion == 0x0001000000000000)
kf.Meta.Version = "1.00"; // KeePass <= 2.46 used two zeros
else kf.Meta.Version = StrUtil.VersionToString(uVersion, 2);
if(uVersion == 0x0001000000000000)
kf.Key.Data.Value = Convert.ToBase64String(pbKey);
else if(uVersion == 0x0002000000000000)
{
kf.Key.Data.Value = FormatKeyHex(pbKey, 3);
if(pbHash.Length != 0)
kf.Key.Data.Hash = MemUtil.ByteArrayToHexString(pbHash);
}
else throw new NotSupportedException(KLRes.FileVersionUnsupported);
return kf;
}
internal static KfxFile Create(ulong uVersion, string strKey, string strHash)
{
byte[] pbKey = ParseKey(uVersion, strKey);
byte[] pbHash = ((strHash != null) ? ParseHash(strHash) : null);
return Create(uVersion, pbKey, pbHash);
}
internal static bool CanLoad(string strFilePath)
{
if(string.IsNullOrEmpty(strFilePath)) { Debug.Assert(false); return false; }
try
{
IOConnectionInfo ioc = IOConnectionInfo.FromPath(strFilePath);
using(Stream s = IOConnection.OpenRead(ioc))
{
return (Load(s) != null);
}
}
catch(Exception) { }
return false;
}
public static KfxFile Load(Stream s)
{
return XmlUtilEx.Deserialize<KfxFile>(s);
}
public void Save(Stream s)
{
XmlUtilEx.Serialize<KfxFile>(s, this, true);
}
private static string FormatKeyHex(byte[] pb, int cTabs)
{
StringBuilder sb = new StringBuilder();
string str = MemUtil.ByteArrayToHexString(pb);
for(int i = 0; i < str.Length; ++i)
{
if((i & 0x1F) == 0)
{
sb.AppendLine();
sb.Append('\t', cTabs);
}
else if((i & 0x07) == 0) sb.Append(' ');
sb.Append(str[i]);
}
sb.AppendLine();
if(cTabs > 0) sb.Append('\t', cTabs - 1);
return sb.ToString();
}
private ulong GetVersion()
{
string str = m_meta.Version;
if(string.IsNullOrEmpty(str)) return 0;
return StrUtil.ParseVersion(str);
}
public byte[] GetKey()
{
ulong uVersion = GetVersion();
byte[] pbKey = ParseKey(uVersion, m_key.Data.Value);
if((pbKey == null) || (pbKey.Length == 0))
throw new FormatException(KLRes.FileCorrupted);
byte[] pbHash = ParseHash(m_key.Data.Hash);
VerifyHash(pbKey, pbHash);
return pbKey;
}
private static byte[] HashData(byte[] pb)
{
return MemUtil.Mid(CryptoUtil.HashSha256(pb), 0, KfxDataHashLength);
}
private static void VerifyHash(byte[] pbKey, byte[] pbHash)
{
// The hash is optional; empty hash means success
if((pbHash == null) || (pbHash.Length == 0)) return;
byte[] pbHashCmp = HashData(pbKey);
if(!MemUtil.ArraysEqual(pbHash, pbHashCmp))
throw new Exception("Keyfile hash mismatch!");
}
private static byte[] ParseKey(ulong uVersion, string strKey)
{
if(strKey == null) throw new ArgumentNullException("strKey");
strKey = StrUtil.RemoveWhiteSpace(strKey);
if(string.IsNullOrEmpty(strKey)) return MemUtil.EmptyByteArray;
uVersion &= KfxVersionCriticalMask;
byte[] pbKey;
if(uVersion == 0x0001000000000000)
pbKey = Convert.FromBase64String(strKey);
else if(uVersion == 0x0002000000000000)
pbKey = ParseHex(strKey);
else throw new NotSupportedException(KLRes.FileVersionUnsupported);
return pbKey;
}
private static byte[] ParseHash(string strHash)
{
return ParseHex(strHash);
}
private static byte[] ParseHex(string str)
{
if(str == null) throw new ArgumentNullException("str");
if(str.Length == 0) return MemUtil.EmptyByteArray;
if(((str.Length & 1) != 0) || !StrUtil.IsHexString(str, true))
throw new FormatException();
return MemUtil.HexStringToByteArray(str);
}
}
public sealed class KfxMeta
{
private string m_strVersion = string.Empty;
[DefaultValue("")]
public string Version
{
get { return m_strVersion; }
set
{
if(value == null) throw new ArgumentNullException("value");
m_strVersion = value;
}
}
}
public sealed class KfxKey
{
private KfxData m_data = new KfxData();
public KfxData Data
{
get { return m_data; }
set
{
if(value == null) throw new ArgumentNullException("value");
m_data = value;
}
}
}
public sealed class KfxData
{
private string m_strHash = string.Empty;
[DefaultValue("")]
[XmlAttribute("Hash")]
public string Hash
{
get { return m_strHash; }
set
{
if(value == null) throw new ArgumentNullException("value");
m_strHash = value;
}
}
private string m_strValue = string.Empty;
[DefaultValue("")]
[XmlText]
public string Value
{
get { return m_strValue; }
set
{
if(value == null) throw new ArgumentNullException("value");
m_strValue = value;
}
}
}
}

View File

@@ -127,10 +127,9 @@ namespace KeePassLib.Keys
#endif
}
byte[] pbKey = LoadXmlKeyFile(pbFileData);
if(pbKey == null) pbKey = LoadKeyFile(pbFileData);
byte[] pbKey = LoadKeyFile(pbFileData);
if(pbKey == null) throw new InvalidOperationException();
if (pbKey == null) throw new InvalidOperationException();
m_ioc = iocKeyFile;
m_pbKeyData = new ProtectedBinary(true, pbKey);
@@ -150,54 +149,60 @@ namespace KeePassLib.Keys
// }
private static byte[] LoadKeyFile(byte[] pbFileData)
{
if(pbFileData == null) { Debug.Assert(false); return null; }
private static byte[] LoadKeyFile(byte[] pbFileData)
{
if (pbFileData == null) throw new ArgumentNullException("pbFileData");
int iLength = pbFileData.Length;
byte[] pbKey = LoadKeyFileXml(pbFileData);
if (pbKey != null) return pbKey;
byte[] pbKey = null;
if(iLength == 32) pbKey = LoadBinaryKey32(pbFileData);
else if(iLength == 64) pbKey = LoadHexKey32(pbFileData);
int cb = pbFileData.Length;
if (cb == 32) return pbFileData;
if(pbKey == null)
pbKey = CryptoUtil.HashSha256(pbFileData);
if (cb == 64)
{
pbKey = LoadKeyFileHex(pbFileData);
if (pbKey != null) return pbKey;
}
return pbKey;
}
return CryptoUtil.HashSha256(pbFileData);
}
private static byte[] LoadBinaryKey32(byte[] pbFileData)
{
if(pbFileData == null) { Debug.Assert(false); return null; }
if(pbFileData.Length != 32) { Debug.Assert(false); return null; }
private static byte[] LoadKeyFileXml(byte[] pbFileData)
{
KfxFile kf;
try
{
using (MemoryStream ms = new MemoryStream(pbFileData, false))
{
kf = KfxFile.Load(ms);
}
}
catch (Exception) { return null; }
return pbFileData;
}
// We have a syntactically valid XML key file;
// failing to verify the key should throw an exception
return ((kf != null) ? kf.GetKey() : null);
}
private static byte[] LoadHexKey32(byte[] pbFileData)
{
if(pbFileData == null) { Debug.Assert(false); return null; }
if(pbFileData.Length != 64) { Debug.Assert(false); return null; }
private static byte[] LoadKeyFileHex(byte[] pbFileData)
{
if (pbFileData == null) { Debug.Assert(false); return null; }
try
{
if(!StrUtil.IsHexString(pbFileData, true)) return null;
try
{
int cc = pbFileData.Length;
if ((cc & 1) != 0) { Debug.Assert(false); return null; }
string strHex = StrUtil.Utf8.GetString(pbFileData);
byte[] pbKey = MemUtil.HexStringToByteArray(strHex);
if((pbKey == null) || (pbKey.Length != 32))
{
Debug.Assert(false);
return null;
}
if (!StrUtil.IsHexString(pbFileData, true)) return null;
return pbKey;
}
catch(Exception) { Debug.Assert(false); }
return null;
}
string strHex = StrUtil.Utf8.GetString(pbFileData);
return MemUtil.HexStringToByteArray(strHex);
}
catch (Exception) { Debug.Assert(false); }
return null;
}
/// <summary>
/// Create a new, random key-file.
/// </summary>

View File

@@ -1734,5 +1734,24 @@ namespace KeePassLib.Utility
// behavior of Notepad (on Windows 10)
return str.Replace('\0', ' ');
}
internal static string RemoveWhiteSpace(string str)
{
if (str == null) { Debug.Assert(false); return string.Empty; }
int cc = str.Length;
if (cc == 0) return string.Empty;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < cc; ++i)
{
char ch = str[i];
if (char.IsWhiteSpace(ch)) continue;
sb.Append(ch);
}
return sb.ToString();
}
}
}

View File

@@ -0,0 +1,290 @@
/*
KeePass Password Safe - The Open-Source Password Manager
Copyright (C) 2003-2021 Dominik Reichl <dominik.reichl@t-online.de>
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 2 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, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using System.Xml.XPath;
using KeePassLib.Delegates;
using KeePassLib.Interfaces;
using KeePassLib.Serialization;
namespace KeePassLib.Utility
{
public static class XmlUtilEx
{
public static XmlDocument CreateXmlDocument()
{
XmlDocument d = new XmlDocument();
// .NET 4.5.2 and newer do not resolve external XML resources
// by default; for older .NET versions, we explicitly
// prevent resolving
d.XmlResolver = null; // Default in old .NET: XmlUrlResolver object
return d;
}
public static XmlReaderSettings CreateXmlReaderSettings()
{
XmlReaderSettings xrs = new XmlReaderSettings();
xrs.CloseInput = false;
xrs.IgnoreComments = true;
xrs.IgnoreProcessingInstructions = true;
xrs.IgnoreWhitespace = true;
#if KeePassUAP
xrs.DtdProcessing = DtdProcessing.Prohibit;
#else
// Also see PrepMonoDev.sh script
xrs.ProhibitDtd = true; // Obsolete in .NET 4, but still there
// xrs.DtdProcessing = DtdProcessing.Prohibit; // .NET 4 only
#endif
xrs.ValidationType = ValidationType.None;
xrs.XmlResolver = null;
return xrs;
}
public static XmlReader CreateXmlReader(Stream s)
{
if(s == null) { Debug.Assert(false); throw new ArgumentNullException("s"); }
return XmlReader.Create(s, CreateXmlReaderSettings());
}
public static XmlWriterSettings CreateXmlWriterSettings()
{
XmlWriterSettings xws = new XmlWriterSettings();
xws.CloseOutput = false;
xws.Encoding = StrUtil.Utf8;
xws.Indent = true;
xws.IndentChars = "\t";
xws.NewLineOnAttributes = false;
return xws;
}
public static XmlWriter CreateXmlWriter(Stream s)
{
if(s == null) { Debug.Assert(false); throw new ArgumentNullException("s"); }
return XmlWriter.Create(s, CreateXmlWriterSettings());
}
public static T Deserialize<T>(Stream s)
{
if(s == null) { Debug.Assert(false); throw new ArgumentNullException("s"); }
XmlSerializer xs = new XmlSerializer(typeof(T));
T t = default(T);
using(XmlReader xr = CreateXmlReader(s))
{
t = (T)xs.Deserialize(xr);
}
return t;
}
public static void Serialize<T>(Stream s, T t)
{
if(s == null) { Debug.Assert(false); throw new ArgumentNullException("s"); }
XmlSerializer xs = new XmlSerializer(typeof(T));
using(XmlWriter xw = CreateXmlWriter(s))
{
xs.Serialize(xw, t);
}
}
internal static void Serialize<T>(Stream s, T t, bool bRemoveXsdXsi)
{
// One way to remove the "xsd" and "xsi" namespace declarations
// is to use an XmlSerializerNamespaces object containing only
// a ""/"" pair; this seems to work, but Microsoft's
// documentation explicitly states that it isn't supported:
// https://docs.microsoft.com/en-us/dotnet/api/system.xml.serialization.xmlserializernamespaces
// There are other, more complex ways, but these either rely on
// undocumented details or require the type T to be modified.
string str;
using(MemoryStream ms = new MemoryStream())
{
Serialize<T>(ms, t);
str = StrUtil.Utf8.GetString(ms.ToArray());
}
Func<string, string, bool> fFindPfx = delegate(string strText, string strSub)
{
int i = strText.IndexOf(strSub, StringComparison.Ordinal);
if(i < 0) return false;
if(i == 0) return true;
return char.IsWhiteSpace(strText[i - 1]);
};
if(bRemoveXsdXsi)
{
if(!fFindPfx(str, "xsd:") && !fFindPfx(str, "xsi:"))
{
Debug.Assert(str.IndexOf("xmlns:xsd") > 0);
str = str.Replace(" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"", string.Empty);
Debug.Assert(str.IndexOf("xmlns:xsd") < 0);
Debug.Assert(str.IndexOf("xmlns:xsi") > 0);
str = str.Replace(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"", string.Empty);
Debug.Assert(str.IndexOf("xmlns:xsi") < 0);
}
else { Debug.Assert(false); } // "xsd"/"xsi" decl. may be required
}
MemUtil.Write(s, StrUtil.Utf8.GetBytes(str));
}
#if DEBUG
internal static void ValidateXml(string strXml, bool bReplaceStdEntities)
{
if(strXml == null) throw new ArgumentNullException("strXml");
if(strXml.Length == 0) { Debug.Assert(false); return; }
string str = strXml;
if(bReplaceStdEntities)
str = str.Replace("&nbsp;", "&#160;");
XmlDocument d = new XmlDocument();
d.LoadXml(str);
}
#endif
internal static XPathNodeIterator FindNodes(PwDatabase pd, string strXPath,
IStatusLogger sl, out XmlDocument xd)
{
if(pd == null) throw new ArgumentNullException("pd");
if(strXPath == null) { Debug.Assert(false); strXPath = string.Empty; }
KdbxFile kdbx = new KdbxFile(pd);
byte[] pbXml;
using(MemoryStream ms = new MemoryStream())
{
kdbx.Save(ms, null, KdbxFormat.PlainXml, sl);
pbXml = ms.ToArray();
}
string strXml = StrUtil.Utf8.GetString(pbXml);
xd = CreateXmlDocument();
xd.LoadXml(strXml);
XPathNavigator xpNav = xd.CreateNavigator();
return xpNav.Select(strXPath);
// XPathExpression xpExpr = xpNav.Compile(strXPath);
// xpExpr.SetContext(new XuXsltContext());
// return xpNav.Select(xpExpr);
}
/* private sealed class XuFnMatches : IXsltContextFunction
{
private readonly XPathResultType[] m_vArgTypes = new XPathResultType[] {
XPathResultType.String, XPathResultType.String, XPathResultType.String
};
public XPathResultType[] ArgTypes { get { return m_vArgTypes; } }
public int Maxargs { get { return 3; } }
public int Minargs { get { return 2; } }
public XPathResultType ReturnType { get { return XPathResultType.Boolean; } }
private static string GetArgString(object[] args, int i, string strDefault)
{
if(args == null) { Debug.Assert(false); return strDefault; }
if(i >= args.Length) return strDefault;
object o = args[i];
if(o == null) return strDefault;
XPathNodeIterator it = (o as XPathNodeIterator);
if(it != null) o = it.Current.Value;
return (o.ToString() ?? strDefault);
}
public object Invoke(XsltContext xsltContext, object[] args,
XPathNavigator docContext)
{
string strInput = GetArgString(args, 0, string.Empty);
string strPattern = GetArgString(args, 1, string.Empty);
string strFlags = GetArgString(args, 2, null);
RegexOptions ro = RegexOptions.None;
if(!string.IsNullOrEmpty(strFlags))
{
if(strFlags.IndexOf('s') >= 0) ro |= RegexOptions.Singleline;
if(strFlags.IndexOf('m') >= 0) ro |= RegexOptions.Multiline;
if(strFlags.IndexOf('i') >= 0) ro |= RegexOptions.IgnoreCase;
if(strFlags.IndexOf('x') >= 0) ro |= RegexOptions.IgnorePatternWhitespace;
}
return Regex.IsMatch(strInput, strPattern, ro);
}
}
private sealed class XuXsltContext : XsltContext
{
public override bool Whitespace { get { return false; } }
public override int CompareDocument(string baseUri, string nextbaseUri)
{
return string.CompareOrdinal(baseUri, nextbaseUri);
}
public override bool PreserveWhitespace(XPathNavigator node)
{
return false;
}
public override IXsltContextFunction ResolveFunction(string prefix,
string name, XPathResultType[] ArgTypes)
{
if(prefix != "kp") { Debug.Assert(false); return null; }
if(name == "matches") return new XuFnMatches();
Debug.Assert(false);
return null;
}
public override IXsltContextVariable ResolveVariable(string prefix,
string name)
{
Debug.Assert(false);
return null;
}
} */
}
}