diff --git a/ModernKeePassLib.Test/Cryptography/Cipher/AesTests.cs b/ModernKeePassLib.Test/Cryptography/Cipher/AesTests.cs new file mode 100644 index 0000000..04efde7 --- /dev/null +++ b/ModernKeePassLib.Test/Cryptography/Cipher/AesTests.cs @@ -0,0 +1,79 @@ +using System.IO; +using System.Text; +using ModernKeePassLib.Serialization; +using ModernKeePassLib.Cryptography.Cipher; +using ModernKeePassLib.Utility; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Parameters; +using Xunit; +using System.Security.Cryptography; +using ModernKeePassLib.Cryptography; + +namespace ModernKeePassLib.Test.Cryptography.Cipher +{ + public class AesTests + { + // Test vector (official ECB test vector #356) + private readonly byte[] _pbReferenceCt = + { + 0x75, 0xD1, 0x1B, 0x0E, 0x3A, 0x68, 0xC4, 0x22, + 0x3D, 0x88, 0xDB, 0xF0, 0x17, 0x97, 0x7D, 0xD7 + }; + private readonly byte[] _pbIv = new byte[16]; + private readonly byte[] _pbTestKey = new byte[32]; + private readonly byte[] _pbTestData = + { + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + [Fact] + public void TestEncryptStream() + { + var a = CryptoUtil.CreateAes(); + if (a.BlockSize != 128) // AES block size + { + //Debug.Assert(false); + a.BlockSize = 128; + } + + a.IV = _pbIv; + a.KeySize = 256; + a.Key = _pbTestKey; + a.Mode = CipherMode.ECB; + var iCrypt = a.CreateEncryptor(); + + iCrypt.TransformBlock(_pbTestData, 0, 16, _pbTestData, 0); + + Assert.True(MemUtil.ArraysEqual(_pbTestData, _pbReferenceCt)); + } + + [Fact] + public void TestDecryptStream() + { + // Possible Mono Bug? This only works with size >= 48 + using (var inStream = new MemoryStream(new byte[32])) + { + inStream.Write(_pbReferenceCt, 0, _pbReferenceCt.Length); + inStream.Position = 0; + var aes = new StandardAesEngine(); + using (var outStream = aes.DecryptStream(inStream, _pbTestKey, _pbIv)) + { + var outBytes = new BinaryReaderEx(outStream, Encoding.UTF8, string.Empty).ReadBytes(16); + Assert.True(MemUtil.ArraysEqual(outBytes, _pbTestData)); + } + } + } + + [Fact] + public void TestBouncyCastleAes() + { + var aesEngine = new AesEngine(); + //var parametersWithIv = new ParametersWithIV(new KeyParameter(pbTestKey), pbIV); + aesEngine.Init(true, new KeyParameter(_pbTestKey)); + Assert.Equal(_pbTestData.Length, aesEngine.GetBlockSize()); + aesEngine.ProcessBlock(_pbTestData, 0, _pbTestData, 0); + Assert.True(MemUtil.ArraysEqual(_pbReferenceCt,_pbTestData)); + } + } +} diff --git a/ModernKeePassLib.Test/Cryptography/Cipher/Chacha20Tests.cs b/ModernKeePassLib.Test/Cryptography/Cipher/Chacha20Tests.cs new file mode 100644 index 0000000..15b9dc3 --- /dev/null +++ b/ModernKeePassLib.Test/Cryptography/Cipher/Chacha20Tests.cs @@ -0,0 +1,203 @@ +using System; +using System.Diagnostics; +using System.IO; +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Cryptography.Cipher; +using ModernKeePassLib.Utility; +using Xunit; + +namespace ModernKeePassLib.Test.Cryptography.Cipher +{ + public class Chacha20Tests + { + [Fact] + public void TestChacha20Cipher() + { + // ====================================================== + // Test vector from RFC 7539, section 2.3.2 + + var pbKey = new byte[32]; + for (var i = 0; i < 32; ++i) pbKey[i] = (byte)i; + + var pbIV = new byte[12]; + pbIV[3] = 0x09; + pbIV[7] = 0x4A; + + var pbExpc = new byte[64] { + 0x10, 0xF1, 0xE7, 0xE4, 0xD1, 0x3B, 0x59, 0x15, + 0x50, 0x0F, 0xDD, 0x1F, 0xA3, 0x20, 0x71, 0xC4, + 0xC7, 0xD1, 0xF4, 0xC7, 0x33, 0xC0, 0x68, 0x03, + 0x04, 0x22, 0xAA, 0x9A, 0xC3, 0xD4, 0x6C, 0x4E, + 0xD2, 0x82, 0x64, 0x46, 0x07, 0x9F, 0xAA, 0x09, + 0x14, 0xC2, 0xD7, 0x05, 0xD9, 0x8B, 0x02, 0xA2, + 0xB5, 0x12, 0x9C, 0xD1, 0xDE, 0x16, 0x4E, 0xB9, + 0xCB, 0xD0, 0x83, 0xE8, 0xA2, 0x50, 0x3C, 0x4E + }; + + var pb = new byte[64]; + + using (var c = new ChaCha20Cipher(pbKey, pbIV)) + { + c.Seek(64, SeekOrigin.Begin); // Skip first block + c.Encrypt(pb, 0, pb.Length); + + Assert.True(MemUtil.ArraysEqual(pb, pbExpc)); + } + +#if DEBUG + // ====================================================== + // Test vector from RFC 7539, section 2.4.2 + + pbIV[3] = 0; + + pb = StrUtil.Utf8.GetBytes("Ladies and Gentlemen of the clas" + + @"s of '99: If I could offer you only one tip for " + + @"the future, sunscreen would be it."); + + pbExpc = new byte[] { + 0x6E, 0x2E, 0x35, 0x9A, 0x25, 0x68, 0xF9, 0x80, + 0x41, 0xBA, 0x07, 0x28, 0xDD, 0x0D, 0x69, 0x81, + 0xE9, 0x7E, 0x7A, 0xEC, 0x1D, 0x43, 0x60, 0xC2, + 0x0A, 0x27, 0xAF, 0xCC, 0xFD, 0x9F, 0xAE, 0x0B, + 0xF9, 0x1B, 0x65, 0xC5, 0x52, 0x47, 0x33, 0xAB, + 0x8F, 0x59, 0x3D, 0xAB, 0xCD, 0x62, 0xB3, 0x57, + 0x16, 0x39, 0xD6, 0x24, 0xE6, 0x51, 0x52, 0xAB, + 0x8F, 0x53, 0x0C, 0x35, 0x9F, 0x08, 0x61, 0xD8, + 0x07, 0xCA, 0x0D, 0xBF, 0x50, 0x0D, 0x6A, 0x61, + 0x56, 0xA3, 0x8E, 0x08, 0x8A, 0x22, 0xB6, 0x5E, + 0x52, 0xBC, 0x51, 0x4D, 0x16, 0xCC, 0xF8, 0x06, + 0x81, 0x8C, 0xE9, 0x1A, 0xB7, 0x79, 0x37, 0x36, + 0x5A, 0xF9, 0x0B, 0xBF, 0x74, 0xA3, 0x5B, 0xE6, + 0xB4, 0x0B, 0x8E, 0xED, 0xF2, 0x78, 0x5E, 0x42, + 0x87, 0x4D + }; + + var pb64 = new byte[64]; + + using (var c = new ChaCha20Cipher(pbKey, pbIV)) + { + c.Encrypt(pb64, 0, pb64.Length); // Skip first block + c.Encrypt(pb, 0, pb.Length); + + Assert.True(MemUtil.ArraysEqual(pb, pbExpc)); + } + + // ====================================================== + // Test vector from RFC 7539, appendix A.2 #2 + + Array.Clear(pbKey, 0, pbKey.Length); + pbKey[31] = 1; + + Array.Clear(pbIV, 0, pbIV.Length); + pbIV[11] = 2; + + pb = StrUtil.Utf8.GetBytes("Any submission to the IETF inten" + + "ded by the Contributor for publication as all or" + + " part of an IETF Internet-Draft or RFC and any s" + + "tatement made within the context of an IETF acti" + + "vity is considered an \"IETF Contribution\". Such " + + "statements include oral statements in IETF sessi" + + "ons, as well as written and electronic communica" + + "tions made at any time or place, which are addressed to"); + + pbExpc = MemUtil.HexStringToByteArray( + "A3FBF07DF3FA2FDE4F376CA23E82737041605D9F4F4F57BD8CFF2C1D4B7955EC" + + "2A97948BD3722915C8F3D337F7D370050E9E96D647B7C39F56E031CA5EB6250D" + + "4042E02785ECECFA4B4BB5E8EAD0440E20B6E8DB09D881A7C6132F420E527950" + + "42BDFA7773D8A9051447B3291CE1411C680465552AA6C405B7764D5E87BEA85A" + + "D00F8449ED8F72D0D662AB052691CA66424BC86D2DF80EA41F43ABF937D3259D" + + "C4B2D0DFB48A6C9139DDD7F76966E928E635553BA76C5C879D7B35D49EB2E62B" + + "0871CDAC638939E25E8A1E0EF9D5280FA8CA328B351C3C765989CBCF3DAA8B6C" + + "CC3AAF9F3979C92B3720FC88DC95ED84A1BE059C6499B9FDA236E7E818B04B0B" + + "C39C1E876B193BFE5569753F88128CC08AAA9B63D1A16F80EF2554D7189C411F" + + "5869CA52C5B83FA36FF216B9C1D30062BEBCFD2DC5BCE0911934FDA79A86F6E6" + + "98CED759C3FF9B6477338F3DA4F9CD8514EA9982CCAFB341B2384DD902F3D1AB" + + "7AC61DD29C6F21BA5B862F3730E37CFDC4FD806C22F221"); + + using (var msEnc = new MemoryStream()) + { + using (var c = new ChaCha20Stream(msEnc, true, pbKey, pbIV)) + { + var r = CryptoRandom.NewWeakRandom(); + r.NextBytes(pb64); + c.Write(pb64, 0, pb64.Length); // Skip first block + + var p = 0; + while (p < pb.Length) + { + var cb = r.Next(1, pb.Length - p + 1); + c.Write(pb, p, cb); + p += cb; + } + Debug.Assert(p == pb.Length); + } + + var pbEnc0 = msEnc.ToArray(); + var pbEnc = MemUtil.Mid(pbEnc0, 64, pbEnc0.Length - 64); + Assert.True(MemUtil.ArraysEqual(pbEnc, pbExpc)); + + using (var msCT = new MemoryStream(pbEnc0, false)) + { + using (var cDec = new ChaCha20Stream(msCT, false, + pbKey, pbIV)) + { + var pbPT = MemUtil.Read(cDec, pbEnc0.Length); + + Assert.True(cDec.ReadByte() < 0); + Assert.True(MemUtil.ArraysEqual(MemUtil.Mid(pbPT, 0, 64), pb64)); + Assert.True(MemUtil.ArraysEqual(MemUtil.Mid(pbPT, 64, pbEnc.Length), pb)); + } + } + } + + // ====================================================== + // Test vector TC8 from RFC draft by J. Strombergson: + // https://tools.ietf.org/html/draft-strombergson-chacha-test-vectors-01 + + pbKey = new byte[32] { + 0xC4, 0x6E, 0xC1, 0xB1, 0x8C, 0xE8, 0xA8, 0x78, + 0x72, 0x5A, 0x37, 0xE7, 0x80, 0xDF, 0xB7, 0x35, + 0x1F, 0x68, 0xED, 0x2E, 0x19, 0x4C, 0x79, 0xFB, + 0xC6, 0xAE, 0xBE, 0xE1, 0xA6, 0x67, 0x97, 0x5D + }; + + // The first 4 bytes are set to zero and a large counter + // is used; this makes the RFC 7539 version of ChaCha20 + // compatible with the original specification by + // D. J. Bernstein. + pbIV = new byte[12] { 0x00, 0x00, 0x00, 0x00, + 0x1A, 0xDA, 0x31, 0xD5, 0xCF, 0x68, 0x82, 0x21 + }; + + pb = new byte[128]; + + pbExpc = new byte[128] { + 0xF6, 0x3A, 0x89, 0xB7, 0x5C, 0x22, 0x71, 0xF9, + 0x36, 0x88, 0x16, 0x54, 0x2B, 0xA5, 0x2F, 0x06, + 0xED, 0x49, 0x24, 0x17, 0x92, 0x30, 0x2B, 0x00, + 0xB5, 0xE8, 0xF8, 0x0A, 0xE9, 0xA4, 0x73, 0xAF, + 0xC2, 0x5B, 0x21, 0x8F, 0x51, 0x9A, 0xF0, 0xFD, + 0xD4, 0x06, 0x36, 0x2E, 0x8D, 0x69, 0xDE, 0x7F, + 0x54, 0xC6, 0x04, 0xA6, 0xE0, 0x0F, 0x35, 0x3F, + 0x11, 0x0F, 0x77, 0x1B, 0xDC, 0xA8, 0xAB, 0x92, + + 0xE5, 0xFB, 0xC3, 0x4E, 0x60, 0xA1, 0xD9, 0xA9, + 0xDB, 0x17, 0x34, 0x5B, 0x0A, 0x40, 0x27, 0x36, + 0x85, 0x3B, 0xF9, 0x10, 0xB0, 0x60, 0xBD, 0xF1, + 0xF8, 0x97, 0xB6, 0x29, 0x0F, 0x01, 0xD1, 0x38, + 0xAE, 0x2C, 0x4C, 0x90, 0x22, 0x5B, 0xA9, 0xEA, + 0x14, 0xD5, 0x18, 0xF5, 0x59, 0x29, 0xDE, 0xA0, + 0x98, 0xCA, 0x7A, 0x6C, 0xCF, 0xE6, 0x12, 0x27, + 0x05, 0x3C, 0x84, 0xE4, 0x9A, 0x4A, 0x33, 0x32 + }; + + using (var c = new ChaCha20Cipher(pbKey, pbIV, true)) + { + c.Decrypt(pb, 0, pb.Length); + + Assert.True(MemUtil.ArraysEqual(pb, pbExpc)); + } +#endif + } + } +} \ No newline at end of file diff --git a/ModernKeePassLib.Test/Cryptography/Cipher/Salsa20Tests.cs b/ModernKeePassLib.Test/Cryptography/Cipher/Salsa20Tests.cs new file mode 100644 index 0000000..e82f299 --- /dev/null +++ b/ModernKeePassLib.Test/Cryptography/Cipher/Salsa20Tests.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Cryptography.Cipher; +using ModernKeePassLib.Utility; +using Xunit; + +namespace ModernKeePassLib.Test.Cryptography.Cipher +{ + public class Salsa20Tests + { + [Fact] + public void TestSalsa20Cipher() + { + var r = CryptoRandom.NewWeakRandom(); + + // Test values from official set 6, vector 3 + var pbKey = new byte[] { + 0x0F, 0x62, 0xB5, 0x08, 0x5B, 0xAE, 0x01, 0x54, + 0xA7, 0xFA, 0x4D, 0xA0, 0xF3, 0x46, 0x99, 0xEC, + 0x3F, 0x92, 0xE5, 0x38, 0x8B, 0xDE, 0x31, 0x84, + 0xD7, 0x2A, 0x7D, 0xD0, 0x23, 0x76, 0xC9, 0x1C + }; + var pbIv = new byte[] { 0x28, 0x8F, 0xF6, 0x5D, + 0xC4, 0x2B, 0x92, 0xF9 }; + var pbExpected = new byte[] { + 0x5E, 0x5E, 0x71, 0xF9, 0x01, 0x99, 0x34, 0x03, + 0x04, 0xAB, 0xB2, 0x2A, 0x37, 0xB6, 0x62, 0x5B + }; + + var pb = new byte[16]; + var c = new Salsa20Cipher(pbKey, pbIv); + c.Encrypt(pb, 0, pb.Length); + Assert.True(MemUtil.ArraysEqual(pb, pbExpected)); + + // Extended test + var pbExpected2 = new byte[] { + 0xAB, 0xF3, 0x9A, 0x21, 0x0E, 0xEE, 0x89, 0x59, + 0x8B, 0x71, 0x33, 0x37, 0x70, 0x56, 0xC2, 0xFE + }; + var pbExpected3 = new byte[] { + 0x1B, 0xA8, 0x9D, 0xBD, 0x3F, 0x98, 0x83, 0x97, + 0x28, 0xF5, 0x67, 0x91, 0xD5, 0xB7, 0xCE, 0x23 + }; + + var nPos = Salsa20ToPos(c, r, pb.Length, 65536); + Array.Clear(pb, 0, pb.Length); + c.Encrypt(pb, 0, pb.Length); + Assert.True(MemUtil.ArraysEqual(pb, pbExpected2)); + + Salsa20ToPos(c, r, nPos + pb.Length, 131008); + Array.Clear(pb, 0, pb.Length); + c.Encrypt(pb, 0, pb.Length); + Assert.True(MemUtil.ArraysEqual(pb, pbExpected3)); + + var d = new Dictionary(); + const int nRounds = 100; + for (var i = 0; i < nRounds; ++i) + { + var z = new byte[32]; + c = new Salsa20Cipher(z, MemUtil.Int64ToBytes(i)); + c.Encrypt(z, 0, z.Length); + d[MemUtil.ByteArrayToHexString(z)] = true; + } + Assert.Equal(nRounds, d.Count); + } + + private static int Salsa20ToPos(Salsa20Cipher c, Random r, int nPos, + int nTargetPos) + { + var pb = new byte[512]; + + while (nPos < nTargetPos) + { + var x = r.Next(1, 513); + var nGen = Math.Min(nTargetPos - nPos, x); + c.Encrypt(pb, 0, nGen); + nPos += nGen; + } + + return nTargetPos; + } + } +} \ No newline at end of file diff --git a/ModernKeePassLib.Test/Cryptography/CryptoRandomStreamTests.cs b/ModernKeePassLib.Test/Cryptography/CryptoRandomStreamTests.cs new file mode 100644 index 0000000..288c496 --- /dev/null +++ b/ModernKeePassLib.Test/Cryptography/CryptoRandomStreamTests.cs @@ -0,0 +1,54 @@ +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Utility; +using Xunit; + +namespace ModernKeePassLib.Test.Cryptography +{ + public class CryptoRandomStreamTests + { + private void TestGetRandomBytes(CryptoRandomStream stream) + { + const uint length = 16; + var bytes1 = stream.GetRandomBytes(length); + Assert.Equal(bytes1.Length, (int)length); + var bytes2 = stream.GetRandomBytes(length); + Assert.False(MemUtil.ArraysEqual(bytes2, bytes1)); + } + + [Fact] + public void TestGetRandomBytesCrsAlgorithmSalsa20() + { + var stream = new CryptoRandomStream(CrsAlgorithm.Salsa20, new byte[16]); + TestGetRandomBytes(stream); + } + + [Fact] + public void TestGetRandomBytesCrsAlgorithmArcFourVariant() + { + var stream = new CryptoRandomStream(CrsAlgorithm.ArcFourVariant, new byte[16]); + TestGetRandomBytes(stream); + } + + private void TestGetRandomInt64(CryptoRandomStream stream) + { + var value1 = stream.GetRandomUInt64(); + var value2 = stream.GetRandomUInt64(); + Assert.NotEqual(value2, value1); + } + + [Fact] + public void TestGetRandomInt64AlgorithmSalsa20() + { + var stream = new CryptoRandomStream(CrsAlgorithm.Salsa20, new byte[16]); + TestGetRandomInt64(stream); + } + + [Fact] + public void TestGetRandomInt64AlgorithmArcFourVariant() + { + var stream = new CryptoRandomStream(CrsAlgorithm.ArcFourVariant, new byte[16]); + TestGetRandomInt64(stream); + } + } +} + diff --git a/ModernKeePassLib.Test/Cryptography/CryptoRandomTests.cs b/ModernKeePassLib.Test/Cryptography/CryptoRandomTests.cs new file mode 100644 index 0000000..de087e0 --- /dev/null +++ b/ModernKeePassLib.Test/Cryptography/CryptoRandomTests.cs @@ -0,0 +1,37 @@ +using ModernKeePassLib.Cryptography; +using Xunit; + +namespace ModernKeePassLib.Test.Cryptography +{ + public class CryptoRandomTests + { + [Fact] + public void TestAddEntropy() + { + // just making sure it does not throw an exception + CryptoRandom.Instance.AddEntropy(new byte[1]); + } + + [Fact] + public void TestGetRandomBytes() + { + const int length = 32; + var bytes1 = CryptoRandom.Instance.GetRandomBytes(length); + Assert.Equal(bytes1.Length, length); + var bytes2 = CryptoRandom.Instance.GetRandomBytes(length); + Assert.NotEqual(bytes2, bytes1); + } + + [Fact] + public void TestGeneratedBytesCount() + { + const int length = 1; + CryptoRandom.Instance.GetRandomBytes(length); + var count1 = CryptoRandom.Instance.GeneratedBytesCount; + CryptoRandom.Instance.GetRandomBytes(length); + var count2 = CryptoRandom.Instance.GeneratedBytesCount; + Assert.True(count2 > count1); + } + } +} + diff --git a/ModernKeePassLib.Test/Cryptography/Hash/Blake2bTests.cs b/ModernKeePassLib.Test/Cryptography/Hash/Blake2bTests.cs new file mode 100644 index 0000000..c6b6fca --- /dev/null +++ b/ModernKeePassLib.Test/Cryptography/Hash/Blake2bTests.cs @@ -0,0 +1,100 @@ +using System; +using System.Text; +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Cryptography.Hash; +using ModernKeePassLib.Utility; +using Xunit; + +namespace ModernKeePassLib.Test.Cryptography.Hash +{ + public class Blake2bTests + { + [Fact] + public void TestBlake2bUtf8() + { + Blake2b h = new Blake2b(); + + // ====================================================== + // From https://tools.ietf.org/html/rfc7693 + + byte[] pbData = StrUtil.Utf8.GetBytes("abc"); + byte[] pbExpc = new byte[64] + { + 0xBA, 0x80, 0xA5, 0x3F, 0x98, 0x1C, 0x4D, 0x0D, + 0x6A, 0x27, 0x97, 0xB6, 0x9F, 0x12, 0xF6, 0xE9, + 0x4C, 0x21, 0x2F, 0x14, 0x68, 0x5A, 0xC4, 0xB7, + 0x4B, 0x12, 0xBB, 0x6F, 0xDB, 0xFF, 0xA2, 0xD1, + 0x7D, 0x87, 0xC5, 0x39, 0x2A, 0xAB, 0x79, 0x2D, + 0xC2, 0x52, 0xD5, 0xDE, 0x45, 0x33, 0xCC, 0x95, + 0x18, 0xD3, 0x8A, 0xA8, 0xDB, 0xF1, 0x92, 0x5A, + 0xB9, 0x23, 0x86, 0xED, 0xD4, 0x00, 0x99, 0x23 + }; + + byte[] pbC = h.ComputeHash(pbData); + Assert.True(MemUtil.ArraysEqual(pbC, pbExpc)); + } + + [Fact] + public void TestBlake2bEmpty() + { + // ====================================================== + // Computed using the official b2sum tool + Blake2b h = new Blake2b(); + + var pbExpc = new byte[64] + { + 0x78, 0x6A, 0x02, 0xF7, 0x42, 0x01, 0x59, 0x03, + 0xC6, 0xC6, 0xFD, 0x85, 0x25, 0x52, 0xD2, 0x72, + 0x91, 0x2F, 0x47, 0x40, 0xE1, 0x58, 0x47, 0x61, + 0x8A, 0x86, 0xE2, 0x17, 0xF7, 0x1F, 0x54, 0x19, + 0xD2, 0x5E, 0x10, 0x31, 0xAF, 0xEE, 0x58, 0x53, + 0x13, 0x89, 0x64, 0x44, 0x93, 0x4E, 0xB0, 0x4B, + 0x90, 0x3A, 0x68, 0x5B, 0x14, 0x48, 0xB7, 0x55, + 0xD5, 0x6F, 0x70, 0x1A, 0xFE, 0x9B, 0xE2, 0xCE + }; + + var pbC = h.ComputeHash(new byte[0]); + Assert.True(MemUtil.ArraysEqual(pbC, pbExpc)); + } + + [Fact] + public void TestBlake2bString() + { + // ====================================================== + // Computed using the official b2sum tool + Blake2b h = new Blake2b(); + + string strS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.:,;_-\r\n"; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 1000; ++i) sb.Append(strS); + var pbData = StrUtil.Utf8.GetBytes(sb.ToString()); + + var pbExpc = new byte[64] { + 0x59, 0x69, 0x8D, 0x3B, 0x83, 0xF4, 0x02, 0x4E, + 0xD8, 0x99, 0x26, 0x0E, 0xF4, 0xE5, 0x9F, 0x20, + 0xDC, 0x31, 0xEE, 0x5B, 0x45, 0xEA, 0xBB, 0xFC, + 0x1C, 0x0A, 0x8E, 0xED, 0xAA, 0x7A, 0xFF, 0x50, + 0x82, 0xA5, 0x8F, 0xBC, 0x4A, 0x46, 0xFC, 0xC5, + 0xEF, 0x44, 0x4E, 0x89, 0x80, 0x7D, 0x3F, 0x1C, + 0xC1, 0x94, 0x45, 0xBB, 0xC0, 0x2C, 0x95, 0xAA, + 0x3F, 0x08, 0x8A, 0x93, 0xF8, 0x75, 0x91, 0xB0 + }; + + Random r = CryptoRandom.NewWeakRandom(); + int p = 0; + while (p < pbData.Length) + { + int cb = r.Next(1, pbData.Length - p + 1); + h.TransformBlock(pbData, p, cb, pbData, p); + p += cb; + } + Assert.Equal(p, pbData.Length); + + h.TransformFinalBlock(new byte[0], 0, 0); + + Assert.True(MemUtil.ArraysEqual(h.Hash, pbExpc)); + + h.Clear(); + } + } +} \ No newline at end of file diff --git a/ModernKeePassLib.Test/Cryptography/Hash/HmacTests.cs b/ModernKeePassLib.Test/Cryptography/Hash/HmacTests.cs new file mode 100644 index 0000000..9039267 --- /dev/null +++ b/ModernKeePassLib.Test/Cryptography/Hash/HmacTests.cs @@ -0,0 +1,116 @@ +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Utility; +using System.Security.Cryptography; +using Xunit; + +namespace ModernKeePassLib.Test.Cryptography.Hash +{ + public class HmacTests + { + [Fact] + public void TestHmac1() + { + // Test vectors from RFC 4231 + + var pbKey = new byte[20]; + for (var i = 0; i < pbKey.Length; ++i) pbKey[i] = 0x0B; + var pbMsg = StrUtil.Utf8.GetBytes("Hi There"); + var pbExpc = new byte[] + { + 0xB0, 0x34, 0x4C, 0x61, 0xD8, 0xDB, 0x38, 0x53, + 0x5C, 0xA8, 0xAF, 0xCE, 0xAF, 0x0B, 0xF1, 0x2B, + 0x88, 0x1D, 0xC2, 0x00, 0xC9, 0x83, 0x3D, 0xA7, + 0x26, 0xE9, 0x37, 0x6C, 0x2E, 0x32, 0xCF, 0xF7 + }; + HmacEval(pbKey, pbMsg, pbExpc); + } + + [Fact] + public void TestHmac2() + { + var pbKey = new byte[131]; + for (var i = 0; i < pbKey.Length; ++i) pbKey[i] = 0xAA; + var pbMsg = StrUtil.Utf8.GetBytes( + "This is a test using a larger than block-size key and " + + "a larger than block-size data. The key needs to be " + + "hashed before being used by the HMAC algorithm."); + var pbExpc = new byte[] { + 0x9B, 0x09, 0xFF, 0xA7, 0x1B, 0x94, 0x2F, 0xCB, + 0x27, 0x63, 0x5F, 0xBC, 0xD5, 0xB0, 0xE9, 0x44, + 0xBF, 0xDC, 0x63, 0x64, 0x4F, 0x07, 0x13, 0x93, + 0x8A, 0x7F, 0x51, 0x53, 0x5C, 0x3A, 0x35, 0xE2 + }; + HmacEval(pbKey, pbMsg, pbExpc); + } + + [Fact] + public void TestHmacSha1ComputeHash() + { + var expectedHash = "AC2C2E614882CE7158F69B7E3B12114465945D01"; + var message = StrUtil.Utf8.GetBytes("testing123"); + var key = StrUtil.Utf8.GetBytes("hello"); + using (var result = new HMACSHA1(key)) + { + Assert.Equal(ByteToString(result.ComputeHash(message)), expectedHash); + } + } + + [Fact] + public void TestHmacSha256ComputeHash() + { + var expectedHash = "09C1BD2DE4E5659C0EFAF9E6AE4723E9CF96B69609B4E562F6AFF1745D7BF4E0"; + var message = StrUtil.Utf8.GetBytes("testing123"); + var key = StrUtil.Utf8.GetBytes("hello"); + using (var result = new HMACSHA256(key)) + { + Assert.Equal(ByteToString(result.ComputeHash(message)), expectedHash); + } + } + + private static string ByteToString(byte[] buff) + { + string sbinary = ""; + + for (int i = 0; i < buff.Length; i++) + { + sbinary += buff[i].ToString("X2"); // hex format + } + return (sbinary); + } + + [Fact] + public void TestHmacOtp() + { + var pbSecret = StrUtil.Utf8.GetBytes("12345678901234567890"); + var vExp = new []{ "755224", "287082", "359152", + "969429", "338314", "254676", "287922", "162583", "399871", + "520489" }; + + for (var i = 0; i < vExp.Length; ++i) + { + Assert.Equal(HmacOtp.Generate(pbSecret, (ulong)i, 6, false, -1), vExp[i]); + } + } + + private static void HmacEval(byte[] pbKey, byte[] pbMsg, + byte[] pbExpc) + { + using (var h = new HMACSHA256(pbKey)) + { + h.TransformBlock(pbMsg, 0, pbMsg.Length, pbMsg, 0); + h.TransformFinalBlock(new byte[0], 0, 0); + + byte[] pbHash = h.Hash; + Assert.True(MemUtil.ArraysEqual(pbHash, pbExpc)); + + // Reuse the object + h.Initialize(); + h.TransformBlock(pbMsg, 0, pbMsg.Length, pbMsg, 0); + h.TransformFinalBlock(new byte[0], 0, 0); + + pbHash = h.Hash; + Assert.True(MemUtil.ArraysEqual(pbHash, pbExpc)); + } + } + } +} \ No newline at end of file diff --git a/ModernKeePassLib.Test/Cryptography/Hash/SHAManagedTests.cs b/ModernKeePassLib.Test/Cryptography/Hash/SHAManagedTests.cs new file mode 100644 index 0000000..894c358 --- /dev/null +++ b/ModernKeePassLib.Test/Cryptography/Hash/SHAManagedTests.cs @@ -0,0 +1,73 @@ +using ModernKeePassLib.Utility; +using System.Security.Cryptography; +using ModernKeePassLib.Cryptography; +using Xunit; + +namespace ModernKeePassLib.Test.Cryptography.Hash +{ + public class ShaManagedTests + { + [Fact] + public void TestSha256() + { + var r = CryptoRandom.NewWeakRandom(); + var pbData = new byte[517]; + r.NextBytes(pbData); + + byte[] pbH1; + using (var h1 = new SHA256Managed()) + { + var i = 0; + while (i != pbData.Length) + { + var cb = r.Next(pbData.Length - i) + 1; + h1.TransformBlock(pbData, i, cb, pbData, i); + i += cb; + } + h1.TransformFinalBlock(MemUtil.EmptyByteArray, 0, 0); + pbH1 = h1.Hash; + } + + byte[] pbH2; + using (var h2 = new SHA256Managed()) + { + pbH2 = h2.ComputeHash(pbData); + } + + Assert.True(MemUtil.ArraysEqual(pbH1, pbH2)); + } + + [Fact] + public void TestSha256ComputeHash() + { + var expectedHash = "B822F1CD2DCFC685B47E83E3980289FD5D8E3FF3A82DEF24D7D1D68BB272EB32"; + var message = StrUtil.Utf8.GetBytes("testing123"); + using (var result = new SHA256Managed()) + { + Assert.Equal(ByteToString(result.ComputeHash(message)), expectedHash); + } + } + + [Fact] + public void TestSha512ComputeHash() + { + var expectedHash = "4120117B3190BA5E24044732B0B09AA9ED50EB1567705ABCBFA78431A4E0A96B1152ED7F4925966B1C82325E186A8100E692E6D2FCB6702572765820D25C7E9E"; + var message = StrUtil.Utf8.GetBytes("testing123"); + using (var result = new SHA512Managed()) + { + Assert.Equal(ByteToString(result.ComputeHash(message)), expectedHash); + } + } + + private static string ByteToString(byte[] buff) + { + var sbinary = ""; + + for (var i = 0; i < buff.Length; i++) + { + sbinary += buff[i].ToString("X2"); // hex format + } + return (sbinary); + } + } +} \ No newline at end of file diff --git a/ModernKeePassLib.Test/Cryptography/HashingStreamExTests.cs b/ModernKeePassLib.Test/Cryptography/HashingStreamExTests.cs new file mode 100644 index 0000000..3ceeee9 --- /dev/null +++ b/ModernKeePassLib.Test/Cryptography/HashingStreamExTests.cs @@ -0,0 +1,81 @@ +using System.IO; +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Utility; +using Xunit; + +namespace ModernKeePassLib.Test.Cryptography +{ + public class HashingStreamExTests + { + const string data = "test"; + + // The expected hash includes the \n added by WriteLine + static readonly byte[] sha256HashOfData = + { + 0xf2, 0xca, 0x1b, 0xb6, 0xc7, 0xe9, 0x07, 0xd0, + 0x6d, 0xaf, 0xe4, 0x68, 0x7e, 0x57, 0x9f, 0xce, + 0x76, 0xb3, 0x7e, 0x4e, 0x93, 0xb7, 0x60, 0x50, + 0x22, 0xda, 0x52, 0xe6, 0xcc, 0xc2, 0x6f, 0xd2 + }; + + [Fact] + public void TestRead() + { + // if we use larger size, StreamReader will read past newline and cause bad hash + var bytes = new byte[data.Length + 1]; + using (var ms = new MemoryStream(bytes)) + { + using (var sw = new StreamWriter(ms)) + { + // set NewLine to ensure we don't run into cross-platform issues on Windows + sw.NewLine = "\n"; + sw.WriteLine(data); + } + } + using (var ms = new MemoryStream(bytes)) + { + using (var hs = new HashingStreamEx(ms, false, null)) + { + using (var sr = new StreamReader(hs)) + { + var read = sr.ReadLine(); + Assert.Equal(read, data); + } + // When the StreamReader is disposed, it calls Dispose on the + //HasingStreamEx, which computes the hash. + Assert.True(MemUtil.ArraysEqual(hs.Hash, sha256HashOfData)); + } + } + } + + [Fact] + public void TestWrite() + { + var bytes = new byte[16]; + using (var ms = new MemoryStream(bytes)) + { + using (var hs = new HashingStreamEx(ms, true, null)) + { + using (var sw = new StreamWriter(hs)) + { + // set NewLine to ensure we don't run into cross-platform issues on Windows + sw.NewLine = "\n"; + sw.WriteLine(data); + } + // When the StreamWriter is disposed, it calls Dispose on the + //HasingStreamEx, which computes the hash. + Assert.True(MemUtil.ArraysEqual(hs.Hash, sha256HashOfData)); + } + } + using (var ms = new MemoryStream(bytes)) + { + using (var sr = new StreamReader(ms)) + { + var read = sr.ReadLine(); + Assert.Equal(read, data); + } + } + } + } +} + diff --git a/ModernKeePassLib.Test/Cryptography/HmacOtpTests.cs b/ModernKeePassLib.Test/Cryptography/HmacOtpTests.cs new file mode 100644 index 0000000..31a10fe --- /dev/null +++ b/ModernKeePassLib.Test/Cryptography/HmacOtpTests.cs @@ -0,0 +1,31 @@ +using System.Text; +using ModernKeePassLib.Cryptography; +using Xunit; + +namespace ModernKeePassLib.Test.Cryptography +{ + public class HmacOtpTests + { + // Using the test case from Appendix D of RFC 4226 + + const string secret = "12345678901234567890"; + + static readonly string[] expectedHOTP = new string[] + { + "755224", "287082", "359152", "969429", "338314", + "254676", "287922", "162583", "399871", "520489" + }; + + [Fact] + public void TestGenerate() + { + var secretBytes = Encoding.UTF8.GetBytes(secret); + + for (ulong i = 0; i < 10; i++) + { + var hotp = HmacOtp.Generate(secretBytes, i, 6, false, -1); + Assert.Equal(hotp, expectedHOTP[i]); + } + } + } +} diff --git a/ModernKeePassLib.Test/Cryptography/KeyDerivation/AesKdfTests.cs b/ModernKeePassLib.Test/Cryptography/KeyDerivation/AesKdfTests.cs new file mode 100644 index 0000000..cea74c5 --- /dev/null +++ b/ModernKeePassLib.Test/Cryptography/KeyDerivation/AesKdfTests.cs @@ -0,0 +1,41 @@ +using System; +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Cryptography.KeyDerivation; +using ModernKeePassLib.Utility; +using Xunit; + +namespace ModernKeePassLib.Test.Cryptography.KeyDerivation +{ + public class AesKdfTests + { + [Fact] + public void TestAesKdf() + { + // Up to KeePass 2.34, the OtpKeyProv plugin used the public + // CompositeKey.TransformKeyManaged method (and a finalizing + // SHA-256 computation), which became an internal method of + // the AesKdf class in KeePass 2.35, thus OtpKeyProv now + // uses the AesKdf class; here we ensure that the results + // are the same + var r = CryptoRandom.NewWeakRandom(); + var pbKey = new byte[32]; + r.NextBytes(pbKey); + var pbSeed = new byte[32]; + r.NextBytes(pbSeed); + var uRounds = (ulong)r.Next(1, 0x7FFF); + + var pbMan = new byte[pbKey.Length]; + Array.Copy(pbKey, pbMan, pbKey.Length); + Assert.True(AesKdf.TransformKeyManaged(pbMan, pbSeed, uRounds)); + pbMan = CryptoUtil.HashSha256(pbMan); + + var kdf = new AesKdf(); + var p = kdf.GetDefaultParameters(); + p.SetUInt64(AesKdf.ParamRounds, uRounds); + p.SetByteArray(AesKdf.ParamSeed, pbSeed); + var pbKdf = kdf.Transform(pbKey, p); + + Assert.True(MemUtil.ArraysEqual(pbMan, pbKdf)); + } + } +} \ No newline at end of file diff --git a/ModernKeePassLib.Test/Cryptography/KeyDerivation/Argon2Tests.cs b/ModernKeePassLib.Test/Cryptography/KeyDerivation/Argon2Tests.cs new file mode 100644 index 0000000..cefca0e --- /dev/null +++ b/ModernKeePassLib.Test/Cryptography/KeyDerivation/Argon2Tests.cs @@ -0,0 +1,145 @@ +using ModernKeePassLib.Cryptography.KeyDerivation; +using ModernKeePassLib.Utility; +using Xunit; + +namespace ModernKeePassLib.Test.Cryptography.KeyDerivation +{ + public class Argon2Tests + { + [Fact] + public void TestArgon2() + { + Argon2Kdf kdf = new Argon2Kdf(); + + // ====================================================== + // From the official Argon2 1.3 reference code package + // (test vector for Argon2d 1.3); also on + // https://tools.ietf.org/html/draft-irtf-cfrg-argon2-00 + + var p = kdf.GetDefaultParameters(); + kdf.Randomize(p); + + Assert.Equal(0x13U, p.GetUInt32(Argon2Kdf.ParamVersion, 0)); + + byte[] pbMsg = new byte[32]; + for (int i = 0; i < pbMsg.Length; ++i) pbMsg[i] = 1; + + p.SetUInt64(Argon2Kdf.ParamMemory, 32 * 1024); + p.SetUInt64(Argon2Kdf.ParamIterations, 3); + p.SetUInt32(Argon2Kdf.ParamParallelism, 4); + + byte[] pbSalt = new byte[16]; + for (int i = 0; i < pbSalt.Length; ++i) pbSalt[i] = 2; + p.SetByteArray(Argon2Kdf.ParamSalt, pbSalt); + + byte[] pbKey = new byte[8]; + for (int i = 0; i < pbKey.Length; ++i) pbKey[i] = 3; + p.SetByteArray(Argon2Kdf.ParamSecretKey, pbKey); + + byte[] pbAssoc = new byte[12]; + for (int i = 0; i < pbAssoc.Length; ++i) pbAssoc[i] = 4; + p.SetByteArray(Argon2Kdf.ParamAssocData, pbAssoc); + + byte[] pbExpc = new byte[32] { + 0x51, 0x2B, 0x39, 0x1B, 0x6F, 0x11, 0x62, 0x97, + 0x53, 0x71, 0xD3, 0x09, 0x19, 0x73, 0x42, 0x94, + 0xF8, 0x68, 0xE3, 0xBE, 0x39, 0x84, 0xF3, 0xC1, + 0xA1, 0x3A, 0x4D, 0xB9, 0xFA, 0xBE, 0x4A, 0xCB + }; + + byte[] pb = kdf.Transform(pbMsg, p); + + Assert.True(MemUtil.ArraysEqual(pb, pbExpc)); + + // ====================================================== + // From the official Argon2 1.3 reference code package + // (test vector for Argon2d 1.0) + + p.SetUInt32(Argon2Kdf.ParamVersion, 0x10); + + pbExpc = new byte[32] { + 0x96, 0xA9, 0xD4, 0xE5, 0xA1, 0x73, 0x40, 0x92, + 0xC8, 0x5E, 0x29, 0xF4, 0x10, 0xA4, 0x59, 0x14, + 0xA5, 0xDD, 0x1F, 0x5C, 0xBF, 0x08, 0xB2, 0x67, + 0x0D, 0xA6, 0x8A, 0x02, 0x85, 0xAB, 0xF3, 0x2B + }; + + pb = kdf.Transform(pbMsg, p); + + Assert.True(MemUtil.ArraysEqual(pb, pbExpc)); + + // ====================================================== + // From the official 'phc-winner-argon2-20151206.zip' + // (test vector for Argon2d 1.0) + + p.SetUInt64(Argon2Kdf.ParamMemory, 16 * 1024); + + pbExpc = new byte[32] { + 0x57, 0xB0, 0x61, 0x3B, 0xFD, 0xD4, 0x13, 0x1A, + 0x0C, 0x34, 0x88, 0x34, 0xC6, 0x72, 0x9C, 0x2C, + 0x72, 0x29, 0x92, 0x1E, 0x6B, 0xBA, 0x37, 0x66, + 0x5D, 0x97, 0x8C, 0x4F, 0xE7, 0x17, 0x5E, 0xD2 + }; + + pb = kdf.Transform(pbMsg, p); + + Assert.True(MemUtil.ArraysEqual(pb, pbExpc)); + + // ====================================================== + // Computed using the official 'argon2' application + // (test vectors for Argon2d 1.3) + + p = kdf.GetDefaultParameters(); + + pbMsg = StrUtil.Utf8.GetBytes("ABC1234"); + + p.SetUInt64(Argon2Kdf.ParamMemory, (1 << 11) * 1024); // 2 MB + p.SetUInt64(Argon2Kdf.ParamIterations, 2); + p.SetUInt32(Argon2Kdf.ParamParallelism, 2); + + pbSalt = StrUtil.Utf8.GetBytes("somesalt"); + p.SetByteArray(Argon2Kdf.ParamSalt, pbSalt); + + pbExpc = new byte[32] { + 0x29, 0xCB, 0xD3, 0xA1, 0x93, 0x76, 0xF7, 0xA2, + 0xFC, 0xDF, 0xB0, 0x68, 0xAC, 0x0B, 0x99, 0xBA, + 0x40, 0xAC, 0x09, 0x01, 0x73, 0x42, 0xCE, 0xF1, + 0x29, 0xCC, 0xA1, 0x4F, 0xE1, 0xC1, 0xB7, 0xA3 + }; + + pb = kdf.Transform(pbMsg, p); + + Assert.True(MemUtil.ArraysEqual(pb, pbExpc)); + + p.SetUInt64(Argon2Kdf.ParamMemory, (1 << 10) * 1024); // 1 MB + p.SetUInt64(Argon2Kdf.ParamIterations, 3); + + pbExpc = new byte[32] { + 0x7A, 0xBE, 0x1C, 0x1C, 0x8D, 0x7F, 0xD6, 0xDC, + 0x7C, 0x94, 0x06, 0x3E, 0xD8, 0xBC, 0xD8, 0x1C, + 0x2F, 0x87, 0x84, 0x99, 0x12, 0x83, 0xFE, 0x76, + 0x00, 0x64, 0xC4, 0x58, 0xA4, 0xDA, 0x35, 0x70 + }; + + pb = kdf.Transform(pbMsg, p); + + Assert.True(MemUtil.ArraysEqual(pb, pbExpc)); + + // TODO: Out of memory exception + /*p.SetUInt64(Argon2Kdf.ParamMemory, (1 << 20) * 1024); // 1 GB + p.SetUInt64(Argon2Kdf.ParamIterations, 2); + p.SetUInt32(Argon2Kdf.ParamParallelism, 3); + + pbExpc = new byte[32] { + 0xE6, 0xE7, 0xCB, 0xF5, 0x5A, 0x06, 0x93, 0x05, + 0x32, 0xBA, 0x86, 0xC6, 0x1F, 0x45, 0x17, 0x99, + 0x65, 0x41, 0x77, 0xF9, 0x30, 0x55, 0x9A, 0xE8, + 0x3D, 0x21, 0x48, 0xC6, 0x2D, 0x0C, 0x49, 0x11 + }; + + pb = kdf.Transform(pbMsg, p); + + Assert.IsTrue(MemUtil.ArraysEqual(pb, pbExpc));*/ + } + } +} \ No newline at end of file diff --git a/ModernKeePassLib.Test/Keys/CompositeKeyTests.cs b/ModernKeePassLib.Test/Keys/CompositeKeyTests.cs new file mode 100644 index 0000000..9723314 --- /dev/null +++ b/ModernKeePassLib.Test/Keys/CompositeKeyTests.cs @@ -0,0 +1,34 @@ +using ModernKeePassLib.Cryptography.KeyDerivation; +using ModernKeePassLib.Keys; +using ModernKeePassLib.Utility; +using Xunit; + +namespace ModernKeePassLib.Test.Keys +{ + public class CompositeKeyTests + { + [Fact] + public void TestGenerateKey32() + { + var originalKey = new byte[32]; + var expectedKey = new byte[] + { + 0xF0, 0xED, 0x57, 0xD5, 0xF0, 0xDA, 0xF3, 0x47, + 0x90, 0xD0, 0xDB, 0x43, 0x25, 0xC6, 0x81, 0x2C, + 0x81, 0x6A, 0x0D, 0x94, 0x96, 0xA9, 0x03, 0xE1, + 0x20, 0xD4, 0x3A, 0x3E, 0x45, 0xAD, 0x02, 0x65 + }; + const ulong rounds = 1; + + var composite = new CompositeKey(); + AesKdf kdf = new AesKdf(); + KdfParameters p = kdf.GetDefaultParameters(); + p.SetUInt64(AesKdf.ParamRounds, rounds); + p.SetByteArray(AesKdf.ParamSeed, originalKey); + var key = composite.GenerateKey32(p); + Assert.NotNull(key); + var keyData = key.ReadData(); + Assert.True(MemUtil.ArraysEqual(keyData, expectedKey)); + } + } +} diff --git a/ModernKeePassLib.Test/Keys/KcpCustomKeyTests.cs b/ModernKeePassLib.Test/Keys/KcpCustomKeyTests.cs new file mode 100644 index 0000000..6ed31b9 --- /dev/null +++ b/ModernKeePassLib.Test/Keys/KcpCustomKeyTests.cs @@ -0,0 +1,34 @@ +using ModernKeePassLib.Keys; +using ModernKeePassLib.Utility; +using Xunit; + +namespace ModernKeePassLib.Test.Keys +{ + public class KcpCustomKeyTests + { + static readonly byte[] testData = + { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + [Fact] + public void TestConstruct() + { + var expectedHash = new byte[32] + { + 0xAF, 0x55, 0x70, 0xF5, 0xA1, 0x81, 0x0B, 0x7A, + 0xF7, 0x8C, 0xAF, 0x4B, 0xC7, 0x0A, 0x66, 0x0F, + 0x0D, 0xF5, 0x1E, 0x42, 0xBA, 0xF9, 0x1D, 0x4D, + 0xE5, 0xB2, 0x32, 0x8D, 0xE0, 0xE8, 0x3D, 0xFC + }; + + var key = new KcpCustomKey("test1", testData, false); + var keyData = key.KeyData.ReadData(); + Assert.True(MemUtil.ArraysEqual(keyData, testData)); + + key = new KcpCustomKey("test2", testData, true); + keyData = key.KeyData.ReadData(); + Assert.True(MemUtil.ArraysEqual(keyData, expectedHash)); + } + } +} diff --git a/ModernKeePassLib.Test/Keys/KcpKeyFileTests.cs b/ModernKeePassLib.Test/Keys/KcpKeyFileTests.cs new file mode 100644 index 0000000..e51fad2 --- /dev/null +++ b/ModernKeePassLib.Test/Keys/KcpKeyFileTests.cs @@ -0,0 +1,82 @@ +using System; +using System.IO; +using ModernKeePassLib.Keys; +using ModernKeePassLib.Utility; +using Windows.Storage; +using Xunit; + +namespace ModernKeePassLib.Test.Keys +{ + public class KcpKeyFileTests + { + private const string TestCreateFile = "TestCreate.xml"; + private const string TestKey = "0123456789"; + + private const string ExpectedFileStart = + "\r\n" + + "\r\n" + + "\t\r\n" + + "\t\t1.00\r\n" + + "\t\r\n" + + "\t\r\n" + + "\t\t"; + + private const string ExpectedFileEnd = "\r\n\t\r\n"; + + [Fact] + public void TestConstruct() + { + var expectedKeyData = new byte[] + { + 0x95, 0x94, 0xdc, 0xb9, 0x91, 0xc6, 0x65, 0xa0, + 0x81, 0xf6, 0x6f, 0xca, 0x07, 0x1a, 0x30, 0xd1, + 0x1d, 0x65, 0xcf, 0x8d, 0x9c, 0x60, 0xfb, 0xe6, + 0x45, 0xfc, 0xc8, 0x92, 0xbd, 0xeb, 0xaf, 0xc3 + }; + + var folder = StorageFolder.GetFolderFromPathAsync(Path.GetTempPath()).GetAwaiter().GetResult(); + var file = folder.CreateFileAsync(TestCreateFile, CreationCollisionOption.ReplaceExisting).GetAwaiter().GetResult(); + using (var fs = file.OpenStreamForWriteAsync().GetAwaiter().GetResult()) + { + using (var sw = new StreamWriter(fs)) + { + sw.Write(ExpectedFileStart); + sw.Write(TestKey); + sw.Write(ExpectedFileEnd); + } + } + + try + { + var keyFile = new KcpKeyFile(file); + var keyData = keyFile.KeyData.ReadData(); + Assert.True(MemUtil.ArraysEqual(keyData, expectedKeyData)); + } + finally + { + file.DeleteAsync().GetAwaiter().GetResult(); + } + } + + [Fact] + public void TestCreate() + { + var folder = StorageFolder.GetFolderFromPathAsync(Path.GetTempPath()).GetAwaiter().GetResult(); + var file = folder.CreateFileAsync(TestCreateFile, CreationCollisionOption.ReplaceExisting).GetAwaiter().GetResult(); + KcpKeyFile.Create(file, null); + try + { + var fileContents = FileIO.ReadTextAsync(file).GetAwaiter().GetResult(); + + Assert.Equal(185, fileContents.Length); + Assert.StartsWith(ExpectedFileStart, fileContents); + Assert.EndsWith(ExpectedFileEnd, fileContents); + } + finally + { + file.DeleteAsync().GetAwaiter().GetResult(); + } + } + } +} + diff --git a/ModernKeePassLib.Test/Keys/KcpPasswordTests.cs b/ModernKeePassLib.Test/Keys/KcpPasswordTests.cs new file mode 100644 index 0000000..1a8e755 --- /dev/null +++ b/ModernKeePassLib.Test/Keys/KcpPasswordTests.cs @@ -0,0 +1,28 @@ +using ModernKeePassLib.Keys; +using ModernKeePassLib.Utility; +using Xunit; + +namespace ModernKeePassLib.Test.Keys +{ + public class KcpPasswordTests + { + const string testPassword = "password"; + + [Fact] + public void TestConstruct() + { + var expectedHash = new byte[32] + { + 0x5E, 0x88, 0x48, 0x98, 0xDA, 0x28, 0x04, 0x71, + 0x51, 0xD0, 0xE5, 0x6F, 0x8D, 0xC6, 0x29, 0x27, + 0x73, 0x60, 0x3D, 0x0D, 0x6A, 0xAB, 0xBD, 0xD6, + 0x2A, 0x11, 0xEF, 0x72, 0x1D, 0x15, 0x42, 0xD8 + }; + + var key = new KcpPassword(testPassword); + var keyData = key.KeyData.ReadData(); + Assert.True(MemUtil.ArraysEqual(keyData, expectedHash)); + } + } +} + diff --git a/ModernKeePassLib.Test/ModernKeePassLib.Test.csproj b/ModernKeePassLib.Test/ModernKeePassLib.Test.csproj new file mode 100644 index 0000000..e7e8aea --- /dev/null +++ b/ModernKeePassLib.Test/ModernKeePassLib.Test.csproj @@ -0,0 +1,31 @@ + + + + netcoreapp3.0 + + false + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + ..\ModernKeePassLib\Libs\Windows.winmd + true + + + + diff --git a/ModernKeePassLib.Test/Security/ProtectedObjectsTests.cs b/ModernKeePassLib.Test/Security/ProtectedObjectsTests.cs new file mode 100644 index 0000000..d6e2242 --- /dev/null +++ b/ModernKeePassLib.Test/Security/ProtectedObjectsTests.cs @@ -0,0 +1,110 @@ +using System.Text; +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Security; +using ModernKeePassLib.Utility; +using Xunit; + +namespace ModernKeePassLib.Test.Security +{ + public class ProtectedObjectsTests + { + private readonly Encoding _enc = StrUtil.Utf8; + + [Fact] + public void TestAreBinaryObjectsProtected() + { + var pbData = _enc.GetBytes("Test Test Test Test"); + var pb = new ProtectedBinary(true, pbData); + Assert.True(pb.IsProtected); + + var pbDec = pb.ReadData(); + Assert.True(MemUtil.ArraysEqual(pbData, pbDec)); + Assert.True(pb.IsProtected); + + var pbData2 = _enc.GetBytes("Test Test Test Test"); + var pbData3 = _enc.GetBytes("Test Test Test Test Test"); + var pb2 = new ProtectedBinary(true, pbData2); + var pb3 = new ProtectedBinary(true, pbData3); + Assert.True(pb.Equals(pb2)); + Assert.False(pb.Equals(pb3)); + Assert.False(pb2.Equals(pb3)); + + Assert.Equal(pb.GetHashCode(), pb2.GetHashCode()); + Assert.True(pb.Equals((object) pb2)); + Assert.False(pb.Equals((object) pb3)); + Assert.False(pb2.Equals((object) pb3)); + } + + [Fact] + public void TestIsEmptyProtectedStringEmpty() + { + var ps = new ProtectedString(); + Assert.Equal(0, ps.Length); + Assert.True(ps.IsEmpty); + Assert.Equal(0, ps.ReadString().Length); + } + + [Fact] + public void TestAreEqualStringsProtected() + { + var ps = new ProtectedString(true, "Test"); + var ps2 = new ProtectedString(true, _enc.GetBytes("Test")); + Assert.False(ps.IsEmpty); + var pbData = ps.ReadUtf8(); + var pbData2 = ps2.ReadUtf8(); + Assert.True(MemUtil.ArraysEqual(pbData, pbData2)); + Assert.Equal(4, pbData.Length); + Assert.Equal(ps.ReadString(), ps2.ReadString()); + pbData = ps.ReadUtf8(); + pbData2 = ps2.ReadUtf8(); + Assert.True(MemUtil.ArraysEqual(pbData, pbData2)); + Assert.True(ps.IsProtected); + Assert.True(ps2.IsProtected); + } + + [Fact] + public void TestIsRandomStringProtected() + { + var r = CryptoRandom.NewWeakRandom(); + var str = string.Empty; + var ps = new ProtectedString(); + for (var i = 0; i < 100; ++i) + { + var bProt = ((r.Next() % 4) != 0); + ps = ps.WithProtection(bProt); + + var x = r.Next(str.Length + 1); + var c = r.Next(20); + var ch = (char) r.Next(1, 256); + + var strIns = new string(ch, c); + str = str.Insert(x, strIns); + ps = ps.Insert(x, strIns); + + Assert.Equal(bProt, ps.IsProtected); + Assert.Equal(str, ps.ReadString()); + + ps = ps.WithProtection(bProt); + + x = r.Next(str.Length); + c = r.Next(str.Length - x + 1); + + str = str.Remove(x, c); + ps = ps.Remove(x, c); + + Assert.Equal(bProt, ps.IsProtected); + Assert.Equal(str, ps.ReadString()); + } + } + + [Fact] + public void TestAreConcatenatedStringsProtected() + { + var ps = new ProtectedString(false, "ABCD"); + var ps2 = new ProtectedString(true, "EFG"); + ps += (ps2 + "HI"); + Assert.True(ps.Equals(new ProtectedString(true, "ABCDEFGHI"), true)); + Assert.True(ps.Equals(new ProtectedString(false, "ABCDEFGHI"), false)); + } + } +} \ No newline at end of file diff --git a/ModernKeePassLib.Test/Serialization/HashedBlockStreamTests.cs b/ModernKeePassLib.Test/Serialization/HashedBlockStreamTests.cs new file mode 100644 index 0000000..0322325 --- /dev/null +++ b/ModernKeePassLib.Test/Serialization/HashedBlockStreamTests.cs @@ -0,0 +1,72 @@ +using System.IO; +using ModernKeePassLib.Serialization; +using ModernKeePassLib.Utility; +using Xunit; + +namespace ModernKeePassLib.Test.Serialization +{ + public class HashedBlockStreamTests + { + static readonly byte[] data = new byte[16]; + + static readonly byte[] hashStreamData = new byte[] + { + // The first 4 bytes are an integer indicating the block index + 0x00, 0x00, 0x00, 0x00, + // Then the SHA-256 hash of the data + 0x37, 0x47, 0x08, 0xFF, 0xF7, 0x71, 0x9D, 0xD5, + 0x97, 0x9E, 0xC8, 0x75, 0xD5, 0x6C, 0xD2, 0x28, + 0x6F, 0x6D, 0x3C, 0xF7, 0xEC, 0x31, 0x7A, 0x3B, + 0x25, 0x63, 0x2A, 0xAB, 0x28, 0xEC, 0x37, 0xBB, + // then an integer that is the length of the data + 0x10, 0x00, 0x00, 0x00, + // and finally the data itself + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Next, a terminating block + 0x01, 0x00, 0x00, 0x00, + // terminating block is indicated by a hash of all 0s... + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // ...and by a size of 0 + 0x00, 0x00, 0x00, 0x00 + }; + + [Fact] + public void TestRead() + { + using (var ms = new MemoryStream(hashStreamData)) + { + using (var hbs = new HashedBlockStream(ms, false)) + { + using (var br = new BinaryReader(hbs)) + { + var bytes = br.ReadBytes(data.Length); + Assert.True(MemUtil.ArraysEqual(bytes, data)); + Assert.Throws(() => br.ReadByte()); + } + } + } + } + + [Fact] + public void TestWrite() + { + var buffer = new byte[hashStreamData.Length]; + using (var ms = new MemoryStream(buffer)) + { + using (var hbs = new HashedBlockStream(ms, true)) + { + using (var bw = new BinaryWriter(hbs)) + { + bw.Write(data); + } + } + Assert.True(MemUtil.ArraysEqual(buffer, hashStreamData)); + } + } + } +} + diff --git a/ModernKeePassLib.Test/Serialization/KdbxFileTests.cs b/ModernKeePassLib.Test/Serialization/KdbxFileTests.cs new file mode 100644 index 0000000..34a49f5 --- /dev/null +++ b/ModernKeePassLib.Test/Serialization/KdbxFileTests.cs @@ -0,0 +1,170 @@ +using System; +using System.Globalization; +using System.IO; +using System.Text; +using ModernKeePassLib.Keys; +using ModernKeePassLib.Security; +using ModernKeePassLib.Serialization; +using ModernKeePassLib.Collections; +using Xunit; + +namespace ModernKeePassLib.Test.Serialization +{ + public class KdbxFileTests + { + const string TestLocalizedAppName = "My Localized App Name"; + + const string TestDatabaseName = "My Database Name"; + const string TestDatabaseDescription = "My Database Description"; + const string TestDefaultUserName = "My Default User Name"; + const string TestColor = "#FF0000"; // Red + + const string TestRootGroupName = "My Root Group Name"; + const string TestRootGroupNotes = "My Root Group Notes"; + const string TestRootGroupDefaultAutoTypeSequence = "My Root Group Default Auto Type Sequence"; + + const string TestDatabase = "\r\n" + + "\r\n" + + "\t\r\n" + + "\t\t" + TestLocalizedAppName + "\r\n" + + "\t\t" + TestDatabaseName + "\r\n" + + "\t\t2017-10-23T08:03:55Z\r\n" + + "\t\t" + TestDatabaseDescription + "\r\n" + + "\t\t2017-10-23T08:03:55Z\r\n" + + "\t\t" + TestDefaultUserName + "\r\n" + + "\t\t2017-10-23T08:03:55Z\r\n" + + "\t\t365\r\n" + + //"\t\t" + testColor + "\r\n" + + "\t\t\r\n" + + "\t\t2017-10-23T08:03:55Z\r\n" + + "\t\t-1\r\n" + + "\t\t-1\r\n" + + "\t\t\r\n" + + "\t\t\tFalse\r\n" + + "\t\t\tFalse\r\n" + + "\t\t\tTrue\r\n" + + "\t\t\tFalse\r\n" + + "\t\t\tFalse\r\n" + + "\t\t\r\n" + + "\t\tTrue\r\n" + + "\t\tAAAAAAAAAAAAAAAAAAAAAA==\r\n" + + "\t\t2017-10-23T08:03:55Z\r\n" + + "\t\tAAAAAAAAAAAAAAAAAAAAAA==\r\n" + + "\t\t2017-10-23T08:03:55Z\r\n" + + "\t\t10\r\n" + + "\t\t6291456\r\n" + + "\t\tAAAAAAAAAAAAAAAAAAAAAA==\r\n" + + "\t\tAAAAAAAAAAAAAAAAAAAAAA==\r\n" + + "\t\t\r\n" + + "\t\t\r\n" + + "\t\r\n" + + "\t\r\n" + + "\t\t\r\n" + + "\t\t\tAAAAAAAAAAAAAAAAAAAAAA==\r\n" + + "\t\t\t" + TestRootGroupName + "\r\n" + + "\t\t\t" + TestRootGroupNotes + "\r\n" + + "\t\t\t49\r\n" + + "\t\t\t\r\n" + + "\t\t\t\t2017-10-23T08:03:55Z\r\n" + + "\t\t\t\t2017-10-23T08:03:55Z\r\n" + + "\t\t\t\t2017-10-23T08:03:55Z\r\n" + + "\t\t\t\t2017-10-23T08:03:55Z\r\n" + + "\t\t\t\tFalse\r\n" + + "\t\t\t\t0\r\n" + + "\t\t\t\t2017-10-23T08:03:55Z\r\n" + + "\t\t\t\r\n" + + "\t\t\tTrue\r\n" + + "\t\t\t" + TestRootGroupDefaultAutoTypeSequence + "\r\n" + + "\t\t\tnull\r\n" + + "\t\t\tnull\r\n" + + "\t\t\tAAAAAAAAAAAAAAAAAAAAAA==\r\n" + + "\t\t\r\n" + + "\t\t\r\n" + + "\t\r\n" + + ""; + + const string TestDate = "2017-10-23T08:03:55Z"; + + [Fact] + public void TestLoad() + { + var database = new PwDatabase(); + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(TestDatabase))) + { + var file = new KdbxFile(database); + file.Load(ms, KdbxFormat.PlainXml, null); + } + //Assert.That(database.Color.ToArgb(), Is.EqualTo(Color.Red.ToArgb())); + Assert.Equal(PwCompressionAlgorithm.GZip, database.Compression); + //Assert.That (database.CustomData, Is.EqualTo ()); + Assert.True(database.CustomIcons.Count == 0); + } + + [Fact] + public void TestSave() + { + var buffer = new byte[4096]; + using (var ms = new MemoryStream(buffer)) + { + var database = new PwDatabase(); + database.New(new IOConnectionInfo(), new CompositeKey()); + var date = DateTime.Parse(TestDate, CultureInfo.CurrentCulture, DateTimeStyles.AdjustToUniversal); + //var date = DateTime.UtcNow; + PwDatabase.LocalizedAppName = TestLocalizedAppName; + database.Name = TestDatabaseName; + database.NameChanged = date; + database.Description = TestDatabaseDescription; + database.DescriptionChanged = date; + database.DefaultUserName = TestDefaultUserName; + database.DefaultUserNameChanged = date; + //database.Color = Color.Red; + database.MasterKeyChanged = date; + database.RecycleBinChanged = date; + database.EntryTemplatesGroupChanged = date; + database.RootGroup.Uuid = PwUuid.Zero; + database.RootGroup.Name = TestRootGroupName; + database.RootGroup.Notes = TestRootGroupNotes; + database.RootGroup.DefaultAutoTypeSequence = TestRootGroupDefaultAutoTypeSequence; + database.RootGroup.CreationTime = date; + database.RootGroup.LastModificationTime = date; + database.RootGroup.LastAccessTime = date; + database.RootGroup.ExpiryTime = date; + database.RootGroup.LocationChanged = date; + var file = new KdbxFile(database); + file.Save(ms, null, KdbxFormat.PlainXml, null); + } + var fileContents = Encoding.UTF8.GetString(buffer, 0, buffer.Length).Replace("\0", ""); + if (typeof(KdbxFile).Namespace.StartsWith("KeePassLib.")) + { + // Upstream KeePassLib does not specify line endings for XmlTextWriter, + // so it uses native line endings. + fileContents = fileContents.Replace("\n", "\r\n"); + } + Assert.Equal(fileContents, TestDatabase); + } + + [Fact] + public void TestSearch() + { + var database = new PwDatabase(); + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(TestDatabase))) + { + var file = new KdbxFile(database); + file.Load(ms, KdbxFormat.PlainXml, null); + } + var sp = new SearchParameters() + { + SearchString = "sfsoiwsefsi" + }; + var listStorage = new PwObjectList(); + database.RootGroup.SearchEntries(sp, listStorage); + Assert.Equal(0U, listStorage.UCount); + var entry = new PwEntry(true, true); + entry.Strings.Set("Title", new ProtectedString(false, "NaMe")); + database.RootGroup.AddEntry(entry, true); + sp.SearchString = "name"; + database.RootGroup.SearchEntries(sp, listStorage); + Assert.Equal(1U, listStorage.UCount); + } + } +} diff --git a/ModernKeePassLib.Test/Utility/GfxUtilTests.cs b/ModernKeePassLib.Test/Utility/GfxUtilTests.cs new file mode 100644 index 0000000..f9d05dc --- /dev/null +++ b/ModernKeePassLib.Test/Utility/GfxUtilTests.cs @@ -0,0 +1,26 @@ +using System; +using ModernKeePassLib.Utility; +using Xunit; + +namespace ModernKeePassLib.Test.Utility +{ + public class GfxUtilTests + { + // 16x16 all white PNG file, base64 encoded + const string testImageData = + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAAsTAAA" + + "LEwEAmpwYAAAAB3RJTUUH3wMOFgIgmTCUMQAAABl0RVh0Q29tbWVudABDcmVhdG" + + "VkIHdpdGggR0lNUFeBDhcAAAAaSURBVCjPY/z//z8DKYCJgUQwqmFUw9DRAABVb" + + "QMdny4VogAAAABJRU5ErkJggg=="; + + //[Fact] + //public void TestLoadImage () + //{ + // var testData = Convert.FromBase64String (testImageData); + // var image = GfxUtil.ScaleImage(testData, 16, 16, ScaleTransformFlags.UIIcon); + // //var image = GfxUtil.LoadImage(testData); + // Assert.Equal(image.Width, 16); + // Assert.Equal(image.Height, 16); + //} + } +} diff --git a/ModernKeePassLib.Test/Utility/MemUtilTests.cs b/ModernKeePassLib.Test/Utility/MemUtilTests.cs new file mode 100644 index 0000000..703126b --- /dev/null +++ b/ModernKeePassLib.Test/Utility/MemUtilTests.cs @@ -0,0 +1,88 @@ +using System.Text; +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Utility; +using Xunit; + +namespace ModernKeePassLib.Test.Utility +{ + public class MemUtilTests + { + private byte[] _pb = CryptoRandom.Instance.GetRandomBytes((uint)CryptoRandom.NewWeakRandom().Next(0, 0x2FFFF)); + + [Fact] + public void TestGzip() + { + var pbCompressed = MemUtil.Compress(_pb); + Assert.True(MemUtil.ArraysEqual(MemUtil.Decompress(pbCompressed), _pb)); + } + + [Fact] + public void TestMemUtil() + { + var enc = StrUtil.Utf8; + _pb = enc.GetBytes("012345678901234567890a"); + var pbN = enc.GetBytes("9012"); + Assert.Equal(9, MemUtil.IndexOf(_pb, pbN)); + + pbN = enc.GetBytes("01234567890123"); + Assert.Equal(0, MemUtil.IndexOf(_pb, pbN)); + + pbN = enc.GetBytes("a"); + Assert.Equal(21, MemUtil.IndexOf(_pb, pbN)); + + pbN = enc.GetBytes("0a"); + Assert.Equal(20, MemUtil.IndexOf(_pb, pbN)); + + pbN = enc.GetBytes("1"); + Assert.Equal(1, MemUtil.IndexOf(_pb, pbN)); + + pbN = enc.GetBytes("b"); + Assert.True(MemUtil.IndexOf(_pb, pbN) < 0); + + pbN = enc.GetBytes("012b"); + Assert.True(MemUtil.IndexOf(_pb, pbN) < 0); + } + + [Fact] + public void TestBase32() + { + var pbRes = MemUtil.ParseBase32("MY======"); + var pbExp = Encoding.UTF8.GetBytes("f"); + Assert.True(MemUtil.ArraysEqual(pbRes, pbExp)); + + pbRes = MemUtil.ParseBase32("MZXQ===="); + pbExp = Encoding.UTF8.GetBytes("fo"); + Assert.True(MemUtil.ArraysEqual(pbRes, pbExp)); + + pbRes = MemUtil.ParseBase32("MZXW6==="); + pbExp = Encoding.UTF8.GetBytes("foo"); + Assert.True(MemUtil.ArraysEqual(pbRes, pbExp)); + + pbRes = MemUtil.ParseBase32("MZXW6YQ="); + pbExp = Encoding.UTF8.GetBytes("foob"); + Assert.True(MemUtil.ArraysEqual(pbRes, pbExp)); + + pbRes = MemUtil.ParseBase32("MZXW6YTB"); + pbExp = Encoding.UTF8.GetBytes("fooba"); + Assert.True(MemUtil.ArraysEqual(pbRes, pbExp)); + + pbRes = MemUtil.ParseBase32("MZXW6YTBOI======"); + pbExp = Encoding.UTF8.GetBytes("foobar"); + Assert.True(MemUtil.ArraysEqual(pbRes, pbExp)); + + pbRes = MemUtil.ParseBase32("JNSXSIDQOJXXM2LEMVZCAYTBONSWIIDPNYQG63TFFV2GS3LFEBYGC43TO5XXEZDTFY======"); + pbExp = Encoding.UTF8.GetBytes("Key provider based on one-time passwords."); + Assert.True(MemUtil.ArraysEqual(pbRes, pbExp)); + } + + [Fact] + public void TestMemUtil2() + { + var i = 0 - 0x10203040; + var pbRes = MemUtil.Int32ToBytes(i); + Assert.Equal("C0CFDFEF", MemUtil.ByteArrayToHexString(pbRes)); + Assert.Equal(MemUtil.BytesToUInt32(pbRes), (uint)i); + Assert.Equal(MemUtil.BytesToInt32(pbRes), i); + } + } +} \ No newline at end of file diff --git a/ModernKeePassLib/Collections/AutoTypeConfig.cs b/ModernKeePassLib/Collections/AutoTypeConfig.cs new file mode 100644 index 0000000..390fccd --- /dev/null +++ b/ModernKeePassLib/Collections/AutoTypeConfig.cs @@ -0,0 +1,244 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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 ModernKeePassLib.Interfaces; + +namespace ModernKeePassLib.Collections +{ + [Flags] + public enum AutoTypeObfuscationOptions + { + None = 0, + UseClipboard = 1 + } + + public sealed class AutoTypeAssociation : IEquatable, + IDeepCloneable + { + private string m_strWindow = string.Empty; + public string WindowName + { + get { return m_strWindow; } + set + { + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + m_strWindow = value; + } + } + + private string m_strSequence = string.Empty; + public string Sequence + { + get { return m_strSequence; } + set + { + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + m_strSequence = value; + } + } + + public AutoTypeAssociation() { } + + public AutoTypeAssociation(string strWindow, string strSeq) + { + if(strWindow == null) throw new ArgumentNullException("strWindow"); + if(strSeq == null) throw new ArgumentNullException("strSeq"); + + m_strWindow = strWindow; + m_strSequence = strSeq; + } + + public bool Equals(AutoTypeAssociation other) + { + if(other == null) return false; + + if(m_strWindow != other.m_strWindow) return false; + if(m_strSequence != other.m_strSequence) return false; + + return true; + } + + public AutoTypeAssociation CloneDeep() + { + return (AutoTypeAssociation)this.MemberwiseClone(); + } + } + + /// + /// A list of auto-type associations. + /// + public sealed class AutoTypeConfig : IEquatable, + IDeepCloneable + { + private bool m_bEnabled = true; + private AutoTypeObfuscationOptions m_atooObfuscation = + AutoTypeObfuscationOptions.None; + private string m_strDefaultSequence = string.Empty; + private List m_lWindowAssocs = + new List(); + + /// + /// Specify whether auto-type is enabled or not. + /// + public bool Enabled + { + get { return m_bEnabled; } + set { m_bEnabled = value; } + } + + /// + /// Specify whether the typing should be obfuscated. + /// + public AutoTypeObfuscationOptions ObfuscationOptions + { + get { return m_atooObfuscation; } + set { m_atooObfuscation = value; } + } + + /// + /// The default keystroke sequence that is auto-typed if + /// no matching window is found in the Associations + /// container. + /// + public string DefaultSequence + { + get { return m_strDefaultSequence; } + set + { + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + m_strDefaultSequence = value; + } + } + + /// + /// Get all auto-type window/keystroke sequence pairs. + /// + public IEnumerable Associations + { + get { return m_lWindowAssocs; } + } + + public int AssociationsCount + { + get { return m_lWindowAssocs.Count; } + } + + /// + /// Construct a new auto-type associations list. + /// + public AutoTypeConfig() + { + } + + /// + /// Remove all associations. + /// + public void Clear() + { + m_lWindowAssocs.Clear(); + } + + /// + /// Clone the auto-type associations list. + /// + /// New, cloned object. + public AutoTypeConfig CloneDeep() + { + AutoTypeConfig newCfg = new AutoTypeConfig(); + + newCfg.m_bEnabled = m_bEnabled; + newCfg.m_atooObfuscation = m_atooObfuscation; + newCfg.m_strDefaultSequence = m_strDefaultSequence; + + foreach(AutoTypeAssociation a in m_lWindowAssocs) + newCfg.Add(a.CloneDeep()); + + return newCfg; + } + + public bool Equals(AutoTypeConfig other) + { + if(other == null) { Debug.Assert(false); return false; } + + if(m_bEnabled != other.m_bEnabled) return false; + if(m_atooObfuscation != other.m_atooObfuscation) return false; + if(m_strDefaultSequence != other.m_strDefaultSequence) return false; + + if(m_lWindowAssocs.Count != other.m_lWindowAssocs.Count) return false; + for(int i = 0; i < m_lWindowAssocs.Count; ++i) + { + if(!m_lWindowAssocs[i].Equals(other.m_lWindowAssocs[i])) + return false; + } + + return true; + } + + public AutoTypeAssociation GetAt(int iIndex) + { + if((iIndex < 0) || (iIndex >= m_lWindowAssocs.Count)) + throw new ArgumentOutOfRangeException("iIndex"); + + return m_lWindowAssocs[iIndex]; + } + + public void Add(AutoTypeAssociation a) + { + if(a == null) { Debug.Assert(false); throw new ArgumentNullException("a"); } + + m_lWindowAssocs.Add(a); + } + + public void Insert(int iIndex, AutoTypeAssociation a) + { + if((iIndex < 0) || (iIndex > m_lWindowAssocs.Count)) + throw new ArgumentOutOfRangeException("iIndex"); + if(a == null) { Debug.Assert(false); throw new ArgumentNullException("a"); } + + m_lWindowAssocs.Insert(iIndex, a); + } + + public void RemoveAt(int iIndex) + { + if((iIndex < 0) || (iIndex >= m_lWindowAssocs.Count)) + throw new ArgumentOutOfRangeException("iIndex"); + + m_lWindowAssocs.RemoveAt(iIndex); + } + + // public void Sort() + // { + // m_lWindowAssocs.Sort(AutoTypeConfig.AssocCompareFn); + // } + + // private static int AssocCompareFn(AutoTypeAssociation x, + // AutoTypeAssociation y) + // { + // if(x == null) { Debug.Assert(false); return ((y == null) ? 0 : -1); } + // if(y == null) { Debug.Assert(false); return 1; } + // int cn = x.WindowName.CompareTo(y.WindowName); + // if(cn != 0) return cn; + // return x.Sequence.CompareTo(y.Sequence); + // } + } +} diff --git a/ModernKeePassLib/Collections/ProtectedBinaryDictionary.cs b/ModernKeePassLib/Collections/ProtectedBinaryDictionary.cs new file mode 100644 index 0000000..8d51964 --- /dev/null +++ b/ModernKeePassLib/Collections/ProtectedBinaryDictionary.cs @@ -0,0 +1,172 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; +using System.Collections.Generic; +using System.Text; +using System.Diagnostics; + +using ModernKeePassLib.Interfaces; +using ModernKeePassLib.Security; + +#if KeePassLibSD +using KeePassLibSD; +#endif + +namespace ModernKeePassLib.Collections +{ + /// + /// A list of ProtectedBinary objects (dictionary). + /// + public sealed class ProtectedBinaryDictionary : + IDeepCloneable, + IEnumerable> + { + private SortedDictionary m_vBinaries = + new SortedDictionary(); + + /// + /// Get the number of binaries in this entry. + /// + public uint UCount + { + get { return (uint)m_vBinaries.Count; } + } + + /// + /// Construct a new list of protected binaries. + /// + public ProtectedBinaryDictionary() + { + } + + IEnumerator IEnumerable.GetEnumerator() + { + return m_vBinaries.GetEnumerator(); + } + + public IEnumerator> GetEnumerator() + { + return m_vBinaries.GetEnumerator(); + } + + public void Clear() + { + m_vBinaries.Clear(); + } + + /// + /// Clone the current ProtectedBinaryList object, including all + /// stored protected strings. + /// + /// New ProtectedBinaryList object. + public ProtectedBinaryDictionary CloneDeep() + { + ProtectedBinaryDictionary plNew = new ProtectedBinaryDictionary(); + + foreach(KeyValuePair kvpBin in m_vBinaries) + { + // ProtectedBinary objects are immutable + plNew.Set(kvpBin.Key, kvpBin.Value); + } + + return plNew; + } + + public bool EqualsDictionary(ProtectedBinaryDictionary dict) + { + if(dict == null) { Debug.Assert(false); return false; } + + if(m_vBinaries.Count != dict.m_vBinaries.Count) return false; + + foreach(KeyValuePair kvp in m_vBinaries) + { + ProtectedBinary pb = dict.Get(kvp.Key); + if(pb == null) return false; + if(!pb.Equals(kvp.Value)) return false; + } + + return true; + } + + /// + /// Get one of the stored binaries. + /// + /// Binary identifier. + /// Protected binary. If the binary identified by + /// cannot be found, the function + /// returns null. + /// Thrown if the input + /// parameter is null. + public ProtectedBinary Get(string strName) + { + Debug.Assert(strName != null); if(strName == null) throw new ArgumentNullException("strName"); + + ProtectedBinary pb; + if(m_vBinaries.TryGetValue(strName, out pb)) return pb; + + return null; + } + + /// + /// Set a binary object. + /// + /// Identifier of the binary field to modify. + /// New value. This parameter must not be null. + /// Thrown if any of the input + /// parameters is null. + public void Set(string strField, ProtectedBinary pbNewValue) + { + Debug.Assert(strField != null); if(strField == null) throw new ArgumentNullException("strField"); + Debug.Assert(pbNewValue != null); if(pbNewValue == null) throw new ArgumentNullException("pbNewValue"); + + m_vBinaries[strField] = pbNewValue; + } + + /// + /// Remove a binary object. + /// + /// Identifier of the binary field to remove. + /// Returns true if the object has been successfully + /// removed, otherwise false. + /// Thrown if the input parameter + /// is null. + public bool Remove(string strField) + { + Debug.Assert(strField != null); if(strField == null) throw new ArgumentNullException("strField"); + + return m_vBinaries.Remove(strField); + } + + public string KeysToString() + { + if(m_vBinaries.Count == 0) return string.Empty; + + StringBuilder sb = new StringBuilder(); + foreach(KeyValuePair kvp in m_vBinaries) + { + if(sb.Length > 0) sb.Append(", "); + sb.Append(kvp.Key); + } + + return sb.ToString(); + } + } +} diff --git a/ModernKeePassLib/Collections/ProtectedBinarySet.cs b/ModernKeePassLib/Collections/ProtectedBinarySet.cs new file mode 100644 index 0000000..1d95c71 --- /dev/null +++ b/ModernKeePassLib/Collections/ProtectedBinarySet.cs @@ -0,0 +1,174 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +using ModernKeePassLib.Delegates; +using ModernKeePassLib.Security; + +namespace ModernKeePassLib.Collections +{ + internal sealed class ProtectedBinarySet : IEnumerable> + { + private Dictionary m_d = + new Dictionary(); + + public ProtectedBinarySet() + { + } + + IEnumerator IEnumerable.GetEnumerator() + { + return m_d.GetEnumerator(); + } + + public IEnumerator> GetEnumerator() + { + return m_d.GetEnumerator(); + } + + public void Clear() + { + m_d.Clear(); + } + + private int GetFreeID() + { + int i = m_d.Count; + while(m_d.ContainsKey(i)) { ++i; } + Debug.Assert(i == m_d.Count); // m_d.Count should be free + return i; + } + + public ProtectedBinary Get(int iID) + { + ProtectedBinary pb; + if(m_d.TryGetValue(iID, out pb)) return pb; + + // Debug.Assert(false); // No assert + return null; + } + + public int Find(ProtectedBinary pb) + { + if(pb == null) { Debug.Assert(false); return -1; } + + // Fast search by reference + foreach(KeyValuePair kvp in m_d) + { + if(object.ReferenceEquals(pb, kvp.Value)) + { + Debug.Assert(pb.Equals(kvp.Value)); + return kvp.Key; + } + } + + // Slow search by content + foreach(KeyValuePair kvp in m_d) + { + if(pb.Equals(kvp.Value)) return kvp.Key; + } + + // Debug.Assert(false); // No assert + return -1; + } + + public void Set(int iID, ProtectedBinary pb) + { + if(iID < 0) { Debug.Assert(false); return; } + if(pb == null) { Debug.Assert(false); return; } + + m_d[iID] = pb; + } + + public void Add(ProtectedBinary pb) + { + if(pb == null) { Debug.Assert(false); return; } + + int i = Find(pb); + if(i >= 0) return; // Exists already + + i = GetFreeID(); + m_d[i] = pb; + } + + public void AddFrom(ProtectedBinaryDictionary d) + { + if(d == null) { Debug.Assert(false); return; } + + foreach(KeyValuePair kvp in d) + { + Add(kvp.Value); + } + } + + public void AddFrom(PwGroup pg) + { + if(pg == null) { Debug.Assert(false); return; } + + EntryHandler eh = delegate(PwEntry pe) + { + if(pe == null) { Debug.Assert(false); return true; } + + AddFrom(pe.Binaries); + foreach(PwEntry peHistory in pe.History) + { + if(peHistory == null) { Debug.Assert(false); continue; } + AddFrom(peHistory.Binaries); + } + + return true; + }; + + pg.TraverseTree(TraversalMethod.PreOrder, null, eh); + } + + public ProtectedBinary[] ToArray() + { + int n = m_d.Count; + ProtectedBinary[] v = new ProtectedBinary[n]; + + foreach(KeyValuePair kvp in m_d) + { + if((kvp.Key < 0) || (kvp.Key >= n)) + { + Debug.Assert(false); + throw new InvalidOperationException(); + } + + v[kvp.Key] = kvp.Value; + } + + for(int i = 0; i < n; ++i) + { + if(v[i] == null) + { + Debug.Assert(false); + throw new InvalidOperationException(); + } + } + + return v; + } + } +} diff --git a/ModernKeePassLib/Collections/ProtectedStringDictionary.cs b/ModernKeePassLib/Collections/ProtectedStringDictionary.cs new file mode 100644 index 0000000..d02e5bc --- /dev/null +++ b/ModernKeePassLib/Collections/ProtectedStringDictionary.cs @@ -0,0 +1,298 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +using ModernKeePassLib.Interfaces; +using ModernKeePassLib.Security; +using ModernKeePassLib.Utility; + +#if KeePassLibSD +using KeePassLibSD; +#endif + +namespace ModernKeePassLib.Collections +{ + /// + /// A list of ProtectedString objects (dictionary). + /// + public sealed class ProtectedStringDictionary : + IDeepCloneable, + IEnumerable> + { + private SortedDictionary m_vStrings = + new SortedDictionary(); + + /// + /// Get the number of strings in this entry. + /// + public uint UCount + { + get { return (uint)m_vStrings.Count; } + } + + /// + /// Construct a new list of protected strings. + /// + public ProtectedStringDictionary() + { + } + + IEnumerator IEnumerable.GetEnumerator() + { + return m_vStrings.GetEnumerator(); + } + + public IEnumerator> GetEnumerator() + { + return m_vStrings.GetEnumerator(); + } + + public void Clear() + { + m_vStrings.Clear(); + } + + /// + /// Clone the current ProtectedStringList object, including all + /// stored protected strings. + /// + /// New ProtectedStringList object. + public ProtectedStringDictionary CloneDeep() + { + ProtectedStringDictionary plNew = new ProtectedStringDictionary(); + + foreach(KeyValuePair kvpStr in m_vStrings) + { + // ProtectedString objects are immutable + plNew.Set(kvpStr.Key, kvpStr.Value); + } + + return plNew; + } + + [Obsolete] + public bool EqualsDictionary(ProtectedStringDictionary dict) + { + return EqualsDictionary(dict, PwCompareOptions.None, MemProtCmpMode.None); + } + + [Obsolete] + public bool EqualsDictionary(ProtectedStringDictionary dict, + MemProtCmpMode mpCompare) + { + return EqualsDictionary(dict, PwCompareOptions.None, mpCompare); + } + + public bool EqualsDictionary(ProtectedStringDictionary dict, + PwCompareOptions pwOpt, MemProtCmpMode mpCompare) + { + if(dict == null) { Debug.Assert(false); return false; } + + bool bNeEqStd = ((pwOpt & PwCompareOptions.NullEmptyEquivStd) != + PwCompareOptions.None); + if(!bNeEqStd) + { + if(m_vStrings.Count != dict.m_vStrings.Count) return false; + } + + foreach(KeyValuePair kvp in m_vStrings) + { + bool bStdField = PwDefs.IsStandardField(kvp.Key); + ProtectedString ps = dict.Get(kvp.Key); + + if(bNeEqStd && (ps == null) && bStdField) + ps = ProtectedString.Empty; + + if(ps == null) return false; + + if(mpCompare == MemProtCmpMode.Full) + { + if(ps.IsProtected != kvp.Value.IsProtected) return false; + } + else if(mpCompare == MemProtCmpMode.CustomOnly) + { + if(!bStdField && (ps.IsProtected != kvp.Value.IsProtected)) + return false; + } + + if(!ps.Equals(kvp.Value, false)) return false; + } + + if(bNeEqStd) + { + foreach(KeyValuePair kvp in dict.m_vStrings) + { + ProtectedString ps = Get(kvp.Key); + + if(ps != null) continue; // Compared previously + if(!PwDefs.IsStandardField(kvp.Key)) return false; + if(!kvp.Value.IsEmpty) return false; + } + } + + return true; + } + + /// + /// Get one of the protected strings. + /// + /// String identifier. + /// Protected string. If the string identified by + /// cannot be found, the function + /// returns null. + /// Thrown if the input parameter + /// is null. + public ProtectedString Get(string strName) + { + Debug.Assert(strName != null); if(strName == null) throw new ArgumentNullException("strName"); + + ProtectedString ps; + if(m_vStrings.TryGetValue(strName, out ps)) return ps; + + return null; + } + + /// + /// Get one of the protected strings. The return value is never null. + /// If the requested string cannot be found, an empty protected string + /// object is returned. + /// + /// String identifier. + /// Returns a protected string object. If the standard string + /// has not been set yet, the return value is an empty string (""). + /// Thrown if the input + /// parameter is null. + public ProtectedString GetSafe(string strName) + { + Debug.Assert(strName != null); if(strName == null) throw new ArgumentNullException("strName"); + + ProtectedString ps; + if(m_vStrings.TryGetValue(strName, out ps)) return ps; + + return ProtectedString.Empty; + } + + /// + /// Test if a named string exists. + /// + /// Name of the string to try. + /// Returns true if the string exists, otherwise false. + /// Thrown if + /// is null. + public bool Exists(string strName) + { + Debug.Assert(strName != null); if(strName == null) throw new ArgumentNullException("strName"); + + return m_vStrings.ContainsKey(strName); + } + + /// + /// Get one of the protected strings. If the string doesn't exist, the + /// return value is an empty string (""). + /// + /// Name of the requested string. + /// Requested string value or an empty string, if the named + /// string doesn't exist. + /// Thrown if the input + /// parameter is null. + public string ReadSafe(string strName) + { + Debug.Assert(strName != null); if(strName == null) throw new ArgumentNullException("strName"); + + ProtectedString ps; + if(m_vStrings.TryGetValue(strName, out ps)) + return ps.ReadString(); + + return string.Empty; + } + + /// + /// Get one of the entry strings. If the string doesn't exist, the + /// return value is an empty string (""). If the string is + /// in-memory protected, the return value is PwDefs.HiddenPassword. + /// + /// Name of the requested string. + /// Returns the requested string in plain-text or + /// PwDefs.HiddenPassword if the string cannot be found. + /// Thrown if the input + /// parameter is null. + public string ReadSafeEx(string strName) + { + Debug.Assert(strName != null); if(strName == null) throw new ArgumentNullException("strName"); + + ProtectedString ps; + if(m_vStrings.TryGetValue(strName, out ps)) + { + if(ps.IsProtected) return PwDefs.HiddenPassword; + return ps.ReadString(); + } + + return string.Empty; + } + + /// + /// Set a string. + /// + /// Identifier of the string field to modify. + /// New value. This parameter must not be null. + /// Thrown if one of the input + /// parameters is null. + public void Set(string strField, ProtectedString psNewValue) + { + Debug.Assert(strField != null); if(strField == null) throw new ArgumentNullException("strField"); + Debug.Assert(psNewValue != null); if(psNewValue == null) throw new ArgumentNullException("psNewValue"); + + m_vStrings[strField] = psNewValue; + } + + /// + /// Delete a string. + /// + /// Name of the string field to delete. + /// Returns true if the field has been successfully + /// removed, otherwise the return value is false. + /// Thrown if the input + /// parameter is null. + public bool Remove(string strField) + { + Debug.Assert(strField != null); if(strField == null) throw new ArgumentNullException("strField"); + + return m_vStrings.Remove(strField); + } + + public List GetKeys() + { + return new List(m_vStrings.Keys); + } + + public void EnableProtection(string strField, bool bProtect) + { + ProtectedString ps = Get(strField); + if(ps == null) return; // Nothing to do, no assert + + if(ps.IsProtected != bProtect) + Set(strField, ps.WithProtection(bProtect)); + } + } +} diff --git a/ModernKeePassLib/Collections/PwObjectList.cs b/ModernKeePassLib/Collections/PwObjectList.cs new file mode 100644 index 0000000..814b1cb --- /dev/null +++ b/ModernKeePassLib/Collections/PwObjectList.cs @@ -0,0 +1,380 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; +using System.Collections.Generic; +using System.Diagnostics; + +using ModernKeePassLib.Interfaces; + +namespace ModernKeePassLib.Collections +{ + /// + /// List of objects that implement IDeepCloneable, + /// and cannot be null. + /// + /// Type specifier. + public sealed class PwObjectList : IEnumerable + where T : class, IDeepCloneable + { + private List m_vObjects = new List(); + + /// + /// Get number of objects in this list. + /// + public uint UCount + { + get { return (uint)m_vObjects.Count; } + } + + /// + /// Construct a new list of objects. + /// + public PwObjectList() + { + } + + IEnumerator IEnumerable.GetEnumerator() + { + return m_vObjects.GetEnumerator(); + } + + public IEnumerator GetEnumerator() + { + return m_vObjects.GetEnumerator(); + } + + public void Clear() + { + // Do not destroy contained objects! + m_vObjects.Clear(); + } + + /// + /// Clone the current PwObjectList, including all + /// stored objects (deep copy). + /// + /// New PwObjectList. + public PwObjectList CloneDeep() + { + PwObjectList pl = new PwObjectList(); + + foreach(T po in m_vObjects) + pl.Add(po.CloneDeep()); + + return pl; + } + + public PwObjectList CloneShallow() + { + PwObjectList tNew = new PwObjectList(); + + foreach(T po in m_vObjects) tNew.Add(po); + + return tNew; + } + + public List CloneShallowToList() + { + PwObjectList tNew = CloneShallow(); + return tNew.m_vObjects; + } + + /// + /// Add an object to this list. + /// + /// Object to be added. + /// Thrown if the input + /// parameter is null. + public void Add(T pwObject) + { + Debug.Assert(pwObject != null); + if(pwObject == null) throw new ArgumentNullException("pwObject"); + + m_vObjects.Add(pwObject); + } + + public void Add(PwObjectList vObjects) + { + Debug.Assert(vObjects != null); + if(vObjects == null) throw new ArgumentNullException("vObjects"); + + foreach(T po in vObjects) + { + m_vObjects.Add(po); + } + } + + public void Add(List vObjects) + { + Debug.Assert(vObjects != null); + if(vObjects == null) throw new ArgumentNullException("vObjects"); + + foreach(T po in vObjects) + { + m_vObjects.Add(po); + } + } + + public void Insert(uint uIndex, T pwObject) + { + Debug.Assert(pwObject != null); + if(pwObject == null) throw new ArgumentNullException("pwObject"); + + m_vObjects.Insert((int)uIndex, pwObject); + } + + /// + /// Get an object of the list. + /// + /// Index of the object to get. Must be valid, otherwise an + /// exception is thrown. + /// Reference to an existing T object. Is never null. + public T GetAt(uint uIndex) + { + Debug.Assert(uIndex < m_vObjects.Count); + if(uIndex >= m_vObjects.Count) throw new ArgumentOutOfRangeException("uIndex"); + + return m_vObjects[(int)uIndex]; + } + + public void SetAt(uint uIndex, T pwObject) + { + Debug.Assert(pwObject != null); + if(pwObject == null) throw new ArgumentNullException("pwObject"); + if(uIndex >= (uint)m_vObjects.Count) + throw new ArgumentOutOfRangeException("uIndex"); + + m_vObjects[(int)uIndex] = pwObject; + } + + /// + /// Get a range of objects. + /// + /// Index of the first object to be + /// returned (inclusive). + /// Index of the last object to be + /// returned (inclusive). + /// + public List GetRange(uint uStartIndexIncl, uint uEndIndexIncl) + { + if(uStartIndexIncl >= (uint)m_vObjects.Count) + throw new ArgumentOutOfRangeException("uStartIndexIncl"); + if(uEndIndexIncl >= (uint)m_vObjects.Count) + throw new ArgumentOutOfRangeException("uEndIndexIncl"); + if(uStartIndexIncl > uEndIndexIncl) + throw new ArgumentException(); + + List list = new List((int)(uEndIndexIncl - uStartIndexIncl) + 1); + for(uint u = uStartIndexIncl; u <= uEndIndexIncl; ++u) + { + list.Add(m_vObjects[(int)u]); + } + + return list; + } + + public int IndexOf(T pwReference) + { + Debug.Assert(pwReference != null); if(pwReference == null) throw new ArgumentNullException("pwReference"); + + return m_vObjects.IndexOf(pwReference); + } + + /// + /// Delete an object of this list. The object to be deleted is identified + /// by a reference handle. + /// + /// Reference of the object to be deleted. + /// Returns true if the object was deleted, false if + /// the object wasn't found in this list. + /// Thrown if the input + /// parameter is null. + public bool Remove(T pwReference) + { + Debug.Assert(pwReference != null); if(pwReference == null) throw new ArgumentNullException("pwReference"); + + return m_vObjects.Remove(pwReference); + } + + public void RemoveAt(uint uIndex) + { + m_vObjects.RemoveAt((int)uIndex); + } + + /// + /// Move an object up or down. + /// + /// The object to be moved. + /// Move one up. If false, move one down. + public void MoveOne(T tObject, bool bUp) + { + Debug.Assert(tObject != null); + if(tObject == null) throw new ArgumentNullException("tObject"); + + int nCount = m_vObjects.Count; + if(nCount <= 1) return; + + int nIndex = m_vObjects.IndexOf(tObject); + if(nIndex < 0) { Debug.Assert(false); return; } + + if(bUp && (nIndex > 0)) // No assert for top item + { + T tTemp = m_vObjects[nIndex - 1]; + m_vObjects[nIndex - 1] = m_vObjects[nIndex]; + m_vObjects[nIndex] = tTemp; + } + else if(!bUp && (nIndex != (nCount - 1))) // No assert for bottom item + { + T tTemp = m_vObjects[nIndex + 1]; + m_vObjects[nIndex + 1] = m_vObjects[nIndex]; + m_vObjects[nIndex] = tTemp; + } + } + + public void MoveOne(T[] vObjects, bool bUp) + { + Debug.Assert(vObjects != null); + if(vObjects == null) throw new ArgumentNullException("vObjects"); + + List lIndices = new List(); + foreach(T t in vObjects) + { + if(t == null) { Debug.Assert(false); continue; } + + int p = IndexOf(t); + if(p >= 0) lIndices.Add(p); + else { Debug.Assert(false); } + } + + MoveOne(lIndices.ToArray(), bUp); + } + + public void MoveOne(int[] vIndices, bool bUp) + { + Debug.Assert(vIndices != null); + if(vIndices == null) throw new ArgumentNullException("vIndices"); + + int n = m_vObjects.Count; + if(n <= 1) return; // No moving possible + + int m = vIndices.Length; + if(m == 0) return; // Nothing to move + + int[] v = new int[m]; + Array.Copy(vIndices, v, m); + Array.Sort(v); + + if((bUp && (v[0] <= 0)) || (!bUp && (v[m - 1] >= (n - 1)))) + return; // Moving as a block is not possible + + int iStart = (bUp ? 0 : (m - 1)); + int iExcl = (bUp ? m : -1); + int iStep = (bUp ? 1 : -1); + + for(int i = iStart; i != iExcl; i += iStep) + { + int p = v[i]; + if((p < 0) || (p >= n)) { Debug.Assert(false); continue; } + + T t = m_vObjects[p]; + + if(bUp) + { + Debug.Assert(p > 0); + m_vObjects.RemoveAt(p); + m_vObjects.Insert(p - 1, t); + } + else // Down + { + Debug.Assert(p < (n - 1)); + m_vObjects.RemoveAt(p); + m_vObjects.Insert(p + 1, t); + } + } + } + + /// + /// Move some of the objects in this list to the top/bottom. + /// + /// List of objects to be moved. + /// Move to top. If false, move to bottom. + public void MoveTopBottom(T[] vObjects, bool bTop) + { + Debug.Assert(vObjects != null); + if(vObjects == null) throw new ArgumentNullException("vObjects"); + + if(vObjects.Length == 0) return; + + int nCount = m_vObjects.Count; + foreach(T t in vObjects) m_vObjects.Remove(t); + + if(bTop) + { + int nPos = 0; + foreach(T t in vObjects) + { + m_vObjects.Insert(nPos, t); + ++nPos; + } + } + else // Move to bottom + { + foreach(T t in vObjects) m_vObjects.Add(t); + } + + Debug.Assert(nCount == m_vObjects.Count); + if(nCount != m_vObjects.Count) + throw new ArgumentException("At least one of the T objects in the vObjects list doesn't exist!"); + } + + public void Sort(IComparer tComparer) + { + if(tComparer == null) throw new ArgumentNullException("tComparer"); + + m_vObjects.Sort(tComparer); + } + + public void Sort(Comparison tComparison) + { + if(tComparison == null) throw new ArgumentNullException("tComparison"); + + m_vObjects.Sort(tComparison); + } + + public static PwObjectList FromArray(T[] tArray) + { + if(tArray == null) throw new ArgumentNullException("tArray"); + + PwObjectList l = new PwObjectList(); + foreach(T t in tArray) { l.Add(t); } + return l; + } + + public static PwObjectList FromList(List tList) + { + if(tList == null) throw new ArgumentNullException("tList"); + + PwObjectList l = new PwObjectList(); + l.Add(tList); + return l; + } + } +} diff --git a/ModernKeePassLib/Collections/PwObjectPool.cs b/ModernKeePassLib/Collections/PwObjectPool.cs new file mode 100644 index 0000000..6178279 --- /dev/null +++ b/ModernKeePassLib/Collections/PwObjectPool.cs @@ -0,0 +1,232 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +using ModernKeePassLib.Delegates; +using ModernKeePassLib.Interfaces; +using ModernKeePassLib.Utility; + +#if KeePassLibSD +using KeePassLibSD; +#endif + +namespace ModernKeePassLib.Collections +{ + public sealed class PwObjectPool + { + private SortedDictionary m_dict = + new SortedDictionary(); + + public static PwObjectPool FromGroupRecursive(PwGroup pgRoot, bool bEntries) + { + if(pgRoot == null) throw new ArgumentNullException("pgRoot"); + + PwObjectPool p = new PwObjectPool(); + + if(!bEntries) p.m_dict[pgRoot.Uuid] = pgRoot; + GroupHandler gh = delegate(PwGroup pg) + { + p.m_dict[pg.Uuid] = pg; + return true; + }; + + EntryHandler eh = delegate(PwEntry pe) + { + p.m_dict[pe.Uuid] = pe; + return true; + }; + + pgRoot.TraverseTree(TraversalMethod.PreOrder, bEntries ? null : gh, + bEntries ? eh : null); + return p; + } + + public IStructureItem Get(PwUuid pwUuid) + { + IStructureItem pItem; + m_dict.TryGetValue(pwUuid, out pItem); + return pItem; + } + + public bool ContainsOnlyType(Type t) + { + foreach(KeyValuePair kvp in m_dict) + { + if(kvp.Value.GetType() != t) return false; + } + + return true; + } + } + + internal sealed class PwObjectPoolEx + { + private Dictionary m_dUuidToId = + new Dictionary(); + private Dictionary m_dIdToItem = + new Dictionary(); + + private PwObjectPoolEx() + { + } + + public static PwObjectPoolEx FromGroup(PwGroup pg) + { + PwObjectPoolEx p = new PwObjectPoolEx(); + + if(pg == null) { Debug.Assert(false); return p; } + + ulong uFreeId = 2; // 0 = "not found", 1 is a hole + + p.m_dUuidToId[pg.Uuid] = uFreeId; + p.m_dIdToItem[uFreeId] = pg; + uFreeId += 2; // Make hole + + p.AddGroupRec(pg, ref uFreeId); + return p; + } + + private void AddGroupRec(PwGroup pg, ref ulong uFreeId) + { + if(pg == null) { Debug.Assert(false); return; } + + ulong uId = uFreeId; + + // Consecutive entries must have consecutive IDs + foreach(PwEntry pe in pg.Entries) + { + Debug.Assert(!m_dUuidToId.ContainsKey(pe.Uuid)); + Debug.Assert(!m_dIdToItem.ContainsValue(pe)); + + m_dUuidToId[pe.Uuid] = uId; + m_dIdToItem[uId] = pe; + ++uId; + } + ++uId; // Make hole + + // Consecutive groups must have consecutive IDs + foreach(PwGroup pgSub in pg.Groups) + { + Debug.Assert(!m_dUuidToId.ContainsKey(pgSub.Uuid)); + Debug.Assert(!m_dIdToItem.ContainsValue(pgSub)); + + m_dUuidToId[pgSub.Uuid] = uId; + m_dIdToItem[uId] = pgSub; + ++uId; + } + ++uId; // Make hole + + foreach(PwGroup pgSub in pg.Groups) + { + AddGroupRec(pgSub, ref uId); + } + + uFreeId = uId; + } + + public ulong GetIdByUuid(PwUuid pwUuid) + { + if(pwUuid == null) { Debug.Assert(false); return 0; } + + ulong uId; + m_dUuidToId.TryGetValue(pwUuid, out uId); + return uId; + } + + public IStructureItem GetItemByUuid(PwUuid pwUuid) + { + if(pwUuid == null) { Debug.Assert(false); return null; } + + ulong uId; + if(!m_dUuidToId.TryGetValue(pwUuid, out uId)) return null; + Debug.Assert(uId != 0); + + return GetItemById(uId); + } + + public IStructureItem GetItemById(ulong uId) + { + IStructureItem p; + m_dIdToItem.TryGetValue(uId, out p); + return p; + } + } + + internal sealed class PwObjectBlock : IEnumerable + where T : class, ITimeLogger, IStructureItem, IDeepCloneable + { + private List m_l = new List(); + + public T PrimaryItem + { + get { return ((m_l.Count > 0) ? m_l[0] : null); } + } + + private DateTime m_dtLocationChanged = TimeUtil.SafeMinValueUtc; + public DateTime LocationChanged + { + get { return m_dtLocationChanged; } + } + + private PwObjectPoolEx m_poolAssoc = null; + public PwObjectPoolEx PoolAssoc + { + get { return m_poolAssoc; } + } + + public PwObjectBlock() + { + } + +#if DEBUG + public override string ToString() + { + return ("PwObjectBlock, Count = " + m_l.Count.ToString()); + } +#endif + + IEnumerator IEnumerable.GetEnumerator() + { + return m_l.GetEnumerator(); + } + + public IEnumerator GetEnumerator() + { + return m_l.GetEnumerator(); + } + + public void Add(T t, DateTime dtLoc, PwObjectPoolEx pool) + { + if(t == null) { Debug.Assert(false); return; } + + m_l.Add(t); + + if(dtLoc > m_dtLocationChanged) + { + m_dtLocationChanged = dtLoc; + m_poolAssoc = pool; + } + } + } +} diff --git a/ModernKeePassLib/Collections/StringDictionaryEx.cs b/ModernKeePassLib/Collections/StringDictionaryEx.cs new file mode 100644 index 0000000..a14a24b --- /dev/null +++ b/ModernKeePassLib/Collections/StringDictionaryEx.cs @@ -0,0 +1,130 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; +using System.Collections.Generic; +using System.Text; +using System.Diagnostics; + +using ModernKeePassLib.Interfaces; + +#if KeePassLibSD +using KeePassLibSD; +#endif + +namespace ModernKeePassLib.Collections +{ + public sealed class StringDictionaryEx : IDeepCloneable, + IEnumerable>, IEquatable + { + private SortedDictionary m_dict = + new SortedDictionary(); + + public int Count + { + get { return m_dict.Count; } + } + + public StringDictionaryEx() + { + } + + IEnumerator IEnumerable.GetEnumerator() + { + return m_dict.GetEnumerator(); + } + + public IEnumerator> GetEnumerator() + { + return m_dict.GetEnumerator(); + } + + public StringDictionaryEx CloneDeep() + { + StringDictionaryEx sdNew = new StringDictionaryEx(); + + foreach(KeyValuePair kvp in m_dict) + sdNew.m_dict[kvp.Key] = kvp.Value; // Strings are immutable + + return sdNew; + } + + public bool Equals(StringDictionaryEx sdOther) + { + if(sdOther == null) { Debug.Assert(false); return false; } + + if(m_dict.Count != sdOther.m_dict.Count) return false; + + foreach(KeyValuePair kvp in sdOther.m_dict) + { + string str = Get(kvp.Key); + if((str == null) || (str != kvp.Value)) return false; + } + + return true; + } + + public string Get(string strName) + { + if(strName == null) { Debug.Assert(false); throw new ArgumentNullException("strName"); } + + string s; + if(m_dict.TryGetValue(strName, out s)) return s; + return null; + } + + public bool Exists(string strName) + { + if(strName == null) { Debug.Assert(false); throw new ArgumentNullException("strName"); } + + return m_dict.ContainsKey(strName); + } + + /// + /// Set a string. + /// + /// Identifier of the string field to modify. + /// New value. This parameter must not be null. + /// Thrown if one of the input + /// parameters is null. + public void Set(string strField, string strNewValue) + { + if(strField == null) { Debug.Assert(false); throw new ArgumentNullException("strField"); } + if(strNewValue == null) { Debug.Assert(false); throw new ArgumentNullException("strNewValue"); } + + m_dict[strField] = strNewValue; + } + + /// + /// Delete a string. + /// + /// Name of the string field to delete. + /// Returns true, if the field has been successfully + /// removed. Otherwise, the return value is false. + /// Thrown if the input + /// parameter is null. + public bool Remove(string strField) + { + if(strField == null) { Debug.Assert(false); throw new ArgumentNullException("strField"); } + + return m_dict.Remove(strField); + } + } +} diff --git a/ModernKeePassLib/Collections/VariantDictionary.cs b/ModernKeePassLib/Collections/VariantDictionary.cs new file mode 100644 index 0000000..c3b7dc9 --- /dev/null +++ b/ModernKeePassLib/Collections/VariantDictionary.cs @@ -0,0 +1,415 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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 ModernKeePassLib.Resources; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Collections +{ + public class VariantDictionary + { + private const ushort VdVersion = 0x0100; + private const ushort VdmCritical = 0xFF00; + private const ushort VdmInfo = 0x00FF; + + private Dictionary m_d = new Dictionary(); + + private enum VdType : byte + { + None = 0, + + // Byte = 0x02, + // UInt16 = 0x03, + UInt32 = 0x04, + UInt64 = 0x05, + + // Signed mask: 0x08 + Bool = 0x08, + // SByte = 0x0A, + // Int16 = 0x0B, + Int32 = 0x0C, + Int64 = 0x0D, + + // Float = 0x10, + // Double = 0x11, + // Decimal = 0x12, + + // Char = 0x17, // 16-bit Unicode character + String = 0x18, + + // Array mask: 0x40 + ByteArray = 0x42 + } + + public int Count + { + get { return m_d.Count; } + } + + public VariantDictionary() + { + Debug.Assert((VdmCritical & VdmInfo) == ushort.MinValue); + Debug.Assert((VdmCritical | VdmInfo) == ushort.MaxValue); + } + + private bool Get(string strName, out T t) + { + t = default(T); + + if(string.IsNullOrEmpty(strName)) { Debug.Assert(false); return false; } + + object o; + if(!m_d.TryGetValue(strName, out o)) return false; // No assert + + if(o == null) { Debug.Assert(false); return false; } + if(o.GetType() != typeof(T)) { Debug.Assert(false); return false; } + + t = (T)o; + return true; + } + + private void SetStruct(string strName, T t) + where T : struct + { + if(string.IsNullOrEmpty(strName)) { Debug.Assert(false); return; } + +#if DEBUG + T tEx; + Get(strName, out tEx); // Assert same type +#endif + + m_d[strName] = t; + } + + private void SetRef(string strName, T t) + where T : class + { + if(string.IsNullOrEmpty(strName)) { Debug.Assert(false); return; } + if(t == null) { Debug.Assert(false); return; } + +#if DEBUG + T tEx; + Get(strName, out tEx); // Assert same type +#endif + + m_d[strName] = t; + } + + public bool Remove(string strName) + { + if(string.IsNullOrEmpty(strName)) { Debug.Assert(false); return false; } + + return m_d.Remove(strName); + } + + public void CopyTo(VariantDictionary d) + { + if(d == null) { Debug.Assert(false); return; } + + // Do not clear the target + foreach(KeyValuePair kvp in m_d) + { + d.m_d[kvp.Key] = kvp.Value; + } + } + + public Type GetTypeOf(string strName) + { + if(string.IsNullOrEmpty(strName)) { Debug.Assert(false); return null; } + + object o; + m_d.TryGetValue(strName, out o); + if(o == null) return null; // No assert + + return o.GetType(); + } + + public uint GetUInt32(string strName, uint uDefault) + { + uint u; + if(Get(strName, out u)) return u; + return uDefault; + } + + public void SetUInt32(string strName, uint uValue) + { + SetStruct(strName, uValue); + } + + public ulong GetUInt64(string strName, ulong uDefault) + { + ulong u; + if(Get(strName, out u)) return u; + return uDefault; + } + + public void SetUInt64(string strName, ulong uValue) + { + SetStruct(strName, uValue); + } + + public bool GetBool(string strName, bool bDefault) + { + bool b; + if(Get(strName, out b)) return b; + return bDefault; + } + + public void SetBool(string strName, bool bValue) + { + SetStruct(strName, bValue); + } + + public int GetInt32(string strName, int iDefault) + { + int i; + if(Get(strName, out i)) return i; + return iDefault; + } + + public void SetInt32(string strName, int iValue) + { + SetStruct(strName, iValue); + } + + public long GetInt64(string strName, long lDefault) + { + long l; + if(Get(strName, out l)) return l; + return lDefault; + } + + public void SetInt64(string strName, long lValue) + { + SetStruct(strName, lValue); + } + + public string GetString(string strName) + { + string str; + Get(strName, out str); + return str; + } + + public void SetString(string strName, string strValue) + { + SetRef(strName, strValue); + } + + public byte[] GetByteArray(string strName) + { + byte[] pb; + Get(strName, out pb); + return pb; + } + + public void SetByteArray(string strName, byte[] pbValue) + { + SetRef(strName, pbValue); + } + + /// + /// Create a deep copy. + /// + public virtual object Clone() + { + VariantDictionary vdNew = new VariantDictionary(); + + foreach(KeyValuePair kvp in m_d) + { + object o = kvp.Value; + if(o == null) { Debug.Assert(false); continue; } + + Type t = o.GetType(); + if(t == typeof(byte[])) + { + byte[] p = (byte[])o; + byte[] pNew = new byte[p.Length]; + if(p.Length > 0) Array.Copy(p, pNew, p.Length); + + o = pNew; + } + + vdNew.m_d[kvp.Key] = o; + } + + return vdNew; + } + + public static byte[] Serialize(VariantDictionary p) + { + if(p == null) { Debug.Assert(false); return null; } + + byte[] pbRet; + using(MemoryStream ms = new MemoryStream()) + { + MemUtil.Write(ms, MemUtil.UInt16ToBytes(VdVersion)); + + foreach(KeyValuePair kvp in p.m_d) + { + string strName = kvp.Key; + if(string.IsNullOrEmpty(strName)) { Debug.Assert(false); continue; } + byte[] pbName = StrUtil.Utf8.GetBytes(strName); + + object o = kvp.Value; + if(o == null) { Debug.Assert(false); continue; } + + Type t = o.GetType(); + VdType vt = VdType.None; + byte[] pbValue = null; + if(t == typeof(uint)) + { + vt = VdType.UInt32; + pbValue = MemUtil.UInt32ToBytes((uint)o); + } + else if(t == typeof(ulong)) + { + vt = VdType.UInt64; + pbValue = MemUtil.UInt64ToBytes((ulong)o); + } + else if(t == typeof(bool)) + { + vt = VdType.Bool; + pbValue = new byte[1]; + pbValue[0] = ((bool)o ? (byte)1 : (byte)0); + } + else if(t == typeof(int)) + { + vt = VdType.Int32; + pbValue = MemUtil.Int32ToBytes((int)o); + } + else if(t == typeof(long)) + { + vt = VdType.Int64; + pbValue = MemUtil.Int64ToBytes((long)o); + } + else if(t == typeof(string)) + { + vt = VdType.String; + pbValue = StrUtil.Utf8.GetBytes((string)o); + } + else if(t == typeof(byte[])) + { + vt = VdType.ByteArray; + pbValue = (byte[])o; + } + else { Debug.Assert(false); continue; } // Unknown type + + ms.WriteByte((byte)vt); + MemUtil.Write(ms, MemUtil.Int32ToBytes(pbName.Length)); + MemUtil.Write(ms, pbName); + MemUtil.Write(ms, MemUtil.Int32ToBytes(pbValue.Length)); + MemUtil.Write(ms, pbValue); + } + + ms.WriteByte((byte)VdType.None); + pbRet = ms.ToArray(); + } + + return pbRet; + } + + public static VariantDictionary Deserialize(byte[] pb) + { + if(pb == null) { Debug.Assert(false); return null; } + + VariantDictionary d = new VariantDictionary(); + using(MemoryStream ms = new MemoryStream(pb, false)) + { + ushort uVersion = MemUtil.BytesToUInt16(MemUtil.Read(ms, 2)); + if((uVersion & VdmCritical) > (VdVersion & VdmCritical)) + throw new FormatException(KLRes.FileNewVerReq); + + while(true) + { + int iType = ms.ReadByte(); + if(iType < 0) throw new EndOfStreamException(KLRes.FileCorrupted); + byte btType = (byte)iType; + if(btType == (byte)VdType.None) break; + + int cbName = MemUtil.BytesToInt32(MemUtil.Read(ms, 4)); + byte[] pbName = MemUtil.Read(ms, cbName); + if(pbName.Length != cbName) + throw new EndOfStreamException(KLRes.FileCorrupted); + string strName = StrUtil.Utf8.GetString(pbName, 0, pbName.Length); + + int cbValue = MemUtil.BytesToInt32(MemUtil.Read(ms, 4)); + byte[] pbValue = MemUtil.Read(ms, cbValue); + if(pbValue.Length != cbValue) + throw new EndOfStreamException(KLRes.FileCorrupted); + + switch(btType) + { + case (byte)VdType.UInt32: + if(cbValue == 4) + d.SetUInt32(strName, MemUtil.BytesToUInt32(pbValue)); + else { Debug.Assert(false); } + break; + + case (byte)VdType.UInt64: + if(cbValue == 8) + d.SetUInt64(strName, MemUtil.BytesToUInt64(pbValue)); + else { Debug.Assert(false); } + break; + + case (byte)VdType.Bool: + if(cbValue == 1) + d.SetBool(strName, (pbValue[0] != 0)); + else { Debug.Assert(false); } + break; + + case (byte)VdType.Int32: + if(cbValue == 4) + d.SetInt32(strName, MemUtil.BytesToInt32(pbValue)); + else { Debug.Assert(false); } + break; + + case (byte)VdType.Int64: + if(cbValue == 8) + d.SetInt64(strName, MemUtil.BytesToInt64(pbValue)); + else { Debug.Assert(false); } + break; + + case (byte)VdType.String: + d.SetString(strName, StrUtil.Utf8.GetString(pbValue, 0, pbValue.Length)); + break; + + case (byte)VdType.ByteArray: + d.SetByteArray(strName, pbValue); + break; + + default: + Debug.Assert(false); // Unknown type + break; + } + } + + Debug.Assert(ms.ReadByte() < 0); + } + + return d; + } + } +} diff --git a/ModernKeePassLib/Cryptography/Cipher/ChaCha20Cipher.cs b/ModernKeePassLib/Cryptography/Cipher/ChaCha20Cipher.cs new file mode 100644 index 0000000..ce6bc20 --- /dev/null +++ b/ModernKeePassLib/Cryptography/Cipher/ChaCha20Cipher.cs @@ -0,0 +1,254 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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 ModernKeePassLib.Resources; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Cryptography.Cipher +{ + /// + /// Implementation of the ChaCha20 cipher with a 96-bit nonce, + /// as specified in RFC 7539. + /// https://tools.ietf.org/html/rfc7539 + /// + public sealed class ChaCha20Cipher : CtrBlockCipher + { + private uint[] m_s = new uint[16]; // State + private uint[] m_x = new uint[16]; // Working buffer + + private bool m_bLargeCounter; // See constructor documentation + + private static readonly uint[] g_sigma = new uint[4] { + 0x61707865, 0x3320646E, 0x79622D32, 0x6B206574 + }; + + private const string StrNameRfc = "ChaCha20 (RFC 7539)"; + + public override int BlockSize + { + get { return 64; } + } + + public ChaCha20Cipher(byte[] pbKey32, byte[] pbIV12) : + this(pbKey32, pbIV12, false) + { + } + + /// + /// Constructor. + /// + /// Key (32 bytes). + /// Nonce (12 bytes). + /// If false, the RFC 7539 version + /// of ChaCha20 is used. In this case, only 256 GB of data can be + /// encrypted securely (because the block counter is a 32-bit variable); + /// an attempt to encrypt more data throws an exception. + /// If is true, the 32-bit + /// counter overflows to another 32-bit variable (i.e. the counter + /// effectively is a 64-bit variable), like in the original ChaCha20 + /// specification by D. J. Bernstein (which has a 64-bit counter and a + /// 64-bit nonce). To be compatible with this version, the 64-bit nonce + /// must be stored in the last 8 bytes of + /// and the first 4 bytes must be 0. + /// If the IV was generated randomly, a 12-byte IV and a large counter + /// can be used to securely encrypt more than 256 GB of data (but note + /// this is incompatible with RFC 7539 and the original specification). + public ChaCha20Cipher(byte[] pbKey32, byte[] pbIV12, bool bLargeCounter) : + base() + { + if(pbKey32 == null) throw new ArgumentNullException("pbKey32"); + if(pbKey32.Length != 32) throw new ArgumentOutOfRangeException("pbKey32"); + if(pbIV12 == null) throw new ArgumentNullException("pbIV12"); + if(pbIV12.Length != 12) throw new ArgumentOutOfRangeException("pbIV12"); + + m_bLargeCounter = bLargeCounter; + + // Key setup + m_s[4] = MemUtil.BytesToUInt32(pbKey32, 0); + m_s[5] = MemUtil.BytesToUInt32(pbKey32, 4); + m_s[6] = MemUtil.BytesToUInt32(pbKey32, 8); + m_s[7] = MemUtil.BytesToUInt32(pbKey32, 12); + m_s[8] = MemUtil.BytesToUInt32(pbKey32, 16); + m_s[9] = MemUtil.BytesToUInt32(pbKey32, 20); + m_s[10] = MemUtil.BytesToUInt32(pbKey32, 24); + m_s[11] = MemUtil.BytesToUInt32(pbKey32, 28); + m_s[0] = g_sigma[0]; + m_s[1] = g_sigma[1]; + m_s[2] = g_sigma[2]; + m_s[3] = g_sigma[3]; + + // IV setup + m_s[12] = 0; // Counter + m_s[13] = MemUtil.BytesToUInt32(pbIV12, 0); + m_s[14] = MemUtil.BytesToUInt32(pbIV12, 4); + m_s[15] = MemUtil.BytesToUInt32(pbIV12, 8); + } + + protected override void Dispose(bool bDisposing) + { + if(bDisposing) + { + MemUtil.ZeroArray(m_s); + MemUtil.ZeroArray(m_x); + } + + base.Dispose(bDisposing); + } + + protected override void NextBlock(byte[] pBlock) + { + if(pBlock == null) throw new ArgumentNullException("pBlock"); + if(pBlock.Length != 64) throw new ArgumentOutOfRangeException("pBlock"); + + // x is a local alias for the working buffer; with this, + // the compiler/runtime might remove some checks + uint[] x = m_x; + if(x == null) throw new InvalidOperationException(); + if(x.Length < 16) throw new InvalidOperationException(); + + uint[] s = m_s; + if(s == null) throw new InvalidOperationException(); + if(s.Length < 16) throw new InvalidOperationException(); + + Array.Copy(s, x, 16); + + unchecked + { + // 10 * 8 quarter rounds = 20 rounds + for(int i = 0; i < 10; ++i) + { + // Column quarter rounds + x[ 0] += x[ 4]; + x[12] = MemUtil.RotateLeft32(x[12] ^ x[ 0], 16); + x[ 8] += x[12]; + x[ 4] = MemUtil.RotateLeft32(x[ 4] ^ x[ 8], 12); + x[ 0] += x[ 4]; + x[12] = MemUtil.RotateLeft32(x[12] ^ x[ 0], 8); + x[ 8] += x[12]; + x[ 4] = MemUtil.RotateLeft32(x[ 4] ^ x[ 8], 7); + + x[ 1] += x[ 5]; + x[13] = MemUtil.RotateLeft32(x[13] ^ x[ 1], 16); + x[ 9] += x[13]; + x[ 5] = MemUtil.RotateLeft32(x[ 5] ^ x[ 9], 12); + x[ 1] += x[ 5]; + x[13] = MemUtil.RotateLeft32(x[13] ^ x[ 1], 8); + x[ 9] += x[13]; + x[ 5] = MemUtil.RotateLeft32(x[ 5] ^ x[ 9], 7); + + x[ 2] += x[ 6]; + x[14] = MemUtil.RotateLeft32(x[14] ^ x[ 2], 16); + x[10] += x[14]; + x[ 6] = MemUtil.RotateLeft32(x[ 6] ^ x[10], 12); + x[ 2] += x[ 6]; + x[14] = MemUtil.RotateLeft32(x[14] ^ x[ 2], 8); + x[10] += x[14]; + x[ 6] = MemUtil.RotateLeft32(x[ 6] ^ x[10], 7); + + x[ 3] += x[ 7]; + x[15] = MemUtil.RotateLeft32(x[15] ^ x[ 3], 16); + x[11] += x[15]; + x[ 7] = MemUtil.RotateLeft32(x[ 7] ^ x[11], 12); + x[ 3] += x[ 7]; + x[15] = MemUtil.RotateLeft32(x[15] ^ x[ 3], 8); + x[11] += x[15]; + x[ 7] = MemUtil.RotateLeft32(x[ 7] ^ x[11], 7); + + // Diagonal quarter rounds + x[ 0] += x[ 5]; + x[15] = MemUtil.RotateLeft32(x[15] ^ x[ 0], 16); + x[10] += x[15]; + x[ 5] = MemUtil.RotateLeft32(x[ 5] ^ x[10], 12); + x[ 0] += x[ 5]; + x[15] = MemUtil.RotateLeft32(x[15] ^ x[ 0], 8); + x[10] += x[15]; + x[ 5] = MemUtil.RotateLeft32(x[ 5] ^ x[10], 7); + + x[ 1] += x[ 6]; + x[12] = MemUtil.RotateLeft32(x[12] ^ x[ 1], 16); + x[11] += x[12]; + x[ 6] = MemUtil.RotateLeft32(x[ 6] ^ x[11], 12); + x[ 1] += x[ 6]; + x[12] = MemUtil.RotateLeft32(x[12] ^ x[ 1], 8); + x[11] += x[12]; + x[ 6] = MemUtil.RotateLeft32(x[ 6] ^ x[11], 7); + + x[ 2] += x[ 7]; + x[13] = MemUtil.RotateLeft32(x[13] ^ x[ 2], 16); + x[ 8] += x[13]; + x[ 7] = MemUtil.RotateLeft32(x[ 7] ^ x[ 8], 12); + x[ 2] += x[ 7]; + x[13] = MemUtil.RotateLeft32(x[13] ^ x[ 2], 8); + x[ 8] += x[13]; + x[ 7] = MemUtil.RotateLeft32(x[ 7] ^ x[ 8], 7); + + x[ 3] += x[ 4]; + x[14] = MemUtil.RotateLeft32(x[14] ^ x[ 3], 16); + x[ 9] += x[14]; + x[ 4] = MemUtil.RotateLeft32(x[ 4] ^ x[ 9], 12); + x[ 3] += x[ 4]; + x[14] = MemUtil.RotateLeft32(x[14] ^ x[ 3], 8); + x[ 9] += x[14]; + x[ 4] = MemUtil.RotateLeft32(x[ 4] ^ x[ 9], 7); + } + + for(int i = 0; i < 16; ++i) x[i] += s[i]; + + for(int i = 0; i < 16; ++i) + { + int i4 = i << 2; + uint xi = x[i]; + + pBlock[i4] = (byte)xi; + pBlock[i4 + 1] = (byte)(xi >> 8); + pBlock[i4 + 2] = (byte)(xi >> 16); + pBlock[i4 + 3] = (byte)(xi >> 24); + } + + ++s[12]; + if(s[12] == 0) + { + if(!m_bLargeCounter) + throw new InvalidOperationException( + KLRes.EncDataTooLarge.Replace(@"{PARAM}", StrNameRfc)); + ++s[13]; // Increment high half of large counter + } + } + } + + public long Seek(long lOffset, SeekOrigin so) + { + if(so != SeekOrigin.Begin) throw new NotSupportedException(); + + if((lOffset < 0) || ((lOffset & 63) != 0) || + ((lOffset >> 6) > (long)uint.MaxValue)) + throw new ArgumentOutOfRangeException("lOffset"); + + m_s[12] = (uint)(lOffset >> 6); + InvalidateBlock(); + + return lOffset; + } + } +} diff --git a/ModernKeePassLib/Cryptography/Cipher/ChaCha20Engine.cs b/ModernKeePassLib/Cryptography/Cipher/ChaCha20Engine.cs new file mode 100644 index 0000000..369911d --- /dev/null +++ b/ModernKeePassLib/Cryptography/Cipher/ChaCha20Engine.cs @@ -0,0 +1,176 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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 ModernKeePassLib.Resources; + +namespace ModernKeePassLib.Cryptography.Cipher +{ + public sealed class ChaCha20Engine : ICipherEngine2 + { + private PwUuid m_uuid = new PwUuid(new byte[] { + 0xD6, 0x03, 0x8A, 0x2B, 0x8B, 0x6F, 0x4C, 0xB5, + 0xA5, 0x24, 0x33, 0x9A, 0x31, 0xDB, 0xB5, 0x9A + }); + public PwUuid CipherUuid + { + get { return m_uuid; } + } + + public string DisplayName + { + get + { + return ("ChaCha20 (" + KLRes.KeyBits.Replace(@"{PARAM}", + "256") + ", RFC 7539)"); + } + } + + public int KeyLength + { + get { return 32; } + } + + public int IVLength + { + get { return 12; } // 96 bits + } + + public Stream EncryptStream(Stream s, byte[] pbKey, byte[] pbIV) + { + return new ChaCha20Stream(s, true, pbKey, pbIV); + } + + public Stream DecryptStream(Stream s, byte[] pbKey, byte[] pbIV) + { + return new ChaCha20Stream(s, false, pbKey, pbIV); + } + } + + public sealed class ChaCha20Stream : Stream + { + private Stream m_sBase; + private readonly bool m_bWriting; + private ChaCha20Cipher m_c; + + private byte[] m_pbBuffer = null; + + public override bool CanRead + { + get { return !m_bWriting; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return m_bWriting; } + } + + public override long Length + { + get { Debug.Assert(false); throw new NotSupportedException(); } + } + + public override long Position + { + get { Debug.Assert(false); throw new NotSupportedException(); } + set { Debug.Assert(false); throw new NotSupportedException(); } + } + + public ChaCha20Stream(Stream sBase, bool bWriting, byte[] pbKey32, + byte[] pbIV12) + { + if(sBase == null) throw new ArgumentNullException("sBase"); + + m_sBase = sBase; + m_bWriting = bWriting; + m_c = new ChaCha20Cipher(pbKey32, pbIV12); + } + + protected override void Dispose(bool bDisposing) + { + if(bDisposing) + { + if(m_sBase != null) + { + m_c.Dispose(); + m_c = null; + + m_sBase.Dispose(); + m_sBase = null; + } + + m_pbBuffer = null; + } + + base.Dispose(bDisposing); + } + + public override void Flush() + { + Debug.Assert(m_sBase != null); + if(m_bWriting && (m_sBase != null)) m_sBase.Flush(); + } + + public override long Seek(long lOffset, SeekOrigin soOrigin) + { + Debug.Assert(false); + throw new NotImplementedException(); + } + + public override void SetLength(long lValue) + { + Debug.Assert(false); + throw new NotImplementedException(); + } + + public override int Read(byte[] pbBuffer, int iOffset, int nCount) + { + if(m_bWriting) throw new InvalidOperationException(); + + int cbRead = m_sBase.Read(pbBuffer, iOffset, nCount); + m_c.Decrypt(pbBuffer, iOffset, cbRead); + return cbRead; + } + + public override void Write(byte[] pbBuffer, int iOffset, int nCount) + { + if(nCount < 0) throw new ArgumentOutOfRangeException("nCount"); + if(nCount == 0) return; + + if(!m_bWriting) throw new InvalidOperationException(); + + if((m_pbBuffer == null) || (m_pbBuffer.Length < nCount)) + m_pbBuffer = new byte[nCount]; + Array.Copy(pbBuffer, iOffset, m_pbBuffer, 0, nCount); + + m_c.Encrypt(m_pbBuffer, 0, nCount); + m_sBase.Write(m_pbBuffer, 0, nCount); + } + } +} diff --git a/ModernKeePassLib/Cryptography/Cipher/CipherPool.cs b/ModernKeePassLib/Cryptography/Cipher/CipherPool.cs new file mode 100644 index 0000000..c38aabd --- /dev/null +++ b/ModernKeePassLib/Cryptography/Cipher/CipherPool.cs @@ -0,0 +1,165 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; + +namespace ModernKeePassLib.Cryptography.Cipher +{ + /// + /// Pool of encryption/decryption algorithms (ciphers). + /// + public sealed class CipherPool + { + private List m_lCiphers = new List(); + + private static CipherPool m_poolGlobal = null; + public static CipherPool GlobalPool + { + get + { + CipherPool cp = m_poolGlobal; + if(cp == null) + { + cp = new CipherPool(); + cp.AddCipher(new StandardAesEngine()); + cp.AddCipher(new ChaCha20Engine()); + + m_poolGlobal = cp; + } + + return cp; + } + } + + /// + /// Remove all cipher engines from the current pool. + /// + public void Clear() + { + m_lCiphers.Clear(); + } + + /// + /// Add a cipher engine to the pool. + /// + /// Cipher engine to add. Must not be null. + public void AddCipher(ICipherEngine c) + { + if(c == null) { Debug.Assert(false); throw new ArgumentNullException("c"); } + + // Return if a cipher with that ID is registered already + foreach(ICipherEngine cEx in m_lCiphers) + { + if(cEx.CipherUuid.Equals(c.CipherUuid)) + return; + } + + m_lCiphers.Add(c); + } + + /// + /// Get a cipher identified by its UUID. + /// + /// UUID of the cipher to return. + /// Reference to the requested cipher. If the cipher is + /// not found, null is returned. + public ICipherEngine GetCipher(PwUuid uuidCipher) + { + foreach(ICipherEngine c in m_lCiphers) + { + if(c.CipherUuid.Equals(uuidCipher)) + return c; + } + + return null; + } + + /// + /// Get the index of a cipher. This index is temporary and should + /// not be stored or used to identify a cipher. + /// + /// UUID of the cipher. + /// Index of the requested cipher. Returns -1 if + /// the specified cipher is not found. + public int GetCipherIndex(PwUuid uuidCipher) + { + for(int i = 0; i < m_lCiphers.Count; ++i) + { + if(m_lCiphers[i].CipherUuid.Equals(uuidCipher)) + return i; + } + + Debug.Assert(false); + return -1; + } + + /// + /// Get the index of a cipher. This index is temporary and should + /// not be stored or used to identify a cipher. + /// + /// Name of the cipher. Note that + /// multiple ciphers can have the same name. In this case, the + /// first matching cipher is returned. + /// Cipher with the specified name or -1 if + /// no cipher with that name is found. + public int GetCipherIndex(string strDisplayName) + { + for(int i = 0; i < m_lCiphers.Count; ++i) + { + if(m_lCiphers[i].DisplayName == strDisplayName) + return i; + } + + Debug.Assert(false); + return -1; + } + + /// + /// Get the number of cipher engines in this pool. + /// + public int EngineCount + { + get { return m_lCiphers.Count; } + } + + /// + /// Get the cipher engine at the specified position. Throws + /// an exception if the index is invalid. You can use this + /// to iterate over all ciphers, but do not use it to + /// identify ciphers. + /// + /// Index of the requested cipher engine. + /// Reference to the cipher engine at the specified + /// position. + public ICipherEngine this[int nIndex] + { + get + { + if((nIndex < 0) || (nIndex >= m_lCiphers.Count)) + throw new ArgumentOutOfRangeException("nIndex"); + + return m_lCiphers[nIndex]; + } + } + } +} diff --git a/ModernKeePassLib/Cryptography/Cipher/CtrBlockCipher.cs b/ModernKeePassLib/Cryptography/Cipher/CtrBlockCipher.cs new file mode 100644 index 0000000..3fb83a4 --- /dev/null +++ b/ModernKeePassLib/Cryptography/Cipher/CtrBlockCipher.cs @@ -0,0 +1,109 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Cryptography.Cipher +{ + public abstract class CtrBlockCipher : IDisposable + { + private bool m_bDisposed = false; + + private byte[] m_pBlock; + private int m_iBlockPos; + + public abstract int BlockSize + { + get; + } + + public CtrBlockCipher() + { + int cb = this.BlockSize; + if(cb <= 0) throw new InvalidOperationException("this.BlockSize"); + + m_pBlock = new byte[cb]; + m_iBlockPos = cb; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool bDisposing) + { + if(bDisposing) + { + MemUtil.ZeroByteArray(m_pBlock); + m_iBlockPos = m_pBlock.Length; + + m_bDisposed = true; + } + } + + protected void InvalidateBlock() + { + m_iBlockPos = m_pBlock.Length; + } + + protected abstract void NextBlock(byte[] pBlock); + + public void Encrypt(byte[] m, int iOffset, int cb) + { + if(m_bDisposed) throw new ObjectDisposedException(null); + if(m == null) throw new ArgumentNullException("m"); + if(iOffset < 0) throw new ArgumentOutOfRangeException("iOffset"); + if(cb < 0) throw new ArgumentOutOfRangeException("cb"); + if(iOffset > (m.Length - cb)) throw new ArgumentOutOfRangeException("cb"); + + int cbBlock = m_pBlock.Length; + + while(cb > 0) + { + Debug.Assert(m_iBlockPos <= cbBlock); + if(m_iBlockPos == cbBlock) + { + NextBlock(m_pBlock); + m_iBlockPos = 0; + } + + int cbCopy = Math.Min(cbBlock - m_iBlockPos, cb); + Debug.Assert(cbCopy > 0); + + MemUtil.XorArray(m_pBlock, m_iBlockPos, m, iOffset, cbCopy); + + m_iBlockPos += cbCopy; + iOffset += cbCopy; + cb -= cbCopy; + } + } + + public void Decrypt(byte[] m, int iOffset, int cb) + { + Encrypt(m, iOffset, cb); + } + } +} diff --git a/ModernKeePassLib/Cryptography/Cipher/ICipherEngine.cs b/ModernKeePassLib/Cryptography/Cipher/ICipherEngine.cs new file mode 100644 index 0000000..5dc2c93 --- /dev/null +++ b/ModernKeePassLib/Cryptography/Cipher/ICipherEngine.cs @@ -0,0 +1,69 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.IO; + +namespace ModernKeePassLib.Cryptography.Cipher +{ + public interface ICipherEngine + { + /// + /// UUID of the engine. If you want to write an engine/plugin, + /// please contact the KeePass team to obtain a new UUID. + /// + PwUuid CipherUuid + { + get; + } + + /// + /// Name displayed in the list of available encryption/decryption + /// engines in the GUI. + /// + string DisplayName + { + get; + } + + Stream EncryptStream(Stream s, byte[] pbKey, byte[] pbIV); + Stream DecryptStream(Stream s, byte[] pbKey, byte[] pbIV); + } + + public interface ICipherEngine2 : ICipherEngine + { + /// + /// Length of an encryption key in bytes. + /// The base ICipherEngine assumes 32. + /// + int KeyLength + { + get; + } + + /// + /// Length of the initialization vector in bytes. + /// The base ICipherEngine assumes 16. + /// + int IVLength + { + get; + } + } +} diff --git a/ModernKeePassLib/Cryptography/Cipher/Salsa20Cipher.cs b/ModernKeePassLib/Cryptography/Cipher/Salsa20Cipher.cs new file mode 100644 index 0000000..2052a72 --- /dev/null +++ b/ModernKeePassLib/Cryptography/Cipher/Salsa20Cipher.cs @@ -0,0 +1,165 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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 +*/ + +// Implementation of the Salsa20 cipher, based on the eSTREAM +// submission by D. J. Bernstein. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Cryptography.Cipher +{ + public sealed class Salsa20Cipher : CtrBlockCipher + { + private uint[] m_s = new uint[16]; // State + private uint[] m_x = new uint[16]; // Working buffer + + private static readonly uint[] g_sigma = new uint[4] { + 0x61707865, 0x3320646E, 0x79622D32, 0x6B206574 + }; + + public override int BlockSize + { + get { return 64; } + } + + public Salsa20Cipher(byte[] pbKey32, byte[] pbIV8) : base() + { + if(pbKey32 == null) throw new ArgumentNullException("pbKey32"); + if(pbKey32.Length != 32) throw new ArgumentOutOfRangeException("pbKey32"); + if(pbIV8 == null) throw new ArgumentNullException("pbIV8"); + if(pbIV8.Length != 8) throw new ArgumentOutOfRangeException("pbIV8"); + + // Key setup + m_s[1] = MemUtil.BytesToUInt32(pbKey32, 0); + m_s[2] = MemUtil.BytesToUInt32(pbKey32, 4); + m_s[3] = MemUtil.BytesToUInt32(pbKey32, 8); + m_s[4] = MemUtil.BytesToUInt32(pbKey32, 12); + m_s[11] = MemUtil.BytesToUInt32(pbKey32, 16); + m_s[12] = MemUtil.BytesToUInt32(pbKey32, 20); + m_s[13] = MemUtil.BytesToUInt32(pbKey32, 24); + m_s[14] = MemUtil.BytesToUInt32(pbKey32, 28); + m_s[0] = g_sigma[0]; + m_s[5] = g_sigma[1]; + m_s[10] = g_sigma[2]; + m_s[15] = g_sigma[3]; + + // IV setup + m_s[6] = MemUtil.BytesToUInt32(pbIV8, 0); + m_s[7] = MemUtil.BytesToUInt32(pbIV8, 4); + m_s[8] = 0; // Counter, low + m_s[9] = 0; // Counter, high + } + + protected override void Dispose(bool bDisposing) + { + if(bDisposing) + { + MemUtil.ZeroArray(m_s); + MemUtil.ZeroArray(m_x); + } + + base.Dispose(bDisposing); + } + + protected override void NextBlock(byte[] pBlock) + { + if(pBlock == null) throw new ArgumentNullException("pBlock"); + if(pBlock.Length != 64) throw new ArgumentOutOfRangeException("pBlock"); + + // x is a local alias for the working buffer; with this, + // the compiler/runtime might remove some checks + uint[] x = m_x; + if(x == null) throw new InvalidOperationException(); + if(x.Length < 16) throw new InvalidOperationException(); + + uint[] s = m_s; + if(s == null) throw new InvalidOperationException(); + if(s.Length < 16) throw new InvalidOperationException(); + + Array.Copy(s, x, 16); + + unchecked + { + // 10 * 8 quarter rounds = 20 rounds + for(int i = 0; i < 10; ++i) + { + x[ 4] ^= MemUtil.RotateLeft32(x[ 0] + x[12], 7); + x[ 8] ^= MemUtil.RotateLeft32(x[ 4] + x[ 0], 9); + x[12] ^= MemUtil.RotateLeft32(x[ 8] + x[ 4], 13); + x[ 0] ^= MemUtil.RotateLeft32(x[12] + x[ 8], 18); + + x[ 9] ^= MemUtil.RotateLeft32(x[ 5] + x[ 1], 7); + x[13] ^= MemUtil.RotateLeft32(x[ 9] + x[ 5], 9); + x[ 1] ^= MemUtil.RotateLeft32(x[13] + x[ 9], 13); + x[ 5] ^= MemUtil.RotateLeft32(x[ 1] + x[13], 18); + + x[14] ^= MemUtil.RotateLeft32(x[10] + x[ 6], 7); + x[ 2] ^= MemUtil.RotateLeft32(x[14] + x[10], 9); + x[ 6] ^= MemUtil.RotateLeft32(x[ 2] + x[14], 13); + x[10] ^= MemUtil.RotateLeft32(x[ 6] + x[ 2], 18); + + x[ 3] ^= MemUtil.RotateLeft32(x[15] + x[11], 7); + x[ 7] ^= MemUtil.RotateLeft32(x[ 3] + x[15], 9); + x[11] ^= MemUtil.RotateLeft32(x[ 7] + x[ 3], 13); + x[15] ^= MemUtil.RotateLeft32(x[11] + x[ 7], 18); + + x[ 1] ^= MemUtil.RotateLeft32(x[ 0] + x[ 3], 7); + x[ 2] ^= MemUtil.RotateLeft32(x[ 1] + x[ 0], 9); + x[ 3] ^= MemUtil.RotateLeft32(x[ 2] + x[ 1], 13); + x[ 0] ^= MemUtil.RotateLeft32(x[ 3] + x[ 2], 18); + + x[ 6] ^= MemUtil.RotateLeft32(x[ 5] + x[ 4], 7); + x[ 7] ^= MemUtil.RotateLeft32(x[ 6] + x[ 5], 9); + x[ 4] ^= MemUtil.RotateLeft32(x[ 7] + x[ 6], 13); + x[ 5] ^= MemUtil.RotateLeft32(x[ 4] + x[ 7], 18); + + x[11] ^= MemUtil.RotateLeft32(x[10] + x[ 9], 7); + x[ 8] ^= MemUtil.RotateLeft32(x[11] + x[10], 9); + x[ 9] ^= MemUtil.RotateLeft32(x[ 8] + x[11], 13); + x[10] ^= MemUtil.RotateLeft32(x[ 9] + x[ 8], 18); + + x[12] ^= MemUtil.RotateLeft32(x[15] + x[14], 7); + x[13] ^= MemUtil.RotateLeft32(x[12] + x[15], 9); + x[14] ^= MemUtil.RotateLeft32(x[13] + x[12], 13); + x[15] ^= MemUtil.RotateLeft32(x[14] + x[13], 18); + } + + for(int i = 0; i < 16; ++i) x[i] += s[i]; + + for(int i = 0; i < 16; ++i) + { + int i4 = i << 2; + uint xi = x[i]; + + pBlock[i4] = (byte)xi; + pBlock[i4 + 1] = (byte)(xi >> 8); + pBlock[i4 + 2] = (byte)(xi >> 16); + pBlock[i4 + 3] = (byte)(xi >> 24); + } + + ++s[8]; + if(s[8] == 0) ++s[9]; + } + } + } +} diff --git a/ModernKeePassLib/Cryptography/Cipher/StandardAesEngine.cs b/ModernKeePassLib/Cryptography/Cipher/StandardAesEngine.cs new file mode 100644 index 0000000..80fcf32 --- /dev/null +++ b/ModernKeePassLib/Cryptography/Cipher/StandardAesEngine.cs @@ -0,0 +1,133 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Security; +using System.Text; + +#if !KeePassUAP +using System.Security.Cryptography; +#endif + +using ModernKeePassLib.Resources; + +namespace ModernKeePassLib.Cryptography.Cipher +{ + public sealed class StandardAesEngine : ICipherEngine + { +#if !KeePassUAP + private const CipherMode SaeCipherMode = CipherMode.CBC; + private const PaddingMode SaePaddingMode = PaddingMode.PKCS7; +#endif + + private static PwUuid g_uuidAes = null; + public static PwUuid AesUuid + { + get + { + PwUuid pu = g_uuidAes; + if(pu == null) + { + pu = new PwUuid(new byte[] { + 0x31, 0xC1, 0xF2, 0xE6, 0xBF, 0x71, 0x43, 0x50, + 0xBE, 0x58, 0x05, 0x21, 0x6A, 0xFC, 0x5A, 0xFF }); + g_uuidAes = pu; + } + + return pu; + } + } + + public PwUuid CipherUuid + { + get { return StandardAesEngine.AesUuid; } + } + + public string DisplayName + { + get + { + return ("AES/Rijndael (" + KLRes.KeyBits.Replace(@"{PARAM}", + "256") + ", FIPS 197)"); + } + } + + private static void ValidateArguments(Stream s, bool bEncrypt, byte[] pbKey, byte[] pbIV) + { + if(s == null) { Debug.Assert(false); throw new ArgumentNullException("s"); } + + if(pbKey == null) { Debug.Assert(false); throw new ArgumentNullException("pbKey"); } + if(pbKey.Length != 32) { Debug.Assert(false); throw new ArgumentOutOfRangeException("pbKey"); } + + if(pbIV == null) { Debug.Assert(false); throw new ArgumentNullException("pbIV"); } + if(pbIV.Length != 16) { Debug.Assert(false); throw new ArgumentOutOfRangeException("pbIV"); } + + if(bEncrypt) + { + Debug.Assert(s.CanWrite); + if(!s.CanWrite) throw new ArgumentException("Stream must be writable!"); + } + else // Decrypt + { + Debug.Assert(s.CanRead); + if(!s.CanRead) throw new ArgumentException("Stream must be readable!"); + } + } + + private static Stream CreateStream(Stream s, bool bEncrypt, byte[] pbKey, byte[] pbIV) + { + StandardAesEngine.ValidateArguments(s, bEncrypt, pbKey, pbIV); + +#if KeePassUAP + return StandardAesEngineExt.CreateStream(s, bEncrypt, pbKey, pbIV); +#else + SymmetricAlgorithm a = CryptoUtil.CreateAes(); + if(a.BlockSize != 128) // AES block size + { + Debug.Assert(false); + a.BlockSize = 128; + } + a.KeySize = 256; + a.Mode = SaeCipherMode; + a.Padding = SaePaddingMode; + + ICryptoTransform t; + if(bEncrypt) t = a.CreateEncryptor(pbKey, pbIV); + else t = a.CreateDecryptor(pbKey, pbIV); + if(t == null) { Debug.Assert(false); throw new SecurityException("Unable to create AES transform!"); } + + return new CryptoStreamEx(s, t, bEncrypt ? CryptoStreamMode.Write : + CryptoStreamMode.Read, a); +#endif + } + + public Stream EncryptStream(Stream s, byte[] pbKey, byte[] pbIV) + { + return StandardAesEngine.CreateStream(s, true, pbKey, pbIV); + } + + public Stream DecryptStream(Stream s, byte[] pbKey, byte[] pbIV) + { + return StandardAesEngine.CreateStream(s, false, pbKey, pbIV); + } + } +} diff --git a/ModernKeePassLib/Cryptography/CryptoRandom.cs b/ModernKeePassLib/Cryptography/CryptoRandom.cs new file mode 100644 index 0000000..534c626 --- /dev/null +++ b/ModernKeePassLib/Cryptography/CryptoRandom.cs @@ -0,0 +1,387 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; + +#if !KeePassUAP +using System.Drawing; +using System.Security.Cryptography; +#endif +#if !ModernKeePassLib +using System.Windows.Forms; +#endif + +using ModernKeePassLib.Delegates; +using ModernKeePassLib.Native; +using ModernKeePassLib.Security; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Cryptography +{ + /// + /// Cryptographically secure pseudo-random number generator. + /// The returned values are unpredictable and cannot be reproduced. + /// CryptoRandom is a singleton class. + /// + public sealed class CryptoRandom + { + private ProtectedBinary m_pbEntropyPool = new ProtectedBinary( + true, new byte[64]); + private RNGCryptoServiceProvider m_rng = new RNGCryptoServiceProvider(); + private ulong m_uCounter; + private ulong m_uGeneratedBytesCount = 0; + + private static readonly object g_oSyncRoot = new object(); + private readonly object m_oSyncRoot = new object(); + + private static CryptoRandom g_pInstance = null; + public static CryptoRandom Instance + { + get + { + CryptoRandom cr; + lock(g_oSyncRoot) + { + cr = g_pInstance; + if(cr == null) + { + cr = new CryptoRandom(); + g_pInstance = cr; + } + } + + return cr; + } + } + + /// + /// Get the number of random bytes that this instance generated so far. + /// Note that this number can be higher than the number of random bytes + /// actually requested using the GetRandomBytes method. + /// + public ulong GeneratedBytesCount + { + get + { + ulong u; + lock(m_oSyncRoot) { u = m_uGeneratedBytesCount; } + return u; + } + } + + /// + /// Event that is triggered whenever the internal GenerateRandom256 + /// method is called to generate random bytes. + /// + public event EventHandler GenerateRandom256Pre; + + private CryptoRandom() + { + m_uCounter = (ulong)DateTime.UtcNow.ToBinary(); + + byte[] pb = GetSystemEntropy(); + AddEntropy(pb); + MemUtil.ZeroByteArray(pb); + } + + /// + /// Update the internal seed of the random number generator based + /// on entropy data. + /// This method is thread-safe. + /// + /// Entropy bytes. + public void AddEntropy(byte[] pbEntropy) + { + if(pbEntropy == null) { Debug.Assert(false); return; } + if(pbEntropy.Length == 0) { Debug.Assert(false); return; } + + byte[] pbNewData = pbEntropy; + if(pbEntropy.Length > 64) + { +#if KeePassLibSD + using(SHA256Managed shaNew = new SHA256Managed()) +#else + using(SHA512Managed shaNew = new SHA512Managed()) +#endif + { + pbNewData = shaNew.ComputeHash(pbEntropy); + } + } + + lock(m_oSyncRoot) + { + byte[] pbPool = m_pbEntropyPool.ReadData(); + int cbPool = pbPool.Length; + int cbNew = pbNewData.Length; + + byte[] pbCmp = new byte[cbPool + cbNew]; + Array.Copy(pbPool, pbCmp, cbPool); + Array.Copy(pbNewData, 0, pbCmp, cbPool, cbNew); + +#if KeePassLibSD + using(SHA256Managed shaPool = new SHA256Managed()) +#else + using(SHA512Managed shaPool = new SHA512Managed()) +#endif + { + byte[] pbNewPool = shaPool.ComputeHash(pbCmp); + m_pbEntropyPool = new ProtectedBinary(true, pbNewPool); + MemUtil.ZeroByteArray(pbNewPool); + } + + MemUtil.ZeroByteArray(pbCmp); + MemUtil.ZeroByteArray(pbPool); + } + + if(pbNewData != pbEntropy) MemUtil.ZeroByteArray(pbNewData); + } + + private byte[] GetSystemEntropy() + { + SHA512Managed h = new SHA512Managed(); + byte[] pb4 = new byte[4]; + byte[] pb8 = new byte[8]; + + GAction f = delegate(byte[] pbValue, bool bClearValue) + { + if(pbValue == null) { Debug.Assert(false); return; } + if(pbValue.Length == 0) return; + h.TransformBlock(pbValue, 0, pbValue.Length, pbValue, 0); + if(bClearValue) MemUtil.ZeroByteArray(pbValue); + }; + Action fI32 = delegate(int iValue) + { + MemUtil.Int32ToBytesEx(iValue, pb4, 0); + f(pb4, false); + }; + Action fI64 = delegate(long lValue) + { + MemUtil.Int64ToBytesEx(lValue, pb8, 0); + f(pb8, false); + }; + Action fStr = delegate(string strValue) + { + if(strValue == null) { Debug.Assert(false); return; } + if(strValue.Length == 0) return; + f(StrUtil.Utf8.GetBytes(strValue), false); + }; + + fI32(Environment.TickCount); + fI64(DateTime.UtcNow.ToBinary()); + +#if !KeePassLibSD && !ModernKeePassLib + // In try-catch for systems without GUI; + // https://sourceforge.net/p/keepass/discussion/329221/thread/20335b73/ + try + { + Point pt = Cursor.Position; + fI32(pt.X); + fI32(pt.Y); + } + catch(Exception) { Debug.Assert(NativeLib.IsUnix()); } +#endif + + try + { + fI32((int)NativeLib.GetPlatformID()); +#if KeePassUAP + fStr(EnvironmentExt.OSVersion.VersionString); +#else + fStr(Environment.OSVersion.VersionString); +#endif + + fI32(Environment.ProcessorCount); + +#if !KeePassUAP + fStr(Environment.CommandLine); + fI64(Environment.WorkingSet); +#endif + } + catch(Exception) { Debug.Assert(false); } + + try + { + foreach(DictionaryEntry de in Environment.GetEnvironmentVariables()) + { + fStr(de.Key as string); + fStr(de.Value as string); + } + } + catch(Exception) { Debug.Assert(false); } + + try + { +#if KeePassUAP + f(DiagnosticsExt.GetProcessEntropy(), true); +#elif !KeePassLibSD + using(Process p = Process.GetCurrentProcess()) + { + fI64(p.Handle.ToInt64()); + fI32(p.HandleCount); + fI32(p.Id); + fI64(p.NonpagedSystemMemorySize64); + fI64(p.PagedMemorySize64); + fI64(p.PagedSystemMemorySize64); + fI64(p.PeakPagedMemorySize64); + fI64(p.PeakVirtualMemorySize64); + fI64(p.PeakWorkingSet64); + fI64(p.PrivateMemorySize64); + fI64(p.StartTime.ToBinary()); + fI64(p.VirtualMemorySize64); + fI64(p.WorkingSet64); + + // Not supported in Mono 1.2.6: + // fI32(p.SessionId); + } +#endif + } + catch(Exception) { Debug.Assert(NativeLib.IsUnix()); } + + try + { + CultureInfo ci = CultureInfo.CurrentCulture; + if(ci != null) fI32(ci.GetHashCode()); + else { Debug.Assert(false); } + } + catch(Exception) { Debug.Assert(false); } + + f(Guid.NewGuid().ToByteArray(), false); + f(GetCspRandom(), true); + + h.TransformFinalBlock(MemUtil.EmptyByteArray, 0, 0); + byte[] pbHash = h.Hash; + h.Clear(); + MemUtil.ZeroByteArray(pb4); + MemUtil.ZeroByteArray(pb8); + return pbHash; + } + + private byte[] GetCspRandom() + { + byte[] pb = new byte[32]; + + try { m_rng.GetBytes(pb); } + catch(Exception) + { + Debug.Assert(false); + MemUtil.Int64ToBytesEx(DateTime.UtcNow.ToBinary(), pb, 0); + } + + return pb; + } + + private byte[] GenerateRandom256() + { + if(this.GenerateRandom256Pre != null) + this.GenerateRandom256Pre(this, EventArgs.Empty); + + byte[] pbCmp; + lock(m_oSyncRoot) + { + m_uCounter += 0x74D8B29E4D38E161UL; // Prime number + byte[] pbCounter = MemUtil.UInt64ToBytes(m_uCounter); + + byte[] pbCsp = GetCspRandom(); + + byte[] pbPool = m_pbEntropyPool.ReadData(); + int cbPool = pbPool.Length; + int cbCtr = pbCounter.Length; + int cbCsp = pbCsp.Length; + + pbCmp = new byte[cbPool + cbCtr + cbCsp]; + Array.Copy(pbPool, pbCmp, cbPool); + Array.Copy(pbCounter, 0, pbCmp, cbPool, cbCtr); + Array.Copy(pbCsp, 0, pbCmp, cbPool + cbCtr, cbCsp); + + MemUtil.ZeroByteArray(pbCsp); + MemUtil.ZeroByteArray(pbPool); + + m_uGeneratedBytesCount += 32; + } + + byte[] pbRet = CryptoUtil.HashSha256(pbCmp); + MemUtil.ZeroByteArray(pbCmp); + return pbRet; + } + + /// + /// Get a number of cryptographically strong random bytes. + /// This method is thread-safe. + /// + /// Number of requested random bytes. + /// A byte array consisting of + /// random bytes. + public byte[] GetRandomBytes(uint uRequestedBytes) + { + if(uRequestedBytes == 0) return MemUtil.EmptyByteArray; + if(uRequestedBytes > (uint)int.MaxValue) + { + Debug.Assert(false); + throw new ArgumentOutOfRangeException("uRequestedBytes"); + } + + int cbRem = (int)uRequestedBytes; + byte[] pbRes = new byte[cbRem]; + int iPos = 0; + + while(cbRem != 0) + { + byte[] pbRandom256 = GenerateRandom256(); + Debug.Assert(pbRandom256.Length == 32); + + int cbCopy = Math.Min(cbRem, pbRandom256.Length); + Array.Copy(pbRandom256, 0, pbRes, iPos, cbCopy); + + MemUtil.ZeroByteArray(pbRandom256); + + iPos += cbCopy; + cbRem -= cbCopy; + } + + Debug.Assert(iPos == pbRes.Length); + return pbRes; + } + + private static int g_iWeakSeed = 0; + public static Random NewWeakRandom() + { + long s64 = DateTime.UtcNow.ToBinary(); + int s32 = (int)((s64 >> 32) ^ s64); + + lock(g_oSyncRoot) + { + unchecked + { + g_iWeakSeed += 0x78A8C4B7; // Prime number + s32 ^= g_iWeakSeed; + } + } + + // Prevent overflow in the Random constructor of .NET 2.0 + if(s32 == int.MinValue) s32 = int.MaxValue; + + return new Random(s32); + } + } +} diff --git a/ModernKeePassLib/Cryptography/CryptoRandomStream.cs b/ModernKeePassLib/Cryptography/CryptoRandomStream.cs new file mode 100644 index 0000000..edadfbc --- /dev/null +++ b/ModernKeePassLib/Cryptography/CryptoRandomStream.cs @@ -0,0 +1,262 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Diagnostics; + +#if !KeePassUAP +using System.Security.Cryptography; +#endif + +using ModernKeePassLib.Cryptography.Cipher; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Cryptography +{ + /// + /// Algorithms supported by CryptoRandomStream. + /// + public enum CrsAlgorithm + { + /// + /// Not supported. + /// + Null = 0, + + /// + /// A variant of the ARCFour algorithm (RC4 incompatible). + /// Insecure; for backward compatibility only. + /// + ArcFourVariant = 1, + + /// + /// Salsa20 stream cipher algorithm. + /// + Salsa20 = 2, + + /// + /// ChaCha20 stream cipher algorithm. + /// + ChaCha20 = 3, + + Count = 4 + } + + /// + /// A random stream class. The class is initialized using random + /// bytes provided by the caller. The produced stream has random + /// properties, but for the same seed always the same stream + /// is produced, i.e. this class can be used as stream cipher. + /// + public sealed class CryptoRandomStream : IDisposable + { + private readonly CrsAlgorithm m_crsAlgorithm; + private bool m_bDisposed = false; + + private byte[] m_pbState = null; + private byte m_i = 0; + private byte m_j = 0; + + private Salsa20Cipher m_salsa20 = null; + private ChaCha20Cipher m_chacha20 = null; + + /// + /// Construct a new cryptographically secure random stream object. + /// + /// Algorithm to use. + /// Initialization key. Must not be null + /// and must contain at least 1 byte. + public CryptoRandomStream(CrsAlgorithm a, byte[] pbKey) + { + if(pbKey == null) { Debug.Assert(false); throw new ArgumentNullException("pbKey"); } + + int cbKey = pbKey.Length; + if(cbKey <= 0) + { + Debug.Assert(false); // Need at least one byte + throw new ArgumentOutOfRangeException("pbKey"); + } + + m_crsAlgorithm = a; + + if(a == CrsAlgorithm.ChaCha20) + { + byte[] pbKey32 = new byte[32]; + byte[] pbIV12 = new byte[12]; + + using(SHA512Managed h = new SHA512Managed()) + { + byte[] pbHash = h.ComputeHash(pbKey); + Array.Copy(pbHash, pbKey32, 32); + Array.Copy(pbHash, 32, pbIV12, 0, 12); + MemUtil.ZeroByteArray(pbHash); + } + + m_chacha20 = new ChaCha20Cipher(pbKey32, pbIV12, true); + } + else if(a == CrsAlgorithm.Salsa20) + { + byte[] pbKey32 = CryptoUtil.HashSha256(pbKey); + byte[] pbIV8 = new byte[8] { 0xE8, 0x30, 0x09, 0x4B, + 0x97, 0x20, 0x5D, 0x2A }; // Unique constant + + m_salsa20 = new Salsa20Cipher(pbKey32, pbIV8); + } + else if(a == CrsAlgorithm.ArcFourVariant) + { + // Fill the state linearly + m_pbState = new byte[256]; + for(int w = 0; w < 256; ++w) m_pbState[w] = (byte)w; + + unchecked + { + byte j = 0, t; + int inxKey = 0; + for(int w = 0; w < 256; ++w) // Key setup + { + j += (byte)(m_pbState[w] + pbKey[inxKey]); + + t = m_pbState[0]; // Swap entries + m_pbState[0] = m_pbState[j]; + m_pbState[j] = t; + + ++inxKey; + if(inxKey >= cbKey) inxKey = 0; + } + } + + GetRandomBytes(512); // Increases security, see cryptanalysis + } + else // Unknown algorithm + { + Debug.Assert(false); + throw new ArgumentOutOfRangeException("a"); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if(disposing) + { + if(m_crsAlgorithm == CrsAlgorithm.ChaCha20) + m_chacha20.Dispose(); + else if(m_crsAlgorithm == CrsAlgorithm.Salsa20) + m_salsa20.Dispose(); + else if(m_crsAlgorithm == CrsAlgorithm.ArcFourVariant) + { + MemUtil.ZeroByteArray(m_pbState); + m_i = 0; + m_j = 0; + } + else { Debug.Assert(false); } + + m_bDisposed = true; + } + } + + /// + /// Get random bytes. + /// + /// Number of random bytes to retrieve. + /// Returns random bytes. + public byte[] GetRandomBytes(uint uRequestedCount) + { + if(m_bDisposed) throw new ObjectDisposedException(null); + + if(uRequestedCount == 0) return MemUtil.EmptyByteArray; + if(uRequestedCount > (uint)int.MaxValue) + throw new ArgumentOutOfRangeException("uRequestedCount"); + int cb = (int)uRequestedCount; + + byte[] pbRet = new byte[cb]; + + if(m_crsAlgorithm == CrsAlgorithm.ChaCha20) + m_chacha20.Encrypt(pbRet, 0, cb); + else if(m_crsAlgorithm == CrsAlgorithm.Salsa20) + m_salsa20.Encrypt(pbRet, 0, cb); + else if(m_crsAlgorithm == CrsAlgorithm.ArcFourVariant) + { + unchecked + { + for(int w = 0; w < cb; ++w) + { + ++m_i; + m_j += m_pbState[m_i]; + + byte t = m_pbState[m_i]; // Swap entries + m_pbState[m_i] = m_pbState[m_j]; + m_pbState[m_j] = t; + + t = (byte)(m_pbState[m_i] + m_pbState[m_j]); + pbRet[w] = m_pbState[t]; + } + } + } + else { Debug.Assert(false); } + + return pbRet; + } + + public ulong GetRandomUInt64() + { + byte[] pb = GetRandomBytes(8); + return MemUtil.BytesToUInt64(pb); + } + +#if CRSBENCHMARK + public static string Benchmark() + { + int nRounds = 2000000; + + string str = "ArcFour small: " + BenchTime(CrsAlgorithm.ArcFourVariant, + nRounds, 16).ToString() + "\r\n"; + str += "ArcFour big: " + BenchTime(CrsAlgorithm.ArcFourVariant, + 32, 2 * 1024 * 1024).ToString() + "\r\n"; + str += "Salsa20 small: " + BenchTime(CrsAlgorithm.Salsa20, + nRounds, 16).ToString() + "\r\n"; + str += "Salsa20 big: " + BenchTime(CrsAlgorithm.Salsa20, + 32, 2 * 1024 * 1024).ToString(); + return str; + } + + private static int BenchTime(CrsAlgorithm cra, int nRounds, int nDataSize) + { + byte[] pbKey = new byte[4] { 0x00, 0x01, 0x02, 0x03 }; + + int nStart = Environment.TickCount; + for(int i = 0; i < nRounds; ++i) + { + using(CryptoRandomStream c = new CryptoRandomStream(cra, pbKey)) + { + c.GetRandomBytes((uint)nDataSize); + } + } + int nEnd = Environment.TickCount; + + return (nEnd - nStart); + } +#endif + } +} diff --git a/ModernKeePassLib/Cryptography/CryptoStreamEx.cs b/ModernKeePassLib/Cryptography/CryptoStreamEx.cs new file mode 100644 index 0000000..3b4e70b --- /dev/null +++ b/ModernKeePassLib/Cryptography/CryptoStreamEx.cs @@ -0,0 +1,64 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; + +#if !KeePassUAP +using System.Security.Cryptography; + +namespace ModernKeePassLib.Cryptography +{ + public sealed class CryptoStreamEx : CryptoStream + { + private ICryptoTransform m_t; + private SymmetricAlgorithm m_a; + + public CryptoStreamEx(Stream s, ICryptoTransform t, CryptoStreamMode m, + SymmetricAlgorithm a) : base(s, t, m) + { + m_t = t; + m_a = a; + } + + protected override void Dispose(bool disposing) + { + try { base.Dispose(disposing); } + // Unnecessary exception from CryptoStream with + // RijndaelManagedTransform when a stream hasn't been + // read completely (e.g. incorrect master key) + catch (CryptographicException) { } + catch (Exception) { Debug.Assert(false); } + + if (disposing) + { + try { if (m_t != null) { m_t.Dispose(); m_t = null; } } + catch (Exception) { Debug.Assert(false); } + + // In .NET 2.0, SymmetricAlgorithm.Dispose() is not public + try { if (m_a != null) { m_a.Clear(); m_a = null; } } + catch (Exception) { Debug.Assert(false); } + } + } + } +} +#endif diff --git a/ModernKeePassLib/Cryptography/CryptoUtil.cs b/ModernKeePassLib/Cryptography/CryptoUtil.cs new file mode 100644 index 0000000..1c94a88 --- /dev/null +++ b/ModernKeePassLib/Cryptography/CryptoUtil.cs @@ -0,0 +1,254 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Reflection; +using System.Text; + +#if !KeePassUAP +using System.Security.Cryptography; +#endif + +using ModernKeePassLib.Native; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Cryptography +{ + public static class CryptoUtil + { + private static bool? g_obProtData = null; + public static bool IsProtectedDataSupported + { + get + { + if(g_obProtData.HasValue) return g_obProtData.Value; + + bool b = false; + try + { + Random r = CryptoRandom.NewWeakRandom(); + + byte[] pbData = new byte[137]; + r.NextBytes(pbData); + + byte[] pbEnt = new byte[41]; + r.NextBytes(pbEnt); + + byte[] pbEnc = ProtectedData.Protect(pbData, pbEnt, + DataProtectionScope.CurrentUser); + if((pbEnc != null) && !MemUtil.ArraysEqual(pbEnc, pbData)) + { + byte[] pbDec = ProtectedData.Unprotect(pbEnc, pbEnt, + DataProtectionScope.CurrentUser); + if((pbDec != null) && MemUtil.ArraysEqual(pbDec, pbData)) + b = true; + } + } + catch(Exception) { Debug.Assert(false); } + + Debug.Assert(b); // Should be supported on all systems + g_obProtData = b; + return b; + } + } + + public static byte[] HashSha256(byte[] pbData) + { + if(pbData == null) throw new ArgumentNullException("pbData"); + + return HashSha256(pbData, 0, pbData.Length); + } + + public static byte[] HashSha256(byte[] pbData, int iOffset, int cbCount) + { + if(pbData == null) throw new ArgumentNullException("pbData"); + +#if DEBUG + byte[] pbCopy = new byte[pbData.Length]; + Array.Copy(pbData, pbCopy, pbData.Length); +#endif + + byte[] pbHash; + using(SHA256Managed h = new SHA256Managed()) + { + pbHash = h.ComputeHash(pbData, iOffset, cbCount); + } + +#if DEBUG + // Ensure the data has not been modified + Debug.Assert(MemUtil.ArraysEqual(pbData, pbCopy)); + + Debug.Assert((pbHash != null) && (pbHash.Length == 32)); + byte[] pbZero = new byte[32]; + Debug.Assert(!MemUtil.ArraysEqual(pbHash, pbZero)); +#endif + + return pbHash; + } + + internal static byte[] HashSha256(string strFilePath) + { + byte[] pbHash = null; + + using(FileStream fs = new FileStream(strFilePath, FileMode.Open, + FileAccess.Read, FileShare.Read)) + { + using(SHA256Managed h = new SHA256Managed()) + { + pbHash = h.ComputeHash(fs); + } + } + + return pbHash; + } + + /// + /// Create a cryptographic key of length + /// (in bytes) from . + /// + public static byte[] ResizeKey(byte[] pbIn, int iInOffset, + int cbIn, int cbOut) + { + if(pbIn == null) throw new ArgumentNullException("pbIn"); + if(cbOut < 0) throw new ArgumentOutOfRangeException("cbOut"); + + if(cbOut == 0) return MemUtil.EmptyByteArray; + + byte[] pbHash; + if(cbOut <= 32) pbHash = HashSha256(pbIn, iInOffset, cbIn); + else + { + using(SHA512Managed h = new SHA512Managed()) + { + pbHash = h.ComputeHash(pbIn, iInOffset, cbIn); + } + } + + if(cbOut == pbHash.Length) return pbHash; + + byte[] pbRet = new byte[cbOut]; + if(cbOut < pbHash.Length) + Array.Copy(pbHash, pbRet, cbOut); + else + { + int iPos = 0; + ulong r = 0; + while(iPos < cbOut) + { + Debug.Assert(pbHash.Length == 64); + using(HMACSHA256 h = new HMACSHA256(pbHash)) + { + byte[] pbR = MemUtil.UInt64ToBytes(r); + byte[] pbPart = h.ComputeHash(pbR); + + int cbCopy = Math.Min(cbOut - iPos, pbPart.Length); + Debug.Assert(cbCopy > 0); + + Array.Copy(pbPart, 0, pbRet, iPos, cbCopy); + iPos += cbCopy; + ++r; + + MemUtil.ZeroByteArray(pbPart); + } + } + Debug.Assert(iPos == cbOut); + } + +#if DEBUG + byte[] pbZero = new byte[pbHash.Length]; + Debug.Assert(!MemUtil.ArraysEqual(pbHash, pbZero)); +#endif + MemUtil.ZeroByteArray(pbHash); + return pbRet; + } + +#if !KeePassUAP + private static bool? g_obAesCsp = null; + public static SymmetricAlgorithm CreateAes() + { + if(g_obAesCsp.HasValue) + return (g_obAesCsp.Value ? CreateAesCsp() : new RijndaelManaged()); + + SymmetricAlgorithm a = CreateAesCsp(); + g_obAesCsp = (a != null); + return (a ?? new RijndaelManaged()); + } + + private static SymmetricAlgorithm CreateAesCsp() + { + try + { + // On Windows, the CSP implementation is only minimally + // faster (and for key derivations it's not used anyway, + // as KeePass uses a native implementation based on + // CNG/BCrypt, which is much faster) + if(!NativeLib.IsUnix()) return null; + + string strFqn = Assembly.CreateQualifiedName( + "System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", + "System.Security.Cryptography.AesCryptoServiceProvider"); + + Type t = Type.GetType(strFqn); + if(t == null) return null; + + return (Activator.CreateInstance(t) as SymmetricAlgorithm); + } + catch(Exception) { Debug.Assert(false); } + + return null; + } +#endif + + public static byte[] ProtectData(byte[] pb, byte[] pbOptEntropy, + DataProtectionScope s) + { + return ProtectDataPriv(pb, true, pbOptEntropy, s); + } + + public static byte[] UnprotectData(byte[] pb, byte[] pbOptEntropy, + DataProtectionScope s) + { + return ProtectDataPriv(pb, false, pbOptEntropy, s); + } + + private static byte[] ProtectDataPriv(byte[] pb, bool bProtect, + byte[] pbOptEntropy, DataProtectionScope s) + { + if(pb == null) throw new ArgumentNullException("pb"); + + if((pbOptEntropy != null) && (pbOptEntropy.Length == 0)) + pbOptEntropy = null; + + if(CryptoUtil.IsProtectedDataSupported) + { + if(bProtect) + return ProtectedData.Protect(pb, pbOptEntropy, s); + return ProtectedData.Unprotect(pb, pbOptEntropy, s); + } + + Debug.Assert(false); + byte[] pbCopy = new byte[pb.Length]; + Array.Copy(pb, pbCopy, pb.Length); + return pbCopy; + } + } +} diff --git a/ModernKeePassLib/Cryptography/Hash/Blake2b.cs b/ModernKeePassLib/Cryptography/Hash/Blake2b.cs new file mode 100644 index 0000000..d4658be --- /dev/null +++ b/ModernKeePassLib/Cryptography/Hash/Blake2b.cs @@ -0,0 +1,232 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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 +*/ + +// This implementation is based on the official reference C +// implementation by Samuel Neves (CC0 1.0 Universal). + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +#if !KeePassUAP +using System.Security.Cryptography; +#endif + +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Cryptography.Hash +{ + public sealed class Blake2b : HashAlgorithm + { + private const int NbRounds = 12; + private const int NbBlockBytes = 128; + private const int NbMaxOutBytes = 64; + + private static readonly ulong[] g_vIV = new ulong[8] { + 0x6A09E667F3BCC908UL, 0xBB67AE8584CAA73BUL, + 0x3C6EF372FE94F82BUL, 0xA54FF53A5F1D36F1UL, + 0x510E527FADE682D1UL, 0x9B05688C2B3E6C1FUL, + 0x1F83D9ABFB41BD6BUL, 0x5BE0CD19137E2179UL + }; + + private static readonly int[] g_vSigma = new int[NbRounds * 16] { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3, + 11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4, + 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8, + 9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13, + 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9, + 12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11, + 13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10, + 6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5, + 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 + }; + + private readonly int m_cbHashLength; + + private ulong[] m_h = new ulong[8]; + private ulong[] m_t = new ulong[2]; + private ulong[] m_f = new ulong[2]; + private byte[] m_buf = new byte[NbBlockBytes]; + private int m_cbBuf = 0; + + private ulong[] m_m = new ulong[16]; + private ulong[] m_v = new ulong[16]; + + public Blake2b() + { + m_cbHashLength = NbMaxOutBytes; + this.HashSizeValue = NbMaxOutBytes * 8; // Bits + + Initialize(); + } + + public Blake2b(int cbHashLength) + { + if((cbHashLength < 0) || (cbHashLength > NbMaxOutBytes)) + throw new ArgumentOutOfRangeException("cbHashLength"); + + m_cbHashLength = cbHashLength; + this.HashSizeValue = cbHashLength * 8; // Bits + + Initialize(); + } + + public override void Initialize() + { + Debug.Assert(m_h.Length == g_vIV.Length); + Array.Copy(g_vIV, m_h, m_h.Length); + + // Fan-out = 1, depth = 1 + m_h[0] ^= 0x0000000001010000UL ^ (ulong)m_cbHashLength; + + Array.Clear(m_t, 0, m_t.Length); + Array.Clear(m_f, 0, m_f.Length); + Array.Clear(m_buf, 0, m_buf.Length); + m_cbBuf = 0; + + Array.Clear(m_m, 0, m_m.Length); + Array.Clear(m_v, 0, m_v.Length); + } + + private static void G(ulong[] v, ulong[] m, int r16, int i, + int a, int b, int c, int d) + { + int p = r16 + i; + + v[a] += v[b] + m[g_vSigma[p]]; + v[d] = MemUtil.RotateRight64(v[d] ^ v[a], 32); + v[c] += v[d]; + v[b] = MemUtil.RotateRight64(v[b] ^ v[c], 24); + v[a] += v[b] + m[g_vSigma[p + 1]]; + v[d] = MemUtil.RotateRight64(v[d] ^ v[a], 16); + v[c] += v[d]; + v[b] = MemUtil.RotateRight64(v[b] ^ v[c], 63); + } + + private void Compress(byte[] pb, int iOffset) + { + ulong[] v = m_v; + ulong[] m = m_m; + ulong[] h = m_h; + + for(int i = 0; i < 16; ++i) + m[i] = MemUtil.BytesToUInt64(pb, iOffset + (i << 3)); + + Array.Copy(h, v, 8); + v[8] = g_vIV[0]; + v[9] = g_vIV[1]; + v[10] = g_vIV[2]; + v[11] = g_vIV[3]; + v[12] = g_vIV[4] ^ m_t[0]; + v[13] = g_vIV[5] ^ m_t[1]; + v[14] = g_vIV[6] ^ m_f[0]; + v[15] = g_vIV[7] ^ m_f[1]; + + for(int r = 0; r < NbRounds; ++r) + { + int r16 = r << 4; + + G(v, m, r16, 0, 0, 4, 8, 12); + G(v, m, r16, 2, 1, 5, 9, 13); + G(v, m, r16, 4, 2, 6, 10, 14); + G(v, m, r16, 6, 3, 7, 11, 15); + G(v, m, r16, 8, 0, 5, 10, 15); + G(v, m, r16, 10, 1, 6, 11, 12); + G(v, m, r16, 12, 2, 7, 8, 13); + G(v, m, r16, 14, 3, 4, 9, 14); + } + + for(int i = 0; i < 8; ++i) + h[i] ^= v[i] ^ v[i + 8]; + } + + private void IncrementCounter(ulong cb) + { + m_t[0] += cb; + if(m_t[0] < cb) ++m_t[1]; + } + + protected override void HashCore(byte[] array, int ibStart, int cbSize) + { + Debug.Assert(m_f[0] == 0); + + if((m_cbBuf + cbSize) > NbBlockBytes) // Not '>=' (buffer must not be empty) + { + int cbFill = NbBlockBytes - m_cbBuf; + if(cbFill > 0) Array.Copy(array, ibStart, m_buf, m_cbBuf, cbFill); + + IncrementCounter((ulong)NbBlockBytes); + Compress(m_buf, 0); + + m_cbBuf = 0; + cbSize -= cbFill; + ibStart += cbFill; + + while(cbSize > NbBlockBytes) // Not '>=' (buffer must not be empty) + { + IncrementCounter((ulong)NbBlockBytes); + Compress(array, ibStart); + + cbSize -= NbBlockBytes; + ibStart += NbBlockBytes; + } + } + + if(cbSize > 0) + { + Debug.Assert((m_cbBuf + cbSize) <= NbBlockBytes); + + Array.Copy(array, ibStart, m_buf, m_cbBuf, cbSize); + m_cbBuf += cbSize; + } + } + + protected override byte[] HashFinal() + { + if(m_f[0] != 0) { Debug.Assert(false); throw new InvalidOperationException(); } + Debug.Assert(((m_t[1] == 0) && (m_t[0] == 0)) || + (m_cbBuf > 0)); // Buffer must not be empty for last block processing + + m_f[0] = ulong.MaxValue; // Indicate last block + + int cbFill = NbBlockBytes - m_cbBuf; + if(cbFill > 0) Array.Clear(m_buf, m_cbBuf, cbFill); + + IncrementCounter((ulong)m_cbBuf); + Compress(m_buf, 0); + + byte[] pbHash = new byte[NbMaxOutBytes]; + for(int i = 0; i < m_h.Length; ++i) + MemUtil.UInt64ToBytesEx(m_h[i], pbHash, i << 3); + + if(m_cbHashLength == NbMaxOutBytes) return pbHash; + Debug.Assert(m_cbHashLength < NbMaxOutBytes); + + byte[] pbShort = new byte[m_cbHashLength]; + if(m_cbHashLength > 0) + Array.Copy(pbHash, pbShort, m_cbHashLength); + MemUtil.ZeroByteArray(pbHash); + return pbShort; + } + } +} diff --git a/ModernKeePassLib/Cryptography/HashingStreamEx.cs b/ModernKeePassLib/Cryptography/HashingStreamEx.cs new file mode 100644 index 0000000..0288753 --- /dev/null +++ b/ModernKeePassLib/Cryptography/HashingStreamEx.cs @@ -0,0 +1,187 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; + +#if !KeePassUAP +using System.Security.Cryptography; +#endif + +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Cryptography +{ + public sealed class HashingStreamEx : Stream + { + private readonly Stream m_sBaseStream; + private readonly bool m_bWriting; + private HashAlgorithm m_hash; + + private byte[] m_pbFinalHash = null; + + public byte[] Hash + { + get { return m_pbFinalHash; } + } + + public override bool CanRead + { + get { return !m_bWriting; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return m_bWriting; } + } + + public override long Length + { + get { return m_sBaseStream.Length; } + } + + public override long Position + { + get { return m_sBaseStream.Position; } + set { Debug.Assert(false); throw new NotSupportedException(); } + } + + public HashingStreamEx(Stream sBaseStream, bool bWriting, HashAlgorithm hashAlgorithm) + { + if(sBaseStream == null) throw new ArgumentNullException("sBaseStream"); + + m_sBaseStream = sBaseStream; + m_bWriting = bWriting; + +#if !KeePassLibSD + m_hash = (hashAlgorithm ?? new SHA256Managed()); +#else // KeePassLibSD + m_hash = null; + + try { m_hash = HashAlgorithm.Create("SHA256"); } + catch(Exception) { } + try { if(m_hash == null) m_hash = HashAlgorithm.Create(); } + catch(Exception) { } +#endif + if(m_hash == null) { Debug.Assert(false); return; } + + // Validate hash algorithm + if(!m_hash.CanReuseTransform || !m_hash.CanTransformMultipleBlocks) + { + Debug.Assert(false); + m_hash = null; + } + } + + protected override void Dispose(bool disposing) + { + if(disposing) + { + if(m_hash != null) + { + try + { + m_hash.TransformFinalBlock(MemUtil.EmptyByteArray, 0, 0); + m_pbFinalHash = m_hash.Hash; + m_hash.Clear(); + } + catch(Exception) { Debug.Assert(false); } + + m_hash = null; + } + + m_sBaseStream.Dispose(); + } + + base.Dispose(disposing); + } + + public override void Flush() + { + m_sBaseStream.Flush(); + } + + public override long Seek(long lOffset, SeekOrigin soOrigin) + { + Debug.Assert(false); + throw new NotSupportedException(); + } + + public override void SetLength(long lValue) + { + Debug.Assert(false); + throw new NotSupportedException(); + } + + public override int Read(byte[] pbBuffer, int nOffset, int nCount) + { + if(m_bWriting) { Debug.Assert(false); throw new InvalidOperationException(); } + + int nRead = m_sBaseStream.Read(pbBuffer, nOffset, nCount); + int nPartialRead = nRead; + while((nRead < nCount) && (nPartialRead != 0)) + { + nPartialRead = m_sBaseStream.Read(pbBuffer, nOffset + nRead, + nCount - nRead); + nRead += nPartialRead; + } + +#if DEBUG + byte[] pbOrg = new byte[pbBuffer.Length]; + Array.Copy(pbBuffer, pbOrg, pbBuffer.Length); +#endif + + if((m_hash != null) && (nRead > 0)) + m_hash.TransformBlock(pbBuffer, nOffset, nRead, pbBuffer, nOffset); + +#if DEBUG + Debug.Assert(MemUtil.ArraysEqual(pbBuffer, pbOrg)); +#endif + + return nRead; + } + + public override void Write(byte[] pbBuffer, int nOffset, int nCount) + { + if(!m_bWriting) { Debug.Assert(false); throw new InvalidOperationException(); } + +#if DEBUG + byte[] pbOrg = new byte[pbBuffer.Length]; + Array.Copy(pbBuffer, pbOrg, pbBuffer.Length); +#endif + + if((m_hash != null) && (nCount > 0)) + m_hash.TransformBlock(pbBuffer, nOffset, nCount, pbBuffer, nOffset); + +#if DEBUG + Debug.Assert(MemUtil.ArraysEqual(pbBuffer, pbOrg)); +#endif + + m_sBaseStream.Write(pbBuffer, nOffset, nCount); + } + } +} diff --git a/ModernKeePassLib/Cryptography/HmacOtp.cs b/ModernKeePassLib/Cryptography/HmacOtp.cs new file mode 100644 index 0000000..1b06119 --- /dev/null +++ b/ModernKeePassLib/Cryptography/HmacOtp.cs @@ -0,0 +1,98 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Globalization; +using System.Text; + +#if !KeePassUAP +using System.Security.Cryptography; +#endif + +using ModernKeePassLib.Utility; + +#if !KeePassLibSD +namespace ModernKeePassLib.Cryptography +{ + /// + /// Generate HMAC-based one-time passwords as specified in RFC 4226. + /// + public static class HmacOtp + { + private static readonly uint[] g_vDigitsPower = new uint[] { 1, + 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000 }; + + public static string Generate(byte[] pbSecret, ulong uFactor, + uint uCodeDigits, bool bAddChecksum, int iTruncationOffset) + { + byte[] pbText = MemUtil.UInt64ToBytes(uFactor); + Array.Reverse(pbText); // To big-endian + + byte[] pbHash; + using(HMACSHA1 h = new HMACSHA1(pbSecret)) + { + pbHash = h.ComputeHash(pbText); + } + + uint uOffset = (uint)(pbHash[pbHash.Length - 1] & 0xF); + if((iTruncationOffset >= 0) && (iTruncationOffset < (pbHash.Length - 4))) + uOffset = (uint)iTruncationOffset; + + uint uBinary = (uint)(((pbHash[uOffset] & 0x7F) << 24) | + ((pbHash[uOffset + 1] & 0xFF) << 16) | + ((pbHash[uOffset + 2] & 0xFF) << 8) | + (pbHash[uOffset + 3] & 0xFF)); + + uint uOtp = (uBinary % g_vDigitsPower[uCodeDigits]); + if(bAddChecksum) + uOtp = ((uOtp * 10) + CalculateChecksum(uOtp, uCodeDigits)); + + uint uDigits = (bAddChecksum ? (uCodeDigits + 1) : uCodeDigits); + return uOtp.ToString(NumberFormatInfo.InvariantInfo).PadLeft( + (int)uDigits, '0'); + } + + private static readonly uint[] g_vDoubleDigits = new uint[] { + 0, 2, 4, 6, 8, 1, 3, 5, 7, 9 }; + + private static uint CalculateChecksum(uint uNum, uint uDigits) + { + bool bDoubleDigit = true; + uint uTotal = 0; + + while(0 < uDigits--) + { + uint uDigit = (uNum % 10); + uNum /= 10; + + if(bDoubleDigit) uDigit = g_vDoubleDigits[uDigit]; + + uTotal += uDigit; + bDoubleDigit = !bDoubleDigit; + } + + uint uResult = (uTotal % 10); + if(uResult != 0) uResult = 10 - uResult; + + return uResult; + } + } +} +#endif diff --git a/ModernKeePassLib/Cryptography/KeyDerivation/AesKdf.GCrypt.cs b/ModernKeePassLib/Cryptography/KeyDerivation/AesKdf.GCrypt.cs new file mode 100644 index 0000000..45de516 --- /dev/null +++ b/ModernKeePassLib/Cryptography/KeyDerivation/AesKdf.GCrypt.cs @@ -0,0 +1,399 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Runtime.InteropServices; +using System.Text; +using System.Threading; + +using ModernKeePassLib.Native; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Cryptography.KeyDerivation +{ + public sealed partial class AesKdf : KdfEngine + { + private static bool TransformKeyGCrypt(byte[] pbData32, byte[] pbSeed32, + ulong uRounds) + { + byte[] pbNewData32 = null; + try + { + if(GCryptInitLib()) + { + pbNewData32 = new byte[32]; + Array.Copy(pbData32, pbNewData32, 32); + + if(TransformKeyGCryptPriv(pbNewData32, pbSeed32, uRounds)) + { + Array.Copy(pbNewData32, pbData32, 32); + return true; + } + } + } + catch(Exception) { } + finally { if(pbNewData32 != null) MemUtil.ZeroByteArray(pbNewData32); } + + return false; + } + + private static bool TransformKeyBenchmarkGCrypt(uint uTimeMs, out ulong uRounds) + { + uRounds = 0; + + try + { + if(GCryptInitLib()) + return TransformKeyBenchmarkGCryptPriv(uTimeMs, ref uRounds); + } + catch(Exception) { } + + return false; + } + + private static bool GCryptInitLib() + { + if(!NativeLib.IsUnix()) return false; // Independent of workaround state + if(!MonoWorkarounds.IsRequired(1468)) return false; // Can be turned off + + // gcry_check_version initializes the library; + // throws when LibGCrypt is not available + NativeMethods.gcry_check_version(IntPtr.Zero); + return true; + } + + // ============================================================= + // Multi-threaded implementation + + // For some reason, the following multi-threaded implementation + // is slower than the single-threaded implementation below + // (threading overhead by Mono? LibGCrypt threading issues?) + /* private sealed class GCryptTransformInfo : IDisposable + { + public IntPtr Data16; + public IntPtr Seed32; + public ulong Rounds; + public uint TimeMs; + + public bool Success = false; + + public GCryptTransformInfo(byte[] pbData32, int iDataOffset, + byte[] pbSeed32, ulong uRounds, uint uTimeMs) + { + this.Data16 = Marshal.AllocCoTaskMem(16); + Marshal.Copy(pbData32, iDataOffset, this.Data16, 16); + + this.Seed32 = Marshal.AllocCoTaskMem(32); + Marshal.Copy(pbSeed32, 0, this.Seed32, 32); + + this.Rounds = uRounds; + this.TimeMs = uTimeMs; + } + + public void Dispose() + { + if(this.Data16 != IntPtr.Zero) + { + Marshal.WriteInt64(this.Data16, 0); + Marshal.WriteInt64(this.Data16, 8, 0); + Marshal.FreeCoTaskMem(this.Data16); + this.Data16 = IntPtr.Zero; + } + + if(this.Seed32 != IntPtr.Zero) + { + Marshal.FreeCoTaskMem(this.Seed32); + this.Seed32 = IntPtr.Zero; + } + } + } + + private static GCryptTransformInfo[] GCryptRun(byte[] pbData32, + byte[] pbSeed32, ulong uRounds, uint uTimeMs, ParameterizedThreadStart fL, + ParameterizedThreadStart fR) + { + GCryptTransformInfo tiL = new GCryptTransformInfo(pbData32, 0, + pbSeed32, uRounds, uTimeMs); + GCryptTransformInfo tiR = new GCryptTransformInfo(pbData32, 16, + pbSeed32, uRounds, uTimeMs); + + Thread th = new Thread(fL); + th.Start(tiL); + + fR(tiR); + + th.Join(); + + Marshal.Copy(tiL.Data16, pbData32, 0, 16); + Marshal.Copy(tiR.Data16, pbData32, 16, 16); + + tiL.Dispose(); + tiR.Dispose(); + + if(tiL.Success && tiR.Success) + return new GCryptTransformInfo[2] { tiL, tiR }; + return null; + } + + private static bool TransformKeyGCryptPriv(byte[] pbData32, byte[] pbSeed32, + ulong uRounds) + { + return (GCryptRun(pbData32, pbSeed32, uRounds, 0, + new ParameterizedThreadStart(AesKdf.GCryptTransformTh), + new ParameterizedThreadStart(AesKdf.GCryptTransformTh)) != null); + } + + private static bool GCryptInitCipher(ref IntPtr h, GCryptTransformInfo ti) + { + NativeMethods.gcry_cipher_open(ref h, NativeMethods.GCRY_CIPHER_AES256, + NativeMethods.GCRY_CIPHER_MODE_ECB, 0); + if(h == IntPtr.Zero) { Debug.Assert(false); return false; } + + IntPtr n32 = new IntPtr(32); + if(NativeMethods.gcry_cipher_setkey(h, ti.Seed32, n32) != 0) + { + Debug.Assert(false); + return false; + } + + return true; + } + + private static void GCryptTransformTh(object o) + { + IntPtr h = IntPtr.Zero; + try + { + GCryptTransformInfo ti = (o as GCryptTransformInfo); + if(ti == null) { Debug.Assert(false); return; } + + if(!GCryptInitCipher(ref h, ti)) return; + + IntPtr n16 = new IntPtr(16); + for(ulong u = 0; u < ti.Rounds; ++u) + { + if(NativeMethods.gcry_cipher_encrypt(h, ti.Data16, n16, + IntPtr.Zero, IntPtr.Zero) != 0) + { + Debug.Assert(false); + return; + } + } + + ti.Success = true; + } + catch(Exception) { Debug.Assert(false); } + finally + { + try { if(h != IntPtr.Zero) NativeMethods.gcry_cipher_close(h); } + catch(Exception) { Debug.Assert(false); } + } + } + + private static bool TransformKeyBenchmarkGCryptPriv(uint uTimeMs, ref ulong uRounds) + { + GCryptTransformInfo[] v = GCryptRun(new byte[32], new byte[32], + 0, uTimeMs, + new ParameterizedThreadStart(AesKdf.GCryptBenchmarkTh), + new ParameterizedThreadStart(AesKdf.GCryptBenchmarkTh)); + + if(v != null) + { + ulong uL = Math.Min(v[0].Rounds, ulong.MaxValue >> 1); + ulong uR = Math.Min(v[1].Rounds, ulong.MaxValue >> 1); + uRounds = (uL + uR) / 2; + + return true; + } + return false; + } + + private static void GCryptBenchmarkTh(object o) + { + IntPtr h = IntPtr.Zero; + try + { + GCryptTransformInfo ti = (o as GCryptTransformInfo); + if(ti == null) { Debug.Assert(false); return; } + + if(!GCryptInitCipher(ref h, ti)) return; + + ulong r = 0; + IntPtr n16 = new IntPtr(16); + int tStart = Environment.TickCount; + while(true) + { + for(ulong j = 0; j < BenchStep; ++j) + { + if(NativeMethods.gcry_cipher_encrypt(h, ti.Data16, n16, + IntPtr.Zero, IntPtr.Zero) != 0) + { + Debug.Assert(false); + return; + } + } + + r += BenchStep; + if(r < BenchStep) // Overflow check + { + r = ulong.MaxValue; + break; + } + + uint tElapsed = (uint)(Environment.TickCount - tStart); + if(tElapsed > ti.TimeMs) break; + } + + ti.Rounds = r; + ti.Success = true; + } + catch(Exception) { Debug.Assert(false); } + finally + { + try { if(h != IntPtr.Zero) NativeMethods.gcry_cipher_close(h); } + catch(Exception) { Debug.Assert(false); } + } + } */ + + // ============================================================= + // Single-threaded implementation + + private static bool GCryptInitCipher(ref IntPtr h, IntPtr pSeed32) + { + NativeMethods.gcry_cipher_open(ref h, NativeMethods.GCRY_CIPHER_AES256, + NativeMethods.GCRY_CIPHER_MODE_ECB, 0); + if(h == IntPtr.Zero) { Debug.Assert(false); return false; } + + IntPtr n32 = new IntPtr(32); + if(NativeMethods.gcry_cipher_setkey(h, pSeed32, n32) != 0) + { + Debug.Assert(false); + return false; + } + + return true; + } + + private static bool GCryptBegin(byte[] pbData32, byte[] pbSeed32, + ref IntPtr h, ref IntPtr pData32, ref IntPtr pSeed32) + { + pData32 = Marshal.AllocCoTaskMem(32); + pSeed32 = Marshal.AllocCoTaskMem(32); + + Marshal.Copy(pbData32, 0, pData32, 32); + Marshal.Copy(pbSeed32, 0, pSeed32, 32); + + return GCryptInitCipher(ref h, pSeed32); + } + + private static void GCryptEnd(IntPtr h, IntPtr pData32, IntPtr pSeed32) + { + NativeMethods.gcry_cipher_close(h); + + Marshal.WriteInt64(pData32, 0); + Marshal.WriteInt64(pData32, 8, 0); + Marshal.WriteInt64(pData32, 16, 0); + Marshal.WriteInt64(pData32, 24, 0); + + Marshal.FreeCoTaskMem(pData32); + Marshal.FreeCoTaskMem(pSeed32); + } + + private static bool TransformKeyGCryptPriv(byte[] pbData32, byte[] pbSeed32, + ulong uRounds) + { + IntPtr h = IntPtr.Zero, pData32 = IntPtr.Zero, pSeed32 = IntPtr.Zero; + if(!GCryptBegin(pbData32, pbSeed32, ref h, ref pData32, ref pSeed32)) + return false; + + try + { + IntPtr n32 = new IntPtr(32); + for(ulong i = 0; i < uRounds; ++i) + { + if(NativeMethods.gcry_cipher_encrypt(h, pData32, n32, + IntPtr.Zero, IntPtr.Zero) != 0) + { + Debug.Assert(false); + return false; + } + } + + Marshal.Copy(pData32, pbData32, 0, 32); + return true; + } + catch(Exception) { Debug.Assert(false); } + finally { GCryptEnd(h, pData32, pSeed32); } + + return false; + } + + private static bool TransformKeyBenchmarkGCryptPriv(uint uTimeMs, ref ulong uRounds) + { + byte[] pbData32 = new byte[32]; + byte[] pbSeed32 = new byte[32]; + + IntPtr h = IntPtr.Zero, pData32 = IntPtr.Zero, pSeed32 = IntPtr.Zero; + if(!GCryptBegin(pbData32, pbSeed32, ref h, ref pData32, ref pSeed32)) + return false; + + uint uMaxMs = uTimeMs; + ulong uDiv = 1; + if(uMaxMs <= (uint.MaxValue >> 1)) { uMaxMs *= 2U; uDiv = 2; } + + try + { + ulong r = 0; + IntPtr n32 = new IntPtr(32); + int tStart = Environment.TickCount; + while(true) + { + for(ulong j = 0; j < BenchStep; ++j) + { + if(NativeMethods.gcry_cipher_encrypt(h, pData32, n32, + IntPtr.Zero, IntPtr.Zero) != 0) + { + Debug.Assert(false); + return false; + } + } + + r += BenchStep; + if(r < BenchStep) // Overflow check + { + r = ulong.MaxValue; + break; + } + + uint tElapsed = (uint)(Environment.TickCount - tStart); + if(tElapsed > uMaxMs) break; + } + + uRounds = r / uDiv; + return true; + } + catch(Exception) { Debug.Assert(false); } + finally { GCryptEnd(h, pData32, pSeed32); } + + return false; + } + } +} diff --git a/ModernKeePassLib/Cryptography/KeyDerivation/AesKdf.cs b/ModernKeePassLib/Cryptography/KeyDerivation/AesKdf.cs new file mode 100644 index 0000000..112660f --- /dev/null +++ b/ModernKeePassLib/Cryptography/KeyDerivation/AesKdf.cs @@ -0,0 +1,281 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +#if KeePassUAP +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Parameters; +#else +using System.Security.Cryptography; +#endif + +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Native; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Cryptography.KeyDerivation +{ + public sealed partial class AesKdf : KdfEngine + { + private static readonly PwUuid g_uuid = new PwUuid(new byte[] { + 0xC9, 0xD9, 0xF3, 0x9A, 0x62, 0x8A, 0x44, 0x60, + 0xBF, 0x74, 0x0D, 0x08, 0xC1, 0x8A, 0x4F, 0xEA }); + + public static readonly string ParamRounds = "R"; // UInt64 + public static readonly string ParamSeed = "S"; // Byte[32] + + private const ulong BenchStep = 3001; + + public override PwUuid Uuid + { + get { return g_uuid; } + } + + public override string Name + { + get { return "AES-KDF"; } + } + + public AesKdf() + { + } + + public override KdfParameters GetDefaultParameters() + { + KdfParameters p = base.GetDefaultParameters(); + p.SetUInt64(ParamRounds, PwDefs.DefaultKeyEncryptionRounds); + return p; + } + + public override void Randomize(KdfParameters p) + { + if(p == null) { Debug.Assert(false); return; } + Debug.Assert(g_uuid.Equals(p.KdfUuid)); + + byte[] pbSeed = CryptoRandom.Instance.GetRandomBytes(32); + p.SetByteArray(ParamSeed, pbSeed); + } + + public override byte[] Transform(byte[] pbMsg, KdfParameters p) + { + if(pbMsg == null) throw new ArgumentNullException("pbMsg"); + if(p == null) throw new ArgumentNullException("p"); + + Type tRounds = p.GetTypeOf(ParamRounds); + if(tRounds == null) throw new ArgumentNullException("p.Rounds"); + if(tRounds != typeof(ulong)) throw new ArgumentOutOfRangeException("p.Rounds"); + ulong uRounds = p.GetUInt64(ParamRounds, 0); + + byte[] pbSeed = p.GetByteArray(ParamSeed); + if(pbSeed == null) throw new ArgumentNullException("p.Seed"); + + if(pbMsg.Length != 32) + { + Debug.Assert(false); + pbMsg = CryptoUtil.HashSha256(pbMsg); + } + + if(pbSeed.Length != 32) + { + Debug.Assert(false); + pbSeed = CryptoUtil.HashSha256(pbSeed); + } + + return TransformKey(pbMsg, pbSeed, uRounds); + } + + private static byte[] TransformKey(byte[] pbOriginalKey32, byte[] pbKeySeed32, + ulong uNumRounds) + { + Debug.Assert((pbOriginalKey32 != null) && (pbOriginalKey32.Length == 32)); + if(pbOriginalKey32 == null) throw new ArgumentNullException("pbOriginalKey32"); + if(pbOriginalKey32.Length != 32) throw new ArgumentException(); + + Debug.Assert((pbKeySeed32 != null) && (pbKeySeed32.Length == 32)); + if(pbKeySeed32 == null) throw new ArgumentNullException("pbKeySeed32"); + if(pbKeySeed32.Length != 32) throw new ArgumentException(); + + byte[] pbNewKey = new byte[32]; + Array.Copy(pbOriginalKey32, pbNewKey, pbNewKey.Length); + + try + { +#if !ModernKeePassLib + if(NativeLib.TransformKey256(pbNewKey, pbKeySeed32, uNumRounds)) + return CryptoUtil.HashSha256(pbNewKey); +#endif + + if(TransformKeyGCrypt(pbNewKey, pbKeySeed32, uNumRounds)) + return CryptoUtil.HashSha256(pbNewKey); + + if(TransformKeyManaged(pbNewKey, pbKeySeed32, uNumRounds)) + return CryptoUtil.HashSha256(pbNewKey); + } + finally { MemUtil.ZeroByteArray(pbNewKey); } + + return null; + } + + public static bool TransformKeyManaged(byte[] pbNewKey32, byte[] pbKeySeed32, + ulong uNumRounds) + { +#if KeePassUAP + KeyParameter kp = new KeyParameter(pbKeySeed32); + AesEngine aes = new AesEngine(); + aes.Init(true, kp); + + for(ulong u = 0; u < uNumRounds; ++u) + { + aes.ProcessBlock(pbNewKey32, 0, pbNewKey32, 0); + aes.ProcessBlock(pbNewKey32, 16, pbNewKey32, 16); + } + + aes.Reset(); +#else + byte[] pbIV = new byte[16]; + + using(SymmetricAlgorithm a = CryptoUtil.CreateAes()) + { + if(a.BlockSize != 128) // AES block size + { + Debug.Assert(false); + a.BlockSize = 128; + } + a.KeySize = 256; + a.Mode = CipherMode.ECB; + + using(ICryptoTransform t = a.CreateEncryptor(pbKeySeed32, pbIV)) + { + // !t.CanReuseTransform -- doesn't work with Mono + if((t == null) || (t.InputBlockSize != 16) || + (t.OutputBlockSize != 16)) + { + Debug.Assert(false); + return false; + } + + for(ulong u = 0; u < uNumRounds; ++u) + { + t.TransformBlock(pbNewKey32, 0, 16, pbNewKey32, 0); + t.TransformBlock(pbNewKey32, 16, 16, pbNewKey32, 16); + } + } + } +#endif + + return true; + } + + public override KdfParameters GetBestParameters(uint uMilliseconds) + { + KdfParameters p = GetDefaultParameters(); + ulong uRounds; +#if !ModernKeePassLib + // Try native method + if(NativeLib.TransformKeyBenchmark256(uMilliseconds, out uRounds)) + { + p.SetUInt64(ParamRounds, uRounds); + return p; + } +#endif + if(TransformKeyBenchmarkGCrypt(uMilliseconds, out uRounds)) + { + p.SetUInt64(ParamRounds, uRounds); + return p; + } + + byte[] pbKey = new byte[32]; + byte[] pbNewKey = new byte[32]; + for(int i = 0; i < pbKey.Length; ++i) + { + pbKey[i] = (byte)i; + pbNewKey[i] = (byte)i; + } + +#if KeePassUAP + KeyParameter kp = new KeyParameter(pbKey); + AesEngine aes = new AesEngine(); + aes.Init(true, kp); +#else + byte[] pbIV = new byte[16]; + + using(SymmetricAlgorithm a = CryptoUtil.CreateAes()) + { + if(a.BlockSize != 128) // AES block size + { + Debug.Assert(false); + a.BlockSize = 128; + } + a.KeySize = 256; + a.Mode = CipherMode.ECB; + + using(ICryptoTransform t = a.CreateEncryptor(pbKey, pbIV)) + { + // !t.CanReuseTransform -- doesn't work with Mono + if((t == null) || (t.InputBlockSize != 16) || + (t.OutputBlockSize != 16)) + { + Debug.Assert(false); + p.SetUInt64(ParamRounds, PwDefs.DefaultKeyEncryptionRounds); + return p; + } +#endif + + uRounds = 0; + int tStart = Environment.TickCount; + while(true) + { + for(ulong j = 0; j < BenchStep; ++j) + { +#if KeePassUAP + aes.ProcessBlock(pbNewKey, 0, pbNewKey, 0); + aes.ProcessBlock(pbNewKey, 16, pbNewKey, 16); +#else + t.TransformBlock(pbNewKey, 0, 16, pbNewKey, 0); + t.TransformBlock(pbNewKey, 16, 16, pbNewKey, 16); +#endif + } + + uRounds += BenchStep; + if(uRounds < BenchStep) // Overflow check + { + uRounds = ulong.MaxValue; + break; + } + + uint tElapsed = (uint)(Environment.TickCount - tStart); + if(tElapsed > uMilliseconds) break; + } + + p.SetUInt64(ParamRounds, uRounds); +#if KeePassUAP + aes.Reset(); +#else + } + } +#endif + return p; + } + } +} diff --git a/ModernKeePassLib/Cryptography/KeyDerivation/Argon2Kdf.Core.cs b/ModernKeePassLib/Cryptography/KeyDerivation/Argon2Kdf.Core.cs new file mode 100644 index 0000000..eff035f --- /dev/null +++ b/ModernKeePassLib/Cryptography/KeyDerivation/Argon2Kdf.Core.cs @@ -0,0 +1,637 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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 +*/ + +// This implementation is based on the official reference C +// implementation by Daniel Dinu and Dmitry Khovratovich (CC0 1.0). + +// Relative iterations (* = B2ROUND_ARRAYS \\ G_INLINED): +// * | false true +// ------+----------- +// false | 8885 9618 +// true | 9009 9636 +#define ARGON2_B2ROUND_ARRAYS +#define ARGON2_G_INLINED + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using ModernKeePassLib.Cryptography.Hash; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Cryptography.KeyDerivation +{ + public sealed partial class Argon2Kdf : KdfEngine + { + private const ulong NbBlockSize = 1024; + private const ulong NbBlockSizeInQW = NbBlockSize / 8UL; + private const ulong NbSyncPoints = 4; + + private const int NbPreHashDigestLength = 64; + private const int NbPreHashSeedLength = NbPreHashDigestLength + 8; + +#if ARGON2_B2ROUND_ARRAYS + private static int[][] g_vFBCols = null; + private static int[][] g_vFBRows = null; +#endif + + private sealed class Argon2Ctx + { + public uint Version = 0; + + public ulong Lanes = 0; + public ulong TCost = 0; + public ulong MCost = 0; + public ulong MemoryBlocks = 0; + public ulong SegmentLength = 0; + public ulong LaneLength = 0; + + public ulong[] Mem = null; + } + + private sealed class Argon2ThreadInfo + { + public Argon2Ctx Context = null; + public ManualResetEvent Finished = new ManualResetEvent(false); + + public ulong Pass = 0; + public ulong Lane = 0; + public ulong Slice = 0; + public ulong Index = 0; + + public void Release() + { + if(this.Finished != null) + { + this.Finished.Dispose(); + this.Finished = null; + } + else { Debug.Assert(false); } + } + } + + private static byte[] Argon2d(byte[] pbMsg, byte[] pbSalt, uint uParallel, + ulong uMem, ulong uIt, int cbOut, uint uVersion, byte[] pbSecretKey, + byte[] pbAssocData) + { + pbSecretKey = (pbSecretKey ?? MemUtil.EmptyByteArray); + pbAssocData = (pbAssocData ?? MemUtil.EmptyByteArray); + +#if ARGON2_B2ROUND_ARRAYS + InitB2RoundIndexArrays(); +#endif + + Argon2Ctx ctx = new Argon2Ctx(); + ctx.Version = uVersion; + + ctx.Lanes = uParallel; + ctx.TCost = uIt; + ctx.MCost = uMem / NbBlockSize; + ctx.MemoryBlocks = Math.Max(ctx.MCost, 2UL * NbSyncPoints * ctx.Lanes); + + ctx.SegmentLength = ctx.MemoryBlocks / (ctx.Lanes * NbSyncPoints); + ctx.MemoryBlocks = ctx.SegmentLength * ctx.Lanes * NbSyncPoints; + + ctx.LaneLength = ctx.SegmentLength * NbSyncPoints; + + Debug.Assert(NbBlockSize == (NbBlockSizeInQW * +#if ModernKeePassLib || KeePassUAP + (ulong)Marshal.SizeOf() +#else + (ulong)Marshal.SizeOf(typeof(ulong)) +#endif + )); + ctx.Mem = new ulong[ctx.MemoryBlocks * NbBlockSizeInQW]; + + Blake2b h = new Blake2b(); + + // Initial hash + Debug.Assert(h.HashSize == (NbPreHashDigestLength * 8)); + byte[] pbBuf = new byte[4]; + MemUtil.UInt32ToBytesEx(uParallel, pbBuf, 0); + h.TransformBlock(pbBuf, 0, pbBuf.Length, pbBuf, 0); + MemUtil.UInt32ToBytesEx((uint)cbOut, pbBuf, 0); + h.TransformBlock(pbBuf, 0, pbBuf.Length, pbBuf, 0); + MemUtil.UInt32ToBytesEx((uint)ctx.MCost, pbBuf, 0); + h.TransformBlock(pbBuf, 0, pbBuf.Length, pbBuf, 0); + MemUtil.UInt32ToBytesEx((uint)uIt, pbBuf, 0); + h.TransformBlock(pbBuf, 0, pbBuf.Length, pbBuf, 0); + MemUtil.UInt32ToBytesEx(uVersion, pbBuf, 0); + h.TransformBlock(pbBuf, 0, pbBuf.Length, pbBuf, 0); + MemUtil.UInt32ToBytesEx(0, pbBuf, 0); // Argon2d type = 0 + h.TransformBlock(pbBuf, 0, pbBuf.Length, pbBuf, 0); + MemUtil.UInt32ToBytesEx((uint)pbMsg.Length, pbBuf, 0); + h.TransformBlock(pbBuf, 0, pbBuf.Length, pbBuf, 0); + h.TransformBlock(pbMsg, 0, pbMsg.Length, pbMsg, 0); + MemUtil.UInt32ToBytesEx((uint)pbSalt.Length, pbBuf, 0); + h.TransformBlock(pbBuf, 0, pbBuf.Length, pbBuf, 0); + h.TransformBlock(pbSalt, 0, pbSalt.Length, pbSalt, 0); + MemUtil.UInt32ToBytesEx((uint)pbSecretKey.Length, pbBuf, 0); + h.TransformBlock(pbBuf, 0, pbBuf.Length, pbBuf, 0); + h.TransformBlock(pbSecretKey, 0, pbSecretKey.Length, pbSecretKey, 0); + MemUtil.UInt32ToBytesEx((uint)pbAssocData.Length, pbBuf, 0); + h.TransformBlock(pbBuf, 0, pbBuf.Length, pbBuf, 0); + h.TransformBlock(pbAssocData, 0, pbAssocData.Length, pbAssocData, 0); + h.TransformFinalBlock(MemUtil.EmptyByteArray, 0, 0); + byte[] pbH0 = h.Hash; + Debug.Assert(pbH0.Length == 64); + + byte[] pbBlockHash = new byte[NbPreHashSeedLength]; + Array.Copy(pbH0, pbBlockHash, pbH0.Length); + MemUtil.ZeroByteArray(pbH0); + + FillFirstBlocks(ctx, pbBlockHash, h); + MemUtil.ZeroByteArray(pbBlockHash); + + FillMemoryBlocks(ctx); + + byte[] pbOut = FinalHash(ctx, cbOut, h); + + h.Clear(); + MemUtil.ZeroArray(ctx.Mem); + return pbOut; + } + + private static void LoadBlock(ulong[] pqDst, ulong uDstOffset, byte[] pbIn) + { + // for(ulong i = 0; i < NbBlockSizeInQW; ++i) + // pqDst[uDstOffset + i] = MemUtil.BytesToUInt64(pbIn, (int)(i << 3)); + + Debug.Assert((uDstOffset + NbBlockSizeInQW - 1UL) <= (ulong)int.MaxValue); + int iDstOffset = (int)uDstOffset; + for(int i = 0; i < (int)NbBlockSizeInQW; ++i) + pqDst[iDstOffset + i] = MemUtil.BytesToUInt64(pbIn, i << 3); + } + + private static void StoreBlock(byte[] pbDst, ulong[] pqSrc) + { + for(int i = 0; i < (int)NbBlockSizeInQW; ++i) + MemUtil.UInt64ToBytesEx(pqSrc[i], pbDst, i << 3); + } + + private static void CopyBlock(ulong[] vDst, ulong uDstOffset, ulong[] vSrc, + ulong uSrcOffset) + { + // for(ulong i = 0; i < NbBlockSizeInQW; ++i) + // vDst[uDstOffset + i] = vSrc[uSrcOffset + i]; + + // Debug.Assert((uDstOffset + NbBlockSizeInQW - 1UL) <= (ulong)int.MaxValue); + // Debug.Assert((uSrcOffset + NbBlockSizeInQW - 1UL) <= (ulong)int.MaxValue); + // int iDstOffset = (int)uDstOffset; + // int iSrcOffset = (int)uSrcOffset; + // for(int i = 0; i < (int)NbBlockSizeInQW; ++i) + // vDst[iDstOffset + i] = vSrc[iSrcOffset + i]; + +#if ModernKeePassLib || KeePassUAP + Array.Copy(vSrc, (int)uSrcOffset, vDst, (int)uDstOffset, + (int)NbBlockSizeInQW); +#else + Array.Copy(vSrc, (long)uSrcOffset, vDst, (long)uDstOffset, + (long)NbBlockSizeInQW); +#endif + } + + private static void XorBlock(ulong[] vDst, ulong uDstOffset, ulong[] vSrc, + ulong uSrcOffset) + { + // for(ulong i = 0; i < NbBlockSizeInQW; ++i) + // vDst[uDstOffset + i] ^= vSrc[uSrcOffset + i]; + + Debug.Assert((uDstOffset + NbBlockSizeInQW - 1UL) <= (ulong)int.MaxValue); + Debug.Assert((uSrcOffset + NbBlockSizeInQW - 1UL) <= (ulong)int.MaxValue); + int iDstOffset = (int)uDstOffset; + int iSrcOffset = (int)uSrcOffset; + for(int i = 0; i < (int)NbBlockSizeInQW; ++i) + vDst[iDstOffset + i] ^= vSrc[iSrcOffset + i]; + } + + private static void Blake2bLong(byte[] pbOut, int cbOut, + byte[] pbIn, int cbIn, Blake2b h) + { + Debug.Assert((h != null) && (h.HashSize == (64 * 8))); + + byte[] pbOutLen = new byte[4]; + MemUtil.UInt32ToBytesEx((uint)cbOut, pbOutLen, 0); + + if(cbOut <= 64) + { + Blake2b hOut = ((cbOut == 64) ? h : new Blake2b(cbOut)); + if(cbOut == 64) hOut.Initialize(); + + hOut.TransformBlock(pbOutLen, 0, pbOutLen.Length, pbOutLen, 0); + hOut.TransformBlock(pbIn, 0, cbIn, pbIn, 0); + hOut.TransformFinalBlock(MemUtil.EmptyByteArray, 0, 0); + + Array.Copy(hOut.Hash, pbOut, cbOut); + + if(cbOut < 64) hOut.Clear(); + return; + } + + h.Initialize(); + h.TransformBlock(pbOutLen, 0, pbOutLen.Length, pbOutLen, 0); + h.TransformBlock(pbIn, 0, cbIn, pbIn, 0); + h.TransformFinalBlock(MemUtil.EmptyByteArray, 0, 0); + + byte[] pbOutBuffer = new byte[64]; + Array.Copy(h.Hash, pbOutBuffer, pbOutBuffer.Length); + + int ibOut = 64 / 2; + Array.Copy(pbOutBuffer, pbOut, ibOut); + int cbToProduce = cbOut - ibOut; + + h.Initialize(); + while(cbToProduce > 64) + { + byte[] pbHash = h.ComputeHash(pbOutBuffer); + Array.Copy(pbHash, pbOutBuffer, 64); + + Array.Copy(pbHash, 0, pbOut, ibOut, 64 / 2); + ibOut += 64 / 2; + cbToProduce -= 64 / 2; + + MemUtil.ZeroByteArray(pbHash); + } + + using(Blake2b hOut = new Blake2b(cbToProduce)) + { + byte[] pbHash = hOut.ComputeHash(pbOutBuffer); + Array.Copy(pbHash, 0, pbOut, ibOut, cbToProduce); + + MemUtil.ZeroByteArray(pbHash); + } + + MemUtil.ZeroByteArray(pbOutBuffer); + } + +#if !ARGON2_G_INLINED + private static ulong BlaMka(ulong x, ulong y) + { + ulong xy = (x & 0xFFFFFFFFUL) * (y & 0xFFFFFFFFUL); + return (x + y + (xy << 1)); + } + + private static void G(ulong[] v, int a, int b, int c, int d) + { + ulong va = v[a], vb = v[b], vc = v[c], vd = v[d]; + + va = BlaMka(va, vb); + vd = MemUtil.RotateRight64(vd ^ va, 32); + vc = BlaMka(vc, vd); + vb = MemUtil.RotateRight64(vb ^ vc, 24); + va = BlaMka(va, vb); + vd = MemUtil.RotateRight64(vd ^ va, 16); + vc = BlaMka(vc, vd); + vb = MemUtil.RotateRight64(vb ^ vc, 63); + + v[a] = va; + v[b] = vb; + v[c] = vc; + v[d] = vd; + } +#else + private static void G(ulong[] v, int a, int b, int c, int d) + { + ulong va = v[a], vb = v[b], vc = v[c], vd = v[d]; + + ulong xy = (va & 0xFFFFFFFFUL) * (vb & 0xFFFFFFFFUL); + va += vb + (xy << 1); + + vd = MemUtil.RotateRight64(vd ^ va, 32); + + xy = (vc & 0xFFFFFFFFUL) * (vd & 0xFFFFFFFFUL); + vc += vd + (xy << 1); + + vb = MemUtil.RotateRight64(vb ^ vc, 24); + + xy = (va & 0xFFFFFFFFUL) * (vb & 0xFFFFFFFFUL); + va += vb + (xy << 1); + + vd = MemUtil.RotateRight64(vd ^ va, 16); + + xy = (vc & 0xFFFFFFFFUL) * (vd & 0xFFFFFFFFUL); + vc += vd + (xy << 1); + + vb = MemUtil.RotateRight64(vb ^ vc, 63); + + v[a] = va; + v[b] = vb; + v[c] = vc; + v[d] = vd; + } +#endif + +#if ARGON2_B2ROUND_ARRAYS + private static void Blake2RoundNoMsg(ulong[] pbR, int[] v) + { + G(pbR, v[0], v[4], v[8], v[12]); + G(pbR, v[1], v[5], v[9], v[13]); + G(pbR, v[2], v[6], v[10], v[14]); + G(pbR, v[3], v[7], v[11], v[15]); + G(pbR, v[0], v[5], v[10], v[15]); + G(pbR, v[1], v[6], v[11], v[12]); + G(pbR, v[2], v[7], v[8], v[13]); + G(pbR, v[3], v[4], v[9], v[14]); + } +#else + private static void Blake2RoundNoMsgCols16i(ulong[] pbR, int i) + { + G(pbR, i, i + 4, i + 8, i + 12); + G(pbR, i + 1, i + 5, i + 9, i + 13); + G(pbR, i + 2, i + 6, i + 10, i + 14); + G(pbR, i + 3, i + 7, i + 11, i + 15); + G(pbR, i, i + 5, i + 10, i + 15); + G(pbR, i + 1, i + 6, i + 11, i + 12); + G(pbR, i + 2, i + 7, i + 8, i + 13); + G(pbR, i + 3, i + 4, i + 9, i + 14); + } + + private static void Blake2RoundNoMsgRows2i(ulong[] pbR, int i) + { + G(pbR, i, i + 32, i + 64, i + 96); + G(pbR, i + 1, i + 33, i + 65, i + 97); + G(pbR, i + 16, i + 48, i + 80, i + 112); + G(pbR, i + 17, i + 49, i + 81, i + 113); + G(pbR, i, i + 33, i + 80, i + 113); + G(pbR, i + 1, i + 48, i + 81, i + 96); + G(pbR, i + 16, i + 49, i + 64, i + 97); + G(pbR, i + 17, i + 32, i + 65, i + 112); + } +#endif + + private static void FillFirstBlocks(Argon2Ctx ctx, byte[] pbBlockHash, + Blake2b h) + { + byte[] pbBlock = new byte[NbBlockSize]; + + for(ulong l = 0; l < ctx.Lanes; ++l) + { + MemUtil.UInt32ToBytesEx(0, pbBlockHash, NbPreHashDigestLength); + MemUtil.UInt32ToBytesEx((uint)l, pbBlockHash, NbPreHashDigestLength + 4); + + Blake2bLong(pbBlock, (int)NbBlockSize, pbBlockHash, + NbPreHashSeedLength, h); + LoadBlock(ctx.Mem, l * ctx.LaneLength * NbBlockSizeInQW, pbBlock); + + MemUtil.UInt32ToBytesEx(1, pbBlockHash, NbPreHashDigestLength); + + Blake2bLong(pbBlock, (int)NbBlockSize, pbBlockHash, + NbPreHashSeedLength, h); + LoadBlock(ctx.Mem, (l * ctx.LaneLength + 1UL) * NbBlockSizeInQW, pbBlock); + } + + MemUtil.ZeroByteArray(pbBlock); + } + + private static ulong IndexAlpha(Argon2Ctx ctx, Argon2ThreadInfo ti, + uint uPseudoRand, bool bSameLane) + { + ulong uRefAreaSize; + if(ti.Pass == 0) + { + if(ti.Slice == 0) + { + Debug.Assert(ti.Index > 0); + uRefAreaSize = ti.Index - 1UL; + } + else + { + if(bSameLane) + uRefAreaSize = ti.Slice * ctx.SegmentLength + + ti.Index - 1UL; + else + uRefAreaSize = ti.Slice * ctx.SegmentLength - + ((ti.Index == 0UL) ? 1UL : 0UL); + } + } + else + { + if(bSameLane) + uRefAreaSize = ctx.LaneLength - ctx.SegmentLength + + ti.Index - 1UL; + else + uRefAreaSize = ctx.LaneLength - ctx.SegmentLength - + ((ti.Index == 0) ? 1UL : 0UL); + } + Debug.Assert(uRefAreaSize <= (ulong)uint.MaxValue); + + ulong uRelPos = uPseudoRand; + uRelPos = (uRelPos * uRelPos) >> 32; + uRelPos = uRefAreaSize - 1UL - ((uRefAreaSize * uRelPos) >> 32); + + ulong uStart = 0; + if(ti.Pass != 0) + uStart = (((ti.Slice + 1UL) == NbSyncPoints) ? 0UL : + ((ti.Slice + 1UL) * ctx.SegmentLength)); + Debug.Assert(uStart <= (ulong)uint.MaxValue); + + Debug.Assert(ctx.LaneLength <= (ulong)uint.MaxValue); + return ((uStart + uRelPos) % ctx.LaneLength); + } + + private static void FillMemoryBlocks(Argon2Ctx ctx) + { + int np = (int)ctx.Lanes; + Argon2ThreadInfo[] v = new Argon2ThreadInfo[np]; + + for(ulong r = 0; r < ctx.TCost; ++r) + { + for(ulong s = 0; s < NbSyncPoints; ++s) + { + for(int l = 0; l < np; ++l) + { + Argon2ThreadInfo ti = new Argon2ThreadInfo(); + ti.Context = ctx; + + ti.Pass = r; + ti.Lane = (ulong)l; + ti.Slice = s; + +#if ModernKeePassLib + Task.Factory.StartNew(FillSegmentThr, ti); + //ThreadPool.RunAsync(a => FillSegmentThr(ti)); +#else + if(!ThreadPool.QueueUserWorkItem(FillSegmentThr, ti)) + { + Debug.Assert(false); + throw new OutOfMemoryException(); + } +#endif + v[l] = ti; + } + + for(int l = 0; l < np; ++l) + { + v[l].Finished.WaitOne(); + v[l].Release(); + } + } + } + } + + private static void FillSegmentThr(object o) + { + Argon2ThreadInfo ti = (o as Argon2ThreadInfo); + if(ti == null) { Debug.Assert(false); return; } + + try + { + Argon2Ctx ctx = ti.Context; + if(ctx == null) { Debug.Assert(false); return; } + + Debug.Assert(ctx.Version >= MinVersion); + bool bCanXor = (ctx.Version >= 0x13U); + + ulong uStart = 0; + if((ti.Pass == 0) && (ti.Slice == 0)) uStart = 2; + + ulong uCur = (ti.Lane * ctx.LaneLength) + (ti.Slice * + ctx.SegmentLength) + uStart; + + ulong uPrev = (((uCur % ctx.LaneLength) == 0) ? + (uCur + ctx.LaneLength - 1UL) : (uCur - 1UL)); + + ulong[] pbR = new ulong[NbBlockSizeInQW]; + ulong[] pbTmp = new ulong[NbBlockSizeInQW]; + + for(ulong i = uStart; i < ctx.SegmentLength; ++i) + { + if((uCur % ctx.LaneLength) == 1) + uPrev = uCur - 1UL; + + ulong uPseudoRand = ctx.Mem[uPrev * NbBlockSizeInQW]; + ulong uRefLane = (uPseudoRand >> 32) % ctx.Lanes; + if((ti.Pass == 0) && (ti.Slice == 0)) + uRefLane = ti.Lane; + + ti.Index = i; + ulong uRefIndex = IndexAlpha(ctx, ti, (uint)uPseudoRand, + (uRefLane == ti.Lane)); + + ulong uRefBlockIndex = (ctx.LaneLength * uRefLane + + uRefIndex) * NbBlockSizeInQW; + ulong uCurBlockIndex = uCur * NbBlockSizeInQW; + + FillBlock(ctx.Mem, uPrev * NbBlockSizeInQW, uRefBlockIndex, + uCurBlockIndex, ((ti.Pass != 0) && bCanXor), pbR, pbTmp); + + ++uCur; + ++uPrev; + } + + MemUtil.ZeroArray(pbR); + MemUtil.ZeroArray(pbTmp); + } + catch(Exception) { Debug.Assert(false); } + + try { ti.Finished.Set(); } + catch(Exception) { Debug.Assert(false); } + } + +#if ARGON2_B2ROUND_ARRAYS + private static void InitB2RoundIndexArrays() + { + int[][] vCols = g_vFBCols; + if(vCols == null) + { + vCols = new int[8][]; + Debug.Assert(vCols.Length == 8); + int e = 0; + for(int i = 0; i < 8; ++i) + { + vCols[i] = new int[16]; + for(int j = 0; j < 16; ++j) + { + vCols[i][j] = e; + ++e; + } + } + + g_vFBCols = vCols; + } + + int[][] vRows = g_vFBRows; + if(vRows == null) + { + vRows = new int[8][]; + for(int i = 0; i < 8; ++i) + { + vRows[i] = new int[16]; + for(int j = 0; j < 16; ++j) + { + int jh = j / 2; + vRows[i][j] = (2 * i) + (16 * jh) + (j & 1); + } + } + + g_vFBRows = vRows; + } + } +#endif + + private static void FillBlock(ulong[] pMem, ulong uPrev, ulong uRef, + ulong uNext, bool bXor, ulong[] pbR, ulong[] pbTmp) + { + CopyBlock(pbR, 0, pMem, uRef); + XorBlock(pbR, 0, pMem, uPrev); + CopyBlock(pbTmp, 0, pbR, 0); + if(bXor) XorBlock(pbTmp, 0, pMem, uNext); + +#if ARGON2_B2ROUND_ARRAYS + int[][] vCols = g_vFBCols; + int[][] vRows = g_vFBRows; + for(int i = 0; i < 8; ++i) + Blake2RoundNoMsg(pbR, vCols[i]); + for(int i = 0; i < 8; ++i) + Blake2RoundNoMsg(pbR, vRows[i]); +#else + for(int i = 0; i < (8 * 16); i += 16) + Blake2RoundNoMsgCols16i(pbR, i); + for(int i = 0; i < (8 * 2); i += 2) + Blake2RoundNoMsgRows2i(pbR, i); +#endif + + CopyBlock(pMem, uNext, pbTmp, 0); + XorBlock(pMem, uNext, pbR, 0); + } + + private static byte[] FinalHash(Argon2Ctx ctx, int cbOut, Blake2b h) + { + ulong[] pqBlockHash = new ulong[NbBlockSizeInQW]; + CopyBlock(pqBlockHash, 0, ctx.Mem, (ctx.LaneLength - 1UL) * + NbBlockSizeInQW); + for(ulong l = 1; l < ctx.Lanes; ++l) + XorBlock(pqBlockHash, 0, ctx.Mem, (l * ctx.LaneLength + + ctx.LaneLength - 1UL) * NbBlockSizeInQW); + + byte[] pbBlockHashBytes = new byte[NbBlockSize]; + StoreBlock(pbBlockHashBytes, pqBlockHash); + + byte[] pbOut = new byte[cbOut]; + Blake2bLong(pbOut, cbOut, pbBlockHashBytes, (int)NbBlockSize, h); + + MemUtil.ZeroArray(pqBlockHash); + MemUtil.ZeroByteArray(pbBlockHashBytes); + return pbOut; + } + } +} diff --git a/ModernKeePassLib/Cryptography/KeyDerivation/Argon2Kdf.cs b/ModernKeePassLib/Cryptography/KeyDerivation/Argon2Kdf.cs new file mode 100644 index 0000000..dcb92ee --- /dev/null +++ b/ModernKeePassLib/Cryptography/KeyDerivation/Argon2Kdf.cs @@ -0,0 +1,144 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +namespace ModernKeePassLib.Cryptography.KeyDerivation +{ + public sealed partial class Argon2Kdf : KdfEngine + { + private static readonly PwUuid g_uuid = new PwUuid(new byte[] { + 0xEF, 0x63, 0x6D, 0xDF, 0x8C, 0x29, 0x44, 0x4B, + 0x91, 0xF7, 0xA9, 0xA4, 0x03, 0xE3, 0x0A, 0x0C }); + + public static readonly string ParamSalt = "S"; // Byte[] + public static readonly string ParamParallelism = "P"; // UInt32 + public static readonly string ParamMemory = "M"; // UInt64 + public static readonly string ParamIterations = "I"; // UInt64 + public static readonly string ParamVersion = "V"; // UInt32 + public static readonly string ParamSecretKey = "K"; // Byte[] + public static readonly string ParamAssocData = "A"; // Byte[] + + private const uint MinVersion = 0x10; + private const uint MaxVersion = 0x13; + + private const int MinSalt = 8; + private const int MaxSalt = int.MaxValue; // .NET limit; 2^32 - 1 in spec + + internal const ulong MinIterations = 1; + internal const ulong MaxIterations = uint.MaxValue; + + internal const ulong MinMemory = 1024 * 8; // For parallelism = 1 + // internal const ulong MaxMemory = (ulong)uint.MaxValue * 1024UL; // Spec + internal const ulong MaxMemory = int.MaxValue; // .NET limit + + internal const uint MinParallelism = 1; + internal const uint MaxParallelism = (1 << 24) - 1; + + internal const ulong DefaultIterations = 2; + internal const ulong DefaultMemory = 1024 * 1024; // 1 MB + internal const uint DefaultParallelism = 2; + + public override PwUuid Uuid + { + get { return g_uuid; } + } + + public override string Name + { + get { return "Argon2"; } + } + + public Argon2Kdf() + { + } + + public override KdfParameters GetDefaultParameters() + { + KdfParameters p = base.GetDefaultParameters(); + + p.SetUInt32(ParamVersion, MaxVersion); + + p.SetUInt64(ParamIterations, DefaultIterations); + p.SetUInt64(ParamMemory, DefaultMemory); + p.SetUInt32(ParamParallelism, DefaultParallelism); + + return p; + } + + public override void Randomize(KdfParameters p) + { + if(p == null) { Debug.Assert(false); return; } + Debug.Assert(g_uuid.Equals(p.KdfUuid)); + + byte[] pb = CryptoRandom.Instance.GetRandomBytes(32); + p.SetByteArray(ParamSalt, pb); + } + + public override byte[] Transform(byte[] pbMsg, KdfParameters p) + { + if(pbMsg == null) throw new ArgumentNullException("pbMsg"); + if(p == null) throw new ArgumentNullException("p"); + + byte[] pbSalt = p.GetByteArray(ParamSalt); + if(pbSalt == null) + throw new ArgumentNullException("p.Salt"); + if((pbSalt.Length < MinSalt) || (pbSalt.Length > MaxSalt)) + throw new ArgumentOutOfRangeException("p.Salt"); + + uint uPar = p.GetUInt32(ParamParallelism, 0); + if((uPar < MinParallelism) || (uPar > MaxParallelism)) + throw new ArgumentOutOfRangeException("p.Parallelism"); + + ulong uMem = p.GetUInt64(ParamMemory, 0); + if((uMem < MinMemory) || (uMem > MaxMemory)) + throw new ArgumentOutOfRangeException("p.Memory"); + + ulong uIt = p.GetUInt64(ParamIterations, 0); + if((uIt < MinIterations) || (uIt > MaxIterations)) + throw new ArgumentOutOfRangeException("p.Iterations"); + + uint v = p.GetUInt32(ParamVersion, 0); + if((v < MinVersion) || (v > MaxVersion)) + throw new ArgumentOutOfRangeException("p.Version"); + + byte[] pbSecretKey = p.GetByteArray(ParamSecretKey); + byte[] pbAssocData = p.GetByteArray(ParamAssocData); + + byte[] pbRet = Argon2d(pbMsg, pbSalt, uPar, uMem, uIt, + 32, v, pbSecretKey, pbAssocData); + + if(uMem > (100UL * 1024UL * 1024UL)) GC.Collect(); + return pbRet; + } + + public override KdfParameters GetBestParameters(uint uMilliseconds) + { + KdfParameters p = GetDefaultParameters(); + Randomize(p); + + MaximizeParamUInt64(p, ParamIterations, MinIterations, + MaxIterations, uMilliseconds, true); + return p; + } + } +} diff --git a/ModernKeePassLib/Cryptography/KeyDerivation/KdfEngine.cs b/ModernKeePassLib/Cryptography/KeyDerivation/KdfEngine.cs new file mode 100644 index 0000000..56b5291 --- /dev/null +++ b/ModernKeePassLib/Cryptography/KeyDerivation/KdfEngine.cs @@ -0,0 +1,142 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +namespace ModernKeePassLib.Cryptography.KeyDerivation +{ + public abstract class KdfEngine + { + public abstract PwUuid Uuid + { + get; + } + + public abstract string Name + { + get; + } + + public virtual KdfParameters GetDefaultParameters() + { + return new KdfParameters(this.Uuid); + } + + /// + /// Generate random seeds and store them in . + /// + public virtual void Randomize(KdfParameters p) + { + Debug.Assert(p != null); + Debug.Assert(p.KdfUuid.Equals(this.Uuid)); + } + + public abstract byte[] Transform(byte[] pbMsg, KdfParameters p); + + public virtual KdfParameters GetBestParameters(uint uMilliseconds) + { + throw new NotImplementedException(); + } + + protected void MaximizeParamUInt64(KdfParameters p, string strName, + ulong uMin, ulong uMax, uint uMilliseconds, bool bInterpSearch) + { + if(p == null) { Debug.Assert(false); return; } + if(string.IsNullOrEmpty(strName)) { Debug.Assert(false); return; } + if(uMin > uMax) { Debug.Assert(false); return; } + + if(uMax > (ulong.MaxValue >> 1)) + { + Debug.Assert(false); + uMax = ulong.MaxValue >> 1; + + if(uMin > uMax) { p.SetUInt64(strName, uMin); return; } + } + + byte[] pbMsg = new byte[32]; + for(int i = 0; i < pbMsg.Length; ++i) pbMsg[i] = (byte)i; + + ulong uLow = uMin; + ulong uHigh = uMin + 1UL; + long tLow = 0; + long tHigh = 0; + long tTarget = (long)uMilliseconds; + + // Determine range + while(uHigh <= uMax) + { + p.SetUInt64(strName, uHigh); + + // GC.Collect(); + Stopwatch sw = Stopwatch.StartNew(); + Transform(pbMsg, p); + sw.Stop(); + + tHigh = sw.ElapsedMilliseconds; + if(tHigh > tTarget) break; + + uLow = uHigh; + tLow = tHigh; + uHigh <<= 1; + } + if(uHigh > uMax) { uHigh = uMax; tHigh = 0; } + if(uLow > uHigh) uLow = uHigh; // Skips to end + + // Find optimal number of iterations + while((uHigh - uLow) >= 2UL) + { + ulong u = (uHigh + uLow) >> 1; // Binary search + // Interpolation search, if possible + if(bInterpSearch && (tLow > 0) && (tHigh > tTarget) && + (tLow <= tTarget)) + { + u = uLow + (((uHigh - uLow) * (ulong)(tTarget - tLow)) / + (ulong)(tHigh - tLow)); + if((u >= uLow) && (u <= uHigh)) + { + u = Math.Max(u, uLow + 1UL); + u = Math.Min(u, uHigh - 1UL); + } + else + { + Debug.Assert(false); + u = (uHigh + uLow) >> 1; + } + } + + p.SetUInt64(strName, u); + + // GC.Collect(); + Stopwatch sw = Stopwatch.StartNew(); + Transform(pbMsg, p); + sw.Stop(); + + long t = sw.ElapsedMilliseconds; + if(t == tTarget) { uLow = u; break; } + else if(t > tTarget) { uHigh = u; tHigh = t; } + else { uLow = u; tLow = t; } + } + + p.SetUInt64(strName, uLow); + } + } +} diff --git a/ModernKeePassLib/Cryptography/KeyDerivation/KdfParameters.cs b/ModernKeePassLib/Cryptography/KeyDerivation/KdfParameters.cs new file mode 100644 index 0000000..31d374f --- /dev/null +++ b/ModernKeePassLib/Cryptography/KeyDerivation/KdfParameters.cs @@ -0,0 +1,80 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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 ModernKeePassLib.Collections; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Cryptography.KeyDerivation +{ + public sealed class KdfParameters : VariantDictionary + { + private const string ParamUuid = @"$UUID"; + + private readonly PwUuid m_puKdf; + public PwUuid KdfUuid + { + get { return m_puKdf; } + } + + public KdfParameters(PwUuid puKdf) + { + if(puKdf == null) throw new ArgumentNullException("puKdf"); + + m_puKdf = puKdf; + SetByteArray(ParamUuid, puKdf.UuidBytes); + } + + /// + /// Unsupported. + /// + public override object Clone() + { + throw new NotSupportedException(); + } + + public static byte[] SerializeExt(KdfParameters p) + { + return VariantDictionary.Serialize(p); + } + + public static KdfParameters DeserializeExt(byte[] pb) + { + VariantDictionary d = VariantDictionary.Deserialize(pb); + if(d == null) { Debug.Assert(false); return null; } + + byte[] pbUuid = d.GetByteArray(ParamUuid); + if((pbUuid == null) || (pbUuid.Length != (int)PwUuid.UuidSize)) + { + Debug.Assert(false); + return null; + } + + PwUuid pu = new PwUuid(pbUuid); + KdfParameters p = new KdfParameters(pu); + d.CopyTo(p); + return p; + } + } +} diff --git a/ModernKeePassLib/Cryptography/KeyDerivation/KdfPool.cs b/ModernKeePassLib/Cryptography/KeyDerivation/KdfPool.cs new file mode 100644 index 0000000..cd14869 --- /dev/null +++ b/ModernKeePassLib/Cryptography/KeyDerivation/KdfPool.cs @@ -0,0 +1,96 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Cryptography.KeyDerivation +{ + public static class KdfPool + { + private static List g_l = new List(); + + public static IEnumerable Engines + { + get + { + EnsureInitialized(); + return g_l; + } + } + + private static void EnsureInitialized() + { + if(g_l.Count > 0) return; + + g_l.Add(new AesKdf()); + g_l.Add(new Argon2Kdf()); + } + + internal static KdfParameters GetDefaultParameters() + { + EnsureInitialized(); + return g_l[0].GetDefaultParameters(); + } + + public static KdfEngine Get(PwUuid pu) + { + if(pu == null) { Debug.Assert(false); return null; } + + EnsureInitialized(); + + foreach(KdfEngine kdf in g_l) + { + if(pu.Equals(kdf.Uuid)) return kdf; + } + + return null; + } + + public static KdfEngine Get(string strName) + { + if(string.IsNullOrEmpty(strName)) { Debug.Assert(false); return null; } + + EnsureInitialized(); + + foreach(KdfEngine kdf in g_l) + { + if(strName.Equals(kdf.Name, StrUtil.CaseIgnoreCmp)) return kdf; + } + + return null; + } + + public static void Add(KdfEngine kdf) + { + if(kdf == null) { Debug.Assert(false); return; } + + EnsureInitialized(); + + if(Get(kdf.Uuid) != null) { Debug.Assert(false); return; } + if(Get(kdf.Name) != null) { Debug.Assert(false); return; } + + g_l.Add(kdf); + } + } +} diff --git a/ModernKeePassLib/Cryptography/PasswordGenerator/CharSetBasedGenerator.cs b/ModernKeePassLib/Cryptography/PasswordGenerator/CharSetBasedGenerator.cs new file mode 100644 index 0000000..a50df78 --- /dev/null +++ b/ModernKeePassLib/Cryptography/PasswordGenerator/CharSetBasedGenerator.cs @@ -0,0 +1,64 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +using ModernKeePassLib.Security; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Cryptography.PasswordGenerator +{ + internal static class CharSetBasedGenerator + { + internal static PwgError Generate(out ProtectedString psOut, + PwProfile pwProfile, CryptoRandomStream crsRandomSource) + { + psOut = ProtectedString.Empty; + if(pwProfile.Length == 0) return PwgError.Success; + + PwCharSet pcs = new PwCharSet(pwProfile.CharSet.ToString()); + PwGenerator.PrepareCharSet(pcs, pwProfile); + + char[] v = new char[pwProfile.Length]; + try + { + for(int i = 0; i < v.Length; ++i) + { + char ch = PwGenerator.GenerateCharacter(pwProfile, + pcs, crsRandomSource); + + if(ch == char.MinValue) + return PwgError.TooFewCharacters; + + v[i] = ch; + } + + byte[] pbUtf8 = StrUtil.Utf8.GetBytes(v); + psOut = new ProtectedString(true, pbUtf8); + MemUtil.ZeroByteArray(pbUtf8); + } + finally { MemUtil.ZeroArray(v); } + + return PwgError.Success; + } + } +} diff --git a/ModernKeePassLib/Cryptography/PasswordGenerator/CustomPwGenerator.cs b/ModernKeePassLib/Cryptography/PasswordGenerator/CustomPwGenerator.cs new file mode 100644 index 0000000..15b5860 --- /dev/null +++ b/ModernKeePassLib/Cryptography/PasswordGenerator/CustomPwGenerator.cs @@ -0,0 +1,66 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +using ModernKeePassLib; +using ModernKeePassLib.Security; + +namespace ModernKeePassLib.Cryptography.PasswordGenerator +{ + public abstract class CustomPwGenerator + { + /// + /// Each custom password generation algorithm must have + /// its own unique UUID. + /// + public abstract PwUuid Uuid { get; } + + /// + /// Displayable name of the password generation algorithm. + /// + public abstract string Name { get; } + + public virtual bool SupportsOptions + { + get { return false; } + } + + /// + /// Password generation function. + /// + /// Password generation options chosen + /// by the user. This may be null, if the default + /// options should be used. + /// Source that the algorithm + /// can use to generate random numbers. + /// Generated password or null in case + /// of failure. If returning null, the caller assumes + /// that an error message has already been shown to the user. + public abstract ProtectedString Generate(PwProfile prf, + CryptoRandomStream crsRandomSource); + + public virtual string GetOptions(string strCurrentOptions) + { + return string.Empty; + } + } +} diff --git a/ModernKeePassLib/Cryptography/PasswordGenerator/CustomPwGeneratorPool.cs b/ModernKeePassLib/Cryptography/PasswordGenerator/CustomPwGeneratorPool.cs new file mode 100644 index 0000000..967e224 --- /dev/null +++ b/ModernKeePassLib/Cryptography/PasswordGenerator/CustomPwGeneratorPool.cs @@ -0,0 +1,110 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; +using System.Collections.Generic; +using System.Text; + +namespace ModernKeePassLib.Cryptography.PasswordGenerator +{ + public sealed class CustomPwGeneratorPool : IEnumerable + { + private List m_vGens = new List(); + + public int Count + { + get { return m_vGens.Count; } + } + + public CustomPwGeneratorPool() + { + } + + IEnumerator IEnumerable.GetEnumerator() + { + return m_vGens.GetEnumerator(); + } + + public IEnumerator GetEnumerator() + { + return m_vGens.GetEnumerator(); + } + + public void Add(CustomPwGenerator pwg) + { + if(pwg == null) throw new ArgumentNullException("pwg"); + + PwUuid uuid = pwg.Uuid; + if(uuid == null) throw new ArgumentException(); + + int nIndex = FindIndex(uuid); + + if(nIndex >= 0) m_vGens[nIndex] = pwg; // Replace + else m_vGens.Add(pwg); + } + + public CustomPwGenerator Find(PwUuid uuid) + { + if(uuid == null) throw new ArgumentNullException("uuid"); + + foreach(CustomPwGenerator pwg in m_vGens) + { + if(uuid.Equals(pwg.Uuid)) return pwg; + } + + return null; + } + + public CustomPwGenerator Find(string strName) + { + if(strName == null) throw new ArgumentNullException("strName"); + + foreach(CustomPwGenerator pwg in m_vGens) + { + if(pwg.Name == strName) return pwg; + } + + return null; + } + + private int FindIndex(PwUuid uuid) + { + if(uuid == null) throw new ArgumentNullException("uuid"); + + for(int i = 0; i < m_vGens.Count; ++i) + { + if(uuid.Equals(m_vGens[i].Uuid)) return i; + } + + return -1; + } + + public bool Remove(PwUuid uuid) + { + if(uuid == null) throw new ArgumentNullException("uuid"); + + int nIndex = FindIndex(uuid); + if(nIndex < 0) return false; + + m_vGens.RemoveAt(nIndex); + return true; + } + } +} diff --git a/ModernKeePassLib/Cryptography/PasswordGenerator/PatternBasedGenerator.cs b/ModernKeePassLib/Cryptography/PasswordGenerator/PatternBasedGenerator.cs new file mode 100644 index 0000000..55773e6 --- /dev/null +++ b/ModernKeePassLib/Cryptography/PasswordGenerator/PatternBasedGenerator.cs @@ -0,0 +1,187 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +using ModernKeePassLib.Security; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Cryptography.PasswordGenerator +{ + internal static class PatternBasedGenerator + { + internal static PwgError Generate(out ProtectedString psOut, + PwProfile pwProfile, CryptoRandomStream crsRandomSource) + { + psOut = ProtectedString.Empty; + + LinkedList llGenerated = new LinkedList(); + PwCharSet pcsCurrent = new PwCharSet(); + PwCharSet pcsCustom = new PwCharSet(); + PwCharSet pcsUsed = new PwCharSet(); + bool bInCharSetDef = false; + + string strPattern = ExpandPattern(pwProfile.Pattern); + if(strPattern.Length == 0) return PwgError.Success; + + CharStream csStream = new CharStream(strPattern); + char ch = csStream.ReadChar(); + + while(ch != char.MinValue) + { + pcsCurrent.Clear(); + + bool bGenerateChar = false; + + if(ch == '\\') + { + ch = csStream.ReadChar(); + if(ch == char.MinValue) // Backslash at the end + { + llGenerated.AddLast('\\'); + break; + } + + if(bInCharSetDef) pcsCustom.Add(ch); + else + { + llGenerated.AddLast(ch); + pcsUsed.Add(ch); + } + } + else if(ch == '^') + { + ch = csStream.ReadChar(); + if(ch == char.MinValue) // ^ at the end + { + llGenerated.AddLast('^'); + break; + } + + if(bInCharSetDef) pcsCustom.Remove(ch); + } + else if(ch == '[') + { + pcsCustom.Clear(); + bInCharSetDef = true; + } + else if(ch == ']') + { + pcsCurrent.Add(pcsCustom.ToString()); + + bInCharSetDef = false; + bGenerateChar = true; + } + else if(bInCharSetDef) + { + if(pcsCustom.AddCharSet(ch) == false) + pcsCustom.Add(ch); + } + else if(pcsCurrent.AddCharSet(ch) == false) + { + llGenerated.AddLast(ch); + pcsUsed.Add(ch); + } + else bGenerateChar = true; + + if(bGenerateChar) + { + PwGenerator.PrepareCharSet(pcsCurrent, pwProfile); + + if(pwProfile.NoRepeatingCharacters) + pcsCurrent.Remove(pcsUsed.ToString()); + + char chGen = PwGenerator.GenerateCharacter(pwProfile, + pcsCurrent, crsRandomSource); + + if(chGen == char.MinValue) return PwgError.TooFewCharacters; + + llGenerated.AddLast(chGen); + pcsUsed.Add(chGen); + } + + ch = csStream.ReadChar(); + } + + if(llGenerated.Count == 0) return PwgError.Success; + + char[] v = new char[llGenerated.Count]; + llGenerated.CopyTo(v, 0); + + if(pwProfile.PatternPermutePassword) + PwGenerator.Shuffle(v, crsRandomSource); + + byte[] pbUtf8 = StrUtil.Utf8.GetBytes(v); + psOut = new ProtectedString(true, pbUtf8); + MemUtil.ZeroByteArray(pbUtf8); + + MemUtil.ZeroArray(v); + llGenerated.Clear(); + + return PwgError.Success; + } + + private static string ExpandPattern(string strPattern) + { + if(strPattern == null) { Debug.Assert(false); return string.Empty; } + + string str = strPattern; + + while(true) + { + int nOpen = FindFirstUnescapedChar(str, '{'); + int nClose = FindFirstUnescapedChar(str, '}'); + + if((nOpen >= 0) && (nOpen < nClose)) + { + string strCount = str.Substring(nOpen + 1, nClose - nOpen - 1); + str = str.Remove(nOpen, nClose - nOpen + 1); + + uint uRepeat; + if(StrUtil.TryParseUInt(strCount, out uRepeat) && (nOpen >= 1)) + { + if(uRepeat == 0) + str = str.Remove(nOpen - 1, 1); + else + str = str.Insert(nOpen, new string(str[nOpen - 1], (int)uRepeat - 1)); + } + } + else break; + } + + return str; + } + + private static int FindFirstUnescapedChar(string str, char ch) + { + for(int i = 0; i < str.Length; ++i) + { + char chCur = str[i]; + + if(chCur == '\\') ++i; // Next is escaped, skip it + else if(chCur == ch) return i; + } + + return -1; + } + } +} diff --git a/ModernKeePassLib/Cryptography/PasswordGenerator/PwCharSet.cs b/ModernKeePassLib/Cryptography/PasswordGenerator/PwCharSet.cs new file mode 100644 index 0000000..4774b7f --- /dev/null +++ b/ModernKeePassLib/Cryptography/PasswordGenerator/PwCharSet.cs @@ -0,0 +1,351 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +namespace ModernKeePassLib.Cryptography.PasswordGenerator +{ + public sealed class PwCharSet + { + public static readonly string UpperCase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + public static readonly string LowerCase = "abcdefghijklmnopqrstuvwxyz"; + public static readonly string Digits = "0123456789"; + + public static readonly string UpperConsonants = "BCDFGHJKLMNPQRSTVWXYZ"; + public static readonly string LowerConsonants = "bcdfghjklmnpqrstvwxyz"; + public static readonly string UpperVowels = "AEIOU"; + public static readonly string LowerVowels = "aeiou"; + + public static readonly string Punctuation = @",.;:"; + public static readonly string Brackets = @"[]{}()<>"; + + public static readonly string PrintableAsciiSpecial = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"; + + public static readonly string UpperHex = "0123456789ABCDEF"; + public static readonly string LowerHex = "0123456789abcdef"; + + public static readonly string Invalid = "\t\r\n"; + public static readonly string LookAlike = @"O0l1I|"; + + internal static readonly string MenuAccels = PwCharSet.LowerCase + PwCharSet.Digits; + + private const int CharTabSize = 0x10000 / 8; + + private List m_vChars = new List(); + private byte[] m_vTab = new byte[CharTabSize]; + + private static string m_strHighAnsi = null; + public static string HighAnsiChars + { + get + { + if(m_strHighAnsi == null) { new PwCharSet(); } // Create string + Debug.Assert(m_strHighAnsi != null); + return m_strHighAnsi; + } + } + + private static string m_strSpecial = null; + public static string SpecialChars + { + get + { + if(m_strSpecial == null) { new PwCharSet(); } // Create string + Debug.Assert(m_strSpecial != null); + return m_strSpecial; + } + } + + /// + /// Create a new, empty character set collection object. + /// + public PwCharSet() + { + Initialize(true); + } + + public PwCharSet(string strCharSet) + { + Initialize(true); + Add(strCharSet); + } + + private PwCharSet(bool bFullInitialize) + { + Initialize(bFullInitialize); + } + + private void Initialize(bool bFullInitialize) + { + Clear(); + + if(!bFullInitialize) return; + + if(m_strHighAnsi == null) + { + StringBuilder sbHighAnsi = new StringBuilder(); + // [U+0080, U+009F] are C1 control characters, + // U+00A0 is non-breaking space + for(char ch = '\u00A1'; ch <= '\u00AC'; ++ch) + sbHighAnsi.Append(ch); + // U+00AD is soft hyphen (format character) + for(char ch = '\u00AE'; ch < '\u00FF'; ++ch) + sbHighAnsi.Append(ch); + sbHighAnsi.Append('\u00FF'); + + m_strHighAnsi = sbHighAnsi.ToString(); + } + + if(m_strSpecial == null) + { + PwCharSet pcs = new PwCharSet(false); + pcs.AddRange('!', '/'); + pcs.AddRange(':', '@'); + pcs.AddRange('[', '`'); + pcs.Add(@"|~"); + pcs.Remove(@"-_ "); + pcs.Remove(PwCharSet.Brackets); + + m_strSpecial = pcs.ToString(); + } + } + + /// + /// Number of characters in this set. + /// + public uint Size + { + get { return (uint)m_vChars.Count; } + } + + /// + /// Get a character of the set using an index. + /// + /// Index of the character to get. + /// Character at the specified position. If the index is invalid, + /// an ArgumentOutOfRangeException is thrown. + public char this[uint uPos] + { + get + { + if(uPos >= (uint)m_vChars.Count) + throw new ArgumentOutOfRangeException("uPos"); + + return m_vChars[(int)uPos]; + } + } + + /// + /// Remove all characters from this set. + /// + public void Clear() + { + m_vChars.Clear(); + Array.Clear(m_vTab, 0, m_vTab.Length); + } + + public bool Contains(char ch) + { + return (((m_vTab[ch / 8] >> (ch % 8)) & 1) != char.MinValue); + } + + public bool Contains(string strCharacters) + { + Debug.Assert(strCharacters != null); + if(strCharacters == null) throw new ArgumentNullException("strCharacters"); + + foreach(char ch in strCharacters) + { + if(!Contains(ch)) return false; + } + + return true; + } + + /// + /// Add characters to the set. + /// + /// Character to add. + public void Add(char ch) + { + if(ch == char.MinValue) { Debug.Assert(false); return; } + + if(!Contains(ch)) + { + m_vChars.Add(ch); + m_vTab[ch / 8] |= (byte)(1 << (ch % 8)); + } + } + + /// + /// Add characters to the set. + /// + /// String containing characters to add. + public void Add(string strCharSet) + { + Debug.Assert(strCharSet != null); + if(strCharSet == null) throw new ArgumentNullException("strCharSet"); + + m_vChars.Capacity = m_vChars.Count + strCharSet.Length; + + foreach(char ch in strCharSet) + Add(ch); + } + + public void Add(string strCharSet1, string strCharSet2) + { + Add(strCharSet1); + Add(strCharSet2); + } + + public void Add(string strCharSet1, string strCharSet2, string strCharSet3) + { + Add(strCharSet1); + Add(strCharSet2); + Add(strCharSet3); + } + + public void AddRange(char chMin, char chMax) + { + m_vChars.Capacity = m_vChars.Count + (chMax - chMin) + 1; + + for(char ch = chMin; ch < chMax; ++ch) + Add(ch); + + Add(chMax); + } + + public bool AddCharSet(char chCharSetIdentifier) + { + bool bResult = true; + + switch(chCharSetIdentifier) + { + case 'a': Add(PwCharSet.LowerCase, PwCharSet.Digits); break; + case 'A': Add(PwCharSet.LowerCase, PwCharSet.UpperCase, + PwCharSet.Digits); break; + case 'U': Add(PwCharSet.UpperCase, PwCharSet.Digits); break; + case 'c': Add(PwCharSet.LowerConsonants); break; + case 'C': Add(PwCharSet.LowerConsonants, + PwCharSet.UpperConsonants); break; + case 'z': Add(PwCharSet.UpperConsonants); break; + case 'd': Add(PwCharSet.Digits); break; // Digit + case 'h': Add(PwCharSet.LowerHex); break; + case 'H': Add(PwCharSet.UpperHex); break; + case 'l': Add(PwCharSet.LowerCase); break; + case 'L': Add(PwCharSet.LowerCase, PwCharSet.UpperCase); break; + case 'u': Add(PwCharSet.UpperCase); break; + case 'p': Add(PwCharSet.Punctuation); break; + case 'b': Add(PwCharSet.Brackets); break; + case 's': Add(PwCharSet.PrintableAsciiSpecial); break; + case 'S': Add(PwCharSet.UpperCase, PwCharSet.LowerCase); + Add(PwCharSet.Digits, PwCharSet.PrintableAsciiSpecial); break; + case 'v': Add(PwCharSet.LowerVowels); break; + case 'V': Add(PwCharSet.LowerVowels, PwCharSet.UpperVowels); break; + case 'Z': Add(PwCharSet.UpperVowels); break; + case 'x': Add(m_strHighAnsi); break; + default: bResult = false; break; + } + + return bResult; + } + + public bool Remove(char ch) + { + m_vTab[ch / 8] &= (byte)(~(1 << (ch % 8))); + return m_vChars.Remove(ch); + } + + public bool Remove(string strCharacters) + { + Debug.Assert(strCharacters != null); + if(strCharacters == null) throw new ArgumentNullException("strCharacters"); + + bool bResult = true; + foreach(char ch in strCharacters) + { + if(!Remove(ch)) bResult = false; + } + + return bResult; + } + + public bool RemoveIfAllExist(string strCharacters) + { + Debug.Assert(strCharacters != null); + if(strCharacters == null) throw new ArgumentNullException("strCharacters"); + + if(!Contains(strCharacters)) + return false; + + return Remove(strCharacters); + } + + /// + /// Convert the character set to a string containing all its characters. + /// + /// String containing all character set characters. + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + foreach(char ch in m_vChars) + sb.Append(ch); + + return sb.ToString(); + } + + public string PackAndRemoveCharRanges() + { + StringBuilder sb = new StringBuilder(); + + sb.Append(RemoveIfAllExist(PwCharSet.UpperCase) ? 'U' : '_'); + sb.Append(RemoveIfAllExist(PwCharSet.LowerCase) ? 'L' : '_'); + sb.Append(RemoveIfAllExist(PwCharSet.Digits) ? 'D' : '_'); + sb.Append(RemoveIfAllExist(m_strSpecial) ? 'S' : '_'); + sb.Append(RemoveIfAllExist(PwCharSet.Punctuation) ? 'P' : '_'); + sb.Append(RemoveIfAllExist(@"-") ? 'm' : '_'); + sb.Append(RemoveIfAllExist(@"_") ? 'u' : '_'); + sb.Append(RemoveIfAllExist(@" ") ? 's' : '_'); + sb.Append(RemoveIfAllExist(PwCharSet.Brackets) ? 'B' : '_'); + sb.Append(RemoveIfAllExist(m_strHighAnsi) ? 'H' : '_'); + + return sb.ToString(); + } + + public void UnpackCharRanges(string strRanges) + { + if(strRanges == null) { Debug.Assert(false); return; } + if(strRanges.Length < 10) { Debug.Assert(false); return; } + + if(strRanges[0] != '_') Add(PwCharSet.UpperCase); + if(strRanges[1] != '_') Add(PwCharSet.LowerCase); + if(strRanges[2] != '_') Add(PwCharSet.Digits); + if(strRanges[3] != '_') Add(m_strSpecial); + if(strRanges[4] != '_') Add(PwCharSet.Punctuation); + if(strRanges[5] != '_') Add('-'); + if(strRanges[6] != '_') Add('_'); + if(strRanges[7] != '_') Add(' '); + if(strRanges[8] != '_') Add(PwCharSet.Brackets); + if(strRanges[9] != '_') Add(m_strHighAnsi); + } + } +} diff --git a/ModernKeePassLib/Cryptography/PasswordGenerator/PwGenerator.cs b/ModernKeePassLib/Cryptography/PasswordGenerator/PwGenerator.cs new file mode 100644 index 0000000..472bec0 --- /dev/null +++ b/ModernKeePassLib/Cryptography/PasswordGenerator/PwGenerator.cs @@ -0,0 +1,163 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +#if !KeePassUAP +using System.Security.Cryptography; +#endif + +using ModernKeePassLib.Security; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Cryptography.PasswordGenerator +{ + public enum PwgError + { + Success = 0, + Unknown = 1, + TooFewCharacters = 2, + UnknownAlgorithm = 3 + } + + /// + /// Utility functions for generating random passwords. + /// + public static class PwGenerator + { + public static PwgError Generate(out ProtectedString psOut, + PwProfile pwProfile, byte[] pbUserEntropy, + CustomPwGeneratorPool pwAlgorithmPool) + { + Debug.Assert(pwProfile != null); + if(pwProfile == null) throw new ArgumentNullException("pwProfile"); + + PwgError e = PwgError.Unknown; + CryptoRandomStream crs = null; + byte[] pbKey = null; + try + { + crs = CreateRandomStream(pbUserEntropy, out pbKey); + + if(pwProfile.GeneratorType == PasswordGeneratorType.CharSet) + e = CharSetBasedGenerator.Generate(out psOut, pwProfile, crs); + else if(pwProfile.GeneratorType == PasswordGeneratorType.Pattern) + e = PatternBasedGenerator.Generate(out psOut, pwProfile, crs); + else if(pwProfile.GeneratorType == PasswordGeneratorType.Custom) + e = GenerateCustom(out psOut, pwProfile, crs, pwAlgorithmPool); + else { Debug.Assert(false); psOut = ProtectedString.Empty; } + } + finally + { + if(crs != null) crs.Dispose(); + if(pbKey != null) MemUtil.ZeroByteArray(pbKey); + } + + return e; + } + + private static CryptoRandomStream CreateRandomStream(byte[] pbAdditionalEntropy, + out byte[] pbKey) + { + pbKey = CryptoRandom.Instance.GetRandomBytes(128); + + // Mix in additional entropy + Debug.Assert(pbKey.Length >= 64); + if((pbAdditionalEntropy != null) && (pbAdditionalEntropy.Length > 0)) + { + using(SHA512Managed h = new SHA512Managed()) + { + byte[] pbHash = h.ComputeHash(pbAdditionalEntropy); + MemUtil.XorArray(pbHash, 0, pbKey, 0, pbHash.Length); + } + } + + return new CryptoRandomStream(CrsAlgorithm.ChaCha20, pbKey); + } + + internal static char GenerateCharacter(PwProfile pwProfile, + PwCharSet pwCharSet, CryptoRandomStream crsRandomSource) + { + if(pwCharSet.Size == 0) return char.MinValue; + + ulong uIndex = crsRandomSource.GetRandomUInt64(); + uIndex %= (ulong)pwCharSet.Size; + + char ch = pwCharSet[(uint)uIndex]; + + if(pwProfile.NoRepeatingCharacters) + pwCharSet.Remove(ch); + + return ch; + } + + internal static void PrepareCharSet(PwCharSet pwCharSet, PwProfile pwProfile) + { + pwCharSet.Remove(PwCharSet.Invalid); + + if(pwProfile.ExcludeLookAlike) pwCharSet.Remove(PwCharSet.LookAlike); + + if(pwProfile.ExcludeCharacters.Length > 0) + pwCharSet.Remove(pwProfile.ExcludeCharacters); + } + + internal static void Shuffle(char[] v, CryptoRandomStream crsRandomSource) + { + if(v == null) { Debug.Assert(false); return; } + if(crsRandomSource == null) { Debug.Assert(false); return; } + + for(int i = v.Length - 1; i >= 1; --i) + { + ulong r = crsRandomSource.GetRandomUInt64(); + int j = (int)(r % (ulong)(i + 1)); + + char t = v[i]; + v[i] = v[j]; + v[j] = t; + } + } + + private static PwgError GenerateCustom(out ProtectedString psOut, + PwProfile pwProfile, CryptoRandomStream crs, + CustomPwGeneratorPool pwAlgorithmPool) + { + psOut = ProtectedString.Empty; + + Debug.Assert(pwProfile.GeneratorType == PasswordGeneratorType.Custom); + if(pwAlgorithmPool == null) return PwgError.UnknownAlgorithm; + + string strID = pwProfile.CustomAlgorithmUuid; + if(string.IsNullOrEmpty(strID)) return PwgError.UnknownAlgorithm; + + byte[] pbUuid = Convert.FromBase64String(strID); + PwUuid uuid = new PwUuid(pbUuid); + CustomPwGenerator pwg = pwAlgorithmPool.Find(uuid); + if(pwg == null) { Debug.Assert(false); return PwgError.UnknownAlgorithm; } + + ProtectedString pwd = pwg.Generate(pwProfile.CloneDeep(), crs); + if(pwd == null) return PwgError.Unknown; + + psOut = pwd; + return PwgError.Success; + } + } +} diff --git a/ModernKeePassLib/Cryptography/PasswordGenerator/PwProfile.cs b/ModernKeePassLib/Cryptography/PasswordGenerator/PwProfile.cs new file mode 100644 index 0000000..3122d49 --- /dev/null +++ b/ModernKeePassLib/Cryptography/PasswordGenerator/PwProfile.cs @@ -0,0 +1,276 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; +using System.Xml.Serialization; +using System.ComponentModel; +using System.Diagnostics; + +using ModernKeePassLib.Interfaces; +using ModernKeePassLib.Security; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Cryptography.PasswordGenerator +{ + /// + /// Type of the password generator. Different types like generators + /// based on given patterns, based on character sets, etc. are + /// available. + /// + public enum PasswordGeneratorType + { + /// + /// Generator based on character spaces/sets, i.e. groups + /// of characters like lower-case, upper-case or numeric characters. + /// + CharSet = 0, + + /// + /// Password generation based on a pattern. The user has provided + /// a pattern, which describes how the generated password has to + /// look like. + /// + Pattern = 1, + + Custom = 2 + } + + public sealed class PwProfile : IDeepCloneable + { + private string m_strName = string.Empty; + [DefaultValue("")] + public string Name + { + get { return m_strName; } + set { m_strName = value; } + } + + private PasswordGeneratorType m_type = PasswordGeneratorType.CharSet; + public PasswordGeneratorType GeneratorType + { + get { return m_type; } + set { m_type = value; } + } + + private bool m_bUserEntropy = false; + [DefaultValue(false)] + public bool CollectUserEntropy + { + get { return m_bUserEntropy; } + set { m_bUserEntropy = value; } + } + + private uint m_uLength = 20; + public uint Length + { + get { return m_uLength; } + set { m_uLength = value; } + } + + private PwCharSet m_pwCharSet = new PwCharSet(PwCharSet.UpperCase + + PwCharSet.LowerCase + PwCharSet.Digits); + [XmlIgnore] + public PwCharSet CharSet + { + get { return m_pwCharSet; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_pwCharSet = value; + } + } + + private string m_strCharSetRanges = string.Empty; + [DefaultValue("")] + public string CharSetRanges + { + get { this.UpdateCharSet(true); return m_strCharSetRanges; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strCharSetRanges = value; + this.UpdateCharSet(false); + } + } + + private string m_strCharSetAdditional = string.Empty; + [DefaultValue("")] + public string CharSetAdditional + { + get { this.UpdateCharSet(true); return m_strCharSetAdditional; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strCharSetAdditional = value; + this.UpdateCharSet(false); + } + } + + private string m_strPattern = string.Empty; + [DefaultValue("")] + public string Pattern + { + get { return m_strPattern; } + set { m_strPattern = value; } + } + + private bool m_bPatternPermute = false; + [DefaultValue(false)] + public bool PatternPermutePassword + { + get { return m_bPatternPermute; } + set { m_bPatternPermute = value; } + } + + private bool m_bNoLookAlike = false; + [DefaultValue(false)] + public bool ExcludeLookAlike + { + get { return m_bNoLookAlike; } + set { m_bNoLookAlike = value; } + } + + private bool m_bNoRepeat = false; + [DefaultValue(false)] + public bool NoRepeatingCharacters + { + get { return m_bNoRepeat; } + set { m_bNoRepeat = value; } + } + + private string m_strExclude = string.Empty; + [DefaultValue("")] + public string ExcludeCharacters + { + get { return m_strExclude; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strExclude = value; + } + } + + private string m_strCustomID = string.Empty; + [DefaultValue("")] + public string CustomAlgorithmUuid + { + get { return m_strCustomID; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strCustomID = value; + } + } + + private string m_strCustomOpt = string.Empty; + [DefaultValue("")] + public string CustomAlgorithmOptions + { + get { return m_strCustomOpt; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strCustomOpt = value; + } + } + + public PwProfile() + { + } + + public PwProfile CloneDeep() + { + PwProfile p = new PwProfile(); + + p.m_strName = m_strName; + p.m_type = m_type; + p.m_bUserEntropy = m_bUserEntropy; + p.m_uLength = m_uLength; + p.m_pwCharSet = new PwCharSet(m_pwCharSet.ToString()); + p.m_strCharSetRanges = m_strCharSetRanges; + p.m_strCharSetAdditional = m_strCharSetAdditional; + p.m_strPattern = m_strPattern; + p.m_bPatternPermute = m_bPatternPermute; + p.m_bNoLookAlike = m_bNoLookAlike; + p.m_bNoRepeat = m_bNoRepeat; + p.m_strExclude = m_strExclude; + p.m_strCustomID = m_strCustomID; + p.m_strCustomOpt = m_strCustomOpt; + + return p; + } + + private void UpdateCharSet(bool bSetXml) + { + if(bSetXml) + { + PwCharSet pcs = new PwCharSet(m_pwCharSet.ToString()); + m_strCharSetRanges = pcs.PackAndRemoveCharRanges(); + m_strCharSetAdditional = pcs.ToString(); + } + else + { + PwCharSet pcs = new PwCharSet(m_strCharSetAdditional); + pcs.UnpackCharRanges(m_strCharSetRanges); + m_pwCharSet = pcs; + } + } + + public static PwProfile DeriveFromPassword(ProtectedString psPassword) + { + PwProfile pp = new PwProfile(); + Debug.Assert(psPassword != null); if(psPassword == null) return pp; + + char[] vChars = psPassword.ReadChars(); + + pp.GeneratorType = PasswordGeneratorType.CharSet; + pp.Length = (uint)vChars.Length; + + PwCharSet pcs = pp.CharSet; + pcs.Clear(); + + foreach(char ch in vChars) + { + if((ch >= 'A') && (ch <= 'Z')) pcs.Add(PwCharSet.UpperCase); + else if((ch >= 'a') && (ch <= 'z')) pcs.Add(PwCharSet.LowerCase); + else if((ch >= '0') && (ch <= '9')) pcs.Add(PwCharSet.Digits); + else if(PwCharSet.SpecialChars.IndexOf(ch) >= 0) + pcs.Add(PwCharSet.SpecialChars); + else if(ch == ' ') pcs.Add(' '); + else if(ch == '-') pcs.Add('-'); + else if(ch == '_') pcs.Add('_'); + else if(PwCharSet.Brackets.IndexOf(ch) >= 0) + pcs.Add(PwCharSet.Brackets); + else if(PwCharSet.HighAnsiChars.IndexOf(ch) >= 0) + pcs.Add(PwCharSet.HighAnsiChars); + else pcs.Add(ch); + } + + MemUtil.ZeroArray(vChars); + return pp; + } + + public bool HasSecurityReducingOption() + { + return (m_bNoLookAlike || m_bNoRepeat || (m_strExclude.Length > 0)); + } + } +} diff --git a/ModernKeePassLib/Cryptography/PopularPasswords.cs b/ModernKeePassLib/Cryptography/PopularPasswords.cs new file mode 100644 index 0000000..48b38a4 --- /dev/null +++ b/ModernKeePassLib/Cryptography/PopularPasswords.cs @@ -0,0 +1,133 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Cryptography +{ + public static class PopularPasswords + { + private static Dictionary> m_dicts = + new Dictionary>(); + + internal static int MaxLength + { + get + { + Debug.Assert(m_dicts.Count > 0); // Should be initialized + + int iMaxLen = 0; + foreach(int iLen in m_dicts.Keys) + { + if(iLen > iMaxLen) iMaxLen = iLen; + } + + return iMaxLen; + } + } + + internal static bool ContainsLength(int nLength) + { + Dictionary dDummy; + return m_dicts.TryGetValue(nLength, out dDummy); + } + + public static bool IsPopularPassword(char[] vPassword) + { + ulong uDummy; + return IsPopularPassword(vPassword, out uDummy); + } + + public static bool IsPopularPassword(char[] vPassword, out ulong uDictSize) + { + if(vPassword == null) throw new ArgumentNullException("vPassword"); + if(vPassword.Length == 0) { uDictSize = 0; return false; } + +#if DEBUG + Array.ForEach(vPassword, ch => Debug.Assert(ch == char.ToLower(ch))); +#endif + + try { return IsPopularPasswordPriv(vPassword, out uDictSize); } + catch(Exception) { Debug.Assert(false); } + + uDictSize = 0; + return false; + } + + private static bool IsPopularPasswordPriv(char[] vPassword, out ulong uDictSize) + { + Debug.Assert(m_dicts.Count > 0); // Should be initialized with data + + Dictionary d; + if(!m_dicts.TryGetValue(vPassword.Length, out d)) + { + uDictSize = 0; + return false; + } + + uDictSize = (ulong)d.Count; + return d.ContainsKey(vPassword); + } + + public static void Add(byte[] pbData, bool bGZipped) + { + try + { + if(bGZipped) + pbData = MemUtil.Decompress(pbData); + + string strData = StrUtil.Utf8.GetString(pbData, 0, pbData.Length); + if(string.IsNullOrEmpty(strData)) { Debug.Assert(false); return; } + + StringBuilder sb = new StringBuilder(); + for(int i = 0; i <= strData.Length; ++i) + { + char ch = ((i == strData.Length) ? ' ' : strData[i]); + + if(char.IsWhiteSpace(ch)) + { + int cc = sb.Length; + if(cc > 0) + { + char[] vWord = new char[cc]; + sb.CopyTo(0, vWord, 0, cc); + + Dictionary d; + if(!m_dicts.TryGetValue(cc, out d)) + { + d = new Dictionary(MemUtil.ArrayHelperExOfChar); + m_dicts[cc] = d; + } + + d[vWord] = true; + sb.Remove(0, cc); + } + } + else sb.Append(char.ToLower(ch)); + } + } + catch(Exception) { Debug.Assert(false); } + } + } +} diff --git a/ModernKeePassLib/Cryptography/ProtectedData.cs b/ModernKeePassLib/Cryptography/ProtectedData.cs new file mode 100644 index 0000000..c82a7d6 --- /dev/null +++ b/ModernKeePassLib/Cryptography/ProtectedData.cs @@ -0,0 +1,19 @@ +using System; +using System.Security.Cryptography; +using ModernKeePassLib.Native; + +namespace ModernKeePassLib.Cryptography +{ + public static class ProtectedData + { + public static byte[] Unprotect(byte[] pbEnc, byte[] mPbOptEnt, DataProtectionScope currentUser) + { + throw new NotImplementedException(); + } + + public static byte[] Protect(byte[] pbPlain, byte[] mPbOptEnt, DataProtectionScope currentUser) + { + throw new NotImplementedException(); + } + } +} diff --git a/ModernKeePassLib/Cryptography/QualityEstimation.cs b/ModernKeePassLib/Cryptography/QualityEstimation.cs new file mode 100644 index 0000000..5aa89b9 --- /dev/null +++ b/ModernKeePassLib/Cryptography/QualityEstimation.cs @@ -0,0 +1,779 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +using ModernKeePassLib.Cryptography.PasswordGenerator; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Cryptography +{ + /// + /// A class that offers static functions to estimate the quality of + /// passwords. + /// + public static class QualityEstimation + { + private static class PatternID + { + internal const char LowerAlpha = 'L'; + internal const char UpperAlpha = 'U'; + internal const char Digit = 'D'; + internal const char Special = 'S'; + internal const char High = 'H'; + internal const char Other = 'X'; + + internal const char Dictionary = 'W'; + internal const char Repetition = 'R'; + internal const char Number = 'N'; + internal const char DiffSeq = 'C'; + + internal const string All = "LUDSHXWRNC"; + } + + // private static class CharDistrib + // { + // public static readonly ulong[] LowerAlpha = new ulong[26] { + // 884, 211, 262, 249, 722, 98, 172, 234, 556, 124, 201, 447, 321, + // 483, 518, 167, 18, 458, 416, 344, 231, 105, 80, 48, 238, 76 + // }; + // public static readonly ulong[] UpperAlpha = new ulong[26] { + // 605, 188, 209, 200, 460, 81, 130, 163, 357, 122, 144, 332, 260, + // 317, 330, 132, 18, 320, 315, 250, 137, 76, 60, 36, 161, 54 + // }; + // public static readonly ulong[] Digit = new ulong[10] { + // 574, 673, 524, 377, 339, 336, 312, 310, 357, 386 + // }; + // } + + private sealed class QeCharType + { + private readonly char m_chTypeID; + public char TypeID { get { return m_chTypeID; } } + + private readonly string m_strAlph; + public string Alphabet { get { return m_strAlph; } } + + private readonly int m_nChars; + public int CharCount { get { return m_nChars; } } + + private readonly char m_chFirst; + private readonly char m_chLast; + + private readonly double m_dblCharSize; + public double CharSize { get { return m_dblCharSize; } } + + public QeCharType(char chTypeID, string strAlphabet, bool bIsConsecutive) + { + if(strAlphabet == null) throw new ArgumentNullException(); + if(strAlphabet.Length == 0) throw new ArgumentException(); + + m_chTypeID = chTypeID; + m_strAlph = strAlphabet; + m_nChars = m_strAlph.Length; + m_chFirst = (bIsConsecutive ? m_strAlph[0] : char.MinValue); + m_chLast = (bIsConsecutive ? m_strAlph[m_nChars - 1] : char.MinValue); + + m_dblCharSize = Log2(m_nChars); + + Debug.Assert(((int)(m_chLast - m_chFirst) == (m_nChars - 1)) || + !bIsConsecutive); + } + + public QeCharType(char chTypeID, int nChars) // Catch-none set + { + if(nChars <= 0) throw new ArgumentOutOfRangeException(); + + m_chTypeID = chTypeID; + m_strAlph = string.Empty; + m_nChars = nChars; + m_chFirst = char.MinValue; + m_chLast = char.MinValue; + + m_dblCharSize = Log2(m_nChars); + } + + public bool Contains(char ch) + { + if(m_chLast != char.MinValue) + return ((ch >= m_chFirst) && (ch <= m_chLast)); + + Debug.Assert(m_strAlph.Length > 0); // Don't call for catch-none set + return (m_strAlph.IndexOf(ch) >= 0); + } + } + + private sealed class EntropyEncoder + { + private readonly string m_strAlph; + private Dictionary m_dHisto = new Dictionary(); + private readonly ulong m_uBaseWeight; + private readonly ulong m_uCharWeight; + private readonly ulong m_uOccExclThreshold; + + public EntropyEncoder(string strAlphabet, ulong uBaseWeight, + ulong uCharWeight, ulong uOccExclThreshold) + { + if(strAlphabet == null) throw new ArgumentNullException(); + if(strAlphabet.Length == 0) throw new ArgumentException(); + + m_strAlph = strAlphabet; + m_uBaseWeight = uBaseWeight; + m_uCharWeight = uCharWeight; + m_uOccExclThreshold = uOccExclThreshold; + +#if DEBUG + Dictionary d = new Dictionary(); + foreach(char ch in m_strAlph) { d[ch] = true; } + Debug.Assert(d.Count == m_strAlph.Length); // No duplicates +#endif + } + + public void Reset() + { + m_dHisto.Clear(); + } + + public void Write(char ch) + { + Debug.Assert(m_strAlph.IndexOf(ch) >= 0); + + ulong uOcc; + m_dHisto.TryGetValue(ch, out uOcc); + Debug.Assert(m_dHisto.ContainsKey(ch) || (uOcc == 0)); + m_dHisto[ch] = uOcc + 1; + } + + public double GetOutputSize() + { + ulong uTotalWeight = m_uBaseWeight * (ulong)m_strAlph.Length; + foreach(ulong u in m_dHisto.Values) + { + Debug.Assert(u >= 1); + if(u > m_uOccExclThreshold) + uTotalWeight += (u - m_uOccExclThreshold) * m_uCharWeight; + } + + double dSize = 0.0, dTotalWeight = (double)uTotalWeight; + foreach(ulong u in m_dHisto.Values) + { + ulong uWeight = m_uBaseWeight; + if(u > m_uOccExclThreshold) + uWeight += (u - m_uOccExclThreshold) * m_uCharWeight; + + dSize -= (double)u * Log2((double)uWeight / dTotalWeight); + } + + return dSize; + } + } + + private sealed class MultiEntropyEncoder + { + private Dictionary m_dEncs = + new Dictionary(); + + public MultiEntropyEncoder() + { + } + + public void AddEncoder(char chTypeID, EntropyEncoder ec) + { + if(ec == null) { Debug.Assert(false); return; } + + Debug.Assert(!m_dEncs.ContainsKey(chTypeID)); + m_dEncs[chTypeID] = ec; + } + + public void Reset() + { + foreach(EntropyEncoder ec in m_dEncs.Values) { ec.Reset(); } + } + + public bool Write(char chTypeID, char chData) + { + EntropyEncoder ec; + if(!m_dEncs.TryGetValue(chTypeID, out ec)) + return false; + + ec.Write(chData); + return true; + } + + public double GetOutputSize() + { + double d = 0.0; + + foreach(EntropyEncoder ec in m_dEncs.Values) + { + d += ec.GetOutputSize(); + } + + return d; + } + } + + private sealed class QePatternInstance + { + private readonly int m_iPos; + public int Position { get { return m_iPos; } } + + private readonly int m_nLen; + public int Length { get { return m_nLen; } } + + private readonly char m_chPatternID; + public char PatternID { get { return m_chPatternID; } } + + private readonly double m_dblCost; + public double Cost { get { return m_dblCost; } } + + private readonly QeCharType m_ctSingle; + public QeCharType SingleCharType { get { return m_ctSingle; } } + + public QePatternInstance(int iPosition, int nLength, char chPatternID, + double dblCost) + { + m_iPos = iPosition; + m_nLen = nLength; + m_chPatternID = chPatternID; + m_dblCost = dblCost; + m_ctSingle = null; + } + + public QePatternInstance(int iPosition, int nLength, QeCharType ctSingle) + { + m_iPos = iPosition; + m_nLen = nLength; + m_chPatternID = ctSingle.TypeID; + m_dblCost = ctSingle.CharSize; + m_ctSingle = ctSingle; + } + } + + private sealed class QePathState + { + public readonly int Position; + public readonly List Path; + + public QePathState(int iPosition, List lPath) + { + this.Position = iPosition; + this.Path = lPath; + } + } + + private static readonly object m_objSyncInit = new object(); + private static List m_lCharTypes = null; + + private static void EnsureInitialized() + { + lock(m_objSyncInit) + { + if(m_lCharTypes == null) + { + string strSpecial = PwCharSet.PrintableAsciiSpecial; + if(strSpecial.IndexOf(' ') >= 0) { Debug.Assert(false); } + else strSpecial = strSpecial + " "; + + int nSp = strSpecial.Length; + int nHi = PwCharSet.HighAnsiChars.Length; + + m_lCharTypes = new List(); + + m_lCharTypes.Add(new QeCharType(PatternID.LowerAlpha, + PwCharSet.LowerCase, true)); + m_lCharTypes.Add(new QeCharType(PatternID.UpperAlpha, + PwCharSet.UpperCase, true)); + m_lCharTypes.Add(new QeCharType(PatternID.Digit, + PwCharSet.Digits, true)); + m_lCharTypes.Add(new QeCharType(PatternID.Special, + strSpecial, false)); + m_lCharTypes.Add(new QeCharType(PatternID.High, + PwCharSet.HighAnsiChars, false)); + m_lCharTypes.Add(new QeCharType(PatternID.Other, + 0x10000 - (2 * 26) - 10 - nSp - nHi)); + } + } + } + + /// + /// Estimate the quality of a password. + /// + /// Password to check. + /// Estimated bit-strength of the password. + public static uint EstimatePasswordBits(char[] vPassword) + { + if(vPassword == null) { Debug.Assert(false); return 0; } + if(vPassword.Length == 0) return 0; + + EnsureInitialized(); + + int n = vPassword.Length; + List[] vPatterns = new List[n]; + for(int i = 0; i < n; ++i) + { + vPatterns[i] = new List(); + + QePatternInstance piChar = new QePatternInstance(i, 1, + GetCharType(vPassword[i])); + vPatterns[i].Add(piChar); + } + + FindRepetitions(vPassword, vPatterns); + FindNumbers(vPassword, vPatterns); + FindDiffSeqs(vPassword, vPatterns); + FindPopularPasswords(vPassword, vPatterns); + + // Encoders must not be static, because the entropy estimation + // may run concurrently in multiple threads and the encoders are + // not read-only + EntropyEncoder ecPattern = new EntropyEncoder(PatternID.All, 0, 1, 0); + MultiEntropyEncoder mcData = new MultiEntropyEncoder(); + for(int i = 0; i < (m_lCharTypes.Count - 1); ++i) + { + // Let m be the alphabet size. In order to ensure that two same + // characters cost at least as much as a single character, for + // the probability p and weight w of the character it must hold: + // -log(1/m) >= -2*log(p) + // <=> log(1/m) <= log(p^2) <=> 1/m <= p^2 <=> p >= sqrt(1/m); + // sqrt(1/m) = (1+w)/(m+w) + // <=> m+w = (1+w)*sqrt(m) <=> m+w = sqrt(m) + w*sqrt(m) + // <=> w*(1-sqrt(m)) = sqrt(m) - m <=> w = (sqrt(m)-m)/(1-sqrt(m)) + // <=> w = (sqrt(m)-m)*(1+sqrt(m))/(1-m) + // <=> w = (sqrt(m)-m+m-m*sqrt(m))/(1-m) <=> w = sqrt(m) + ulong uw = (ulong)Math.Sqrt((double)m_lCharTypes[i].CharCount); + + mcData.AddEncoder(m_lCharTypes[i].TypeID, new EntropyEncoder( + m_lCharTypes[i].Alphabet, 1, uw, 1)); + } + + double dblMinCost = (double)int.MaxValue; + int tStart = Environment.TickCount; + + Stack sRec = new Stack(); + sRec.Push(new QePathState(0, new List())); + while(sRec.Count > 0) + { + int tDiff = Environment.TickCount - tStart; + if(tDiff > 500) break; + + QePathState s = sRec.Pop(); + + if(s.Position >= n) + { + Debug.Assert(s.Position == n); + + double dblCost = ComputePathCost(s.Path, vPassword, + ecPattern, mcData); + if(dblCost < dblMinCost) dblMinCost = dblCost; + } + else + { + List lSubs = vPatterns[s.Position]; + for(int i = lSubs.Count - 1; i >= 0; --i) + { + QePatternInstance pi = lSubs[i]; + Debug.Assert(pi.Position == s.Position); + Debug.Assert(pi.Length >= 1); + + List lNewPath = + new List(s.Path.Count + 1); + lNewPath.AddRange(s.Path); + lNewPath.Add(pi); + Debug.Assert(lNewPath.Capacity == (s.Path.Count + 1)); + + QePathState sNew = new QePathState(s.Position + + pi.Length, lNewPath); + sRec.Push(sNew); + } + } + } + + return (uint)Math.Ceiling(dblMinCost); + } + + /// + /// Estimate the quality of a password. + /// + /// Password to check, UTF-8 encoded. + /// Estimated bit-strength of the password. + public static uint EstimatePasswordBits(byte[] pbUnprotectedUtf8) + { + if(pbUnprotectedUtf8 == null) { Debug.Assert(false); return 0; } + + char[] v = StrUtil.Utf8.GetChars(pbUnprotectedUtf8); + uint r; + try { r = EstimatePasswordBits(v); } + finally { MemUtil.ZeroArray(v); } + + return r; + } + + private static QeCharType GetCharType(char ch) + { + int nTypes = m_lCharTypes.Count; + Debug.Assert((nTypes > 0) && (m_lCharTypes[nTypes - 1].CharCount > 256)); + + for(int i = 0; i < (nTypes - 1); ++i) + { + if(m_lCharTypes[i].Contains(ch)) + return m_lCharTypes[i]; + } + + return m_lCharTypes[nTypes - 1]; + } + + private static double ComputePathCost(List l, + char[] vPassword, EntropyEncoder ecPattern, MultiEntropyEncoder mcData) + { + ecPattern.Reset(); + for(int i = 0; i < l.Count; ++i) + ecPattern.Write(l[i].PatternID); + double dblPatternCost = ecPattern.GetOutputSize(); + + mcData.Reset(); + double dblDataCost = 0.0; + foreach(QePatternInstance pi in l) + { + QeCharType tChar = pi.SingleCharType; + if(tChar != null) + { + char ch = vPassword[pi.Position]; + if(!mcData.Write(tChar.TypeID, ch)) + dblDataCost += pi.Cost; + } + else dblDataCost += pi.Cost; + } + dblDataCost += mcData.GetOutputSize(); + + return (dblPatternCost + dblDataCost); + } + + private static void FindPopularPasswords(char[] vPassword, + List[] vPatterns) + { + int n = vPassword.Length; + + char[] vLower = new char[n]; + char[] vLeet = new char[n]; + for(int i = 0; i < n; ++i) + { + char ch = vPassword[i]; + + vLower[i] = char.ToLower(ch); + vLeet[i] = char.ToLower(DecodeLeetChar(ch)); + } + + char chErased = default(char); // The value that Array.Clear uses + Debug.Assert(chErased == char.MinValue); + + int nMaxLen = Math.Min(n, PopularPasswords.MaxLength); + for(int nSubLen = nMaxLen; nSubLen >= 3; --nSubLen) + { + if(!PopularPasswords.ContainsLength(nSubLen)) continue; + + char[] vSub = new char[nSubLen]; + + for(int i = 0; i <= (n - nSubLen); ++i) + { + if(Array.IndexOf(vLower, chErased, i, nSubLen) >= 0) + continue; + + Array.Copy(vLower, i, vSub, 0, nSubLen); + if(!EvalAddPopularPasswordPattern(vPatterns, vPassword, + i, vSub, 0.0)) + { + Array.Copy(vLeet, i, vSub, 0, nSubLen); + if(EvalAddPopularPasswordPattern(vPatterns, vPassword, + i, vSub, 1.5)) + { + Array.Clear(vLower, i, nSubLen); // Not vLeet + Debug.Assert(vLower[i] == chErased); + } + } + else + { + Array.Clear(vLower, i, nSubLen); + Debug.Assert(vLower[i] == chErased); + } + } + + MemUtil.ZeroArray(vSub); + } + + MemUtil.ZeroArray(vLower); + MemUtil.ZeroArray(vLeet); + } + + private static bool EvalAddPopularPasswordPattern(List[] vPatterns, + char[] vPassword, int i, char[] vSub, double dblCostPerMod) + { + ulong uDictSize; + if(!PopularPasswords.IsPopularPassword(vSub, out uDictSize)) + return false; + + int n = vSub.Length; + int d = HammingDist(vSub, 0, vPassword, i, n); + + double dblCost = Log2((double)uDictSize); + + // dblCost += log2(n binom d) + int k = Math.Min(d, n - d); + for(int j = n; j > (n - k); --j) + dblCost += Log2(j); + for(int j = k; j >= 2; --j) + dblCost -= Log2(j); + + dblCost += dblCostPerMod * (double)d; + + vPatterns[i].Add(new QePatternInstance(i, n, PatternID.Dictionary, + dblCost)); + return true; + } + + private static char DecodeLeetChar(char chLeet) + { + if((chLeet >= '\u00C0') && (chLeet <= '\u00C6')) return 'a'; + if((chLeet >= '\u00C8') && (chLeet <= '\u00CB')) return 'e'; + if((chLeet >= '\u00CC') && (chLeet <= '\u00CF')) return 'i'; + if((chLeet >= '\u00D2') && (chLeet <= '\u00D6')) return 'o'; + if((chLeet >= '\u00D9') && (chLeet <= '\u00DC')) return 'u'; + if((chLeet >= '\u00E0') && (chLeet <= '\u00E6')) return 'a'; + if((chLeet >= '\u00E8') && (chLeet <= '\u00EB')) return 'e'; + if((chLeet >= '\u00EC') && (chLeet <= '\u00EF')) return 'i'; + if((chLeet >= '\u00F2') && (chLeet <= '\u00F6')) return 'o'; + if((chLeet >= '\u00F9') && (chLeet <= '\u00FC')) return 'u'; + + char ch; + switch(chLeet) + { + case '4': + case '@': + case '?': + case '^': + case '\u00AA': ch = 'a'; break; + case '8': + case '\u00DF': ch = 'b'; break; + case '(': + case '{': + case '[': + case '<': + case '\u00A2': + case '\u00A9': + case '\u00C7': + case '\u00E7': ch = 'c'; break; + case '\u00D0': + case '\u00F0': ch = 'd'; break; + case '3': + case '\u20AC': + case '&': + case '\u00A3': ch = 'e'; break; + case '6': + case '9': ch = 'g'; break; + case '#': ch = 'h'; break; + case '1': + case '!': + case '|': + case '\u00A1': + case '\u00A6': ch = 'i'; break; + case '\u00D1': + case '\u00F1': ch = 'n'; break; + case '0': + case '*': + case '\u00A4': // Currency + case '\u00B0': // Degree + case '\u00D8': + case '\u00F8': ch = 'o'; break; + case '\u00AE': ch = 'r'; break; + case '$': + case '5': + case '\u00A7': ch = 's'; break; + case '+': + case '7': ch = 't'; break; + case '\u00B5': ch = 'u'; break; + case '%': + case '\u00D7': ch = 'x'; break; + case '\u00A5': + case '\u00DD': + case '\u00FD': + case '\u00FF': ch = 'y'; break; + case '2': ch = 'z'; break; + default: ch = chLeet; break; + } + + return ch; + } + + private static int HammingDist(char[] v1, int iOffset1, + char[] v2, int iOffset2, int nLength) + { + int nDist = 0; + for(int i = 0; i < nLength; ++i) + { + if(v1[iOffset1 + i] != v2[iOffset2 + i]) ++nDist; + } + + return nDist; + } + + private static void FindRepetitions(char[] vPassword, + List[] vPatterns) + { + int n = vPassword.Length; + char[] v = new char[n]; + Array.Copy(vPassword, v, n); + + char chErased = char.MaxValue; + for(int m = (n / 2); m >= 3; --m) + { + for(int x1 = 0; x1 <= (n - (2 * m)); ++x1) + { + bool bFoundRep = false; + + for(int x2 = (x1 + m); x2 <= (n - m); ++x2) + { + if(PartsEqual(v, x1, x2, m)) + { + double dblCost = Log2(x1 + 1) + Log2(m); + vPatterns[x2].Add(new QePatternInstance(x2, m, + PatternID.Repetition, dblCost)); + + ErasePart(v, x2, m, ref chErased); + bFoundRep = true; + } + } + + if(bFoundRep) ErasePart(v, x1, m, ref chErased); + } + } + + MemUtil.ZeroArray(v); + } + + private static bool PartsEqual(char[] v, int x1, int x2, int nLength) + { + for(int i = 0; i < nLength; ++i) + { + if(v[x1 + i] != v[x2 + i]) return false; + } + + return true; + } + + private static void ErasePart(char[] v, int i, int n, ref char chErased) + { + for(int j = 0; j < n; ++j) + { + v[i + j] = chErased; + --chErased; + } + } + + private static void FindNumbers(char[] vPassword, + List[] vPatterns) + { + int n = vPassword.Length; + StringBuilder sb = new StringBuilder(); + + for(int i = 0; i < n; ++i) + { + char ch = vPassword[i]; + if((ch >= '0') && (ch <= '9')) sb.Append(ch); + else + { + AddNumberPattern(vPatterns, sb, i - sb.Length); + sb.Remove(0, sb.Length); + } + } + AddNumberPattern(vPatterns, sb, n - sb.Length); + } + + private static void AddNumberPattern(List[] vPatterns, + StringBuilder sb, int i) + { + if(sb.Length <= 2) return; + string strNumber = sb.ToString(); + + int nZeros = 0; + for(int j = 0; j < strNumber.Length; ++j) + { + if(strNumber[j] != '0') break; + ++nZeros; + } + + double dblCost = Log2(nZeros + 1); + if(nZeros < strNumber.Length) + { + string strNonZero = strNumber.Substring(nZeros); + +#if KeePassLibSD + try { dblCost += Log2(double.Parse(strNonZero)); } + catch(Exception) { Debug.Assert(false); return; } +#else + double d; + if(double.TryParse(strNonZero, out d)) + dblCost += Log2(d); + else { Debug.Assert(false); return; } +#endif + } + + vPatterns[i].Add(new QePatternInstance(i, strNumber.Length, + PatternID.Number, dblCost)); + } + + private static void FindDiffSeqs(char[] vPassword, + List[] vPatterns) + { + int n = vPassword.Length; + int d = int.MaxValue, p = 0; + + for(int i = 1; i <= n; ++i) + { + int dCur = ((i == n) ? int.MinValue : + ((int)vPassword[i] - (int)vPassword[i - 1])); + if(dCur != d) + { + if((i - p) >= 3) // At least 3 chars involved + { + QeCharType ct = GetCharType(vPassword[p]); + double dblCost = ct.CharSize + Log2(i - p - 1); + + vPatterns[p].Add(new QePatternInstance(p, + i - p, PatternID.DiffSeq, dblCost)); + } + + d = dCur; + p = i - 1; + } + } + } + + private static double Log2(double dblValue) + { +#if KeePassLibSD + return (Math.Log(dblValue) / Math.Log(2.0)); +#else + return Math.Log(dblValue, 2.0); +#endif + } + } +} diff --git a/ModernKeePassLib/Cryptography/SelfTest.cs b/ModernKeePassLib/Cryptography/SelfTest.cs new file mode 100644 index 0000000..c9631ea --- /dev/null +++ b/ModernKeePassLib/Cryptography/SelfTest.cs @@ -0,0 +1,1169 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using System.Security; +using System.Text; + +#if KeePassUAP +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Parameters; +#else +using System.Security.Cryptography; +#endif + +using ModernKeePassLib.Cryptography.Cipher; +using ModernKeePassLib.Cryptography.Hash; +using ModernKeePassLib.Cryptography.KeyDerivation; +using ModernKeePassLib.Keys; +using ModernKeePassLib.Native; +using ModernKeePassLib.Resources; +using ModernKeePassLib.Security; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Cryptography +{ + /// + /// Class containing self-test methods. + /// + public static class SelfTest + { + /// + /// Perform a self-test. + /// + public static void Perform() + { +#if KeePassUAP + Debug.Assert(Marshal.SizeOf() == 4); + Debug.Assert(Marshal.SizeOf() == 4); + Debug.Assert(Marshal.SizeOf() == 8); + Debug.Assert(Marshal.SizeOf() == 8); + Debug.Assert(Marshal.SizeOf() == IntPtr.Size); +#else + Debug.Assert(Marshal.SizeOf(typeof(int)) == 4); + Debug.Assert(Marshal.SizeOf(typeof(uint)) == 4); + Debug.Assert(Marshal.SizeOf(typeof(long)) == 8); + Debug.Assert(Marshal.SizeOf(typeof(ulong)) == 8); + Debug.Assert(Marshal.SizeOf(typeof(IntPtr)) == IntPtr.Size); +#endif + Debug.Assert((IntPtr.Size == 4) || (IntPtr.Size == 8)); + + Debug.Assert((int)PwIcon.World == 1); + Debug.Assert((int)PwIcon.Warning == 2); + Debug.Assert((int)PwIcon.BlackBerry == 68); + + Random r = CryptoRandom.NewWeakRandom(); + + TestFipsComplianceProblems(); // Must be the first test + + TestAes(); + TestSalsa20(r); + TestChaCha20(r); + TestSha256(r); + TestBlake2b(r); + TestArgon2(); + TestHmac(); + + TestKeyTransform(r); + TestNativeKeyTransform(r); + + TestHmacOtp(); + + TestProtectedObjects(r); + TestMemUtil(r); + TestStrUtil(); + TestUrlUtil(); + +#if KeePassUAP + SelfTestEx.Perform(); +#endif + } + + internal static void TestFipsComplianceProblems() + { +#if !KeePassUAP + try { using(RijndaelManaged r = new RijndaelManaged()) { } } + catch(Exception exAes) + { + throw new SecurityException("AES/Rijndael: " + exAes.Message); + } +#endif + + try { using(SHA256Managed h = new SHA256Managed()) { } } + catch(Exception exSha256) + { + throw new SecurityException("SHA-256: " + exSha256.Message); + } + } + + private static void TestAes() + { + // Test vector (official ECB test vector #356) + byte[] pbKey = new byte[32]; + byte[] pbIV = new byte[16]; + byte[] pbData = new byte[16]; + pbData[0] = 0x04; + byte[] pbRefCT = new byte[16] { + 0x75, 0xD1, 0x1B, 0x0E, 0x3A, 0x68, 0xC4, 0x22, + 0x3D, 0x88, 0xDB, 0xF0, 0x17, 0x97, 0x7D, 0xD7 }; + +#if KeePassUAP + AesEngine aes = new AesEngine(); + aes.Init(true, new KeyParameter(pbKey)); + if(aes.GetBlockSize() != pbData.Length) + throw new SecurityException("AES (BC)"); + aes.ProcessBlock(pbData, 0, pbData, 0); + aes.Reset(); +#else + using(SymmetricAlgorithm a = CryptoUtil.CreateAes()) + { + if(a.BlockSize != 128) // AES block size + { + Debug.Assert(false); + a.BlockSize = 128; + } + a.KeySize = 256; + a.Mode = CipherMode.ECB; + + using(ICryptoTransform t = a.CreateEncryptor(pbKey, pbIV)) + { + t.TransformBlock(pbData, 0, 16, pbData, 0); + } + } +#endif + + if(!MemUtil.ArraysEqual(pbData, pbRefCT)) + throw new SecurityException("AES"); + } + + private static void TestSalsa20(Random r) + { +#if DEBUG + // Test values from official set 6, vector 3 + byte[] pbKey = new byte[32] { + 0x0F, 0x62, 0xB5, 0x08, 0x5B, 0xAE, 0x01, 0x54, + 0xA7, 0xFA, 0x4D, 0xA0, 0xF3, 0x46, 0x99, 0xEC, + 0x3F, 0x92, 0xE5, 0x38, 0x8B, 0xDE, 0x31, 0x84, + 0xD7, 0x2A, 0x7D, 0xD0, 0x23, 0x76, 0xC9, 0x1C + }; + byte[] pbIV = new byte[8] { 0x28, 0x8F, 0xF6, 0x5D, + 0xC4, 0x2B, 0x92, 0xF9 }; + byte[] pbExpected = new byte[16] { + 0x5E, 0x5E, 0x71, 0xF9, 0x01, 0x99, 0x34, 0x03, + 0x04, 0xAB, 0xB2, 0x2A, 0x37, 0xB6, 0x62, 0x5B + }; + + byte[] pb = new byte[16]; + Salsa20Cipher c = new Salsa20Cipher(pbKey, pbIV); + c.Encrypt(pb, 0, pb.Length); + if(!MemUtil.ArraysEqual(pb, pbExpected)) + throw new SecurityException("Salsa20-1"); + + // Extended test + byte[] pbExpected2 = new byte[16] { + 0xAB, 0xF3, 0x9A, 0x21, 0x0E, 0xEE, 0x89, 0x59, + 0x8B, 0x71, 0x33, 0x37, 0x70, 0x56, 0xC2, 0xFE + }; + byte[] pbExpected3 = new byte[16] { + 0x1B, 0xA8, 0x9D, 0xBD, 0x3F, 0x98, 0x83, 0x97, + 0x28, 0xF5, 0x67, 0x91, 0xD5, 0xB7, 0xCE, 0x23 + }; + + int nPos = Salsa20ToPos(c, r, pb.Length, 65536); + Array.Clear(pb, 0, pb.Length); + c.Encrypt(pb, 0, pb.Length); + if(!MemUtil.ArraysEqual(pb, pbExpected2)) + throw new SecurityException("Salsa20-2"); + + nPos = Salsa20ToPos(c, r, nPos + pb.Length, 131008); + Array.Clear(pb, 0, pb.Length); + c.Encrypt(pb, 0, pb.Length); + if(!MemUtil.ArraysEqual(pb, pbExpected3)) + throw new SecurityException("Salsa20-3"); + + Dictionary d = new Dictionary(); + const int nRounds = 100; + for(int i = 0; i < nRounds; ++i) + { + byte[] z = new byte[32]; + c = new Salsa20Cipher(z, MemUtil.Int64ToBytes(i)); + c.Encrypt(z, 0, z.Length); + d[MemUtil.ByteArrayToHexString(z)] = true; + } + if(d.Count != nRounds) throw new SecurityException("Salsa20-4"); +#endif + } + +#if DEBUG + private static int Salsa20ToPos(Salsa20Cipher c, Random r, int nPos, + int nTargetPos) + { + byte[] pb = new byte[512]; + + while(nPos < nTargetPos) + { + int x = r.Next(1, 513); + int nGen = Math.Min(nTargetPos - nPos, x); + c.Encrypt(pb, 0, nGen); + nPos += nGen; + } + + return nTargetPos; + } +#endif + + private static void TestChaCha20(Random r) + { + // ====================================================== + // Test vector from RFC 7539, section 2.3.2 + + byte[] pbKey = new byte[32]; + for(int i = 0; i < 32; ++i) pbKey[i] = (byte)i; + + byte[] pbIV = new byte[12]; + pbIV[3] = 0x09; + pbIV[7] = 0x4A; + + byte[] pbExpc = new byte[64] { + 0x10, 0xF1, 0xE7, 0xE4, 0xD1, 0x3B, 0x59, 0x15, + 0x50, 0x0F, 0xDD, 0x1F, 0xA3, 0x20, 0x71, 0xC4, + 0xC7, 0xD1, 0xF4, 0xC7, 0x33, 0xC0, 0x68, 0x03, + 0x04, 0x22, 0xAA, 0x9A, 0xC3, 0xD4, 0x6C, 0x4E, + 0xD2, 0x82, 0x64, 0x46, 0x07, 0x9F, 0xAA, 0x09, + 0x14, 0xC2, 0xD7, 0x05, 0xD9, 0x8B, 0x02, 0xA2, + 0xB5, 0x12, 0x9C, 0xD1, 0xDE, 0x16, 0x4E, 0xB9, + 0xCB, 0xD0, 0x83, 0xE8, 0xA2, 0x50, 0x3C, 0x4E + }; + + byte[] pb = new byte[64]; + + using(ChaCha20Cipher c = new ChaCha20Cipher(pbKey, pbIV)) + { + c.Seek(64, SeekOrigin.Begin); // Skip first block + c.Encrypt(pb, 0, pb.Length); + + if(!MemUtil.ArraysEqual(pb, pbExpc)) + throw new SecurityException("ChaCha20-1"); + } + +#if DEBUG + // ====================================================== + // Test vector from RFC 7539, section 2.4.2 + + pbIV[3] = 0; + + pb = StrUtil.Utf8.GetBytes("Ladies and Gentlemen of the clas" + + @"s of '99: If I could offer you only one tip for " + + @"the future, sunscreen would be it."); + + pbExpc = new byte[] { + 0x6E, 0x2E, 0x35, 0x9A, 0x25, 0x68, 0xF9, 0x80, + 0x41, 0xBA, 0x07, 0x28, 0xDD, 0x0D, 0x69, 0x81, + 0xE9, 0x7E, 0x7A, 0xEC, 0x1D, 0x43, 0x60, 0xC2, + 0x0A, 0x27, 0xAF, 0xCC, 0xFD, 0x9F, 0xAE, 0x0B, + 0xF9, 0x1B, 0x65, 0xC5, 0x52, 0x47, 0x33, 0xAB, + 0x8F, 0x59, 0x3D, 0xAB, 0xCD, 0x62, 0xB3, 0x57, + 0x16, 0x39, 0xD6, 0x24, 0xE6, 0x51, 0x52, 0xAB, + 0x8F, 0x53, 0x0C, 0x35, 0x9F, 0x08, 0x61, 0xD8, + 0x07, 0xCA, 0x0D, 0xBF, 0x50, 0x0D, 0x6A, 0x61, + 0x56, 0xA3, 0x8E, 0x08, 0x8A, 0x22, 0xB6, 0x5E, + 0x52, 0xBC, 0x51, 0x4D, 0x16, 0xCC, 0xF8, 0x06, + 0x81, 0x8C, 0xE9, 0x1A, 0xB7, 0x79, 0x37, 0x36, + 0x5A, 0xF9, 0x0B, 0xBF, 0x74, 0xA3, 0x5B, 0xE6, + 0xB4, 0x0B, 0x8E, 0xED, 0xF2, 0x78, 0x5E, 0x42, + 0x87, 0x4D + }; + + byte[] pb64 = new byte[64]; + + using(ChaCha20Cipher c = new ChaCha20Cipher(pbKey, pbIV)) + { + c.Encrypt(pb64, 0, pb64.Length); // Skip first block + c.Encrypt(pb, 0, pb.Length); + + if(!MemUtil.ArraysEqual(pb, pbExpc)) + throw new SecurityException("ChaCha20-2"); + } + + // ====================================================== + // Test vector from RFC 7539, appendix A.2 #2 + + Array.Clear(pbKey, 0, pbKey.Length); + pbKey[31] = 1; + + Array.Clear(pbIV, 0, pbIV.Length); + pbIV[11] = 2; + + pb = StrUtil.Utf8.GetBytes("Any submission to the IETF inten" + + "ded by the Contributor for publication as all or" + + " part of an IETF Internet-Draft or RFC and any s" + + "tatement made within the context of an IETF acti" + + "vity is considered an \"IETF Contribution\". Such " + + "statements include oral statements in IETF sessi" + + "ons, as well as written and electronic communica" + + "tions made at any time or place, which are addressed to"); + + pbExpc = MemUtil.HexStringToByteArray( + "A3FBF07DF3FA2FDE4F376CA23E82737041605D9F4F4F57BD8CFF2C1D4B7955EC" + + "2A97948BD3722915C8F3D337F7D370050E9E96D647B7C39F56E031CA5EB6250D" + + "4042E02785ECECFA4B4BB5E8EAD0440E20B6E8DB09D881A7C6132F420E527950" + + "42BDFA7773D8A9051447B3291CE1411C680465552AA6C405B7764D5E87BEA85A" + + "D00F8449ED8F72D0D662AB052691CA66424BC86D2DF80EA41F43ABF937D3259D" + + "C4B2D0DFB48A6C9139DDD7F76966E928E635553BA76C5C879D7B35D49EB2E62B" + + "0871CDAC638939E25E8A1E0EF9D5280FA8CA328B351C3C765989CBCF3DAA8B6C" + + "CC3AAF9F3979C92B3720FC88DC95ED84A1BE059C6499B9FDA236E7E818B04B0B" + + "C39C1E876B193BFE5569753F88128CC08AAA9B63D1A16F80EF2554D7189C411F" + + "5869CA52C5B83FA36FF216B9C1D30062BEBCFD2DC5BCE0911934FDA79A86F6E6" + + "98CED759C3FF9B6477338F3DA4F9CD8514EA9982CCAFB341B2384DD902F3D1AB" + + "7AC61DD29C6F21BA5B862F3730E37CFDC4FD806C22F221"); + + using(MemoryStream msEnc = new MemoryStream()) + { + using(ChaCha20Stream c = new ChaCha20Stream(msEnc, true, pbKey, pbIV)) + { + r.NextBytes(pb64); + c.Write(pb64, 0, pb64.Length); // Skip first block + + int p = 0; + while(p < pb.Length) + { + int cb = r.Next(1, pb.Length - p + 1); + c.Write(pb, p, cb); + p += cb; + } + Debug.Assert(p == pb.Length); + } + + byte[] pbEnc0 = msEnc.ToArray(); + byte[] pbEnc = MemUtil.Mid(pbEnc0, 64, pbEnc0.Length - 64); + if(!MemUtil.ArraysEqual(pbEnc, pbExpc)) + throw new SecurityException("ChaCha20-3"); + + using(MemoryStream msCT = new MemoryStream(pbEnc0, false)) + { + using(ChaCha20Stream cDec = new ChaCha20Stream(msCT, false, + pbKey, pbIV)) + { + byte[] pbPT = MemUtil.Read(cDec, pbEnc0.Length); + if(cDec.ReadByte() >= 0) + throw new SecurityException("ChaCha20-4"); + if(!MemUtil.ArraysEqual(MemUtil.Mid(pbPT, 0, 64), pb64)) + throw new SecurityException("ChaCha20-5"); + if(!MemUtil.ArraysEqual(MemUtil.Mid(pbPT, 64, pbEnc.Length), pb)) + throw new SecurityException("ChaCha20-6"); + } + } + } + + // ====================================================== + // Test vector TC8 from RFC draft by J. Strombergson: + // https://tools.ietf.org/html/draft-strombergson-chacha-test-vectors-01 + + pbKey = new byte[32] { + 0xC4, 0x6E, 0xC1, 0xB1, 0x8C, 0xE8, 0xA8, 0x78, + 0x72, 0x5A, 0x37, 0xE7, 0x80, 0xDF, 0xB7, 0x35, + 0x1F, 0x68, 0xED, 0x2E, 0x19, 0x4C, 0x79, 0xFB, + 0xC6, 0xAE, 0xBE, 0xE1, 0xA6, 0x67, 0x97, 0x5D + }; + + // The first 4 bytes are set to zero and a large counter + // is used; this makes the RFC 7539 version of ChaCha20 + // compatible with the original specification by + // D. J. Bernstein. + pbIV = new byte[12] { 0x00, 0x00, 0x00, 0x00, + 0x1A, 0xDA, 0x31, 0xD5, 0xCF, 0x68, 0x82, 0x21 + }; + + pb = new byte[128]; + + pbExpc = new byte[128] { + 0xF6, 0x3A, 0x89, 0xB7, 0x5C, 0x22, 0x71, 0xF9, + 0x36, 0x88, 0x16, 0x54, 0x2B, 0xA5, 0x2F, 0x06, + 0xED, 0x49, 0x24, 0x17, 0x92, 0x30, 0x2B, 0x00, + 0xB5, 0xE8, 0xF8, 0x0A, 0xE9, 0xA4, 0x73, 0xAF, + 0xC2, 0x5B, 0x21, 0x8F, 0x51, 0x9A, 0xF0, 0xFD, + 0xD4, 0x06, 0x36, 0x2E, 0x8D, 0x69, 0xDE, 0x7F, + 0x54, 0xC6, 0x04, 0xA6, 0xE0, 0x0F, 0x35, 0x3F, + 0x11, 0x0F, 0x77, 0x1B, 0xDC, 0xA8, 0xAB, 0x92, + + 0xE5, 0xFB, 0xC3, 0x4E, 0x60, 0xA1, 0xD9, 0xA9, + 0xDB, 0x17, 0x34, 0x5B, 0x0A, 0x40, 0x27, 0x36, + 0x85, 0x3B, 0xF9, 0x10, 0xB0, 0x60, 0xBD, 0xF1, + 0xF8, 0x97, 0xB6, 0x29, 0x0F, 0x01, 0xD1, 0x38, + 0xAE, 0x2C, 0x4C, 0x90, 0x22, 0x5B, 0xA9, 0xEA, + 0x14, 0xD5, 0x18, 0xF5, 0x59, 0x29, 0xDE, 0xA0, + 0x98, 0xCA, 0x7A, 0x6C, 0xCF, 0xE6, 0x12, 0x27, + 0x05, 0x3C, 0x84, 0xE4, 0x9A, 0x4A, 0x33, 0x32 + }; + + using(ChaCha20Cipher c = new ChaCha20Cipher(pbKey, pbIV, true)) + { + c.Decrypt(pb, 0, pb.Length); + + if(!MemUtil.ArraysEqual(pb, pbExpc)) + throw new SecurityException("ChaCha20-7"); + } +#endif + } + + private static void TestSha256(Random r) + { +#if DEBUG + byte[] pbData = new byte[517]; + r.NextBytes(pbData); + + byte[] pbH1; + using(SHA256Managed h1 = new SHA256Managed()) + { + int i = 0; + while(i != pbData.Length) + { + int cb = r.Next(pbData.Length - i) + 1; + h1.TransformBlock(pbData, i, cb, pbData, i); + i += cb; + } + h1.TransformFinalBlock(MemUtil.EmptyByteArray, 0, 0); + pbH1 = h1.Hash; + } + + byte[] pbH2; + using(SHA256Managed h2 = new SHA256Managed()) + { + pbH2 = h2.ComputeHash(pbData); + } + + if(!MemUtil.ArraysEqual(pbH1, pbH2)) + throw new SecurityException("SHA-256"); +#endif + } + + private static void TestBlake2b(Random r) + { +#if DEBUG + Blake2b h = new Blake2b(); + + // ====================================================== + // From https://tools.ietf.org/html/rfc7693 + + byte[] pbData = StrUtil.Utf8.GetBytes("abc"); + byte[] pbExpc = new byte[64] { + 0xBA, 0x80, 0xA5, 0x3F, 0x98, 0x1C, 0x4D, 0x0D, + 0x6A, 0x27, 0x97, 0xB6, 0x9F, 0x12, 0xF6, 0xE9, + 0x4C, 0x21, 0x2F, 0x14, 0x68, 0x5A, 0xC4, 0xB7, + 0x4B, 0x12, 0xBB, 0x6F, 0xDB, 0xFF, 0xA2, 0xD1, + 0x7D, 0x87, 0xC5, 0x39, 0x2A, 0xAB, 0x79, 0x2D, + 0xC2, 0x52, 0xD5, 0xDE, 0x45, 0x33, 0xCC, 0x95, + 0x18, 0xD3, 0x8A, 0xA8, 0xDB, 0xF1, 0x92, 0x5A, + 0xB9, 0x23, 0x86, 0xED, 0xD4, 0x00, 0x99, 0x23 + }; + + byte[] pbC = h.ComputeHash(pbData); + if(!MemUtil.ArraysEqual(pbC, pbExpc)) + throw new SecurityException("Blake2b-1"); + + // ====================================================== + // Computed using the official b2sum tool + + pbExpc = new byte[64] { + 0x78, 0x6A, 0x02, 0xF7, 0x42, 0x01, 0x59, 0x03, + 0xC6, 0xC6, 0xFD, 0x85, 0x25, 0x52, 0xD2, 0x72, + 0x91, 0x2F, 0x47, 0x40, 0xE1, 0x58, 0x47, 0x61, + 0x8A, 0x86, 0xE2, 0x17, 0xF7, 0x1F, 0x54, 0x19, + 0xD2, 0x5E, 0x10, 0x31, 0xAF, 0xEE, 0x58, 0x53, + 0x13, 0x89, 0x64, 0x44, 0x93, 0x4E, 0xB0, 0x4B, + 0x90, 0x3A, 0x68, 0x5B, 0x14, 0x48, 0xB7, 0x55, + 0xD5, 0x6F, 0x70, 0x1A, 0xFE, 0x9B, 0xE2, 0xCE + }; + + pbC = h.ComputeHash(MemUtil.EmptyByteArray); + if(!MemUtil.ArraysEqual(pbC, pbExpc)) + throw new SecurityException("Blake2b-2"); + + // ====================================================== + // Computed using the official b2sum tool + + string strS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.:,;_-\r\n"; + StringBuilder sb = new StringBuilder(); + for(int i = 0; i < 1000; ++i) sb.Append(strS); + pbData = StrUtil.Utf8.GetBytes(sb.ToString()); + + pbExpc = new byte[64] { + 0x59, 0x69, 0x8D, 0x3B, 0x83, 0xF4, 0x02, 0x4E, + 0xD8, 0x99, 0x26, 0x0E, 0xF4, 0xE5, 0x9F, 0x20, + 0xDC, 0x31, 0xEE, 0x5B, 0x45, 0xEA, 0xBB, 0xFC, + 0x1C, 0x0A, 0x8E, 0xED, 0xAA, 0x7A, 0xFF, 0x50, + 0x82, 0xA5, 0x8F, 0xBC, 0x4A, 0x46, 0xFC, 0xC5, + 0xEF, 0x44, 0x4E, 0x89, 0x80, 0x7D, 0x3F, 0x1C, + 0xC1, 0x94, 0x45, 0xBB, 0xC0, 0x2C, 0x95, 0xAA, + 0x3F, 0x08, 0x8A, 0x93, 0xF8, 0x75, 0x91, 0xB0 + }; + + int p = 0; + while(p < pbData.Length) + { + int cb = r.Next(pbData.Length - p) + 1; + h.TransformBlock(pbData, p, cb, pbData, p); + p += cb; + } + Debug.Assert(p == pbData.Length); + + h.TransformFinalBlock(MemUtil.EmptyByteArray, 0, 0); + + if(!MemUtil.ArraysEqual(h.Hash, pbExpc)) + throw new SecurityException("Blake2b-3"); + + h.Clear(); +#endif + } + + private static void TestArgon2() + { +#if DEBUG + Argon2Kdf kdf = new Argon2Kdf(); + + // ====================================================== + // From the official Argon2 1.3 reference code package + // (test vector for Argon2d 1.3); also on + // https://tools.ietf.org/html/draft-irtf-cfrg-argon2-00 + + KdfParameters p = kdf.GetDefaultParameters(); + kdf.Randomize(p); + + Debug.Assert(p.GetUInt32(Argon2Kdf.ParamVersion, 0) == 0x13U); + + byte[] pbMsg = new byte[32]; + for(int i = 0; i < pbMsg.Length; ++i) pbMsg[i] = 1; + + p.SetUInt64(Argon2Kdf.ParamMemory, 32 * 1024); + p.SetUInt64(Argon2Kdf.ParamIterations, 3); + p.SetUInt32(Argon2Kdf.ParamParallelism, 4); + + byte[] pbSalt = new byte[16]; + for(int i = 0; i < pbSalt.Length; ++i) pbSalt[i] = 2; + p.SetByteArray(Argon2Kdf.ParamSalt, pbSalt); + + byte[] pbKey = new byte[8]; + for(int i = 0; i < pbKey.Length; ++i) pbKey[i] = 3; + p.SetByteArray(Argon2Kdf.ParamSecretKey, pbKey); + + byte[] pbAssoc = new byte[12]; + for(int i = 0; i < pbAssoc.Length; ++i) pbAssoc[i] = 4; + p.SetByteArray(Argon2Kdf.ParamAssocData, pbAssoc); + + byte[] pbExpc = new byte[32] { + 0x51, 0x2B, 0x39, 0x1B, 0x6F, 0x11, 0x62, 0x97, + 0x53, 0x71, 0xD3, 0x09, 0x19, 0x73, 0x42, 0x94, + 0xF8, 0x68, 0xE3, 0xBE, 0x39, 0x84, 0xF3, 0xC1, + 0xA1, 0x3A, 0x4D, 0xB9, 0xFA, 0xBE, 0x4A, 0xCB + }; + + byte[] pb = kdf.Transform(pbMsg, p); + + if(!MemUtil.ArraysEqual(pb, pbExpc)) + throw new SecurityException("Argon2-1"); + + // ====================================================== + // From the official Argon2 1.3 reference code package + // (test vector for Argon2d 1.0) + + p.SetUInt32(Argon2Kdf.ParamVersion, 0x10); + + pbExpc = new byte[32] { + 0x96, 0xA9, 0xD4, 0xE5, 0xA1, 0x73, 0x40, 0x92, + 0xC8, 0x5E, 0x29, 0xF4, 0x10, 0xA4, 0x59, 0x14, + 0xA5, 0xDD, 0x1F, 0x5C, 0xBF, 0x08, 0xB2, 0x67, + 0x0D, 0xA6, 0x8A, 0x02, 0x85, 0xAB, 0xF3, 0x2B + }; + + pb = kdf.Transform(pbMsg, p); + + if(!MemUtil.ArraysEqual(pb, pbExpc)) + throw new SecurityException("Argon2-2"); + + // ====================================================== + // From the official 'phc-winner-argon2-20151206.zip' + // (test vector for Argon2d 1.0) + + p.SetUInt64(Argon2Kdf.ParamMemory, 16 * 1024); + + pbExpc = new byte[32] { + 0x57, 0xB0, 0x61, 0x3B, 0xFD, 0xD4, 0x13, 0x1A, + 0x0C, 0x34, 0x88, 0x34, 0xC6, 0x72, 0x9C, 0x2C, + 0x72, 0x29, 0x92, 0x1E, 0x6B, 0xBA, 0x37, 0x66, + 0x5D, 0x97, 0x8C, 0x4F, 0xE7, 0x17, 0x5E, 0xD2 + }; + + pb = kdf.Transform(pbMsg, p); + + if(!MemUtil.ArraysEqual(pb, pbExpc)) + throw new SecurityException("Argon2-3"); + +#if SELFTEST_ARGON2_LONG + // ====================================================== + // Computed using the official 'argon2' application + // (test vectors for Argon2d 1.3) + + p = kdf.GetDefaultParameters(); + + pbMsg = StrUtil.Utf8.GetBytes("ABC1234"); + + p.SetUInt64(Argon2Kdf.ParamMemory, (1 << 11) * 1024); // 2 MB + p.SetUInt64(Argon2Kdf.ParamIterations, 2); + p.SetUInt32(Argon2Kdf.ParamParallelism, 2); + + pbSalt = StrUtil.Utf8.GetBytes("somesalt"); + p.SetByteArray(Argon2Kdf.ParamSalt, pbSalt); + + pbExpc = new byte[32] { + 0x29, 0xCB, 0xD3, 0xA1, 0x93, 0x76, 0xF7, 0xA2, + 0xFC, 0xDF, 0xB0, 0x68, 0xAC, 0x0B, 0x99, 0xBA, + 0x40, 0xAC, 0x09, 0x01, 0x73, 0x42, 0xCE, 0xF1, + 0x29, 0xCC, 0xA1, 0x4F, 0xE1, 0xC1, 0xB7, 0xA3 + }; + + pb = kdf.Transform(pbMsg, p); + + if(!MemUtil.ArraysEqual(pb, pbExpc)) + throw new SecurityException("Argon2-4"); + + p.SetUInt64(Argon2Kdf.ParamMemory, (1 << 10) * 1024); // 1 MB + p.SetUInt64(Argon2Kdf.ParamIterations, 3); + + pbExpc = new byte[32] { + 0x7A, 0xBE, 0x1C, 0x1C, 0x8D, 0x7F, 0xD6, 0xDC, + 0x7C, 0x94, 0x06, 0x3E, 0xD8, 0xBC, 0xD8, 0x1C, + 0x2F, 0x87, 0x84, 0x99, 0x12, 0x83, 0xFE, 0x76, + 0x00, 0x64, 0xC4, 0x58, 0xA4, 0xDA, 0x35, 0x70 + }; + + pb = kdf.Transform(pbMsg, p); + + if(!MemUtil.ArraysEqual(pb, pbExpc)) + throw new SecurityException("Argon2-5"); + +#if SELFTEST_ARGON2_LONGER + p.SetUInt64(Argon2Kdf.ParamMemory, (1 << 20) * 1024); // 1 GB + p.SetUInt64(Argon2Kdf.ParamIterations, 2); + p.SetUInt32(Argon2Kdf.ParamParallelism, 3); + + pbExpc = new byte[32] { + 0xE6, 0xE7, 0xCB, 0xF5, 0x5A, 0x06, 0x93, 0x05, + 0x32, 0xBA, 0x86, 0xC6, 0x1F, 0x45, 0x17, 0x99, + 0x65, 0x41, 0x77, 0xF9, 0x30, 0x55, 0x9A, 0xE8, + 0x3D, 0x21, 0x48, 0xC6, 0x2D, 0x0C, 0x49, 0x11 + }; + + pb = kdf.Transform(pbMsg, p); + + if(!MemUtil.ArraysEqual(pb, pbExpc)) + throw new SecurityException("Argon2-6"); +#endif // SELFTEST_ARGON2_LONGER +#endif // SELFTEST_ARGON2_LONG +#endif // DEBUG + } + + private static void TestHmac() + { +#if DEBUG + // Test vectors from RFC 4231 + + byte[] pbKey = new byte[20]; + for(int i = 0; i < pbKey.Length; ++i) pbKey[i] = 0x0B; + byte[] pbMsg = StrUtil.Utf8.GetBytes("Hi There"); + byte[] pbExpc = new byte[32] { + 0xB0, 0x34, 0x4C, 0x61, 0xD8, 0xDB, 0x38, 0x53, + 0x5C, 0xA8, 0xAF, 0xCE, 0xAF, 0x0B, 0xF1, 0x2B, + 0x88, 0x1D, 0xC2, 0x00, 0xC9, 0x83, 0x3D, 0xA7, + 0x26, 0xE9, 0x37, 0x6C, 0x2E, 0x32, 0xCF, 0xF7 + }; + HmacEval(pbKey, pbMsg, pbExpc, "1"); + + pbKey = new byte[131]; + for(int i = 0; i < pbKey.Length; ++i) pbKey[i] = 0xAA; + pbMsg = StrUtil.Utf8.GetBytes( + "This is a test using a larger than block-size key and " + + "a larger than block-size data. The key needs to be " + + "hashed before being used by the HMAC algorithm."); + pbExpc = new byte[32] { + 0x9B, 0x09, 0xFF, 0xA7, 0x1B, 0x94, 0x2F, 0xCB, + 0x27, 0x63, 0x5F, 0xBC, 0xD5, 0xB0, 0xE9, 0x44, + 0xBF, 0xDC, 0x63, 0x64, 0x4F, 0x07, 0x13, 0x93, + 0x8A, 0x7F, 0x51, 0x53, 0x5C, 0x3A, 0x35, 0xE2 + }; + HmacEval(pbKey, pbMsg, pbExpc, "2"); +#endif + } + +#if DEBUG + private static void HmacEval(byte[] pbKey, byte[] pbMsg, + byte[] pbExpc, string strID) + { + using(HMACSHA256 h = new HMACSHA256(pbKey)) + { + h.TransformBlock(pbMsg, 0, pbMsg.Length, pbMsg, 0); + h.TransformFinalBlock(MemUtil.EmptyByteArray, 0, 0); + + byte[] pbHash = h.Hash; + if(!MemUtil.ArraysEqual(pbHash, pbExpc)) + throw new SecurityException("HMAC-SHA-256-" + strID); + + // Reuse the object + h.Initialize(); + h.TransformBlock(pbMsg, 0, pbMsg.Length, pbMsg, 0); + h.TransformFinalBlock(MemUtil.EmptyByteArray, 0, 0); + + pbHash = h.Hash; + if(!MemUtil.ArraysEqual(pbHash, pbExpc)) + throw new SecurityException("HMAC-SHA-256-" + strID + "-R"); + } + } +#endif + + private static void TestKeyTransform(Random r) + { +#if DEBUG + // Up to KeePass 2.34, the OtpKeyProv plugin used the public + // CompositeKey.TransformKeyManaged method (and a finalizing + // SHA-256 computation), which became an internal method of + // the AesKdf class in KeePass 2.35, thus OtpKeyProv now + // uses the AesKdf class; here we ensure that the results + // are the same + + byte[] pbKey = new byte[32]; + r.NextBytes(pbKey); + byte[] pbSeed = new byte[32]; + r.NextBytes(pbSeed); + ulong uRounds = (ulong)r.Next(1, 0x7FFF); + + byte[] pbMan = new byte[pbKey.Length]; + Array.Copy(pbKey, pbMan, pbKey.Length); + if(!AesKdf.TransformKeyManaged(pbMan, pbSeed, uRounds)) + throw new SecurityException("AES-KDF-1"); + pbMan = CryptoUtil.HashSha256(pbMan); + + AesKdf kdf = new AesKdf(); + KdfParameters p = kdf.GetDefaultParameters(); + p.SetUInt64(AesKdf.ParamRounds, uRounds); + p.SetByteArray(AesKdf.ParamSeed, pbSeed); + byte[] pbKdf = kdf.Transform(pbKey, p); + + if(!MemUtil.ArraysEqual(pbMan, pbKdf)) + throw new SecurityException("AES-KDF-2"); +#endif + } + + private static void TestNativeKeyTransform(Random r) + { +#if DEBUG + byte[] pbOrgKey = CryptoRandom.Instance.GetRandomBytes(32); + byte[] pbSeed = CryptoRandom.Instance.GetRandomBytes(32); + ulong uRounds = (ulong)r.Next(1, 0x3FFF); + + byte[] pbManaged = new byte[32]; + Array.Copy(pbOrgKey, pbManaged, 32); + if(!AesKdf.TransformKeyManaged(pbManaged, pbSeed, uRounds)) + throw new SecurityException("AES-KDF-1"); + + byte[] pbNative = new byte[32]; + Array.Copy(pbOrgKey, pbNative, 32); + if(!NativeLib.TransformKey256(pbNative, pbSeed, uRounds)) + return; // Native library not available ("success") + + if(!MemUtil.ArraysEqual(pbManaged, pbNative)) + throw new SecurityException("AES-KDF-2"); +#endif + } + + private static void TestMemUtil(Random r) + { +#if DEBUG + byte[] pb = CryptoRandom.Instance.GetRandomBytes((uint)r.Next( + 0, 0x2FFFF)); + + byte[] pbCompressed = MemUtil.Compress(pb); + if(!MemUtil.ArraysEqual(MemUtil.Decompress(pbCompressed), pb)) + throw new InvalidOperationException("GZip"); + + Encoding enc = StrUtil.Utf8; + pb = enc.GetBytes("012345678901234567890a"); + byte[] pbN = enc.GetBytes("9012"); + if(MemUtil.IndexOf(pb, pbN) != 9) + throw new InvalidOperationException("MemUtil-1"); + pbN = enc.GetBytes("01234567890123"); + if(MemUtil.IndexOf(pb, pbN) != 0) + throw new InvalidOperationException("MemUtil-2"); + pbN = enc.GetBytes("a"); + if(MemUtil.IndexOf(pb, pbN) != 21) + throw new InvalidOperationException("MemUtil-3"); + pbN = enc.GetBytes("0a"); + if(MemUtil.IndexOf(pb, pbN) != 20) + throw new InvalidOperationException("MemUtil-4"); + pbN = enc.GetBytes("1"); + if(MemUtil.IndexOf(pb, pbN) != 1) + throw new InvalidOperationException("MemUtil-5"); + pbN = enc.GetBytes("b"); + if(MemUtil.IndexOf(pb, pbN) >= 0) + throw new InvalidOperationException("MemUtil-6"); + pbN = enc.GetBytes("012b"); + if(MemUtil.IndexOf(pb, pbN) >= 0) + throw new InvalidOperationException("MemUtil-7"); + + byte[] pbRes = MemUtil.ParseBase32("MY======"); + byte[] pbExp = Encoding.UTF8.GetBytes("f"); + if(!MemUtil.ArraysEqual(pbRes, pbExp)) throw new Exception("Base32-1"); + + pbRes = MemUtil.ParseBase32("MZXQ===="); + pbExp = Encoding.UTF8.GetBytes("fo"); + if(!MemUtil.ArraysEqual(pbRes, pbExp)) throw new Exception("Base32-2"); + + pbRes = MemUtil.ParseBase32("MZXW6==="); + pbExp = Encoding.UTF8.GetBytes("foo"); + if(!MemUtil.ArraysEqual(pbRes, pbExp)) throw new Exception("Base32-3"); + + pbRes = MemUtil.ParseBase32("MZXW6YQ="); + pbExp = Encoding.UTF8.GetBytes("foob"); + if(!MemUtil.ArraysEqual(pbRes, pbExp)) throw new Exception("Base32-4"); + + pbRes = MemUtil.ParseBase32("MZXW6YTB"); + pbExp = Encoding.UTF8.GetBytes("fooba"); + if(!MemUtil.ArraysEqual(pbRes, pbExp)) throw new Exception("Base32-5"); + + pbRes = MemUtil.ParseBase32("MZXW6YTBOI======"); + pbExp = Encoding.UTF8.GetBytes("foobar"); + if(!MemUtil.ArraysEqual(pbRes, pbExp)) throw new Exception("Base32-6"); + + pbRes = MemUtil.ParseBase32("JNSXSIDQOJXXM2LEMVZCAYTBONSWIIDPNYQG63TFFV2GS3LFEBYGC43TO5XXEZDTFY======"); + pbExp = Encoding.UTF8.GetBytes("Key provider based on one-time passwords."); + if(!MemUtil.ArraysEqual(pbRes, pbExp)) throw new Exception("Base32-7"); + + int i = 0 - 0x10203040; + pbRes = MemUtil.Int32ToBytes(i); + if(MemUtil.ByteArrayToHexString(pbRes) != "C0CFDFEF") + throw new Exception("MemUtil-8"); // Must be little-endian + if(MemUtil.BytesToUInt32(pbRes) != (uint)i) + throw new Exception("MemUtil-9"); + if(MemUtil.BytesToInt32(pbRes) != i) + throw new Exception("MemUtil-10"); + + ArrayHelperEx ah = MemUtil.ArrayHelperExOfChar; + for(int j = 0; j < 30; ++j) + { + string strA = r.Next(30).ToString(); + string strB = r.Next(30).ToString(); + char[] vA = strA.ToCharArray(); + char[] vB = strB.ToCharArray(); + + if(ah.Equals(vA, vB) != string.Equals(strA, strB)) + throw new Exception("MemUtil-11"); + if((vA.Length == vB.Length) && (Math.Sign(ah.Compare(vA, vB)) != + Math.Sign(string.CompareOrdinal(strA, strB)))) + throw new Exception("MemUtil-12"); + } +#endif + } + + private static void TestHmacOtp() + { +#if (DEBUG && !KeePassLibSD) + byte[] pbSecret = StrUtil.Utf8.GetBytes("12345678901234567890"); + string[] vExp = new string[]{ "755224", "287082", "359152", + "969429", "338314", "254676", "287922", "162583", "399871", + "520489" }; + + for(int i = 0; i < vExp.Length; ++i) + { + if(HmacOtp.Generate(pbSecret, (ulong)i, 6, false, -1) != vExp[i]) + throw new InvalidOperationException("HmacOtp"); + } +#endif + } + + private static void TestProtectedObjects(Random r) + { +#if DEBUG + Encoding enc = StrUtil.Utf8; + + byte[] pbData = enc.GetBytes("Test Test Test Test"); + ProtectedBinary pb = new ProtectedBinary(true, pbData); + if(!pb.IsProtected) throw new SecurityException("ProtectedBinary-1"); + + byte[] pbDec = pb.ReadData(); + if(!MemUtil.ArraysEqual(pbData, pbDec)) + throw new SecurityException("ProtectedBinary-2"); + if(!pb.IsProtected) throw new SecurityException("ProtectedBinary-3"); + + byte[] pbData2 = enc.GetBytes("Test Test Test Test"); + byte[] pbData3 = enc.GetBytes("Test Test Test Test Test"); + ProtectedBinary pb2 = new ProtectedBinary(true, pbData2); + ProtectedBinary pb3 = new ProtectedBinary(true, pbData3); + if(!pb.Equals(pb2)) throw new SecurityException("ProtectedBinary-4"); + if(pb.Equals(pb3)) throw new SecurityException("ProtectedBinary-5"); + if(pb2.Equals(pb3)) throw new SecurityException("ProtectedBinary-6"); + + if(pb.GetHashCode() != pb2.GetHashCode()) + throw new SecurityException("ProtectedBinary-7"); + if(!((object)pb).Equals((object)pb2)) + throw new SecurityException("ProtectedBinary-8"); + if(((object)pb).Equals((object)pb3)) + throw new SecurityException("ProtectedBinary-9"); + if(((object)pb2).Equals((object)pb3)) + throw new SecurityException("ProtectedBinary-10"); + + ProtectedString ps = new ProtectedString(); + if(ps.Length != 0) throw new SecurityException("ProtectedString-1"); + if(!ps.IsEmpty) throw new SecurityException("ProtectedString-2"); + if(ps.ReadString().Length != 0) + throw new SecurityException("ProtectedString-3"); + + ps = new ProtectedString(true, "Test"); + ProtectedString ps2 = new ProtectedString(true, enc.GetBytes("Test")); + if(ps.IsEmpty) throw new SecurityException("ProtectedString-4"); + pbData = ps.ReadUtf8(); + pbData2 = ps2.ReadUtf8(); + if(!MemUtil.ArraysEqual(pbData, pbData2)) + throw new SecurityException("ProtectedString-5"); + if(pbData.Length != 4) + throw new SecurityException("ProtectedString-6"); + if(ps.ReadString() != ps2.ReadString()) + throw new SecurityException("ProtectedString-7"); + pbData = ps.ReadUtf8(); + pbData2 = ps2.ReadUtf8(); + if(!MemUtil.ArraysEqual(pbData, pbData2)) + throw new SecurityException("ProtectedString-8"); + if(!ps.IsProtected) throw new SecurityException("ProtectedString-9"); + if(!ps2.IsProtected) throw new SecurityException("ProtectedString-10"); + + string str = string.Empty; + ps = new ProtectedString(); + for(int i = 0; i < 100; ++i) + { + bool bProt = ((r.Next() % 4) != 0); + ps = ps.WithProtection(bProt); + + int x = r.Next(str.Length + 1); + int c = r.Next(20); + char ch = (char)r.Next(1, 256); + + string strIns = new string(ch, c); + str = str.Insert(x, strIns); + ps = ps.Insert(x, strIns); + + if(ps.IsProtected != bProt) + throw new SecurityException("ProtectedString-11"); + if(ps.ReadString() != str) + throw new SecurityException("ProtectedString-12"); + + ps = ps.WithProtection(bProt); + + x = r.Next(str.Length); + c = r.Next(str.Length - x + 1); + + str = str.Remove(x, c); + ps = ps.Remove(x, c); + + if(ps.IsProtected != bProt) + throw new SecurityException("ProtectedString-13"); + if(ps.ReadString() != str) + throw new SecurityException("ProtectedString-14"); + } + + ps = new ProtectedString(false, "ABCD"); + ps2 = new ProtectedString(true, "EFG"); + ps += (ps2 + "HI"); + if(!ps.Equals(new ProtectedString(true, "ABCDEFGHI"), true)) + throw new SecurityException("ProtectedString-15"); + if(!ps.Equals(new ProtectedString(false, "ABCDEFGHI"), false)) + throw new SecurityException("ProtectedString-16"); +#endif + } + + private static void TestStrUtil() + { +#if DEBUG + string[] vSeps = new string[]{ "ax", "b", "c" }; + const string str1 = "axbqrstcdeax"; + List v1 = StrUtil.SplitWithSep(str1, vSeps, true); + + if(v1.Count != 9) throw new InvalidOperationException("StrUtil-1"); + if(v1[0].Length > 0) throw new InvalidOperationException("StrUtil-2"); + if(!v1[1].Equals("ax")) throw new InvalidOperationException("StrUtil-3"); + if(v1[2].Length > 0) throw new InvalidOperationException("StrUtil-4"); + if(!v1[3].Equals("b")) throw new InvalidOperationException("StrUtil-5"); + if(!v1[4].Equals("qrst")) throw new InvalidOperationException("StrUtil-6"); + if(!v1[5].Equals("c")) throw new InvalidOperationException("StrUtil-7"); + if(!v1[6].Equals("de")) throw new InvalidOperationException("StrUtil-8"); + if(!v1[7].Equals("ax")) throw new InvalidOperationException("StrUtil-9"); + if(v1[8].Length > 0) throw new InvalidOperationException("StrUtil-10"); + + const string str2 = "12ab56"; + List v2 = StrUtil.SplitWithSep(str2, new string[]{ "AB" }, false); + if(v2.Count != 3) throw new InvalidOperationException("StrUtil-11"); + if(!v2[0].Equals("12")) throw new InvalidOperationException("StrUtil-12"); + if(!v2[1].Equals("AB")) throw new InvalidOperationException("StrUtil-13"); + if(!v2[2].Equals("56")) throw new InvalidOperationException("StrUtil-14"); + + List v3 = StrUtil.SplitWithSep("pqrs", vSeps, false); + if(v3.Count != 1) throw new InvalidOperationException("StrUtil-15"); + if(!v3[0].Equals("pqrs")) throw new InvalidOperationException("StrUtil-16"); + + if(StrUtil.VersionToString(0x000F000E000D000CUL) != "15.14.13.12") + throw new InvalidOperationException("StrUtil-V1"); + if(StrUtil.VersionToString(0x00FF000E00010000UL) != "255.14.1") + throw new InvalidOperationException("StrUtil-V2"); + if(StrUtil.VersionToString(0x000F00FF00000000UL) != "15.255") + throw new InvalidOperationException("StrUtil-V3"); + if(StrUtil.VersionToString(0x00FF000000000000UL) != "255") + throw new InvalidOperationException("StrUtil-V4"); + if(StrUtil.VersionToString(0x00FF000000000000UL, 2) != "255.0") + throw new InvalidOperationException("StrUtil-V5"); + if(StrUtil.VersionToString(0x0000000000070000UL) != "0.0.7") + throw new InvalidOperationException("StrUtil-V6"); + if(StrUtil.VersionToString(0x0000000000000000UL) != "0") + throw new InvalidOperationException("StrUtil-V7"); + if(StrUtil.VersionToString(0x00000000FFFF0000UL, 4) != "0.0.65535.0") + throw new InvalidOperationException("StrUtil-V8"); + if(StrUtil.VersionToString(0x0000000000000000UL, 4) != "0.0.0.0") + throw new InvalidOperationException("StrUtil-V9"); + + if(StrUtil.RtfEncodeChar('\u0000') != "\\u0?") + throw new InvalidOperationException("StrUtil-Rtf1"); + if(StrUtil.RtfEncodeChar('\u7FFF') != "\\u32767?") + throw new InvalidOperationException("StrUtil-Rtf2"); + if(StrUtil.RtfEncodeChar('\u8000') != "\\u-32768?") + throw new InvalidOperationException("StrUtil-Rtf3"); + if(StrUtil.RtfEncodeChar('\uFFFF') != "\\u-1?") + throw new InvalidOperationException("StrUtil-Rtf4"); + + if(!StrUtil.StringToBool(Boolean.TrueString)) + throw new InvalidOperationException("StrUtil-Bool1"); + if(StrUtil.StringToBool(Boolean.FalseString)) + throw new InvalidOperationException("StrUtil-Bool2"); + + if(StrUtil.Count("Abracadabra", "a") != 4) + throw new InvalidOperationException("StrUtil-Count1"); + if(StrUtil.Count("Bla", "U") != 0) + throw new InvalidOperationException("StrUtil-Count2"); + if(StrUtil.Count("AAAAA", "AA") != 4) + throw new InvalidOperationException("StrUtil-Count3"); + + const string sU = "data:mytype;base64,"; + if(!StrUtil.IsDataUri(sU)) + throw new InvalidOperationException("StrUtil-DataUri1"); + if(!StrUtil.IsDataUri(sU, "mytype")) + throw new InvalidOperationException("StrUtil-DataUri2"); + if(StrUtil.IsDataUri(sU, "notmytype")) + throw new InvalidOperationException("StrUtil-DataUri3"); + + uint u = 0x7FFFFFFFU; + if(u.ToString(NumberFormatInfo.InvariantInfo) != "2147483647") + throw new InvalidOperationException("StrUtil-Inv1"); + if(uint.MaxValue.ToString(NumberFormatInfo.InvariantInfo) != + "4294967295") + throw new InvalidOperationException("StrUtil-Inv2"); + if(long.MinValue.ToString(NumberFormatInfo.InvariantInfo) != + "-9223372036854775808") + throw new InvalidOperationException("StrUtil-Inv3"); + if(short.MinValue.ToString(NumberFormatInfo.InvariantInfo) != + "-32768") + throw new InvalidOperationException("StrUtil-Inv4"); + + if(!string.Equals("abcd", "aBcd", StrUtil.CaseIgnoreCmp)) + throw new InvalidOperationException("StrUtil-Case1"); + if(string.Equals(@"ab", StrUtil.CaseIgnoreCmp)) + throw new InvalidOperationException("StrUtil-Case2"); + + const string strNL = "\n01\r23\n45\r\n67\r"; + string strW = StrUtil.NormalizeNewLines(strNL, true); + string strL = StrUtil.NormalizeNewLines(strNL, false); + if(strW != "\r\n01\r\n23\r\n45\r\n67\r\n") + throw new InvalidOperationException("StrUtil-NewLine1"); + if(strL != "\n01\n23\n45\n67\n") + throw new InvalidOperationException("StrUtil-NewLine2"); + if(StrUtil.IsNewLineNormalized(strNL.ToCharArray(), true)) + throw new InvalidOperationException("StrUtil-NewLine3"); + if(StrUtil.IsNewLineNormalized(strNL.ToCharArray(), false)) + throw new InvalidOperationException("StrUtil-NewLine4"); + if(!StrUtil.IsNewLineNormalized(strW.ToCharArray(), true)) + throw new InvalidOperationException("StrUtil-NewLine5"); + if(StrUtil.IsNewLineNormalized(strW.ToCharArray(), false)) + throw new InvalidOperationException("StrUtil-NewLine6"); + if(StrUtil.IsNewLineNormalized(strL.ToCharArray(), true)) + throw new InvalidOperationException("StrUtil-NewLine7"); + if(!StrUtil.IsNewLineNormalized(strL.ToCharArray(), false)) + throw new InvalidOperationException("StrUtil-NewLine8"); + if(!StrUtil.IsNewLineNormalized(string.Empty.ToCharArray(), true)) + throw new InvalidOperationException("StrUtil-NewLine9"); + if(!StrUtil.IsNewLineNormalized(string.Empty.ToCharArray(), false)) + throw new InvalidOperationException("StrUtil-NewLine10"); +#endif + } + + private static void TestUrlUtil() + { +#if DEBUG +#if !KeePassUAP + Debug.Assert(Uri.UriSchemeHttp.Equals("http", StrUtil.CaseIgnoreCmp)); + Debug.Assert(Uri.UriSchemeHttps.Equals("https", StrUtil.CaseIgnoreCmp)); +#endif + + if(UrlUtil.GetHost(@"scheme://domain:port/path?query_string#fragment_id") != + "domain") + throw new InvalidOperationException("UrlUtil-H1"); + if(UrlUtil.GetHost(@"https://example.org:443") != "example.org") + throw new InvalidOperationException("UrlUtil-H2"); + if(UrlUtil.GetHost(@"mailto:bob@example.com") != "example.com") + throw new InvalidOperationException("UrlUtil-H3"); + if(UrlUtil.GetHost(@"ftp://asmith@ftp.example.org") != "ftp.example.org") + throw new InvalidOperationException("UrlUtil-H4"); + if(UrlUtil.GetHost(@"scheme://username:password@domain:port/path?query_string#fragment_id") != + "domain") + throw new InvalidOperationException("UrlUtil-H5"); + if(UrlUtil.GetHost(@"bob@example.com") != "example.com") + throw new InvalidOperationException("UrlUtil-H6"); + if(UrlUtil.GetHost(@"s://u:p@d.tld:p/p?q#f") != "d.tld") + throw new InvalidOperationException("UrlUtil-H7"); + + if(NativeLib.IsUnix()) return; + + string strBase = "\\\\HOMESERVER\\Apps\\KeePass\\KeePass.exe"; + string strDoc = "\\\\HOMESERVER\\Documents\\KeePass\\NewDatabase.kdbx"; + string strRel = "..\\..\\Documents\\KeePass\\NewDatabase.kdbx"; + + string str = UrlUtil.MakeRelativePath(strBase, strDoc); + if(!str.Equals(strRel)) throw new InvalidOperationException("UrlUtil-R1"); + + str = UrlUtil.MakeAbsolutePath(strBase, strRel); + if(!str.Equals(strDoc)) throw new InvalidOperationException("UrlUtil-R2"); + + str = UrlUtil.GetQuotedAppPath(" \"Test\" \"%1\" "); + if(str != "Test") throw new InvalidOperationException("UrlUtil-Q1"); + str = UrlUtil.GetQuotedAppPath("C:\\Program Files\\Test.exe"); + if(str != "C:\\Program Files\\Test.exe") throw new InvalidOperationException("UrlUtil-Q2"); + str = UrlUtil.GetQuotedAppPath("Reg.exe \"Test\" \"Test 2\""); + if(str != "Reg.exe \"Test\" \"Test 2\"") throw new InvalidOperationException("UrlUtil-Q3"); +#endif + } + } +} diff --git a/ModernKeePassLib/Delegates/Handlers.cs b/ModernKeePassLib/Delegates/Handlers.cs new file mode 100644 index 0000000..949821c --- /dev/null +++ b/ModernKeePassLib/Delegates/Handlers.cs @@ -0,0 +1,66 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; + +namespace ModernKeePassLib.Delegates +{ + /// + /// Function definition of a method that performs an action on a group. + /// When traversing the internal tree, this function will be invoked + /// for all visited groups. + /// + /// Currently visited group. + /// You must return true if you want to continue the + /// traversal. If you want to immediately stop the whole traversal, + /// return false. + public delegate bool GroupHandler(PwGroup pg); + + /// + /// Function definition of a method that performs an action on an entry. + /// When traversing the internal tree, this function will be invoked + /// for all visited entries. + /// + /// Currently visited entry. + /// You must return true if you want to continue the + /// traversal. If you want to immediately stop the whole traversal, + /// return false. + public delegate bool EntryHandler(PwEntry pe); + + public delegate void VoidDelegate(); + public delegate string StrPwEntryDelegate(string str, PwEntry pe); + + // Action<...> with 0 or >= 2 parameters has been introduced in .NET 3.5 + public delegate void GAction(); + public delegate void GAction(T o); + public delegate void GAction(T1 o1, T2 o2); + public delegate void GAction(T1 o1, T2 o2, T3 o3); + public delegate void GAction(T1 o1, T2 o2, T3 o3, T4 o4); + public delegate void GAction(T1 o1, T2 o2, T3 o3, T4 o4, T5 o5); + public delegate void GAction(T1 o1, T2 o2, T3 o3, T4 o4, T5 o5, T6 o6); + + // Func<...> has been introduced in .NET 3.5 + public delegate TResult GFunc(); + public delegate TResult GFunc(T o); + public delegate TResult GFunc(T1 o1, T2 o2); + public delegate TResult GFunc(T1 o1, T2 o2, T3 o3); + public delegate TResult GFunc(T1 o1, T2 o2, T3 o3, T4 o4); + public delegate TResult GFunc(T1 o1, T2 o2, T3 o3, T4 o4, T5 o5); + public delegate TResult GFunc(T1 o1, T2 o2, T3 o3, T4 o4, T5 o5, T6 o6); +} diff --git a/ModernKeePassLib/Interfaces/IDeepCloneable.cs b/ModernKeePassLib/Interfaces/IDeepCloneable.cs new file mode 100644 index 0000000..7d7647c --- /dev/null +++ b/ModernKeePassLib/Interfaces/IDeepCloneable.cs @@ -0,0 +1,37 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; + +namespace ModernKeePassLib.Interfaces +{ + /// + /// Interface for objects that are deeply cloneable. + /// + /// Reference type. + public interface IDeepCloneable where T : class + { + /// + /// Deeply clone the object. + /// + /// Cloned object. + T CloneDeep(); + } +} diff --git a/ModernKeePassLib/Interfaces/IStatusLogger.cs b/ModernKeePassLib/Interfaces/IStatusLogger.cs new file mode 100644 index 0000000..8a275cd --- /dev/null +++ b/ModernKeePassLib/Interfaces/IStatusLogger.cs @@ -0,0 +1,105 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; + +namespace ModernKeePassLib.Interfaces +{ + /// + /// Status message types. + /// + public enum LogStatusType + { + /// + /// Default type: simple information type. + /// + Info = 0, + + /// + /// Warning message. + /// + Warning, + + /// + /// Error message. + /// + Error, + + /// + /// Additional information. Depends on lines above. + /// + AdditionalInfo + } + + /// + /// Status logging interface. + /// + public interface IStatusLogger + { + /// + /// Function which needs to be called when logging is started. + /// + /// This string should roughly describe + /// the operation, of which the status is logged. + /// Specifies whether the + /// operation is written to the log or not. + void StartLogging(string strOperation, bool bWriteOperationToLog); + + /// + /// Function which needs to be called when logging is ended + /// (i.e. when no more messages will be logged and when the + /// percent value won't change any more). + /// + void EndLogging(); + + /// + /// Set the current progress in percent. + /// + /// Percent of work finished. + /// Returns true if the caller should continue + /// the current work. + bool SetProgress(uint uPercent); + + /// + /// Set the current status text. + /// + /// Status text. + /// Type of the message. + /// Returns true if the caller should continue + /// the current work. + bool SetText(string strNewText, LogStatusType lsType); + + /// + /// Check if the user cancelled the current work. + /// + /// Returns true if the caller should continue + /// the current work. + bool ContinueWork(); + } + + public sealed class NullStatusLogger : IStatusLogger + { + public void StartLogging(string strOperation, bool bWriteOperationToLog) { } + public void EndLogging() { } + public bool SetProgress(uint uPercent) { return true; } + public bool SetText(string strNewText, LogStatusType lsType) { return true; } + public bool ContinueWork() { return true; } + } +} diff --git a/ModernKeePassLib/Interfaces/IStructureItem.cs b/ModernKeePassLib/Interfaces/IStructureItem.cs new file mode 100644 index 0000000..93e017b --- /dev/null +++ b/ModernKeePassLib/Interfaces/IStructureItem.cs @@ -0,0 +1,37 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; + +namespace ModernKeePassLib.Interfaces +{ + public interface IStructureItem : ITimeLogger // Provides LocationChanged + { + PwUuid Uuid + { + get; + set; + } + + PwGroup ParentGroup + { + get; + } + } +} diff --git a/ModernKeePassLib/Interfaces/ITimeLogger.cs b/ModernKeePassLib/Interfaces/ITimeLogger.cs new file mode 100644 index 0000000..20dd502 --- /dev/null +++ b/ModernKeePassLib/Interfaces/ITimeLogger.cs @@ -0,0 +1,105 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; + +namespace ModernKeePassLib.Interfaces +{ + /// + /// Interface for objects that support various times (creation time, last + /// access time, last modification time and expiry time). Offers + /// several helper functions (for example a function to touch the current + /// object). + /// + public interface ITimeLogger + { + /// + /// The date/time when the object was created. + /// + DateTime CreationTime + { + get; + set; + } + + /// + /// The date/time when the object was last modified. + /// + DateTime LastModificationTime + { + get; + set; + } + + /// + /// The date/time when the object was last accessed. + /// + DateTime LastAccessTime + { + get; + set; + } + + /// + /// The date/time when the object expires. + /// + DateTime ExpiryTime + { + get; + set; + } + + /// + /// Flag that determines if the object does expire. + /// + bool Expires + { + get; + set; + } + + /// + /// Get or set the usage count of the object. To increase the usage + /// count by one, use the Touch function. + /// + ulong UsageCount + { + get; + set; + } + + /// + /// The date/time when the location of the object was last changed. + /// + DateTime LocationChanged + { + get; + set; + } + + /// + /// Touch the object. This function updates the internal last access + /// time. If the parameter is true, + /// the last modification time gets updated, too. Each time you call + /// Touch, the usage count of the object is increased by one. + /// + /// Update last modification time. + void Touch(bool bModified); + } +} diff --git a/ModernKeePassLib/Interfaces/IUIOperations.cs b/ModernKeePassLib/Interfaces/IUIOperations.cs new file mode 100644 index 0000000..b8e9a1a --- /dev/null +++ b/ModernKeePassLib/Interfaces/IUIOperations.cs @@ -0,0 +1,37 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +namespace ModernKeePassLib.Interfaces +{ + public interface IUIOperations + { + /// + /// Let the user interface save the current database. + /// + /// If true, the UI will not ask for + /// whether to synchronize or overwrite, it'll simply overwrite the + /// file. + /// Returns true if the file has been saved. + bool UIFileSave(bool bForceSave); + } +} diff --git a/ModernKeePassLib/Interfaces/IXmlSerializerEx.cs b/ModernKeePassLib/Interfaces/IXmlSerializerEx.cs new file mode 100644 index 0000000..04df73f --- /dev/null +++ b/ModernKeePassLib/Interfaces/IXmlSerializerEx.cs @@ -0,0 +1,33 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; +using System.IO; +using System.Xml; + +namespace ModernKeePassLib.Interfaces +{ + public interface IXmlSerializerEx + { + void Serialize(XmlWriter xmlWriter, object o); + object Deserialize(Stream s); + } +} diff --git a/ModernKeePassLib/Keys/CompositeKey.cs b/ModernKeePassLib/Keys/CompositeKey.cs new file mode 100644 index 0000000..a9dcff9 --- /dev/null +++ b/ModernKeePassLib/Keys/CompositeKey.cs @@ -0,0 +1,295 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Cryptography.KeyDerivation; +using ModernKeePassLib.Resources; +using ModernKeePassLib.Security; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Keys +{ + /// + /// Represents a key. A key can be build up using several user key data sources + /// like a password, a key file, the currently logged on user credentials, + /// the current computer ID, etc. + /// + public sealed class CompositeKey + { + private List m_vUserKeys = new List(); + + /// + /// List of all user keys contained in the current composite key. + /// + public IEnumerable UserKeys + { + get { return m_vUserKeys; } + } + + public uint UserKeyCount + { + get { return (uint)m_vUserKeys.Count; } + } + + /// + /// Construct a new, empty key object. + /// + public CompositeKey() + { + } + + // /// + // /// Deconstructor, clears up the key. + // /// + // ~CompositeKey() + // { + // Clear(); + // } + + // /// + // /// Clears the key. This function also erases all previously stored + // /// user key data objects. + // /// + // public void Clear() + // { + // foreach(IUserKey pKey in m_vUserKeys) + // pKey.Clear(); + // m_vUserKeys.Clear(); + // } + + /// + /// Add a user key. + /// + /// User key to add. + public void AddUserKey(IUserKey pKey) + { + Debug.Assert(pKey != null); if(pKey == null) throw new ArgumentNullException("pKey"); + + m_vUserKeys.Add(pKey); + } + + /// + /// Remove a user key. + /// + /// User key to remove. + /// Returns true if the key was removed successfully. + public bool RemoveUserKey(IUserKey pKey) + { + Debug.Assert(pKey != null); if(pKey == null) throw new ArgumentNullException("pKey"); + + Debug.Assert(m_vUserKeys.IndexOf(pKey) >= 0); + return m_vUserKeys.Remove(pKey); + } + + /// + /// Test whether the composite key contains a specific type of + /// user keys (password, key file, ...). If at least one user + /// key of that type is present, the function returns true. + /// + /// User key type. + /// Returns true, if the composite key contains + /// a user key of the specified type. + public bool ContainsType(Type tUserKeyType) + { + Debug.Assert(tUserKeyType != null); + if(tUserKeyType == null) throw new ArgumentNullException("tUserKeyType"); + + foreach(IUserKey pKey in m_vUserKeys) + { + if(pKey == null) { Debug.Assert(false); continue; } + +#if KeePassUAP + if(pKey.GetType() == tUserKeyType) + return true; +#else + if(tUserKeyType.IsInstanceOfType(pKey)) + return true; +#endif + } + + return false; + } + + /// + /// Get the first user key of a specified type. + /// + /// Type of the user key to get. + /// Returns the first user key of the specified type + /// or null if no key of that type is found. + public IUserKey GetUserKey(Type tUserKeyType) + { + Debug.Assert(tUserKeyType != null); + if(tUserKeyType == null) throw new ArgumentNullException("tUserKeyType"); + + foreach(IUserKey pKey in m_vUserKeys) + { + if(pKey == null) { Debug.Assert(false); continue; } + +#if KeePassUAP + if(pKey.GetType() == tUserKeyType) + return pKey; +#else + if(tUserKeyType.IsInstanceOfType(pKey)) + return pKey; +#endif + } + + return null; + } + + /// + /// Creates the composite key from the supplied user key sources (password, + /// key file, user account, computer ID, etc.). + /// + private byte[] CreateRawCompositeKey32() + { + ValidateUserKeys(); + + // Concatenate user key data + List lData = new List(); + int cbData = 0; + foreach(IUserKey pKey in m_vUserKeys) + { + ProtectedBinary b = pKey.KeyData; + if(b != null) + { + byte[] pbKeyData = b.ReadData(); + lData.Add(pbKeyData); + cbData += pbKeyData.Length; + } + } + + byte[] pbAllData = new byte[cbData]; + int p = 0; + foreach(byte[] pbData in lData) + { + Array.Copy(pbData, 0, pbAllData, p, pbData.Length); + p += pbData.Length; + MemUtil.ZeroByteArray(pbData); + } + Debug.Assert(p == cbData); + + byte[] pbHash = CryptoUtil.HashSha256(pbAllData); + MemUtil.ZeroByteArray(pbAllData); + return pbHash; + } + + public bool EqualsValue(CompositeKey ckOther) + { + if(ckOther == null) throw new ArgumentNullException("ckOther"); + + byte[] pbThis = CreateRawCompositeKey32(); + byte[] pbOther = ckOther.CreateRawCompositeKey32(); + bool bResult = MemUtil.ArraysEqual(pbThis, pbOther); + MemUtil.ZeroByteArray(pbOther); + MemUtil.ZeroByteArray(pbThis); + + return bResult; + } + + [Obsolete] + public ProtectedBinary GenerateKey32(byte[] pbKeySeed32, ulong uNumRounds) + { + Debug.Assert(pbKeySeed32 != null); + if(pbKeySeed32 == null) throw new ArgumentNullException("pbKeySeed32"); + Debug.Assert(pbKeySeed32.Length == 32); + if(pbKeySeed32.Length != 32) throw new ArgumentException("pbKeySeed32"); + + AesKdf kdf = new AesKdf(); + KdfParameters p = kdf.GetDefaultParameters(); + p.SetUInt64(AesKdf.ParamRounds, uNumRounds); + p.SetByteArray(AesKdf.ParamSeed, pbKeySeed32); + + return GenerateKey32(p); + } + + /// + /// Generate a 32-byte (256-bit) key from the composite key. + /// + public ProtectedBinary GenerateKey32(KdfParameters p) + { + if(p == null) { Debug.Assert(false); throw new ArgumentNullException("p"); } + + byte[] pbRaw32 = CreateRawCompositeKey32(); + if((pbRaw32 == null) || (pbRaw32.Length != 32)) + { Debug.Assert(false); return null; } + + KdfEngine kdf = KdfPool.Get(p.KdfUuid); + if(kdf == null) // CryptographicExceptions are translated to "file corrupted" + throw new Exception(KLRes.UnknownKdf + MessageService.NewParagraph + + KLRes.FileNewVerOrPlgReq + MessageService.NewParagraph + + "UUID: " + p.KdfUuid.ToHexString() + "."); + + byte[] pbTrf32 = kdf.Transform(pbRaw32, p); + if(pbTrf32 == null) { Debug.Assert(false); return null; } + + if(pbTrf32.Length != 32) + { + Debug.Assert(false); + pbTrf32 = CryptoUtil.HashSha256(pbTrf32); + } + + ProtectedBinary pbRet = new ProtectedBinary(true, pbTrf32); + MemUtil.ZeroByteArray(pbTrf32); + MemUtil.ZeroByteArray(pbRaw32); + return pbRet; + } + + private void ValidateUserKeys() + { + int nAccounts = 0; + + foreach(IUserKey uKey in m_vUserKeys) + { + if(uKey is KcpUserAccount) + ++nAccounts; + } + + if(nAccounts >= 2) + { + Debug.Assert(false); + throw new InvalidOperationException(); + } + } + } + + public sealed class InvalidCompositeKeyException : Exception + { + public override string Message + { + get + { + return KLRes.InvalidCompositeKey + MessageService.NewParagraph + + KLRes.InvalidCompositeKeyHint; + } + } + + /// + /// Construct a new invalid composite key exception. + /// + public InvalidCompositeKeyException() + { + } + } +} diff --git a/ModernKeePassLib/Keys/IUserKey.cs b/ModernKeePassLib/Keys/IUserKey.cs new file mode 100644 index 0000000..09cc4c7 --- /dev/null +++ b/ModernKeePassLib/Keys/IUserKey.cs @@ -0,0 +1,46 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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 ModernKeePassLib.Security; + +namespace ModernKeePassLib.Keys +{ + /// + /// Interface to a user key, like a password, key file data, etc. + /// + public interface IUserKey + { + /// + /// Get key data. Querying this property is fast (it returns a + /// reference to a cached ProtectedBinary object). + /// If no key data is available, null is returned. + /// + ProtectedBinary KeyData + { + get; + } + + // /// + // /// Clear the key and securely erase all security-critical information. + // /// + // void Clear(); + } +} diff --git a/ModernKeePassLib/Keys/KcpCustomKey.cs b/ModernKeePassLib/Keys/KcpCustomKey.cs new file mode 100644 index 0000000..5e6cca7 --- /dev/null +++ b/ModernKeePassLib/Keys/KcpCustomKey.cs @@ -0,0 +1,68 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Security; + +namespace ModernKeePassLib.Keys +{ + public sealed class KcpCustomKey : IUserKey + { + private readonly string m_strName; + private ProtectedBinary m_pbKey; + + /// + /// Name of the provider that generated the custom key. + /// + public string Name + { + get { return m_strName; } + } + + public ProtectedBinary KeyData + { + get { return m_pbKey; } + } + + public KcpCustomKey(string strName, byte[] pbKeyData, bool bPerformHash) + { + Debug.Assert(strName != null); if(strName == null) throw new ArgumentNullException("strName"); + Debug.Assert(pbKeyData != null); if(pbKeyData == null) throw new ArgumentNullException("pbKeyData"); + + m_strName = strName; + + if(bPerformHash) + { + byte[] pbRaw = CryptoUtil.HashSha256(pbKeyData); + m_pbKey = new ProtectedBinary(true, pbRaw); + } + else m_pbKey = new ProtectedBinary(true, pbKeyData); + } + + // public void Clear() + // { + // m_pbKey = null; + // } + } +} diff --git a/ModernKeePassLib/Keys/KcpKeyFile.cs b/ModernKeePassLib/Keys/KcpKeyFile.cs new file mode 100644 index 0000000..c62493a --- /dev/null +++ b/ModernKeePassLib/Keys/KcpKeyFile.cs @@ -0,0 +1,322 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Diagnostics; +using System.IO; +using System.Security; +using System.Text; +using System.Xml; +#if ModernKeePassLib +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using Windows.Security.Cryptography; +using Windows.Security.Cryptography.Core; +using Windows.Storage; +#else +using System.Security.Cryptography; +#endif + +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Resources; +using ModernKeePassLib.Security; +using ModernKeePassLib.Serialization; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Keys +{ + /// + /// Key files as provided by the user. + /// + public sealed class KcpKeyFile : IUserKey + { + private string m_strPath; + private ProtectedBinary m_pbKeyData; + + /// + /// Path to the key file. + /// + public string Path + { + get { return m_strPath; } + } + + /// + /// Get key data. Querying this property is fast (it returns a + /// reference to a cached ProtectedBinary object). + /// If no key data is available, null is returned. + /// + public ProtectedBinary KeyData + { + get { return m_pbKeyData; } + } + +#if ModernKeePassLib + public KcpKeyFile(StorageFile strKeyFile) + { + Construct(IOConnectionInfo.FromFile(strKeyFile), false); + } +#else + public KcpKeyFile(string strKeyFile) + { + Construct(IOConnectionInfo.FromPath(strKeyFile), false); + } + + public KcpKeyFile(string strKeyFile, bool bThrowIfDbFile) + { + Construct(IOConnectionInfo.FromPath(strKeyFile), bThrowIfDbFile); + } +#endif + + public KcpKeyFile(IOConnectionInfo iocKeyFile) + { + Construct(iocKeyFile, false); + } + + public KcpKeyFile(IOConnectionInfo iocKeyFile, bool bThrowIfDbFile) + { + Construct(iocKeyFile, bThrowIfDbFile); + } + + private void Construct(IOConnectionInfo iocFile, bool bThrowIfDbFile) + { + byte[] pbFileData = IOConnection.ReadFile(iocFile); + if(pbFileData == null) throw new FileNotFoundException(); + + if(bThrowIfDbFile && (pbFileData.Length >= 8)) + { + uint uSig1 = MemUtil.BytesToUInt32(MemUtil.Mid(pbFileData, 0, 4)); + uint uSig2 = MemUtil.BytesToUInt32(MemUtil.Mid(pbFileData, 4, 4)); + + if(((uSig1 == KdbxFile.FileSignature1) && + (uSig2 == KdbxFile.FileSignature2)) || + ((uSig1 == KdbxFile.FileSignaturePreRelease1) && + (uSig2 == KdbxFile.FileSignaturePreRelease2)) || + ((uSig1 == KdbxFile.FileSignatureOld1) && + (uSig2 == KdbxFile.FileSignatureOld2))) +#if KeePassLibSD + throw new Exception(KLRes.KeyFileDbSel); +#else + throw new InvalidDataException(KLRes.KeyFileDbSel); +#endif + } + + byte[] pbKey = LoadXmlKeyFile(pbFileData); + if(pbKey == null) pbKey = LoadKeyFile(pbFileData); + + if(pbKey == null) throw new InvalidOperationException(); + + m_strPath = iocFile.Path; + m_pbKeyData = new ProtectedBinary(true, pbKey); + + MemUtil.ZeroByteArray(pbKey); + } + + // public void Clear() + // { + // m_strPath = string.Empty; + // m_pbKeyData = null; + // } + + private static byte[] LoadKeyFile(byte[] pbFileData) + { + if(pbFileData == null) { Debug.Assert(false); return null; } + + int iLength = pbFileData.Length; + + byte[] pbKey = null; + if(iLength == 32) pbKey = LoadBinaryKey32(pbFileData); + else if(iLength == 64) pbKey = LoadHexKey32(pbFileData); + + if(pbKey == null) + pbKey = CryptoUtil.HashSha256(pbFileData); + + return pbKey; + } + + private static byte[] LoadBinaryKey32(byte[] pbFileData) + { + if(pbFileData == null) { Debug.Assert(false); return null; } + if(pbFileData.Length != 32) { Debug.Assert(false); return null; } + + return pbFileData; + } + + private static byte[] LoadHexKey32(byte[] pbFileData) + { + if(pbFileData == null) { Debug.Assert(false); return null; } + if(pbFileData.Length != 64) { Debug.Assert(false); return null; } + + try + { + if(!StrUtil.IsHexString(pbFileData, true)) return null; + + string strHex = StrUtil.Utf8.GetString(pbFileData, 0, pbFileData.Length); + byte[] pbKey = MemUtil.HexStringToByteArray(strHex); + if((pbKey == null) || (pbKey.Length != 32)) + { + Debug.Assert(false); + return null; + } + + return pbKey; + } + catch(Exception) { Debug.Assert(false); } + + return null; + } + + /// + /// Create a new, random key-file. + /// + /// Path where the key-file should be saved to. + /// If the file exists already, it will be overwritten. + /// Additional entropy used to generate + /// the random key. May be null (in this case only the KeePass-internal + /// random number generator is used). + /// Returns a FileSaveResult error code. +#if ModernKeePassLib + public static void Create(StorageFile strFilePath, byte[] pbAdditionalEntropy) +#else + public static void Create(string strFilePath, byte[] pbAdditionalEntropy) +#endif + { + byte[] pbKey32 = CryptoRandom.Instance.GetRandomBytes(32); + if(pbKey32 == null) throw new SecurityException(); + + byte[] pbFinalKey32; + if((pbAdditionalEntropy == null) || (pbAdditionalEntropy.Length == 0)) + pbFinalKey32 = pbKey32; + else + { + using(MemoryStream ms = new MemoryStream()) + { + MemUtil.Write(ms, pbAdditionalEntropy); + MemUtil.Write(ms, pbKey32); + + pbFinalKey32 = CryptoUtil.HashSha256(ms.ToArray()); + } + } + + CreateXmlKeyFile(strFilePath, pbFinalKey32); + } + + // ================================================================ + // XML Key Files + // ================================================================ + + // Sample XML file: + // + // + // + // 1.00 + // + // + // ySFoKuCcJblw8ie6RkMBdVCnAf4EedSch7ItujK6bmI= + // + // + + private const string RootElementName = "KeyFile"; + private const string MetaElementName = "Meta"; + private const string VersionElementName = "Version"; + private const string KeyElementName = "Key"; + private const string KeyDataElementName = "Data"; + + private static byte[] LoadXmlKeyFile(byte[] pbFileData) + { + if(pbFileData == null) { Debug.Assert(false); return null; } + + MemoryStream ms = new MemoryStream(pbFileData, false); + byte[] pbKeyData = null; + + try + { + XmlDocument doc = XmlUtilEx.CreateXmlDocument(); + doc.Load(ms); + + XmlElement el = doc.DocumentElement; + if((el == null) || !el.Name.Equals(RootElementName)) return null; + if(el.ChildNodes.Count < 2) return null; + + foreach(XmlNode xmlChild in el.ChildNodes) + { + if(xmlChild.Name.Equals(MetaElementName)) { } // Ignore Meta + else if(xmlChild.Name == KeyElementName) + { + foreach(XmlNode xmlKeyChild in xmlChild.ChildNodes) + { + if(xmlKeyChild.Name == KeyDataElementName) + { + if(pbKeyData == null) + pbKeyData = Convert.FromBase64String(xmlKeyChild.InnerText); + } + } + } + } + } + catch(Exception) { pbKeyData = null; } + finally { ms.Dispose(); } + + return pbKeyData; + } +#if ModernKeePassLib + private static void CreateXmlKeyFile(StorageFile strFile, byte[] pbKeyData) +#else + private static void CreateXmlKeyFile(string strFile, byte[] pbKeyData) +#endif + { + Debug.Assert(strFile != null); + if(strFile == null) throw new ArgumentNullException("strFile"); + Debug.Assert(pbKeyData != null); + if(pbKeyData == null) throw new ArgumentNullException("pbKeyData"); + +#if ModernKeePassLib + IOConnectionInfo ioc = IOConnectionInfo.FromFile(strFile); +#else + IOConnectionInfo ioc = IOConnectionInfo.FromPath(strFile); +#endif + using(Stream s = IOConnection.OpenWrite(ioc)) + { + using(XmlWriter xw = XmlUtilEx.CreateXmlWriter(s)) + { + xw.WriteStartDocument(); + xw.WriteStartElement(RootElementName); // + + xw.WriteStartElement(MetaElementName); // + xw.WriteStartElement(VersionElementName); // + xw.WriteString("1.00"); + xw.WriteEndElement(); // + xw.WriteEndElement(); // + + xw.WriteStartElement(KeyElementName); // + + xw.WriteStartElement(KeyDataElementName); // + xw.WriteString(Convert.ToBase64String(pbKeyData)); + xw.WriteEndElement(); // + + xw.WriteEndElement(); // + + xw.WriteEndElement(); // + xw.WriteEndDocument(); + } + } + } + } +} diff --git a/ModernKeePassLib/Keys/KcpPassword.cs b/ModernKeePassLib/Keys/KcpPassword.cs new file mode 100644 index 0000000..b69bae4 --- /dev/null +++ b/ModernKeePassLib/Keys/KcpPassword.cs @@ -0,0 +1,111 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Diagnostics; +using System.Text; + +using ModernKeePassLib.Security; +using ModernKeePassLib.Utility; +using ModernKeePassLib.Cryptography; + +namespace ModernKeePassLib.Keys +{ + /// + /// Master password/passphrase as provided by the user. + /// + public sealed class KcpPassword : IUserKey + { + private ProtectedString m_psPassword = null; // Optional + private ProtectedBinary m_pbKeyData; + + /// + /// Get the password as protected string. This is null + /// unless remembering the password has been turned on. + /// + public ProtectedString Password + { + get { return m_psPassword; } + } + + /// + /// Get key data. Querying this property is fast (it returns a + /// reference to a cached ProtectedBinary object). + /// If no key data is available, null is returned. + /// + public ProtectedBinary KeyData + { + get { return m_pbKeyData; } + } + + public KcpPassword(byte[] pbPasswordUtf8) + { + SetKey(pbPasswordUtf8, true); + } + + public KcpPassword(byte[] pbPasswordUtf8, bool bRememberPassword) + { + SetKey(pbPasswordUtf8, bRememberPassword); + } + + public KcpPassword(string strPassword) + { + byte[] pb = StrUtil.Utf8.GetBytes(strPassword); + try { SetKey(pb, true); } + finally { MemUtil.ZeroByteArray(pb); } + } + + private void SetKey(byte[] pbPasswordUtf8, bool bRememberPassword) + { + Debug.Assert(pbPasswordUtf8 != null); + if(pbPasswordUtf8 == null) throw new ArgumentNullException("pbPasswordUtf8"); + +#if (DEBUG && !KeePassLibSD) + Debug.Assert(ValidatePassword(pbPasswordUtf8)); +#endif + + byte[] pbRaw = CryptoUtil.HashSha256(pbPasswordUtf8); + try { m_pbKeyData = new ProtectedBinary(true, pbRaw); } + finally { MemUtil.ZeroByteArray(pbRaw); } + + if(bRememberPassword) + m_psPassword = new ProtectedString(true, pbPasswordUtf8); + } + + // public void Clear() + // { + // m_psPassword = null; + // m_pbKeyData = null; + // } + +#if (DEBUG && !KeePassLibSD) + private static bool ValidatePassword(byte[] pb) + { + try + { + string str = StrUtil.Utf8.GetString(pb); + return str.IsNormalized(NormalizationForm.FormC); + } + catch(Exception) { Debug.Assert(false); } + + return false; + } +#endif + } +} diff --git a/ModernKeePassLib/Keys/KcpUserAccount.cs b/ModernKeePassLib/Keys/KcpUserAccount.cs new file mode 100644 index 0000000..452ba39 --- /dev/null +++ b/ModernKeePassLib/Keys/KcpUserAccount.cs @@ -0,0 +1,167 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Diagnostics; +using System.IO; +using System.Security; + +#if ModernKeePassLib +using Windows.Storage; +using System.Security.Cryptography; +#endif + +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Native; +using ModernKeePassLib.Resources; +using ModernKeePassLib.Security; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Keys +{ + /// + /// A user key depending on the currently logged on Windows user account. + /// + public sealed class KcpUserAccount : IUserKey + { + private ProtectedBinary m_pbKeyData = null; + + // Constant initialization vector (unique for KeePass) + private static readonly byte[] m_pbEntropy = new byte[] { + 0xDE, 0x13, 0x5B, 0x5F, 0x18, 0xA3, 0x46, 0x70, + 0xB2, 0x57, 0x24, 0x29, 0x69, 0x88, 0x98, 0xE6 + }; + + private const string UserKeyFileName = "ProtectedUserKey.bin"; + + /// + /// Get key data. Querying this property is fast (it returns a + /// reference to a cached ProtectedBinary object). + /// If no key data is available, null is returned. + /// + public ProtectedBinary KeyData + { + get { return m_pbKeyData; } + } + + /// + /// Construct a user account key. + /// + public KcpUserAccount() + { + if(!CryptoUtil.IsProtectedDataSupported) + throw new PlatformNotSupportedException(); // Windows 98/ME + + byte[] pbKey = LoadUserKey(false); + if(pbKey == null) pbKey = CreateUserKey(); + if(pbKey == null) // Should never happen + { + Debug.Assert(false); + throw new SecurityException(KLRes.UserAccountKeyError); + } + + m_pbKeyData = new ProtectedBinary(true, pbKey); + MemUtil.ZeroByteArray(pbKey); + } + + // public void Clear() + // { + // m_pbKeyData = null; + // } + + private static string GetUserKeyFilePath(bool bCreate) + { +#if ModernKeePassLib + string strUserDir = Windows.Storage.ApplicationData.Current.RoamingFolder.Path; +#else + string strUserDir = Environment.GetFolderPath( + Environment.SpecialFolder.ApplicationData); +#endif + + strUserDir = UrlUtil.EnsureTerminatingSeparator(strUserDir, false); + strUserDir += PwDefs.ShortProductName; + +#if !ModernKeePassLib + + if(bCreate && !Directory.Exists(strUserDir)) + Directory.CreateDirectory(strUserDir); +#endif + + strUserDir = UrlUtil.EnsureTerminatingSeparator(strUserDir, false); + return (strUserDir + UserKeyFileName); + } + + private static byte[] LoadUserKey(bool bThrow) + { + byte[] pbKey = null; + +#if !KeePassLibSD + try + { + string strFilePath = GetUserKeyFilePath(false); + +#if ModernKeePassLib + var fileStream = StorageFile.GetFileFromPathAsync(strFilePath).GetAwaiter().GetResult().OpenStreamForReadAsync().GetAwaiter().GetResult(); + var pbProtectedKey = new byte[(int)fileStream.Length]; + fileStream.Read(pbProtectedKey, 0, (int)fileStream.Length); + fileStream.Dispose(); +#else + byte[] pbProtectedKey = File.ReadAllBytes(strFilePath); +#endif + + pbKey = CryptoUtil.UnprotectData(pbProtectedKey, m_pbEntropy, + DataProtectionScope.CurrentUser); + } + catch(Exception) + { + if(bThrow) throw; + pbKey = null; + } +#endif + + return pbKey; + } + + private static byte[] CreateUserKey() + { +#if KeePassLibSD + return null; +#else + string strFilePath = GetUserKeyFilePath(true); + + byte[] pbRandomKey = CryptoRandom.Instance.GetRandomBytes(64); + byte[] pbProtectedKey = CryptoUtil.ProtectData(pbRandomKey, + m_pbEntropy, DataProtectionScope.CurrentUser); +#if ModernKeePassLib + var fileStream = StorageFile.GetFileFromPathAsync(strFilePath).GetAwaiter().GetResult().OpenStreamForWriteAsync().GetAwaiter().GetResult(); + fileStream.Write(pbProtectedKey, 0, (int)fileStream.Length); + fileStream.Dispose(); +#else + File.WriteAllBytes(strFilePath, pbProtectedKey); +#endif + + byte[] pbKey = LoadUserKey(true); + Debug.Assert(MemUtil.ArraysEqual(pbKey, pbRandomKey)); + + MemUtil.ZeroByteArray(pbRandomKey); + return pbKey; +#endif + } + } +} diff --git a/ModernKeePassLib/Keys/KeyProvider.cs b/ModernKeePassLib/Keys/KeyProvider.cs new file mode 100644 index 0000000..16a9f1d --- /dev/null +++ b/ModernKeePassLib/Keys/KeyProvider.cs @@ -0,0 +1,152 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +using ModernKeePassLib.Serialization; + +namespace ModernKeePassLib.Keys +{ + public sealed class KeyProviderQueryContext + { + private IOConnectionInfo m_ioInfo; + public IOConnectionInfo DatabaseIOInfo + { + get { return m_ioInfo; } + } + + public string DatabasePath + { + get { return m_ioInfo.Path; } + } + + private bool m_bCreatingNewKey; + public bool CreatingNewKey + { + get { return m_bCreatingNewKey; } + } + + private bool m_bSecDesktop; + public bool IsOnSecureDesktop + { + get { return m_bSecDesktop; } + } + + public KeyProviderQueryContext(IOConnectionInfo ioInfo, bool bCreatingNewKey, + bool bOnSecDesktop) + { + if(ioInfo == null) throw new ArgumentNullException("ioInfo"); + + m_ioInfo = ioInfo.CloneDeep(); + m_bCreatingNewKey = bCreatingNewKey; + m_bSecDesktop = bOnSecDesktop; + } + } + + public abstract class KeyProvider + { + /// + /// Name of your key provider (should be unique). + /// + public abstract string Name + { + get; + } + + /// + /// Property indicating whether the provider is exclusive. + /// If the provider is exclusive, KeePass doesn't allow other + /// key sources (master password, Windows user account, ...) + /// to be combined with the provider. + /// Key providers typically should return false + /// (to allow non-exclusive use), i.e. don't override this + /// property. + /// + public virtual bool Exclusive + { + get { return false; } + } + + /// + /// Property that specifies whether the returned key data + /// gets hashed by KeePass first or is written directly to + /// the user key data stream. + /// Standard key provider plugins should return false + /// (i.e. don't overwrite this property). Returning true + /// may cause severe security problems and is highly + /// discouraged. + /// + public virtual bool DirectKey + { + get { return false; } + } + + // public virtual PwIcon ImageIndex + // { + // get { return PwIcon.UserKey; } + // } + + /// + /// This property specifies whether the GetKey method might + /// show a form or dialog. If there is any chance that the method shows + /// one, this property must return true. Only if it's guaranteed + /// that the GetKey method doesn't show any form or dialog, this + /// property should return false. + /// + public virtual bool GetKeyMightShowGui + { + get { return true; } + } + + /// + /// This property specifies whether the key provider is compatible + /// with the secure desktop mode. This almost never is the case, + /// so you usually won't override this property. + /// + public virtual bool SecureDesktopCompatible + { + get { return false; } + } + + public abstract byte[] GetKey(KeyProviderQueryContext ctx); + } + +#if DEBUG + public sealed class SampleKeyProvider : KeyProvider + { + public override string Name + { + get { return "Built-In Sample Key Provider"; } + } + + // Do not just copy this to your own key provider class! See above. + public override bool GetKeyMightShowGui + { + get { return false; } + } + + public override byte[] GetKey(KeyProviderQueryContext ctx) + { + return new byte[]{ 2, 3, 5, 7, 11, 13 }; + } + } +#endif +} diff --git a/ModernKeePassLib/Keys/KeyProviderPool.cs b/ModernKeePassLib/Keys/KeyProviderPool.cs new file mode 100644 index 0000000..26183d9 --- /dev/null +++ b/ModernKeePassLib/Keys/KeyProviderPool.cs @@ -0,0 +1,105 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; +using System.Collections.Generic; +using System.Text; +using System.Diagnostics; + +namespace ModernKeePassLib.Keys +{ + public sealed class KeyProviderPool : IEnumerable + { + private List m_vProviders = new List(); + + public int Count + { + get { return m_vProviders.Count; } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return m_vProviders.GetEnumerator(); + } + + public IEnumerator GetEnumerator() + { + return m_vProviders.GetEnumerator(); + } + + public void Add(KeyProvider prov) + { + Debug.Assert(prov != null); if(prov == null) throw new ArgumentNullException("prov"); + + m_vProviders.Add(prov); + } + + public bool Remove(KeyProvider prov) + { + Debug.Assert(prov != null); if(prov == null) throw new ArgumentNullException("prov"); + + return m_vProviders.Remove(prov); + } + + public KeyProvider Get(string strProviderName) + { + if(strProviderName == null) throw new ArgumentNullException("strProviderName"); + + foreach(KeyProvider prov in m_vProviders) + { + if(prov.Name == strProviderName) return prov; + } + + return null; + } + + public bool IsKeyProvider(string strName) + { + Debug.Assert(strName != null); if(strName == null) throw new ArgumentNullException("strName"); + + foreach(KeyProvider prov in m_vProviders) + { + if(prov.Name == strName) return true; + } + + return false; + } + + internal byte[] GetKey(string strProviderName, KeyProviderQueryContext ctx, + out bool bPerformHash) + { + Debug.Assert(strProviderName != null); if(strProviderName == null) throw new ArgumentNullException("strProviderName"); + + bPerformHash = true; + + foreach(KeyProvider prov in m_vProviders) + { + if(prov.Name == strProviderName) + { + bPerformHash = !prov.DirectKey; + return prov.GetKey(ctx); + } + } + + Debug.Assert(false); + return null; + } + } +} diff --git a/ModernKeePassLib/Keys/KeyValidator.cs b/ModernKeePassLib/Keys/KeyValidator.cs new file mode 100644 index 0000000..cb9a40a --- /dev/null +++ b/ModernKeePassLib/Keys/KeyValidator.cs @@ -0,0 +1,51 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +namespace ModernKeePassLib.Keys +{ + public enum KeyValidationType + { + MasterPassword = 0 + } + + public abstract class KeyValidator + { + /// + /// Name of your key validator (should be unique). + /// + public abstract string Name + { + get; + } + + /// + /// Validate a key. + /// + /// Key to validate. + /// Type of the validation to perform. + /// Returns null, if the validation is successful. + /// If there's a problem with the key, the returned string describes + /// the problem. + public abstract string Validate(string strKey, KeyValidationType t); + } +} diff --git a/ModernKeePassLib/Keys/KeyValidatorPool.cs b/ModernKeePassLib/Keys/KeyValidatorPool.cs new file mode 100644 index 0000000..2b0a98e --- /dev/null +++ b/ModernKeePassLib/Keys/KeyValidatorPool.cs @@ -0,0 +1,86 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; +using System.Collections.Generic; +using System.Text; +using System.Diagnostics; + +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Keys +{ + public sealed class KeyValidatorPool : IEnumerable + { + private List m_vValidators = new List(); + + public int Count + { + get { return m_vValidators.Count; } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return m_vValidators.GetEnumerator(); + } + + public IEnumerator GetEnumerator() + { + return m_vValidators.GetEnumerator(); + } + + public void Add(KeyValidator v) + { + Debug.Assert(v != null); if(v == null) throw new ArgumentNullException("v"); + + m_vValidators.Add(v); + } + + public bool Remove(KeyValidator v) + { + Debug.Assert(v != null); if(v == null) throw new ArgumentNullException("v"); + + return m_vValidators.Remove(v); + } + + public string Validate(string strKey, KeyValidationType t) + { + Debug.Assert(strKey != null); if(strKey == null) throw new ArgumentNullException("strKey"); + + foreach(KeyValidator v in m_vValidators) + { + string strResult = v.Validate(strKey, t); + if(strResult != null) return strResult; + } + + return null; + } + + public string Validate(byte[] pbKeyUtf8, KeyValidationType t) + { + Debug.Assert(pbKeyUtf8 != null); if(pbKeyUtf8 == null) throw new ArgumentNullException("pbKeyUtf8"); + + if(m_vValidators.Count == 0) return null; + + string strKey = StrUtil.Utf8.GetString(pbKeyUtf8, 0, pbKeyUtf8.Length); + return Validate(strKey, t); + } + } +} diff --git a/ModernKeePassLib/Keys/UserKeyType.cs b/ModernKeePassLib/Keys/UserKeyType.cs new file mode 100644 index 0000000..3b7528f --- /dev/null +++ b/ModernKeePassLib/Keys/UserKeyType.cs @@ -0,0 +1,33 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; + +namespace ModernKeePassLib.Keys +{ + [Flags] + public enum UserKeyType + { + None = 0, + Other = 1, + Password = 2, + KeyFile = 4, + UserAccount = 8 + } +} diff --git a/ModernKeePassLib/Libs/Windows.winmd b/ModernKeePassLib/Libs/Windows.winmd new file mode 100644 index 0000000..d6498e8 Binary files /dev/null and b/ModernKeePassLib/Libs/Windows.winmd differ diff --git a/ModernKeePassLib/ModernKeePassLib.csproj b/ModernKeePassLib/ModernKeePassLib.csproj new file mode 100644 index 0000000..a0de3e6 --- /dev/null +++ b/ModernKeePassLib/ModernKeePassLib.csproj @@ -0,0 +1,74 @@ + + + + netstandard2.0 + true + 2.41.0 + Geoffroy Bonneville + https://www.gnu.org/licenses/gpl-3.0.en.html + https://github.com/wismna/ModernKeePass + Portable KeePass Password Management Library that targets .Net Standard and WinRT. Allows reading, editing and writing to KeePass 2.x databases. + wismna + ModernKeePassLib + Updated to version 2.41 + KeePass KeePassLib Portable PCL NetStandard ModernKeePass + Copyright © 2019 Geoffroy Bonneville + + + + TRACE;ModernKeePassLib + + + + TRACE;ModernKeePassLib + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ..\..\..\..\..\..\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.17134.0\Windows.winmd + true + + + + diff --git a/ModernKeePassLib/Native/ClipboardU.cs b/ModernKeePassLib/Native/ClipboardU.cs new file mode 100644 index 0000000..ccfc1b6 --- /dev/null +++ b/ModernKeePassLib/Native/ClipboardU.cs @@ -0,0 +1,190 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Reflection; +using System.Text; + +namespace ModernKeePassLib.Native +{ + internal static class ClipboardU + { + private const string XSel = "xsel"; + private const string XSelV = "--version"; + private const string XSelR = "--output --clipboard"; + private const string XSelC = "--clear --clipboard"; + private const string XSelW = "--input --clipboard"; + private const string XSelND = " --nodetach"; + private const AppRunFlags XSelWF = AppRunFlags.WaitForExit; + + private static bool? g_obXSel = null; + + public static string GetText() + { + // System.Windows.Forms.Clipboard doesn't work properly, + // see Mono workaround 1530 + + // string str = GtkGetText(); + // if(str != null) return str; + + return XSelGetText(); + } + + public static bool SetText(string strText, bool bMayBlock) + { + string str = (strText ?? string.Empty); + + // System.Windows.Forms.Clipboard doesn't work properly, + // see Mono workaround 1530 + + // if(GtkSetText(str)) return true; + + return XSelSetText(str, bMayBlock); + } + + // ============================================================= + // LibGTK + + // Even though GTK+ 3 appears to be loaded already, performing a + // P/Invoke of LibGTK's gtk_init_check function terminates the + // process (!) with the following error message: + // "Gtk-ERROR **: GTK+ 2.x symbols detected. Using GTK+ 2.x and + // GTK+ 3 in the same process is not supported". + + /* private static bool GtkInit() + { + try + { + // GTK requires GLib; + // the following throws if and only if GLib is unavailable + NativeMethods.g_free(IntPtr.Zero); + + if(NativeMethods.gtk_init_check(IntPtr.Zero, IntPtr.Zero) != + NativeMethods.G_FALSE) + return true; + + Debug.Assert(false); + } + catch(Exception) { Debug.Assert(false); } + + return false; + } + + private static string GtkGetText() + { + IntPtr lpText = IntPtr.Zero; + try + { + if(GtkInit()) + { + IntPtr h = NativeMethods.gtk_clipboard_get( + NativeMethods.GDK_SELECTION_CLIPBOARD); + if(h != IntPtr.Zero) + { + lpText = NativeMethods.gtk_clipboard_wait_for_text(h); + if(lpText != IntPtr.Zero) + return NativeMethods.Utf8ZToString(lpText); + } + } + } + catch(Exception) { Debug.Assert(false); } + finally + { + try { NativeMethods.g_free(lpText); } + catch(Exception) { Debug.Assert(false); } + } + + return null; + } + + private static bool GtkSetText(string str) + { + IntPtr lpText = IntPtr.Zero; + try + { + if(GtkInit()) + { + lpText = NativeMethods.Utf8ZFromString(str ?? string.Empty); + if(lpText == IntPtr.Zero) { Debug.Assert(false); return false; } + + bool b = false; + for(int i = 0; i < 2; ++i) + { + IntPtr h = NativeMethods.gtk_clipboard_get((i == 0) ? + NativeMethods.GDK_SELECTION_PRIMARY : + NativeMethods.GDK_SELECTION_CLIPBOARD); + if(h != IntPtr.Zero) + { + NativeMethods.gtk_clipboard_clear(h); + NativeMethods.gtk_clipboard_set_text(h, lpText, -1); + NativeMethods.gtk_clipboard_store(h); + b = true; + } + } + + return b; + } + } + catch(Exception) { Debug.Assert(false); } + finally { NativeMethods.Utf8ZFree(lpText); } + + return false; + } */ + + // ============================================================= + // XSel + + private static bool XSelInit() + { + if(g_obXSel.HasValue) return g_obXSel.Value; + + string strTest = NativeLib.RunConsoleApp(XSel, XSelV); + + bool b = (strTest != null); + g_obXSel = b; + return b; + } + + private static string XSelGetText() + { + if(!XSelInit()) return null; + + return NativeLib.RunConsoleApp(XSel, XSelR); + } + + private static bool XSelSetText(string str, bool bMayBlock) + { + if(!XSelInit()) return false; + + string strOpt = (bMayBlock ? XSelND : string.Empty); + + // xsel with an empty input can hang, thus use --clear + if(str.Length == 0) + return (NativeLib.RunConsoleApp(XSel, XSelC + strOpt, + null, XSelWF) != null); + + // Use --nodetach to prevent clipboard corruption; + // https://sourceforge.net/p/keepass/bugs/1603/ + return (NativeLib.RunConsoleApp(XSel, XSelW + strOpt, + str, XSelWF) != null); + } + } +} diff --git a/ModernKeePassLib/Native/Native.PCL.cs b/ModernKeePassLib/Native/Native.PCL.cs new file mode 100644 index 0000000..8c7a312 --- /dev/null +++ b/ModernKeePassLib/Native/Native.PCL.cs @@ -0,0 +1,90 @@ +using System; + +namespace ModernKeePassLib.Native +{ + internal static class NativeLib + { + public static ulong MonoVersion { + get { throw new NotImplementedException(); } + } + + public static bool IsUnix() + { + return true; + } + + public static bool TransformKey256(byte[] pbNative, byte[] pbSeed, ulong uRounds) + { + return false; + } + + public static System.PlatformID GetPlatformID() + { + return Environment.OSVersion.Platform; + } + } + + internal static class NativeMethods + { + public static bool SupportsStrCmpNaturally => false; + + internal const int GCRY_CIPHER_AES256 = 9; + internal const int GCRY_CIPHER_MODE_ECB = 1; + + public static int StrCmpNaturally (string s1, string s2) + { + throw new NotImplementedException(); + } + + internal static void gcry_check_version(IntPtr zero) + { + throw new NotImplementedException(); + } + + public static void gcry_cipher_open(ref IntPtr intPtr, object gcryCipherAes256, object gcryCipherModeEcb, int i) + { + throw new NotImplementedException(); + } + + internal static int gcry_cipher_setkey(IntPtr h, IntPtr pSeed32, IntPtr n32) + { + throw new NotImplementedException(); + } + + internal static void gcry_cipher_close(IntPtr h) + { + throw new NotImplementedException(); + } + + internal static int gcry_cipher_encrypt(IntPtr h, IntPtr pData32, IntPtr n32, IntPtr zero1, IntPtr zero2) + { + throw new NotImplementedException(); + } + + public static string GetUserRuntimeDir() + { + throw new NotImplementedException(); + } + } + + internal enum MemoryProtectionScope + { + CrossProcess, + SameLogon, + SameProcess + } + + internal static class ProtectedMemory + { + public static byte[] Protect(byte[] userData, MemoryProtectionScope scope) + { + throw new NotImplementedException(); + } + + public static byte[] Unprotect(byte[] userData, MemoryProtectionScope scope) + { + throw new NotImplementedException(); + } + } +} + diff --git a/ModernKeePassLib/Native/NativeLib.cs b/ModernKeePassLib/Native/NativeLib.cs new file mode 100644 index 0000000..a10f433 --- /dev/null +++ b/ModernKeePassLib/Native/NativeLib.cs @@ -0,0 +1,460 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; + +#if !KeePassUAP +using System.IO; +using System.Threading; +#if !ModernKeePassLib +using System.Windows.Forms; +#endif +#endif + +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Native +{ + /// + /// Interface to native library (library containing fast versions of + /// several cryptographic functions). + /// + public static class NativeLib + { + private static bool m_bAllowNative = true; + + /// + /// If this property is set to true, the native library is used. + /// If it is false, all calls to functions in this class will fail. + /// + public static bool AllowNative + { + get { return m_bAllowNative; } + set { m_bAllowNative = value; } + } + + private static ulong? m_ouMonoVersion = null; + public static ulong MonoVersion + { + get + { + if(m_ouMonoVersion.HasValue) return m_ouMonoVersion.Value; + + ulong uVersion = 0; + try + { + Type t = Type.GetType("Mono.Runtime"); + if(t != null) + { + MethodInfo mi = t.GetMethod("GetDisplayName", + BindingFlags.NonPublic | BindingFlags.Static); + if(mi != null) + { + string strName = (mi.Invoke(null, null) as string); + if(!string.IsNullOrEmpty(strName)) + { + Match m = Regex.Match(strName, "\\d+(\\.\\d+)+"); + if(m.Success) + uVersion = StrUtil.ParseVersion(m.Value); + else { Debug.Assert(false); } + } + else { Debug.Assert(false); } + } + else { Debug.Assert(false); } + } + } + catch(Exception) { Debug.Assert(false); } + + m_ouMonoVersion = uVersion; + return uVersion; + } + } + + /// + /// Determine if the native library is installed. + /// + /// Returns true, if the native library is installed. + public static bool IsLibraryInstalled() + { + byte[] pDummy0 = new byte[32]; + byte[] pDummy1 = new byte[32]; + + // Save the native state + bool bCachedNativeState = m_bAllowNative; + + // Temporarily allow native functions and try to load the library + m_bAllowNative = true; + bool bResult = TransformKey256(pDummy0, pDummy1, 16); + + // Pop native state and return result + m_bAllowNative = bCachedNativeState; + return bResult; + } + + private static bool? m_bIsUnix = null; + public static bool IsUnix() + { + if(m_bIsUnix.HasValue) return m_bIsUnix.Value; + + PlatformID p = GetPlatformID(); + + // Mono defines Unix as 128 in early .NET versions +#if !KeePassLibSD + m_bIsUnix = ((p == PlatformID.Unix) || (p == PlatformID.MacOSX) || + ((int)p == 128)); +#else + m_bIsUnix = (((int)p == 4) || ((int)p == 6) || ((int)p == 128)); +#endif + return m_bIsUnix.Value; + } + + private static PlatformID? m_platID = null; + public static PlatformID GetPlatformID() + { + if(m_platID.HasValue) return m_platID.Value; + +#if KeePassUAP + m_platID = EnvironmentExt.OSVersion.Platform; +#else + m_platID = Environment.OSVersion.Platform; +#endif + +#if (!KeePassLibSD && !KeePassUAP) + // Mono returns PlatformID.Unix on Mac OS X, workaround this + if(m_platID.Value == PlatformID.Unix) + { + if((RunConsoleApp("uname", null) ?? string.Empty).Trim().Equals( + "Darwin", StrUtil.CaseIgnoreCmp)) + m_platID = PlatformID.MacOSX; + } +#endif + + return m_platID.Value; + } + + private static DesktopType? m_tDesktop = null; + public static DesktopType GetDesktopType() + { + if(!m_tDesktop.HasValue) + { + DesktopType t = DesktopType.None; + if(!IsUnix()) t = DesktopType.Windows; + else + { + try + { + string strXdg = (Environment.GetEnvironmentVariable( + "XDG_CURRENT_DESKTOP") ?? string.Empty).Trim(); + string strGdm = (Environment.GetEnvironmentVariable( + "GDMSESSION") ?? string.Empty).Trim(); + StringComparison sc = StrUtil.CaseIgnoreCmp; + + if(strXdg.Equals("Unity", sc)) + t = DesktopType.Unity; + else if(strXdg.Equals("LXDE", sc)) + t = DesktopType.Lxde; + else if(strXdg.Equals("XFCE", sc)) + t = DesktopType.Xfce; + else if(strXdg.Equals("MATE", sc)) + t = DesktopType.Mate; + else if(strXdg.Equals("X-Cinnamon", sc)) // Mint 18.3 + t = DesktopType.Cinnamon; + else if(strXdg.Equals("Pantheon", sc)) // Elementary OS + t = DesktopType.Pantheon; + else if(strXdg.Equals("KDE", sc) || // Mint 16, Kubuntu 17.10 + strGdm.Equals("kde-plasma", sc)) // Ubuntu 12.04 + t = DesktopType.Kde; + else if(strXdg.Equals("GNOME", sc)) + { + if(strGdm.Equals("cinnamon", sc)) // Mint 13 + t = DesktopType.Cinnamon; + else t = DesktopType.Gnome; // Fedora 27 + } + else if(strXdg.Equals("ubuntu:GNOME", sc)) + t = DesktopType.Gnome; + } + catch(Exception) { Debug.Assert(false); } + } + + m_tDesktop = t; + } + + return m_tDesktop.Value; + } + +#if (!KeePassLibSD && !KeePassUAP) + public static string RunConsoleApp(string strAppPath, string strParams) + { + return RunConsoleApp(strAppPath, strParams, null); + } + + public static string RunConsoleApp(string strAppPath, string strParams, + string strStdInput) + { + return RunConsoleApp(strAppPath, strParams, strStdInput, + (AppRunFlags.GetStdOutput | AppRunFlags.WaitForExit)); + } + + private delegate string RunProcessDelegate(); + + public static string RunConsoleApp(string strAppPath, string strParams, + string strStdInput, AppRunFlags f) + { + if(strAppPath == null) throw new ArgumentNullException("strAppPath"); + if(strAppPath.Length == 0) throw new ArgumentException("strAppPath"); + + bool bStdOut = ((f & AppRunFlags.GetStdOutput) != AppRunFlags.None); + + RunProcessDelegate fnRun = delegate() + { + Process pToDispose = null; + try + { + ProcessStartInfo psi = new ProcessStartInfo(); + + psi.CreateNoWindow = true; + psi.FileName = strAppPath; + psi.WindowStyle = ProcessWindowStyle.Hidden; + psi.UseShellExecute = false; + psi.RedirectStandardOutput = bStdOut; + + if(strStdInput != null) psi.RedirectStandardInput = true; + + if(!string.IsNullOrEmpty(strParams)) psi.Arguments = strParams; + + Process p = Process.Start(psi); + pToDispose = p; + + if(strStdInput != null) + { + EnsureNoBom(p.StandardInput); + + p.StandardInput.Write(strStdInput); + p.StandardInput.Close(); + } + + string strOutput = string.Empty; + if(bStdOut) strOutput = p.StandardOutput.ReadToEnd(); + + if((f & AppRunFlags.WaitForExit) != AppRunFlags.None) + p.WaitForExit(); + else if((f & AppRunFlags.GCKeepAlive) != AppRunFlags.None) + { + pToDispose = null; // Thread disposes it + + Thread th = new Thread(delegate() + { + try { p.WaitForExit(); p.Dispose(); } + catch(Exception) { Debug.Assert(false); } + }); + th.Start(); + } + + return strOutput; + } +#if DEBUG + catch(Exception ex) { Debug.Assert(ex is ThreadAbortException); } +#else + catch(Exception) { } +#endif + finally + { + try { if(pToDispose != null) pToDispose.Dispose(); } + catch(Exception) { Debug.Assert(false); } + } + + return null; + }; + +#if !ModernKeePassLib + if((f & AppRunFlags.DoEvents) != AppRunFlags.None) + { + List
lDisabledForms = new List(); + if((f & AppRunFlags.DisableForms) != AppRunFlags.None) + { + foreach(Form form in Application.OpenForms) + { + if(!form.Enabled) continue; + + lDisabledForms.Add(form); + form.Enabled = false; + } + } + + IAsyncResult ar = fnRun.BeginInvoke(null, null); + + while(!ar.AsyncWaitHandle.WaitOne(0)) + { + Application.DoEvents(); + Thread.Sleep(2); + } + + string strRet = fnRun.EndInvoke(ar); + + for(int i = lDisabledForms.Count - 1; i >= 0; --i) + lDisabledForms[i].Enabled = true; + + return strRet; + } +#endif + return fnRun(); + } + + private static void EnsureNoBom(StreamWriter sw) + { + if(sw == null) { Debug.Assert(false); return; } + if(!MonoWorkarounds.IsRequired(1219)) return; + + try + { + Encoding enc = sw.Encoding; + if(enc == null) { Debug.Assert(false); return; } + byte[] pbBom = enc.GetPreamble(); + if((pbBom == null) || (pbBom.Length == 0)) return; + + // For Mono >= 4.0 (using Microsoft's reference source) + try + { + FieldInfo fi = typeof(StreamWriter).GetField("haveWrittenPreamble", + BindingFlags.Instance | BindingFlags.NonPublic); + if(fi != null) + { + fi.SetValue(sw, true); + return; + } + } + catch(Exception) { Debug.Assert(false); } + + // For Mono < 4.0 + FieldInfo fiPD = typeof(StreamWriter).GetField("preamble_done", + BindingFlags.Instance | BindingFlags.NonPublic); + if(fiPD != null) fiPD.SetValue(sw, true); + else { Debug.Assert(false); } + } + catch(Exception) { Debug.Assert(false); } + } +#endif + + /// + /// Transform a key. + /// + /// Source and destination buffer. + /// Key to use in the transformation. + /// Number of transformation rounds. + /// Returns true, if the key was transformed successfully. + public static bool TransformKey256(byte[] pBuf256, byte[] pKey256, + ulong uRounds) + { +#if KeePassUAP || ModernKeePassLib + return false; +#else + if(!m_bAllowNative) return false; + + KeyValuePair kvp = PrepareArrays256(pBuf256, pKey256); + bool bResult = false; + + try + { + bResult = NativeMethods.TransformKey(kvp.Key, kvp.Value, uRounds); + } + catch(Exception) { bResult = false; } + + if(bResult) GetBuffers256(kvp, pBuf256, pKey256); + + FreeArrays(kvp); + return bResult; +#endif + } + + /// + /// Benchmark key transformation. + /// + /// Number of milliseconds to perform the benchmark. + /// Number of transformations done. + /// Returns true, if the benchmark was successful. + public static bool TransformKeyBenchmark256(uint uTimeMs, out ulong puRounds) + { + puRounds = 0; + +#if KeePassUAP || ModernKeePassLib + return false; +#else + if(!m_bAllowNative) return false; + + try { puRounds = NativeMethods.TransformKeyBenchmark(uTimeMs); } + catch(Exception) { return false; } + + return true; +#endif + } + + private static KeyValuePair PrepareArrays256(byte[] pBuf256, + byte[] pKey256) + { + Debug.Assert((pBuf256 != null) && (pBuf256.Length == 32)); + if(pBuf256 == null) throw new ArgumentNullException("pBuf256"); + if(pBuf256.Length != 32) throw new ArgumentException(); + + Debug.Assert((pKey256 != null) && (pKey256.Length == 32)); + if(pKey256 == null) throw new ArgumentNullException("pKey256"); + if(pKey256.Length != 32) throw new ArgumentException(); + + IntPtr hBuf = Marshal.AllocHGlobal(pBuf256.Length); + Marshal.Copy(pBuf256, 0, hBuf, pBuf256.Length); + + IntPtr hKey = Marshal.AllocHGlobal(pKey256.Length); + Marshal.Copy(pKey256, 0, hKey, pKey256.Length); + + return new KeyValuePair(hBuf, hKey); + } + + private static void GetBuffers256(KeyValuePair kvpSource, + byte[] pbDestBuf, byte[] pbDestKey) + { + if(kvpSource.Key != IntPtr.Zero) + Marshal.Copy(kvpSource.Key, pbDestBuf, 0, pbDestBuf.Length); + + if(kvpSource.Value != IntPtr.Zero) + Marshal.Copy(kvpSource.Value, pbDestKey, 0, pbDestKey.Length); + } + + private static void FreeArrays(KeyValuePair kvpPointers) + { + if(kvpPointers.Key != IntPtr.Zero) + Marshal.FreeHGlobal(kvpPointers.Key); + + if(kvpPointers.Value != IntPtr.Zero) + Marshal.FreeHGlobal(kvpPointers.Value); + } + + internal static Type GetUwpType(string strType) + { + if(string.IsNullOrEmpty(strType)) { Debug.Assert(false); return null; } + + // https://referencesource.microsoft.com/#mscorlib/system/runtime/interopservices/windowsruntime/winrtclassactivator.cs + return Type.GetType(strType + ", Windows, ContentType=WindowsRuntime", false); + } + } +} diff --git a/ModernKeePassLib/Native/NativeMethods.Unix.cs b/ModernKeePassLib/Native/NativeMethods.Unix.cs new file mode 100644 index 0000000..2b3f2de --- /dev/null +++ b/ModernKeePassLib/Native/NativeMethods.Unix.cs @@ -0,0 +1,216 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Reflection; +using System.Runtime.InteropServices; +using System.Text; + +#if !KeePassUAP +using System.Windows.Forms; +#endif + +namespace ModernKeePassLib.Native +{ + internal static partial class NativeMethods + { +#if (!KeePassLibSD && !KeePassUAP) + [StructLayout(LayoutKind.Sequential)] + private struct XClassHint + { + public IntPtr res_name; + public IntPtr res_class; + } + + [DllImport("libX11")] + private static extern int XSetClassHint(IntPtr display, IntPtr window, IntPtr class_hints); + + private static Type m_tXplatUIX11 = null; + private static Type GetXplatUIX11Type(bool bThrowOnError) + { + if(m_tXplatUIX11 == null) + { + // CheckState is in System.Windows.Forms + string strTypeCS = typeof(CheckState).AssemblyQualifiedName; + string strTypeX11 = strTypeCS.Replace("CheckState", "XplatUIX11"); + m_tXplatUIX11 = Type.GetType(strTypeX11, bThrowOnError, true); + } + + return m_tXplatUIX11; + } + + private static Type m_tHwnd = null; + private static Type GetHwndType(bool bThrowOnError) + { + if(m_tHwnd == null) + { + // CheckState is in System.Windows.Forms + string strTypeCS = typeof(CheckState).AssemblyQualifiedName; + string strTypeHwnd = strTypeCS.Replace("CheckState", "Hwnd"); + m_tHwnd = Type.GetType(strTypeHwnd, bThrowOnError, true); + } + + return m_tHwnd; + } + + internal static void SetWmClass(Form f, string strName, string strClass) + { + if(f == null) { Debug.Assert(false); return; } + + // The following crashes under Mac OS X (SIGSEGV in native code, + // not just an exception), thus skip it when we're on Mac OS X; + // https://sourceforge.net/projects/keepass/forums/forum/329221/topic/5860588 + if(NativeLib.GetPlatformID() == PlatformID.MacOSX) return; + + try + { + Type tXplatUIX11 = GetXplatUIX11Type(true); + FieldInfo fiDisplayHandle = tXplatUIX11.GetField("DisplayHandle", + BindingFlags.NonPublic | BindingFlags.Static); + IntPtr hDisplay = (IntPtr)fiDisplayHandle.GetValue(null); + + Type tHwnd = GetHwndType(true); + MethodInfo miObjectFromHandle = tHwnd.GetMethod("ObjectFromHandle", + BindingFlags.Public | BindingFlags.Static); + object oHwnd = miObjectFromHandle.Invoke(null, new object[] { f.Handle }); + + FieldInfo fiWholeWindow = tHwnd.GetField("whole_window", + BindingFlags.NonPublic | BindingFlags.Instance); + IntPtr hWindow = (IntPtr)fiWholeWindow.GetValue(oHwnd); + + XClassHint xch = new XClassHint(); + xch.res_name = Marshal.StringToCoTaskMemAnsi(strName ?? string.Empty); + xch.res_class = Marshal.StringToCoTaskMemAnsi(strClass ?? string.Empty); + IntPtr pXch = Marshal.AllocCoTaskMem(Marshal.SizeOf(xch)); + Marshal.StructureToPtr(xch, pXch, false); + + XSetClassHint(hDisplay, hWindow, pXch); + + Marshal.FreeCoTaskMem(pXch); + Marshal.FreeCoTaskMem(xch.res_name); + Marshal.FreeCoTaskMem(xch.res_class); + } + catch(Exception) { Debug.Assert(false); } + } +#endif + + // ============================================================= + // LibGCrypt 1.8.1 + + private const string LibGCrypt = "libgcrypt.so.20"; + + internal const int GCRY_CIPHER_AES256 = 9; + internal const int GCRY_CIPHER_MODE_ECB = 1; + + [DllImport(LibGCrypt)] + internal static extern IntPtr gcry_check_version(IntPtr lpReqVersion); + + [DllImport(LibGCrypt)] + internal static extern uint gcry_cipher_open(ref IntPtr ph, int nAlgo, + int nMode, uint uFlags); + + [DllImport(LibGCrypt)] + internal static extern void gcry_cipher_close(IntPtr h); + + [DllImport(LibGCrypt)] + internal static extern uint gcry_cipher_setkey(IntPtr h, IntPtr pbKey, + IntPtr cbKey); // cbKey is size_t + + [DllImport(LibGCrypt)] + internal static extern uint gcry_cipher_encrypt(IntPtr h, IntPtr pbOut, + IntPtr cbOut, IntPtr pbIn, IntPtr cbIn); // cb* are size_t + + /* internal static IntPtr Utf8ZFromString(string str) + { + byte[] pb = StrUtil.Utf8.GetBytes(str ?? string.Empty); + + IntPtr p = Marshal.AllocCoTaskMem(pb.Length + 1); + if(p != IntPtr.Zero) + { + Marshal.Copy(pb, 0, p, pb.Length); + Marshal.WriteByte(p, pb.Length, 0); + } + else { Debug.Assert(false); } + + return p; + } + + internal static string Utf8ZToString(IntPtr p) + { + if(p == IntPtr.Zero) { Debug.Assert(false); return null; } + + List l = new List(); + for(int i = 0; i < int.MaxValue; ++i) + { + byte bt = Marshal.ReadByte(p, i); + if(bt == 0) break; + + l.Add(bt); + } + + return StrUtil.Utf8.GetString(l.ToArray()); + } + + internal static void Utf8ZFree(IntPtr p) + { + if(p != IntPtr.Zero) Marshal.FreeCoTaskMem(p); + } */ + + /* // ============================================================= + // LibGLib 2 + + private const string LibGLib = "libglib-2.0.so.0"; + + internal const int G_FALSE = 0; + + // https://developer.gnome.org/glib/stable/glib-Memory-Allocation.html + [DllImport(LibGLib)] + internal static extern void g_free(IntPtr pMem); // pMem may be null + + // ============================================================= + // LibGTK 3 (3.22.11 / 3.22.24) + + private const string LibGtk = "libgtk-3.so.0"; + + internal static readonly IntPtr GDK_SELECTION_PRIMARY = new IntPtr(1); + internal static readonly IntPtr GDK_SELECTION_CLIPBOARD = new IntPtr(69); + + [DllImport(LibGtk)] + internal static extern int gtk_init_check(IntPtr pArgc, IntPtr pArgv); + + [DllImport(LibGtk)] + // The returned handle is owned by GTK and must not be freed + internal static extern IntPtr gtk_clipboard_get(IntPtr pSelection); + + [DllImport(LibGtk)] + internal static extern void gtk_clipboard_clear(IntPtr hClipboard); + + [DllImport(LibGtk)] + internal static extern IntPtr gtk_clipboard_wait_for_text(IntPtr hClipboard); + + [DllImport(LibGtk)] + internal static extern void gtk_clipboard_set_text(IntPtr hClipboard, + IntPtr lpText, int cbLen); + + [DllImport(LibGtk)] + internal static extern void gtk_clipboard_store(IntPtr hClipboard); */ + } +} diff --git a/ModernKeePassLib/Native/NativeMethods.cs b/ModernKeePassLib/Native/NativeMethods.cs new file mode 100644 index 0000000..6880a7a --- /dev/null +++ b/ModernKeePassLib/Native/NativeMethods.cs @@ -0,0 +1,260 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Native +{ + internal static partial class NativeMethods + { + internal const int MAX_PATH = 260; + + internal const long INVALID_HANDLE_VALUE = -1; + + internal const uint MOVEFILE_REPLACE_EXISTING = 0x00000001; + internal const uint MOVEFILE_COPY_ALLOWED = 0x00000002; + + internal const uint FILE_SUPPORTS_TRANSACTIONS = 0x00200000; + internal const int MAX_TRANSACTION_DESCRIPTION_LENGTH = 64; + + // internal const uint TF_SFT_SHOWNORMAL = 0x00000001; + // internal const uint TF_SFT_HIDDEN = 0x00000008; + + /* [DllImport("KeePassNtv32.dll", EntryPoint = "TransformKey")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool TransformKey32(IntPtr pBuf256, + IntPtr pKey256, UInt64 uRounds); + + [DllImport("KeePassNtv64.dll", EntryPoint = "TransformKey")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool TransformKey64(IntPtr pBuf256, + IntPtr pKey256, UInt64 uRounds); + + internal static bool TransformKey(IntPtr pBuf256, IntPtr pKey256, + UInt64 uRounds) + { + if(IntPtr.Size == 4) + return TransformKey32(pBuf256, pKey256, uRounds); + return TransformKey64(pBuf256, pKey256, uRounds); + } + + [DllImport("KeePassNtv32.dll", EntryPoint = "TransformKeyTimed")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool TransformKeyTimed32(IntPtr pBuf256, + IntPtr pKey256, ref UInt64 puRounds, UInt32 uSeconds); + + [DllImport("KeePassNtv64.dll", EntryPoint = "TransformKeyTimed")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool TransformKeyTimed64(IntPtr pBuf256, + IntPtr pKey256, ref UInt64 puRounds, UInt32 uSeconds); + + internal static bool TransformKeyTimed(IntPtr pBuf256, IntPtr pKey256, + ref UInt64 puRounds, UInt32 uSeconds) + { + if(IntPtr.Size == 4) + return TransformKeyTimed32(pBuf256, pKey256, ref puRounds, uSeconds); + return TransformKeyTimed64(pBuf256, pKey256, ref puRounds, uSeconds); + } */ + +#if !KeePassUAP + [DllImport("KeePassLibC32.dll", EntryPoint = "TransformKey256")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool TransformKey32(IntPtr pBuf256, + IntPtr pKey256, UInt64 uRounds); + + [DllImport("KeePassLibC64.dll", EntryPoint = "TransformKey256")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool TransformKey64(IntPtr pBuf256, + IntPtr pKey256, UInt64 uRounds); + + internal static bool TransformKey(IntPtr pBuf256, IntPtr pKey256, + UInt64 uRounds) + { + if(IntPtr.Size == 4) + return TransformKey32(pBuf256, pKey256, uRounds); + return TransformKey64(pBuf256, pKey256, uRounds); + } + + [DllImport("KeePassLibC32.dll", EntryPoint = "TransformKeyBenchmark256")] + private static extern UInt64 TransformKeyBenchmark32(UInt32 uTimeMs); + + [DllImport("KeePassLibC64.dll", EntryPoint = "TransformKeyBenchmark256")] + private static extern UInt64 TransformKeyBenchmark64(UInt32 uTimeMs); + + internal static UInt64 TransformKeyBenchmark(UInt32 uTimeMs) + { + if(IntPtr.Size == 4) + return TransformKeyBenchmark32(uTimeMs); + return TransformKeyBenchmark64(uTimeMs); + } +#endif + + /* [DllImport("KeePassLibC32.dll", EntryPoint = "TF_ShowLangBar")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool TF_ShowLangBar32(UInt32 dwFlags); + + [DllImport("KeePassLibC64.dll", EntryPoint = "TF_ShowLangBar")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool TF_ShowLangBar64(UInt32 dwFlags); + + internal static bool TfShowLangBar(uint dwFlags) + { + if(IntPtr.Size == 4) return TF_ShowLangBar32(dwFlags); + return TF_ShowLangBar64(dwFlags); + } */ + + [DllImport("KeePassLibC32.dll", EntryPoint = "ProtectProcessWithDacl")] + private static extern void ProtectProcessWithDacl32(); + + [DllImport("KeePassLibC64.dll", EntryPoint = "ProtectProcessWithDacl")] + private static extern void ProtectProcessWithDacl64(); + + internal static void ProtectProcessWithDacl() + { + try + { + if(NativeLib.IsUnix()) return; + + if(IntPtr.Size == 4) ProtectProcessWithDacl32(); + else ProtectProcessWithDacl64(); + } + catch(Exception) { Debug.Assert(false); } + } + + [DllImport("Kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CloseHandle(IntPtr hObject); + + [DllImport("Kernel32.dll", CharSet = CharSet.Auto, ExactSpelling = false, + SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool GetVolumeInformation(string lpRootPathName, + StringBuilder lpVolumeNameBuffer, UInt32 nVolumeNameSize, + ref UInt32 lpVolumeSerialNumber, ref UInt32 lpMaximumComponentLength, + ref UInt32 lpFileSystemFlags, StringBuilder lpFileSystemNameBuffer, + UInt32 nFileSystemNameSize); + + [DllImport("Kernel32.dll", CharSet = CharSet.Auto, ExactSpelling = false, + SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool MoveFileEx(string lpExistingFileName, + string lpNewFileName, UInt32 dwFlags); + + [DllImport("KtmW32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, + SetLastError = true)] + internal static extern IntPtr CreateTransaction(IntPtr lpTransactionAttributes, + IntPtr lpUOW, UInt32 dwCreateOptions, UInt32 dwIsolationLevel, + UInt32 dwIsolationFlags, UInt32 dwTimeout, string lpDescription); + + [DllImport("KtmW32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CommitTransaction(IntPtr hTransaction); + + [DllImport("Kernel32.dll", CharSet = CharSet.Auto, ExactSpelling = false, + SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool MoveFileTransacted(string lpExistingFileName, + string lpNewFileName, IntPtr lpProgressRoutine, IntPtr lpData, + UInt32 dwFlags, IntPtr hTransaction); + +#if (!KeePassLibSD && !KeePassUAP) + [DllImport("ShlWApi.dll", CharSet = CharSet.Auto)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool PathRelativePathTo([Out] StringBuilder pszPath, + [In] string pszFrom, uint dwAttrFrom, [In] string pszTo, uint dwAttrTo); + + [DllImport("ShlWApi.dll", CharSet = CharSet.Unicode, ExactSpelling = true)] + private static extern int StrCmpLogicalW(string x, string y); + + private static bool? m_obSupportsLogicalCmp = null; + + private static void TestNaturalComparisonsSupport() + { + try + { + StrCmpLogicalW("0", "0"); // Throws exception if unsupported + m_obSupportsLogicalCmp = true; + } + catch(Exception) { m_obSupportsLogicalCmp = false; } + } +#endif + + internal static bool SupportsStrCmpNaturally + { + get + { +#if (!KeePassLibSD && !KeePassUAP) + if(!m_obSupportsLogicalCmp.HasValue) + TestNaturalComparisonsSupport(); + + return m_obSupportsLogicalCmp.Value; +#else + return false; +#endif + } + } + + internal static int StrCmpNaturally(string x, string y) + { +#if (!KeePassLibSD && !KeePassUAP) + if(!NativeMethods.SupportsStrCmpNaturally) + { + Debug.Assert(false); + return string.Compare(x, y, true); + } + + return StrCmpLogicalW(x, y); +#else + Debug.Assert(false); + return string.Compare(x, y, true); +#endif + } + + internal static string GetUserRuntimeDir() + { +#if KeePassLibSD + return Path.GetTempPath(); +#else +#if KeePassUAP + string strRtDir = EnvironmentExt.AppDataLocalFolderPath; +#else + string strRtDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR"); + if(string.IsNullOrEmpty(strRtDir)) + strRtDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + if(string.IsNullOrEmpty(strRtDir)) + { + Debug.Assert(false); + return Path.GetTempPath(); // Not UrlUtil (otherwise cyclic) + } +#endif + + strRtDir = UrlUtil.EnsureTerminatingSeparator(strRtDir, false); + strRtDir += PwDefs.ShortProductName; + + return strRtDir; +#endif + } + } +} diff --git a/ModernKeePassLib/PwCustomIcon.cs b/ModernKeePassLib/PwCustomIcon.cs new file mode 100644 index 0000000..00343df --- /dev/null +++ b/ModernKeePassLib/PwCustomIcon.cs @@ -0,0 +1,127 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; +#if !ModernKeePassLib +using System.Drawing; +#else +using Windows.UI.Xaml.Controls; +#endif + +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib +{ + /// + /// Custom icon. PwCustomIcon objects are immutable. + /// + public sealed class PwCustomIcon + { + private readonly PwUuid m_pwUuid; + private readonly byte[] m_pbImageDataPng; + + private readonly Image m_imgOrg; + private Dictionary m_dImageCache = new Dictionary(); + + // Recommended maximum sizes, not obligatory + internal const int MaxWidth = 128; + internal const int MaxHeight = 128; + + public PwUuid Uuid + { + get { return m_pwUuid; } + } + + public byte[] ImageDataPng + { + get { return m_pbImageDataPng; } + } + + [Obsolete("Use GetImage instead.")] + public Image Image + { +#if (!KeePassLibSD && !KeePassUAP) + get { return GetImage(16, 16); } // Backward compatibility +#else + get { return GetImage(); } // Backward compatibility +#endif + } + + public PwCustomIcon(PwUuid pwUuid, byte[] pbImageDataPng) + { + Debug.Assert(pwUuid != null); + if(pwUuid == null) throw new ArgumentNullException("pwUuid"); + Debug.Assert(!pwUuid.Equals(PwUuid.Zero)); + if(pwUuid.Equals(PwUuid.Zero)) throw new ArgumentException("pwUuid == 0."); + Debug.Assert(pbImageDataPng != null); + if(pbImageDataPng == null) throw new ArgumentNullException("pbImageDataPng"); + + m_pwUuid = pwUuid; + m_pbImageDataPng = pbImageDataPng; + + // MemoryStream ms = new MemoryStream(m_pbImageDataPng, false); + // m_imgOrg = Image.FromStream(ms); + // ms.Close(); + try { m_imgOrg = GfxUtil.LoadImage(m_pbImageDataPng); } + catch(Exception) { Debug.Assert(false); m_imgOrg = null; } + + if(m_imgOrg != null) + m_dImageCache[GetID((int)m_imgOrg.Width, (int)m_imgOrg.Height)] = + m_imgOrg; + } + + private static long GetID(int w, int h) + { + return (((long)w << 32) ^ (long)h); + } + + /// + /// Get the icon as an Image (original size). + /// + public Image GetImage() + { + return m_imgOrg; + } + +#if (!KeePassLibSD && !KeePassUAP) + /// + /// Get the icon as an Image (with the specified size). + /// + /// Width of the returned image. + /// Height of the returned image. + public Image GetImage(int w, int h) + { + if(w < 0) { Debug.Assert(false); return m_imgOrg; } + if(h < 0) { Debug.Assert(false); return m_imgOrg; } + if(m_imgOrg == null) return null; + + long lID = GetID(w, h); + + Image img; + if(m_dImageCache.TryGetValue(lID, out img)) return img; + + img = GfxUtil.ScaleImage(m_imgOrg, w, h, ScaleTransformFlags.UIIcon); + m_dImageCache[lID] = img; + return img; + } +#endif + } +} diff --git a/ModernKeePassLib/PwDatabase.cs b/ModernKeePassLib/PwDatabase.cs new file mode 100644 index 0000000..4e63824 --- /dev/null +++ b/ModernKeePassLib/PwDatabase.cs @@ -0,0 +1,2077 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Drawing; + +#if ModernKeePassLib +using Windows.UI.Xaml.Controls; +#endif + +using ModernKeePassLib.Collections; +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Cryptography.Cipher; +using ModernKeePassLib.Cryptography.KeyDerivation; +using ModernKeePassLib.Delegates; +using ModernKeePassLib.Interfaces; +using ModernKeePassLib.Keys; +using ModernKeePassLib.Resources; +using ModernKeePassLib.Security; +using ModernKeePassLib.Serialization; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib +{ + /// + /// The core password manager class. It contains a number of groups, which + /// contain the actual entries. + /// + public sealed class PwDatabase + { + internal const int DefaultHistoryMaxItems = 10; // -1 = unlimited + internal const long DefaultHistoryMaxSize = 6 * 1024 * 1024; // -1 = unlimited + + private static bool m_bPrimaryCreated = false; + + // Initializations: see Clear() + private PwGroup m_pgRootGroup = null; + private PwObjectList m_vDeletedObjects = new PwObjectList(); + + private PwUuid m_uuidDataCipher = StandardAesEngine.AesUuid; + private PwCompressionAlgorithm m_caCompression = PwCompressionAlgorithm.GZip; + // private ulong m_uKeyEncryptionRounds = PwDefs.DefaultKeyEncryptionRounds; + private KdfParameters m_kdfParams = KdfPool.GetDefaultParameters(); + + private CompositeKey m_pwUserKey = null; + private MemoryProtectionConfig m_memProtConfig = new MemoryProtectionConfig(); + + private List m_vCustomIcons = new List(); + private bool m_bUINeedsIconUpdate = true; + + private DateTime m_dtSettingsChanged = PwDefs.DtDefaultNow; + private string m_strName = string.Empty; + private DateTime m_dtNameChanged = PwDefs.DtDefaultNow; + private string m_strDesc = string.Empty; + private DateTime m_dtDescChanged = PwDefs.DtDefaultNow; + private string m_strDefaultUserName = string.Empty; + private DateTime m_dtDefaultUserChanged = PwDefs.DtDefaultNow; + private uint m_uMntncHistoryDays = 365; + private Color m_clr = Color.Empty; + + private DateTime m_dtKeyLastChanged = PwDefs.DtDefaultNow; + private long m_lKeyChangeRecDays = -1; + private long m_lKeyChangeForceDays = -1; + private bool m_bKeyChangeForceOnce = false; + + private IOConnectionInfo m_ioSource = new IOConnectionInfo(); + private bool m_bDatabaseOpened = false; + private bool m_bModified = false; + + private PwUuid m_pwLastSelectedGroup = PwUuid.Zero; + private PwUuid m_pwLastTopVisibleGroup = PwUuid.Zero; + + private bool m_bUseRecycleBin = true; + private PwUuid m_pwRecycleBin = PwUuid.Zero; + private DateTime m_dtRecycleBinChanged = PwDefs.DtDefaultNow; + private PwUuid m_pwEntryTemplatesGroup = PwUuid.Zero; + private DateTime m_dtEntryTemplatesChanged = PwDefs.DtDefaultNow; + + private int m_nHistoryMaxItems = DefaultHistoryMaxItems; + private long m_lHistoryMaxSize = DefaultHistoryMaxSize; // In bytes + + private StringDictionaryEx m_dCustomData = new StringDictionaryEx(); + private VariantDictionary m_dPublicCustomData = new VariantDictionary(); + + private byte[] m_pbHashOfFileOnDisk = null; + private byte[] m_pbHashOfLastIO = null; + + private bool m_bUseFileTransactions = false; + private bool m_bUseFileLocks = false; + + private IStatusLogger m_slStatus = null; + + private static string m_strLocalizedAppName = string.Empty; + + // private const string StrBackupExtension = ".bak"; + + /// + /// Get the root group that contains all groups and entries stored in the + /// database. + /// + /// Root group. The return value is null, if no database + /// has been opened. + public PwGroup RootGroup + { + get { return m_pgRootGroup; } + set + { + Debug.Assert(value != null); + if(value == null) throw new ArgumentNullException("value"); + + m_pgRootGroup = value; + } + } + + /// + /// IOConnection of the currently opened database file. + /// Is never null. + /// + public IOConnectionInfo IOConnectionInfo + { + get { return m_ioSource; } + } + + /// + /// If this is true, a database is currently open. + /// + public bool IsOpen + { + get { return m_bDatabaseOpened; } + } + + /// + /// Modification flag. If true, the class has been modified and the + /// user interface should prompt the user to save the changes before + /// closing the database for example. + /// + public bool Modified + { + get { return m_bModified; } + set { m_bModified = value; } + } + + /// + /// The user key used for database encryption. This key must be created + /// and set before using any of the database load/save functions. + /// + public CompositeKey MasterKey + { + get { return m_pwUserKey; } + set + { + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + + m_pwUserKey = value; + } + } + + public DateTime SettingsChanged + { + get { return m_dtSettingsChanged; } + set { m_dtSettingsChanged = value; } + } + + /// + /// Name of the database. + /// + public string Name + { + get { return m_strName; } + set + { + Debug.Assert(value != null); + if(value != null) m_strName = value; + } + } + + public DateTime NameChanged + { + get { return m_dtNameChanged; } + set { m_dtNameChanged = value; } + } + + /// + /// Database description. + /// + public string Description + { + get { return m_strDesc; } + set + { + Debug.Assert(value != null); + if(value != null) m_strDesc = value; + } + } + + public DateTime DescriptionChanged + { + get { return m_dtDescChanged; } + set { m_dtDescChanged = value; } + } + + /// + /// Default user name used for new entries. + /// + public string DefaultUserName + { + get { return m_strDefaultUserName; } + set + { + Debug.Assert(value != null); + if(value != null) m_strDefaultUserName = value; + } + } + + public DateTime DefaultUserNameChanged + { + get { return m_dtDefaultUserChanged; } + set { m_dtDefaultUserChanged = value; } + } + + /// + /// Number of days until history entries are being deleted + /// in a database maintenance operation. + /// + public uint MaintenanceHistoryDays + { + get { return m_uMntncHistoryDays; } + set { m_uMntncHistoryDays = value; } + } + + public Color Color + { + get { return m_clr; } + set { m_clr = value; } + } + + public DateTime MasterKeyChanged + { + get { return m_dtKeyLastChanged; } + set { m_dtKeyLastChanged = value; } + } + + public long MasterKeyChangeRec + { + get { return m_lKeyChangeRecDays; } + set { m_lKeyChangeRecDays = value; } + } + + public long MasterKeyChangeForce + { + get { return m_lKeyChangeForceDays; } + set { m_lKeyChangeForceDays = value; } + } + + public bool MasterKeyChangeForceOnce + { + get { return m_bKeyChangeForceOnce; } + set { m_bKeyChangeForceOnce = value; } + } + + /// + /// The encryption algorithm used to encrypt the data part of the database. + /// + public PwUuid DataCipherUuid + { + get { return m_uuidDataCipher; } + set + { + Debug.Assert(value != null); + if(value != null) m_uuidDataCipher = value; + } + } + + /// + /// Compression algorithm used to encrypt the data part of the database. + /// + public PwCompressionAlgorithm Compression + { + get { return m_caCompression; } + set { m_caCompression = value; } + } + + // /// + // /// Number of key transformation rounds (KDF parameter). + // /// + // public ulong KeyEncryptionRounds + // { + // get { return m_uKeyEncryptionRounds; } + // set { m_uKeyEncryptionRounds = value; } + // } + + public KdfParameters KdfParameters + { + get { return m_kdfParams; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_kdfParams = value; + } + } + + /// + /// Memory protection configuration (for default fields). + /// + public MemoryProtectionConfig MemoryProtection + { + get { return m_memProtConfig; } + set + { + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + + m_memProtConfig = value; + } + } + + /// + /// Get a list of all deleted objects. + /// + public PwObjectList DeletedObjects + { + get { return m_vDeletedObjects; } + } + + /// + /// Get all custom icons stored in this database. + /// + public List CustomIcons + { + get { return m_vCustomIcons; } + } + + /// + /// This is a dirty-flag for the UI. It is used to indicate when an + /// icon list update is required. + /// + public bool UINeedsIconUpdate + { + get { return m_bUINeedsIconUpdate; } + set { m_bUINeedsIconUpdate = value; } + } + + public PwUuid LastSelectedGroup + { + get { return m_pwLastSelectedGroup; } + set + { + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + m_pwLastSelectedGroup = value; + } + } + + public PwUuid LastTopVisibleGroup + { + get { return m_pwLastTopVisibleGroup; } + set + { + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + m_pwLastTopVisibleGroup = value; + } + } + + public bool RecycleBinEnabled + { + get { return m_bUseRecycleBin; } + set { m_bUseRecycleBin = value; } + } + + public PwUuid RecycleBinUuid + { + get { return m_pwRecycleBin; } + set + { + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + m_pwRecycleBin = value; + } + } + + public DateTime RecycleBinChanged + { + get { return m_dtRecycleBinChanged; } + set { m_dtRecycleBinChanged = value; } + } + + /// + /// UUID of the group containing template entries. May be + /// PwUuid.Zero, if no entry templates group has been specified. + /// + public PwUuid EntryTemplatesGroup + { + get { return m_pwEntryTemplatesGroup; } + set + { + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + m_pwEntryTemplatesGroup = value; + } + } + + public DateTime EntryTemplatesGroupChanged + { + get { return m_dtEntryTemplatesChanged; } + set { m_dtEntryTemplatesChanged = value; } + } + + public int HistoryMaxItems + { + get { return m_nHistoryMaxItems; } + set { m_nHistoryMaxItems = value; } + } + + public long HistoryMaxSize + { + get { return m_lHistoryMaxSize; } + set { m_lHistoryMaxSize = value; } + } + + /// + /// Custom data container that can be used by plugins to store + /// own data in KeePass databases. + /// The data is stored in the encrypted part of encrypted + /// database files. + /// Use unique names for your items, e.g. "PluginName_ItemName". + /// + public StringDictionaryEx CustomData + { + get { return m_dCustomData; } + internal set + { + if(value == null) { Debug.Assert(false); throw new ArgumentNullException("value"); } + m_dCustomData = value; + } + } + + /// + /// Custom data container that can be used by plugins to store + /// own data in KeePass databases. + /// The data is stored in the *unencrypted* part of database files, + /// and it is not supported by all file formats (e.g. supported by KDBX, + /// unsupported by XML). + /// It is highly recommended to use CustomData instead, + /// if possible. + /// Use unique names for your items, e.g. "PluginName_ItemName". + /// + public VariantDictionary PublicCustomData + { + get { return m_dPublicCustomData; } + internal set + { + if(value == null) { Debug.Assert(false); throw new ArgumentNullException("value"); } + m_dPublicCustomData = value; + } + } + + /// + /// Hash value of the primary file on disk (last read or last write). + /// A call to SaveAs without making the saved file primary will + /// not change this hash. May be null. + /// + public byte[] HashOfFileOnDisk + { + get { return m_pbHashOfFileOnDisk; } + } + + public byte[] HashOfLastIO + { + get { return m_pbHashOfLastIO; } + } + + public bool UseFileTransactions + { + get { return m_bUseFileTransactions; } + set { m_bUseFileTransactions = value; } + } + + public bool UseFileLocks + { + get { return m_bUseFileLocks; } + set { m_bUseFileLocks = value; } + } + + private string m_strDetachBins = null; + /// + /// Detach binaries when opening a file. If this isn't null, + /// all binaries are saved to the specified path and are removed + /// from the database. + /// + public string DetachBinaries + { + get { return m_strDetachBins; } + set { m_strDetachBins = value; } + } + + /// + /// Localized application name. + /// + public static string LocalizedAppName + { + get { return m_strLocalizedAppName; } + set { Debug.Assert(value != null); m_strLocalizedAppName = value; } + } + + /// + /// Constructs an empty password manager object. + /// + public PwDatabase() + { + if(m_bPrimaryCreated == false) m_bPrimaryCreated = true; + + Clear(); + } + + private void Clear() + { + m_pgRootGroup = null; + m_vDeletedObjects = new PwObjectList(); + + m_uuidDataCipher = StandardAesEngine.AesUuid; + m_caCompression = PwCompressionAlgorithm.GZip; + // m_uKeyEncryptionRounds = PwDefs.DefaultKeyEncryptionRounds; + m_kdfParams = KdfPool.GetDefaultParameters(); + + m_pwUserKey = null; + m_memProtConfig = new MemoryProtectionConfig(); + + m_vCustomIcons = new List(); + m_bUINeedsIconUpdate = true; + + DateTime dtNow = DateTime.UtcNow; + + m_dtSettingsChanged = dtNow; + m_strName = string.Empty; + m_dtNameChanged = dtNow; + m_strDesc = string.Empty; + m_dtDescChanged = dtNow; + m_strDefaultUserName = string.Empty; + m_dtDefaultUserChanged = dtNow; + m_uMntncHistoryDays = 365; + m_clr = Color.Empty; + + m_dtKeyLastChanged = dtNow; + m_lKeyChangeRecDays = -1; + m_lKeyChangeForceDays = -1; + m_bKeyChangeForceOnce = false; + + m_ioSource = new IOConnectionInfo(); + m_bDatabaseOpened = false; + m_bModified = false; + + m_pwLastSelectedGroup = PwUuid.Zero; + m_pwLastTopVisibleGroup = PwUuid.Zero; + + m_bUseRecycleBin = true; + m_pwRecycleBin = PwUuid.Zero; + m_dtRecycleBinChanged = dtNow; + m_pwEntryTemplatesGroup = PwUuid.Zero; + m_dtEntryTemplatesChanged = dtNow; + + m_nHistoryMaxItems = DefaultHistoryMaxItems; + m_lHistoryMaxSize = DefaultHistoryMaxSize; + + m_dCustomData = new StringDictionaryEx(); + m_dPublicCustomData = new VariantDictionary(); + + m_pbHashOfFileOnDisk = null; + m_pbHashOfLastIO = null; + + m_bUseFileTransactions = false; + m_bUseFileLocks = false; + } + + /// + /// Initialize the class for managing a new database. Previously loaded + /// data is deleted. + /// + /// IO connection of the new database. + /// Key to open the database. + public void New(IOConnectionInfo ioConnection, CompositeKey pwKey) + { + Debug.Assert(ioConnection != null); + if(ioConnection == null) throw new ArgumentNullException("ioConnection"); + Debug.Assert(pwKey != null); + if(pwKey == null) throw new ArgumentNullException("pwKey"); + + Close(); + + m_ioSource = ioConnection; + m_pwUserKey = pwKey; + + m_bDatabaseOpened = true; + m_bModified = true; + + m_pgRootGroup = new PwGroup(true, true, UrlUtil.StripExtension( + UrlUtil.GetFileName(ioConnection.Path)), PwIcon.FolderOpen); + m_pgRootGroup.IsExpanded = true; + } + + /// + /// Open a database. The URL may point to any supported data source. + /// + /// IO connection to load the database from. + /// Key used to open the specified database. + /// Logger, which gets all status messages. + public void Open(IOConnectionInfo ioSource, CompositeKey pwKey, + IStatusLogger slLogger) + { + Debug.Assert(ioSource != null); + if(ioSource == null) throw new ArgumentNullException("ioSource"); + Debug.Assert(pwKey != null); + if(pwKey == null) throw new ArgumentNullException("pwKey"); + + Close(); + + try + { + m_pgRootGroup = new PwGroup(true, true, UrlUtil.StripExtension( + UrlUtil.GetFileName(ioSource.Path)), PwIcon.FolderOpen); + m_pgRootGroup.IsExpanded = true; + + m_pwUserKey = pwKey; + m_bModified = false; + + KdbxFile kdbx = new KdbxFile(this); + kdbx.DetachBinaries = m_strDetachBins; + + using(Stream s = IOConnection.OpenRead(ioSource)) + { + kdbx.Load(s, KdbxFormat.Default, slLogger); + } + + m_pbHashOfLastIO = kdbx.HashOfFileOnDisk; + m_pbHashOfFileOnDisk = kdbx.HashOfFileOnDisk; + Debug.Assert(m_pbHashOfFileOnDisk != null); + + m_bDatabaseOpened = true; + m_ioSource = ioSource; + } + catch(Exception) + { + Clear(); + throw; + } + } + + /// + /// Save the currently opened database. The file is written to the location + /// it has been opened from. + /// + /// Logger that recieves status information. + public void Save(IStatusLogger slLogger) + { + Debug.Assert(!HasDuplicateUuids()); + + FileLock fl = null; + if(m_bUseFileLocks) fl = new FileLock(m_ioSource); + try + { + KdbxFile kdbx = new KdbxFile(this); + + using(FileTransactionEx ft = new FileTransactionEx(m_ioSource, + m_bUseFileTransactions)) + { + using(Stream s = ft.OpenWrite()) + { + kdbx.Save(s, null, KdbxFormat.Default, slLogger); + } + + ft.CommitWrite(); + } + + m_pbHashOfLastIO = kdbx.HashOfFileOnDisk; + m_pbHashOfFileOnDisk = kdbx.HashOfFileOnDisk; + Debug.Assert(m_pbHashOfFileOnDisk != null); + } + finally { if(fl != null) fl.Dispose(); } + + m_bModified = false; + } + + /// + /// Save the currently opened database to a different location. If + /// is true, the specified + /// location is made the default location for future saves + /// using SaveDatabase. + /// + /// New location to serialize the database to. + /// If true, the new location is made the + /// standard location for the database. If false, a copy of the currently + /// opened database is saved to the specified location, but it isn't + /// made the default location (i.e. no lock files will be moved for + /// example). + /// Logger that recieves status information. + public void SaveAs(IOConnectionInfo ioConnection, bool bIsPrimaryNow, + IStatusLogger slLogger) + { + Debug.Assert(ioConnection != null); + if(ioConnection == null) throw new ArgumentNullException("ioConnection"); + + IOConnectionInfo ioCurrent = m_ioSource; // Remember current + m_ioSource = ioConnection; + + byte[] pbHashCopy = m_pbHashOfFileOnDisk; + + try { this.Save(slLogger); } + catch(Exception) + { + m_ioSource = ioCurrent; // Restore + m_pbHashOfFileOnDisk = pbHashCopy; + + m_pbHashOfLastIO = null; + throw; + } + + if(!bIsPrimaryNow) + { + m_ioSource = ioCurrent; // Restore + m_pbHashOfFileOnDisk = pbHashCopy; + } + } + + /// + /// Closes the currently opened database. No confirmation message is shown + /// before closing. Unsaved changes will be lost. + /// + public void Close() + { + Clear(); + } + + public void MergeIn(PwDatabase pdSource, PwMergeMethod mm) + { + MergeIn(pdSource, mm, null); + } + + public void MergeIn(PwDatabase pdSource, PwMergeMethod mm, + IStatusLogger slStatus) + { + if(pdSource == null) throw new ArgumentNullException("pdSource"); + + if(mm == PwMergeMethod.CreateNewUuids) + { + pdSource.RootGroup.Uuid = new PwUuid(true); + pdSource.RootGroup.CreateNewItemUuids(true, true, true); + } + + // PwGroup pgOrgStructure = m_pgRootGroup.CloneStructure(); + // PwGroup pgSrcStructure = pdSource.RootGroup.CloneStructure(); + // Later in case 'if(mm == PwMergeMethod.Synchronize)': + // PwObjectPoolEx ppOrg = PwObjectPoolEx.FromGroup(pgOrgStructure); + // PwObjectPoolEx ppSrc = PwObjectPoolEx.FromGroup(pgSrcStructure); + + PwObjectPoolEx ppOrg = PwObjectPoolEx.FromGroup(m_pgRootGroup); + PwObjectPoolEx ppSrc = PwObjectPoolEx.FromGroup(pdSource.RootGroup); + + GroupHandler ghSrc = delegate(PwGroup pg) + { + // if(pg == pdSource.m_pgRootGroup) return true; + + // Do not use ppOrg for finding the group, because new groups + // might have been added (which are not in the pool, and the + // pool should not be modified) + PwGroup pgLocal = m_pgRootGroup.FindGroup(pg.Uuid, true); + + if(pgLocal == null) + { + PwGroup pgSourceParent = pg.ParentGroup; + PwGroup pgLocalContainer; + if(pgSourceParent == null) + { + // pg is the root group of pdSource, and no corresponding + // local group was found; create the group within the + // local root group + Debug.Assert(pg == pdSource.m_pgRootGroup); + pgLocalContainer = m_pgRootGroup; + } + else if(pgSourceParent == pdSource.m_pgRootGroup) + pgLocalContainer = m_pgRootGroup; + else + pgLocalContainer = m_pgRootGroup.FindGroup(pgSourceParent.Uuid, true); + Debug.Assert(pgLocalContainer != null); + if(pgLocalContainer == null) pgLocalContainer = m_pgRootGroup; + + PwGroup pgNew = new PwGroup(false, false); + pgNew.Uuid = pg.Uuid; + pgNew.AssignProperties(pg, false, true); + + // pgLocalContainer.AddGroup(pgNew, true); + InsertObjectAtBestPos(pgLocalContainer.Groups, pgNew, ppSrc); + pgNew.ParentGroup = pgLocalContainer; + } + else // pgLocal != null + { + Debug.Assert(mm != PwMergeMethod.CreateNewUuids); + + if(mm == PwMergeMethod.OverwriteExisting) + pgLocal.AssignProperties(pg, false, false); + else if((mm == PwMergeMethod.OverwriteIfNewer) || + (mm == PwMergeMethod.Synchronize)) + { + pgLocal.AssignProperties(pg, true, false); + } + // else if(mm == PwMergeMethod.KeepExisting) ... + } + + return ((slStatus != null) ? slStatus.ContinueWork() : true); + }; + + EntryHandler ehSrc = delegate(PwEntry pe) + { + // PwEntry peLocal = m_pgRootGroup.FindEntry(pe.Uuid, true); + PwEntry peLocal = (ppOrg.GetItemByUuid(pe.Uuid) as PwEntry); + Debug.Assert(object.ReferenceEquals(peLocal, + m_pgRootGroup.FindEntry(pe.Uuid, true))); + + if(peLocal == null) + { + PwGroup pgSourceParent = pe.ParentGroup; + PwGroup pgLocalContainer; + if(pgSourceParent == pdSource.m_pgRootGroup) + pgLocalContainer = m_pgRootGroup; + else + pgLocalContainer = m_pgRootGroup.FindGroup(pgSourceParent.Uuid, true); + Debug.Assert(pgLocalContainer != null); + if(pgLocalContainer == null) pgLocalContainer = m_pgRootGroup; + + PwEntry peNew = new PwEntry(false, false); + peNew.Uuid = pe.Uuid; + peNew.AssignProperties(pe, false, true, true); + + // pgLocalContainer.AddEntry(peNew, true); + InsertObjectAtBestPos(pgLocalContainer.Entries, peNew, ppSrc); + peNew.ParentGroup = pgLocalContainer; + } + else // peLocal != null + { + Debug.Assert(mm != PwMergeMethod.CreateNewUuids); + + const PwCompareOptions cmpOpt = (PwCompareOptions.IgnoreParentGroup | + PwCompareOptions.IgnoreLastAccess | PwCompareOptions.IgnoreHistory | + PwCompareOptions.NullEmptyEquivStd); + bool bEquals = peLocal.EqualsEntry(pe, cmpOpt, MemProtCmpMode.None); + + bool bOrgBackup = !bEquals; + if(mm != PwMergeMethod.OverwriteExisting) + bOrgBackup &= (TimeUtil.CompareLastMod(pe, peLocal, true) > 0); + bOrgBackup &= !pe.HasBackupOfData(peLocal, false, true); + if(bOrgBackup) peLocal.CreateBackup(null); // Maintain at end + + bool bSrcBackup = !bEquals && (mm != PwMergeMethod.OverwriteExisting); + bSrcBackup &= (TimeUtil.CompareLastMod(peLocal, pe, true) > 0); + bSrcBackup &= !peLocal.HasBackupOfData(pe, false, true); + if(bSrcBackup) pe.CreateBackup(null); // Maintain at end + + if(mm == PwMergeMethod.OverwriteExisting) + peLocal.AssignProperties(pe, false, false, false); + else if((mm == PwMergeMethod.OverwriteIfNewer) || + (mm == PwMergeMethod.Synchronize)) + { + peLocal.AssignProperties(pe, true, false, false); + } + // else if(mm == PwMergeMethod.KeepExisting) ... + + MergeEntryHistory(peLocal, pe, mm); + } + + return ((slStatus != null) ? slStatus.ContinueWork() : true); + }; + + ghSrc(pdSource.RootGroup); + if(!pdSource.RootGroup.TraverseTree(TraversalMethod.PreOrder, ghSrc, ehSrc)) + throw new InvalidOperationException(); + + IStatusLogger slPrevStatus = m_slStatus; + m_slStatus = slStatus; + + if(mm == PwMergeMethod.Synchronize) + { + RelocateGroups(ppOrg, ppSrc); + RelocateEntries(ppOrg, ppSrc); + ReorderObjects(m_pgRootGroup, ppOrg, ppSrc); + + // After all relocations and reorderings + MergeInLocationChanged(m_pgRootGroup, ppOrg, ppSrc); + ppOrg = null; // Pools are now invalid, because the location + ppSrc = null; // changed times have been merged in + + // Delete *after* relocating, because relocating might + // empty some groups that are marked for deletion (and + // objects that weren't relocated yet might prevent the + // deletion) + Dictionary dOrgDel = CreateDeletedObjectsPool(); + MergeInDeletionInfo(pdSource.m_vDeletedObjects, dOrgDel); + ApplyDeletions(m_pgRootGroup, dOrgDel); + + // The list and the dictionary should be kept in sync + Debug.Assert(m_vDeletedObjects.UCount == (uint)dOrgDel.Count); + } + + // Must be called *after* merging groups, because group UUIDs + // are required for recycle bin and entry template UUIDs + MergeInDbProperties(pdSource, mm); + + MergeInCustomIcons(pdSource); + + MaintainBackups(); + + Debug.Assert(!HasDuplicateUuids()); + m_slStatus = slPrevStatus; + } + + private void MergeInCustomIcons(PwDatabase pdSource) + { + foreach(PwCustomIcon pwci in pdSource.CustomIcons) + { + if(GetCustomIconIndex(pwci.Uuid) >= 0) continue; + + m_vCustomIcons.Add(pwci); // PwCustomIcon is immutable + m_bUINeedsIconUpdate = true; + } + } + + private Dictionary CreateDeletedObjectsPool() + { + Dictionary d = + new Dictionary(); + + int n = (int)m_vDeletedObjects.UCount; + for(int i = n - 1; i >= 0; --i) + { + PwDeletedObject pdo = m_vDeletedObjects.GetAt((uint)i); + + PwDeletedObject pdoEx; + if(d.TryGetValue(pdo.Uuid, out pdoEx)) + { + Debug.Assert(false); // Found duplicate, which should not happen + + if(pdo.DeletionTime > pdoEx.DeletionTime) + pdoEx.DeletionTime = pdo.DeletionTime; + + m_vDeletedObjects.RemoveAt((uint)i); + } + else d[pdo.Uuid] = pdo; + } + + return d; + } + + private void MergeInDeletionInfo(PwObjectList lSrc, + Dictionary dOrgDel) + { + foreach(PwDeletedObject pdoSrc in lSrc) + { + PwDeletedObject pdoOrg; + if(dOrgDel.TryGetValue(pdoSrc.Uuid, out pdoOrg)) // Update + { + Debug.Assert(pdoOrg.Uuid.Equals(pdoSrc.Uuid)); + + if(pdoSrc.DeletionTime > pdoOrg.DeletionTime) + pdoOrg.DeletionTime = pdoSrc.DeletionTime; + } + else // Add + { + m_vDeletedObjects.Add(pdoSrc); + dOrgDel[pdoSrc.Uuid] = pdoSrc; + } + } + } + + private void ApplyDeletions(PwObjectList l, Predicate fCanDelete, + Dictionary dOrgDel) + where T : class, ITimeLogger, IStructureItem, IDeepCloneable + { + int n = (int)l.UCount; + for(int i = n - 1; i >= 0; --i) + { + if((m_slStatus != null) && !m_slStatus.ContinueWork()) break; + + T t = l.GetAt((uint)i); + + PwDeletedObject pdo; + if(dOrgDel.TryGetValue(t.Uuid, out pdo)) + { + Debug.Assert(t.Uuid.Equals(pdo.Uuid)); + + bool bDel = (TimeUtil.Compare(t.LastModificationTime, + pdo.DeletionTime, true) < 0); + bDel &= fCanDelete(t); + + if(bDel) l.RemoveAt((uint)i); + else + { + // Prevent future deletion attempts; this also prevents + // delayed deletions (emptying a group could cause a + // group to be deleted, if the deletion was prevented + // before due to the group not being empty) + if(!m_vDeletedObjects.Remove(pdo)) { Debug.Assert(false); } + if(!dOrgDel.Remove(pdo.Uuid)) { Debug.Assert(false); } + } + } + } + } + + private static bool SafeCanDeleteGroup(PwGroup pg) + { + if(pg == null) { Debug.Assert(false); return false; } + + if(pg.Groups.UCount > 0) return false; + if(pg.Entries.UCount > 0) return false; + return true; + } + + private static bool SafeCanDeleteEntry(PwEntry pe) + { + if(pe == null) { Debug.Assert(false); return false; } + + return true; + } + + // Apply deletions on all objects in the specified container + // (but not the container itself), using post-order traversal + // to avoid implicit deletions; + // https://sourceforge.net/p/keepass/bugs/1499/ + private void ApplyDeletions(PwGroup pgContainer, + Dictionary dOrgDel) + { + foreach(PwGroup pg in pgContainer.Groups) // Post-order traversal + { + ApplyDeletions(pg, dOrgDel); + } + + ApplyDeletions(pgContainer.Groups, PwDatabase.SafeCanDeleteGroup, dOrgDel); + ApplyDeletions(pgContainer.Entries, PwDatabase.SafeCanDeleteEntry, dOrgDel); + } + + private void RelocateGroups(PwObjectPoolEx ppOrg, PwObjectPoolEx ppSrc) + { + PwObjectList vGroups = m_pgRootGroup.GetGroups(true); + + foreach(PwGroup pg in vGroups) + { + if((m_slStatus != null) && !m_slStatus.ContinueWork()) break; + + // PwGroup pgOrg = pgOrgStructure.FindGroup(pg.Uuid, true); + IStructureItem ptOrg = ppOrg.GetItemByUuid(pg.Uuid); + if(ptOrg == null) continue; + // PwGroup pgSrc = pgSrcStructure.FindGroup(pg.Uuid, true); + IStructureItem ptSrc = ppSrc.GetItemByUuid(pg.Uuid); + if(ptSrc == null) continue; + + PwGroup pgOrgParent = ptOrg.ParentGroup; + // vGroups does not contain the root group, thus pgOrgParent + // should not be null + if(pgOrgParent == null) { Debug.Assert(false); continue; } + + PwGroup pgSrcParent = ptSrc.ParentGroup; + // pgSrcParent may be null (for the source root group) + if(pgSrcParent == null) continue; + + if(pgOrgParent.Uuid.Equals(pgSrcParent.Uuid)) + { + // pg.LocationChanged = ((ptSrc.LocationChanged > ptOrg.LocationChanged) ? + // ptSrc.LocationChanged : ptOrg.LocationChanged); + continue; + } + + if(ptSrc.LocationChanged > ptOrg.LocationChanged) + { + PwGroup pgLocal = m_pgRootGroup.FindGroup(pgSrcParent.Uuid, true); + if(pgLocal == null) { Debug.Assert(false); continue; } + + if(pgLocal.IsContainedIn(pg)) continue; + + pg.ParentGroup.Groups.Remove(pg); + + // pgLocal.AddGroup(pg, true); + InsertObjectAtBestPos(pgLocal.Groups, pg, ppSrc); + pg.ParentGroup = pgLocal; + + // pg.LocationChanged = ptSrc.LocationChanged; + } + else + { + Debug.Assert(pg.ParentGroup.Uuid.Equals(pgOrgParent.Uuid)); + Debug.Assert(pg.LocationChanged == ptOrg.LocationChanged); + } + } + + Debug.Assert(m_pgRootGroup.GetGroups(true).UCount == vGroups.UCount); + } + + private void RelocateEntries(PwObjectPoolEx ppOrg, PwObjectPoolEx ppSrc) + { + PwObjectList vEntries = m_pgRootGroup.GetEntries(true); + + foreach(PwEntry pe in vEntries) + { + if((m_slStatus != null) && !m_slStatus.ContinueWork()) break; + + // PwEntry peOrg = pgOrgStructure.FindEntry(pe.Uuid, true); + IStructureItem ptOrg = ppOrg.GetItemByUuid(pe.Uuid); + if(ptOrg == null) continue; + // PwEntry peSrc = pgSrcStructure.FindEntry(pe.Uuid, true); + IStructureItem ptSrc = ppSrc.GetItemByUuid(pe.Uuid); + if(ptSrc == null) continue; + + PwGroup pgOrg = ptOrg.ParentGroup; + PwGroup pgSrc = ptSrc.ParentGroup; + if(pgOrg.Uuid.Equals(pgSrc.Uuid)) + { + // pe.LocationChanged = ((ptSrc.LocationChanged > ptOrg.LocationChanged) ? + // ptSrc.LocationChanged : ptOrg.LocationChanged); + continue; + } + + if(ptSrc.LocationChanged > ptOrg.LocationChanged) + { + PwGroup pgLocal = m_pgRootGroup.FindGroup(pgSrc.Uuid, true); + if(pgLocal == null) { Debug.Assert(false); continue; } + + pe.ParentGroup.Entries.Remove(pe); + + // pgLocal.AddEntry(pe, true); + InsertObjectAtBestPos(pgLocal.Entries, pe, ppSrc); + pe.ParentGroup = pgLocal; + + // pe.LocationChanged = ptSrc.LocationChanged; + } + else + { + Debug.Assert(pe.ParentGroup.Uuid.Equals(pgOrg.Uuid)); + Debug.Assert(pe.LocationChanged == ptOrg.LocationChanged); + } + } + + Debug.Assert(m_pgRootGroup.GetEntries(true).UCount == vEntries.UCount); + } + + private void ReorderObjects(PwGroup pg, PwObjectPoolEx ppOrg, + PwObjectPoolEx ppSrc) + { + ReorderObjectList(pg.Groups, ppOrg, ppSrc); + ReorderObjectList(pg.Entries, ppOrg, ppSrc); + + foreach(PwGroup pgSub in pg.Groups) + { + ReorderObjects(pgSub, ppOrg, ppSrc); + } + } + + private void ReorderObjectList(PwObjectList lItems, + PwObjectPoolEx ppOrg, PwObjectPoolEx ppSrc) + where T : class, ITimeLogger, IStructureItem, IDeepCloneable + { + List> lBlocks = PartitionConsec(lItems, ppOrg, ppSrc); + if(lBlocks.Count <= 1) return; + +#if DEBUG + PwObjectList lOrgItems = lItems.CloneShallow(); +#endif + + Queue> qToDo = new Queue>(); + qToDo.Enqueue(new KeyValuePair(0, lBlocks.Count - 1)); + + while(qToDo.Count > 0) + { + if((m_slStatus != null) && !m_slStatus.ContinueWork()) break; + + KeyValuePair kvp = qToDo.Dequeue(); + if(kvp.Key >= kvp.Value) { Debug.Assert(false); continue; } + + PwObjectPoolEx pPool; + int iPivot = FindLocationChangedPivot(lBlocks, kvp, out pPool); + PwObjectBlock bPivot = lBlocks[iPivot]; + + T tPivotPrimary = bPivot.PrimaryItem; + if(tPivotPrimary == null) { Debug.Assert(false); continue; } + ulong idPivot = pPool.GetIdByUuid(tPivotPrimary.Uuid); + if(idPivot == 0) { Debug.Assert(false); continue; } + + Queue> qBefore = new Queue>(); + Queue> qAfter = new Queue>(); + bool bBefore = true; + + for(int i = kvp.Key; i <= kvp.Value; ++i) + { + if(i == iPivot) { bBefore = false; continue; } + + PwObjectBlock b = lBlocks[i]; + Debug.Assert(b.LocationChanged <= bPivot.LocationChanged); + + T t = b.PrimaryItem; + if(t != null) + { + ulong idBPri = pPool.GetIdByUuid(t.Uuid); + if(idBPri > 0) + { + if(idBPri < idPivot) qBefore.Enqueue(b); + else qAfter.Enqueue(b); + + continue; + } + } + else { Debug.Assert(false); } + + if(bBefore) qBefore.Enqueue(b); + else qAfter.Enqueue(b); + } + + int j = kvp.Key; + while(qBefore.Count > 0) { lBlocks[j] = qBefore.Dequeue(); ++j; } + int iNewPivot = j; + lBlocks[j] = bPivot; + ++j; + while(qAfter.Count > 0) { lBlocks[j] = qAfter.Dequeue(); ++j; } + Debug.Assert(j == (kvp.Value + 1)); + + if((iNewPivot - 1) > kvp.Key) + qToDo.Enqueue(new KeyValuePair(kvp.Key, iNewPivot - 1)); + if((iNewPivot + 1) < kvp.Value) + qToDo.Enqueue(new KeyValuePair(iNewPivot + 1, kvp.Value)); + } + + uint u = 0; + foreach(PwObjectBlock b in lBlocks) + { + foreach(T t in b) + { + lItems.SetAt(u, t); + ++u; + } + } + Debug.Assert(u == lItems.UCount); + +#if DEBUG + Debug.Assert(u == lOrgItems.UCount); + foreach(T ptItem in lOrgItems) + { + Debug.Assert(lItems.IndexOf(ptItem) >= 0); + } +#endif + } + + private static List> PartitionConsec(PwObjectList lItems, + PwObjectPoolEx ppOrg, PwObjectPoolEx ppSrc) + where T : class, ITimeLogger, IStructureItem, IDeepCloneable + { + List> lBlocks = new List>(); + + Dictionary dItemUuids = new Dictionary(); + foreach(T t in lItems) { dItemUuids[t.Uuid] = true; } + + uint n = lItems.UCount; + for(uint u = 0; u < n; ++u) + { + T t = lItems.GetAt(u); + + PwObjectBlock b = new PwObjectBlock(); + + DateTime dtLoc; + PwObjectPoolEx pPool = GetBestPool(t, ppOrg, ppSrc, out dtLoc); + b.Add(t, dtLoc, pPool); + + lBlocks.Add(b); + + ulong idOrg = ppOrg.GetIdByUuid(t.Uuid); + ulong idSrc = ppSrc.GetIdByUuid(t.Uuid); + if((idOrg == 0) || (idSrc == 0)) continue; + + for(uint x = u + 1; x < n; ++x) + { + T tNext = lItems.GetAt(x); + + ulong idOrgNext = idOrg + 1; + while(true) + { + IStructureItem ptOrg = ppOrg.GetItemById(idOrgNext); + if(ptOrg == null) { idOrgNext = 0; break; } + if(ptOrg.Uuid.Equals(tNext.Uuid)) break; // Found it + if(dItemUuids.ContainsKey(ptOrg.Uuid)) { idOrgNext = 0; break; } + ++idOrgNext; + } + if(idOrgNext == 0) break; + + ulong idSrcNext = idSrc + 1; + while(true) + { + IStructureItem ptSrc = ppSrc.GetItemById(idSrcNext); + if(ptSrc == null) { idSrcNext = 0; break; } + if(ptSrc.Uuid.Equals(tNext.Uuid)) break; // Found it + if(dItemUuids.ContainsKey(ptSrc.Uuid)) { idSrcNext = 0; break; } + ++idSrcNext; + } + if(idSrcNext == 0) break; + + pPool = GetBestPool(tNext, ppOrg, ppSrc, out dtLoc); + b.Add(tNext, dtLoc, pPool); + + ++u; + idOrg = idOrgNext; + idSrc = idSrcNext; + } + } + + return lBlocks; + } + + private static PwObjectPoolEx GetBestPool(T t, PwObjectPoolEx ppOrg, + PwObjectPoolEx ppSrc, out DateTime dtLoc) + where T : class, ITimeLogger, IStructureItem, IDeepCloneable + { + PwObjectPoolEx p = null; + dtLoc = TimeUtil.SafeMinValueUtc; + + IStructureItem ptOrg = ppOrg.GetItemByUuid(t.Uuid); + if(ptOrg != null) + { + dtLoc = ptOrg.LocationChanged; + p = ppOrg; + } + + IStructureItem ptSrc = ppSrc.GetItemByUuid(t.Uuid); + if((ptSrc != null) && (ptSrc.LocationChanged > dtLoc)) + { + dtLoc = ptSrc.LocationChanged; + p = ppSrc; + } + + Debug.Assert(p != null); + return p; + } + + private static int FindLocationChangedPivot(List> lBlocks, + KeyValuePair kvpRange, out PwObjectPoolEx pPool) + where T : class, ITimeLogger, IStructureItem, IDeepCloneable + { + pPool = null; + + int iPosMax = kvpRange.Key; + DateTime dtMax = TimeUtil.SafeMinValueUtc; + + for(int i = kvpRange.Key; i <= kvpRange.Value; ++i) + { + PwObjectBlock b = lBlocks[i]; + if(b.LocationChanged > dtMax) + { + iPosMax = i; + dtMax = b.LocationChanged; + pPool = b.PoolAssoc; + } + } + + return iPosMax; + } + + private static void MergeInLocationChanged(PwGroup pg, + PwObjectPoolEx ppOrg, PwObjectPoolEx ppSrc) + { + GroupHandler gh = delegate(PwGroup pgSub) + { + DateTime dt; + if(GetBestPool(pgSub, ppOrg, ppSrc, out dt) != null) + pgSub.LocationChanged = dt; + else { Debug.Assert(false); } + return true; + }; + + EntryHandler eh = delegate(PwEntry pe) + { + DateTime dt; + if(GetBestPool(pe, ppOrg, ppSrc, out dt) != null) + pe.LocationChanged = dt; + else { Debug.Assert(false); } + return true; + }; + + gh(pg); + pg.TraverseTree(TraversalMethod.PreOrder, gh, eh); + } + + private static void InsertObjectAtBestPos(PwObjectList lItems, + T tNew, PwObjectPoolEx ppSrc) + where T : class, ITimeLogger, IStructureItem, IDeepCloneable + { + if(tNew == null) { Debug.Assert(false); return; } + + ulong idSrc = ppSrc.GetIdByUuid(tNew.Uuid); + if(idSrc == 0) { Debug.Assert(false); lItems.Add(tNew); return; } + + const uint uIdOffset = 2; + Dictionary dOrg = new Dictionary(); + for(uint u = 0; u < lItems.UCount; ++u) + dOrg[lItems.GetAt(u).Uuid] = uIdOffset + u; + + ulong idSrcNext = idSrc + 1; + uint idOrgNext = 0; + while(true) + { + IStructureItem pNext = ppSrc.GetItemById(idSrcNext); + if(pNext == null) break; + if(dOrg.TryGetValue(pNext.Uuid, out idOrgNext)) break; + ++idSrcNext; + } + + if(idOrgNext != 0) + { + lItems.Insert(idOrgNext - uIdOffset, tNew); + return; + } + + ulong idSrcPrev = idSrc - 1; + uint idOrgPrev = 0; + while(true) + { + IStructureItem pPrev = ppSrc.GetItemById(idSrcPrev); + if(pPrev == null) break; + if(dOrg.TryGetValue(pPrev.Uuid, out idOrgPrev)) break; + --idSrcPrev; + } + + if(idOrgPrev != 0) + { + lItems.Insert(idOrgPrev + 1 - uIdOffset, tNew); + return; + } + + lItems.Add(tNew); + } + + private void MergeInDbProperties(PwDatabase pdSource, PwMergeMethod mm) + { + if(pdSource == null) { Debug.Assert(false); return; } + if((mm == PwMergeMethod.KeepExisting) || (mm == PwMergeMethod.None)) + return; + + bool bForce = (mm == PwMergeMethod.OverwriteExisting); + bool bSourceNewer = (pdSource.m_dtSettingsChanged > m_dtSettingsChanged); + + if(bForce || bSourceNewer) + { + m_dtSettingsChanged = pdSource.m_dtSettingsChanged; + + m_clr = pdSource.m_clr; + } + + if(bForce || (pdSource.m_dtNameChanged > m_dtNameChanged)) + { + m_strName = pdSource.m_strName; + m_dtNameChanged = pdSource.m_dtNameChanged; + } + + if(bForce || (pdSource.m_dtDescChanged > m_dtDescChanged)) + { + m_strDesc = pdSource.m_strDesc; + m_dtDescChanged = pdSource.m_dtDescChanged; + } + + if(bForce || (pdSource.m_dtDefaultUserChanged > m_dtDefaultUserChanged)) + { + m_strDefaultUserName = pdSource.m_strDefaultUserName; + m_dtDefaultUserChanged = pdSource.m_dtDefaultUserChanged; + } + + PwUuid pwPrefBin = m_pwRecycleBin, pwAltBin = pdSource.m_pwRecycleBin; + if(bForce || (pdSource.m_dtRecycleBinChanged > m_dtRecycleBinChanged)) + { + pwPrefBin = pdSource.m_pwRecycleBin; + pwAltBin = m_pwRecycleBin; + m_bUseRecycleBin = pdSource.m_bUseRecycleBin; + m_dtRecycleBinChanged = pdSource.m_dtRecycleBinChanged; + } + if(m_pgRootGroup.FindGroup(pwPrefBin, true) != null) + m_pwRecycleBin = pwPrefBin; + else if(m_pgRootGroup.FindGroup(pwAltBin, true) != null) + m_pwRecycleBin = pwAltBin; + else m_pwRecycleBin = PwUuid.Zero; // Debug.Assert(false); + + PwUuid pwPrefTmp = m_pwEntryTemplatesGroup, pwAltTmp = pdSource.m_pwEntryTemplatesGroup; + if(bForce || (pdSource.m_dtEntryTemplatesChanged > m_dtEntryTemplatesChanged)) + { + pwPrefTmp = pdSource.m_pwEntryTemplatesGroup; + pwAltTmp = m_pwEntryTemplatesGroup; + m_dtEntryTemplatesChanged = pdSource.m_dtEntryTemplatesChanged; + } + if(m_pgRootGroup.FindGroup(pwPrefTmp, true) != null) + m_pwEntryTemplatesGroup = pwPrefTmp; + else if(m_pgRootGroup.FindGroup(pwAltTmp, true) != null) + m_pwEntryTemplatesGroup = pwAltTmp; + else m_pwEntryTemplatesGroup = PwUuid.Zero; // Debug.Assert(false); + + foreach(KeyValuePair kvp in pdSource.m_dCustomData) + { + if(bSourceNewer || !m_dCustomData.Exists(kvp.Key)) + m_dCustomData.Set(kvp.Key, kvp.Value); + } + + VariantDictionary vdLocal = m_dPublicCustomData; // Backup + m_dPublicCustomData = (VariantDictionary)pdSource.m_dPublicCustomData.Clone(); + if(!bSourceNewer) vdLocal.CopyTo(m_dPublicCustomData); // Merge + } + + private void MergeEntryHistory(PwEntry pe, PwEntry peSource, + PwMergeMethod mm) + { + if(!pe.Uuid.Equals(peSource.Uuid)) { Debug.Assert(false); return; } + + if(pe.History.UCount == peSource.History.UCount) + { + bool bEqual = true; + for(uint uEnum = 0; uEnum < pe.History.UCount; ++uEnum) + { + if(pe.History.GetAt(uEnum).LastModificationTime != + peSource.History.GetAt(uEnum).LastModificationTime) + { + bEqual = false; + break; + } + } + + if(bEqual) return; + } + + if((m_slStatus != null) && !m_slStatus.ContinueWork()) return; + + IDictionary dict = +#if KeePassLibSD + new SortedList(); +#else + new SortedDictionary(); +#endif + foreach(PwEntry peOrg in pe.History) + { + dict[peOrg.LastModificationTime] = peOrg; + } + + foreach(PwEntry peSrc in peSource.History) + { + DateTime dt = peSrc.LastModificationTime; + if(dict.ContainsKey(dt)) + { + if(mm == PwMergeMethod.OverwriteExisting) + dict[dt] = peSrc.CloneDeep(); + } + else dict[dt] = peSrc.CloneDeep(); + } + + pe.History.Clear(); + foreach(KeyValuePair kvpCur in dict) + { + Debug.Assert(kvpCur.Value.Uuid.Equals(pe.Uuid)); + Debug.Assert(kvpCur.Value.History.UCount == 0); + pe.History.Add(kvpCur.Value); + } + } + + public bool MaintainBackups() + { + if(m_pgRootGroup == null) { Debug.Assert(false); return false; } + + bool bDeleted = false; + EntryHandler eh = delegate(PwEntry pe) + { + if(pe.MaintainBackups(this)) bDeleted = true; + return true; + }; + + m_pgRootGroup.TraverseTree(TraversalMethod.PreOrder, null, eh); + return bDeleted; + } + + /* /// + /// Synchronize current database with another one. + /// + /// Source file. + public void Synchronize(string strFile) + { + PwDatabase pdSource = new PwDatabase(); + + IOConnectionInfo ioc = IOConnectionInfo.FromPath(strFile); + pdSource.Open(ioc, m_pwUserKey, null); + + MergeIn(pdSource, PwMergeMethod.Synchronize); + } */ + + /// + /// Get the index of a custom icon. + /// + /// ID of the icon. + /// Index of the icon. + public int GetCustomIconIndex(PwUuid pwIconId) + { + for(int i = 0; i < m_vCustomIcons.Count; ++i) + { + PwCustomIcon pwci = m_vCustomIcons[i]; + if(pwci.Uuid.Equals(pwIconId)) + return i; + } + + // Debug.Assert(false); // Do not assert + return -1; + } + + public int GetCustomIconIndex(byte[] pbPngData) + { + if(pbPngData == null) { Debug.Assert(false); return -1; } + + for(int i = 0; i < m_vCustomIcons.Count; ++i) + { + PwCustomIcon pwci = m_vCustomIcons[i]; + byte[] pbEx = pwci.ImageDataPng; + if(pbEx == null) { Debug.Assert(false); continue; } + + if(MemUtil.ArraysEqual(pbEx, pbPngData)) + return i; + } + + return -1; + } + +#if KeePassUAP + public Image GetCustomIcon(PwUuid pwIconId) + { + int nIndex = GetCustomIconIndex(pwIconId); + if(nIndex >= 0) + return m_vCustomIcons[nIndex].GetImage(); + else { Debug.Assert(false); } + + return null; + } +#elif !KeePassLibSD + [Obsolete("Additionally specify the size.")] + public Image GetCustomIcon(PwUuid pwIconId) + { + return GetCustomIcon(pwIconId, 16, 16); // Backward compatibility + } + + /// + /// Get a custom icon. This method can return null, + /// e.g. if no cached image of the icon is available. + /// + /// ID of the icon. + /// Width of the returned image. If this is + /// negative, the image is returned in its original size. + /// Height of the returned image. If this is + /// negative, the image is returned in its original size. + public Image GetCustomIcon(PwUuid pwIconId, int w, int h) + { + int nIndex = GetCustomIconIndex(pwIconId); + if(nIndex >= 0) + { + if((w >= 0) && (h >= 0)) + return m_vCustomIcons[nIndex].GetImage(w, h); + else return m_vCustomIcons[nIndex].GetImage(); // No assert + } + else { Debug.Assert(false); } + + return null; + } +#endif + + public bool DeleteCustomIcons(List vUuidsToDelete) + { + Debug.Assert(vUuidsToDelete != null); + if(vUuidsToDelete == null) throw new ArgumentNullException("vUuidsToDelete"); + if(vUuidsToDelete.Count <= 0) return true; + + GroupHandler gh = delegate(PwGroup pg) + { + PwUuid uuidThis = pg.CustomIconUuid; + if(uuidThis.Equals(PwUuid.Zero)) return true; + + foreach(PwUuid uuidDelete in vUuidsToDelete) + { + if(uuidThis.Equals(uuidDelete)) + { + pg.CustomIconUuid = PwUuid.Zero; + break; + } + } + + return true; + }; + + EntryHandler eh = delegate(PwEntry pe) + { + RemoveCustomIconUuid(pe, vUuidsToDelete); + return true; + }; + + gh(m_pgRootGroup); + if(!m_pgRootGroup.TraverseTree(TraversalMethod.PreOrder, gh, eh)) + { + Debug.Assert(false); + return false; + } + + foreach(PwUuid pwUuid in vUuidsToDelete) + { + int nIndex = GetCustomIconIndex(pwUuid); + if(nIndex >= 0) m_vCustomIcons.RemoveAt(nIndex); + } + + return true; + } + + private static void RemoveCustomIconUuid(PwEntry pe, List vToDelete) + { + PwUuid uuidThis = pe.CustomIconUuid; + if(uuidThis.Equals(PwUuid.Zero)) return; + + foreach(PwUuid uuidDelete in vToDelete) + { + if(uuidThis.Equals(uuidDelete)) + { + pe.CustomIconUuid = PwUuid.Zero; + break; + } + } + + foreach(PwEntry peHistory in pe.History) + RemoveCustomIconUuid(peHistory, vToDelete); + } + + private int GetTotalObjectUuidCount() + { + uint uGroups, uEntries; + m_pgRootGroup.GetCounts(true, out uGroups, out uEntries); + + uint uTotal = uGroups + uEntries + 1; // 1 for root group + if(uTotal > 0x7FFFFFFFU) { Debug.Assert(false); return 0x7FFFFFFF; } + return (int)uTotal; + } + + internal bool HasDuplicateUuids() + { + int nTotal = GetTotalObjectUuidCount(); + Dictionary d = new Dictionary(nTotal); + bool bDupFound = false; + + GroupHandler gh = delegate(PwGroup pg) + { + PwUuid pu = pg.Uuid; + if(d.ContainsKey(pu)) + { + bDupFound = true; + return false; + } + + d.Add(pu, null); + Debug.Assert(d.ContainsKey(pu)); + return true; + }; + + EntryHandler eh = delegate(PwEntry pe) + { + PwUuid pu = pe.Uuid; + if(d.ContainsKey(pu)) + { + bDupFound = true; + return false; + } + + d.Add(pu, null); + Debug.Assert(d.ContainsKey(pu)); + return true; + }; + + gh(m_pgRootGroup); + m_pgRootGroup.TraverseTree(TraversalMethod.PreOrder, gh, eh); + + Debug.Assert(bDupFound || (d.Count == nTotal)); + return bDupFound; + } + + internal void FixDuplicateUuids() + { + int nTotal = GetTotalObjectUuidCount(); + Dictionary d = new Dictionary(nTotal); + + GroupHandler gh = delegate(PwGroup pg) + { + PwUuid pu = pg.Uuid; + if(d.ContainsKey(pu)) + { + pu = new PwUuid(true); + while(d.ContainsKey(pu)) { Debug.Assert(false); pu = new PwUuid(true); } + + pg.Uuid = pu; + } + + d.Add(pu, null); + return true; + }; + + EntryHandler eh = delegate(PwEntry pe) + { + PwUuid pu = pe.Uuid; + if(d.ContainsKey(pu)) + { + pu = new PwUuid(true); + while(d.ContainsKey(pu)) { Debug.Assert(false); pu = new PwUuid(true); } + + pe.SetUuid(pu, true); + } + + d.Add(pu, null); + return true; + }; + + gh(m_pgRootGroup); + m_pgRootGroup.TraverseTree(TraversalMethod.PreOrder, gh, eh); + + Debug.Assert(d.Count == nTotal); + Debug.Assert(!HasDuplicateUuids()); + } + + /* public void CreateBackupFile(IStatusLogger sl) + { + if(sl != null) sl.SetText(KLRes.CreatingBackupFile, LogStatusType.Info); + + IOConnectionInfo iocBk = m_ioSource.CloneDeep(); + iocBk.Path += StrBackupExtension; + + bool bMadeUnhidden = UrlUtil.UnhideFile(iocBk.Path); + + bool bFastCopySuccess = false; + if(m_ioSource.IsLocalFile() && (m_ioSource.UserName.Length == 0) && + (m_ioSource.Password.Length == 0)) + { + try + { + string strFile = m_ioSource.Path + StrBackupExtension; + File.Copy(m_ioSource.Path, strFile, true); + bFastCopySuccess = true; + } + catch(Exception) { Debug.Assert(false); } + } + + if(bFastCopySuccess == false) + { + using(Stream sIn = IOConnection.OpenRead(m_ioSource)) + { + using(Stream sOut = IOConnection.OpenWrite(iocBk)) + { + MemUtil.CopyStream(sIn, sOut); + } + } + } + + if(bMadeUnhidden) UrlUtil.HideFile(iocBk.Path, true); // Hide again + } */ + + /* private static void RemoveData(PwGroup pg) + { + EntryHandler eh = delegate(PwEntry pe) + { + pe.AutoType.Clear(); + pe.Binaries.Clear(); + pe.History.Clear(); + pe.Strings.Clear(); + return true; + }; + + pg.TraverseTree(TraversalMethod.PreOrder, null, eh); + } */ + + public uint DeleteDuplicateEntries(IStatusLogger sl) + { + uint uDeleted = 0; + + PwGroup pgRecycleBin = null; + if(m_bUseRecycleBin) + pgRecycleBin = m_pgRootGroup.FindGroup(m_pwRecycleBin, true); + + DateTime dtNow = DateTime.UtcNow; + PwObjectList l = m_pgRootGroup.GetEntries(true); + int i = 0; + while(true) + { + if(i >= ((int)l.UCount - 1)) break; + + if(sl != null) + { + long lCnt = (long)l.UCount, li = (long)i; + long nArTotal = (lCnt * lCnt) / 2L; + long nArCur = li * lCnt - ((li * li) / 2L); + long nArPct = (nArCur * 100L) / nArTotal; + if(nArPct < 0) nArPct = 0; + if(nArPct > 100) nArPct = 100; + if(!sl.SetProgress((uint)nArPct)) break; + } + + PwEntry peA = l.GetAt((uint)i); + + for(uint j = (uint)i + 1; j < l.UCount; ++j) + { + PwEntry peB = l.GetAt(j); + if(!DupEntriesEqual(peA, peB)) continue; + + bool bDeleteA = (TimeUtil.CompareLastMod(peA, peB, true) <= 0); + if(pgRecycleBin != null) + { + bool bAInBin = peA.IsContainedIn(pgRecycleBin); + bool bBInBin = peB.IsContainedIn(pgRecycleBin); + + if(bAInBin && !bBInBin) bDeleteA = true; + else if(bBInBin && !bAInBin) bDeleteA = false; + } + + if(bDeleteA) + { + peA.ParentGroup.Entries.Remove(peA); + m_vDeletedObjects.Add(new PwDeletedObject(peA.Uuid, dtNow)); + + l.RemoveAt((uint)i); + --i; + } + else + { + peB.ParentGroup.Entries.Remove(peB); + m_vDeletedObjects.Add(new PwDeletedObject(peB.Uuid, dtNow)); + + l.RemoveAt(j); + } + + ++uDeleted; + break; + } + + ++i; + } + + return uDeleted; + } + + private static List m_lStdFields = null; + private static bool DupEntriesEqual(PwEntry a, PwEntry b) + { + if(m_lStdFields == null) m_lStdFields = PwDefs.GetStandardFields(); + + foreach(string strStdKey in m_lStdFields) + { + string strA = a.Strings.ReadSafe(strStdKey); + string strB = b.Strings.ReadSafe(strStdKey); + if(!strA.Equals(strB)) return false; + } + + foreach(KeyValuePair kvpA in a.Strings) + { + if(PwDefs.IsStandardField(kvpA.Key)) continue; + + ProtectedString psB = b.Strings.Get(kvpA.Key); + if(psB == null) return false; + + // Ignore protection setting, compare values only + if(!psB.Equals(kvpA.Value, false)) return false; + } + + foreach(KeyValuePair kvpB in b.Strings) + { + if(PwDefs.IsStandardField(kvpB.Key)) continue; + + ProtectedString psA = a.Strings.Get(kvpB.Key); + if(psA == null) return false; + + // Must be equal by logic + Debug.Assert(psA.Equals(kvpB.Value, false)); + } + + if(a.Binaries.UCount != b.Binaries.UCount) return false; + foreach(KeyValuePair kvpBin in a.Binaries) + { + ProtectedBinary pbA = kvpBin.Value; + ProtectedBinary pbB = b.Binaries.Get(kvpBin.Key); + if(pbB == null) return false; + + // Ignore protection setting, compare values only + if(!pbB.Equals(pbA, false)) return false; + } + + return true; + } + + public uint DeleteEmptyGroups() + { + uint uDeleted = 0; + + PwObjectList l = m_pgRootGroup.GetGroups(true); + int iStart = (int)l.UCount - 1; + for(int i = iStart; i >= 0; --i) + { + PwGroup pg = l.GetAt((uint)i); + if((pg.Groups.UCount > 0) || (pg.Entries.UCount > 0)) continue; + + pg.ParentGroup.Groups.Remove(pg); + m_vDeletedObjects.Add(new PwDeletedObject(pg.Uuid, DateTime.UtcNow)); + + ++uDeleted; + } + + return uDeleted; + } + + public uint DeleteUnusedCustomIcons() + { + List lToDelete = new List(); + foreach(PwCustomIcon pwci in m_vCustomIcons) + lToDelete.Add(pwci.Uuid); + + GroupHandler gh = delegate(PwGroup pg) + { + PwUuid pwUuid = pg.CustomIconUuid; + if((pwUuid == null) || pwUuid.Equals(PwUuid.Zero)) return true; + + for(int i = 0; i < lToDelete.Count; ++i) + { + if(lToDelete[i].Equals(pwUuid)) + { + lToDelete.RemoveAt(i); + break; + } + } + + return true; + }; + + EntryHandler eh = delegate(PwEntry pe) + { + PwUuid pwUuid = pe.CustomIconUuid; + if((pwUuid == null) || pwUuid.Equals(PwUuid.Zero)) return true; + + for(int i = 0; i < lToDelete.Count; ++i) + { + if(lToDelete[i].Equals(pwUuid)) + { + lToDelete.RemoveAt(i); + break; + } + } + + return true; + }; + + gh(m_pgRootGroup); + m_pgRootGroup.TraverseTree(TraversalMethod.PreOrder, gh, eh); + + uint uDeleted = 0; + foreach(PwUuid pwDel in lToDelete) + { + int nIndex = GetCustomIconIndex(pwDel); + if(nIndex < 0) { Debug.Assert(false); continue; } + + m_vCustomIcons.RemoveAt(nIndex); + ++uDeleted; + } + + if(uDeleted > 0) m_bUINeedsIconUpdate = true; + return uDeleted; + } + } +} diff --git a/ModernKeePassLib/PwDefs.cs b/ModernKeePassLib/PwDefs.cs new file mode 100644 index 0000000..441e711 --- /dev/null +++ b/ModernKeePassLib/PwDefs.cs @@ -0,0 +1,517 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Xml.Serialization; + +using ModernKeePassLib.Delegates; +using ModernKeePassLib.Interfaces; +using ModernKeePassLib.Serialization; + +namespace ModernKeePassLib +{ + /// + /// Contains KeePassLib-global definitions and enums. + /// + public static class PwDefs + { + /// + /// The product name. + /// + public static readonly string ProductName = "KeePass Password Safe"; + + /// + /// A short, simple string representing the product name. The string + /// should contain no spaces, directory separator characters, etc. + /// + public static readonly string ShortProductName = "KeePass"; + + internal const string UnixName = "keepass2"; + internal const string ResClass = "KeePass2"; // With initial capital + + /// + /// Version, encoded as 32-bit unsigned integer. + /// 2.00 = 0x02000000, 2.01 = 0x02000100, ..., 2.18 = 0x02010800. + /// As of 2.19, the version is encoded component-wise per byte, + /// e.g. 2.19 = 0x02130000. + /// It is highly recommended to use FileVersion64 instead. + /// + public static readonly uint Version32 = 0x02290000; + + /// + /// Version, encoded as 64-bit unsigned integer + /// (component-wise, 16 bits per component). + /// + public static readonly ulong FileVersion64 = 0x0002002900000000UL; + + /// + /// Version, encoded as string. + /// + public static readonly string VersionString = "2.41"; + + public static readonly string Copyright = @"Copyright © 2003-2019 Dominik Reichl"; + + /// + /// Product website URL. Terminated by a forward slash. + /// + public static readonly string HomepageUrl = "https://keepass.info/"; + + /// + /// URL to the online translations page. + /// + public static readonly string TranslationsUrl = "https://keepass.info/translations.html"; + + /// + /// URL to the online plugins page. + /// + public static readonly string PluginsUrl = "https://keepass.info/plugins.html"; + + /// + /// Product donations URL. + /// + public static readonly string DonationsUrl = "https://keepass.info/donate.html"; + + /// + /// URL to the root path of the online KeePass help. Terminated by + /// a forward slash. + /// + public static readonly string HelpUrl = "https://keepass.info/help/"; + + /// + /// URL to a TXT file (eventually compressed) that contains information + /// about the latest KeePass version available on the website. + /// + public static readonly string VersionUrl = "https://www.dominik-reichl.de/update/version2x.txt.gz"; + + /// + /// A DateTime object that represents the time when the assembly + /// was loaded. + /// + public static readonly DateTime DtDefaultNow = DateTime.UtcNow; + + /// + /// Default number of master key encryption/transformation rounds + /// (making dictionary attacks harder). + /// + public static readonly ulong DefaultKeyEncryptionRounds = 60000; + + /// + /// Default identifier string for the title field. + /// Should not contain spaces, tabs or other whitespace. + /// + public const string TitleField = "Title"; + // Const instead of static readonly for backward compatibility with plugins + + /// + /// Default identifier string for the user name field. + /// Should not contain spaces, tabs or other whitespace. + /// + public const string UserNameField = "UserName"; + // Const instead of static readonly for backward compatibility with plugins + + /// + /// Default identifier string for the password field. + /// Should not contain spaces, tabs or other whitespace. + /// + public const string PasswordField = "Password"; + // Const instead of static readonly for backward compatibility with plugins + + /// + /// Default identifier string for the URL field. + /// Should not contain spaces, tabs or other whitespace. + /// + public const string UrlField = "URL"; + // Const instead of static readonly for backward compatibility with plugins + + /// + /// Default identifier string for the notes field. + /// Should not contain spaces, tabs or other whitespace. + /// + public const string NotesField = "Notes"; + // Const instead of static readonly for backward compatibility with plugins + + /// + /// Default identifier string for the field which will contain TAN indices. + /// + public static readonly string TanIndexField = UserNameField; + + /// + /// Default title of an entry that is really a TAN entry. + /// + public static readonly string TanTitle = @""; + + /// + /// Prefix of a custom auto-type string field. + /// + public static readonly string AutoTypeStringPrefix = "S:"; + + /// + /// Default string representing a hidden password. + /// + public static readonly string HiddenPassword = "********"; + + /// + /// Default auto-type keystroke sequence. If no custom sequence is + /// specified, this sequence is used. + /// + public static readonly string DefaultAutoTypeSequence = @"{USERNAME}{TAB}{PASSWORD}{ENTER}"; + + /// + /// Default auto-type keystroke sequence for TAN entries. If no custom + /// sequence is specified, this sequence is used. + /// + public static readonly string DefaultAutoTypeSequenceTan = @"{PASSWORD}"; + + /// + /// Check if a name is a standard field name. + /// + /// Input field name. + /// Returns true, if the field name is a standard + /// field name (title, user name, password, ...), otherwise false. + public static bool IsStandardField(string strFieldName) + { + Debug.Assert(strFieldName != null); if(strFieldName == null) return false; + + if(strFieldName.Equals(TitleField)) return true; + if(strFieldName.Equals(UserNameField)) return true; + if(strFieldName.Equals(PasswordField)) return true; + if(strFieldName.Equals(UrlField)) return true; + if(strFieldName.Equals(NotesField)) return true; + + return false; + } + + public static List GetStandardFields() + { + List l = new List(); + + l.Add(TitleField); + l.Add(UserNameField); + l.Add(PasswordField); + l.Add(UrlField); + l.Add(NotesField); + + return l; + } + + /// + /// Check whether an entry is a TAN entry. + /// + public static bool IsTanEntry(PwEntry pe) + { + if(pe == null) { Debug.Assert(false); return false; } + + return (pe.Strings.ReadSafe(PwDefs.TitleField) == TanTitle); + } + + internal static string GetTranslationDisplayVersion(string strFileVersion) + { + if(strFileVersion == null) { Debug.Assert(false); return string.Empty; } + + if(strFileVersion == "2.39") return "2.39 / 2.39.1"; + + return strFileVersion; + } + } + + // #pragma warning disable 1591 // Missing XML comments warning + /// + /// Search parameters for group and entry searches. + /// + public sealed class SearchParameters + { + private string m_strText = string.Empty; + [DefaultValue("")] + public string SearchString + { + get { return m_strText; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strText = value; + } + } + + private bool m_bRegex = false; + [DefaultValue(false)] + public bool RegularExpression + { + get { return m_bRegex; } + set { m_bRegex = value; } + } + + private bool m_bSearchInTitles = true; + [DefaultValue(true)] + public bool SearchInTitles + { + get { return m_bSearchInTitles; } + set { m_bSearchInTitles = value; } + } + + private bool m_bSearchInUserNames = true; + [DefaultValue(true)] + public bool SearchInUserNames + { + get { return m_bSearchInUserNames; } + set { m_bSearchInUserNames = value; } + } + + private bool m_bSearchInPasswords = false; + [DefaultValue(false)] + public bool SearchInPasswords + { + get { return m_bSearchInPasswords; } + set { m_bSearchInPasswords = value; } + } + + private bool m_bSearchInUrls = true; + [DefaultValue(true)] + public bool SearchInUrls + { + get { return m_bSearchInUrls; } + set { m_bSearchInUrls = value; } + } + + private bool m_bSearchInNotes = true; + [DefaultValue(true)] + public bool SearchInNotes + { + get { return m_bSearchInNotes; } + set { m_bSearchInNotes = value; } + } + + private bool m_bSearchInOther = true; + [DefaultValue(true)] + public bool SearchInOther + { + get { return m_bSearchInOther; } + set { m_bSearchInOther = value; } + } + + private bool m_bSearchInStringNames = false; + [DefaultValue(false)] + public bool SearchInStringNames + { + get { return m_bSearchInStringNames; } + set { m_bSearchInStringNames = value; } + } + + private bool m_bSearchInTags = true; + [DefaultValue(true)] + public bool SearchInTags + { + get { return m_bSearchInTags; } + set { m_bSearchInTags = value; } + } + + private bool m_bSearchInUuids = false; + [DefaultValue(false)] + public bool SearchInUuids + { + get { return m_bSearchInUuids; } + set { m_bSearchInUuids = value; } + } + + private bool m_bSearchInGroupPaths = false; + [DefaultValue(false)] + public bool SearchInGroupPaths + { + get { return m_bSearchInGroupPaths; } + set { m_bSearchInGroupPaths = value; } + } + + private bool m_bSearchInGroupNames = false; + [DefaultValue(false)] + public bool SearchInGroupNames + { + get { return m_bSearchInGroupNames; } + set { m_bSearchInGroupNames = value; } + } + +#if ModernKeePassLib || KeePassUAP + private StringComparison m_scType = StringComparison.OrdinalIgnoreCase; +#else + private StringComparison m_scType = StringComparison.InvariantCultureIgnoreCase; +#endif + /// + /// String comparison type. Specifies the condition when the specified + /// text matches a group/entry string. + /// + public StringComparison ComparisonMode + { + get { return m_scType; } + set { m_scType = value; } + } + + private bool m_bExcludeExpired = false; + [DefaultValue(false)] + public bool ExcludeExpired + { + get { return m_bExcludeExpired; } + set { m_bExcludeExpired = value; } + } + + private bool m_bRespectEntrySearchingDisabled = true; + [DefaultValue(true)] + public bool RespectEntrySearchingDisabled + { + get { return m_bRespectEntrySearchingDisabled; } + set { m_bRespectEntrySearchingDisabled = value; } + } + + private StrPwEntryDelegate m_fnDataTrf = null; + [XmlIgnore] + public StrPwEntryDelegate DataTransformationFn + { + get { return m_fnDataTrf; } + set { m_fnDataTrf = value; } + } + + private string m_strDataTrf = string.Empty; + /// + /// Only for serialization. + /// + [DefaultValue("")] + public string DataTransformation + { + get { return m_strDataTrf; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strDataTrf = value; + } + } + + [XmlIgnore] + public static SearchParameters None + { + get + { + SearchParameters sp = new SearchParameters(); + + Debug.Assert(sp.m_strText.Length == 0); + Debug.Assert(!sp.m_bRegex); + sp.m_bSearchInTitles = false; + sp.m_bSearchInUserNames = false; + Debug.Assert(!sp.m_bSearchInPasswords); + sp.m_bSearchInUrls = false; + sp.m_bSearchInNotes = false; + sp.m_bSearchInOther = false; + Debug.Assert(!sp.m_bSearchInStringNames); + sp.m_bSearchInTags = false; + Debug.Assert(!sp.m_bSearchInUuids); + Debug.Assert(!sp.m_bSearchInGroupPaths); + Debug.Assert(!sp.m_bSearchInGroupNames); + // Debug.Assert(sp.m_scType == StringComparison.InvariantCultureIgnoreCase); + Debug.Assert(!sp.m_bExcludeExpired); + Debug.Assert(sp.m_bRespectEntrySearchingDisabled); + + return sp; + } + } + + /// + /// Construct a new search parameters object. + /// + public SearchParameters() + { + } + + public SearchParameters Clone() + { + return (SearchParameters)this.MemberwiseClone(); + } + } + // #pragma warning restore 1591 // Missing XML comments warning + + // #pragma warning disable 1591 // Missing XML comments warning + /// + /// Memory protection configuration structure (for default fields). + /// + public sealed class MemoryProtectionConfig : IDeepCloneable + { + public bool ProtectTitle = false; + public bool ProtectUserName = false; + public bool ProtectPassword = true; + public bool ProtectUrl = false; + public bool ProtectNotes = false; + + // public bool AutoEnableVisualHiding = false; + + public MemoryProtectionConfig CloneDeep() + { + return (MemoryProtectionConfig)this.MemberwiseClone(); + } + + public bool GetProtection(string strField) + { + if(strField == PwDefs.TitleField) return this.ProtectTitle; + if(strField == PwDefs.UserNameField) return this.ProtectUserName; + if(strField == PwDefs.PasswordField) return this.ProtectPassword; + if(strField == PwDefs.UrlField) return this.ProtectUrl; + if(strField == PwDefs.NotesField) return this.ProtectNotes; + + return false; + } + } + // #pragma warning restore 1591 // Missing XML comments warning + + public sealed class ObjectTouchedEventArgs : EventArgs + { + private object m_o; + public object Object { get { return m_o; } } + + private bool m_bModified; + public bool Modified { get { return m_bModified; } } + + private bool m_bParentsTouched; + public bool ParentsTouched { get { return m_bParentsTouched; } } + + public ObjectTouchedEventArgs(object o, bool bModified, + bool bParentsTouched) + { + m_o = o; + m_bModified = bModified; + m_bParentsTouched = bParentsTouched; + } + } + + public sealed class IOAccessEventArgs : EventArgs + { + private IOConnectionInfo m_ioc; + public IOConnectionInfo IOConnectionInfo { get { return m_ioc; } } + + private IOConnectionInfo m_ioc2; + public IOConnectionInfo IOConnectionInfo2 { get { return m_ioc2; } } + + private IOAccessType m_t; + public IOAccessType Type { get { return m_t; } } + + public IOAccessEventArgs(IOConnectionInfo ioc, IOConnectionInfo ioc2, + IOAccessType t) + { + m_ioc = ioc; + m_ioc2 = ioc2; + m_t = t; + } + } +} diff --git a/ModernKeePassLib/PwDeletedObject.cs b/ModernKeePassLib/PwDeletedObject.cs new file mode 100644 index 0000000..474fe6a --- /dev/null +++ b/ModernKeePassLib/PwDeletedObject.cs @@ -0,0 +1,86 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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 ModernKeePassLib.Interfaces; + +namespace ModernKeePassLib +{ + /// + /// Represents an object that has been deleted. + /// + public sealed class PwDeletedObject : IDeepCloneable + { + private PwUuid m_uuid = PwUuid.Zero; + /// + /// UUID of the entry that has been deleted. + /// + public PwUuid Uuid + { + get { return m_uuid; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_uuid = value; + } + } + + private DateTime m_dtDeletionTime = PwDefs.DtDefaultNow; + /// + /// The date/time when the entry has been deleted. + /// + public DateTime DeletionTime + { + get { return m_dtDeletionTime; } + set { m_dtDeletionTime = value; } + } + + /// + /// Construct a new PwDeletedObject object. + /// + public PwDeletedObject() + { + } + + public PwDeletedObject(PwUuid uuid, DateTime dtDeletionTime) + { + if(uuid == null) throw new ArgumentNullException("uuid"); + + m_uuid = uuid; + m_dtDeletionTime = dtDeletionTime; + } + + /// + /// Clone the object. + /// + /// Value copy of the current object. + public PwDeletedObject CloneDeep() + { + PwDeletedObject pdo = new PwDeletedObject(); + + pdo.m_uuid = m_uuid; // PwUuid objects are immutable + pdo.m_dtDeletionTime = m_dtDeletionTime; + + return pdo; + } + } +} diff --git a/ModernKeePassLib/PwEntry.cs b/ModernKeePassLib/PwEntry.cs new file mode 100644 index 0000000..46259ab --- /dev/null +++ b/ModernKeePassLib/PwEntry.cs @@ -0,0 +1,947 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Drawing; + +using ModernKeePassLib.Collections; +using ModernKeePassLib.Interfaces; +using ModernKeePassLib.Security; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib +{ + /// + /// A class representing a password entry. A password entry consists of several + /// fields like title, user name, password, etc. Each password entry has a + /// unique ID (UUID). + /// + public sealed class PwEntry : ITimeLogger, IStructureItem, IDeepCloneable + { + private PwUuid m_uuid = PwUuid.Zero; + private PwGroup m_pParentGroup = null; + private DateTime m_tParentGroupLastMod = PwDefs.DtDefaultNow; + + private ProtectedStringDictionary m_listStrings = new ProtectedStringDictionary(); + private ProtectedBinaryDictionary m_listBinaries = new ProtectedBinaryDictionary(); + private AutoTypeConfig m_listAutoType = new AutoTypeConfig(); + private PwObjectList m_listHistory = new PwObjectList(); + + private PwIcon m_pwIcon = PwIcon.Key; + private PwUuid m_pwCustomIconID = PwUuid.Zero; + + private Color m_clrForeground = Color.Empty; + private Color m_clrBackground = Color.Empty; + + private DateTime m_tCreation = PwDefs.DtDefaultNow; + private DateTime m_tLastMod = PwDefs.DtDefaultNow; + private DateTime m_tLastAccess = PwDefs.DtDefaultNow; + private DateTime m_tExpire = PwDefs.DtDefaultNow; + private bool m_bExpires = false; + private ulong m_uUsageCount = 0; + + private string m_strOverrideUrl = string.Empty; + + private List m_vTags = new List(); + + private StringDictionaryEx m_dCustomData = new StringDictionaryEx(); + + /// + /// UUID of this entry. + /// + public PwUuid Uuid + { + get { return m_uuid; } + set + { + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + m_uuid = value; + } + } + + /// + /// Reference to a group which contains the current entry. + /// + public PwGroup ParentGroup + { + get { return m_pParentGroup; } + + // Plugins: use PwGroup.AddEntry instead. + internal set { m_pParentGroup = value; } + } + + /// + /// The date/time when the location of the object was last changed. + /// + public DateTime LocationChanged + { + get { return m_tParentGroupLastMod; } + set { m_tParentGroupLastMod = value; } + } + + /// + /// Get or set all entry strings. + /// + public ProtectedStringDictionary Strings + { + get { return m_listStrings; } + set + { + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + m_listStrings = value; + } + } + + /// + /// Get or set all entry binaries. + /// + public ProtectedBinaryDictionary Binaries + { + get { return m_listBinaries; } + set + { + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + m_listBinaries = value; + } + } + + /// + /// Get or set all auto-type window/keystroke sequence associations. + /// + public AutoTypeConfig AutoType + { + get { return m_listAutoType; } + set + { + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + m_listAutoType = value; + } + } + + /// + /// Get all previous versions of this entry (backups). + /// + public PwObjectList History + { + get { return m_listHistory; } + set + { + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + m_listHistory = value; + } + } + + /// + /// Image ID specifying the icon that will be used for this entry. + /// + public PwIcon IconId + { + get { return m_pwIcon; } + set { m_pwIcon = value; } + } + + /// + /// Get the custom icon ID. This value is 0, if no custom icon is + /// being used (i.e. the icon specified by the IconID property + /// should be displayed). + /// + public PwUuid CustomIconUuid + { + get { return m_pwCustomIconID; } + set + { + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + m_pwCustomIconID = value; + } + } + + /// + /// Get or set the foreground color of this entry. + /// + public Color ForegroundColor + { + get { return m_clrForeground; } + set { m_clrForeground = value; } + } + + /// + /// Get or set the background color of this entry. + /// + public Color BackgroundColor + { + get { return m_clrBackground; } + set { m_clrBackground = value; } + } + + /// + /// The date/time when this entry was created. + /// + public DateTime CreationTime + { + get { return m_tCreation; } + set { m_tCreation = value; } + } + + /// + /// The date/time when this entry was last modified. + /// + public DateTime LastModificationTime + { + get { return m_tLastMod; } + set { m_tLastMod = value; } + } + + /// + /// The date/time when this entry was last accessed (read). + /// + public DateTime LastAccessTime + { + get { return m_tLastAccess; } + set { m_tLastAccess = value; } + } + + /// + /// The date/time when this entry expires. Use the Expires property + /// to specify if the entry does actually expire or not. + /// + public DateTime ExpiryTime + { + get { return m_tExpire; } + set { m_tExpire = value; } + } + + /// + /// Specifies whether the entry expires or not. + /// + public bool Expires + { + get { return m_bExpires; } + set { m_bExpires = value; } + } + + /// + /// Get or set the usage count of the entry. To increase the usage + /// count by one, use the Touch function. + /// + public ulong UsageCount + { + get { return m_uUsageCount; } + set { m_uUsageCount = value; } + } + + /// + /// Entry-specific override URL. If this string is non-empty, + /// + public string OverrideUrl + { + get { return m_strOverrideUrl; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strOverrideUrl = value; + } + } + + /// + /// List of tags associated with this entry. + /// + public List Tags + { + get { return m_vTags; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_vTags = value; + } + } + + /// + /// Custom data container that can be used by plugins to store + /// own data in KeePass entries. + /// The data is stored in the encrypted part of encrypted + /// database files. + /// Use unique names for your items, e.g. "PluginName_ItemName". + /// + public StringDictionaryEx CustomData + { + get { return m_dCustomData; } + internal set + { + if(value == null) { Debug.Assert(false); throw new ArgumentNullException("value"); } + m_dCustomData = value; + } + } + + public static EventHandler EntryTouched; + public EventHandler Touched; + + /// + /// Construct a new, empty password entry. Member variables will be initialized + /// to their default values. + /// + /// If true, a new UUID will be created + /// for this entry. If false, the UUID is zero and you must set it + /// manually later. + /// If true, the creation, last modification + /// and last access times will be set to the current system time. + public PwEntry(bool bCreateNewUuid, bool bSetTimes) + { + if(bCreateNewUuid) m_uuid = new PwUuid(true); + + if(bSetTimes) + { + DateTime dtNow = DateTime.UtcNow; + m_tCreation = dtNow; + m_tLastMod = dtNow; + m_tLastAccess = dtNow; + m_tParentGroupLastMod = dtNow; + } + } + + /// + /// Construct a new, empty password entry. Member variables will be initialized + /// to their default values. + /// + /// Reference to the containing group, this + /// parameter may be null and set later manually. + /// If true, a new UUID will be created + /// for this entry. If false, the UUID is zero and you must set it + /// manually later. + /// If true, the creation, last modification + /// and last access times will be set to the current system time. + [Obsolete("Use a different constructor. To add an entry to a group, use AddEntry of PwGroup.")] + public PwEntry(PwGroup pwParentGroup, bool bCreateNewUuid, bool bSetTimes) + { + m_pParentGroup = pwParentGroup; + + if(bCreateNewUuid) m_uuid = new PwUuid(true); + + if(bSetTimes) + { + DateTime dtNow = DateTime.UtcNow; + m_tCreation = dtNow; + m_tLastMod = dtNow; + m_tLastAccess = dtNow; + m_tParentGroupLastMod = dtNow; + } + } + +#if DEBUG + // For display in debugger + public override string ToString() + { + return (@"PwEntry '" + m_listStrings.ReadSafe(PwDefs.TitleField) + @"'"); + } +#endif + + /// + /// Clone the current entry. The returned entry is an exact value copy + /// of the current entry (including UUID and parent group reference). + /// All mutable members are cloned. + /// + /// Exact value clone. All references to mutable values changed. + public PwEntry CloneDeep() + { + PwEntry peNew = new PwEntry(false, false); + + peNew.m_uuid = m_uuid; // PwUuid is immutable + peNew.m_pParentGroup = m_pParentGroup; + peNew.m_tParentGroupLastMod = m_tParentGroupLastMod; + + peNew.m_listStrings = m_listStrings.CloneDeep(); + peNew.m_listBinaries = m_listBinaries.CloneDeep(); + peNew.m_listAutoType = m_listAutoType.CloneDeep(); + peNew.m_listHistory = m_listHistory.CloneDeep(); + + peNew.m_pwIcon = m_pwIcon; + peNew.m_pwCustomIconID = m_pwCustomIconID; + + peNew.m_clrForeground = m_clrForeground; + peNew.m_clrBackground = m_clrBackground; + + peNew.m_tCreation = m_tCreation; + peNew.m_tLastMod = m_tLastMod; + peNew.m_tLastAccess = m_tLastAccess; + peNew.m_tExpire = m_tExpire; + peNew.m_bExpires = m_bExpires; + peNew.m_uUsageCount = m_uUsageCount; + + peNew.m_strOverrideUrl = m_strOverrideUrl; + + peNew.m_vTags = new List(m_vTags); + + peNew.m_dCustomData = m_dCustomData.CloneDeep(); + + return peNew; + } + + public PwEntry CloneStructure() + { + PwEntry peNew = new PwEntry(false, false); + + peNew.m_uuid = m_uuid; // PwUuid is immutable + peNew.m_tParentGroupLastMod = m_tParentGroupLastMod; + // Do not assign m_pParentGroup + + return peNew; + } + + private static PwCompareOptions BuildCmpOpt(bool bIgnoreParentGroup, + bool bIgnoreLastMod, bool bIgnoreLastAccess, bool bIgnoreHistory, + bool bIgnoreThisLastBackup) + { + PwCompareOptions pwOpt = PwCompareOptions.None; + if(bIgnoreParentGroup) pwOpt |= PwCompareOptions.IgnoreParentGroup; + if(bIgnoreLastMod) pwOpt |= PwCompareOptions.IgnoreLastMod; + if(bIgnoreLastAccess) pwOpt |= PwCompareOptions.IgnoreLastAccess; + if(bIgnoreHistory) pwOpt |= PwCompareOptions.IgnoreHistory; + if(bIgnoreThisLastBackup) pwOpt |= PwCompareOptions.IgnoreLastBackup; + return pwOpt; + } + + [Obsolete] + public bool EqualsEntry(PwEntry pe, bool bIgnoreParentGroup, bool bIgnoreLastMod, + bool bIgnoreLastAccess, bool bIgnoreHistory, bool bIgnoreThisLastBackup) + { + return EqualsEntry(pe, BuildCmpOpt(bIgnoreParentGroup, bIgnoreLastMod, + bIgnoreLastAccess, bIgnoreHistory, bIgnoreThisLastBackup), + MemProtCmpMode.None); + } + + [Obsolete] + public bool EqualsEntry(PwEntry pe, bool bIgnoreParentGroup, bool bIgnoreLastMod, + bool bIgnoreLastAccess, bool bIgnoreHistory, bool bIgnoreThisLastBackup, + MemProtCmpMode mpCmpStr) + { + return EqualsEntry(pe, BuildCmpOpt(bIgnoreParentGroup, bIgnoreLastMod, + bIgnoreLastAccess, bIgnoreHistory, bIgnoreThisLastBackup), mpCmpStr); + } + + public bool EqualsEntry(PwEntry pe, PwCompareOptions pwOpt, + MemProtCmpMode mpCmpStr) + { + if(pe == null) { Debug.Assert(false); return false; } + + bool bNeEqStd = ((pwOpt & PwCompareOptions.NullEmptyEquivStd) != + PwCompareOptions.None); + bool bIgnoreLastAccess = ((pwOpt & PwCompareOptions.IgnoreLastAccess) != + PwCompareOptions.None); + bool bIgnoreLastMod = ((pwOpt & PwCompareOptions.IgnoreLastMod) != + PwCompareOptions.None); + + if(!m_uuid.Equals(pe.m_uuid)) return false; + if((pwOpt & PwCompareOptions.IgnoreParentGroup) == PwCompareOptions.None) + { + if(m_pParentGroup != pe.m_pParentGroup) return false; + if(!bIgnoreLastMod && (m_tParentGroupLastMod != pe.m_tParentGroupLastMod)) + return false; + } + + if(!m_listStrings.EqualsDictionary(pe.m_listStrings, pwOpt, mpCmpStr)) + return false; + if(!m_listBinaries.EqualsDictionary(pe.m_listBinaries)) return false; + + if(!m_listAutoType.Equals(pe.m_listAutoType)) return false; + + if((pwOpt & PwCompareOptions.IgnoreHistory) == PwCompareOptions.None) + { + bool bIgnoreLastBackup = ((pwOpt & PwCompareOptions.IgnoreLastBackup) != + PwCompareOptions.None); + + if(!bIgnoreLastBackup && (m_listHistory.UCount != pe.m_listHistory.UCount)) + return false; + if(bIgnoreLastBackup && (m_listHistory.UCount == 0)) + { + Debug.Assert(false); + return false; + } + if(bIgnoreLastBackup && ((m_listHistory.UCount - 1) != pe.m_listHistory.UCount)) + return false; + + PwCompareOptions cmpSub = PwCompareOptions.IgnoreParentGroup; + if(bNeEqStd) cmpSub |= PwCompareOptions.NullEmptyEquivStd; + if(bIgnoreLastMod) cmpSub |= PwCompareOptions.IgnoreLastMod; + if(bIgnoreLastAccess) cmpSub |= PwCompareOptions.IgnoreLastAccess; + + for(uint uHist = 0; uHist < pe.m_listHistory.UCount; ++uHist) + { + if(!m_listHistory.GetAt(uHist).EqualsEntry(pe.m_listHistory.GetAt( + uHist), cmpSub, MemProtCmpMode.None)) + return false; + } + } + + if(m_pwIcon != pe.m_pwIcon) return false; + if(!m_pwCustomIconID.Equals(pe.m_pwCustomIconID)) return false; + + if(m_clrForeground != pe.m_clrForeground) return false; + if(m_clrBackground != pe.m_clrBackground) return false; + + if(m_tCreation != pe.m_tCreation) return false; + if(!bIgnoreLastMod && (m_tLastMod != pe.m_tLastMod)) return false; + if(!bIgnoreLastAccess && (m_tLastAccess != pe.m_tLastAccess)) return false; + if(m_tExpire != pe.m_tExpire) return false; + if(m_bExpires != pe.m_bExpires) return false; + if(!bIgnoreLastAccess && (m_uUsageCount != pe.m_uUsageCount)) return false; + + if(m_strOverrideUrl != pe.m_strOverrideUrl) return false; + + if(m_vTags.Count != pe.m_vTags.Count) return false; + for(int iTag = 0; iTag < m_vTags.Count; ++iTag) + { + if(m_vTags[iTag] != pe.m_vTags[iTag]) return false; + } + + if(!m_dCustomData.Equals(pe.m_dCustomData)) return false; + + return true; + } + + /// + /// Assign properties to the current entry based on a template entry. + /// + /// Template entry. Must not be null. + /// Only set the properties of the template entry + /// if it is newer than the current one. + /// If true, the history will be + /// copied, too. + /// If true, the + /// LocationChanged property is copied, otherwise not. + public void AssignProperties(PwEntry peTemplate, bool bOnlyIfNewer, + bool bIncludeHistory, bool bAssignLocationChanged) + { + if(peTemplate == null) { Debug.Assert(false); throw new ArgumentNullException("peTemplate"); } + + if(bOnlyIfNewer && (TimeUtil.Compare(peTemplate.m_tLastMod, + m_tLastMod, true) < 0)) + return; + + // Template UUID should be the same as the current one + Debug.Assert(m_uuid.Equals(peTemplate.m_uuid)); + m_uuid = peTemplate.m_uuid; + + if(bAssignLocationChanged) + m_tParentGroupLastMod = peTemplate.m_tParentGroupLastMod; + + m_listStrings = peTemplate.m_listStrings.CloneDeep(); + m_listBinaries = peTemplate.m_listBinaries.CloneDeep(); + m_listAutoType = peTemplate.m_listAutoType.CloneDeep(); + if(bIncludeHistory) + m_listHistory = peTemplate.m_listHistory.CloneDeep(); + + m_pwIcon = peTemplate.m_pwIcon; + m_pwCustomIconID = peTemplate.m_pwCustomIconID; // Immutable + + m_clrForeground = peTemplate.m_clrForeground; + m_clrBackground = peTemplate.m_clrBackground; + + m_tCreation = peTemplate.m_tCreation; + m_tLastMod = peTemplate.m_tLastMod; + m_tLastAccess = peTemplate.m_tLastAccess; + m_tExpire = peTemplate.m_tExpire; + m_bExpires = peTemplate.m_bExpires; + m_uUsageCount = peTemplate.m_uUsageCount; + + m_strOverrideUrl = peTemplate.m_strOverrideUrl; + + m_vTags = new List(peTemplate.m_vTags); + + m_dCustomData = peTemplate.m_dCustomData.CloneDeep(); + } + + /// + /// Touch the entry. This function updates the internal last access + /// time. If the parameter is true, + /// the last modification time gets updated, too. + /// + /// Modify last modification time. + public void Touch(bool bModified) + { + Touch(bModified, true); + } + + /// + /// Touch the entry. This function updates the internal last access + /// time. If the parameter is true, + /// the last modification time gets updated, too. + /// + /// Modify last modification time. + /// If true, all parent objects + /// get touched, too. + public void Touch(bool bModified, bool bTouchParents) + { + m_tLastAccess = DateTime.UtcNow; + ++m_uUsageCount; + + if(bModified) m_tLastMod = m_tLastAccess; + + if(this.Touched != null) + this.Touched(this, new ObjectTouchedEventArgs(this, + bModified, bTouchParents)); + if(PwEntry.EntryTouched != null) + PwEntry.EntryTouched(this, new ObjectTouchedEventArgs(this, + bModified, bTouchParents)); + + if(bTouchParents && (m_pParentGroup != null)) + m_pParentGroup.Touch(bModified, true); + } + + /// + /// Create a backup of this entry. The backup item doesn't contain any + /// history items. + /// + [Obsolete] + public void CreateBackup() + { + CreateBackup(null); + } + + /// + /// Create a backup of this entry. The backup item doesn't contain any + /// history items. + /// If this parameter isn't null, + /// the history list is maintained automatically (i.e. old backups are + /// deleted if there are too many or the history size is too large). + /// This parameter may be null (no maintenance then). + /// + public void CreateBackup(PwDatabase pwHistMntcSettings) + { + PwEntry peCopy = CloneDeep(); + peCopy.History = new PwObjectList(); // Remove history + + m_listHistory.Add(peCopy); // Must be added at end, see EqualsEntry + + if(pwHistMntcSettings != null) MaintainBackups(pwHistMntcSettings); + } + + /// + /// Restore an entry snapshot from backups. + /// + /// Index of the backup item, to which + /// should be reverted. + [Obsolete] + public void RestoreFromBackup(uint uBackupIndex) + { + RestoreFromBackup(uBackupIndex, null); + } + + /// + /// Restore an entry snapshot from backups. + /// + /// Index of the backup item, to which + /// should be reverted. + /// If this parameter isn't null, + /// the history list is maintained automatically (i.e. old backups are + /// deleted if there are too many or the history size is too large). + /// This parameter may be null (no maintenance then). + public void RestoreFromBackup(uint uBackupIndex, PwDatabase pwHistMntcSettings) + { + Debug.Assert(uBackupIndex < m_listHistory.UCount); + if(uBackupIndex >= m_listHistory.UCount) + throw new ArgumentOutOfRangeException("uBackupIndex"); + + PwEntry pe = m_listHistory.GetAt(uBackupIndex); + Debug.Assert(pe != null); if(pe == null) throw new InvalidOperationException(); + + CreateBackup(pwHistMntcSettings); // Backup current data before restoring + AssignProperties(pe, false, false, false); + } + + public bool HasBackupOfData(PwEntry peData, bool bIgnoreLastMod, + bool bIgnoreLastAccess) + { + if(peData == null) { Debug.Assert(false); return false; } + + PwCompareOptions cmpOpt = (PwCompareOptions.IgnoreParentGroup | + PwCompareOptions.IgnoreHistory | PwCompareOptions.NullEmptyEquivStd); + if(bIgnoreLastMod) cmpOpt |= PwCompareOptions.IgnoreLastMod; + if(bIgnoreLastAccess) cmpOpt |= PwCompareOptions.IgnoreLastAccess; + + foreach(PwEntry pe in m_listHistory) + { + if(pe.EqualsEntry(peData, cmpOpt, MemProtCmpMode.None)) return true; + } + + return false; + } + + /// + /// Delete old history items if there are too many or the history + /// size is too large. + /// If one or more history items have been deleted, true + /// is returned. Otherwise false. + /// + public bool MaintainBackups(PwDatabase pwSettings) + { + if(pwSettings == null) { Debug.Assert(false); return false; } + + bool bDeleted = false; + + int nMaxItems = pwSettings.HistoryMaxItems; + if(nMaxItems >= 0) + { + while(m_listHistory.UCount > (uint)nMaxItems) + { + RemoveOldestBackup(); + bDeleted = true; + } + } + + long lMaxSize = pwSettings.HistoryMaxSize; + if(lMaxSize >= 0) + { + while(true) + { + ulong uHistSize = 0; + foreach(PwEntry pe in m_listHistory) { uHistSize += pe.GetSize(); } + + if(uHistSize > (ulong)lMaxSize) + { + RemoveOldestBackup(); + bDeleted = true; + } + else break; + } + } + + return bDeleted; + } + + private void RemoveOldestBackup() + { + DateTime dtMin = TimeUtil.SafeMaxValueUtc; + uint idxRemove = uint.MaxValue; + + for(uint u = 0; u < m_listHistory.UCount; ++u) + { + PwEntry pe = m_listHistory.GetAt(u); + if(TimeUtil.Compare(pe.LastModificationTime, dtMin, true) < 0) + { + idxRemove = u; + dtMin = pe.LastModificationTime; + } + } + + if(idxRemove != uint.MaxValue) m_listHistory.RemoveAt(idxRemove); + } + + public bool GetAutoTypeEnabled() + { + if(!m_listAutoType.Enabled) return false; + + if(m_pParentGroup != null) + return m_pParentGroup.GetAutoTypeEnabledInherited(); + + return PwGroup.DefaultAutoTypeEnabled; + } + + public string GetAutoTypeSequence() + { + string strSeq = m_listAutoType.DefaultSequence; + + PwGroup pg = m_pParentGroup; + while(pg != null) + { + if(strSeq.Length != 0) break; + + strSeq = pg.DefaultAutoTypeSequence; + pg = pg.ParentGroup; + } + + if(strSeq.Length != 0) return strSeq; + + if(PwDefs.IsTanEntry(this)) return PwDefs.DefaultAutoTypeSequenceTan; + return PwDefs.DefaultAutoTypeSequence; + } + + public bool GetSearchingEnabled() + { + if(m_pParentGroup != null) + return m_pParentGroup.GetSearchingEnabledInherited(); + + return PwGroup.DefaultSearchingEnabled; + } + + /// + /// Approximate the total size (in process memory) of this entry + /// in bytes (including strings, binaries and history entries). + /// + /// Size in bytes. + public ulong GetSize() + { + // This method assumes 64-bit pointers/references and Unicode + // strings (i.e. 2 bytes per character) + + ulong cb = 248; // Number of bytes; approx. fixed length data + ulong cc = 0; // Number of characters + + cb += (ulong)m_listStrings.UCount * 40; + foreach(KeyValuePair kvpStr in m_listStrings) + cc += (ulong)kvpStr.Key.Length + (ulong)kvpStr.Value.Length; + + cb += (ulong)m_listBinaries.UCount * 65; + foreach(KeyValuePair kvpBin in m_listBinaries) + { + cc += (ulong)kvpBin.Key.Length; + cb += (ulong)kvpBin.Value.Length; + } + + cc += (ulong)m_listAutoType.DefaultSequence.Length; + cb += (ulong)m_listAutoType.AssociationsCount * 24; + foreach(AutoTypeAssociation a in m_listAutoType.Associations) + cc += (ulong)a.WindowName.Length + (ulong)a.Sequence.Length; + + cb += (ulong)m_listHistory.UCount * 8; + foreach(PwEntry peHistory in m_listHistory) + cb += peHistory.GetSize(); + + cc += (ulong)m_strOverrideUrl.Length; + + cb += (ulong)m_vTags.Count * 8; + foreach(string strTag in m_vTags) + cc += (ulong)strTag.Length; + + cb += (ulong)m_dCustomData.Count * 16; + foreach(KeyValuePair kvp in m_dCustomData) + cc += (ulong)kvp.Key.Length + (ulong)kvp.Value.Length; + + return (cb + (cc << 1)); + } + + public bool HasTag(string strTag) + { + if(string.IsNullOrEmpty(strTag)) { Debug.Assert(false); return false; } + + for(int i = 0; i < m_vTags.Count; ++i) + { + if(m_vTags[i].Equals(strTag, StrUtil.CaseIgnoreCmp)) return true; + } + + return false; + } + + public bool AddTag(string strTag) + { + if(string.IsNullOrEmpty(strTag)) { Debug.Assert(false); return false; } + + for(int i = 0; i < m_vTags.Count; ++i) + { + if(m_vTags[i].Equals(strTag, StrUtil.CaseIgnoreCmp)) return false; + } + + m_vTags.Add(strTag); + return true; + } + + public bool RemoveTag(string strTag) + { + if(string.IsNullOrEmpty(strTag)) { Debug.Assert(false); return false; } + + for(int i = 0; i < m_vTags.Count; ++i) + { + if(m_vTags[i].Equals(strTag, StrUtil.CaseIgnoreCmp)) + { + m_vTags.RemoveAt(i); + return true; + } + } + + return false; + } + + public bool IsContainedIn(PwGroup pgContainer) + { + PwGroup pgCur = m_pParentGroup; + while(pgCur != null) + { + if(pgCur == pgContainer) return true; + + pgCur = pgCur.ParentGroup; + } + + return false; + } + + public void SetUuid(PwUuid pwNewUuid, bool bAlsoChangeHistoryUuids) + { + this.Uuid = pwNewUuid; + + if(bAlsoChangeHistoryUuids) + { + foreach(PwEntry peHist in m_listHistory) + { + peHist.Uuid = pwNewUuid; + } + } + } + + public void SetCreatedNow() + { + DateTime dt = DateTime.UtcNow; + + m_tCreation = dt; + m_tLastAccess = dt; + } + + public PwEntry Duplicate() + { + PwEntry pe = CloneDeep(); + + pe.SetUuid(new PwUuid(true), true); + pe.SetCreatedNow(); + + return pe; + } + } + + public sealed class PwEntryComparer : IComparer + { + private string m_strFieldName; + private bool m_bCaseInsensitive; + private bool m_bCompareNaturally; + + public PwEntryComparer(string strFieldName, bool bCaseInsensitive, + bool bCompareNaturally) + { + if(strFieldName == null) throw new ArgumentNullException("strFieldName"); + + m_strFieldName = strFieldName; + m_bCaseInsensitive = bCaseInsensitive; + m_bCompareNaturally = bCompareNaturally; + } + + public int Compare(PwEntry a, PwEntry b) + { + string strA = a.Strings.ReadSafe(m_strFieldName); + string strB = b.Strings.ReadSafe(m_strFieldName); + + if(m_bCompareNaturally) return StrUtil.CompareNaturally(strA, strB); + +#if ModernKeePassLib || KeePassRT + return string.Compare(strA, strB, m_bCaseInsensitive ? + StringComparison.CurrentCultureIgnoreCase : StringComparison.CurrentCulture); +#else + return string.Compare(strA, strB, m_bCaseInsensitive); +#endif + } + } +} diff --git a/ModernKeePassLib/PwEnums.cs b/ModernKeePassLib/PwEnums.cs new file mode 100644 index 0000000..d817303 --- /dev/null +++ b/ModernKeePassLib/PwEnums.cs @@ -0,0 +1,319 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; + +namespace ModernKeePassLib +{ + /// + /// Compression algorithm specifiers. + /// + public enum PwCompressionAlgorithm + { + /// + /// No compression. + /// + None = 0, + + /// + /// GZip compression. + /// + GZip = 1, + + /// + /// Virtual field: currently known number of algorithms. Should not be used + /// by plugins or libraries -- it's used internally only. + /// + Count = 2 + } + + /// + /// Tree traversal methods. + /// + public enum TraversalMethod + { + /// + /// Don't traverse the tree. + /// + None = 0, + + /// + /// Traverse the tree in pre-order mode, i.e. first visit all items + /// in the current node, then visit all subnodes. + /// + PreOrder = 1 + } + + /// + /// Methods for merging password databases/entries. + /// + public enum PwMergeMethod + { + // Do not change the explicitly assigned values, otherwise + // serialization (e.g. of Ecas triggers) breaks + None = 0, + OverwriteExisting = 1, + KeepExisting = 2, + OverwriteIfNewer = 3, + CreateNewUuids = 4, + Synchronize = 5 + } + + /// + /// Icon identifiers for groups and password entries. + /// + public enum PwIcon + { + Key = 0, + World, + Warning, + NetworkServer, + MarkedDirectory, + UserCommunication, + Parts, + Notepad, + WorldSocket, + Identity, + PaperReady, + Digicam, + IRCommunication, + MultiKeys, + Energy, + Scanner, + WorldStar, + CDRom, + Monitor, + EMail, + Configuration, + ClipboardReady, + PaperNew, + Screen, + EnergyCareful, + EMailBox, + Disk, + Drive, + PaperQ, + TerminalEncrypted, + Console, + Printer, + ProgramIcons, + Run, + Settings, + WorldComputer, + Archive, + Homebanking, + DriveWindows, + Clock, + EMailSearch, + PaperFlag, + Memory, + TrashBin, + Note, + Expired, + Info, + Package, + Folder, + FolderOpen, + FolderPackage, + LockOpen, + PaperLocked, + Checked, + Pen, + Thumbnail, + Book, + List, + UserKey, + Tool, + Home, + Star, + Tux, + Feather, + Apple, + Wiki, + Money, + Certificate, + BlackBerry, + + /// + /// Virtual identifier -- represents the number of icons. + /// + Count + } + + public enum ProxyServerType + { + None = 0, + System = 1, + Manual = 2 + } + + public enum ProxyAuthType + { + None = 0, + + /// + /// Use default user credentials (provided by the system). + /// + Default = 1, + + Manual = 2, + + /// + /// Default or Manual, depending on whether + /// manual credentials are available. + /// This type exists for supporting upgrading from KeePass + /// 2.28 to 2.29; the user cannot select this type. + /// + Auto = 3 + } + + /// + /// Comparison modes for in-memory protected objects. + /// + public enum MemProtCmpMode + { + /// + /// Ignore the in-memory protection states. + /// + None = 0, + + /// + /// Ignore the in-memory protection states of standard + /// objects; do compare in-memory protection states of + /// custom objects. + /// + CustomOnly, + + /// + /// Compare in-memory protection states. + /// + Full + } + + [Flags] + public enum PwCompareOptions + { + None = 0x0, + + /// + /// Empty standard string fields are considered to be the + /// same as non-existing standard string fields. + /// This doesn't affect custom string comparisons. + /// + NullEmptyEquivStd = 0x1, + + IgnoreParentGroup = 0x2, + IgnoreLastAccess = 0x4, + IgnoreLastMod = 0x8, + IgnoreHistory = 0x10, + IgnoreLastBackup = 0x20, + + // For groups: + PropertiesOnly = 0x40, + + IgnoreTimes = (IgnoreLastAccess | IgnoreLastMod) + } + + public enum IOAccessType + { + None = 0, + + /// + /// The IO connection is being opened for reading. + /// + Read = 1, + + /// + /// The IO connection is being opened for writing. + /// + Write = 2, + + /// + /// The IO connection is being opened for testing + /// whether a file/object exists. + /// + Exists = 3, + + /// + /// The IO connection is being opened for deleting a file/object. + /// + Delete = 4, + + /// + /// The IO connection is being opened for renaming/moving a file/object. + /// + Move = 5 + } + + // public enum PwLogicalOp + // { + // None = 0, + // Or = 1, + // And = 2, + // NOr = 3, + // NAnd = 4 + // } + + [Flags] + public enum AppRunFlags + { + None = 0, + GetStdOutput = 1, + WaitForExit = 2, + + // https://sourceforge.net/p/keepass/patches/84/ + /// + /// This flag prevents any handles being garbage-collected + /// before the started process has terminated, without + /// blocking the current thread. + /// + GCKeepAlive = 4, + + // https://sourceforge.net/p/keepass/patches/85/ + DoEvents = 8, + DisableForms = 16 + } + + [Flags] + public enum ScaleTransformFlags + { + None = 0, + + /// + /// UIIcon indicates that the returned image is going + /// to be displayed as icon in the UI and that it is not + /// subject to future changes in size. + /// + UIIcon = 1 + } + + public enum DesktopType + { + None = 0, + Windows, + Gnome, + Kde, + Unity, + Lxde, + Xfce, + Mate, + Cinnamon, + Pantheon + } +} diff --git a/ModernKeePassLib/PwGroup.cs b/ModernKeePassLib/PwGroup.cs new file mode 100644 index 0000000..6a58611 --- /dev/null +++ b/ModernKeePassLib/PwGroup.cs @@ -0,0 +1,1750 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text.RegularExpressions; + +using ModernKeePassLib.Collections; +using ModernKeePassLib.Delegates; +using ModernKeePassLib.Interfaces; +using ModernKeePassLib.Security; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib +{ + /// + /// A group containing several password entries. + /// + public sealed class PwGroup : ITimeLogger, IStructureItem, IDeepCloneable + { + public const bool DefaultAutoTypeEnabled = true; + public const bool DefaultSearchingEnabled = true; + + private PwObjectList m_listGroups = new PwObjectList(); + private PwObjectList m_listEntries = new PwObjectList(); + private PwGroup m_pParentGroup = null; + private DateTime m_tParentGroupLastMod = PwDefs.DtDefaultNow; + + private PwUuid m_uuid = PwUuid.Zero; + private string m_strName = string.Empty; + private string m_strNotes = string.Empty; + + private PwIcon m_pwIcon = PwIcon.Folder; + private PwUuid m_pwCustomIconID = PwUuid.Zero; + + private DateTime m_tCreation = PwDefs.DtDefaultNow; + private DateTime m_tLastMod = PwDefs.DtDefaultNow; + private DateTime m_tLastAccess = PwDefs.DtDefaultNow; + private DateTime m_tExpire = PwDefs.DtDefaultNow; + private bool m_bExpires = false; + private ulong m_uUsageCount = 0; + + private bool m_bIsExpanded = true; + private bool m_bVirtual = false; + + private string m_strDefaultAutoTypeSequence = string.Empty; + + private bool? m_bEnableAutoType = null; + private bool? m_bEnableSearching = null; + + private PwUuid m_pwLastTopVisibleEntry = PwUuid.Zero; + + private StringDictionaryEx m_dCustomData = new StringDictionaryEx(); + + /// + /// UUID of this group. + /// + public PwUuid Uuid + { + get { return m_uuid; } + set + { + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + m_uuid = value; + } + } + + /// + /// The name of this group. Cannot be null. + /// + public string Name + { + get { return m_strName; } + set + { + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + m_strName = value; + } + } + + /// + /// Comments about this group. Cannot be null. + /// + public string Notes + { + get { return m_strNotes; } + set + { + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + m_strNotes = value; + } + } + + /// + /// Icon of the group. + /// + public PwIcon IconId + { + get { return m_pwIcon; } + set { m_pwIcon = value; } + } + + /// + /// Get the custom icon ID. This value is 0, if no custom icon is + /// being used (i.e. the icon specified by the IconID property + /// should be displayed). + /// + public PwUuid CustomIconUuid + { + get { return m_pwCustomIconID; } + set + { + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + m_pwCustomIconID = value; + } + } + + /// + /// Reference to the group to which this group belongs. May be null. + /// + public PwGroup ParentGroup + { + get { return m_pParentGroup; } + + // Plugins: use PwGroup.AddGroup instead. + internal set { Debug.Assert(value != this); m_pParentGroup = value; } + } + + /// + /// The date/time when the location of the object was last changed. + /// + public DateTime LocationChanged + { + get { return m_tParentGroupLastMod; } + set { m_tParentGroupLastMod = value; } + } + + /// + /// A flag that specifies if the group is shown as expanded or + /// collapsed in the user interface. + /// + public bool IsExpanded + { + get { return m_bIsExpanded; } + set { m_bIsExpanded = value; } + } + + /// + /// The date/time when this group was created. + /// + public DateTime CreationTime + { + get { return m_tCreation; } + set { m_tCreation = value; } + } + + /// + /// The date/time when this group was last modified. + /// + public DateTime LastModificationTime + { + get { return m_tLastMod; } + set { m_tLastMod = value; } + } + + /// + /// The date/time when this group was last accessed (read). + /// + public DateTime LastAccessTime + { + get { return m_tLastAccess; } + set { m_tLastAccess = value; } + } + + /// + /// The date/time when this group expires. + /// + public DateTime ExpiryTime + { + get { return m_tExpire; } + set { m_tExpire = value; } + } + + /// + /// Flag that determines if the group expires. + /// + public bool Expires + { + get { return m_bExpires; } + set { m_bExpires = value; } + } + + /// + /// Get or set the usage count of the group. To increase the usage + /// count by one, use the Touch function. + /// + public ulong UsageCount + { + get { return m_uUsageCount; } + set { m_uUsageCount = value; } + } + + /// + /// Get a list of subgroups in this group. + /// + public PwObjectList Groups + { + get { return m_listGroups; } + } + + /// + /// Get a list of entries in this group. + /// + public PwObjectList Entries + { + get { return m_listEntries; } + } + + /// + /// A flag specifying whether this group is virtual or not. Virtual + /// groups can contain links to entries stored in other groups. + /// Note that this flag has to be interpreted and set by the calling + /// code; it won't prevent you from accessing and modifying the list + /// of entries in this group in any way. + /// + public bool IsVirtual + { + get { return m_bVirtual; } + set { m_bVirtual = value; } + } + + /// + /// Default auto-type keystroke sequence for all entries in + /// this group. This property can be an empty string, which + /// means that the value should be inherited from the parent. + /// + public string DefaultAutoTypeSequence + { + get { return m_strDefaultAutoTypeSequence; } + set + { + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + m_strDefaultAutoTypeSequence = value; + } + } + + public bool? EnableAutoType + { + get { return m_bEnableAutoType; } + set { m_bEnableAutoType = value; } + } + + public bool? EnableSearching + { + get { return m_bEnableSearching; } + set { m_bEnableSearching = value; } + } + + public PwUuid LastTopVisibleEntry + { + get { return m_pwLastTopVisibleEntry; } + set + { + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + m_pwLastTopVisibleEntry = value; + } + } + + /// + /// Custom data container that can be used by plugins to store + /// own data in KeePass groups. + /// The data is stored in the encrypted part of encrypted + /// database files. + /// Use unique names for your items, e.g. "PluginName_ItemName". + /// + public StringDictionaryEx CustomData + { + get { return m_dCustomData; } + internal set + { + if(value == null) { Debug.Assert(false); throw new ArgumentNullException("value"); } + m_dCustomData = value; + } + } + + public static EventHandler GroupTouched; + public EventHandler Touched; + + /// + /// Construct a new, empty group. + /// + public PwGroup() + { + } + + /// + /// Construct a new, empty group. + /// + /// Create a new UUID for this group. + /// Set creation, last access and last modification times to the current time. + public PwGroup(bool bCreateNewUuid, bool bSetTimes) + { + if(bCreateNewUuid) m_uuid = new PwUuid(true); + + if(bSetTimes) + { + DateTime dtNow = DateTime.UtcNow; + m_tCreation = dtNow; + m_tLastMod = dtNow; + m_tLastAccess = dtNow; + m_tParentGroupLastMod = dtNow; + } + } + + /// + /// Construct a new group. + /// + /// Create a new UUID for this group. + /// Set creation, last access and last modification times to the current time. + /// Name of the new group. + /// Icon of the new group. + public PwGroup(bool bCreateNewUuid, bool bSetTimes, string strName, PwIcon pwIcon) + { + if(bCreateNewUuid) m_uuid = new PwUuid(true); + + if(bSetTimes) + { + DateTime dtNow = DateTime.UtcNow; + m_tCreation = dtNow; + m_tLastMod = dtNow; + m_tLastAccess = dtNow; + m_tParentGroupLastMod = dtNow; + } + + if(strName != null) m_strName = strName; + + m_pwIcon = pwIcon; + } + +#if DEBUG + // For display in debugger + public override string ToString() + { + return (@"PwGroup '" + m_strName + @"'"); + } +#endif + + /// + /// Deeply clone the current group. The returned group will be an exact + /// value copy of the current object (including UUID, etc.). + /// + /// Exact value copy of the current PwGroup object. + public PwGroup CloneDeep() + { + PwGroup pg = new PwGroup(false, false); + + pg.m_uuid = m_uuid; // PwUuid is immutable + + pg.m_listGroups = m_listGroups.CloneDeep(); + pg.m_listEntries = m_listEntries.CloneDeep(); + pg.m_pParentGroup = m_pParentGroup; + pg.m_tParentGroupLastMod = m_tParentGroupLastMod; + + pg.m_strName = m_strName; + pg.m_strNotes = m_strNotes; + + pg.m_pwIcon = m_pwIcon; + pg.m_pwCustomIconID = m_pwCustomIconID; + + pg.m_tCreation = m_tCreation; + pg.m_tLastMod = m_tLastMod; + pg.m_tLastAccess = m_tLastAccess; + pg.m_tExpire = m_tExpire; + pg.m_bExpires = m_bExpires; + pg.m_uUsageCount = m_uUsageCount; + + pg.m_bIsExpanded = m_bIsExpanded; + pg.m_bVirtual = m_bVirtual; + + pg.m_strDefaultAutoTypeSequence = m_strDefaultAutoTypeSequence; + + pg.m_bEnableAutoType = m_bEnableAutoType; + pg.m_bEnableSearching = m_bEnableSearching; + + pg.m_pwLastTopVisibleEntry = m_pwLastTopVisibleEntry; + + pg.m_dCustomData = m_dCustomData.CloneDeep(); + + return pg; + } + + public PwGroup CloneStructure() + { + PwGroup pg = new PwGroup(false, false); + + pg.m_uuid = m_uuid; // PwUuid is immutable + pg.m_tParentGroupLastMod = m_tParentGroupLastMod; + // Do not assign m_pParentGroup + + foreach(PwGroup pgSub in m_listGroups) + pg.AddGroup(pgSub.CloneStructure(), true); + + foreach(PwEntry peSub in m_listEntries) + pg.AddEntry(peSub.CloneStructure(), true); + + return pg; + } + + public bool EqualsGroup(PwGroup pg, PwCompareOptions pwOpt, + MemProtCmpMode mpCmpStr) + { + if(pg == null) { Debug.Assert(false); return false; } + + bool bIgnoreLastAccess = ((pwOpt & PwCompareOptions.IgnoreLastAccess) != + PwCompareOptions.None); + bool bIgnoreLastMod = ((pwOpt & PwCompareOptions.IgnoreLastMod) != + PwCompareOptions.None); + + if(!m_uuid.Equals(pg.m_uuid)) return false; + if((pwOpt & PwCompareOptions.IgnoreParentGroup) == PwCompareOptions.None) + { + if(m_pParentGroup != pg.m_pParentGroup) return false; + if(!bIgnoreLastMod && (m_tParentGroupLastMod != pg.m_tParentGroupLastMod)) + return false; + } + + if(m_strName != pg.m_strName) return false; + if(m_strNotes != pg.m_strNotes) return false; + + if(m_pwIcon != pg.m_pwIcon) return false; + if(!m_pwCustomIconID.Equals(pg.m_pwCustomIconID)) return false; + + if(m_tCreation != pg.m_tCreation) return false; + if(!bIgnoreLastMod && (m_tLastMod != pg.m_tLastMod)) return false; + if(!bIgnoreLastAccess && (m_tLastAccess != pg.m_tLastAccess)) return false; + if(m_tExpire != pg.m_tExpire) return false; + if(m_bExpires != pg.m_bExpires) return false; + if(!bIgnoreLastAccess && (m_uUsageCount != pg.m_uUsageCount)) return false; + + // if(m_bIsExpanded != pg.m_bIsExpanded) return false; + + if(m_strDefaultAutoTypeSequence != pg.m_strDefaultAutoTypeSequence) return false; + + if(m_bEnableAutoType.HasValue != pg.m_bEnableAutoType.HasValue) return false; + if(m_bEnableAutoType.HasValue) + { + if(m_bEnableAutoType.Value != pg.m_bEnableAutoType.Value) return false; + } + if(m_bEnableSearching.HasValue != pg.m_bEnableSearching.HasValue) return false; + if(m_bEnableSearching.HasValue) + { + if(m_bEnableSearching.Value != pg.m_bEnableSearching.Value) return false; + } + + if(!m_pwLastTopVisibleEntry.Equals(pg.m_pwLastTopVisibleEntry)) return false; + + if(!m_dCustomData.Equals(pg.m_dCustomData)) return false; + + if((pwOpt & PwCompareOptions.PropertiesOnly) == PwCompareOptions.None) + { + if(m_listEntries.UCount != pg.m_listEntries.UCount) return false; + for(uint u = 0; u < m_listEntries.UCount; ++u) + { + PwEntry peA = m_listEntries.GetAt(u); + PwEntry peB = pg.m_listEntries.GetAt(u); + if(!peA.EqualsEntry(peB, pwOpt, mpCmpStr)) return false; + } + + if(m_listGroups.UCount != pg.m_listGroups.UCount) return false; + for(uint u = 0; u < m_listGroups.UCount; ++u) + { + PwGroup pgA = m_listGroups.GetAt(u); + PwGroup pgB = pg.m_listGroups.GetAt(u); + if(!pgA.EqualsGroup(pgB, pwOpt, mpCmpStr)) return false; + } + } + + return true; + } + + /// + /// Assign properties to the current group based on a template group. + /// + /// Template group. Must not be null. + /// Only set the properties of the template group + /// if it is newer than the current one. + /// If true, the + /// LocationChanged property is copied, otherwise not. + public void AssignProperties(PwGroup pgTemplate, bool bOnlyIfNewer, + bool bAssignLocationChanged) + { + Debug.Assert(pgTemplate != null); if(pgTemplate == null) throw new ArgumentNullException("pgTemplate"); + + if(bOnlyIfNewer && (TimeUtil.Compare(pgTemplate.m_tLastMod, m_tLastMod, + true) < 0)) + return; + + // Template UUID should be the same as the current one + Debug.Assert(m_uuid.Equals(pgTemplate.m_uuid)); + m_uuid = pgTemplate.m_uuid; + + if(bAssignLocationChanged) + m_tParentGroupLastMod = pgTemplate.m_tParentGroupLastMod; + + m_strName = pgTemplate.m_strName; + m_strNotes = pgTemplate.m_strNotes; + + m_pwIcon = pgTemplate.m_pwIcon; + m_pwCustomIconID = pgTemplate.m_pwCustomIconID; + + m_tCreation = pgTemplate.m_tCreation; + m_tLastMod = pgTemplate.m_tLastMod; + m_tLastAccess = pgTemplate.m_tLastAccess; + m_tExpire = pgTemplate.m_tExpire; + m_bExpires = pgTemplate.m_bExpires; + m_uUsageCount = pgTemplate.m_uUsageCount; + + m_strDefaultAutoTypeSequence = pgTemplate.m_strDefaultAutoTypeSequence; + + m_bEnableAutoType = pgTemplate.m_bEnableAutoType; + m_bEnableSearching = pgTemplate.m_bEnableSearching; + + m_pwLastTopVisibleEntry = pgTemplate.m_pwLastTopVisibleEntry; + + m_dCustomData = pgTemplate.m_dCustomData.CloneDeep(); + } + + /// + /// Touch the group. This function updates the internal last access + /// time. If the parameter is true, + /// the last modification time gets updated, too. + /// + /// Modify last modification time. + public void Touch(bool bModified) + { + Touch(bModified, true); + } + + /// + /// Touch the group. This function updates the internal last access + /// time. If the parameter is true, + /// the last modification time gets updated, too. + /// + /// Modify last modification time. + /// If true, all parent objects + /// get touched, too. + public void Touch(bool bModified, bool bTouchParents) + { + m_tLastAccess = DateTime.UtcNow; + ++m_uUsageCount; + + if(bModified) m_tLastMod = m_tLastAccess; + + if(this.Touched != null) + this.Touched(this, new ObjectTouchedEventArgs(this, + bModified, bTouchParents)); + if(PwGroup.GroupTouched != null) + PwGroup.GroupTouched(this, new ObjectTouchedEventArgs(this, + bModified, bTouchParents)); + + if(bTouchParents && (m_pParentGroup != null)) + m_pParentGroup.Touch(bModified, true); + } + + /// + /// Get number of groups and entries in the current group. This function + /// can also traverse through all subgroups and accumulate their counts + /// (recursive mode). + /// + /// If this parameter is true, all + /// subgroups and entries in subgroups will be counted and added to + /// the returned value. If it is false, only the number of + /// subgroups and entries of the current group is returned. + /// Number of subgroups. + /// Number of entries. + public void GetCounts(bool bRecursive, out uint uNumGroups, out uint uNumEntries) + { + if(bRecursive) + { + uint uTotalGroups = m_listGroups.UCount; + uint uTotalEntries = m_listEntries.UCount; + uint uSubGroupCount, uSubEntryCount; + + foreach(PwGroup pg in m_listGroups) + { + pg.GetCounts(true, out uSubGroupCount, out uSubEntryCount); + + uTotalGroups += uSubGroupCount; + uTotalEntries += uSubEntryCount; + } + + uNumGroups = uTotalGroups; + uNumEntries = uTotalEntries; + } + else // !bRecursive + { + uNumGroups = m_listGroups.UCount; + uNumEntries = m_listEntries.UCount; + } + } + + public uint GetEntriesCount(bool bRecursive) + { + uint uGroups, uEntries; + GetCounts(bRecursive, out uGroups, out uEntries); + return uEntries; + } + + /// + /// Traverse the group/entry tree in the current group. Various traversal + /// methods are available. + /// + /// Specifies the traversal method. + /// Function that performs an action on + /// the currently visited group (see GroupHandler for more). + /// This parameter may be null, in this case the tree is traversed but + /// you don't get notifications for each visited group. + /// Function that performs an action on + /// the currently visited entry (see EntryHandler for more). + /// This parameter may be null. + /// Returns true if all entries and groups have been + /// traversed. If the traversal has been canceled by one of the two + /// handlers, the return value is false. + public bool TraverseTree(TraversalMethod tm, GroupHandler groupHandler, EntryHandler entryHandler) + { + bool bRet = false; + + switch(tm) + { + case TraversalMethod.None: + bRet = true; + break; + case TraversalMethod.PreOrder: + bRet = PreOrderTraverseTree(groupHandler, entryHandler); + break; + default: + Debug.Assert(false); + break; + } + + return bRet; + } + + private bool PreOrderTraverseTree(GroupHandler groupHandler, EntryHandler entryHandler) + { + if(entryHandler != null) + { + foreach(PwEntry pe in m_listEntries) + { + if(!entryHandler(pe)) return false; + } + } + + foreach(PwGroup pg in m_listGroups) + { + if(groupHandler != null) + { + if(!groupHandler(pg)) return false; + } + + if(!pg.PreOrderTraverseTree(groupHandler, entryHandler)) + return false; + } + + return true; + } + + /// + /// Pack all groups into one flat linked list of references (recursively). + /// + /// Flat list of all groups. + public LinkedList GetFlatGroupList() + { + LinkedList list = new LinkedList(); + + foreach(PwGroup pg in m_listGroups) + { + list.AddLast(pg); + + if(pg.Groups.UCount != 0) + LinearizeGroupRecursive(list, pg, 1); + } + + return list; + } + + private void LinearizeGroupRecursive(LinkedList list, PwGroup pg, ushort uLevel) + { + Debug.Assert(pg != null); if(pg == null) return; + + foreach(PwGroup pwg in pg.Groups) + { + list.AddLast(pwg); + + if(pwg.Groups.UCount != 0) + LinearizeGroupRecursive(list, pwg, (ushort)(uLevel + 1)); + } + } + + /// + /// Pack all entries into one flat linked list of references. Temporary + /// group IDs are assigned automatically. + /// + /// A flat group list created by + /// GetFlatGroupList. + /// Flat list of all entries. + public static LinkedList GetFlatEntryList(LinkedList flatGroupList) + { + Debug.Assert(flatGroupList != null); if(flatGroupList == null) return null; + + LinkedList list = new LinkedList(); + foreach(PwGroup pg in flatGroupList) + { + foreach(PwEntry pe in pg.Entries) + list.AddLast(pe); + } + + return list; + } + + /// + /// Enable protection of a specific string field type. + /// + /// Name of the string field to protect or unprotect. + /// Enable protection or not. + /// Returns true, if the operation completed successfully, + /// otherwise false. + public bool EnableStringFieldProtection(string strFieldName, bool bEnable) + { + Debug.Assert(strFieldName != null); + + EntryHandler eh = delegate(PwEntry pe) + { + // Enable protection of current string + pe.Strings.EnableProtection(strFieldName, bEnable); + + // Do the same for all history items + foreach(PwEntry peHistory in pe.History) + { + peHistory.Strings.EnableProtection(strFieldName, bEnable); + } + + return true; + }; + + return PreOrderTraverseTree(null, eh); + } + + /// + /// Search this group and all subgroups for entries. + /// + /// Specifies the search parameters. + /// Entry list in which the search results + /// will be stored. + public void SearchEntries(SearchParameters sp, PwObjectList lResults) + { + SearchEntries(sp, lResults, null); + } + + /// + /// Search this group and all subgroups for entries. + /// + /// Specifies the search parameters. + /// Entry list in which the search results + /// will be stored. + /// Optional status reporting object. + public void SearchEntries(SearchParameters sp, PwObjectList lResults, + IStatusLogger slStatus) + { + if(sp == null) { Debug.Assert(false); return; } + if(lResults == null) { Debug.Assert(false); return; } + + PwObjectList lCand = GetEntries(true); + DateTime dtNow = DateTime.UtcNow; + + PwObjectList l = new PwObjectList(); + foreach(PwEntry pe in lCand) + { + if(sp.RespectEntrySearchingDisabled && !pe.GetSearchingEnabled()) + continue; + if(sp.ExcludeExpired && pe.Expires && (pe.ExpiryTime <= dtNow)) + continue; + + l.Add(pe); + } + lCand = l; + + List lTerms; + if(sp.RegularExpression) + { + lTerms = new List(); + lTerms.Add((sp.SearchString ?? string.Empty).Trim()); + } + else lTerms = StrUtil.SplitSearchTerms(sp.SearchString); + + // Search longer strings first (for improved performance) + lTerms.Sort(StrUtil.CompareLengthGt); + + ulong uPrcEntries = 0, uTotalEntries = lCand.UCount; + SearchParameters spSub = sp.Clone(); + + for(int iTerm = 0; iTerm < lTerms.Count; ++iTerm) + { + // Update counters for a better state guess + if(slStatus != null) + { + ulong uRemRounds = (ulong)(lTerms.Count - iTerm); + uTotalEntries = uPrcEntries + (uRemRounds * + lCand.UCount); + } + + spSub.SearchString = lTerms[iTerm]; // No trim + // spSub.RespectEntrySearchingDisabled = false; // Ignored by sub + // spSub.ExcludeExpired = false; // Ignored by sub + + bool bNegate = false; + if(spSub.SearchString.StartsWith(@"-") && + (spSub.SearchString.Length >= 2)) + { + spSub.SearchString = spSub.SearchString.Substring(1); + bNegate = true; + } + + l = new PwObjectList(); + if(!SearchEntriesSingle(lCand, spSub, l, slStatus, + ref uPrcEntries, uTotalEntries)) + { + lCand.Clear(); + break; + } + + if(bNegate) + { + PwObjectList lRem = new PwObjectList(); + foreach(PwEntry pe in lCand) + { + if(l.IndexOf(pe) < 0) lRem.Add(pe); + } + + lCand = lRem; + } + else lCand = l; + } + + Debug.Assert(lResults.UCount == 0); + lResults.Clear(); + lResults.Add(lCand); + } + + private static bool SearchEntriesSingle(PwObjectList lSource, + SearchParameters sp, PwObjectList lResults, + IStatusLogger slStatus, ref ulong uPrcEntries, ulong uTotalEntries) + { + if(lSource == null) { Debug.Assert(false); return true; } + if(sp == null) { Debug.Assert(false); return true; } + if(lResults == null) { Debug.Assert(false); return true; } + Debug.Assert(lResults.UCount == 0); + + bool bTitle = sp.SearchInTitles; + bool bUserName = sp.SearchInUserNames; + bool bPassword = sp.SearchInPasswords; + bool bUrl = sp.SearchInUrls; + bool bNotes = sp.SearchInNotes; + bool bOther = sp.SearchInOther; + bool bStringName = sp.SearchInStringNames; + bool bTags = sp.SearchInTags; + bool bUuids = sp.SearchInUuids; + bool bGroupPath = sp.SearchInGroupPaths; + bool bGroupName = sp.SearchInGroupNames; + // bool bExcludeExpired = sp.ExcludeExpired; + // bool bRespectEntrySearchingDisabled = sp.RespectEntrySearchingDisabled; + + if(bGroupPath) bGroupName = false; // Name is included in path + + Regex rx = null; + if(sp.RegularExpression) + { + RegexOptions ro = RegexOptions.None; // RegexOptions.Compiled + if((sp.ComparisonMode == StringComparison.CurrentCultureIgnoreCase) || +#if !ModernKeePassLib && !KeePassRT + (sp.ComparisonMode == StringComparison.InvariantCultureIgnoreCase) || +#endif + (sp.ComparisonMode == StringComparison.OrdinalIgnoreCase)) + { + ro |= RegexOptions.IgnoreCase; + } + + rx = new Regex(sp.SearchString, ro); + } + + ulong uLocalPrcEntries = uPrcEntries; + + if(sp.SearchString.Length == 0) lResults.Add(lSource); + else + { + foreach(PwEntry pe in lSource) + { + if(slStatus != null) + { + if(!slStatus.SetProgress((uint)((uLocalPrcEntries * + 100UL) / uTotalEntries))) return false; + ++uLocalPrcEntries; + } + + // if(bRespectEntrySearchingDisabled && !pe.GetSearchingEnabled()) + // continue; + // if(bExcludeExpired && pe.Expires && (pe.ExpiryTime <= dtNow)) + // continue; + + uint uInitialResults = lResults.UCount; + + foreach(KeyValuePair kvp in pe.Strings) + { + string strKey = kvp.Key; + + if(strKey == PwDefs.TitleField) + { + if(bTitle) SearchEvalAdd(sp, kvp.Value.ReadString(), + rx, pe, lResults); + } + else if(strKey == PwDefs.UserNameField) + { + if(bUserName) SearchEvalAdd(sp, kvp.Value.ReadString(), + rx, pe, lResults); + } + else if(strKey == PwDefs.PasswordField) + { + if(bPassword) SearchEvalAdd(sp, kvp.Value.ReadString(), + rx, pe, lResults); + } + else if(strKey == PwDefs.UrlField) + { + if(bUrl) SearchEvalAdd(sp, kvp.Value.ReadString(), + rx, pe, lResults); + } + else if(strKey == PwDefs.NotesField) + { + if(bNotes) SearchEvalAdd(sp, kvp.Value.ReadString(), + rx, pe, lResults); + } + else if(bOther) + SearchEvalAdd(sp, kvp.Value.ReadString(), + rx, pe, lResults); + + // An entry can match only once => break if we have added it + if(lResults.UCount != uInitialResults) break; + } + + if(bStringName) + { + foreach(KeyValuePair kvp in pe.Strings) + { + if(lResults.UCount != uInitialResults) break; + + SearchEvalAdd(sp, kvp.Key, rx, pe, lResults); + } + } + + if(bTags) + { + foreach(string strTag in pe.Tags) + { + if(lResults.UCount != uInitialResults) break; + + SearchEvalAdd(sp, strTag, rx, pe, lResults); + } + } + + if(bUuids && (lResults.UCount == uInitialResults)) + SearchEvalAdd(sp, pe.Uuid.ToHexString(), rx, pe, lResults); + + if(bGroupPath && (lResults.UCount == uInitialResults) && + (pe.ParentGroup != null)) + SearchEvalAdd(sp, pe.ParentGroup.GetFullPath("\n", true), + rx, pe, lResults); + + if(bGroupName && (lResults.UCount == uInitialResults) && + (pe.ParentGroup != null)) + SearchEvalAdd(sp, pe.ParentGroup.Name, rx, pe, lResults); + } + } + + uPrcEntries = uLocalPrcEntries; + return true; + } + + private static void SearchEvalAdd(SearchParameters sp, string strData, + Regex rx, PwEntry pe, PwObjectList lResults) + { + if(sp == null) { Debug.Assert(false); return; } + if(strData == null) { Debug.Assert(false); return; } + if(pe == null) { Debug.Assert(false); return; } + if(lResults == null) { Debug.Assert(false); return; } + + bool bMatch; + if(rx == null) + bMatch = (strData.IndexOf(sp.SearchString, + sp.ComparisonMode) >= 0); + else bMatch = rx.IsMatch(strData); + + if(!bMatch && (sp.DataTransformationFn != null)) + { + string strCmp = sp.DataTransformationFn(strData, pe); + if(!object.ReferenceEquals(strCmp, strData)) + { + if(rx == null) + bMatch = (strCmp.IndexOf(sp.SearchString, + sp.ComparisonMode) >= 0); + else bMatch = rx.IsMatch(strCmp); + } + } + + if(bMatch) lResults.Add(pe); + } + + public List BuildEntryTagsList() + { + return BuildEntryTagsList(false); + } + + public List BuildEntryTagsList(bool bSort) + { + List vTags = new List(); + + EntryHandler eh = delegate(PwEntry pe) + { + foreach(string strTag in pe.Tags) + { + bool bFound = false; + for(int i = 0; i < vTags.Count; ++i) + { + if(vTags[i].Equals(strTag, StrUtil.CaseIgnoreCmp)) + { + bFound = true; + break; + } + } + + if(!bFound) vTags.Add(strTag); + } + + return true; + }; + + TraverseTree(TraversalMethod.PreOrder, null, eh); + if(bSort) vTags.Sort(StrUtil.CaseIgnoreComparer); + return vTags; + } + +#if !KeePassLibSD + public IDictionary BuildEntryTagsDict(bool bSort) + { + IDictionary d; + if(!bSort) d = new Dictionary(StrUtil.CaseIgnoreComparer); + else d = new SortedDictionary(StrUtil.CaseIgnoreComparer); + + EntryHandler eh = delegate(PwEntry pe) + { + foreach(string strTag in pe.Tags) + { + uint u; + if(d.TryGetValue(strTag, out u)) d[strTag] = u + 1; + else d[strTag] = 1; + } + + return true; + }; + + TraverseTree(TraversalMethod.PreOrder, null, eh); + return d; + } +#endif + + public void FindEntriesByTag(string strTag, PwObjectList listStorage, + bool bSearchRecursive) + { + if(strTag == null) throw new ArgumentNullException("strTag"); + if(strTag.Length == 0) return; + + foreach(PwEntry pe in m_listEntries) + { + foreach(string strEntryTag in pe.Tags) + { + if(strEntryTag.Equals(strTag, StrUtil.CaseIgnoreCmp)) + { + listStorage.Add(pe); + break; + } + } + } + + if(bSearchRecursive) + { + foreach(PwGroup pg in m_listGroups) + pg.FindEntriesByTag(strTag, listStorage, true); + } + } + + /// + /// Find a group. + /// + /// UUID identifying the group the caller is looking for. + /// If true, the search is recursive. + /// Returns reference to found group, otherwise null. + public PwGroup FindGroup(PwUuid uuid, bool bSearchRecursive) + { + // Do not assert on PwUuid.Zero + if(m_uuid.Equals(uuid)) return this; + + if(bSearchRecursive) + { + PwGroup pgRec; + foreach(PwGroup pg in m_listGroups) + { + pgRec = pg.FindGroup(uuid, true); + if(pgRec != null) return pgRec; + } + } + else // Not recursive + { + foreach(PwGroup pg in m_listGroups) + { + if(pg.m_uuid.Equals(uuid)) + return pg; + } + } + + return null; + } + + /// + /// Find an object. + /// + /// UUID of the object to find. + /// Specifies whether to search recursively. + /// If null, groups and entries are + /// searched. If true, only entries are searched. If false, + /// only groups are searched. + /// Reference to the object, if found. Otherwise null. + public IStructureItem FindObject(PwUuid uuid, bool bRecursive, + bool? bEntries) + { + if(bEntries.HasValue) + { + if(bEntries.Value) return FindEntry(uuid, bRecursive); + else return FindGroup(uuid, bRecursive); + } + + PwGroup pg = FindGroup(uuid, bRecursive); + if(pg != null) return pg; + return FindEntry(uuid, bRecursive); + } + + /// + /// Try to find a subgroup and create it, if it doesn't exist yet. + /// + /// Name of the subgroup. + /// If the group isn't found: create it. + /// Returns a reference to the requested group or null if + /// it doesn't exist and shouldn't be created. + public PwGroup FindCreateGroup(string strName, bool bCreateIfNotFound) + { + Debug.Assert(strName != null); if(strName == null) throw new ArgumentNullException("strName"); + + foreach(PwGroup pg in m_listGroups) + { + if(pg.Name == strName) return pg; + } + + if(!bCreateIfNotFound) return null; + + PwGroup pgNew = new PwGroup(true, true, strName, PwIcon.Folder); + AddGroup(pgNew, true); + return pgNew; + } + + /// + /// Find an entry. + /// + /// UUID identifying the entry the caller is looking for. + /// If true, the search is recursive. + /// Returns reference to found entry, otherwise null. + public PwEntry FindEntry(PwUuid uuid, bool bSearchRecursive) + { + foreach(PwEntry pe in m_listEntries) + { + if(pe.Uuid.Equals(uuid)) return pe; + } + + if(bSearchRecursive) + { + PwEntry peSub; + foreach(PwGroup pg in m_listGroups) + { + peSub = pg.FindEntry(uuid, true); + if(peSub != null) return peSub; + } + } + + return null; + } + + /// + /// Get the full path of a group. + /// + /// Full path of the group. + public string GetFullPath() + { + return GetFullPath(".", false); + } + + /// + /// Get the full path of a group. + /// + /// String that separates the group + /// names. + /// Specifies whether the returned + /// path starts with the topmost group. + /// Full path of the group. + public string GetFullPath(string strSeparator, bool bIncludeTopMostGroup) + { + Debug.Assert(strSeparator != null); + if(strSeparator == null) throw new ArgumentNullException("strSeparator"); + + string strPath = m_strName; + + PwGroup pg = m_pParentGroup; + while(pg != null) + { + if((!bIncludeTopMostGroup) && (pg.m_pParentGroup == null)) + break; + + strPath = pg.Name + strSeparator + strPath; + + pg = pg.m_pParentGroup; + } + + return strPath; + } + + /// + /// Assign new UUIDs to groups and entries. + /// + /// Create new UUIDs for subgroups. + /// Create new UUIDs for entries. + /// Recursive tree traversal. + public void CreateNewItemUuids(bool bNewGroups, bool bNewEntries, bool bRecursive) + { + if(bNewGroups) + { + foreach(PwGroup pg in m_listGroups) + pg.Uuid = new PwUuid(true); + } + + if(bNewEntries) + { + foreach(PwEntry pe in m_listEntries) + pe.SetUuid(new PwUuid(true), true); + } + + if(bRecursive) + { + foreach(PwGroup pg in m_listGroups) + pg.CreateNewItemUuids(bNewGroups, bNewEntries, true); + } + } + + public void TakeOwnership(bool bTakeSubGroups, bool bTakeEntries, bool bRecursive) + { + if(bTakeSubGroups) + { + foreach(PwGroup pg in m_listGroups) + pg.ParentGroup = this; + } + + if(bTakeEntries) + { + foreach(PwEntry pe in m_listEntries) + pe.ParentGroup = this; + } + + if(bRecursive) + { + foreach(PwGroup pg in m_listGroups) + pg.TakeOwnership(bTakeSubGroups, bTakeEntries, true); + } + } + +#if !KeePassLibSD + /// + /// Find/create a subtree of groups. + /// + /// Tree string. + /// Separators that delimit groups in the + /// strTree parameter. + public PwGroup FindCreateSubTree(string strTree, char[] vSeparators) + { + return FindCreateSubTree(strTree, vSeparators, true); + } + + public PwGroup FindCreateSubTree(string strTree, char[] vSeparators, + bool bAllowCreate) + { + if(vSeparators == null) { Debug.Assert(false); vSeparators = new char[0]; } + + string[] v = new string[vSeparators.Length]; + for(int i = 0; i < vSeparators.Length; ++i) + v[i] = new string(vSeparators[i], 1); + + return FindCreateSubTree(strTree, v, bAllowCreate); + } + + public PwGroup FindCreateSubTree(string strTree, string[] vSeparators, + bool bAllowCreate) + { + Debug.Assert(strTree != null); if(strTree == null) return this; + if(strTree.Length == 0) return this; + + string[] vGroups = strTree.Split(vSeparators, StringSplitOptions.None); + if((vGroups == null) || (vGroups.Length == 0)) return this; + + PwGroup pgContainer = this; + for(int nGroup = 0; nGroup < vGroups.Length; ++nGroup) + { + if(string.IsNullOrEmpty(vGroups[nGroup])) continue; + + bool bFound = false; + foreach(PwGroup pg in pgContainer.Groups) + { + if(pg.Name == vGroups[nGroup]) + { + pgContainer = pg; + bFound = true; + break; + } + } + + if(!bFound) + { + if(!bAllowCreate) return null; + + PwGroup pg = new PwGroup(true, true, vGroups[nGroup], PwIcon.Folder); + pgContainer.AddGroup(pg, true); + pgContainer = pg; + } + } + + return pgContainer; + } +#endif + + /// + /// Get the level of the group (i.e. the number of parent groups). + /// + /// Number of parent groups. + public uint GetLevel() + { + PwGroup pg = m_pParentGroup; + uint uLevel = 0; + + while(pg != null) + { + pg = pg.ParentGroup; + ++uLevel; + } + + return uLevel; + } + + public string GetAutoTypeSequenceInherited() + { + if(m_strDefaultAutoTypeSequence.Length > 0) + return m_strDefaultAutoTypeSequence; + + if(m_pParentGroup != null) + return m_pParentGroup.GetAutoTypeSequenceInherited(); + + return string.Empty; + } + + public bool GetAutoTypeEnabledInherited() + { + if(m_bEnableAutoType.HasValue) return m_bEnableAutoType.Value; + + if(m_pParentGroup != null) + return m_pParentGroup.GetAutoTypeEnabledInherited(); + + return DefaultAutoTypeEnabled; + } + + public bool GetSearchingEnabledInherited() + { + if(m_bEnableSearching.HasValue) return m_bEnableSearching.Value; + + if(m_pParentGroup != null) + return m_pParentGroup.GetSearchingEnabledInherited(); + + return DefaultSearchingEnabled; + } + + /// + /// Get a list of subgroups (not including this one). + /// + /// If true, subgroups are added + /// recursively, i.e. all child groups are returned, too. + /// List of subgroups. If is + /// true, it is guaranteed that subsubgroups appear after + /// subgroups. + public PwObjectList GetGroups(bool bRecursive) + { + if(bRecursive == false) return m_listGroups; + + PwObjectList list = m_listGroups.CloneShallow(); + foreach(PwGroup pgSub in m_listGroups) + { + list.Add(pgSub.GetGroups(true)); + } + + return list; + } + + public PwObjectList GetEntries(bool bIncludeSubGroupEntries) + { + PwObjectList l = new PwObjectList(); + + GroupHandler gh = delegate(PwGroup pg) + { + l.Add(pg.Entries); + return true; + }; + + gh(this); + if(bIncludeSubGroupEntries) + PreOrderTraverseTree(gh, null); + + Debug.Assert(l.UCount == GetEntriesCount(bIncludeSubGroupEntries)); + return l; + } + + /// + /// Get objects contained in this group. + /// + /// Specifies whether to search recursively. + /// If null, the returned list contains + /// groups and entries. If true, the returned list contains only + /// entries. If false, the returned list contains only groups. + /// List of objects. + public List GetObjects(bool bRecursive, bool? bEntries) + { + List list = new List(); + + if(!bEntries.HasValue || !bEntries.Value) + { + PwObjectList lGroups = GetGroups(bRecursive); + foreach(PwGroup pg in lGroups) list.Add(pg); + } + + if(!bEntries.HasValue || bEntries.Value) + { + PwObjectList lEntries = GetEntries(bRecursive); + foreach(PwEntry pe in lEntries) list.Add(pe); + } + + return list; + } + + public bool IsContainedIn(PwGroup pgContainer) + { + PwGroup pgCur = m_pParentGroup; + while(pgCur != null) + { + if(pgCur == pgContainer) return true; + + pgCur = pgCur.m_pParentGroup; + } + + return false; + } + + /// + /// Add a subgroup to this group. + /// + /// Group to be added. Must not be null. + /// If this parameter is true, the + /// parent group reference of the subgroup will be set to the current + /// group (i.e. the current group takes ownership of the subgroup). + public void AddGroup(PwGroup subGroup, bool bTakeOwnership) + { + AddGroup(subGroup, bTakeOwnership, false); + } + + /// + /// Add a subgroup to this group. + /// + /// Group to be added. Must not be null. + /// If this parameter is true, the + /// parent group reference of the subgroup will be set to the current + /// group (i.e. the current group takes ownership of the subgroup). + /// If true, the + /// LocationChanged property of the subgroup is updated. + public void AddGroup(PwGroup subGroup, bool bTakeOwnership, + bool bUpdateLocationChangedOfSub) + { + if(subGroup == null) throw new ArgumentNullException("subGroup"); + + m_listGroups.Add(subGroup); + + if(bTakeOwnership) subGroup.m_pParentGroup = this; + + if(bUpdateLocationChangedOfSub) subGroup.LocationChanged = DateTime.UtcNow; + } + + /// + /// Add an entry to this group. + /// + /// Entry to be added. Must not be null. + /// If this parameter is true, the + /// parent group reference of the entry will be set to the current + /// group (i.e. the current group takes ownership of the entry). + public void AddEntry(PwEntry pe, bool bTakeOwnership) + { + AddEntry(pe, bTakeOwnership, false); + } + + /// + /// Add an entry to this group. + /// + /// Entry to be added. Must not be null. + /// If this parameter is true, the + /// parent group reference of the entry will be set to the current + /// group (i.e. the current group takes ownership of the entry). + /// If true, the + /// LocationChanged property of the entry is updated. + public void AddEntry(PwEntry pe, bool bTakeOwnership, + bool bUpdateLocationChangedOfEntry) + { + if(pe == null) throw new ArgumentNullException("pe"); + + m_listEntries.Add(pe); + + // Do not remove the entry from its previous parent group, + // only assign it to the new one + if(bTakeOwnership) pe.ParentGroup = this; + + if(bUpdateLocationChangedOfEntry) pe.LocationChanged = DateTime.UtcNow; + } + + public void SortSubGroups(bool bRecursive) + { + m_listGroups.Sort(new PwGroupComparer()); + + if(bRecursive) + { + foreach(PwGroup pgSub in m_listGroups) + pgSub.SortSubGroups(true); + } + } + + public void DeleteAllObjects(PwDatabase pdContext) + { + DateTime dtNow = DateTime.UtcNow; + + foreach(PwEntry pe in m_listEntries) + { + PwDeletedObject pdo = new PwDeletedObject(pe.Uuid, dtNow); + pdContext.DeletedObjects.Add(pdo); + } + m_listEntries.Clear(); + + foreach(PwGroup pg in m_listGroups) + { + pg.DeleteAllObjects(pdContext); + + PwDeletedObject pdo = new PwDeletedObject(pg.Uuid, dtNow); + pdContext.DeletedObjects.Add(pdo); + } + m_listGroups.Clear(); + } + + internal List GetTopSearchSkippedGroups() + { + List l = new List(); + + if(!GetSearchingEnabledInherited()) l.Add(this); + else GetTopSearchSkippedGroupsRec(l); + + return l; + } + + private void GetTopSearchSkippedGroupsRec(List l) + { + if(m_bEnableSearching.HasValue && !m_bEnableSearching.Value) + { + l.Add(this); + return; + } + else { Debug.Assert(GetSearchingEnabledInherited()); } + + foreach(PwGroup pgSub in m_listGroups) + pgSub.GetTopSearchSkippedGroupsRec(l); + } + + public void SetCreatedNow(bool bRecursive) + { + DateTime dt = DateTime.UtcNow; + + m_tCreation = dt; + m_tLastAccess = dt; + + if(!bRecursive) return; + + GroupHandler gh = delegate(PwGroup pg) + { + pg.m_tCreation = dt; + pg.m_tLastAccess = dt; + return true; + }; + + EntryHandler eh = delegate(PwEntry pe) + { + pe.CreationTime = dt; + pe.LastAccessTime = dt; + return true; + }; + + TraverseTree(TraversalMethod.PreOrder, gh, eh); + } + + public PwGroup Duplicate() + { + PwGroup pg = CloneDeep(); + + pg.Uuid = new PwUuid(true); + pg.CreateNewItemUuids(true, true, true); + + pg.SetCreatedNow(true); + + pg.TakeOwnership(true, true, true); + + return pg; + } + + internal string[] CollectEntryStrings(GFunc f, bool bSort) + { + if(f == null) { Debug.Assert(false); return new string[0]; } + + Dictionary d = new Dictionary(); + + EntryHandler eh = delegate(PwEntry pe) + { + string str = f(pe); + if(str != null) d[str] = true; + + return true; + }; + TraverseTree(TraversalMethod.PreOrder, null, eh); + + string[] v = new string[d.Count]; + if(d.Count != 0) + { + d.Keys.CopyTo(v, 0); + if(bSort) Array.Sort(v, StrUtil.CaseIgnoreComparer); + } + + return v; + } + + internal string[] GetAutoTypeSequences(bool bWithStd) + { + try + { + Dictionary d = new Dictionary(); + + GAction fAdd = delegate(string str) + { + if(!string.IsNullOrEmpty(str)) d[str] = true; + }; + + if(bWithStd) + { + fAdd(PwDefs.DefaultAutoTypeSequence); + fAdd(PwDefs.DefaultAutoTypeSequenceTan); + } + + GroupHandler gh = delegate(PwGroup pg) + { + fAdd(pg.DefaultAutoTypeSequence); + return true; + }; + + EntryHandler eh = delegate(PwEntry pe) + { + AutoTypeConfig c = pe.AutoType; + + fAdd(c.DefaultSequence); + foreach(AutoTypeAssociation a in c.Associations) + { + fAdd(a.Sequence); + } + + return true; + }; + + gh(this); + TraverseTree(TraversalMethod.PreOrder, gh, eh); + + string[] v = new string[d.Count]; + if(d.Count != 0) + { + d.Keys.CopyTo(v, 0); + Array.Sort(v, StrUtil.CaseIgnoreComparer); + } + + return v; + } + catch(Exception) { Debug.Assert(false); } + + return new string[0]; + } + } + + public sealed class PwGroupComparer : IComparer + { + public PwGroupComparer() + { + } + + public int Compare(PwGroup a, PwGroup b) + { + return StrUtil.CompareNaturally(a.Name, b.Name); + } + } +} diff --git a/ModernKeePassLib/PwUuid.cs b/ModernKeePassLib/PwUuid.cs new file mode 100644 index 0000000..5e020d3 --- /dev/null +++ b/ModernKeePassLib/PwUuid.cs @@ -0,0 +1,215 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Xml; +using System.Diagnostics; + +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib +{ + // [ImmutableObject(true)] + /// + /// Represents an UUID of a password entry or group. Once created, + /// PwUuid objects aren't modifyable anymore (immutable). + /// + public sealed class PwUuid : IComparable, IEquatable + { + /// + /// Standard size in bytes of a UUID. + /// + public const uint UuidSize = 16; + + /// + /// Zero UUID (all bytes are zero). + /// + public static readonly PwUuid Zero = new PwUuid(false); + + private byte[] m_pbUuid = null; // Never null after constructor + + /// + /// Get the 16 UUID bytes. + /// + public byte[] UuidBytes + { + get { return m_pbUuid; } + } + + /// + /// Construct a new UUID object. + /// + /// If this parameter is true, a new + /// UUID is generated. If it is false, the UUID is initialized + /// to zero. + public PwUuid(bool bCreateNew) + { + if(bCreateNew) CreateNew(); + else SetZero(); + } + + /// + /// Construct a new UUID object. + /// + /// Initial value of the PwUuid object. + public PwUuid(byte[] uuidBytes) + { + SetValue(uuidBytes); + } + + /// + /// Create a new, random UUID. + /// + /// Returns true if a random UUID has been generated, + /// otherwise it returns false. + private void CreateNew() + { + Debug.Assert(m_pbUuid == null); // Only call from constructor + while(true) + { + m_pbUuid = Guid.NewGuid().ToByteArray(); + + if((m_pbUuid == null) || (m_pbUuid.Length != (int)UuidSize)) + { + Debug.Assert(false); + throw new InvalidOperationException(); + } + + // Zero is a reserved value -- do not generate Zero + if(!Equals(PwUuid.Zero)) break; + Debug.Assert(false); + } + } + + private void SetValue(byte[] uuidBytes) + { + Debug.Assert((uuidBytes != null) && (uuidBytes.Length == (int)UuidSize)); + if(uuidBytes == null) throw new ArgumentNullException("uuidBytes"); + if(uuidBytes.Length != (int)UuidSize) throw new ArgumentException(); + + Debug.Assert(m_pbUuid == null); // Only call from constructor + m_pbUuid = new byte[UuidSize]; + + Array.Copy(uuidBytes, m_pbUuid, (int)UuidSize); + } + + private void SetZero() + { + Debug.Assert(m_pbUuid == null); // Only call from constructor + m_pbUuid = new byte[UuidSize]; + + // Array.Clear(m_pbUuid, 0, (int)UuidSize); +#if DEBUG + List l = new List(m_pbUuid); + Debug.Assert(l.TrueForAll(bt => (bt == 0))); +#endif + } + + [Obsolete] + public bool EqualsValue(PwUuid uuid) + { + return Equals(uuid); + } + + public override bool Equals(object obj) + { + return Equals(obj as PwUuid); + } + + public bool Equals(PwUuid other) + { + if(other == null) { Debug.Assert(false); return false; } + + for(int i = 0; i < (int)UuidSize; ++i) + { + if(m_pbUuid[i] != other.m_pbUuid[i]) return false; + } + + return true; + } + + private int m_h = 0; + public override int GetHashCode() + { + if(m_h == 0) + m_h = (int)MemUtil.Hash32(m_pbUuid, 0, m_pbUuid.Length); + return m_h; + } + + public int CompareTo(PwUuid other) + { + if(other == null) + { + Debug.Assert(false); + throw new ArgumentNullException("other"); + } + + for(int i = 0; i < (int)UuidSize; ++i) + { + if(m_pbUuid[i] < other.m_pbUuid[i]) return -1; + if(m_pbUuid[i] > other.m_pbUuid[i]) return 1; + } + + return 0; + } + + /// + /// Convert the UUID to its string representation. + /// + /// String containing the UUID value. + public string ToHexString() + { + return MemUtil.ByteArrayToHexString(m_pbUuid); + } + +#if DEBUG + public override string ToString() + { + return ToHexString(); + } +#endif + } + + [Obsolete] + public sealed class PwUuidComparable : IComparable + { + private byte[] m_pbUuid = new byte[PwUuid.UuidSize]; + + public PwUuidComparable(PwUuid pwUuid) + { + if(pwUuid == null) throw new ArgumentNullException("pwUuid"); + + Array.Copy(pwUuid.UuidBytes, m_pbUuid, (int)PwUuid.UuidSize); + } + + public int CompareTo(PwUuidComparable other) + { + if(other == null) throw new ArgumentNullException("other"); + + for(int i = 0; i < (int)PwUuid.UuidSize; ++i) + { + if(m_pbUuid[i] < other.m_pbUuid[i]) return -1; + if(m_pbUuid[i] > other.m_pbUuid[i]) return 1; + } + + return 0; + } + } +} diff --git a/ModernKeePassLib/README.md b/ModernKeePassLib/README.md new file mode 100644 index 0000000..74bb65a --- /dev/null +++ b/ModernKeePassLib/README.md @@ -0,0 +1,13 @@ +# ModernKeePassLib + +This is my adaptation of the KeePassLib (KeePass library) for the Universal Windows Platform and Windows Runtime (WinRT). +It aims at introducing as little change as possible to the original library: overall, except for namespace changes and some added classes (see below), there is almost no change. + +Download the Nuget package [here](https://www.nuget.org/packages/ModernKeePassLib) + +# Features +- Custom implementation of the System.Security.Cryptography.HashAlgoritm class by using WinRT equivalents +- Use of BouncyCastle PCL to implement AES key derivation features +- Lots of small changes in .NET methods (UTF8 instead of ASCII, string.) +- Disabled native functions (because not compatible with WinRT) +- Use of Splat for GfxUtil \ No newline at end of file diff --git a/ModernKeePassLib/Resources/KLRes.Generated.cs b/ModernKeePassLib/Resources/KLRes.Generated.cs new file mode 100644 index 0000000..007d875 --- /dev/null +++ b/ModernKeePassLib/Resources/KLRes.Generated.cs @@ -0,0 +1,546 @@ +// This is a generated file! +// Do not edit manually, changes will be overwritten. + +using System; +using System.Collections.Generic; + +namespace ModernKeePassLib.Resources +{ + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + public static class KLRes + { + private static string TryGetEx(Dictionary dictNew, + string strName, string strDefault) + { + string strTemp; + + if(dictNew.TryGetValue(strName, out strTemp)) + return strTemp; + + return strDefault; + } + + public static void SetTranslatedStrings(Dictionary dictNew) + { + if(dictNew == null) throw new ArgumentNullException("dictNew"); + + m_strCryptoStreamFailed = TryGetEx(dictNew, "CryptoStreamFailed", m_strCryptoStreamFailed); + m_strEncDataTooLarge = TryGetEx(dictNew, "EncDataTooLarge", m_strEncDataTooLarge); + m_strErrorInClipboard = TryGetEx(dictNew, "ErrorInClipboard", m_strErrorInClipboard); + m_strExpect100Continue = TryGetEx(dictNew, "Expect100Continue", m_strExpect100Continue); + m_strFatalError = TryGetEx(dictNew, "FatalError", m_strFatalError); + m_strFatalErrorText = TryGetEx(dictNew, "FatalErrorText", m_strFatalErrorText); + m_strFileCorrupted = TryGetEx(dictNew, "FileCorrupted", m_strFileCorrupted); + m_strFileHeaderCorrupted = TryGetEx(dictNew, "FileHeaderCorrupted", m_strFileHeaderCorrupted); + m_strFileIncomplete = TryGetEx(dictNew, "FileIncomplete", m_strFileIncomplete); + m_strFileIncompleteExpc = TryGetEx(dictNew, "FileIncompleteExpc", m_strFileIncompleteExpc); + m_strFileLoadFailed = TryGetEx(dictNew, "FileLoadFailed", m_strFileLoadFailed); + m_strFileLockedWrite = TryGetEx(dictNew, "FileLockedWrite", m_strFileLockedWrite); + m_strFileNewVerOrPlgReq = TryGetEx(dictNew, "FileNewVerOrPlgReq", m_strFileNewVerOrPlgReq); + m_strFileNewVerReq = TryGetEx(dictNew, "FileNewVerReq", m_strFileNewVerReq); + m_strFileSaveCorruptionWarning = TryGetEx(dictNew, "FileSaveCorruptionWarning", m_strFileSaveCorruptionWarning); + m_strFileSaveFailed = TryGetEx(dictNew, "FileSaveFailed", m_strFileSaveFailed); + m_strFileSigInvalid = TryGetEx(dictNew, "FileSigInvalid", m_strFileSigInvalid); + m_strFileUnknownCipher = TryGetEx(dictNew, "FileUnknownCipher", m_strFileUnknownCipher); + m_strFileUnknownCompression = TryGetEx(dictNew, "FileUnknownCompression", m_strFileUnknownCompression); + m_strFileVersionUnsupported = TryGetEx(dictNew, "FileVersionUnsupported", m_strFileVersionUnsupported); + m_strFinalKeyCreationFailed = TryGetEx(dictNew, "FinalKeyCreationFailed", m_strFinalKeyCreationFailed); + m_strFrameworkNotImplExcp = TryGetEx(dictNew, "FrameworkNotImplExcp", m_strFrameworkNotImplExcp); + m_strGeneral = TryGetEx(dictNew, "General", m_strGeneral); + m_strInvalidCompositeKey = TryGetEx(dictNew, "InvalidCompositeKey", m_strInvalidCompositeKey); + m_strInvalidCompositeKeyHint = TryGetEx(dictNew, "InvalidCompositeKeyHint", m_strInvalidCompositeKeyHint); + m_strInvalidDataWhileDecoding = TryGetEx(dictNew, "InvalidDataWhileDecoding", m_strInvalidDataWhileDecoding); + m_strKeePass1xHint = TryGetEx(dictNew, "KeePass1xHint", m_strKeePass1xHint); + m_strKeyBits = TryGetEx(dictNew, "KeyBits", m_strKeyBits); + m_strKeyFileDbSel = TryGetEx(dictNew, "KeyFileDbSel", m_strKeyFileDbSel); + m_strMasterSeedLengthInvalid = TryGetEx(dictNew, "MasterSeedLengthInvalid", m_strMasterSeedLengthInvalid); + m_strOldFormat = TryGetEx(dictNew, "OldFormat", m_strOldFormat); + m_strPassive = TryGetEx(dictNew, "Passive", m_strPassive); + m_strPreAuth = TryGetEx(dictNew, "PreAuth", m_strPreAuth); + m_strTimeout = TryGetEx(dictNew, "Timeout", m_strTimeout); + m_strTryAgainSecs = TryGetEx(dictNew, "TryAgainSecs", m_strTryAgainSecs); + m_strUnknownHeaderId = TryGetEx(dictNew, "UnknownHeaderId", m_strUnknownHeaderId); + m_strUnknownKdf = TryGetEx(dictNew, "UnknownKdf", m_strUnknownKdf); + m_strUserAccountKeyError = TryGetEx(dictNew, "UserAccountKeyError", m_strUserAccountKeyError); + m_strUserAgent = TryGetEx(dictNew, "UserAgent", m_strUserAgent); + } + + private static readonly string[] m_vKeyNames = { + "CryptoStreamFailed", + "EncDataTooLarge", + "ErrorInClipboard", + "Expect100Continue", + "FatalError", + "FatalErrorText", + "FileCorrupted", + "FileHeaderCorrupted", + "FileIncomplete", + "FileIncompleteExpc", + "FileLoadFailed", + "FileLockedWrite", + "FileNewVerOrPlgReq", + "FileNewVerReq", + "FileSaveCorruptionWarning", + "FileSaveFailed", + "FileSigInvalid", + "FileUnknownCipher", + "FileUnknownCompression", + "FileVersionUnsupported", + "FinalKeyCreationFailed", + "FrameworkNotImplExcp", + "General", + "InvalidCompositeKey", + "InvalidCompositeKeyHint", + "InvalidDataWhileDecoding", + "KeePass1xHint", + "KeyBits", + "KeyFileDbSel", + "MasterSeedLengthInvalid", + "OldFormat", + "Passive", + "PreAuth", + "Timeout", + "TryAgainSecs", + "UnknownHeaderId", + "UnknownKdf", + "UserAccountKeyError", + "UserAgent" + }; + + public static string[] GetKeyNames() + { + return m_vKeyNames; + } + + private static string m_strCryptoStreamFailed = + @"Failed to initialize encryption/decryption stream!"; + /// + /// Look up a localized string similar to + /// 'Failed to initialize encryption/decryption stream!'. + /// + public static string CryptoStreamFailed + { + get { return m_strCryptoStreamFailed; } + } + + private static string m_strEncDataTooLarge = + @"The data is too large to be encrypted/decrypted securely using {PARAM}."; + /// + /// Look up a localized string similar to + /// 'The data is too large to be encrypted/decrypted securely using {PARAM}.'. + /// + public static string EncDataTooLarge + { + get { return m_strEncDataTooLarge; } + } + + private static string m_strErrorInClipboard = + @"An extended error report has been copied to the clipboard."; + /// + /// Look up a localized string similar to + /// 'An extended error report has been copied to the clipboard.'. + /// + public static string ErrorInClipboard + { + get { return m_strErrorInClipboard; } + } + + private static string m_strExpect100Continue = + @"Expect 100-Continue responses"; + /// + /// Look up a localized string similar to + /// 'Expect 100-Continue responses'. + /// + public static string Expect100Continue + { + get { return m_strExpect100Continue; } + } + + private static string m_strFatalError = + @"Fatal Error"; + /// + /// Look up a localized string similar to + /// 'Fatal Error'. + /// + public static string FatalError + { + get { return m_strFatalError; } + } + + private static string m_strFatalErrorText = + @"A fatal error has occurred!"; + /// + /// Look up a localized string similar to + /// 'A fatal error has occurred!'. + /// + public static string FatalErrorText + { + get { return m_strFatalErrorText; } + } + + private static string m_strFileCorrupted = + @"The file is corrupted."; + /// + /// Look up a localized string similar to + /// 'The file is corrupted.'. + /// + public static string FileCorrupted + { + get { return m_strFileCorrupted; } + } + + private static string m_strFileHeaderCorrupted = + @"The file header is corrupted."; + /// + /// Look up a localized string similar to + /// 'The file header is corrupted.'. + /// + public static string FileHeaderCorrupted + { + get { return m_strFileHeaderCorrupted; } + } + + private static string m_strFileIncomplete = + @"Data is missing at the end of the file, i.e. the file is incomplete."; + /// + /// Look up a localized string similar to + /// 'Data is missing at the end of the file, i.e. the file is incomplete.'. + /// + public static string FileIncomplete + { + get { return m_strFileIncomplete; } + } + + private static string m_strFileIncompleteExpc = + @"Less data than expected could be read from the file."; + /// + /// Look up a localized string similar to + /// 'Less data than expected could be read from the file.'. + /// + public static string FileIncompleteExpc + { + get { return m_strFileIncompleteExpc; } + } + + private static string m_strFileLoadFailed = + @"Failed to load the specified file!"; + /// + /// Look up a localized string similar to + /// 'Failed to load the specified file!'. + /// + public static string FileLoadFailed + { + get { return m_strFileLoadFailed; } + } + + private static string m_strFileLockedWrite = + @"The file is locked, because the following user is currently writing to it:"; + /// + /// Look up a localized string similar to + /// 'The file is locked, because the following user is currently writing to it:'. + /// + public static string FileLockedWrite + { + get { return m_strFileLockedWrite; } + } + + private static string m_strFileNewVerOrPlgReq = + @"A newer KeePass version or a plugin is required to open this file."; + /// + /// Look up a localized string similar to + /// 'A newer KeePass version or a plugin is required to open this file.'. + /// + public static string FileNewVerOrPlgReq + { + get { return m_strFileNewVerOrPlgReq; } + } + + private static string m_strFileNewVerReq = + @"A newer KeePass version is required to open this file."; + /// + /// Look up a localized string similar to + /// 'A newer KeePass version is required to open this file.'. + /// + public static string FileNewVerReq + { + get { return m_strFileNewVerReq; } + } + + private static string m_strFileSaveCorruptionWarning = + @"The target file might be corrupted. Please try saving again. If that fails, save the database to a different location."; + /// + /// Look up a localized string similar to + /// 'The target file might be corrupted. Please try saving again. If that fails, save the database to a different location.'. + /// + public static string FileSaveCorruptionWarning + { + get { return m_strFileSaveCorruptionWarning; } + } + + private static string m_strFileSaveFailed = + @"Failed to save the current database to the specified location!"; + /// + /// Look up a localized string similar to + /// 'Failed to save the current database to the specified location!'. + /// + public static string FileSaveFailed + { + get { return m_strFileSaveFailed; } + } + + private static string m_strFileSigInvalid = + @"The file signature is invalid. Either the file isn't a KeePass database file at all or it is corrupted."; + /// + /// Look up a localized string similar to + /// 'The file signature is invalid. Either the file isn't a KeePass database file at all or it is corrupted.'. + /// + public static string FileSigInvalid + { + get { return m_strFileSigInvalid; } + } + + private static string m_strFileUnknownCipher = + @"The file is encrypted using an unknown encryption algorithm!"; + /// + /// Look up a localized string similar to + /// 'The file is encrypted using an unknown encryption algorithm!'. + /// + public static string FileUnknownCipher + { + get { return m_strFileUnknownCipher; } + } + + private static string m_strFileUnknownCompression = + @"The file is compressed using an unknown compression algorithm!"; + /// + /// Look up a localized string similar to + /// 'The file is compressed using an unknown compression algorithm!'. + /// + public static string FileUnknownCompression + { + get { return m_strFileUnknownCompression; } + } + + private static string m_strFileVersionUnsupported = + @"The file version is unsupported."; + /// + /// Look up a localized string similar to + /// 'The file version is unsupported.'. + /// + public static string FileVersionUnsupported + { + get { return m_strFileVersionUnsupported; } + } + + private static string m_strFinalKeyCreationFailed = + @"Failed to create the final encryption/decryption key!"; + /// + /// Look up a localized string similar to + /// 'Failed to create the final encryption/decryption key!'. + /// + public static string FinalKeyCreationFailed + { + get { return m_strFinalKeyCreationFailed; } + } + + private static string m_strFrameworkNotImplExcp = + @"The .NET framework/runtime under which KeePass is currently running does not support this operation."; + /// + /// Look up a localized string similar to + /// 'The .NET framework/runtime under which KeePass is currently running does not support this operation.'. + /// + public static string FrameworkNotImplExcp + { + get { return m_strFrameworkNotImplExcp; } + } + + private static string m_strGeneral = + @"General"; + /// + /// Look up a localized string similar to + /// 'General'. + /// + public static string General + { + get { return m_strGeneral; } + } + + private static string m_strInvalidCompositeKey = + @"The composite key is invalid!"; + /// + /// Look up a localized string similar to + /// 'The composite key is invalid!'. + /// + public static string InvalidCompositeKey + { + get { return m_strInvalidCompositeKey; } + } + + private static string m_strInvalidCompositeKeyHint = + @"Make sure the composite key is correct and try again."; + /// + /// Look up a localized string similar to + /// 'Make sure the composite key is correct and try again.'. + /// + public static string InvalidCompositeKeyHint + { + get { return m_strInvalidCompositeKeyHint; } + } + + private static string m_strInvalidDataWhileDecoding = + @"Found invalid data while decoding."; + /// + /// Look up a localized string similar to + /// 'Found invalid data while decoding.'. + /// + public static string InvalidDataWhileDecoding + { + get { return m_strInvalidDataWhileDecoding; } + } + + private static string m_strKeePass1xHint = + @"In order to import KeePass 1.x KDB files, create a new 2.x database file and click 'File' -> 'Import' in the main menu. In the import dialog, choose 'KeePass KDB (1.x)' as file format."; + /// + /// Look up a localized string similar to + /// 'In order to import KeePass 1.x KDB files, create a new 2.x database file and click 'File' -> 'Import' in the main menu. In the import dialog, choose 'KeePass KDB (1.x)' as file format.'. + /// + public static string KeePass1xHint + { + get { return m_strKeePass1xHint; } + } + + private static string m_strKeyBits = + @"{PARAM}-bit key"; + /// + /// Look up a localized string similar to + /// '{PARAM}-bit key'. + /// + public static string KeyBits + { + get { return m_strKeyBits; } + } + + private static string m_strKeyFileDbSel = + @"Database files cannot be used as key files."; + /// + /// Look up a localized string similar to + /// 'Database files cannot be used as key files.'. + /// + public static string KeyFileDbSel + { + get { return m_strKeyFileDbSel; } + } + + private static string m_strMasterSeedLengthInvalid = + @"The length of the master key seed is invalid!"; + /// + /// Look up a localized string similar to + /// 'The length of the master key seed is invalid!'. + /// + public static string MasterSeedLengthInvalid + { + get { return m_strMasterSeedLengthInvalid; } + } + + private static string m_strOldFormat = + @"The selected file appears to be an old format"; + /// + /// Look up a localized string similar to + /// 'The selected file appears to be an old format'. + /// + public static string OldFormat + { + get { return m_strOldFormat; } + } + + private static string m_strPassive = + @"Passive"; + /// + /// Look up a localized string similar to + /// 'Passive'. + /// + public static string Passive + { + get { return m_strPassive; } + } + + private static string m_strPreAuth = + @"Pre-authenticate"; + /// + /// Look up a localized string similar to + /// 'Pre-authenticate'. + /// + public static string PreAuth + { + get { return m_strPreAuth; } + } + + private static string m_strTimeout = + @"Timeout"; + /// + /// Look up a localized string similar to + /// 'Timeout'. + /// + public static string Timeout + { + get { return m_strTimeout; } + } + + private static string m_strTryAgainSecs = + @"Please try it again in a few seconds."; + /// + /// Look up a localized string similar to + /// 'Please try it again in a few seconds.'. + /// + public static string TryAgainSecs + { + get { return m_strTryAgainSecs; } + } + + private static string m_strUnknownHeaderId = + @"Unknown header ID!"; + /// + /// Look up a localized string similar to + /// 'Unknown header ID!'. + /// + public static string UnknownHeaderId + { + get { return m_strUnknownHeaderId; } + } + + private static string m_strUnknownKdf = + @"Unknown key derivation function!"; + /// + /// Look up a localized string similar to + /// 'Unknown key derivation function!'. + /// + public static string UnknownKdf + { + get { return m_strUnknownKdf; } + } + + private static string m_strUserAccountKeyError = + @"The operating system did not grant KeePass read/write access to the user profile folder, where the protected user key is stored."; + /// + /// Look up a localized string similar to + /// 'The operating system did not grant KeePass read/write access to the user profile folder, where the protected user key is stored.'. + /// + public static string UserAccountKeyError + { + get { return m_strUserAccountKeyError; } + } + + private static string m_strUserAgent = + @"User agent"; + /// + /// Look up a localized string similar to + /// 'User agent'. + /// + public static string UserAgent + { + get { return m_strUserAgent; } + } + } +} diff --git a/ModernKeePassLib/Resources/KSRes.Generated.cs b/ModernKeePassLib/Resources/KSRes.Generated.cs new file mode 100644 index 0000000..7b49547 --- /dev/null +++ b/ModernKeePassLib/Resources/KSRes.Generated.cs @@ -0,0 +1,52 @@ +// This is a generated file! +// Do not edit manually, changes will be overwritten. + +using System; +using System.Collections.Generic; + +namespace ModernKeePassLib.Resources +{ + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + public static class KSRes + { + private static string TryGetEx(Dictionary dictNew, + string strName, string strDefault) + { + string strTemp; + + if(dictNew.TryGetValue(strName, out strTemp)) + return strTemp; + + return strDefault; + } + + public static void SetTranslatedStrings(Dictionary dictNew) + { + if(dictNew == null) throw new ArgumentNullException("dictNew"); + + m_strTest = TryGetEx(dictNew, "Test", m_strTest); + } + + private static readonly string[] m_vKeyNames = { + "Test" + }; + + public static string[] GetKeyNames() + { + return m_vKeyNames; + } + + private static string m_strTest = + @"Test"; + /// + /// Look up a localized string similar to + /// 'Test'. + /// + public static string Test + { + get { return m_strTest; } + } + } +} diff --git a/ModernKeePassLib/Security/ProtectedBinary.cs b/ModernKeePassLib/Security/ProtectedBinary.cs new file mode 100644 index 0000000..7a25eb0 --- /dev/null +++ b/ModernKeePassLib/Security/ProtectedBinary.cs @@ -0,0 +1,465 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Diagnostics; +using System.Threading; + +#if !KeePassUAP +using System.Security.Cryptography; +#endif + +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Cryptography.Cipher; +using ModernKeePassLib.Native; +using ModernKeePassLib.Utility; + +#if KeePassLibSD +using KeePassLibSD; +#endif + +namespace ModernKeePassLib.Security +{ + [Flags] + public enum PbCryptFlags + { + None = 0, + Encrypt = 1, + Decrypt = 2 + } + + public delegate void PbCryptDelegate(byte[] pbData, PbCryptFlags cf, + long lID); + + /// + /// A protected binary, i.e. a byte array that is encrypted in memory. + /// A ProtectedBinary object is immutable and thread-safe. + /// + public sealed class ProtectedBinary : IEquatable + { + private const int BlockSize = 16; + + private static PbCryptDelegate g_fExtCrypt = null; + /// + /// A plugin can provide a custom memory protection method + /// by assigning a non-null delegate to this property. + /// + public static PbCryptDelegate ExtCrypt + { + get { return g_fExtCrypt; } + set { g_fExtCrypt = value; } + } + + // Local copy of the delegate that was used for encryption, + // in order to allow correct decryption even when the global + // delegate changes + private PbCryptDelegate m_fExtCrypt = null; + + private enum PbMemProt + { + None = 0, + ProtectedMemory, // DPAPI on Windows + ChaCha20, + ExtCrypt + } + + // ProtectedMemory is supported only on Windows 2000 SP3 and higher +#if !KeePassLibSD + private static bool? g_obProtectedMemorySupported = null; +#endif + private static bool ProtectedMemorySupported + { + get + { +#if KeePassLibSD + return false; +#else + bool? ob = g_obProtectedMemorySupported; + if(ob.HasValue) return ob.Value; + + // Mono does not implement any encryption for ProtectedMemory + // on Linux (Mono uses DPAPI on Windows); + // https://sourceforge.net/p/keepass/feature-requests/1907/ + if(NativeLib.IsUnix()) + { + g_obProtectedMemorySupported = false; + return false; + } + + ob = false; + try // Test whether ProtectedMemory is supported + { + // BlockSize * 3 in order to test encryption for multiple + // blocks, but not introduce a power of 2 as factor + byte[] pb = new byte[ProtectedBinary.BlockSize * 3]; + for(int i = 0; i < pb.Length; ++i) pb[i] = (byte)i; + + ProtectedMemory.Protect(pb, MemoryProtectionScope.SameProcess); + + for(int i = 0; i < pb.Length; ++i) + { + if(pb[i] != (byte)i) { ob = true; break; } + } + } + catch(Exception) { } // Windows 98 / ME + + g_obProtectedMemorySupported = ob; + return ob.Value; +#endif + } + } + + private static long g_lCurID = 0; + private long m_lID; + + private byte[] m_pbData; // Never null + + // The real length of the data; this value can be different from + // m_pbData.Length, as the length of m_pbData always is a multiple + // of BlockSize (required for ProtectedMemory) + private uint m_uDataLen; + + private bool m_bProtected; // Protection requested by the caller + + private PbMemProt m_mp = PbMemProt.None; // Actual protection + + private readonly object m_objSync = new object(); + + private static byte[] g_pbKey32 = null; + + /// + /// A flag specifying whether the ProtectedBinary object has + /// turned on memory protection or not. + /// + public bool IsProtected + { + get { return m_bProtected; } + } + + /// + /// Length of the stored data. + /// + public uint Length + { + get { return m_uDataLen; } + } + + /// + /// Construct a new, empty protected binary data object. + /// Protection is disabled. + /// + public ProtectedBinary() + { + Init(false, MemUtil.EmptyByteArray, 0, 0); + } + + /// + /// Construct a new protected binary data object. + /// + /// If this paremeter is true, + /// the data will be encrypted in memory. If it is false, the + /// data is stored in plain-text in the process memory. + /// Value of the protected object. + /// The input parameter is not modified and + /// ProtectedBinary doesn't take ownership of the data, + /// i.e. the caller is responsible for clearing it. + public ProtectedBinary(bool bEnableProtection, byte[] pbData) + { + if(pbData == null) throw new ArgumentNullException("pbData"); // For .Length + + Init(bEnableProtection, pbData, 0, pbData.Length); + } + + /// + /// Construct a new protected binary data object. + /// + /// If this paremeter is true, + /// the data will be encrypted in memory. If it is false, the + /// data is stored in plain-text in the process memory. + /// Value of the protected object. + /// The input parameter is not modified and + /// ProtectedBinary doesn't take ownership of the data, + /// i.e. the caller is responsible for clearing it. + /// Offset for . + /// Size for . + public ProtectedBinary(bool bEnableProtection, byte[] pbData, + int iOffset, int cbSize) + { + Init(bEnableProtection, pbData, iOffset, cbSize); + } + + /// + /// Construct a new protected binary data object. + /// Copy the data from a XorredBuffer object. + /// + /// Enable protection or not. + /// XorredBuffer object containing the data. + public ProtectedBinary(bool bEnableProtection, XorredBuffer xb) + { + if(xb == null) { Debug.Assert(false); throw new ArgumentNullException("xb"); } + + byte[] pb = xb.ReadPlainText(); + try { Init(bEnableProtection, pb, 0, pb.Length); } + finally { if(bEnableProtection) MemUtil.ZeroByteArray(pb); } + } + + private void Init(bool bEnableProtection, byte[] pbData, int iOffset, + int cbSize) + { + if(pbData == null) throw new ArgumentNullException("pbData"); + if(iOffset < 0) throw new ArgumentOutOfRangeException("iOffset"); + if(cbSize < 0) throw new ArgumentOutOfRangeException("cbSize"); + if(iOffset > (pbData.Length - cbSize)) + throw new ArgumentOutOfRangeException("cbSize"); + +#if KeePassLibSD + m_lID = ++g_lCurID; +#else + m_lID = Interlocked.Increment(ref g_lCurID); +#endif + + m_bProtected = bEnableProtection; + m_uDataLen = (uint)cbSize; + + const int bs = ProtectedBinary.BlockSize; + int nBlocks = cbSize / bs; + if((nBlocks * bs) < cbSize) ++nBlocks; + Debug.Assert((nBlocks * bs) >= cbSize); + + m_pbData = new byte[nBlocks * bs]; + Array.Copy(pbData, iOffset, m_pbData, 0, cbSize); + + Encrypt(); + } + + private void Encrypt() + { + Debug.Assert(m_mp == PbMemProt.None); + + // Nothing to do if caller didn't request protection + if(!m_bProtected) return; + + // ProtectedMemory.Protect throws for data size == 0 + if(m_pbData.Length == 0) return; + + PbCryptDelegate f = g_fExtCrypt; + if(f != null) + { + f(m_pbData, PbCryptFlags.Encrypt, m_lID); + + m_fExtCrypt = f; + m_mp = PbMemProt.ExtCrypt; + return; + } + + if(ProtectedBinary.ProtectedMemorySupported) + { + ProtectedMemory.Protect(m_pbData, MemoryProtectionScope.SameProcess); + + m_mp = PbMemProt.ProtectedMemory; + return; + } + + byte[] pbKey32 = g_pbKey32; + if(pbKey32 == null) + { + pbKey32 = GetRandom32(); + + byte[] pbUpd = Interlocked.Exchange(ref g_pbKey32, pbKey32); + if(pbUpd != null) pbKey32 = pbUpd; + } + + byte[] pbIV = new byte[12]; + MemUtil.UInt64ToBytesEx((ulong)m_lID, pbIV, 4); + using(ChaCha20Cipher c = new ChaCha20Cipher(pbKey32, pbIV, true)) + { + c.Encrypt(m_pbData, 0, m_pbData.Length); + } + m_mp = PbMemProt.ChaCha20; + } + + private void Decrypt() + { + if(m_pbData.Length == 0) return; + + if(m_mp == PbMemProt.ProtectedMemory) + ProtectedMemory.Unprotect(m_pbData, MemoryProtectionScope.SameProcess); + else if(m_mp == PbMemProt.ChaCha20) + { + byte[] pbIV = new byte[12]; + MemUtil.UInt64ToBytesEx((ulong)m_lID, pbIV, 4); + using(ChaCha20Cipher c = new ChaCha20Cipher(g_pbKey32, pbIV, true)) + { + c.Decrypt(m_pbData, 0, m_pbData.Length); + } + } + else if(m_mp == PbMemProt.ExtCrypt) + m_fExtCrypt(m_pbData, PbCryptFlags.Decrypt, m_lID); + else { Debug.Assert(m_mp == PbMemProt.None); } + + m_mp = PbMemProt.None; + } + + /// + /// Get a copy of the protected data as a byte array. + /// Please note that the returned byte array is not protected and + /// can therefore been read by any other application. + /// Make sure that your clear it properly after usage. + /// + /// Unprotected byte array. This is always a copy of the internal + /// protected data and can therefore be cleared safely. + public byte[] ReadData() + { + if(m_uDataLen == 0) return MemUtil.EmptyByteArray; + + byte[] pbReturn = new byte[m_uDataLen]; + + lock(m_objSync) + { + Decrypt(); + Array.Copy(m_pbData, pbReturn, (int)m_uDataLen); + Encrypt(); + } + + return pbReturn; + } + + /// + /// Get the data xorred with bytes from a CryptoRandomStream. + /// + public byte[] ReadXorredData(CryptoRandomStream crsRandomSource) + { + if(crsRandomSource == null) { Debug.Assert(false); throw new ArgumentNullException("crsRandomSource"); } + + byte[] pbData = ReadData(); + int cb = pbData.Length; + + byte[] pbPad = crsRandomSource.GetRandomBytes((uint)cb); + Debug.Assert(pbPad.Length == cb); + + for(int i = 0; i < cb; ++i) + pbData[i] ^= pbPad[i]; + + MemUtil.ZeroByteArray(pbPad); + return pbData; + } + + private int? m_hash = null; + public override int GetHashCode() + { + if(m_hash.HasValue) return m_hash.Value; + + int h = (m_bProtected ? 0x7B11D289 : 0); + + byte[] pb = ReadData(); + unchecked + { + for(int i = 0; i < pb.Length; ++i) + h = (h << 3) + h + (int)pb[i]; + } + if(m_bProtected) MemUtil.ZeroByteArray(pb); + + m_hash = h; + return h; + } + + public override bool Equals(object obj) + { + return this.Equals(obj as ProtectedBinary, true); + } + + public bool Equals(ProtectedBinary other) + { + return this.Equals(other, true); + } + + public bool Equals(ProtectedBinary other, bool bCheckProtEqual) + { + if(other == null) return false; // No assert + if(object.ReferenceEquals(this, other)) return true; // Perf. opt. + + if(bCheckProtEqual && (m_bProtected != other.m_bProtected)) + return false; + + if(m_uDataLen != other.m_uDataLen) return false; + + byte[] pbL = ReadData(), pbR = null; + bool bEq; + try + { + pbR = other.ReadData(); + bEq = MemUtil.ArraysEqual(pbL, pbR); + } + finally + { + if(m_bProtected) MemUtil.ZeroByteArray(pbL); + if(other.m_bProtected && (pbR != null)) MemUtil.ZeroByteArray(pbR); + } + + return bEq; + } + + private static byte[] GetRandom32() + { + // Do not use CryptoRandom here, as it uses ProtectedBinary; + // we would have an infinite recursion when trying to + // construct a ProtectedBinary object + // return CryptoRandom.Instance.GetRandomBytes(32); + + byte[] pbAll = new byte[32 + 16 + 8 + 4]; + int i = 0; + + try + { + RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider(); + byte[] pb = new byte[32]; + rng.GetBytes(pb); + + Array.Copy(pb, 0, pbAll, i, 32); + i += 32; + + MemUtil.ZeroByteArray(pb); + // In .NET 2.0, RNGCryptoServiceProvider does not + // implement IDisposable; in later .NET versions it does + MemUtil.DisposeIfPossible(rng); + } + catch(Exception) { Debug.Assert(false); } + + try // In case RNGCryptoServiceProvider doesn't work properly + { + byte[] pb = Guid.NewGuid().ToByteArray(); + Array.Copy(pb, 0, pbAll, i, 16); + i += 16; + + MemUtil.Int64ToBytesEx(DateTime.UtcNow.ToBinary(), pbAll, i); + i += 8; + + MemUtil.Int32ToBytesEx(Environment.TickCount, pbAll, i); + i += 4; + } + catch(Exception) { Debug.Assert(false); } + + Debug.Assert(i == pbAll.Length); + + byte[] pbHash = CryptoUtil.HashSha256(pbAll); + MemUtil.ZeroByteArray(pbAll); + return pbHash; + } + } +} diff --git a/ModernKeePassLib/Security/ProtectedString.cs b/ModernKeePassLib/Security/ProtectedString.cs new file mode 100644 index 0000000..23572b1 --- /dev/null +++ b/ModernKeePassLib/Security/ProtectedString.cs @@ -0,0 +1,434 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Diagnostics; +using System.Text; + +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Utility; + +#if KeePassLibSD +using KeePassLibSD; +#endif + +// SecureString objects are limited to 65536 characters, don't use + +namespace ModernKeePassLib.Security +{ + /// + /// A string that is protected in process memory. + /// ProtectedString objects are immutable and thread-safe. + /// +#if (DEBUG && !KeePassLibSD) + [DebuggerDisplay("{ReadString()}")] +#endif + public sealed class ProtectedString + { + // Exactly one of the following will be non-null + private ProtectedBinary m_pbUtf8 = null; + private string m_strPlainText = null; + + private bool m_bIsProtected; + + private static readonly ProtectedString m_psEmpty = new ProtectedString(); + /// + /// Get an empty ProtectedString object, without protection. + /// + public static ProtectedString Empty + { + get { return m_psEmpty; } + } + + private static readonly ProtectedString m_psEmptyEx = new ProtectedString( + true, new byte[0]); + /// + /// Get an empty ProtectedString object, with protection turned on. + /// + public static ProtectedString EmptyEx + { + get { return m_psEmptyEx; } + } + + /// + /// A flag specifying whether the ProtectedString object + /// has turned on memory protection or not. + /// + public bool IsProtected + { + get { return m_bIsProtected; } + } + + public bool IsEmpty + { + get + { + ProtectedBinary p = m_pbUtf8; // Local ref for thread-safety + if(p != null) return (p.Length == 0); + + Debug.Assert(m_strPlainText != null); + return (m_strPlainText.Length == 0); + } + } + + private int m_nCachedLength = -1; + /// + /// Length of the protected string, in characters. + /// + public int Length + { + get + { + if(m_nCachedLength >= 0) return m_nCachedLength; + + ProtectedBinary p = m_pbUtf8; // Local ref for thread-safety + if(p != null) + { + byte[] pbPlain = p.ReadData(); + try { m_nCachedLength = StrUtil.Utf8.GetCharCount(pbPlain); } + finally { MemUtil.ZeroByteArray(pbPlain); } + } + else + { + Debug.Assert(m_strPlainText != null); + m_nCachedLength = m_strPlainText.Length; + } + + return m_nCachedLength; + } + } + + /// + /// Construct a new protected string object. Protection is + /// disabled. + /// + public ProtectedString() + { + Init(false, string.Empty); + } + + /// + /// Construct a new protected string. The string is initialized + /// to the value supplied in the parameters. + /// + /// If this parameter is true, + /// the string will be protected in memory (encrypted). If it + /// is false, the string will be stored as plain-text. + /// The initial string value. + public ProtectedString(bool bEnableProtection, string strValue) + { + Init(bEnableProtection, strValue); + } + + /// + /// Construct a new protected string. The string is initialized + /// to the value supplied in the parameters (UTF-8 encoded string). + /// + /// If this parameter is true, + /// the string will be protected in memory (encrypted). If it + /// is false, the string will be stored as plain-text. + /// The initial string value, encoded as + /// UTF-8 byte array. This parameter won't be modified; the caller + /// is responsible for clearing it. + public ProtectedString(bool bEnableProtection, byte[] vUtf8Value) + { + Init(bEnableProtection, vUtf8Value); + } + + /// + /// Construct a new protected string. The string is initialized + /// to the value passed in the XorredBuffer object. + /// + /// Enable protection or not. + /// XorredBuffer object containing the + /// string in UTF-8 representation. The UTF-8 string must not + /// be null-terminated. + public ProtectedString(bool bEnableProtection, XorredBuffer xb) + { + if(xb == null) { Debug.Assert(false); throw new ArgumentNullException("xb"); } + + byte[] pb = xb.ReadPlainText(); + try { Init(bEnableProtection, pb); } + finally { if(bEnableProtection) MemUtil.ZeroByteArray(pb); } + } + + private void Init(bool bEnableProtection, string str) + { + if(str == null) throw new ArgumentNullException("str"); + + m_bIsProtected = bEnableProtection; + + // As the string already is in memory and immutable, + // protection would be useless + m_strPlainText = str; + } + + private void Init(bool bEnableProtection, byte[] pbUtf8) + { + if(pbUtf8 == null) throw new ArgumentNullException("pbUtf8"); + + m_bIsProtected = bEnableProtection; + + if(bEnableProtection) + m_pbUtf8 = new ProtectedBinary(true, pbUtf8); + else + m_strPlainText = StrUtil.Utf8.GetString(pbUtf8, 0, pbUtf8.Length); + } + + /// + /// Convert the protected string to a standard string object. + /// Be careful with this function, as the returned string object + /// isn't protected anymore and stored in plain-text in the + /// process memory. + /// + /// Plain-text string. Is never null. + public string ReadString() + { + if(m_strPlainText != null) return m_strPlainText; + + byte[] pb = ReadUtf8(); + string str = ((pb.Length == 0) ? string.Empty : + StrUtil.Utf8.GetString(pb, 0, pb.Length)); + // No need to clear pb + + // As the text is now visible in process memory anyway, + // there's no need to protect it anymore (strings are + // immutable and thus cannot be overwritten) + m_strPlainText = str; + m_pbUtf8 = null; // Thread-safe order + + return str; + } + + /// + /// Read out the string and return it as a char array. + /// The returned array is not protected and should be cleared by + /// the caller. + /// + /// Plain-text char array. + public char[] ReadChars() + { + if(m_strPlainText != null) return m_strPlainText.ToCharArray(); + + byte[] pb = ReadUtf8(); + char[] v; + try { v = StrUtil.Utf8.GetChars(pb); } + finally { MemUtil.ZeroByteArray(pb); } + return v; + } + + /// + /// Read out the string and return a byte array that contains the + /// string encoded using UTF-8. + /// The returned array is not protected and should be cleared by + /// the caller. + /// + /// Plain-text UTF-8 byte array. + public byte[] ReadUtf8() + { + ProtectedBinary p = m_pbUtf8; // Local ref for thread-safety + if(p != null) return p.ReadData(); + + return StrUtil.Utf8.GetBytes(m_strPlainText); + } + + /// + /// Get the string as an UTF-8 sequence xorred with bytes + /// from a CryptoRandomStream. + /// + public byte[] ReadXorredString(CryptoRandomStream crsRandomSource) + { + if(crsRandomSource == null) { Debug.Assert(false); throw new ArgumentNullException("crsRandomSource"); } + + byte[] pbData = ReadUtf8(); + int cb = pbData.Length; + + byte[] pbPad = crsRandomSource.GetRandomBytes((uint)cb); + Debug.Assert(pbPad.Length == cb); + + for(int i = 0; i < cb; ++i) + pbData[i] ^= pbPad[i]; + + MemUtil.ZeroByteArray(pbPad); + return pbData; + } + + public ProtectedString WithProtection(bool bProtect) + { + if(bProtect == m_bIsProtected) return this; + + byte[] pb = ReadUtf8(); + + // No need to clear pb; either the current or the new object is unprotected + return new ProtectedString(bProtect, pb); + } + + public bool Equals(ProtectedString ps, bool bCheckProtEqual) + { + if(ps == null) throw new ArgumentNullException("ps"); + if(object.ReferenceEquals(this, ps)) return true; // Perf. opt. + + bool bPA = m_bIsProtected, bPB = ps.m_bIsProtected; + if(bCheckProtEqual && (bPA != bPB)) return false; + if(!bPA && !bPB) return (ReadString() == ps.ReadString()); + + byte[] pbA = ReadUtf8(), pbB = null; + bool bEq; + try + { + pbB = ps.ReadUtf8(); + bEq = MemUtil.ArraysEqual(pbA, pbB); + } + finally + { + if(bPA) MemUtil.ZeroByteArray(pbA); + if(bPB && (pbB != null)) MemUtil.ZeroByteArray(pbB); + } + + return bEq; + } + + public ProtectedString Insert(int iStart, string strInsert) + { + if(iStart < 0) throw new ArgumentOutOfRangeException("iStart"); + if(strInsert == null) throw new ArgumentNullException("strInsert"); + if(strInsert.Length == 0) return this; + + if(!m_bIsProtected) + return new ProtectedString(false, ReadString().Insert( + iStart, strInsert)); + + UTF8Encoding utf8 = StrUtil.Utf8; + char[] v = ReadChars(), vNew = null; + byte[] pbNew = null; + ProtectedString ps; + + try + { + if(iStart > v.Length) + throw new ArgumentOutOfRangeException("iStart"); + + char[] vIns = strInsert.ToCharArray(); + + vNew = new char[v.Length + vIns.Length]; + Array.Copy(v, 0, vNew, 0, iStart); + Array.Copy(vIns, 0, vNew, iStart, vIns.Length); + Array.Copy(v, iStart, vNew, iStart + vIns.Length, + v.Length - iStart); + + pbNew = utf8.GetBytes(vNew); + ps = new ProtectedString(true, pbNew); + + Debug.Assert(utf8.GetString(pbNew, 0, pbNew.Length) == + ReadString().Insert(iStart, strInsert)); + } + finally + { + MemUtil.ZeroArray(v); + if(vNew != null) MemUtil.ZeroArray(vNew); + if(pbNew != null) MemUtil.ZeroByteArray(pbNew); + } + + return ps; + } + + public ProtectedString Remove(int iStart, int nCount) + { + if(iStart < 0) throw new ArgumentOutOfRangeException("iStart"); + if(nCount < 0) throw new ArgumentOutOfRangeException("nCount"); + if(nCount == 0) return this; + + if(!m_bIsProtected) + return new ProtectedString(false, ReadString().Remove( + iStart, nCount)); + + UTF8Encoding utf8 = StrUtil.Utf8; + char[] v = ReadChars(), vNew = null; + byte[] pbNew = null; + ProtectedString ps; + + try + { + if((iStart + nCount) > v.Length) + throw new ArgumentException("(iStart + nCount) > v.Length"); + + vNew = new char[v.Length - nCount]; + Array.Copy(v, 0, vNew, 0, iStart); + Array.Copy(v, iStart + nCount, vNew, iStart, v.Length - + (iStart + nCount)); + + pbNew = utf8.GetBytes(vNew); + ps = new ProtectedString(true, pbNew); + + Debug.Assert(utf8.GetString(pbNew, 0, pbNew.Length) == + ReadString().Remove(iStart, nCount)); + } + finally + { + MemUtil.ZeroArray(v); + if(vNew != null) MemUtil.ZeroArray(vNew); + if(pbNew != null) MemUtil.ZeroByteArray(pbNew); + } + + return ps; + } + + public static ProtectedString operator +(ProtectedString a, ProtectedString b) + { + if(a == null) throw new ArgumentNullException("a"); + if(b == null) throw new ArgumentNullException("b"); + + if(b.IsEmpty) return a; + if(a.IsEmpty) return b; + if(!a.IsProtected && !b.IsProtected) + return new ProtectedString(false, a.ReadString() + b.ReadString()); + + char[] vA = a.ReadChars(), vB = null, vNew = null; + byte[] pbNew = null; + ProtectedString ps; + + try + { + vB = b.ReadChars(); + + vNew = new char[vA.Length + vB.Length]; + Array.Copy(vA, vNew, vA.Length); + Array.Copy(vB, 0, vNew, vA.Length, vB.Length); + + pbNew = StrUtil.Utf8.GetBytes(vNew); + ps = new ProtectedString(true, pbNew); + } + finally + { + MemUtil.ZeroArray(vA); + if(vB != null) MemUtil.ZeroArray(vB); + if(vNew != null) MemUtil.ZeroArray(vNew); + if(pbNew != null) MemUtil.ZeroByteArray(pbNew); + } + + return ps; + } + + public static ProtectedString operator +(ProtectedString a, string b) + { + ProtectedString psB = new ProtectedString(false, b); + return (a + psB); + } + } +} diff --git a/ModernKeePassLib/Security/XorredBuffer.cs b/ModernKeePassLib/Security/XorredBuffer.cs new file mode 100644 index 0000000..3a7768f --- /dev/null +++ b/ModernKeePassLib/Security/XorredBuffer.cs @@ -0,0 +1,109 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Diagnostics; + +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Security +{ + /// + /// A XorredBuffer object stores data that is encrypted + /// using a XOR pad. + /// + public sealed class XorredBuffer : IDisposable + { + private byte[] m_pbCT; + private byte[] m_pbXorPad; + + public uint Length + { + get + { + if(m_pbCT == null) { Debug.Assert(false); throw new ObjectDisposedException(null); } + return (uint)m_pbCT.Length; + } + } + + /// + /// Construct a new XorredBuffer object. + /// The byte array must have the same + /// length as the byte array. + /// The XorredBuffer object takes ownership of the two byte + /// arrays, i.e. the caller must not use them afterwards. + /// + /// Data with XOR pad applied. + /// XOR pad that can be used to decrypt the + /// byte array. + public XorredBuffer(byte[] pbCT, byte[] pbXorPad) + { + if(pbCT == null) { Debug.Assert(false); throw new ArgumentNullException("pbCT"); } + if(pbXorPad == null) { Debug.Assert(false); throw new ArgumentNullException("pbXorPad"); } + if(pbCT.Length != pbXorPad.Length) + { + Debug.Assert(false); + throw new ArgumentOutOfRangeException("pbXorPad"); + } + + m_pbCT = pbCT; + m_pbXorPad = pbXorPad; + } + +#if DEBUG + ~XorredBuffer() + { + Debug.Assert((m_pbCT == null) && (m_pbXorPad == null)); + } +#endif + + public void Dispose() + { + if(m_pbCT == null) return; + + MemUtil.ZeroByteArray(m_pbCT); + m_pbCT = null; + + MemUtil.ZeroByteArray(m_pbXorPad); + m_pbXorPad = null; + } + + /// + /// Get a copy of the plain-text. The caller is responsible + /// for clearing the byte array safely after using it. + /// + /// Plain-text byte array. + public byte[] ReadPlainText() + { + byte[] pbCT = m_pbCT, pbX = m_pbXorPad; + if((pbCT == null) || (pbX == null) || (pbCT.Length != pbX.Length)) + { + Debug.Assert(false); + throw new ObjectDisposedException(null); + } + + byte[] pbPT = new byte[pbCT.Length]; + + for(int i = 0; i < pbPT.Length; ++i) + pbPT[i] = (byte)(pbCT[i] ^ pbX[i]); + + return pbPT; + } + } +} diff --git a/ModernKeePassLib/Serialization/BinaryReaderEx.cs b/ModernKeePassLib/Serialization/BinaryReaderEx.cs new file mode 100644 index 0000000..d851705 --- /dev/null +++ b/ModernKeePassLib/Serialization/BinaryReaderEx.cs @@ -0,0 +1,92 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.IO; +using System.Text; + +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Serialization +{ + public sealed class BinaryReaderEx + { + private Stream m_s; + // private Encoding m_enc; // See constructor + + private string m_strReadExcp; // May be null + public string ReadExceptionText + { + get { return m_strReadExcp; } + set { m_strReadExcp = value; } + } + + private Stream m_sCopyTo = null; + /// + /// If this property is set to a non-null stream, all data that + /// is read from the input stream is automatically written to + /// the copy stream (before returning the read data). + /// + public Stream CopyDataTo + { + get { return m_sCopyTo; } + set { m_sCopyTo = value; } + } + + public BinaryReaderEx(Stream input, Encoding encoding, + string strReadExceptionText) + { + if(input == null) throw new ArgumentNullException("input"); + + m_s = input; + // m_enc = encoding; // Not used yet + m_strReadExcp = strReadExceptionText; + } + + public byte[] ReadBytes(int nCount) + { + try + { + byte[] pb = MemUtil.Read(m_s, nCount); + if((pb == null) || (pb.Length != nCount)) + { + if(!string.IsNullOrEmpty(m_strReadExcp)) + throw new EndOfStreamException(m_strReadExcp); + else throw new EndOfStreamException(); + } + + if(m_sCopyTo != null) m_sCopyTo.Write(pb, 0, pb.Length); + return pb; + } + catch(Exception) + { + if(!string.IsNullOrEmpty(m_strReadExcp)) + throw new IOException(m_strReadExcp); + else throw; + } + } + + public byte ReadByte() + { + byte[] pb = ReadBytes(1); + return pb[0]; + } + } +} diff --git a/ModernKeePassLib/Serialization/FileLock.cs b/ModernKeePassLib/Serialization/FileLock.cs new file mode 100644 index 0000000..2f400ac --- /dev/null +++ b/ModernKeePassLib/Serialization/FileLock.cs @@ -0,0 +1,274 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Threading; +#if ModernKeePassLib +using System.Runtime.InteropServices.WindowsRuntime; +using System.Threading.Tasks; +using Windows.Storage.Streams; +#endif +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Resources; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Serialization +{ + public sealed class FileLockException : Exception + { + private readonly string m_strMsg; + + public override string Message + { + get { return m_strMsg; } + } + + public FileLockException(string strBaseFile, string strUser) + { + StringBuilder sb = new StringBuilder(); + + if(!string.IsNullOrEmpty(strBaseFile)) + { + sb.Append(strBaseFile); + sb.Append(MessageService.NewParagraph); + } + + sb.Append(KLRes.FileLockedWrite); + sb.Append(MessageService.NewLine); + + if(!string.IsNullOrEmpty(strUser)) sb.Append(strUser); + else sb.Append("?"); + + sb.Append(MessageService.NewParagraph); + sb.Append(KLRes.TryAgainSecs); + + m_strMsg = sb.ToString(); + } + } + + public sealed class FileLock : IDisposable + { + private const string LockFileExt = ".lock"; + private const string LockFileHeader = "KeePass Lock File"; + + private IOConnectionInfo m_iocLockFile; + + private sealed class LockFileInfo + { + public readonly string ID; + public readonly DateTime Time; + public readonly string UserName; + public readonly string Machine; + public readonly string Domain; + + private LockFileInfo(string strID, string strTime, string strUserName, + string strMachine, string strDomain) + { + this.ID = (strID ?? string.Empty).Trim(); + + DateTime dt; + if(TimeUtil.TryDeserializeUtc(strTime.Trim(), out dt)) + this.Time = dt; + else + { + Debug.Assert(false); + this.Time = DateTime.UtcNow; + } + + this.UserName = (strUserName ?? string.Empty).Trim(); + this.Machine = (strMachine ?? string.Empty).Trim(); + this.Domain = (strDomain ?? string.Empty).Trim(); + + if(this.Domain.Equals(this.Machine, StrUtil.CaseIgnoreCmp)) + this.Domain = string.Empty; + } + + public string GetOwner() + { + StringBuilder sb = new StringBuilder(); + sb.Append((this.UserName.Length > 0) ? this.UserName : "?"); + + bool bMachine = (this.Machine.Length > 0); + bool bDomain = (this.Domain.Length > 0); + if(bMachine || bDomain) + { + sb.Append(" ("); + sb.Append(this.Machine); + if(bMachine && bDomain) sb.Append(" @ "); + sb.Append(this.Domain); + sb.Append(")"); + } + + return sb.ToString(); + } + + public static LockFileInfo Load(IOConnectionInfo iocLockFile) + { + Stream s = null; + try + { + s = IOConnection.OpenRead(iocLockFile); + if(s == null) return null; + + string str = null; + using(StreamReader sr = new StreamReader(s, StrUtil.Utf8)) + { + str = sr.ReadToEnd(); + } + if(str == null) { Debug.Assert(false); return null; } + + str = StrUtil.NormalizeNewLines(str, false); + string[] v = str.Split('\n'); + if((v == null) || (v.Length < 6)) { Debug.Assert(false); return null; } + + if(!v[0].StartsWith(LockFileHeader)) { Debug.Assert(false); return null; } + return new LockFileInfo(v[1], v[2], v[3], v[4], v[5]); + } + catch(FileNotFoundException) { } + catch(Exception) { Debug.Assert(false); } + finally { if(s != null) s.Dispose(); } + + return null; + } + + // Throws on error + public static LockFileInfo Create(IOConnectionInfo iocLockFile) + { + LockFileInfo lfi; + Stream s = null; + try + { + byte[] pbID = CryptoRandom.Instance.GetRandomBytes(16); + string strTime = TimeUtil.SerializeUtc(DateTime.UtcNow); + + lfi = new LockFileInfo(Convert.ToBase64String(pbID), strTime, +#if KeePassUAP + EnvironmentExt.UserName, EnvironmentExt.MachineName, + EnvironmentExt.UserDomainName); +#elif ModernKeePassLib|| KeePassLibSD + string.Empty, string.Empty, string.Empty); +#else + Environment.UserName, Environment.MachineName, + Environment.UserDomainName); +#endif + + StringBuilder sb = new StringBuilder(); +#if !KeePassLibSD + sb.AppendLine(LockFileHeader); + sb.AppendLine(lfi.ID); + sb.AppendLine(strTime); + sb.AppendLine(lfi.UserName); + sb.AppendLine(lfi.Machine); + sb.AppendLine(lfi.Domain); +#else + sb.Append(LockFileHeader + MessageService.NewLine); + sb.Append(lfi.ID + MessageService.NewLine); + sb.Append(strTime + MessageService.NewLine); + sb.Append(lfi.UserName + MessageService.NewLine); + sb.Append(lfi.Machine + MessageService.NewLine); + sb.Append(lfi.Domain + MessageService.NewLine); +#endif + + byte[] pbFile = StrUtil.Utf8.GetBytes(sb.ToString()); + + s = IOConnection.OpenWrite(iocLockFile); + if(s == null) throw new IOException(iocLockFile.GetDisplayName()); + s.Write(pbFile, 0, pbFile.Length); + } + finally { if(s != null) s.Dispose(); } + + return lfi; + } + } + + public FileLock(IOConnectionInfo iocBaseFile) + { + if(iocBaseFile == null) throw new ArgumentNullException("strBaseFile"); + + m_iocLockFile = iocBaseFile.CloneDeep(); + m_iocLockFile.Path += LockFileExt; + + LockFileInfo lfiEx = LockFileInfo.Load(m_iocLockFile); + if(lfiEx != null) + { + m_iocLockFile = null; // Otherwise Dispose deletes the existing one + throw new FileLockException(iocBaseFile.GetDisplayName(), + lfiEx.GetOwner()); + } + + LockFileInfo.Create(m_iocLockFile); + } + + ~FileLock() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool bDisposing) + { + if(m_iocLockFile == null) return; + + bool bFileDeleted = false; + for(int r = 0; r < 5; ++r) + { + // if(!OwnLockFile()) { bFileDeleted = true; break; } + + try + { + IOConnection.DeleteFile(m_iocLockFile); + bFileDeleted = true; + } + catch(Exception) { Debug.Assert(false); } + + if(bFileDeleted) break; + +#if ModernKeePassLib + if(bDisposing) + Task.Delay(50).Wait(); +#else + if(bDisposing) Thread.Sleep(50); +#endif + } + + // if(bDisposing && !bFileDeleted) + // IOConnection.DeleteFile(m_iocLockFile); // Possibly with exception + + m_iocLockFile = null; + } + + // private bool OwnLockFile() + // { + // if(m_iocLockFile == null) { Debug.Assert(false); return false; } + // if(m_strLockID == null) { Debug.Assert(false); return false; } + // LockFileInfo lfi = LockFileInfo.Load(m_iocLockFile); + // if(lfi == null) return false; + // return m_strLockID.Equals(lfi.ID); + // } + } +} diff --git a/ModernKeePassLib/Serialization/FileTransactionEx.cs b/ModernKeePassLib/Serialization/FileTransactionEx.cs new file mode 100644 index 0000000..01be96c --- /dev/null +++ b/ModernKeePassLib/Serialization/FileTransactionEx.cs @@ -0,0 +1,474 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; + +#if (!ModernKeePassLib && !KeePassLibSD && !KeePassRT) +using System.Security.AccessControl; +#endif + +using ModernKeePassLib.Native; +using ModernKeePassLib.Utility; +using System.Threading.Tasks; +using Windows.Storage; +using Windows.Storage.Streams; +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Resources; + +namespace ModernKeePassLib.Serialization +{ + public sealed class FileTransactionEx : IDisposable + { + private bool m_bTransacted; + private IOConnectionInfo m_iocBase; // Null means disposed + private IOConnectionInfo m_iocTemp; + private IOConnectionInfo m_iocTxfMidFallback = null; // Null <=> TxF not used + + private bool m_bMadeUnhidden = false; + private List m_lToDelete = new List(); + + private const string StrTempSuffix = ".tmp"; + private static readonly string StrTxfTempPrefix = PwDefs.ShortProductName + "_TxF_"; + private const string StrTxfTempSuffix = ".tmp"; + + private static Dictionary g_dEnabled = + new Dictionary(StrUtil.CaseIgnoreComparer); + + private static bool g_bExtraSafe = false; + internal static bool ExtraSafe + { + get { return g_bExtraSafe; } + set { g_bExtraSafe = value; } + } + + public FileTransactionEx(IOConnectionInfo iocBaseFile) : + this(iocBaseFile, true) + { + } + + public FileTransactionEx(IOConnectionInfo iocBaseFile, bool bTransacted) + { + if(iocBaseFile == null) throw new ArgumentNullException("iocBaseFile"); + + m_bTransacted = bTransacted; + + m_iocBase = iocBaseFile.CloneDeep(); + if(m_iocBase.IsLocalFile()) + m_iocBase.Path = UrlUtil.GetShortestAbsolutePath(m_iocBase.Path); + + string strPath = m_iocBase.Path; + +#if !ModernKeePassLib + if(m_iocBase.IsLocalFile()) + { + try + { + if(File.Exists(strPath)) + { + // Symbolic links are realized via reparse points; + // https://msdn.microsoft.com/en-us/library/windows/desktop/aa365503.aspx + // https://msdn.microsoft.com/en-us/library/windows/desktop/aa365680.aspx + // https://msdn.microsoft.com/en-us/library/windows/desktop/aa365006.aspx + // Performing a file transaction on a symbolic link + // would delete/replace the symbolic link instead of + // writing to its target + FileAttributes fa = File.GetAttributes(strPath); + if((long)(fa & FileAttributes.ReparsePoint) != 0) + m_bTransacted = false; + } + else + { + // If the base and the temporary file are in different + // folders and the base file doesn't exist (i.e. we can't + // backup the ACL), a transaction would cause the new file + // to have the default ACL of the temporary folder instead + // of the one of the base folder; therefore, we don't use + // a transaction when the base file doesn't exist (this + // also results in other applications monitoring the folder + // to see one file creation only) + m_bTransacted = false; + } + } + catch(Exception) { Debug.Assert(false); } + } +#endif + +#if !ModernKeePassLib + // Prevent transactions for FTP URLs under .NET 4.0 in order to + // avoid/workaround .NET bug 621450: + // https://connect.microsoft.com/VisualStudio/feedback/details/621450/problem-renaming-file-on-ftp-server-using-ftpwebrequest-in-net-framework-4-0-vs2010-only + if(strPath.StartsWith("ftp:", StrUtil.CaseIgnoreCmp) && + (Environment.Version.Major >= 4) && !NativeLib.IsUnix()) + m_bTransacted = false; +#endif + + foreach(KeyValuePair kvp in g_dEnabled) + { + if(strPath.StartsWith(kvp.Key, StrUtil.CaseIgnoreCmp)) + { + m_bTransacted = kvp.Value; + break; + } + } + + if(m_bTransacted) + { + m_iocTemp = m_iocBase.CloneDeep(); + m_iocTemp.Path += StrTempSuffix; + + TxfPrepare(); // Adjusts m_iocTemp + } + else m_iocTemp = m_iocBase; + } + + ~FileTransactionEx() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool bDisposing) + { + m_iocBase = null; + if(!bDisposing) return; + + try + { + foreach(IOConnectionInfo ioc in m_lToDelete) + { + if(IOConnection.FileExists(ioc, false)) + IOConnection.DeleteFile(ioc); + } + + m_lToDelete.Clear(); + } + catch(Exception) { Debug.Assert(false); } + } + + public Stream OpenWrite() + { + if(m_iocBase == null) { Debug.Assert(false); throw new ObjectDisposedException(null); } + + if(!m_bTransacted) m_bMadeUnhidden |= UrlUtil.UnhideFile(m_iocTemp.Path); + + return IOConnection.OpenWrite(m_iocTemp); + } + + public void CommitWrite() + { + if(m_iocBase == null) { Debug.Assert(false); throw new ObjectDisposedException(null); } + + if(!m_bTransacted) + { + if(m_bMadeUnhidden) UrlUtil.HideFile(m_iocTemp.Path, true); + } + else CommitWriteTransaction(); + + m_iocBase = null; // Dispose + } + + private void CommitWriteTransaction() + { + if(g_bExtraSafe) + { + if(!IOConnection.FileExists(m_iocTemp)) + throw new FileNotFoundException(m_iocTemp.Path + + MessageService.NewLine + KLRes.FileSaveFailed); + } + + bool bMadeUnhidden = UrlUtil.UnhideFile(m_iocBase.Path); + +#if !ModernKeePassLib + // 'All' includes 'Audit' (SACL), which requires SeSecurityPrivilege, + // which we usually don't have and therefore get an exception; + // trying to set 'Owner' or 'Group' can result in an + // UnauthorizedAccessException; thus we restore 'Access' (DACL) only + const AccessControlSections acs = AccessControlSections.Access; + +#endif + bool bEfsEncrypted = false; + byte[] pbSec = null; + DateTime? otCreation = null; + + bool bBaseExists = IOConnection.FileExists(m_iocBase); + if(bBaseExists && m_iocBase.IsLocalFile()) + { + // FileAttributes faBase = FileAttributes.Normal; + try + { +#if !ModernKeePassLib + FileAttributes faBase = File.GetAttributes(m_iocBase.Path); + bEfsEncrypted = ((long)(faBase & FileAttributes.Encrypted) != 0); + try { if(bEfsEncrypted) File.Decrypt(m_iocBase.Path); } // For TxF + catch(Exception) { Debug.Assert(false); } +#endif +#if ModernKeePassLib + otCreation = m_iocBase.StorageFile.DateCreated.UtcDateTime; +#else + otCreation = File.GetCreationTimeUtc(m_iocBase.Path); +#endif +#if !ModernKeePassLib + // May throw with Mono + FileSecurity sec = File.GetAccessControl(m_iocBase.Path, acs); + if(sec != null) pbSec = sec.GetSecurityDescriptorBinaryForm(); +#endif + } + catch(Exception) { Debug.Assert(NativeLib.IsUnix()); } + + // if((long)(faBase & FileAttributes.ReadOnly) != 0) + // throw new UnauthorizedAccessException(); + } + + if(!TxfMove()) + { + if(bBaseExists) IOConnection.DeleteFile(m_iocBase); + IOConnection.RenameFile(m_iocTemp, m_iocBase); + } + else { Debug.Assert(pbSec != null); } // TxF success => NTFS => has ACL + + try + { + // If File.GetCreationTimeUtc fails, it may return a + // date with year 1601, and Unix times start in 1970, + // so testing for 1971 should ensure validity; + // https://msdn.microsoft.com/en-us/library/system.io.file.getcreationtimeutc.aspx +#if !ModernKeePassLib + if(otCreation.HasValue && (otCreation.Value.Year >= 1971)) + File.SetCreationTimeUtc(m_iocBase.Path, otCreation.Value); +#endif + +#if !ModernKeePassLib + if(bEfsEncrypted) + { + try { File.Encrypt(m_iocBase.Path); } + catch(Exception) { Debug.Assert(false); } + } + + // File.SetAccessControl(m_iocBase.Path, secPrev); + // Directly calling File.SetAccessControl with the previous + // FileSecurity object does not work; the binary form + // indirection is required; + // https://sourceforge.net/p/keepass/bugs/1738/ + // https://msdn.microsoft.com/en-us/library/system.io.file.setaccesscontrol.aspx + if((pbSec != null) && (pbSec.Length != 0)) + { + FileSecurity sec = new FileSecurity(); + sec.SetSecurityDescriptorBinaryForm(pbSec, acs); + + File.SetAccessControl(m_iocBase.Path, sec); + } +#endif + } + catch(Exception) { Debug.Assert(false); } + + if(bMadeUnhidden) UrlUtil.HideFile(m_iocBase.Path, true); + } + + // For plugins + public static void Configure(string strPrefix, bool? obTransacted) + { + if(string.IsNullOrEmpty(strPrefix)) { Debug.Assert(false); return; } + + if(obTransacted.HasValue) + g_dEnabled[strPrefix] = obTransacted.Value; + else g_dEnabled.Remove(strPrefix); + } + + private static bool TxfIsSupported(char chDriveLetter) + { + if(chDriveLetter == '\0') return false; + +#if ModernKeePassLib + return true; +#else + try + { + string strRoot = (new string(chDriveLetter, 1)) + ":\\"; + + const int cch = NativeMethods.MAX_PATH + 1; + StringBuilder sbName = new StringBuilder(cch + 1); + uint uSerial = 0, cchMaxComp = 0, uFlags = 0; + StringBuilder sbFileSystem = new StringBuilder(cch + 1); + + if(!NativeMethods.GetVolumeInformation(strRoot, sbName, (uint)cch, + ref uSerial, ref cchMaxComp, ref uFlags, sbFileSystem, (uint)cch)) + { + Debug.Assert(false, (new Win32Exception()).Message); + return false; + } + + return ((uFlags & NativeMethods.FILE_SUPPORTS_TRANSACTIONS) != 0); + } + catch(Exception) { Debug.Assert(false); } + + return false; +#endif + } + + private void TxfPrepare() + { + try + { + if(NativeLib.IsUnix()) return; + if(!m_iocBase.IsLocalFile()) return; + + string strID = StrUtil.AlphaNumericOnly(Convert.ToBase64String( + CryptoRandom.Instance.GetRandomBytes(16))); + string strTempDir = UrlUtil.GetTempPath(); + // See also ClearOld method + string strTemp = UrlUtil.EnsureTerminatingSeparator(strTempDir, + false) + StrTxfTempPrefix + strID + StrTxfTempSuffix; + + char chB = UrlUtil.GetDriveLetter(m_iocBase.Path); + char chT = UrlUtil.GetDriveLetter(strTemp); + if(!TxfIsSupported(chB)) return; + if((chT != chB) && !TxfIsSupported(chT)) return; + + m_iocTxfMidFallback = m_iocTemp; +#if ModernKeePassLib + var tempFile = ApplicationData.Current.TemporaryFolder.CreateFileAsync(m_iocTemp.Path).GetAwaiter() + .GetResult(); + m_iocTemp = IOConnectionInfo.FromFile(tempFile); +#else + m_iocTemp = IOConnectionInfo.FromPath(strTemp); +#endif + + m_lToDelete.Add(m_iocTemp); + } + catch(Exception) { Debug.Assert(false); m_iocTxfMidFallback = null; } + } + + private bool TxfMove() + { + if(m_iocTxfMidFallback == null) return false; + + if(TxfMoveWithTx()) return true; + + // Move the temporary file onto the base file's drive first, + // such that it cannot happen that both the base file and + // the temporary file are deleted/corrupted +#if ModernKeePassLib + m_iocTemp.StorageFile = ApplicationData.Current.TemporaryFolder.CreateFileAsync(m_iocTemp.Path).GetAwaiter() + .GetResult(); +#else + const uint f = (NativeMethods.MOVEFILE_COPY_ALLOWED | + NativeMethods.MOVEFILE_REPLACE_EXISTING); + bool b = NativeMethods.MoveFileEx(m_iocTemp.Path, m_iocTxfMidFallback.Path, f); + if(b) b = NativeMethods.MoveFileEx(m_iocTxfMidFallback.Path, m_iocBase.Path, f); + if(!b) throw new Win32Exception(); + + Debug.Assert(!File.Exists(m_iocTemp.Path)); + Debug.Assert(!File.Exists(m_iocTxfMidFallback.Path)); +#endif + return true; + } + + private bool TxfMoveWithTx() + { +#if ModernKeePassLib + return true; +#else + IntPtr hTx = new IntPtr((int)NativeMethods.INVALID_HANDLE_VALUE); + Debug.Assert(hTx.ToInt64() == NativeMethods.INVALID_HANDLE_VALUE); + try + { + string strTx = PwDefs.ShortProductName + " TxF - " + + StrUtil.AlphaNumericOnly(Convert.ToBase64String( + CryptoRandom.Instance.GetRandomBytes(16))); + const int mchTx = NativeMethods.MAX_TRANSACTION_DESCRIPTION_LENGTH; + if(strTx.Length >= mchTx) strTx = strTx.Substring(0, mchTx - 1); + + hTx = NativeMethods.CreateTransaction(IntPtr.Zero, + IntPtr.Zero, 0, 0, 0, 0, strTx); + if(hTx.ToInt64() == NativeMethods.INVALID_HANDLE_VALUE) + { + Debug.Assert(false, (new Win32Exception()).Message); + return false; + } + + if(!NativeMethods.MoveFileTransacted(m_iocTemp.Path, m_iocBase.Path, + IntPtr.Zero, IntPtr.Zero, (NativeMethods.MOVEFILE_COPY_ALLOWED | + NativeMethods.MOVEFILE_REPLACE_EXISTING), hTx)) + { + Debug.Assert(false, (new Win32Exception()).Message); + return false; + } + + if(!NativeMethods.CommitTransaction(hTx)) + { + Debug.Assert(false, (new Win32Exception()).Message); + return false; + } + + Debug.Assert(!File.Exists(m_iocTemp.Path)); + return true; + } + catch(Exception) { Debug.Assert(false); } + finally + { + if(hTx.ToInt64() != NativeMethods.INVALID_HANDLE_VALUE) + { + try { if(!NativeMethods.CloseHandle(hTx)) { Debug.Assert(false); } } + catch(Exception) { Debug.Assert(false); } + } + } + + return false; +#endif + } + + internal static void ClearOld() + { + try + { +#if ModernKeePassLib + ApplicationData.Current.TemporaryFolder.GetFileAsync(UrlUtil.GetTempPath()).GetAwaiter() + .GetResult().DeleteAsync().GetAwaiter().GetResult(); +#else + // See also TxfPrepare method + DirectoryInfo di = new DirectoryInfo(UrlUtil.GetTempPath()); + List l = UrlUtil.GetFileInfos(di, StrTxfTempPrefix + + "*" + StrTxfTempSuffix, SearchOption.TopDirectoryOnly); + + foreach(FileInfo fi in l) + { + if(fi == null) { Debug.Assert(false); continue; } + if(!fi.Name.StartsWith(StrTxfTempPrefix, StrUtil.CaseIgnoreCmp) || + !fi.Name.EndsWith(StrTxfTempSuffix, StrUtil.CaseIgnoreCmp)) + continue; + + if((DateTime.UtcNow - fi.LastWriteTimeUtc).TotalDays > 1.0) + fi.Delete(); + } +#endif + } + catch(Exception) { Debug.Assert(false); } + } + } +} diff --git a/ModernKeePassLib/Serialization/HashedBlockStream.cs b/ModernKeePassLib/Serialization/HashedBlockStream.cs new file mode 100644 index 0000000..e856f40 --- /dev/null +++ b/ModernKeePassLib/Serialization/HashedBlockStream.cs @@ -0,0 +1,312 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Diagnostics; +using System.IO; +using System.Text; + +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Native; +using ModernKeePassLib.Utility; + +#if KeePassLibSD +using KeePassLibSD; +#endif + +namespace ModernKeePassLib.Serialization +{ + public sealed class HashedBlockStream : Stream + { + private const int NbDefaultBufferSize = 1024 * 1024; // 1 MB + + private Stream m_sBaseStream; + private bool m_bWriting; + private bool m_bVerify; + private bool m_bEos = false; + + private BinaryReader m_brInput; + private BinaryWriter m_bwOutput; + + private byte[] m_pbBuffer; + private int m_nBufferPos = 0; + + private uint m_uBlockIndex = 0; + + public override bool CanRead + { + get { return !m_bWriting; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return m_bWriting; } + } + + public override long Length + { + get { Debug.Assert(false); throw new NotSupportedException(); } + } + + public override long Position + { + get { Debug.Assert(false); throw new NotSupportedException(); } + set { Debug.Assert(false); throw new NotSupportedException(); } + } + + public HashedBlockStream(Stream sBaseStream, bool bWriting) + { + Initialize(sBaseStream, bWriting, 0, true); + } + + public HashedBlockStream(Stream sBaseStream, bool bWriting, int nBufferSize) + { + Initialize(sBaseStream, bWriting, nBufferSize, true); + } + + public HashedBlockStream(Stream sBaseStream, bool bWriting, int nBufferSize, + bool bVerify) + { + Initialize(sBaseStream, bWriting, nBufferSize, bVerify); + } + + private void Initialize(Stream sBaseStream, bool bWriting, int nBufferSize, + bool bVerify) + { + if(sBaseStream == null) throw new ArgumentNullException("sBaseStream"); + if(nBufferSize < 0) throw new ArgumentOutOfRangeException("nBufferSize"); + + if(nBufferSize == 0) nBufferSize = NbDefaultBufferSize; + + m_sBaseStream = sBaseStream; + m_bWriting = bWriting; + m_bVerify = bVerify; + + UTF8Encoding utf8 = StrUtil.Utf8; + if(!m_bWriting) // Reading mode + { + if(!m_sBaseStream.CanRead) + throw new InvalidOperationException(); + + m_brInput = new BinaryReader(sBaseStream, utf8); + + m_pbBuffer = MemUtil.EmptyByteArray; + } + else // Writing mode + { + if(!m_sBaseStream.CanWrite) + throw new InvalidOperationException(); + + m_bwOutput = new BinaryWriter(sBaseStream, utf8); + + m_pbBuffer = new byte[nBufferSize]; + } + } + + protected override void Dispose(bool disposing) + { + if(disposing && (m_sBaseStream != null)) + { + if(!m_bWriting) // Reading mode + { + m_brInput.Dispose(); + m_brInput = null; + } + else // Writing mode + { + if(m_nBufferPos == 0) // No data left in buffer + WriteHashedBlock(); // Write terminating block + else + { + WriteHashedBlock(); // Write remaining buffered data + WriteHashedBlock(); // Write terminating block + } + + Flush(); + m_bwOutput.Dispose(); + m_bwOutput = null; + } + + m_sBaseStream.Dispose(); + m_sBaseStream = null; + } + + base.Dispose(disposing); + } + + public override void Flush() + { + if(m_bWriting) m_bwOutput.Flush(); + } + + public override long Seek(long lOffset, SeekOrigin soOrigin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long lValue) + { + throw new NotSupportedException(); + } + + public override int Read(byte[] pbBuffer, int nOffset, int nCount) + { + if(m_bWriting) throw new InvalidOperationException(); + + int nRemaining = nCount; + while(nRemaining > 0) + { + if(m_nBufferPos == m_pbBuffer.Length) + { + if(ReadHashedBlock() == false) + return (nCount - nRemaining); // Bytes actually read + } + + int nCopy = Math.Min(m_pbBuffer.Length - m_nBufferPos, nRemaining); + + Array.Copy(m_pbBuffer, m_nBufferPos, pbBuffer, nOffset, nCopy); + + nOffset += nCopy; + m_nBufferPos += nCopy; + + nRemaining -= nCopy; + } + + return nCount; + } + + private bool ReadHashedBlock() + { + if(m_bEos) return false; // End of stream reached already + + m_nBufferPos = 0; + + if(m_brInput.ReadUInt32() != m_uBlockIndex) + throw new InvalidDataException(); + ++m_uBlockIndex; + + byte[] pbStoredHash = m_brInput.ReadBytes(32); + if((pbStoredHash == null) || (pbStoredHash.Length != 32)) + throw new InvalidDataException(); + + int nBufferSize = 0; + try { nBufferSize = m_brInput.ReadInt32(); } + catch(NullReferenceException) // Mono bug workaround (LaunchPad 783268) + { + if(!NativeLib.IsUnix()) throw; + } + + if(nBufferSize < 0) + throw new InvalidDataException(); + + if(nBufferSize == 0) + { + for(int iHash = 0; iHash < 32; ++iHash) + { + if(pbStoredHash[iHash] != 0) + throw new InvalidDataException(); + } + + m_bEos = true; + m_pbBuffer = MemUtil.EmptyByteArray; + return false; + } + + m_pbBuffer = m_brInput.ReadBytes(nBufferSize); + if((m_pbBuffer == null) || ((m_pbBuffer.Length != nBufferSize) && m_bVerify)) + throw new InvalidDataException(); + + if(m_bVerify) + { + byte[] pbComputedHash = CryptoUtil.HashSha256(m_pbBuffer); + if((pbComputedHash == null) || (pbComputedHash.Length != 32)) + throw new InvalidOperationException(); + + if(!MemUtil.ArraysEqual(pbStoredHash, pbComputedHash)) + throw new InvalidDataException(); + } + + return true; + } + + public override void Write(byte[] pbBuffer, int nOffset, int nCount) + { + if(!m_bWriting) throw new InvalidOperationException(); + + while(nCount > 0) + { + if(m_nBufferPos == m_pbBuffer.Length) + WriteHashedBlock(); + + int nCopy = Math.Min(m_pbBuffer.Length - m_nBufferPos, nCount); + + Array.Copy(pbBuffer, nOffset, m_pbBuffer, m_nBufferPos, nCopy); + + nOffset += nCopy; + m_nBufferPos += nCopy; + + nCount -= nCopy; + } + } + + private void WriteHashedBlock() + { + m_bwOutput.Write(m_uBlockIndex); + ++m_uBlockIndex; + + if(m_nBufferPos > 0) + { + byte[] pbHash = CryptoUtil.HashSha256(m_pbBuffer, 0, m_nBufferPos); + + // For KeePassLibSD: + // SHA256Managed sha256 = new SHA256Managed(); + // byte[] pbHash; + // if(m_nBufferPos == m_pbBuffer.Length) + // pbHash = sha256.ComputeHash(m_pbBuffer); + // else + // { + // byte[] pbData = new byte[m_nBufferPos]; + // Array.Copy(m_pbBuffer, 0, pbData, 0, m_nBufferPos); + // pbHash = sha256.ComputeHash(pbData); + // } + + m_bwOutput.Write(pbHash); + } + else + { + m_bwOutput.Write((ulong)0); // Zero hash + m_bwOutput.Write((ulong)0); + m_bwOutput.Write((ulong)0); + m_bwOutput.Write((ulong)0); + } + + m_bwOutput.Write(m_nBufferPos); + + if(m_nBufferPos > 0) + m_bwOutput.Write(m_pbBuffer, 0, m_nBufferPos); + + m_nBufferPos = 0; + } + } +} diff --git a/ModernKeePassLib/Serialization/HmacBlockStream.cs b/ModernKeePassLib/Serialization/HmacBlockStream.cs new file mode 100644 index 0000000..b95378f --- /dev/null +++ b/ModernKeePassLib/Serialization/HmacBlockStream.cs @@ -0,0 +1,324 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; + +#if !KeePassUAP +using System.Security.Cryptography; +#endif + +using ModernKeePassLib.Resources; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Serialization +{ + public sealed class HmacBlockStream : Stream + { + private const int NbDefaultBufferSize = 1024 * 1024; // 1 MB + + private Stream m_sBase; + private readonly bool m_bWriting; + private readonly bool m_bVerify; + private byte[] m_pbKey; + + private bool m_bEos = false; + private byte[] m_pbBuffer; + private int m_iBufferPos = 0; + + private ulong m_uBlockIndex = 0; + + public override bool CanRead + { + get { return !m_bWriting; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return m_bWriting; } + } + + public override long Length + { + get { Debug.Assert(false); throw new NotSupportedException(); } + } + + public override long Position + { + get { Debug.Assert(false); throw new NotSupportedException(); } + set { Debug.Assert(false); throw new NotSupportedException(); } + } + + public HmacBlockStream(Stream sBase, bool bWriting, bool bVerify, + byte[] pbKey) + { + if(sBase == null) throw new ArgumentNullException("sBase"); + if(pbKey == null) throw new ArgumentNullException("pbKey"); + + m_sBase = sBase; + m_bWriting = bWriting; + m_bVerify = bVerify; + m_pbKey = pbKey; + + if(!m_bWriting) // Reading mode + { + if(!m_sBase.CanRead) throw new InvalidOperationException(); + + m_pbBuffer = MemUtil.EmptyByteArray; + } + else // Writing mode + { + if(!m_sBase.CanWrite) throw new InvalidOperationException(); + + m_pbBuffer = new byte[NbDefaultBufferSize]; + } + } + + protected override void Dispose(bool disposing) + { + if(disposing && (m_sBase != null)) + { + if(m_bWriting) + { + if(m_iBufferPos == 0) // No data left in buffer + WriteSafeBlock(); // Write terminating block + else + { + WriteSafeBlock(); // Write remaining buffered data + WriteSafeBlock(); // Write terminating block + } + + Flush(); + } + + m_sBase.Dispose(); + m_sBase = null; + } + + base.Dispose(disposing); + } + + public override void Flush() + { + Debug.Assert(m_sBase != null); // Object should not be disposed + if(m_bWriting && (m_sBase != null)) m_sBase.Flush(); + } + + public override long Seek(long lOffset, SeekOrigin soOrigin) + { + Debug.Assert(false); + throw new NotSupportedException(); + } + + public override void SetLength(long lValue) + { + Debug.Assert(false); + throw new NotSupportedException(); + } + + internal static byte[] GetHmacKey64(byte[] pbKey, ulong uBlockIndex) + { + if(pbKey == null) throw new ArgumentNullException("pbKey"); + Debug.Assert(pbKey.Length == 64); + + // We are computing the HMAC using SHA-256, whose internal + // block size is 512 bits; thus create a key that is 512 + // bits long (using SHA-512) + + byte[] pbBlockKey; + using(SHA512Managed h = new SHA512Managed()) + { + byte[] pbIndex = MemUtil.UInt64ToBytes(uBlockIndex); + + h.TransformBlock(pbIndex, 0, pbIndex.Length, pbIndex, 0); + h.TransformBlock(pbKey, 0, pbKey.Length, pbKey, 0); + h.TransformFinalBlock(MemUtil.EmptyByteArray, 0, 0); + + pbBlockKey = h.Hash; + } + +#if DEBUG + byte[] pbZero = new byte[64]; + Debug.Assert((pbBlockKey.Length == 64) && !MemUtil.ArraysEqual( + pbBlockKey, pbZero)); // Ensure we own pbBlockKey +#endif + return pbBlockKey; + } + + public override int Read(byte[] pbBuffer, int iOffset, int nCount) + { + if(m_bWriting) throw new InvalidOperationException(); + + int nRemaining = nCount; + while(nRemaining > 0) + { + if(m_iBufferPos == m_pbBuffer.Length) + { + if(!ReadSafeBlock()) + return (nCount - nRemaining); // Bytes actually read + } + + int nCopy = Math.Min(m_pbBuffer.Length - m_iBufferPos, nRemaining); + Debug.Assert(nCopy > 0); + + Array.Copy(m_pbBuffer, m_iBufferPos, pbBuffer, iOffset, nCopy); + + iOffset += nCopy; + m_iBufferPos += nCopy; + + nRemaining -= nCopy; + } + + return nCount; + } + + private bool ReadSafeBlock() + { + if(m_bEos) return false; // End of stream reached already + + byte[] pbStoredHmac = MemUtil.Read(m_sBase, 32); + if((pbStoredHmac == null) || (pbStoredHmac.Length != 32)) + throw new EndOfStreamException(KLRes.FileCorrupted + " " + + KLRes.FileIncomplete); + + // Block index is implicit: it's used in the HMAC computation, + // but does not need to be stored + // byte[] pbBlockIndex = MemUtil.Read(m_sBase, 8); + // if((pbBlockIndex == null) || (pbBlockIndex.Length != 8)) + // throw new EndOfStreamException(); + // ulong uBlockIndex = MemUtil.BytesToUInt64(pbBlockIndex); + // if((uBlockIndex != m_uBlockIndex) && m_bVerify) + // throw new InvalidDataException(); + byte[] pbBlockIndex = MemUtil.UInt64ToBytes(m_uBlockIndex); + + byte[] pbBlockSize = MemUtil.Read(m_sBase, 4); + if((pbBlockSize == null) || (pbBlockSize.Length != 4)) + throw new EndOfStreamException(KLRes.FileCorrupted + " " + + KLRes.FileIncomplete); + int nBlockSize = MemUtil.BytesToInt32(pbBlockSize); + if(nBlockSize < 0) + throw new InvalidDataException(KLRes.FileCorrupted); + + m_iBufferPos = 0; + + m_pbBuffer = MemUtil.Read(m_sBase, nBlockSize); + if((m_pbBuffer == null) || ((m_pbBuffer.Length != nBlockSize) && m_bVerify)) + throw new EndOfStreamException(KLRes.FileCorrupted + " " + + KLRes.FileIncompleteExpc); + + if(m_bVerify) + { + byte[] pbCmpHmac; + byte[] pbBlockKey = GetHmacKey64(m_pbKey, m_uBlockIndex); + using(HMACSHA256 h = new HMACSHA256(pbBlockKey)) + { + h.TransformBlock(pbBlockIndex, 0, pbBlockIndex.Length, + pbBlockIndex, 0); + h.TransformBlock(pbBlockSize, 0, pbBlockSize.Length, + pbBlockSize, 0); + + if(m_pbBuffer.Length > 0) + h.TransformBlock(m_pbBuffer, 0, m_pbBuffer.Length, + m_pbBuffer, 0); + + h.TransformFinalBlock(MemUtil.EmptyByteArray, 0, 0); + + pbCmpHmac = h.Hash; + } + MemUtil.ZeroByteArray(pbBlockKey); + + if(!MemUtil.ArraysEqual(pbCmpHmac, pbStoredHmac)) + throw new InvalidDataException(KLRes.FileCorrupted); + } + + ++m_uBlockIndex; + + if(nBlockSize == 0) + { + m_bEos = true; + return false; // No further data available + } + return true; + } + + public override void Write(byte[] pbBuffer, int iOffset, int nCount) + { + if(!m_bWriting) throw new InvalidOperationException(); + + while(nCount > 0) + { + if(m_iBufferPos == m_pbBuffer.Length) + WriteSafeBlock(); + + int nCopy = Math.Min(m_pbBuffer.Length - m_iBufferPos, nCount); + Debug.Assert(nCopy > 0); + + Array.Copy(pbBuffer, iOffset, m_pbBuffer, m_iBufferPos, nCopy); + + iOffset += nCopy; + m_iBufferPos += nCopy; + + nCount -= nCopy; + } + } + + private void WriteSafeBlock() + { + byte[] pbBlockIndex = MemUtil.UInt64ToBytes(m_uBlockIndex); + + int cbBlockSize = m_iBufferPos; + byte[] pbBlockSize = MemUtil.Int32ToBytes(cbBlockSize); + + byte[] pbBlockHmac; + byte[] pbBlockKey = GetHmacKey64(m_pbKey, m_uBlockIndex); + using(HMACSHA256 h = new HMACSHA256(pbBlockKey)) + { + h.TransformBlock(pbBlockIndex, 0, pbBlockIndex.Length, + pbBlockIndex, 0); + h.TransformBlock(pbBlockSize, 0, pbBlockSize.Length, + pbBlockSize, 0); + + if(cbBlockSize > 0) + h.TransformBlock(m_pbBuffer, 0, cbBlockSize, m_pbBuffer, 0); + + h.TransformFinalBlock(MemUtil.EmptyByteArray, 0, 0); + + pbBlockHmac = h.Hash; + } + MemUtil.ZeroByteArray(pbBlockKey); + + MemUtil.Write(m_sBase, pbBlockHmac); + // MemUtil.Write(m_sBase, pbBlockIndex); // Implicit + MemUtil.Write(m_sBase, pbBlockSize); + if(cbBlockSize > 0) + m_sBase.Write(m_pbBuffer, 0, cbBlockSize); + + ++m_uBlockIndex; + m_iBufferPos = 0; + } + } +} diff --git a/ModernKeePassLib/Serialization/IOConnection.cs b/ModernKeePassLib/Serialization/IOConnection.cs new file mode 100644 index 0000000..7d20259 --- /dev/null +++ b/ModernKeePassLib/Serialization/IOConnection.cs @@ -0,0 +1,926 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Net; +using System.Reflection; +using System.Text; + +#if (!ModernKeePassLib && !KeePassLibSD && !KeePassUAP) +using System.Net.Cache; +using System.Net.Security; +#endif + +#if !ModernKeePassLib && !KeePassUAP +using System.Security.Cryptography.X509Certificates; +#endif + +#if ModernKeePassLib +using Windows.Storage; +using Windows.Storage.Streams; +#endif +using ModernKeePassLib.Native; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Serialization +{ +#if (!ModernKeePassLib && !KeePassLibSD && !KeePassRT) + internal sealed class IOWebClient : WebClient + { + private IOConnectionInfo m_ioc; + + public IOWebClient(IOConnectionInfo ioc) : base() + { + m_ioc = ioc; + } + + protected override WebRequest GetWebRequest(Uri address) + { + WebRequest request = base.GetWebRequest(address); + IOConnection.ConfigureWebRequest(request, m_ioc); + return request; + } + } +#endif + +#if !ModernKeePassLib + internal abstract class WrapperStream : Stream + { + private readonly Stream m_s; + protected Stream BaseStream + { + get { return m_s; } + } + + public override bool CanRead + { + get { return m_s.CanRead; } + } + + public override bool CanSeek + { + get { return m_s.CanSeek; } + } + + public override bool CanTimeout + { + get { return m_s.CanTimeout; } + } + + public override bool CanWrite + { + get { return m_s.CanWrite; } + } + + public override long Length + { + get { return m_s.Length; } + } + + public override long Position + { + get { return m_s.Position; } + set { m_s.Position = value; } + } + + public override int ReadTimeout + { + get { return m_s.ReadTimeout; } + set { m_s.ReadTimeout = value; } + } + + public override int WriteTimeout + { + get { return m_s.WriteTimeout; } + set { m_s.WriteTimeout = value; } + } + + public WrapperStream(Stream sBase) : base() + { + if(sBase == null) throw new ArgumentNullException("sBase"); + + m_s = sBase; + } + +#if !KeePassUAP + public override IAsyncResult BeginRead(byte[] buffer, int offset, + int count, AsyncCallback callback, object state) + { + return m_s.BeginRead(buffer, offset, count, callback, state); + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, + int count, AsyncCallback callback, object state) + { + return BeginWrite(buffer, offset, count, callback, state); + } +#endif + + protected override void Dispose(bool disposing) + { + if(disposing) m_s.Dispose(); + + base.Dispose(disposing); + } + +#if !KeePassUAP + public override int EndRead(IAsyncResult asyncResult) + { + return m_s.EndRead(asyncResult); + } + + public override void EndWrite(IAsyncResult asyncResult) + { + m_s.EndWrite(asyncResult); + } +#endif + + public override void Flush() + { + m_s.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return m_s.Read(buffer, offset, count); + } + + public override int ReadByte() + { + return m_s.ReadByte(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return m_s.Seek(offset, origin); + } + + public override void SetLength(long value) + { + m_s.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + m_s.Write(buffer, offset, count); + } + + public override void WriteByte(byte value) + { + m_s.WriteByte(value); + } + } + + internal sealed class IocStream : WrapperStream + { + private readonly bool m_bWrite; // Initially opened for writing + private bool m_bDisposed = false; + + public IocStream(Stream sBase) : base(sBase) + { + m_bWrite = sBase.CanWrite; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if(disposing && MonoWorkarounds.IsRequired(10163) && m_bWrite && + !m_bDisposed) + { + try + { + Stream s = this.BaseStream; + Type t = s.GetType(); + if(t.Name == "WebConnectionStream") + { + PropertyInfo pi = t.GetProperty("Request", + BindingFlags.Instance | BindingFlags.NonPublic); + if(pi != null) + { + WebRequest wr = (pi.GetValue(s, null) as WebRequest); + if(wr != null) + IOConnection.DisposeResponse(wr.GetResponse(), false); + else { Debug.Assert(false); } + } + else { Debug.Assert(false); } + } + } + catch(Exception) { Debug.Assert(false); } + } + + m_bDisposed = true; + } + + public static Stream WrapIfRequired(Stream s) + { + if(s == null) { Debug.Assert(false); return null; } + + if(MonoWorkarounds.IsRequired(10163) && s.CanWrite) + return new IocStream(s); + + return s; + } + } +#endif + + public static class IOConnection + { +#if (!ModernKeePassLib && !KeePassLibSD && !KeePassRT) + private static ProxyServerType m_pstProxyType = ProxyServerType.System; + private static string m_strProxyAddr = string.Empty; + private static string m_strProxyPort = string.Empty; + private static ProxyAuthType m_patProxyAuthType = ProxyAuthType.Auto; + private static string m_strProxyUserName = string.Empty; + private static string m_strProxyPassword = string.Empty; + +#if !KeePassUAP + private static bool? m_obDefaultExpect100Continue = null; + + private static bool m_bSslCertsAcceptInvalid = false; + internal static bool SslCertsAcceptInvalid + { + // get { return m_bSslCertsAcceptInvalid; } + set { m_bSslCertsAcceptInvalid = value; } + } +#endif +#endif + + // Web request methods + public static readonly string WrmDeleteFile = "DELETEFILE"; + public static readonly string WrmMoveFile = "MOVEFILE"; + + // Web request headers + public static readonly string WrhMoveFileTo = "MoveFileTo"; + + public static event EventHandler IOAccessPre; + +#if (!ModernKeePassLib && !KeePassLibSD && !KeePassRT) + // Allow self-signed certificates, expired certificates, etc. + private static bool AcceptCertificate(object sender, + X509Certificate certificate, X509Chain chain, + SslPolicyErrors sslPolicyErrors) + { + return true; + } + + internal static void SetProxy(ProxyServerType pst, string strAddr, + string strPort, ProxyAuthType pat, string strUserName, + string strPassword) + { + m_pstProxyType = pst; + m_strProxyAddr = (strAddr ?? string.Empty); + m_strProxyPort = (strPort ?? string.Empty); + m_patProxyAuthType = pat; + m_strProxyUserName = (strUserName ?? string.Empty); + m_strProxyPassword = (strPassword ?? string.Empty); + } + + internal static void ConfigureWebRequest(WebRequest request, + IOConnectionInfo ioc) + { + if(request == null) { Debug.Assert(false); return; } // No throw + + IocProperties p = ((ioc != null) ? ioc.Properties : null); + if(p == null) { Debug.Assert(false); p = new IocProperties(); } + + IHasIocProperties ihpReq = (request as IHasIocProperties); + if(ihpReq != null) + { + IocProperties pEx = ihpReq.IOConnectionProperties; + if(pEx != null) p.CopyTo(pEx); + else ihpReq.IOConnectionProperties = p.CloneDeep(); + } + + if(IsHttpWebRequest(request)) + { + // WebDAV support +#if !KeePassUAP + request.PreAuthenticate = true; // Also auth GET +#endif + if(string.Equals(request.Method, WebRequestMethods.Http.Post, + StrUtil.CaseIgnoreCmp)) + request.Method = WebRequestMethods.Http.Put; + +#if !KeePassUAP + HttpWebRequest hwr = (request as HttpWebRequest); + if(hwr != null) + { + string strUA = p.Get(IocKnownProperties.UserAgent); + if(!string.IsNullOrEmpty(strUA)) hwr.UserAgent = strUA; + } + else { Debug.Assert(false); } +#endif + } +#if !KeePassUAP + else if(IsFtpWebRequest(request)) + { + FtpWebRequest fwr = (request as FtpWebRequest); + if(fwr != null) + { + bool? obPassive = p.GetBool(IocKnownProperties.Passive); + if(obPassive.HasValue) fwr.UsePassive = obPassive.Value; + } + else { Debug.Assert(false); } + } +#endif + +#if !KeePassUAP + // Not implemented and ignored in Mono < 2.10 + try + { + request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore); + } + catch(NotImplementedException) { } + catch(Exception) { Debug.Assert(false); } +#endif + + try + { + IWebProxy prx; + if(GetWebProxy(out prx)) request.Proxy = prx; + } + catch(Exception) { Debug.Assert(false); } + +#if !KeePassUAP + long? olTimeout = p.GetLong(IocKnownProperties.Timeout); + if(olTimeout.HasValue && (olTimeout.Value >= 0)) + request.Timeout = (int)Math.Min(olTimeout.Value, (long)int.MaxValue); + + bool? ob = p.GetBool(IocKnownProperties.PreAuth); + if(ob.HasValue) request.PreAuthenticate = ob.Value; +#endif + } + + internal static void ConfigureWebClient(WebClient wc) + { +#if !KeePassUAP + // Not implemented and ignored in Mono < 2.10 + try + { + wc.CachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore); + } + catch(NotImplementedException) { } + catch(Exception) { Debug.Assert(false); } +#endif + + try + { + IWebProxy prx; + if(GetWebProxy(out prx)) wc.Proxy = prx; + } + catch(Exception) { Debug.Assert(false); } + } + + private static bool GetWebProxy(out IWebProxy prx) + { + bool b = GetWebProxyServer(out prx); + if(b) AssignCredentials(prx); + return b; + } + + private static bool GetWebProxyServer(out IWebProxy prx) + { + prx = null; + + if(m_pstProxyType == ProxyServerType.None) + return true; // Use null proxy + + if(m_pstProxyType == ProxyServerType.Manual) + { + try + { + if(m_strProxyAddr.Length == 0) + { + // First try default (from config), then system + prx = WebRequest.DefaultWebProxy; +#if !KeePassUAP + if(prx == null) prx = WebRequest.GetSystemWebProxy(); +#endif + } + else if(m_strProxyPort.Length > 0) + prx = new WebProxy(m_strProxyAddr, int.Parse(m_strProxyPort)); + else prx = new WebProxy(m_strProxyAddr); + + return (prx != null); + } +#if KeePassUAP + catch(Exception) { Debug.Assert(false); } +#else + catch(Exception ex) + { + string strInfo = m_strProxyAddr; + if(m_strProxyPort.Length > 0) + strInfo += ":" + m_strProxyPort; + MessageService.ShowWarning(strInfo, ex); + } +#endif + + return false; // Use default + } + + Debug.Assert(m_pstProxyType == ProxyServerType.System); + try + { + // First try system, then default (from config) +#if !KeePassUAP + prx = WebRequest.GetSystemWebProxy(); +#endif + if(prx == null) prx = WebRequest.DefaultWebProxy; + + return (prx != null); + } + catch(Exception) { Debug.Assert(false); } + + return false; + } + + private static void AssignCredentials(IWebProxy prx) + { + if(prx == null) return; // No assert + + string strUserName = m_strProxyUserName; + string strPassword = m_strProxyPassword; + + ProxyAuthType pat = m_patProxyAuthType; + if(pat == ProxyAuthType.Auto) + { + if((strUserName.Length > 0) || (strPassword.Length > 0)) + pat = ProxyAuthType.Manual; + else pat = ProxyAuthType.Default; + } + + try + { + if(pat == ProxyAuthType.None) + prx.Credentials = null; + else if(pat == ProxyAuthType.Default) + prx.Credentials = CredentialCache.DefaultCredentials; + else if(pat == ProxyAuthType.Manual) + { + if((strUserName.Length > 0) || (strPassword.Length > 0)) + prx.Credentials = new NetworkCredential( + strUserName, strPassword); + } + else { Debug.Assert(false); } + } + catch(Exception) { Debug.Assert(false); } + } + + private static void PrepareWebAccess(IOConnectionInfo ioc) + { +#if !KeePassUAP + IocProperties p = ((ioc != null) ? ioc.Properties : null); + if(p == null) { Debug.Assert(false); p = new IocProperties(); } + + try + { + if(m_bSslCertsAcceptInvalid) + ServicePointManager.ServerCertificateValidationCallback = + IOConnection.AcceptCertificate; + else + ServicePointManager.ServerCertificateValidationCallback = null; + } + catch(Exception) { Debug.Assert(false); } + + try + { + SecurityProtocolType spt = (SecurityProtocolType.Ssl3 | + SecurityProtocolType.Tls); + + // The flags Tls11 and Tls12 in SecurityProtocolType have been + // introduced in .NET 4.5 and must not be set when running under + // older .NET versions (otherwise an exception is thrown) + Type tSpt = typeof(SecurityProtocolType); + string[] vSpt = Enum.GetNames(tSpt); + foreach(string strSpt in vSpt) + { + if(strSpt.Equals("Tls11", StrUtil.CaseIgnoreCmp)) + spt |= (SecurityProtocolType)Enum.Parse(tSpt, "Tls11", true); + else if(strSpt.Equals("Tls12", StrUtil.CaseIgnoreCmp)) + spt |= (SecurityProtocolType)Enum.Parse(tSpt, "Tls12", true); + } + + ServicePointManager.SecurityProtocol = spt; + } + catch(Exception) { Debug.Assert(false); } + + try + { + bool bCurCont = ServicePointManager.Expect100Continue; + if(!m_obDefaultExpect100Continue.HasValue) + { + Debug.Assert(bCurCont); // Default should be true + m_obDefaultExpect100Continue = bCurCont; + } + + bool bNewCont = m_obDefaultExpect100Continue.Value; + bool? ob = p.GetBool(IocKnownProperties.Expect100Continue); + if(ob.HasValue) bNewCont = ob.Value; + + if(bNewCont != bCurCont) + ServicePointManager.Expect100Continue = bNewCont; + } + catch(Exception) { Debug.Assert(false); } +#endif + } + + private static IOWebClient CreateWebClient(IOConnectionInfo ioc) + { + PrepareWebAccess(ioc); + + IOWebClient wc = new IOWebClient(ioc); + ConfigureWebClient(wc); + + if((ioc.UserName.Length > 0) || (ioc.Password.Length > 0)) + wc.Credentials = new NetworkCredential(ioc.UserName, ioc.Password); + else if(NativeLib.IsUnix()) // Mono requires credentials + wc.Credentials = new NetworkCredential("anonymous", string.Empty); + + return wc; + } + + private static WebRequest CreateWebRequest(IOConnectionInfo ioc) + { + PrepareWebAccess(ioc); + + WebRequest req = WebRequest.Create(ioc.Path); + ConfigureWebRequest(req, ioc); + + if((ioc.UserName.Length > 0) || (ioc.Password.Length > 0)) + req.Credentials = new NetworkCredential(ioc.UserName, ioc.Password); + else if(NativeLib.IsUnix()) // Mono requires credentials + req.Credentials = new NetworkCredential("anonymous", string.Empty); + + return req; + } + + public static Stream OpenRead(IOConnectionInfo ioc) + { + RaiseIOAccessPreEvent(ioc, IOAccessType.Read); + + if(StrUtil.IsDataUri(ioc.Path)) + { + byte[] pbData = StrUtil.DataUriToData(ioc.Path); + if(pbData != null) return new MemoryStream(pbData, false); + } + + if(ioc.IsLocalFile()) return OpenReadLocal(ioc); + + return IocStream.WrapIfRequired(CreateWebClient(ioc).OpenRead( + new Uri(ioc.Path))); + } +#else + public static Stream OpenRead(IOConnectionInfo ioc) + { + RaiseIOAccessPreEvent(ioc, IOAccessType.Read); + + return OpenReadLocal(ioc); + } +#endif + + private static Stream OpenReadLocal(IOConnectionInfo ioc) + { + return ioc.StorageFile.OpenAsync(FileAccessMode.Read).GetAwaiter().GetResult().AsStream(); + } + +#if (!ModernKeePassLib && !KeePassLibSD && !KeePassRT) + public static Stream OpenWrite(IOConnectionInfo ioc) + { + if(ioc == null) { Debug.Assert(false); return null; } + + RaiseIOAccessPreEvent(ioc, IOAccessType.Write); + + if(ioc.IsLocalFile()) return OpenWriteLocal(ioc); + + Uri uri = new Uri(ioc.Path); + Stream s; + + // Mono does not set HttpWebRequest.Method to POST for writes, + // so one needs to set the method to PUT explicitly + if(NativeLib.IsUnix() && IsHttpWebRequest(uri)) + s = CreateWebClient(ioc).OpenWrite(uri, WebRequestMethods.Http.Put); + else s = CreateWebClient(ioc).OpenWrite(uri); + + return IocStream.WrapIfRequired(s); + } +#else + public static Stream OpenWrite(IOConnectionInfo ioc) + { + RaiseIOAccessPreEvent(ioc, IOAccessType.Write); + + return OpenWriteLocal(ioc); + } +#endif + + private static Stream OpenWriteLocal(IOConnectionInfo ioc) + { +#if ModernKeePassLib + return ioc.StorageFile.OpenAsync(FileAccessMode.ReadWrite).GetAwaiter().GetResult().AsStream(); +#else + return new FileStream(ioc.Path, FileMode.Create, FileAccess.Write, + FileShare.None); +#endif + } + + public static bool FileExists(IOConnectionInfo ioc) + { + return FileExists(ioc, false); + } + + public static bool FileExists(IOConnectionInfo ioc, bool bThrowErrors) + { + if(ioc == null) { Debug.Assert(false); return false; } + + RaiseIOAccessPreEvent(ioc, IOAccessType.Exists); + +#if ModernKeePassLib + return ioc.StorageFile.IsAvailable; +#else + if(ioc.IsLocalFile()) return File.Exists(ioc.Path); + +#if !KeePassLibSD + if(ioc.Path.StartsWith("ftp://", StrUtil.CaseIgnoreCmp)) + { + bool b = SendCommand(ioc, WebRequestMethods.Ftp.GetDateTimestamp); + if(!b && bThrowErrors) throw new InvalidOperationException(); + return b; + } +#endif + + try + { + Stream s = OpenRead(ioc); + if(s == null) throw new FileNotFoundException(); + + try { s.ReadByte(); } + catch(Exception) { } + + // We didn't download the file completely; close may throw + // an exception -- that's okay + try { s.Close(); } + catch(Exception) { } + } + catch(Exception) + { + if(bThrowErrors) throw; + return false; + } + + return true; +#endif + } + + public static void DeleteFile(IOConnectionInfo ioc) + { + RaiseIOAccessPreEvent(ioc, IOAccessType.Delete); + +#if ModernKeePassLib + if (!ioc.IsLocalFile()) return; + ioc.StorageFile?.DeleteAsync().GetAwaiter().GetResult(); +#else + if(ioc.IsLocalFile()) { File.Delete(ioc.Path); return; } + +#if !KeePassLibSD + WebRequest req = CreateWebRequest(ioc); + if(req != null) + { + if(IsHttpWebRequest(req)) req.Method = "DELETE"; + else if(IsFtpWebRequest(req)) + req.Method = WebRequestMethods.Ftp.DeleteFile; + else if(IsFileWebRequest(req)) + { + File.Delete(UrlUtil.FileUrlToPath(ioc.Path)); + return; + } + else req.Method = WrmDeleteFile; + + DisposeResponse(req.GetResponse(), true); + } +#endif +#endif + } + + /// + /// Rename/move a file. For local file system and WebDAV, the + /// specified file is moved, i.e. the file destination can be + /// in a different directory/path. In contrast, for FTP the + /// file is renamed, i.e. its destination must be in the same + /// directory/path. + /// + /// Source file path. + /// Target file path. + public static void RenameFile(IOConnectionInfo iocFrom, IOConnectionInfo iocTo) + { + RaiseIOAccessPreEvent(iocFrom, iocTo, IOAccessType.Move); + +#if ModernKeePassLib + if (!iocFrom.IsLocalFile()) return; + iocFrom.StorageFile?.RenameAsync(iocTo.Path).GetAwaiter().GetResult(); +#else + if(iocFrom.IsLocalFile()) { File.Move(iocFrom.Path, iocTo.Path); return; } + +#if !KeePassLibSD + WebRequest req = CreateWebRequest(iocFrom); + if(req != null) + { + if(IsHttpWebRequest(req)) + { +#if KeePassUAP + throw new NotSupportedException(); +#else + req.Method = "MOVE"; + req.Headers.Set("Destination", iocTo.Path); // Full URL supported +#endif + } + else if(IsFtpWebRequest(req)) + { +#if KeePassUAP + throw new NotSupportedException(); +#else + req.Method = WebRequestMethods.Ftp.Rename; + string strTo = UrlUtil.GetFileName(iocTo.Path); + + // We're affected by .NET bug 621450: + // https://connect.microsoft.com/VisualStudio/feedback/details/621450/problem-renaming-file-on-ftp-server-using-ftpwebrequest-in-net-framework-4-0-vs2010-only + // Prepending "./", "%2E/" or "Dummy/../" doesn't work. + + ((FtpWebRequest)req).RenameTo = strTo; +#endif + } + else if(IsFileWebRequest(req)) + { + File.Move(UrlUtil.FileUrlToPath(iocFrom.Path), + UrlUtil.FileUrlToPath(iocTo.Path)); + return; + } + else + { +#if KeePassUAP + throw new NotSupportedException(); +#else + req.Method = WrmMoveFile; + req.Headers.Set(WrhMoveFileTo, iocTo.Path); +#endif + } + +#if !KeePassUAP // Unreachable code + DisposeResponse(req.GetResponse(), true); +#endif + } +#endif + + // using(Stream sIn = IOConnection.OpenRead(iocFrom)) + // { + // using(Stream sOut = IOConnection.OpenWrite(iocTo)) + // { + // MemUtil.CopyStream(sIn, sOut); + // sOut.Close(); + // } + // + // sIn.Close(); + // } + // DeleteFile(iocFrom); +#endif + } + +#if (!ModernKeePassLib && !KeePassLibSD && !KeePassRT) + private static bool SendCommand(IOConnectionInfo ioc, string strMethod) + { + try + { + WebRequest req = CreateWebRequest(ioc); + req.Method = strMethod; + DisposeResponse(req.GetResponse(), true); + } + catch(Exception) { return false; } + + return true; + } +#endif +#if !ModernKeePassLib + internal static void DisposeResponse(WebResponse wr, bool bGetStream) + { + if(wr == null) return; + + try + { + if(bGetStream) + { + Stream s = wr.GetResponseStream(); + if(s != null) s.Close(); + } + } + catch(Exception) { Debug.Assert(false); } + + try { wr.Close(); } + catch(Exception) { Debug.Assert(false); } + } +#endif + public static byte[] ReadFile(IOConnectionInfo ioc) + { + Stream sIn = null; + MemoryStream ms = null; + try + { + sIn = IOConnection.OpenRead(ioc); + if(sIn == null) return null; + + ms = new MemoryStream(); + MemUtil.CopyStream(sIn, ms); + + return ms.ToArray(); + } + catch(Exception) { } + finally + { + if(sIn != null) sIn.Dispose(); + if(ms != null) ms.Dispose(); + } + + return null; + } + + private static void RaiseIOAccessPreEvent(IOConnectionInfo ioc, IOAccessType t) + { + RaiseIOAccessPreEvent(ioc, null, t); + } + + private static void RaiseIOAccessPreEvent(IOConnectionInfo ioc, + IOConnectionInfo ioc2, IOAccessType t) + { + if(ioc == null) { Debug.Assert(false); return; } + // ioc2 may be null + + if(IOConnection.IOAccessPre != null) + { + IOConnectionInfo ioc2Lcl = ((ioc2 != null) ? ioc2.CloneDeep() : null); + IOAccessEventArgs e = new IOAccessEventArgs(ioc.CloneDeep(), ioc2Lcl, t); + IOConnection.IOAccessPre(null, e); + } + } +#if !ModernKeePassLib + private static bool IsHttpWebRequest(Uri uri) + { + if(uri == null) { Debug.Assert(false); return false; } + + string sch = uri.Scheme; + if(sch == null) { Debug.Assert(false); return false; } + return (sch.Equals("http", StrUtil.CaseIgnoreCmp) || // Uri.UriSchemeHttp + sch.Equals("https", StrUtil.CaseIgnoreCmp)); // Uri.UriSchemeHttps + } + + internal static bool IsHttpWebRequest(WebRequest wr) + { + if(wr == null) { Debug.Assert(false); return false; } + +#if KeePassUAP + return IsHttpWebRequest(wr.RequestUri); +#else + return (wr is HttpWebRequest); +#endif + } + + internal static bool IsFtpWebRequest(WebRequest wr) + { + if(wr == null) { Debug.Assert(false); return false; } + +#if KeePassUAP + return string.Equals(wr.RequestUri.Scheme, "ftp", StrUtil.CaseIgnoreCmp); +#else + return (wr is FtpWebRequest); +#endif + } + + private static bool IsFileWebRequest(WebRequest wr) + { + if(wr == null) { Debug.Assert(false); return false; } + +#if KeePassUAP + return string.Equals(wr.RequestUri.Scheme, "file", StrUtil.CaseIgnoreCmp); +#else + return (wr is FileWebRequest); +#endif + } +#endif // ModernKeePass + } +} diff --git a/ModernKeePassLib/Serialization/IOConnectionInfo.cs b/ModernKeePassLib/Serialization/IOConnectionInfo.cs new file mode 100644 index 0000000..8148907 --- /dev/null +++ b/ModernKeePassLib/Serialization/IOConnectionInfo.cs @@ -0,0 +1,407 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; +#if ModernKeePassLib +using Windows.Storage; +//using PCLStorage; +#endif + +using ModernKeePassLib.Interfaces; +using ModernKeePassLib.Utility; +using System.Threading.Tasks; +using Windows.Storage.AccessCache; + +namespace ModernKeePassLib.Serialization +{ + public enum IOCredSaveMode + { + /// + /// Do not remember user name or password. + /// + NoSave = 0, + + /// + /// Remember the user name only, not the password. + /// + UserNameOnly, + + /// + /// Save both user name and password. + /// + SaveCred + } + + public enum IOCredProtMode + { + None = 0, + Obf + } + + /* public enum IOFileFormatHint + { + None = 0, + Deprecated + } */ + + public sealed class IOConnectionInfo : IDeepCloneable + { + // private IOFileFormatHint m_ioHint = IOFileFormatHint.None; + + private string m_strUrl = string.Empty; + public string Path + { + get { return m_strUrl; } + set + { + Debug.Assert(value != null); + if(value == null) throw new ArgumentNullException("value"); + + m_strUrl = value; + } + } + + private string m_strUser = string.Empty; + [DefaultValue("")] + public string UserName + { + get { return m_strUser; } + set + { + Debug.Assert(value != null); + if(value == null) throw new ArgumentNullException("value"); + + m_strUser = value; + } + } + + private string m_strPassword = string.Empty; + [DefaultValue("")] + public string Password + { + get { return m_strPassword; } + set + { + Debug.Assert(value != null); + if(value == null) throw new ArgumentNullException("value"); + + m_strPassword = value; + } + } + + private IOCredProtMode m_ioCredProtMode = IOCredProtMode.None; + public IOCredProtMode CredProtMode + { + get { return m_ioCredProtMode; } + set { m_ioCredProtMode = value; } + } + + private IOCredSaveMode m_ioCredSaveMode = IOCredSaveMode.NoSave; + public IOCredSaveMode CredSaveMode + { + get { return m_ioCredSaveMode; } + set { m_ioCredSaveMode = value; } + } + + private bool m_bComplete = false; + [XmlIgnore] + public bool IsComplete // Credentials etc. fully specified + { + get { return m_bComplete; } + set { m_bComplete = value; } + } + + /* public IOFileFormatHint FileFormatHint + { + get { return m_ioHint; } + set { m_ioHint = value; } + } */ + + private IocProperties m_props = new IocProperties(); + [XmlIgnore] + public IocProperties Properties + { + get { return m_props; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_props = value; + } + } + + /// + /// For serialization only; use Properties in code. + /// + [DefaultValue("")] + public string PropertiesEx + { + get { return m_props.Serialize(); } + set + { + if(value == null) throw new ArgumentNullException("value"); + + IocProperties p = IocProperties.Deserialize(value); + Debug.Assert(p != null); + m_props = (p ?? new IocProperties()); + } + } + + public IOConnectionInfo CloneDeep() + { + IOConnectionInfo ioc = (IOConnectionInfo)this.MemberwiseClone(); + ioc.m_props = m_props.CloneDeep(); + return ioc; + } + +#if DEBUG // For debugger display only + public override string ToString() + { + return GetDisplayName(); + } +#endif + + /* + /// + /// Serialize the current connection info to a string. Credentials + /// are serialized based on the CredSaveMode property. + /// + /// Input object to be serialized. + /// Serialized object as string. + public static string SerializeToString(IOConnectionInfo iocToCompile) + { + Debug.Assert(iocToCompile != null); + if(iocToCompile == null) throw new ArgumentNullException("iocToCompile"); + + string strUrl = iocToCompile.Path; + string strUser = TransformUnreadable(iocToCompile.UserName, true); + string strPassword = TransformUnreadable(iocToCompile.Password, true); + + string strAll = strUrl + strUser + strPassword + "CUN"; + char chSep = StrUtil.GetUnusedChar(strAll); + if(chSep == char.MinValue) throw new FormatException(); + + StringBuilder sb = new StringBuilder(); + sb.Append(chSep); + sb.Append(strUrl); + sb.Append(chSep); + + if(iocToCompile.CredSaveMode == IOCredSaveMode.SaveCred) + { + sb.Append('C'); + sb.Append(chSep); + sb.Append(strUser); + sb.Append(chSep); + sb.Append(strPassword); + } + else if(iocToCompile.CredSaveMode == IOCredSaveMode.UserNameOnly) + { + sb.Append('U'); + sb.Append(chSep); + sb.Append(strUser); + sb.Append(chSep); + } + else // Don't remember credentials + { + sb.Append('N'); + sb.Append(chSep); + sb.Append(chSep); + } + + return sb.ToString(); + } + + public static IOConnectionInfo UnserializeFromString(string strToDecompile) + { + Debug.Assert(strToDecompile != null); + if(strToDecompile == null) throw new ArgumentNullException("strToDecompile"); + if(strToDecompile.Length <= 1) throw new ArgumentException(); + + char chSep = strToDecompile[0]; + string[] vParts = strToDecompile.Substring(1, strToDecompile.Length - + 1).Split(new char[]{ chSep }); + if(vParts.Length < 4) throw new ArgumentException(); + + IOConnectionInfo s = new IOConnectionInfo(); + s.Path = vParts[0]; + + if(vParts[1] == "C") + s.CredSaveMode = IOCredSaveMode.SaveCred; + else if(vParts[1] == "U") + s.CredSaveMode = IOCredSaveMode.UserNameOnly; + else + s.CredSaveMode = IOCredSaveMode.NoSave; + + s.UserName = TransformUnreadable(vParts[2], false); + s.Password = TransformUnreadable(vParts[3], false); + return s; + } + */ + + /* + /// + /// Very simple string protection. Doesn't really encrypt the input + /// string, only encodes it that it's not readable on the first glance. + /// + /// The string to encode/decode. + /// If true, the string will be encoded, + /// otherwise it'll be decoded. + /// Encoded/decoded string. + private static string TransformUnreadable(string strToEncode, bool bEncode) + { + Debug.Assert(strToEncode != null); + if(strToEncode == null) throw new ArgumentNullException("strToEncode"); + + if(bEncode) + { + byte[] pbUtf8 = StrUtil.Utf8.GetBytes(strToEncode); + + unchecked + { + for(int iPos = 0; iPos < pbUtf8.Length; ++iPos) + pbUtf8[iPos] += (byte)(iPos * 11); + } + + return Convert.ToBase64String(pbUtf8); + } + else // Decode + { + byte[] pbBase = Convert.FromBase64String(strToEncode); + + unchecked + { + for(int iPos = 0; iPos < pbBase.Length; ++iPos) + pbBase[iPos] -= (byte)(iPos * 11); + } + + return StrUtil.Utf8.GetString(pbBase, 0, pbBase.Length); + } + } + */ + + public string GetDisplayName() + { + string str = m_strUrl; + + if(m_strUser.Length > 0) + str += (" (" + m_strUser + ")"); + + return str; + } + + public bool IsEmpty() + { + return (m_strUrl.Length == 0); + } + +#if ModernKeePassLib + public static IOConnectionInfo FromFile(StorageFile file) + { + IOConnectionInfo ioc = new IOConnectionInfo(); + + ioc.StorageFile = file; + ioc.Path = file.Path; + ioc.CredSaveMode = IOCredSaveMode.NoSave; + + return ioc; + } + + public static IOConnectionInfo FromPath(string strPath) + { + IOConnectionInfo ioc = new IOConnectionInfo(); + + ioc.Path = strPath; + ioc.StorageFile = StorageApplicationPermissions.FutureAccessList.GetFileAsync(strPath).GetAwaiter().GetResult(); + ioc.CredSaveMode = IOCredSaveMode.NoSave; + + return ioc; + } +#else + public static IOConnectionInfo FromPath(string strPath) + { + IOConnectionInfo ioc = new IOConnectionInfo(); + + ioc.Path = strPath; + ioc.CredSaveMode = IOCredSaveMode.NoSave; + + return ioc; + } +#endif + + + public StorageFile StorageFile { get; set; } + + public bool CanProbablyAccess() + { +#if ModernKeePassLib + if (IsLocalFile()) + { + //return (FileSystem.Current.GetFileFromPathAsync(m_strUrl).Result != null); + var file = StorageFile.GetFileFromPathAsync(m_strUrl).GetAwaiter().GetResult(); + return file != null; + } +#else + if(IsLocalFile()) return File.Exists(m_strUrl); +#endif + + return true; + } + + public bool IsLocalFile() + { + // Not just ":/", see e.g. AppConfigEx.ChangePathRelAbs + return (m_strUrl.IndexOf("://") < 0); + } + + public void ClearCredentials(bool bDependingOnRememberMode) + { + if((bDependingOnRememberMode == false) || + (m_ioCredSaveMode == IOCredSaveMode.NoSave)) + { + m_strUser = string.Empty; + } + + if((bDependingOnRememberMode == false) || + (m_ioCredSaveMode == IOCredSaveMode.NoSave) || + (m_ioCredSaveMode == IOCredSaveMode.UserNameOnly)) + { + m_strPassword = string.Empty; + } + } + + public void Obfuscate(bool bObf) + { + if(bObf && (m_ioCredProtMode == IOCredProtMode.None)) + { + m_strPassword = StrUtil.Obfuscate(m_strPassword); + m_ioCredProtMode = IOCredProtMode.Obf; + } + else if(!bObf && (m_ioCredProtMode == IOCredProtMode.Obf)) + { + m_strPassword = StrUtil.Deobfuscate(m_strPassword); + m_ioCredProtMode = IOCredProtMode.None; + } + } + } +} diff --git a/ModernKeePassLib/Serialization/IocProperties.cs b/ModernKeePassLib/Serialization/IocProperties.cs new file mode 100644 index 0000000..9bbb556 --- /dev/null +++ b/ModernKeePassLib/Serialization/IocProperties.cs @@ -0,0 +1,192 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Globalization; +using System.Text; +using System.Xml; + +using ModernKeePassLib.Interfaces; +using ModernKeePassLib.Utility; + +using StrDict = System.Collections.Generic.Dictionary; + +namespace ModernKeePassLib.Serialization +{ + public interface IHasIocProperties + { + IocProperties IOConnectionProperties { get; set; } + } + + public sealed class IocProperties : IDeepCloneable + { + private StrDict m_dict = new StrDict(); + + public IocProperties() + { + } + + public IocProperties CloneDeep() + { + IocProperties p = new IocProperties(); + p.m_dict = new StrDict(m_dict); + return p; + } + + public string Get(string strKey) + { + if(string.IsNullOrEmpty(strKey)) return null; + + foreach(KeyValuePair kvp in m_dict) + { + if(kvp.Key.Equals(strKey, StrUtil.CaseIgnoreCmp)) + return kvp.Value; + } + + return null; + } + + public void Set(string strKey, string strValue) + { + if(string.IsNullOrEmpty(strKey)) { Debug.Assert(false); return; } + + foreach(KeyValuePair kvp in m_dict) + { + if(kvp.Key.Equals(strKey, StrUtil.CaseIgnoreCmp)) + { + if(string.IsNullOrEmpty(strValue)) m_dict.Remove(kvp.Key); + else m_dict[kvp.Key] = strValue; + return; + } + } + + if(!string.IsNullOrEmpty(strValue)) m_dict[strKey] = strValue; + } + + public bool? GetBool(string strKey) + { + string str = Get(strKey); + if(string.IsNullOrEmpty(str)) return null; + + return StrUtil.StringToBool(str); + } + + public void SetBool(string strKey, bool? ob) + { + if(ob.HasValue) Set(strKey, (ob.Value ? "1" : "0")); + else Set(strKey, null); + } + + public long? GetLong(string strKey) + { + string str = Get(strKey); + if(string.IsNullOrEmpty(str)) return null; + + long l; + if(StrUtil.TryParseLongInvariant(str, out l)) return l; + Debug.Assert(false); + return null; + } + + public void SetLong(string strKey, long? ol) + { + if(ol.HasValue) + Set(strKey, ol.Value.ToString(NumberFormatInfo.InvariantInfo)); + else Set(strKey, null); + } + + public string Serialize() + { + if(m_dict.Count == 0) return string.Empty; + + StringBuilder sbAll = new StringBuilder(); + foreach(KeyValuePair kvp in m_dict) + { + sbAll.Append(kvp.Key); + sbAll.Append(kvp.Value); + } + + string strAll = sbAll.ToString(); + char chSepOuter = ';'; + if(strAll.IndexOf(chSepOuter) >= 0) + chSepOuter = StrUtil.GetUnusedChar(strAll); + + strAll += chSepOuter; + char chSepInner = '='; + if(strAll.IndexOf(chSepInner) >= 0) + chSepInner = StrUtil.GetUnusedChar(strAll); + + StringBuilder sb = new StringBuilder(); + sb.Append(chSepOuter); + sb.Append(chSepInner); + + foreach(KeyValuePair kvp in m_dict) + { + sb.Append(chSepOuter); + sb.Append(kvp.Key); + sb.Append(chSepInner); + sb.Append(kvp.Value); + } + + return sb.ToString(); + } + + public static IocProperties Deserialize(string strSerialized) + { + IocProperties p = new IocProperties(); + if(string.IsNullOrEmpty(strSerialized)) return p; // No assert + + char chSepOuter = strSerialized[0]; + string[] v = strSerialized.Substring(1).Split(new char[] { chSepOuter }); + if((v == null) || (v.Length < 2)) { Debug.Assert(false); return p; } + + string strMeta = v[0]; + if(string.IsNullOrEmpty(strMeta)) { Debug.Assert(false); return p; } + + char chSepInner = strMeta[0]; + char[] vSepInner = new char[] { chSepInner }; + + for(int i = 1; i < v.Length; ++i) + { + string strProp = v[i]; + if(string.IsNullOrEmpty(strProp)) { Debug.Assert(false); continue; } + + string[] vProp = strProp.Split(vSepInner); + if((vProp == null) || (vProp.Length < 2)) { Debug.Assert(false); continue; } + Debug.Assert(vProp.Length == 2); + + p.Set(vProp[0], vProp[1]); + } + + return p; + } + + public void CopyTo(IocProperties p) + { + if(p == null) { Debug.Assert(false); return; } + + foreach(KeyValuePair kvp in m_dict) + { + p.m_dict[kvp.Key] = kvp.Value; + } + } + } +} diff --git a/ModernKeePassLib/Serialization/IocPropertyInfo.cs b/ModernKeePassLib/Serialization/IocPropertyInfo.cs new file mode 100644 index 0000000..00d2e9a --- /dev/null +++ b/ModernKeePassLib/Serialization/IocPropertyInfo.cs @@ -0,0 +1,99 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Serialization +{ + public sealed class IocPropertyInfo + { + private readonly string m_strName; + public string Name + { + get { return m_strName; } + } + + private readonly Type m_t; + public Type Type + { + get { return m_t; } + } + + private string m_strDisplayName; + public string DisplayName + { + get { return m_strDisplayName; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strDisplayName = value; + } + } + + private List m_lProtocols = new List(); + public IEnumerable Protocols + { + get { return m_lProtocols; } + } + + public IocPropertyInfo(string strName, Type t, string strDisplayName, + string[] vProtocols) + { + if(strName == null) throw new ArgumentNullException("strName"); + if(t == null) throw new ArgumentNullException("t"); + if(strDisplayName == null) throw new ArgumentNullException("strDisplayName"); + + m_strName = strName; + m_t = t; + m_strDisplayName = strDisplayName; + + AddProtocols(vProtocols); + } + + public void AddProtocols(string[] v) + { + if(v == null) { Debug.Assert(false); return; } + + foreach(string strProtocol in v) + { + if(strProtocol == null) continue; + + string str = strProtocol.Trim(); + if(str.Length == 0) continue; + + bool bFound = false; + foreach(string strEx in m_lProtocols) + { + if(strEx.Equals(str, StrUtil.CaseIgnoreCmp)) + { + bFound = true; + break; + } + } + + if(!bFound) m_lProtocols.Add(str); + } + } + } +} diff --git a/ModernKeePassLib/Serialization/IocPropertyInfoPool.cs b/ModernKeePassLib/Serialization/IocPropertyInfoPool.cs new file mode 100644 index 0000000..968f26a --- /dev/null +++ b/ModernKeePassLib/Serialization/IocPropertyInfoPool.cs @@ -0,0 +1,123 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +using ModernKeePassLib.Resources; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Serialization +{ + public static class IocKnownProtocols + { + public static readonly string Http = "HTTP"; + public static readonly string Https = "HTTPS"; + public static readonly string WebDav = "WebDAV"; + public static readonly string Ftp = "FTP"; + } + + public static class IocKnownProperties + { + public static readonly string Timeout = "Timeout"; + public static readonly string PreAuth = "PreAuth"; + + public static readonly string UserAgent = "UserAgent"; + public static readonly string Expect100Continue = "Expect100Continue"; + + public static readonly string Passive = "Passive"; + } + + public static class IocPropertyInfoPool + { + private static List m_l = null; + public static IEnumerable PropertyInfos + { + get { EnsureInitialized(); return m_l; } + } + + private static void EnsureInitialized() + { + if(m_l != null) return; + + string strGen = KLRes.General; + string strHttp = IocKnownProtocols.Http; + string strHttps = IocKnownProtocols.Https; + string strWebDav = IocKnownProtocols.WebDav; + string strFtp = IocKnownProtocols.Ftp; + + string[] vGen = new string[] { strGen }; + string[] vHttp = new string[] { strHttp, strHttps, strWebDav }; + string[] vFtp = new string[] { strFtp }; + + List l = new List(); + + l.Add(new IocPropertyInfo(IocKnownProperties.Timeout, + typeof(long), KLRes.Timeout + " [ms]", vGen)); + l.Add(new IocPropertyInfo(IocKnownProperties.PreAuth, + typeof(bool), KLRes.PreAuth, vGen)); + + l.Add(new IocPropertyInfo(IocKnownProperties.UserAgent, + typeof(string), KLRes.UserAgent, vHttp)); + l.Add(new IocPropertyInfo(IocKnownProperties.Expect100Continue, + typeof(bool), KLRes.Expect100Continue, vHttp)); + + l.Add(new IocPropertyInfo(IocKnownProperties.Passive, + typeof(bool), KLRes.Passive, vFtp)); + + // l.Add(new IocPropertyInfo("Test", typeof(bool), + // "Long long long long long long long long long long long long long long long long long long long long", + // new string[] { "Proto 1/9", "Proto 2/9", "Proto 3/9", "Proto 4/9", "Proto 5/9", + // "Proto 6/9", "Proto 7/9", "Proto 8/9", "Proto 9/9" })); + + m_l = l; + } + + public static IocPropertyInfo Get(string strName) + { + if(string.IsNullOrEmpty(strName)) { Debug.Assert(false); return null; } + + EnsureInitialized(); + foreach(IocPropertyInfo pi in m_l) + { + if(pi.Name.Equals(strName, StrUtil.CaseIgnoreCmp)) + return pi; + } + + return null; + } + + public static bool Add(IocPropertyInfo pi) + { + if(pi == null) { Debug.Assert(false); return false; } + + // Name must be non-empty + string strName = pi.Name; + if(string.IsNullOrEmpty(strName)) { Debug.Assert(false); return false; } + + IocPropertyInfo piEx = Get(strName); // Ensures initialized + if(piEx != null) { Debug.Assert(false); return false; } // Exists already + + m_l.Add(pi); + return true; + } + } +} diff --git a/ModernKeePassLib/Serialization/KdbxFile.Read.Streamed.cs b/ModernKeePassLib/Serialization/KdbxFile.Read.Streamed.cs new file mode 100644 index 0000000..4ab2046 --- /dev/null +++ b/ModernKeePassLib/Serialization/KdbxFile.Read.Streamed.cs @@ -0,0 +1,1085 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; + +#if !ModernKeePassLib && !KeePassUAP +using System.Drawing; +#endif + +using ModernKeePassLib; +using ModernKeePassLib.Collections; +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Cryptography.Cipher; +using ModernKeePassLib.Interfaces; +using ModernKeePassLib.Resources; +using ModernKeePassLib.Security; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Serialization +{ + /// + /// Serialization to KeePass KDBX files. + /// + public sealed partial class KdbxFile + { + private enum KdbContext + { + Null, + KeePassFile, + Meta, + Root, + MemoryProtection, + CustomIcons, + CustomIcon, + Binaries, + CustomData, + CustomDataItem, + RootDeletedObjects, + DeletedObject, + Group, + GroupTimes, + GroupCustomData, + GroupCustomDataItem, + Entry, + EntryTimes, + EntryString, + EntryBinary, + EntryAutoType, + EntryAutoTypeItem, + EntryHistory, + EntryCustomData, + EntryCustomDataItem + } + + private bool m_bReadNextNode = true; + private Stack m_ctxGroups = new Stack(); + private PwGroup m_ctxGroup = null; + private PwEntry m_ctxEntry = null; + private string m_ctxStringName = null; + private ProtectedString m_ctxStringValue = null; + private string m_ctxBinaryName = null; + private ProtectedBinary m_ctxBinaryValue = null; + private string m_ctxATName = null; + private string m_ctxATSeq = null; + private bool m_bEntryInHistory = false; + private PwEntry m_ctxHistoryBase = null; + private PwDeletedObject m_ctxDeletedObject = null; + private PwUuid m_uuidCustomIconID = PwUuid.Zero; + private byte[] m_pbCustomIconData = null; + private string m_strCustomDataKey = null; + private string m_strCustomDataValue = null; + private string m_strGroupCustomDataKey = null; + private string m_strGroupCustomDataValue = null; + private string m_strEntryCustomDataKey = null; + private string m_strEntryCustomDataValue = null; + + private void ReadXmlStreamed(Stream sXml, Stream sParent) + { + ReadDocumentStreamed(CreateXmlReader(sXml), sParent); + } + + internal static XmlReaderSettings CreateStdXmlReaderSettings() + { + XmlReaderSettings xrs = new XmlReaderSettings(); + + xrs.CloseInput = true; + xrs.IgnoreComments = true; + xrs.IgnoreProcessingInstructions = true; + xrs.IgnoreWhitespace = true; + +#if ModernKeePassLib || KeePassUAP + xrs.DtdProcessing = DtdProcessing.Prohibit; +#else +#if !KeePassLibSD + // 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; +#endif + + return xrs; + } + + private static XmlReader CreateXmlReader(Stream readerStream) + { + XmlReaderSettings xrs = CreateStdXmlReaderSettings(); + return XmlReader.Create(readerStream, xrs); + } + + private void ReadDocumentStreamed(XmlReader xr, Stream sParentStream) + { + Debug.Assert(xr != null); + if(xr == null) throw new ArgumentNullException("xr"); + + m_ctxGroups.Clear(); + + KdbContext ctx = KdbContext.Null; + + uint uTagCounter = 0; + + bool bSupportsStatus = (m_slLogger != null); + long lStreamLength = 1; + try + { + sParentStream.Position.ToString(); // Test Position support + lStreamLength = sParentStream.Length; + } + catch(Exception) { bSupportsStatus = false; } + if(lStreamLength <= 0) { Debug.Assert(false); lStreamLength = 1; } + + m_bReadNextNode = true; + + while(true) + { + if(m_bReadNextNode) + { + if(!xr.Read()) break; + } + else m_bReadNextNode = true; + + switch(xr.NodeType) + { + case XmlNodeType.Element: + ctx = ReadXmlElement(ctx, xr); + break; + + case XmlNodeType.EndElement: + ctx = EndXmlElement(ctx, xr); + break; + + case XmlNodeType.XmlDeclaration: + break; // Ignore + + default: + Debug.Assert(false); + break; + } + + ++uTagCounter; + if(((uTagCounter % 256) == 0) && bSupportsStatus) + { + Debug.Assert(lStreamLength == sParentStream.Length); + uint uPct = (uint)((sParentStream.Position * 100) / + lStreamLength); + + // Clip percent value in case the stream reports incorrect + // position/length values (M120413) + if(uPct > 100) { Debug.Assert(false); uPct = 100; } + + m_slLogger.SetProgress(uPct); + } + } + + Debug.Assert(ctx == KdbContext.Null); + if(ctx != KdbContext.Null) throw new FormatException(); + + Debug.Assert(m_ctxGroups.Count == 0); + if(m_ctxGroups.Count != 0) throw new FormatException(); + } + + private KdbContext ReadXmlElement(KdbContext ctx, XmlReader xr) + { + Debug.Assert(xr.NodeType == XmlNodeType.Element); + + switch(ctx) + { + case KdbContext.Null: + if(xr.Name == ElemDocNode) + return SwitchContext(ctx, KdbContext.KeePassFile, xr); + else ReadUnknown(xr); + break; + + case KdbContext.KeePassFile: + if(xr.Name == ElemMeta) + return SwitchContext(ctx, KdbContext.Meta, xr); + else if(xr.Name == ElemRoot) + return SwitchContext(ctx, KdbContext.Root, xr); + else ReadUnknown(xr); + break; + + case KdbContext.Meta: + if(xr.Name == ElemGenerator) + ReadString(xr); // Ignore + else if(xr.Name == ElemHeaderHash) + { + // The header hash is typically only stored in + // KDBX <= 3.1 files, not in KDBX >= 4 files + // (here, the header is verified via a HMAC), + // but we also support it for KDBX >= 4 files + // (i.e. if it's present, we check it) + + string strHash = ReadString(xr); + if(!string.IsNullOrEmpty(strHash) && (m_pbHashOfHeader != null) && + !m_bRepairMode) + { + Debug.Assert(m_uFileVersion < FileVersion32_4); + + byte[] pbHash = Convert.FromBase64String(strHash); + if(!MemUtil.ArraysEqual(pbHash, m_pbHashOfHeader)) + throw new InvalidDataException(KLRes.FileCorrupted); + } + } + else if(xr.Name == ElemSettingsChanged) + m_pwDatabase.SettingsChanged = ReadTime(xr); + else if(xr.Name == ElemDbName) + m_pwDatabase.Name = ReadString(xr); + else if(xr.Name == ElemDbNameChanged) + m_pwDatabase.NameChanged = ReadTime(xr); + else if(xr.Name == ElemDbDesc) + m_pwDatabase.Description = ReadString(xr); + else if(xr.Name == ElemDbDescChanged) + m_pwDatabase.DescriptionChanged = ReadTime(xr); + else if(xr.Name == ElemDbDefaultUser) + m_pwDatabase.DefaultUserName = ReadString(xr); + else if(xr.Name == ElemDbDefaultUserChanged) + m_pwDatabase.DefaultUserNameChanged = ReadTime(xr); + else if(xr.Name == ElemDbMntncHistoryDays) + m_pwDatabase.MaintenanceHistoryDays = ReadUInt(xr, 365); + else if(xr.Name == ElemDbColor) + { + string strColor = ReadString(xr); + if(!string.IsNullOrEmpty(strColor)) + m_pwDatabase.Color = ColorTranslator.FromHtml(strColor); + } + else if(xr.Name == ElemDbKeyChanged) + m_pwDatabase.MasterKeyChanged = ReadTime(xr); + else if(xr.Name == ElemDbKeyChangeRec) + m_pwDatabase.MasterKeyChangeRec = ReadLong(xr, -1); + else if(xr.Name == ElemDbKeyChangeForce) + m_pwDatabase.MasterKeyChangeForce = ReadLong(xr, -1); + else if(xr.Name == ElemDbKeyChangeForceOnce) + m_pwDatabase.MasterKeyChangeForceOnce = ReadBool(xr, false); + else if(xr.Name == ElemMemoryProt) + return SwitchContext(ctx, KdbContext.MemoryProtection, xr); + else if(xr.Name == ElemCustomIcons) + return SwitchContext(ctx, KdbContext.CustomIcons, xr); + else if(xr.Name == ElemRecycleBinEnabled) + m_pwDatabase.RecycleBinEnabled = ReadBool(xr, true); + else if(xr.Name == ElemRecycleBinUuid) + m_pwDatabase.RecycleBinUuid = ReadUuid(xr); + else if(xr.Name == ElemRecycleBinChanged) + m_pwDatabase.RecycleBinChanged = ReadTime(xr); + else if(xr.Name == ElemEntryTemplatesGroup) + m_pwDatabase.EntryTemplatesGroup = ReadUuid(xr); + else if(xr.Name == ElemEntryTemplatesGroupChanged) + m_pwDatabase.EntryTemplatesGroupChanged = ReadTime(xr); + else if(xr.Name == ElemHistoryMaxItems) + m_pwDatabase.HistoryMaxItems = ReadInt(xr, -1); + else if(xr.Name == ElemHistoryMaxSize) + m_pwDatabase.HistoryMaxSize = ReadLong(xr, -1); + else if(xr.Name == ElemLastSelectedGroup) + m_pwDatabase.LastSelectedGroup = ReadUuid(xr); + else if(xr.Name == ElemLastTopVisibleGroup) + m_pwDatabase.LastTopVisibleGroup = ReadUuid(xr); + else if(xr.Name == ElemBinaries) + return SwitchContext(ctx, KdbContext.Binaries, xr); + else if(xr.Name == ElemCustomData) + return SwitchContext(ctx, KdbContext.CustomData, xr); + else ReadUnknown(xr); + break; + + case KdbContext.MemoryProtection: + if(xr.Name == ElemProtTitle) + m_pwDatabase.MemoryProtection.ProtectTitle = ReadBool(xr, false); + else if(xr.Name == ElemProtUserName) + m_pwDatabase.MemoryProtection.ProtectUserName = ReadBool(xr, false); + else if(xr.Name == ElemProtPassword) + m_pwDatabase.MemoryProtection.ProtectPassword = ReadBool(xr, true); + else if(xr.Name == ElemProtUrl) + m_pwDatabase.MemoryProtection.ProtectUrl = ReadBool(xr, false); + else if(xr.Name == ElemProtNotes) + m_pwDatabase.MemoryProtection.ProtectNotes = ReadBool(xr, false); + // else if(xr.Name == ElemProtAutoHide) + // m_pwDatabase.MemoryProtection.AutoEnableVisualHiding = ReadBool(xr, true); + else ReadUnknown(xr); + break; + + case KdbContext.CustomIcons: + if(xr.Name == ElemCustomIconItem) + return SwitchContext(ctx, KdbContext.CustomIcon, xr); + else ReadUnknown(xr); + break; + + case KdbContext.CustomIcon: + if(xr.Name == ElemCustomIconItemID) + m_uuidCustomIconID = ReadUuid(xr); + else if(xr.Name == ElemCustomIconItemData) + { + string strData = ReadString(xr); + if(!string.IsNullOrEmpty(strData)) + m_pbCustomIconData = Convert.FromBase64String(strData); + else { Debug.Assert(false); } + } + else ReadUnknown(xr); + break; + + case KdbContext.Binaries: + if(xr.Name == ElemBinary) + { + if(xr.MoveToAttribute(AttrId)) + { + string strKey = xr.Value; + ProtectedBinary pbData = ReadProtectedBinary(xr); + + int iKey; + if(!StrUtil.TryParseIntInvariant(strKey, out iKey)) + throw new FormatException(); + if(iKey < 0) throw new FormatException(); + + Debug.Assert(m_pbsBinaries.Get(iKey) == null); + Debug.Assert(m_pbsBinaries.Find(pbData) < 0); + m_pbsBinaries.Set(iKey, pbData); + } + else ReadUnknown(xr); + } + else ReadUnknown(xr); + break; + + case KdbContext.CustomData: + if(xr.Name == ElemStringDictExItem) + return SwitchContext(ctx, KdbContext.CustomDataItem, xr); + else ReadUnknown(xr); + break; + + case KdbContext.CustomDataItem: + if(xr.Name == ElemKey) + m_strCustomDataKey = ReadString(xr); + else if(xr.Name == ElemValue) + m_strCustomDataValue = ReadString(xr); + else ReadUnknown(xr); + break; + + case KdbContext.Root: + if(xr.Name == ElemGroup) + { + Debug.Assert(m_ctxGroups.Count == 0); + if(m_ctxGroups.Count != 0) throw new FormatException(); + + m_pwDatabase.RootGroup = new PwGroup(false, false); + m_ctxGroups.Push(m_pwDatabase.RootGroup); + m_ctxGroup = m_ctxGroups.Peek(); + + return SwitchContext(ctx, KdbContext.Group, xr); + } + else if(xr.Name == ElemDeletedObjects) + return SwitchContext(ctx, KdbContext.RootDeletedObjects, xr); + else ReadUnknown(xr); + break; + + case KdbContext.Group: + if(xr.Name == ElemUuid) + m_ctxGroup.Uuid = ReadUuid(xr); + else if(xr.Name == ElemName) + m_ctxGroup.Name = ReadString(xr); + else if(xr.Name == ElemNotes) + m_ctxGroup.Notes = ReadString(xr); + else if(xr.Name == ElemIcon) + m_ctxGroup.IconId = ReadIconId(xr, PwIcon.Folder); + else if(xr.Name == ElemCustomIconID) + m_ctxGroup.CustomIconUuid = ReadUuid(xr); + else if(xr.Name == ElemTimes) + return SwitchContext(ctx, KdbContext.GroupTimes, xr); + else if(xr.Name == ElemIsExpanded) + m_ctxGroup.IsExpanded = ReadBool(xr, true); + else if(xr.Name == ElemGroupDefaultAutoTypeSeq) + m_ctxGroup.DefaultAutoTypeSequence = ReadString(xr); + else if(xr.Name == ElemEnableAutoType) + m_ctxGroup.EnableAutoType = StrUtil.StringToBoolEx(ReadString(xr)); + else if(xr.Name == ElemEnableSearching) + m_ctxGroup.EnableSearching = StrUtil.StringToBoolEx(ReadString(xr)); + else if(xr.Name == ElemLastTopVisibleEntry) + m_ctxGroup.LastTopVisibleEntry = ReadUuid(xr); + else if(xr.Name == ElemCustomData) + return SwitchContext(ctx, KdbContext.GroupCustomData, xr); + else if(xr.Name == ElemGroup) + { + m_ctxGroup = new PwGroup(false, false); + m_ctxGroups.Peek().AddGroup(m_ctxGroup, true); + + m_ctxGroups.Push(m_ctxGroup); + + return SwitchContext(ctx, KdbContext.Group, xr); + } + else if(xr.Name == ElemEntry) + { + m_ctxEntry = new PwEntry(false, false); + m_ctxGroup.AddEntry(m_ctxEntry, true); + + m_bEntryInHistory = false; + return SwitchContext(ctx, KdbContext.Entry, xr); + } + else ReadUnknown(xr); + break; + + case KdbContext.GroupCustomData: + if(xr.Name == ElemStringDictExItem) + return SwitchContext(ctx, KdbContext.GroupCustomDataItem, xr); + else ReadUnknown(xr); + break; + + case KdbContext.GroupCustomDataItem: + if(xr.Name == ElemKey) + m_strGroupCustomDataKey = ReadString(xr); + else if(xr.Name == ElemValue) + m_strGroupCustomDataValue = ReadString(xr); + else ReadUnknown(xr); + break; + + case KdbContext.Entry: + if(xr.Name == ElemUuid) + m_ctxEntry.Uuid = ReadUuid(xr); + else if(xr.Name == ElemIcon) + m_ctxEntry.IconId = ReadIconId(xr, PwIcon.Key); + else if(xr.Name == ElemCustomIconID) + m_ctxEntry.CustomIconUuid = ReadUuid(xr); + else if(xr.Name == ElemFgColor) + { + string strColor = ReadString(xr); + if(!string.IsNullOrEmpty(strColor)) + m_ctxEntry.ForegroundColor = ColorTranslator.FromHtml(strColor); + } + else if(xr.Name == ElemBgColor) + { + string strColor = ReadString(xr); + if(!string.IsNullOrEmpty(strColor)) + m_ctxEntry.BackgroundColor = ColorTranslator.FromHtml(strColor); + } + else if(xr.Name == ElemOverrideUrl) + m_ctxEntry.OverrideUrl = ReadString(xr); + else if(xr.Name == ElemTags) + m_ctxEntry.Tags = StrUtil.StringToTags(ReadString(xr)); + else if(xr.Name == ElemTimes) + return SwitchContext(ctx, KdbContext.EntryTimes, xr); + else if(xr.Name == ElemString) + return SwitchContext(ctx, KdbContext.EntryString, xr); + else if(xr.Name == ElemBinary) + return SwitchContext(ctx, KdbContext.EntryBinary, xr); + else if(xr.Name == ElemAutoType) + return SwitchContext(ctx, KdbContext.EntryAutoType, xr); + else if(xr.Name == ElemCustomData) + return SwitchContext(ctx, KdbContext.EntryCustomData, xr); + else if(xr.Name == ElemHistory) + { + Debug.Assert(m_bEntryInHistory == false); + + if(m_bEntryInHistory == false) + { + m_ctxHistoryBase = m_ctxEntry; + return SwitchContext(ctx, KdbContext.EntryHistory, xr); + } + else ReadUnknown(xr); + } + else ReadUnknown(xr); + break; + + case KdbContext.GroupTimes: + case KdbContext.EntryTimes: + ITimeLogger tl = ((ctx == KdbContext.GroupTimes) ? + (ITimeLogger)m_ctxGroup : (ITimeLogger)m_ctxEntry); + Debug.Assert(tl != null); + + if(xr.Name == ElemCreationTime) + tl.CreationTime = ReadTime(xr); + else if(xr.Name == ElemLastModTime) + tl.LastModificationTime = ReadTime(xr); + else if(xr.Name == ElemLastAccessTime) + tl.LastAccessTime = ReadTime(xr); + else if(xr.Name == ElemExpiryTime) + tl.ExpiryTime = ReadTime(xr); + else if(xr.Name == ElemExpires) + tl.Expires = ReadBool(xr, false); + else if(xr.Name == ElemUsageCount) + tl.UsageCount = ReadULong(xr, 0); + else if(xr.Name == ElemLocationChanged) + tl.LocationChanged = ReadTime(xr); + else ReadUnknown(xr); + break; + + case KdbContext.EntryString: + if(xr.Name == ElemKey) + m_ctxStringName = ReadString(xr); + else if(xr.Name == ElemValue) + m_ctxStringValue = ReadProtectedString(xr); + else ReadUnknown(xr); + break; + + case KdbContext.EntryBinary: + if(xr.Name == ElemKey) + m_ctxBinaryName = ReadString(xr); + else if(xr.Name == ElemValue) + m_ctxBinaryValue = ReadProtectedBinary(xr); + else ReadUnknown(xr); + break; + + case KdbContext.EntryAutoType: + if(xr.Name == ElemAutoTypeEnabled) + m_ctxEntry.AutoType.Enabled = ReadBool(xr, true); + else if(xr.Name == ElemAutoTypeObfuscation) + m_ctxEntry.AutoType.ObfuscationOptions = + (AutoTypeObfuscationOptions)ReadInt(xr, 0); + else if(xr.Name == ElemAutoTypeDefaultSeq) + m_ctxEntry.AutoType.DefaultSequence = ReadString(xr); + else if(xr.Name == ElemAutoTypeItem) + return SwitchContext(ctx, KdbContext.EntryAutoTypeItem, xr); + else ReadUnknown(xr); + break; + + case KdbContext.EntryAutoTypeItem: + if(xr.Name == ElemWindow) + m_ctxATName = ReadString(xr); + else if(xr.Name == ElemKeystrokeSequence) + m_ctxATSeq = ReadString(xr); + else ReadUnknown(xr); + break; + + case KdbContext.EntryCustomData: + if(xr.Name == ElemStringDictExItem) + return SwitchContext(ctx, KdbContext.EntryCustomDataItem, xr); + else ReadUnknown(xr); + break; + + case KdbContext.EntryCustomDataItem: + if(xr.Name == ElemKey) + m_strEntryCustomDataKey = ReadString(xr); + else if(xr.Name == ElemValue) + m_strEntryCustomDataValue = ReadString(xr); + else ReadUnknown(xr); + break; + + case KdbContext.EntryHistory: + if(xr.Name == ElemEntry) + { + m_ctxEntry = new PwEntry(false, false); + m_ctxHistoryBase.History.Add(m_ctxEntry); + + m_bEntryInHistory = true; + return SwitchContext(ctx, KdbContext.Entry, xr); + } + else ReadUnknown(xr); + break; + + case KdbContext.RootDeletedObjects: + if(xr.Name == ElemDeletedObject) + { + m_ctxDeletedObject = new PwDeletedObject(); + m_pwDatabase.DeletedObjects.Add(m_ctxDeletedObject); + + return SwitchContext(ctx, KdbContext.DeletedObject, xr); + } + else ReadUnknown(xr); + break; + + case KdbContext.DeletedObject: + if(xr.Name == ElemUuid) + m_ctxDeletedObject.Uuid = ReadUuid(xr); + else if(xr.Name == ElemDeletionTime) + m_ctxDeletedObject.DeletionTime = ReadTime(xr); + else ReadUnknown(xr); + break; + + default: + ReadUnknown(xr); + break; + } + + return ctx; + } + + private KdbContext EndXmlElement(KdbContext ctx, XmlReader xr) + { + Debug.Assert(xr.NodeType == XmlNodeType.EndElement); + + if((ctx == KdbContext.KeePassFile) && (xr.Name == ElemDocNode)) + return KdbContext.Null; + else if((ctx == KdbContext.Meta) && (xr.Name == ElemMeta)) + return KdbContext.KeePassFile; + else if((ctx == KdbContext.Root) && (xr.Name == ElemRoot)) + return KdbContext.KeePassFile; + else if((ctx == KdbContext.MemoryProtection) && (xr.Name == ElemMemoryProt)) + return KdbContext.Meta; + else if((ctx == KdbContext.CustomIcons) && (xr.Name == ElemCustomIcons)) + return KdbContext.Meta; + else if((ctx == KdbContext.CustomIcon) && (xr.Name == ElemCustomIconItem)) + { + if(!m_uuidCustomIconID.Equals(PwUuid.Zero) && + (m_pbCustomIconData != null)) + m_pwDatabase.CustomIcons.Add(new PwCustomIcon( + m_uuidCustomIconID, m_pbCustomIconData)); + else { Debug.Assert(false); } + + m_uuidCustomIconID = PwUuid.Zero; + m_pbCustomIconData = null; + + return KdbContext.CustomIcons; + } + else if((ctx == KdbContext.Binaries) && (xr.Name == ElemBinaries)) + return KdbContext.Meta; + else if((ctx == KdbContext.CustomData) && (xr.Name == ElemCustomData)) + return KdbContext.Meta; + else if((ctx == KdbContext.CustomDataItem) && (xr.Name == ElemStringDictExItem)) + { + if((m_strCustomDataKey != null) && (m_strCustomDataValue != null)) + m_pwDatabase.CustomData.Set(m_strCustomDataKey, m_strCustomDataValue); + else { Debug.Assert(false); } + + m_strCustomDataKey = null; + m_strCustomDataValue = null; + + return KdbContext.CustomData; + } + else if((ctx == KdbContext.Group) && (xr.Name == ElemGroup)) + { + if(PwUuid.Zero.Equals(m_ctxGroup.Uuid)) + m_ctxGroup.Uuid = new PwUuid(true); // No assert (import) + + m_ctxGroups.Pop(); + + if(m_ctxGroups.Count == 0) + { + m_ctxGroup = null; + return KdbContext.Root; + } + else + { + m_ctxGroup = m_ctxGroups.Peek(); + return KdbContext.Group; + } + } + else if((ctx == KdbContext.GroupTimes) && (xr.Name == ElemTimes)) + return KdbContext.Group; + else if((ctx == KdbContext.GroupCustomData) && (xr.Name == ElemCustomData)) + return KdbContext.Group; + else if((ctx == KdbContext.GroupCustomDataItem) && (xr.Name == ElemStringDictExItem)) + { + if((m_strGroupCustomDataKey != null) && (m_strGroupCustomDataValue != null)) + m_ctxGroup.CustomData.Set(m_strGroupCustomDataKey, m_strGroupCustomDataValue); + else { Debug.Assert(false); } + + m_strGroupCustomDataKey = null; + m_strGroupCustomDataValue = null; + + return KdbContext.GroupCustomData; + } + else if((ctx == KdbContext.Entry) && (xr.Name == ElemEntry)) + { + // Create new UUID if absent + if(PwUuid.Zero.Equals(m_ctxEntry.Uuid)) + m_ctxEntry.Uuid = new PwUuid(true); // No assert (import) + + if(m_bEntryInHistory) + { + m_ctxEntry = m_ctxHistoryBase; + return KdbContext.EntryHistory; + } + + return KdbContext.Group; + } + else if((ctx == KdbContext.EntryTimes) && (xr.Name == ElemTimes)) + return KdbContext.Entry; + else if((ctx == KdbContext.EntryString) && (xr.Name == ElemString)) + { + m_ctxEntry.Strings.Set(m_ctxStringName, m_ctxStringValue); + m_ctxStringName = null; + m_ctxStringValue = null; + return KdbContext.Entry; + } + else if((ctx == KdbContext.EntryBinary) && (xr.Name == ElemBinary)) + { + if(string.IsNullOrEmpty(m_strDetachBins)) + m_ctxEntry.Binaries.Set(m_ctxBinaryName, m_ctxBinaryValue); + else + { + SaveBinary(m_ctxBinaryName, m_ctxBinaryValue, m_strDetachBins); + + m_ctxBinaryValue = null; + GC.Collect(); + } + + m_ctxBinaryName = null; + m_ctxBinaryValue = null; + return KdbContext.Entry; + } + else if((ctx == KdbContext.EntryAutoType) && (xr.Name == ElemAutoType)) + return KdbContext.Entry; + else if((ctx == KdbContext.EntryAutoTypeItem) && (xr.Name == ElemAutoTypeItem)) + { + AutoTypeAssociation atAssoc = new AutoTypeAssociation(m_ctxATName, + m_ctxATSeq); + m_ctxEntry.AutoType.Add(atAssoc); + m_ctxATName = null; + m_ctxATSeq = null; + return KdbContext.EntryAutoType; + } + else if((ctx == KdbContext.EntryCustomData) && (xr.Name == ElemCustomData)) + return KdbContext.Entry; + else if((ctx == KdbContext.EntryCustomDataItem) && (xr.Name == ElemStringDictExItem)) + { + if((m_strEntryCustomDataKey != null) && (m_strEntryCustomDataValue != null)) + m_ctxEntry.CustomData.Set(m_strEntryCustomDataKey, m_strEntryCustomDataValue); + else { Debug.Assert(false); } + + m_strEntryCustomDataKey = null; + m_strEntryCustomDataValue = null; + + return KdbContext.EntryCustomData; + } + else if((ctx == KdbContext.EntryHistory) && (xr.Name == ElemHistory)) + { + m_bEntryInHistory = false; + return KdbContext.Entry; + } + else if((ctx == KdbContext.RootDeletedObjects) && (xr.Name == ElemDeletedObjects)) + return KdbContext.Root; + else if((ctx == KdbContext.DeletedObject) && (xr.Name == ElemDeletedObject)) + { + m_ctxDeletedObject = null; + return KdbContext.RootDeletedObjects; + } + else + { + Debug.Assert(false); + throw new FormatException(); + } + } + + private string ReadString(XmlReader xr) + { + XorredBuffer xb = ProcessNode(xr); + if(xb != null) + { + Debug.Assert(false); // Protected data is unexpected here + try + { + byte[] pb = xb.ReadPlainText(); + if(pb.Length == 0) return string.Empty; + try { return StrUtil.Utf8.GetString(pb, 0, pb.Length); } + finally { MemUtil.ZeroByteArray(pb); } + } + finally { xb.Dispose(); } + } + + m_bReadNextNode = false; // ReadElementString skips end tag + return xr.ReadElementString(); + } + + private string ReadStringRaw(XmlReader xr) + { + m_bReadNextNode = false; // ReadElementString skips end tag + return xr.ReadElementString(); + } + + private byte[] ReadBase64(XmlReader xr, bool bRaw) + { + // if(bRaw) return ReadBase64RawInChunks(xr); + + string str = (bRaw ? ReadStringRaw(xr) : ReadString(xr)); + if(string.IsNullOrEmpty(str)) return MemUtil.EmptyByteArray; + + return Convert.FromBase64String(str); + } + + /* private byte[] m_pbBase64ReadBuf = new byte[1024 * 1024 * 3]; + private byte[] ReadBase64RawInChunks(XmlReader xr) + { + xr.MoveToContent(); + + List lParts = new List(); + byte[] pbBuf = m_pbBase64ReadBuf; + while(true) + { + int cb = xr.ReadElementContentAsBase64(pbBuf, 0, pbBuf.Length); + if(cb == 0) break; + + byte[] pb = new byte[cb]; + Array.Copy(pbBuf, 0, pb, 0, cb); + lParts.Add(pb); + + // No break when cb < pbBuf.Length, because ReadElementContentAsBase64 + // moves to the next XML node only when returning 0 + } + m_bReadNextNode = false; + + if(lParts.Count == 0) return MemUtil.EmptyByteArray; + if(lParts.Count == 1) return lParts[0]; + + long cbRes = 0; + for(int i = 0; i < lParts.Count; ++i) + cbRes += lParts[i].Length; + + byte[] pbRes = new byte[cbRes]; + int cbCur = 0; + for(int i = 0; i < lParts.Count; ++i) + { + Array.Copy(lParts[i], 0, pbRes, cbCur, lParts[i].Length); + cbCur += lParts[i].Length; + } + + return pbRes; + } */ + + private bool ReadBool(XmlReader xr, bool bDefault) + { + string str = ReadString(xr); + if(str == ValTrue) return true; + else if(str == ValFalse) return false; + + Debug.Assert(false); + return bDefault; + } + + private PwUuid ReadUuid(XmlReader xr) + { + byte[] pb = ReadBase64(xr, false); + if(pb.Length == 0) return PwUuid.Zero; + return new PwUuid(pb); + } + + private int ReadInt(XmlReader xr, int nDefault) + { + string str = ReadString(xr); + + int n; + if(StrUtil.TryParseIntInvariant(str, out n)) return n; + + // Backward compatibility + if(StrUtil.TryParseInt(str, out n)) return n; + + Debug.Assert(false); + return nDefault; + } + + private uint ReadUInt(XmlReader xr, uint uDefault) + { + string str = ReadString(xr); + + uint u; + if(StrUtil.TryParseUIntInvariant(str, out u)) return u; + + // Backward compatibility + if(StrUtil.TryParseUInt(str, out u)) return u; + + Debug.Assert(false); + return uDefault; + } + + private long ReadLong(XmlReader xr, long lDefault) + { + string str = ReadString(xr); + + long l; + if(StrUtil.TryParseLongInvariant(str, out l)) return l; + + // Backward compatibility + if(StrUtil.TryParseLong(str, out l)) return l; + + Debug.Assert(false); + return lDefault; + } + + private ulong ReadULong(XmlReader xr, ulong uDefault) + { + string str = ReadString(xr); + + ulong u; + if(StrUtil.TryParseULongInvariant(str, out u)) return u; + + // Backward compatibility + if(StrUtil.TryParseULong(str, out u)) return u; + + Debug.Assert(false); + return uDefault; + } + + private DateTime ReadTime(XmlReader xr) + { + // Cf. WriteObject(string, DateTime) + if((m_format == KdbxFormat.Default) && (m_uFileVersion >= FileVersion32_4)) + { + // long l = ReadLong(xr, -1); + // if(l != -1) return DateTime.FromBinary(l); + + byte[] pb = ReadBase64(xr, false); + if(pb.Length != 8) + { + Debug.Assert(false); + byte[] pb8 = new byte[8]; + Array.Copy(pb, pb8, Math.Min(pb.Length, 8)); // Little-endian + pb = pb8; + } + long lSec = MemUtil.BytesToInt64(pb); + return new DateTime(lSec * TimeSpan.TicksPerSecond, DateTimeKind.Utc); + } + else + { + string str = ReadString(xr); + + DateTime dt; + if(TimeUtil.TryDeserializeUtc(str, out dt)) return dt; + } + + Debug.Assert(false); + return m_dtNow; + } + + private PwIcon ReadIconId(XmlReader xr, PwIcon icDefault) + { + int i = ReadInt(xr, (int)icDefault); + if((i >= 0) && (i < (int)PwIcon.Count)) return (PwIcon)i; + + Debug.Assert(false); + return icDefault; + } + + private ProtectedString ReadProtectedString(XmlReader xr) + { + XorredBuffer xb = ProcessNode(xr); + if(xb != null) + { + try { return new ProtectedString(true, xb); } + finally { xb.Dispose(); } + } + + bool bProtect = false; + if(m_format == KdbxFormat.PlainXml) + { + if(xr.MoveToAttribute(AttrProtectedInMemPlainXml)) + { + string strProtect = xr.Value; + bProtect = ((strProtect != null) && (strProtect == ValTrue)); + } + } + + return new ProtectedString(bProtect, ReadString(xr)); + } + + private ProtectedBinary ReadProtectedBinary(XmlReader xr) + { + if(xr.MoveToAttribute(AttrRef)) + { + string strRef = xr.Value; + if(!string.IsNullOrEmpty(strRef)) + { + int iRef; + if(StrUtil.TryParseIntInvariant(strRef, out iRef)) + { + ProtectedBinary pb = m_pbsBinaries.Get(iRef); + if(pb != null) + { + // https://sourceforge.net/p/keepass/feature-requests/2023/ + xr.MoveToElement(); +#if DEBUG + string strInner = ReadStringRaw(xr); + Debug.Assert(string.IsNullOrEmpty(strInner)); +#else + ReadStringRaw(xr); +#endif + + return pb; + } + else { Debug.Assert(false); } + } + else { Debug.Assert(false); } + } + else { Debug.Assert(false); } + } + + bool bCompressed = false; + if(xr.MoveToAttribute(AttrCompressed)) + bCompressed = (xr.Value == ValTrue); + + XorredBuffer xb = ProcessNode(xr); + if(xb != null) + { + Debug.Assert(!bCompressed); // See SubWriteValue(ProtectedBinary value) + try { return new ProtectedBinary(true, xb); } + finally { xb.Dispose(); } + } + + byte[] pbData = ReadBase64(xr, true); + if(pbData.Length == 0) return new ProtectedBinary(); + + if(bCompressed) pbData = MemUtil.Decompress(pbData); + return new ProtectedBinary(false, pbData); + } + + private void ReadUnknown(XmlReader xr) + { + Debug.Assert(false); // Unknown node! + + if(xr.IsEmptyElement) { m_bReadNextNode = true; return; } + + string strUnknownName = xr.Name; + + XorredBuffer xb = ProcessNode(xr); + if(xb != null) { xb.Dispose(); return; } // ProcessNode sets m_bReadNextNode + + bool bRead = true; + while(true) + { + if(bRead) xr.Read(); + + if(xr.NodeType == XmlNodeType.EndElement) break; + if(xr.NodeType != XmlNodeType.Element) { bRead = true; continue; } + + ReadUnknown(xr); + bRead = m_bReadNextNode; + } + + Debug.Assert(xr.Name == strUnknownName); // On end tag + m_bReadNextNode = true; + } + + private XorredBuffer ProcessNode(XmlReader xr) + { + // Debug.Assert(xr.NodeType == XmlNodeType.Element); + + if(xr.HasAttributes) + { + if(xr.MoveToAttribute(AttrProtected)) + { + if(xr.Value == ValTrue) + { + xr.MoveToElement(); + + byte[] pbCT = ReadBase64(xr, true); + byte[] pbPad = m_randomStream.GetRandomBytes((uint)pbCT.Length); + + return new XorredBuffer(pbCT, pbPad); + } + } + } + + return null; + } + + private static KdbContext SwitchContext(KdbContext ctxCurrent, + KdbContext ctxNew, XmlReader xr) + { + if(xr.IsEmptyElement) return ctxCurrent; + return ctxNew; + } + } +} diff --git a/ModernKeePassLib/Serialization/KdbxFile.Read.cs b/ModernKeePassLib/Serialization/KdbxFile.Read.cs new file mode 100644 index 0000000..a7a9733 --- /dev/null +++ b/ModernKeePassLib/Serialization/KdbxFile.Read.cs @@ -0,0 +1,602 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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 +*/ + +// #define KDBX_BENCHMARK + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Security; +using System.Text; +using System.Xml; +#if !ModernKeePassLib && !KeePassUAP +using System.Security.Cryptography; +#else +using Windows.Storage; +#endif + +#if !KeePassLibSD +using System.IO.Compression; +#else +using KeePassLibSD; +#endif + +using ModernKeePassLib.Collections; +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Cryptography.Cipher; +using ModernKeePassLib.Cryptography.KeyDerivation; +using ModernKeePassLib.Interfaces; +using ModernKeePassLib.Keys; +using ModernKeePassLib.Resources; +using ModernKeePassLib.Security; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Serialization +{ + /// + /// Serialization to KeePass KDBX files. + /// + public sealed partial class KdbxFile + { +#if ModernKeePassLib + public void Load(StorageFile file, KdbxFormat fmt, IStatusLogger slLogger) + { + IOConnectionInfo ioc = IOConnectionInfo.FromFile(file); + Load(IOConnection.OpenRead(ioc), fmt, slLogger); + } + +#else + /// + /// Load a KDBX file. + /// + /// File to load. + /// Format. + /// Status logger (optional). + public void Load(string strFilePath, KdbxFormat fmt, IStatusLogger slLogger) + { + IOConnectionInfo ioc = IOConnectionInfo.FromPath(strFilePath); + Load(IOConnection.OpenRead(ioc), fmt, slLogger); + } +#endif + + /// + /// Load a KDBX file from a stream. + /// + /// Stream to read the data from. Must contain + /// a KDBX stream. + /// Format. + /// Status logger (optional). + public void Load(Stream sSource, KdbxFormat fmt, IStatusLogger slLogger) + { + Debug.Assert(sSource != null); + if(sSource == null) throw new ArgumentNullException("sSource"); + + if(m_bUsedOnce) + throw new InvalidOperationException("Do not reuse KdbxFile objects!"); + m_bUsedOnce = true; + +#if KDBX_BENCHMARK + Stopwatch swTime = Stopwatch.StartNew(); +#endif + + m_format = fmt; + m_slLogger = slLogger; + + m_pbsBinaries.Clear(); + + UTF8Encoding encNoBom = StrUtil.Utf8; + byte[] pbCipherKey = null; + byte[] pbHmacKey64 = null; + + List lStreams = new List(); + lStreams.Add(sSource); + + HashingStreamEx sHashing = new HashingStreamEx(sSource, false, null); + lStreams.Add(sHashing); + + try + { + Stream sXml; + if(fmt == KdbxFormat.Default) + { + BinaryReaderEx br = new BinaryReaderEx(sHashing, + encNoBom, KLRes.FileCorrupted); + byte[] pbHeader = LoadHeader(br); + m_pbHashOfHeader = CryptoUtil.HashSha256(pbHeader); + + int cbEncKey, cbEncIV; + ICipherEngine iCipher = GetCipher(out cbEncKey, out cbEncIV); + + ComputeKeys(out pbCipherKey, cbEncKey, out pbHmacKey64); + + string strIncomplete = KLRes.FileHeaderCorrupted + " " + + KLRes.FileIncomplete; + + Stream sPlain; + if(m_uFileVersion < FileVersion32_4) + { + Stream sDecrypted = EncryptStream(sHashing, iCipher, + pbCipherKey, cbEncIV, false); + if((sDecrypted == null) || (sDecrypted == sHashing)) + throw new SecurityException(KLRes.CryptoStreamFailed); + lStreams.Add(sDecrypted); + + BinaryReaderEx brDecrypted = new BinaryReaderEx(sDecrypted, + encNoBom, strIncomplete); + byte[] pbStoredStartBytes = brDecrypted.ReadBytes(32); + + if((m_pbStreamStartBytes == null) || (m_pbStreamStartBytes.Length != 32)) + throw new EndOfStreamException(strIncomplete); + if(!MemUtil.ArraysEqual(pbStoredStartBytes, m_pbStreamStartBytes)) + throw new InvalidCompositeKeyException(); + + sPlain = new HashedBlockStream(sDecrypted, false, 0, !m_bRepairMode); + } + else // KDBX >= 4 + { + byte[] pbStoredHash = MemUtil.Read(sHashing, 32); + if((pbStoredHash == null) || (pbStoredHash.Length != 32)) + throw new EndOfStreamException(strIncomplete); + if(!MemUtil.ArraysEqual(m_pbHashOfHeader, pbStoredHash)) + throw new InvalidDataException(KLRes.FileHeaderCorrupted); + + byte[] pbHeaderHmac = ComputeHeaderHmac(pbHeader, pbHmacKey64); + byte[] pbStoredHmac = MemUtil.Read(sHashing, 32); + if((pbStoredHmac == null) || (pbStoredHmac.Length != 32)) + throw new EndOfStreamException(strIncomplete); + if(!MemUtil.ArraysEqual(pbHeaderHmac, pbStoredHmac)) + throw new InvalidCompositeKeyException(); + + HmacBlockStream sBlocks = new HmacBlockStream(sHashing, + false, !m_bRepairMode, pbHmacKey64); + lStreams.Add(sBlocks); + + sPlain = EncryptStream(sBlocks, iCipher, pbCipherKey, + cbEncIV, false); + if((sPlain == null) || (sPlain == sBlocks)) + throw new SecurityException(KLRes.CryptoStreamFailed); + } + lStreams.Add(sPlain); + + if(m_pwDatabase.Compression == PwCompressionAlgorithm.GZip) + { + sXml = new GZipStream(sPlain, CompressionMode.Decompress); + lStreams.Add(sXml); + } + else sXml = sPlain; + + if(m_uFileVersion >= FileVersion32_4) + LoadInnerHeader(sXml); // Binary header before XML + } + else if(fmt == KdbxFormat.PlainXml) + sXml = sHashing; + else { Debug.Assert(false); throw new ArgumentOutOfRangeException("fmt"); } + + if(fmt == KdbxFormat.Default) + { + if(m_pbInnerRandomStreamKey == null) + { + Debug.Assert(false); + throw new SecurityException("Invalid inner random stream key!"); + } + + m_randomStream = new CryptoRandomStream(m_craInnerRandomStream, + m_pbInnerRandomStreamKey); + } + +#if KeePassDebug_WriteXml + // FileStream fsOut = new FileStream("Raw.xml", FileMode.Create, + // FileAccess.Write, FileShare.None); + // try + // { + // while(true) + // { + // int b = sXml.ReadByte(); + // if(b == -1) break; + // fsOut.WriteByte((byte)b); + // } + // } + // catch(Exception) { } + // fsOut.Close(); +#endif + + ReadXmlStreamed(sXml, sHashing); + // ReadXmlDom(sXml); + } +#if !ModernKeePassLib + catch(CryptographicException) // Thrown on invalid padding + { + throw new CryptographicException(KLRes.FileCorrupted); + } +#endif + finally + { + if(pbCipherKey != null) MemUtil.ZeroByteArray(pbCipherKey); + if(pbHmacKey64 != null) MemUtil.ZeroByteArray(pbHmacKey64); + + CommonCleanUpRead(lStreams, sHashing); + } + +#if KDBX_BENCHMARK + swTime.Stop(); + MessageService.ShowInfo("Loading KDBX took " + + swTime.ElapsedMilliseconds.ToString() + " ms."); +#endif + } + + private void CommonCleanUpRead(List lStreams, HashingStreamEx sHashing) + { + CloseStreams(lStreams); + + Debug.Assert(lStreams.Contains(sHashing)); // sHashing must be closed + m_pbHashOfFileOnDisk = sHashing.Hash; + Debug.Assert(m_pbHashOfFileOnDisk != null); + + CleanUpInnerRandomStream(); + + // Reset memory protection settings (to always use reasonable + // defaults) + m_pwDatabase.MemoryProtection = new MemoryProtectionConfig(); + + // Remove old backups (this call is required here in order to apply + // the default history maintenance settings for people upgrading from + // KeePass <= 2.14 to >= 2.15; also it ensures history integrity in + // case a different application has created the KDBX file and ignored + // the history maintenance settings) + m_pwDatabase.MaintainBackups(); // Don't mark database as modified + + // Expand the root group, such that in case the user accidently + // collapses the root group he can simply reopen the database + PwGroup pgRoot = m_pwDatabase.RootGroup; + if(pgRoot != null) pgRoot.IsExpanded = true; + else { Debug.Assert(false); } + + m_pbHashOfHeader = null; + } + + private byte[] LoadHeader(BinaryReaderEx br) + { + string strPrevExcpText = br.ReadExceptionText; + br.ReadExceptionText = KLRes.FileHeaderCorrupted + " " + + KLRes.FileIncompleteExpc; + + MemoryStream msHeader = new MemoryStream(); + Debug.Assert(br.CopyDataTo == null); + br.CopyDataTo = msHeader; + + byte[] pbSig1 = br.ReadBytes(4); + uint uSig1 = MemUtil.BytesToUInt32(pbSig1); + byte[] pbSig2 = br.ReadBytes(4); + uint uSig2 = MemUtil.BytesToUInt32(pbSig2); + + if((uSig1 == FileSignatureOld1) && (uSig2 == FileSignatureOld2)) + throw new OldFormatException(PwDefs.ShortProductName + @" 1.x", + OldFormatException.OldFormatType.KeePass1x); + + if((uSig1 == FileSignature1) && (uSig2 == FileSignature2)) { } + else if((uSig1 == FileSignaturePreRelease1) && (uSig2 == + FileSignaturePreRelease2)) { } + else throw new FormatException(KLRes.FileSigInvalid); + + byte[] pb = br.ReadBytes(4); + uint uVersion = MemUtil.BytesToUInt32(pb); + if((uVersion & FileVersionCriticalMask) > (FileVersion32 & FileVersionCriticalMask)) + throw new FormatException(KLRes.FileVersionUnsupported + + MessageService.NewParagraph + KLRes.FileNewVerReq); + m_uFileVersion = uVersion; + + while(true) + { + if(!ReadHeaderField(br)) break; + } + + br.CopyDataTo = null; + byte[] pbHeader = msHeader.ToArray(); + msHeader.Dispose(); + + br.ReadExceptionText = strPrevExcpText; + return pbHeader; + } + + private bool ReadHeaderField(BinaryReaderEx brSource) + { + Debug.Assert(brSource != null); + if(brSource == null) throw new ArgumentNullException("brSource"); + + byte btFieldID = brSource.ReadByte(); + + int cbSize; + Debug.Assert(m_uFileVersion > 0); + if(m_uFileVersion < FileVersion32_4) + cbSize = (int)MemUtil.BytesToUInt16(brSource.ReadBytes(2)); + else cbSize = MemUtil.BytesToInt32(brSource.ReadBytes(4)); + if(cbSize < 0) throw new FormatException(KLRes.FileCorrupted); + + byte[] pbData = MemUtil.EmptyByteArray; + if(cbSize > 0) pbData = brSource.ReadBytes(cbSize); + + bool bResult = true; + KdbxHeaderFieldID kdbID = (KdbxHeaderFieldID)btFieldID; + switch(kdbID) + { + case KdbxHeaderFieldID.EndOfHeader: + bResult = false; // Returning false indicates end of header + break; + + case KdbxHeaderFieldID.CipherID: + SetCipher(pbData); + break; + + case KdbxHeaderFieldID.CompressionFlags: + SetCompressionFlags(pbData); + break; + + case KdbxHeaderFieldID.MasterSeed: + m_pbMasterSeed = pbData; + CryptoRandom.Instance.AddEntropy(pbData); + break; + + // Obsolete; for backward compatibility only + case KdbxHeaderFieldID.TransformSeed: + Debug.Assert(m_uFileVersion < FileVersion32_4); + + AesKdf kdfS = new AesKdf(); + if(!m_pwDatabase.KdfParameters.KdfUuid.Equals(kdfS.Uuid)) + m_pwDatabase.KdfParameters = kdfS.GetDefaultParameters(); + + // m_pbTransformSeed = pbData; + m_pwDatabase.KdfParameters.SetByteArray(AesKdf.ParamSeed, pbData); + + CryptoRandom.Instance.AddEntropy(pbData); + break; + + // Obsolete; for backward compatibility only + case KdbxHeaderFieldID.TransformRounds: + Debug.Assert(m_uFileVersion < FileVersion32_4); + + AesKdf kdfR = new AesKdf(); + if(!m_pwDatabase.KdfParameters.KdfUuid.Equals(kdfR.Uuid)) + m_pwDatabase.KdfParameters = kdfR.GetDefaultParameters(); + + // m_pwDatabase.KeyEncryptionRounds = MemUtil.BytesToUInt64(pbData); + m_pwDatabase.KdfParameters.SetUInt64(AesKdf.ParamRounds, + MemUtil.BytesToUInt64(pbData)); + break; + + case KdbxHeaderFieldID.EncryptionIV: + m_pbEncryptionIV = pbData; + break; + + case KdbxHeaderFieldID.InnerRandomStreamKey: + Debug.Assert(m_uFileVersion < FileVersion32_4); + Debug.Assert(m_pbInnerRandomStreamKey == null); + m_pbInnerRandomStreamKey = pbData; + CryptoRandom.Instance.AddEntropy(pbData); + break; + + case KdbxHeaderFieldID.StreamStartBytes: + Debug.Assert(m_uFileVersion < FileVersion32_4); + m_pbStreamStartBytes = pbData; + break; + + case KdbxHeaderFieldID.InnerRandomStreamID: + Debug.Assert(m_uFileVersion < FileVersion32_4); + SetInnerRandomStreamID(pbData); + break; + + case KdbxHeaderFieldID.KdfParameters: + m_pwDatabase.KdfParameters = KdfParameters.DeserializeExt(pbData); + break; + + case KdbxHeaderFieldID.PublicCustomData: + Debug.Assert(m_pwDatabase.PublicCustomData.Count == 0); + m_pwDatabase.PublicCustomData = VariantDictionary.Deserialize(pbData); + break; + + default: + Debug.Assert(false); + if(m_slLogger != null) + m_slLogger.SetText(KLRes.UnknownHeaderId + @": " + + kdbID.ToString() + "!", LogStatusType.Warning); + break; + } + + return bResult; + } + + private void LoadInnerHeader(Stream s) + { + BinaryReaderEx br = new BinaryReaderEx(s, StrUtil.Utf8, + KLRes.FileCorrupted + " " + KLRes.FileIncompleteExpc); + + while(true) + { + if(!ReadInnerHeaderField(br)) break; + } + } + + private bool ReadInnerHeaderField(BinaryReaderEx br) + { + Debug.Assert(br != null); + if(br == null) throw new ArgumentNullException("br"); + + byte btFieldID = br.ReadByte(); + + int cbSize = MemUtil.BytesToInt32(br.ReadBytes(4)); + if(cbSize < 0) throw new FormatException(KLRes.FileCorrupted); + + byte[] pbData = MemUtil.EmptyByteArray; + if(cbSize > 0) pbData = br.ReadBytes(cbSize); + + bool bResult = true; + KdbxInnerHeaderFieldID kdbID = (KdbxInnerHeaderFieldID)btFieldID; + switch(kdbID) + { + case KdbxInnerHeaderFieldID.EndOfHeader: + bResult = false; // Returning false indicates end of header + break; + + case KdbxInnerHeaderFieldID.InnerRandomStreamID: + SetInnerRandomStreamID(pbData); + break; + + case KdbxInnerHeaderFieldID.InnerRandomStreamKey: + Debug.Assert(m_pbInnerRandomStreamKey == null); + m_pbInnerRandomStreamKey = pbData; + CryptoRandom.Instance.AddEntropy(pbData); + break; + + case KdbxInnerHeaderFieldID.Binary: + if(pbData.Length < 1) throw new FormatException(); + KdbxBinaryFlags f = (KdbxBinaryFlags)pbData[0]; + bool bProt = ((f & KdbxBinaryFlags.Protected) != KdbxBinaryFlags.None); + + ProtectedBinary pb = new ProtectedBinary(bProt, pbData, + 1, pbData.Length - 1); + m_pbsBinaries.Add(pb); + + if(bProt) MemUtil.ZeroByteArray(pbData); + break; + + default: + Debug.Assert(false); + break; + } + + return bResult; + } + + private void SetCipher(byte[] pbID) + { + if((pbID == null) || (pbID.Length != (int)PwUuid.UuidSize)) + throw new FormatException(KLRes.FileUnknownCipher); + + m_pwDatabase.DataCipherUuid = new PwUuid(pbID); + } + + private void SetCompressionFlags(byte[] pbFlags) + { + int nID = (int)MemUtil.BytesToUInt32(pbFlags); + if((nID < 0) || (nID >= (int)PwCompressionAlgorithm.Count)) + throw new FormatException(KLRes.FileUnknownCompression); + + m_pwDatabase.Compression = (PwCompressionAlgorithm)nID; + } + + private void SetInnerRandomStreamID(byte[] pbID) + { + uint uID = MemUtil.BytesToUInt32(pbID); + if(uID >= (uint)CrsAlgorithm.Count) + throw new FormatException(KLRes.FileUnknownCipher); + + m_craInnerRandomStream = (CrsAlgorithm)uID; + } + + [Obsolete] + public static List ReadEntries(Stream msData) + { + return ReadEntries(msData, null, false); + } + + [Obsolete] + public static List ReadEntries(PwDatabase pdContext, Stream msData) + { + return ReadEntries(msData, pdContext, true); + } + + /// + /// Read entries from a stream. + /// + /// Input stream to read the entries from. + /// Context database (e.g. for storing icons). + /// If true, custom icons required by + /// the loaded entries are copied to the context database. + /// Loaded entries. + public static List ReadEntries(Stream msData, PwDatabase pdContext, + bool bCopyIcons) + { + List lEntries = new List(); + + if(msData == null) { Debug.Assert(false); return lEntries; } + // pdContext may be null + + /* KdbxFile f = new KdbxFile(pwDatabase); + f.m_format = KdbxFormat.PlainXml; + + XmlDocument doc = XmlUtilEx.CreateXmlDocument(); + doc.Load(msData); + + XmlElement el = doc.DocumentElement; + if(el.Name != ElemRoot) throw new FormatException(); + + List vEntries = new List(); + + foreach(XmlNode xmlChild in el.ChildNodes) + { + if(xmlChild.Name == ElemEntry) + { + PwEntry pe = f.ReadEntry(xmlChild); + pe.Uuid = new PwUuid(true); + + foreach(PwEntry peHistory in pe.History) + peHistory.Uuid = pe.Uuid; + + vEntries.Add(pe); + } + else { Debug.Assert(false); } + } + + return vEntries; */ + + PwDatabase pd = new PwDatabase(); + pd.New(new IOConnectionInfo(), new CompositeKey()); + + KdbxFile f = new KdbxFile(pd); + f.Load(msData, KdbxFormat.PlainXml, null); + + foreach(PwEntry pe in pd.RootGroup.Entries) + { + pe.SetUuid(new PwUuid(true), true); + lEntries.Add(pe); + + if(bCopyIcons && (pdContext != null)) + { + PwUuid pu = pe.CustomIconUuid; + if(!pu.Equals(PwUuid.Zero)) + { + int iSrc = pd.GetCustomIconIndex(pu); + int iDst = pdContext.GetCustomIconIndex(pu); + + if(iSrc < 0) { Debug.Assert(false); } + else if(iDst < 0) + { + pdContext.CustomIcons.Add(pd.CustomIcons[iSrc]); + + pdContext.Modified = true; + pdContext.UINeedsIconUpdate = true; + } + } + } + } + + return lEntries; + } + } +} diff --git a/ModernKeePassLib/Serialization/KdbxFile.Write.cs b/ModernKeePassLib/Serialization/KdbxFile.Write.cs new file mode 100644 index 0000000..ffbd6aa --- /dev/null +++ b/ModernKeePassLib/Serialization/KdbxFile.Write.cs @@ -0,0 +1,1053 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Globalization; +using System.IO; +using System.Security; +using System.Text; +using System.Xml; + +#if !KeePassUAP +using System.Drawing; +using System.Security.Cryptography; +#endif + +#if KeePassLibSD +using KeePassLibSD; +#else +using System.IO.Compression; +#endif + +using ModernKeePassLib.Collections; +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Cryptography.Cipher; +using ModernKeePassLib.Cryptography.KeyDerivation; +using ModernKeePassLib.Delegates; +using ModernKeePassLib.Interfaces; +using ModernKeePassLib.Keys; +using ModernKeePassLib.Resources; +using ModernKeePassLib.Security; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Serialization +{ + /// + /// Serialization to KeePass KDBX files. + /// + public sealed partial class KdbxFile + { + // public void Save(string strFile, PwGroup pgDataSource, KdbxFormat fmt, + // IStatusLogger slLogger) + // { + // bool bMadeUnhidden = UrlUtil.UnhideFile(strFile); + // + // IOConnectionInfo ioc = IOConnectionInfo.FromPath(strFile); + // this.Save(IOConnection.OpenWrite(ioc), pgDataSource, format, slLogger); + // + // if(bMadeUnhidden) UrlUtil.HideFile(strFile, true); // Hide again + // } + + /// + /// Save the contents of the current PwDatabase to a KDBX file. + /// + /// Stream to write the KDBX file into. + /// Group containing all groups and + /// entries to write. If null, the complete database will + /// be written. + /// Format of the file to create. + /// Logger that recieves status information. + public void Save(Stream sSaveTo, PwGroup pgDataSource, KdbxFormat fmt, + IStatusLogger slLogger) + { + Debug.Assert(sSaveTo != null); + if(sSaveTo == null) throw new ArgumentNullException("sSaveTo"); + + if(m_bUsedOnce) + throw new InvalidOperationException("Do not reuse KdbxFile objects!"); + m_bUsedOnce = true; + + m_format = fmt; + m_slLogger = slLogger; + m_xmlWriter = null; + + PwGroup pgRoot = (pgDataSource ?? m_pwDatabase.RootGroup); + UTF8Encoding encNoBom = StrUtil.Utf8; + CryptoRandom cr = CryptoRandom.Instance; + byte[] pbCipherKey = null; + byte[] pbHmacKey64 = null; + + m_pbsBinaries.Clear(); + m_pbsBinaries.AddFrom(pgRoot); + + List lStreams = new List(); + lStreams.Add(sSaveTo); + + HashingStreamEx sHashing = new HashingStreamEx(sSaveTo, true, null); + lStreams.Add(sHashing); + + try + { + m_uFileVersion = GetMinKdbxVersion(); + + int cbEncKey, cbEncIV; + ICipherEngine iCipher = GetCipher(out cbEncKey, out cbEncIV); + + m_pbMasterSeed = cr.GetRandomBytes(32); + m_pbEncryptionIV = cr.GetRandomBytes((uint)cbEncIV); + + // m_pbTransformSeed = cr.GetRandomBytes(32); + PwUuid puKdf = m_pwDatabase.KdfParameters.KdfUuid; + KdfEngine kdf = KdfPool.Get(puKdf); + if(kdf == null) + throw new Exception(KLRes.UnknownKdf + MessageService.NewParagraph + + // KLRes.FileNewVerOrPlgReq + MessageService.NewParagraph + + "UUID: " + puKdf.ToHexString() + "."); + kdf.Randomize(m_pwDatabase.KdfParameters); + + if(m_format == KdbxFormat.Default) + { + if(m_uFileVersion < FileVersion32_4) + { + m_craInnerRandomStream = CrsAlgorithm.Salsa20; + m_pbInnerRandomStreamKey = cr.GetRandomBytes(32); + } + else // KDBX >= 4 + { + m_craInnerRandomStream = CrsAlgorithm.ChaCha20; + m_pbInnerRandomStreamKey = cr.GetRandomBytes(64); + } + + m_randomStream = new CryptoRandomStream(m_craInnerRandomStream, + m_pbInnerRandomStreamKey); + } + + if(m_uFileVersion < FileVersion32_4) + m_pbStreamStartBytes = cr.GetRandomBytes(32); + + Stream sXml; + if(m_format == KdbxFormat.Default) + { + byte[] pbHeader = GenerateHeader(); + m_pbHashOfHeader = CryptoUtil.HashSha256(pbHeader); + + MemUtil.Write(sHashing, pbHeader); + sHashing.Flush(); + + ComputeKeys(out pbCipherKey, cbEncKey, out pbHmacKey64); + + Stream sPlain; + if(m_uFileVersion < FileVersion32_4) + { + Stream sEncrypted = EncryptStream(sHashing, iCipher, + pbCipherKey, cbEncIV, true); + if((sEncrypted == null) || (sEncrypted == sHashing)) + throw new SecurityException(KLRes.CryptoStreamFailed); + lStreams.Add(sEncrypted); + + MemUtil.Write(sEncrypted, m_pbStreamStartBytes); + + sPlain = new HashedBlockStream(sEncrypted, true); + } + else // KDBX >= 4 + { + // For integrity checking (without knowing the master key) + MemUtil.Write(sHashing, m_pbHashOfHeader); + + byte[] pbHeaderHmac = ComputeHeaderHmac(pbHeader, pbHmacKey64); + MemUtil.Write(sHashing, pbHeaderHmac); + + Stream sBlocks = new HmacBlockStream(sHashing, true, + true, pbHmacKey64); + lStreams.Add(sBlocks); + + sPlain = EncryptStream(sBlocks, iCipher, pbCipherKey, + cbEncIV, true); + if((sPlain == null) || (sPlain == sBlocks)) + throw new SecurityException(KLRes.CryptoStreamFailed); + } + lStreams.Add(sPlain); + + if(m_pwDatabase.Compression == PwCompressionAlgorithm.GZip) + { + sXml = new GZipStream(sPlain, CompressionMode.Compress); + lStreams.Add(sXml); + } + else sXml = sPlain; + + if(m_uFileVersion >= FileVersion32_4) + WriteInnerHeader(sXml); // Binary header before XML + } + else if(m_format == KdbxFormat.PlainXml) + sXml = sHashing; + else + { + Debug.Assert(false); + throw new ArgumentOutOfRangeException("fmt"); + } + + m_xmlWriter = XmlUtilEx.CreateXmlWriter(sXml); + + WriteDocument(pgRoot); + + m_xmlWriter.Flush(); + } + finally + { + CommonCleanUpWrite(lStreams, sHashing); + + if(pbCipherKey != null) MemUtil.ZeroByteArray(pbCipherKey); + if(pbHmacKey64 != null) MemUtil.ZeroByteArray(pbHmacKey64); + } + } + + private void CommonCleanUpWrite(List lStreams, HashingStreamEx sHashing) + { + if(m_xmlWriter != null) { m_xmlWriter.Dispose(); m_xmlWriter = null; } + + CloseStreams(lStreams); + + Debug.Assert(lStreams.Contains(sHashing)); // sHashing must be closed + m_pbHashOfFileOnDisk = sHashing.Hash; + Debug.Assert(m_pbHashOfFileOnDisk != null); + + CleanUpInnerRandomStream(); + + m_pbHashOfHeader = null; + } + + private byte[] GenerateHeader() + { + byte[] pbHeader; + using(MemoryStream ms = new MemoryStream()) + { + MemUtil.Write(ms, MemUtil.UInt32ToBytes(FileSignature1)); + MemUtil.Write(ms, MemUtil.UInt32ToBytes(FileSignature2)); + MemUtil.Write(ms, MemUtil.UInt32ToBytes(m_uFileVersion)); + + WriteHeaderField(ms, KdbxHeaderFieldID.CipherID, + m_pwDatabase.DataCipherUuid.UuidBytes); + + int nCprID = (int)m_pwDatabase.Compression; + WriteHeaderField(ms, KdbxHeaderFieldID.CompressionFlags, + MemUtil.UInt32ToBytes((uint)nCprID)); + + WriteHeaderField(ms, KdbxHeaderFieldID.MasterSeed, m_pbMasterSeed); + + if(m_uFileVersion < FileVersion32_4) + { + Debug.Assert(m_pwDatabase.KdfParameters.KdfUuid.Equals( + (new AesKdf()).Uuid)); + WriteHeaderField(ms, KdbxHeaderFieldID.TransformSeed, + m_pwDatabase.KdfParameters.GetByteArray(AesKdf.ParamSeed)); + WriteHeaderField(ms, KdbxHeaderFieldID.TransformRounds, + MemUtil.UInt64ToBytes(m_pwDatabase.KdfParameters.GetUInt64( + AesKdf.ParamRounds, PwDefs.DefaultKeyEncryptionRounds))); + } + else + WriteHeaderField(ms, KdbxHeaderFieldID.KdfParameters, + KdfParameters.SerializeExt(m_pwDatabase.KdfParameters)); + + if(m_pbEncryptionIV.Length > 0) + WriteHeaderField(ms, KdbxHeaderFieldID.EncryptionIV, m_pbEncryptionIV); + + if(m_uFileVersion < FileVersion32_4) + { + WriteHeaderField(ms, KdbxHeaderFieldID.InnerRandomStreamKey, + m_pbInnerRandomStreamKey); + + WriteHeaderField(ms, KdbxHeaderFieldID.StreamStartBytes, + m_pbStreamStartBytes); + + int nIrsID = (int)m_craInnerRandomStream; + WriteHeaderField(ms, KdbxHeaderFieldID.InnerRandomStreamID, + MemUtil.Int32ToBytes(nIrsID)); + } + + // Write public custom data only when there is at least one item, + // because KDBX 3.1 didn't support this field yet + if(m_pwDatabase.PublicCustomData.Count > 0) + WriteHeaderField(ms, KdbxHeaderFieldID.PublicCustomData, + VariantDictionary.Serialize(m_pwDatabase.PublicCustomData)); + + WriteHeaderField(ms, KdbxHeaderFieldID.EndOfHeader, new byte[] { + (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' }); + + pbHeader = ms.ToArray(); + } + + return pbHeader; + } + + private void WriteHeaderField(Stream s, KdbxHeaderFieldID kdbID, + byte[] pbData) + { + s.WriteByte((byte)kdbID); + + byte[] pb = (pbData ?? MemUtil.EmptyByteArray); + int cb = pb.Length; + if(cb < 0) { Debug.Assert(false); throw new OutOfMemoryException(); } + + Debug.Assert(m_uFileVersion > 0); + if(m_uFileVersion < FileVersion32_4) + { + if(cb > (int)ushort.MaxValue) + { + Debug.Assert(false); + throw new ArgumentOutOfRangeException("pbData"); + } + + MemUtil.Write(s, MemUtil.UInt16ToBytes((ushort)cb)); + } + else MemUtil.Write(s, MemUtil.Int32ToBytes(cb)); + + MemUtil.Write(s, pb); + } + + private void WriteInnerHeader(Stream s) + { + int nIrsID = (int)m_craInnerRandomStream; + WriteInnerHeaderField(s, KdbxInnerHeaderFieldID.InnerRandomStreamID, + MemUtil.Int32ToBytes(nIrsID), null); + + WriteInnerHeaderField(s, KdbxInnerHeaderFieldID.InnerRandomStreamKey, + m_pbInnerRandomStreamKey, null); + + ProtectedBinary[] vBin = m_pbsBinaries.ToArray(); + for(int i = 0; i < vBin.Length; ++i) + { + ProtectedBinary pb = vBin[i]; + if(pb == null) throw new InvalidOperationException(); + + KdbxBinaryFlags f = KdbxBinaryFlags.None; + if(pb.IsProtected) f |= KdbxBinaryFlags.Protected; + + byte[] pbFlags = new byte[1] { (byte)f }; + byte[] pbData = pb.ReadData(); + + WriteInnerHeaderField(s, KdbxInnerHeaderFieldID.Binary, + pbFlags, pbData); + + if(pb.IsProtected) MemUtil.ZeroByteArray(pbData); + } + + WriteInnerHeaderField(s, KdbxInnerHeaderFieldID.EndOfHeader, + null, null); + } + + private void WriteInnerHeaderField(Stream s, KdbxInnerHeaderFieldID kdbID, + byte[] pbData1, byte[] pbData2) + { + s.WriteByte((byte)kdbID); + + byte[] pb1 = (pbData1 ?? MemUtil.EmptyByteArray); + byte[] pb2 = (pbData2 ?? MemUtil.EmptyByteArray); + + int cb = pb1.Length + pb2.Length; + if(cb < 0) { Debug.Assert(false); throw new OutOfMemoryException(); } + + MemUtil.Write(s, MemUtil.Int32ToBytes(cb)); + MemUtil.Write(s, pb1); + MemUtil.Write(s, pb2); + } + + private void WriteDocument(PwGroup pgRoot) + { + Debug.Assert(m_xmlWriter != null); + if(m_xmlWriter == null) throw new InvalidOperationException(); + + uint uNumGroups, uNumEntries, uCurEntry = 0; + pgRoot.GetCounts(true, out uNumGroups, out uNumEntries); + + m_xmlWriter.WriteStartDocument(true); + m_xmlWriter.WriteStartElement(ElemDocNode); + + WriteMeta(); + + m_xmlWriter.WriteStartElement(ElemRoot); + StartGroup(pgRoot); + + Stack groupStack = new Stack(); + groupStack.Push(pgRoot); + + GroupHandler gh = delegate(PwGroup pg) + { + Debug.Assert(pg != null); + if(pg == null) throw new ArgumentNullException("pg"); + + while(true) + { + if(pg.ParentGroup == groupStack.Peek()) + { + groupStack.Push(pg); + StartGroup(pg); + break; + } + else + { + groupStack.Pop(); + if(groupStack.Count <= 0) return false; + + EndGroup(); + } + } + + return true; + }; + + EntryHandler eh = delegate(PwEntry pe) + { + Debug.Assert(pe != null); + WriteEntry(pe, false); + + ++uCurEntry; + if(m_slLogger != null) + { + if(!m_slLogger.SetProgress((100 * uCurEntry) / uNumEntries)) + return false; + } + + return true; + }; + + if(!pgRoot.TraverseTree(TraversalMethod.PreOrder, gh, eh)) + throw new InvalidOperationException(); + + while(groupStack.Count > 1) + { + m_xmlWriter.WriteEndElement(); + groupStack.Pop(); + } + + EndGroup(); + + WriteList(ElemDeletedObjects, m_pwDatabase.DeletedObjects); + m_xmlWriter.WriteEndElement(); // Root + + m_xmlWriter.WriteEndElement(); // ElemDocNode + m_xmlWriter.WriteEndDocument(); + } + + private void WriteMeta() + { + m_xmlWriter.WriteStartElement(ElemMeta); + + WriteObject(ElemGenerator, PwDatabase.LocalizedAppName, false); + + if((m_pbHashOfHeader != null) && (m_uFileVersion < FileVersion32_4)) + WriteObject(ElemHeaderHash, Convert.ToBase64String( + m_pbHashOfHeader), false); + + if(m_uFileVersion >= FileVersion32_4) + WriteObject(ElemSettingsChanged, m_pwDatabase.SettingsChanged); + + WriteObject(ElemDbName, m_pwDatabase.Name, true); + WriteObject(ElemDbNameChanged, m_pwDatabase.NameChanged); + WriteObject(ElemDbDesc, m_pwDatabase.Description, true); + WriteObject(ElemDbDescChanged, m_pwDatabase.DescriptionChanged); + WriteObject(ElemDbDefaultUser, m_pwDatabase.DefaultUserName, true); + WriteObject(ElemDbDefaultUserChanged, m_pwDatabase.DefaultUserNameChanged); + WriteObject(ElemDbMntncHistoryDays, m_pwDatabase.MaintenanceHistoryDays); + WriteObject(ElemDbColor, StrUtil.ColorToUnnamedHtml(m_pwDatabase.Color, true), false); + WriteObject(ElemDbKeyChanged, m_pwDatabase.MasterKeyChanged); + WriteObject(ElemDbKeyChangeRec, m_pwDatabase.MasterKeyChangeRec); + WriteObject(ElemDbKeyChangeForce, m_pwDatabase.MasterKeyChangeForce); + if(m_pwDatabase.MasterKeyChangeForceOnce) + WriteObject(ElemDbKeyChangeForceOnce, true); + + WriteList(ElemMemoryProt, m_pwDatabase.MemoryProtection); + + WriteCustomIconList(); + + WriteObject(ElemRecycleBinEnabled, m_pwDatabase.RecycleBinEnabled); + WriteObject(ElemRecycleBinUuid, m_pwDatabase.RecycleBinUuid); + WriteObject(ElemRecycleBinChanged, m_pwDatabase.RecycleBinChanged); + WriteObject(ElemEntryTemplatesGroup, m_pwDatabase.EntryTemplatesGroup); + WriteObject(ElemEntryTemplatesGroupChanged, m_pwDatabase.EntryTemplatesGroupChanged); + WriteObject(ElemHistoryMaxItems, m_pwDatabase.HistoryMaxItems); + WriteObject(ElemHistoryMaxSize, m_pwDatabase.HistoryMaxSize); + + WriteObject(ElemLastSelectedGroup, m_pwDatabase.LastSelectedGroup); + WriteObject(ElemLastTopVisibleGroup, m_pwDatabase.LastTopVisibleGroup); + + if((m_format != KdbxFormat.Default) || (m_uFileVersion < FileVersion32_4)) + WriteBinPool(); + + WriteList(ElemCustomData, m_pwDatabase.CustomData); + + m_xmlWriter.WriteEndElement(); + } + + private void StartGroup(PwGroup pg) + { + m_xmlWriter.WriteStartElement(ElemGroup); + WriteObject(ElemUuid, pg.Uuid); + WriteObject(ElemName, pg.Name, true); + WriteObject(ElemNotes, pg.Notes, true); + WriteObject(ElemIcon, (int)pg.IconId); + + if(!pg.CustomIconUuid.Equals(PwUuid.Zero)) + WriteObject(ElemCustomIconID, pg.CustomIconUuid); + + WriteList(ElemTimes, pg); + WriteObject(ElemIsExpanded, pg.IsExpanded); + WriteObject(ElemGroupDefaultAutoTypeSeq, pg.DefaultAutoTypeSequence, true); + WriteObject(ElemEnableAutoType, StrUtil.BoolToStringEx(pg.EnableAutoType), false); + WriteObject(ElemEnableSearching, StrUtil.BoolToStringEx(pg.EnableSearching), false); + WriteObject(ElemLastTopVisibleEntry, pg.LastTopVisibleEntry); + + if(pg.CustomData.Count > 0) + WriteList(ElemCustomData, pg.CustomData); + } + + private void EndGroup() + { + m_xmlWriter.WriteEndElement(); // Close group element + } + + private void WriteEntry(PwEntry pe, bool bIsHistory) + { + Debug.Assert(pe != null); if(pe == null) throw new ArgumentNullException("pe"); + + m_xmlWriter.WriteStartElement(ElemEntry); + + WriteObject(ElemUuid, pe.Uuid); + WriteObject(ElemIcon, (int)pe.IconId); + + if(!pe.CustomIconUuid.Equals(PwUuid.Zero)) + WriteObject(ElemCustomIconID, pe.CustomIconUuid); + + WriteObject(ElemFgColor, StrUtil.ColorToUnnamedHtml(pe.ForegroundColor, true), false); + WriteObject(ElemBgColor, StrUtil.ColorToUnnamedHtml(pe.BackgroundColor, true), false); + WriteObject(ElemOverrideUrl, pe.OverrideUrl, true); + WriteObject(ElemTags, StrUtil.TagsToString(pe.Tags, false), true); + + WriteList(ElemTimes, pe); + + WriteList(pe.Strings, true); + WriteList(pe.Binaries); + WriteList(ElemAutoType, pe.AutoType); + + if(pe.CustomData.Count > 0) + WriteList(ElemCustomData, pe.CustomData); + + if(!bIsHistory) WriteList(ElemHistory, pe.History, true); + else { Debug.Assert(pe.History.UCount == 0); } + + m_xmlWriter.WriteEndElement(); + } + + private void WriteList(ProtectedStringDictionary dictStrings, bool bEntryStrings) + { + Debug.Assert(dictStrings != null); + if(dictStrings == null) throw new ArgumentNullException("dictStrings"); + + foreach(KeyValuePair kvp in dictStrings) + WriteObject(kvp.Key, kvp.Value, bEntryStrings); + } + + private void WriteList(ProtectedBinaryDictionary dictBinaries) + { + Debug.Assert(dictBinaries != null); + if(dictBinaries == null) throw new ArgumentNullException("dictBinaries"); + + foreach(KeyValuePair kvp in dictBinaries) + WriteObject(kvp.Key, kvp.Value, true); + } + + private void WriteList(string name, AutoTypeConfig cfgAutoType) + { + Debug.Assert(name != null); + Debug.Assert(cfgAutoType != null); + if(cfgAutoType == null) throw new ArgumentNullException("cfgAutoType"); + + m_xmlWriter.WriteStartElement(name); + + WriteObject(ElemAutoTypeEnabled, cfgAutoType.Enabled); + WriteObject(ElemAutoTypeObfuscation, (int)cfgAutoType.ObfuscationOptions); + + if(cfgAutoType.DefaultSequence.Length > 0) + WriteObject(ElemAutoTypeDefaultSeq, cfgAutoType.DefaultSequence, true); + + foreach(AutoTypeAssociation a in cfgAutoType.Associations) + WriteObject(ElemAutoTypeItem, ElemWindow, ElemKeystrokeSequence, + new KeyValuePair(a.WindowName, a.Sequence)); + + m_xmlWriter.WriteEndElement(); + } + + private void WriteList(string name, ITimeLogger times) + { + Debug.Assert(name != null); + Debug.Assert(times != null); if(times == null) throw new ArgumentNullException("times"); + + m_xmlWriter.WriteStartElement(name); + + WriteObject(ElemCreationTime, times.CreationTime); + WriteObject(ElemLastModTime, times.LastModificationTime); + WriteObject(ElemLastAccessTime, times.LastAccessTime); + WriteObject(ElemExpiryTime, times.ExpiryTime); + WriteObject(ElemExpires, times.Expires); + WriteObject(ElemUsageCount, times.UsageCount); + WriteObject(ElemLocationChanged, times.LocationChanged); + + m_xmlWriter.WriteEndElement(); // Name + } + + private void WriteList(string name, PwObjectList value, bool bIsHistory) + { + Debug.Assert(name != null); + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + + m_xmlWriter.WriteStartElement(name); + + foreach(PwEntry pe in value) + WriteEntry(pe, bIsHistory); + + m_xmlWriter.WriteEndElement(); + } + + private void WriteList(string name, PwObjectList value) + { + Debug.Assert(name != null); + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + + m_xmlWriter.WriteStartElement(name); + + foreach(PwDeletedObject pdo in value) + WriteObject(ElemDeletedObject, pdo); + + m_xmlWriter.WriteEndElement(); + } + + private void WriteList(string name, MemoryProtectionConfig value) + { + Debug.Assert(name != null); + Debug.Assert(value != null); + + m_xmlWriter.WriteStartElement(name); + + WriteObject(ElemProtTitle, value.ProtectTitle); + WriteObject(ElemProtUserName, value.ProtectUserName); + WriteObject(ElemProtPassword, value.ProtectPassword); + WriteObject(ElemProtUrl, value.ProtectUrl); + WriteObject(ElemProtNotes, value.ProtectNotes); + // WriteObject(ElemProtAutoHide, value.AutoEnableVisualHiding); + + m_xmlWriter.WriteEndElement(); + } + + private void WriteList(string name, StringDictionaryEx value) + { + Debug.Assert(name != null); + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + + m_xmlWriter.WriteStartElement(name); + + foreach(KeyValuePair kvp in value) + WriteObject(ElemStringDictExItem, ElemKey, ElemValue, kvp); + + m_xmlWriter.WriteEndElement(); + } + + private void WriteCustomIconList() + { + if(m_pwDatabase.CustomIcons.Count == 0) return; + + m_xmlWriter.WriteStartElement(ElemCustomIcons); + + foreach(PwCustomIcon pwci in m_pwDatabase.CustomIcons) + { + m_xmlWriter.WriteStartElement(ElemCustomIconItem); + + WriteObject(ElemCustomIconItemID, pwci.Uuid); + + string strData = Convert.ToBase64String(pwci.ImageDataPng); + WriteObject(ElemCustomIconItemData, strData, false); + + m_xmlWriter.WriteEndElement(); + } + + m_xmlWriter.WriteEndElement(); + } + + private void WriteObject(string name, string value, + bool bFilterValueXmlChars) + { + Debug.Assert(name != null); + Debug.Assert(value != null); + + m_xmlWriter.WriteStartElement(name); + + if(bFilterValueXmlChars) + m_xmlWriter.WriteString(StrUtil.SafeXmlString(value)); + else m_xmlWriter.WriteString(value); + + m_xmlWriter.WriteEndElement(); + } + + private void WriteObject(string name, bool value) + { + Debug.Assert(name != null); + + WriteObject(name, value ? ValTrue : ValFalse, false); + } + + private void WriteObject(string name, PwUuid value) + { + Debug.Assert(name != null); + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + + WriteObject(name, Convert.ToBase64String(value.UuidBytes), false); + } + + private void WriteObject(string name, int value) + { + Debug.Assert(name != null); + + m_xmlWriter.WriteStartElement(name); + m_xmlWriter.WriteString(value.ToString(NumberFormatInfo.InvariantInfo)); + m_xmlWriter.WriteEndElement(); + } + + private void WriteObject(string name, uint value) + { + Debug.Assert(name != null); + + m_xmlWriter.WriteStartElement(name); + m_xmlWriter.WriteString(value.ToString(NumberFormatInfo.InvariantInfo)); + m_xmlWriter.WriteEndElement(); + } + + private void WriteObject(string name, long value) + { + Debug.Assert(name != null); + + m_xmlWriter.WriteStartElement(name); + m_xmlWriter.WriteString(value.ToString(NumberFormatInfo.InvariantInfo)); + m_xmlWriter.WriteEndElement(); + } + + private void WriteObject(string name, ulong value) + { + Debug.Assert(name != null); + + m_xmlWriter.WriteStartElement(name); + m_xmlWriter.WriteString(value.ToString(NumberFormatInfo.InvariantInfo)); + m_xmlWriter.WriteEndElement(); + } + + private void WriteObject(string name, DateTime value) + { + Debug.Assert(name != null); + Debug.Assert(value.Kind == DateTimeKind.Utc); + + // Cf. ReadTime + if((m_format == KdbxFormat.Default) && (m_uFileVersion >= FileVersion32_4)) + { + DateTime dt = TimeUtil.ToUtc(value, false); + + // DateTime dtBase = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc); + // dt -= new TimeSpan(dtBase.Ticks); + + // WriteObject(name, dt.ToBinary()); + + // dt = TimeUtil.RoundToMultOf2PowLess1s(dt); + // long lBin = dt.ToBinary(); + + long lSec = dt.Ticks / TimeSpan.TicksPerSecond; + // WriteObject(name, lSec); + + byte[] pb = MemUtil.Int64ToBytes(lSec); + WriteObject(name, Convert.ToBase64String(pb), false); + } + else WriteObject(name, TimeUtil.SerializeUtc(value), false); + } + + private void WriteObject(string name, string strKeyName, + string strValueName, KeyValuePair kvp) + { + m_xmlWriter.WriteStartElement(name); + + m_xmlWriter.WriteStartElement(strKeyName); + m_xmlWriter.WriteString(StrUtil.SafeXmlString(kvp.Key)); + m_xmlWriter.WriteEndElement(); + m_xmlWriter.WriteStartElement(strValueName); + m_xmlWriter.WriteString(StrUtil.SafeXmlString(kvp.Value)); + m_xmlWriter.WriteEndElement(); + + m_xmlWriter.WriteEndElement(); + } + + private void WriteObject(string name, ProtectedString value, bool bIsEntryString) + { + Debug.Assert(name != null); + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + + m_xmlWriter.WriteStartElement(ElemString); + m_xmlWriter.WriteStartElement(ElemKey); + m_xmlWriter.WriteString(StrUtil.SafeXmlString(name)); + m_xmlWriter.WriteEndElement(); + m_xmlWriter.WriteStartElement(ElemValue); + + bool bProtected = value.IsProtected; + if(bIsEntryString) + { + // Adjust memory protection setting (which might be different + // from the database default, e.g. due to an import which + // didn't specify the correct setting) + if(name == PwDefs.TitleField) + bProtected = m_pwDatabase.MemoryProtection.ProtectTitle; + else if(name == PwDefs.UserNameField) + bProtected = m_pwDatabase.MemoryProtection.ProtectUserName; + else if(name == PwDefs.PasswordField) + bProtected = m_pwDatabase.MemoryProtection.ProtectPassword; + else if(name == PwDefs.UrlField) + bProtected = m_pwDatabase.MemoryProtection.ProtectUrl; + else if(name == PwDefs.NotesField) + bProtected = m_pwDatabase.MemoryProtection.ProtectNotes; + } + + if(bProtected && (m_format == KdbxFormat.Default)) + { + m_xmlWriter.WriteAttributeString(AttrProtected, ValTrue); + + byte[] pbEnc = value.ReadXorredString(m_randomStream); + if(pbEnc.Length > 0) + m_xmlWriter.WriteBase64(pbEnc, 0, pbEnc.Length); + } + else + { + string strValue = value.ReadString(); + + // If names should be localized, we need to apply the language-dependent + // string transformation here. By default, language-dependent conversions + // should be applied, otherwise characters could be rendered incorrectly + // (code page problems). + if(m_bLocalizedNames) + { + StringBuilder sb = new StringBuilder(); + foreach(char ch in strValue) + { + char chMapped = ch; + + // Symbols and surrogates must be moved into the correct code + // page area + if(char.IsSymbol(ch) || char.IsSurrogate(ch)) + { + System.Globalization.UnicodeCategory cat = + CharUnicodeInfo.GetUnicodeCategory(ch); + // Map character to correct position in code page + chMapped = (char)((int)cat * 32 + ch); + } + else if(char.IsControl(ch)) + { + if(ch >= 256) // Control character in high ANSI code page + { + // Some of the control characters map to corresponding ones + // in the low ANSI range (up to 255) when calling + // ToLower on them with invariant culture (see + // http://lists.ximian.com/pipermail/mono-patches/2002-February/086106.html ) +#if !KeePassLibSD + chMapped = char.ToLowerInvariant(ch); +#else + chMapped = char.ToLower(ch); +#endif + } + } + + sb.Append(chMapped); + } + + strValue = sb.ToString(); // Correct string for current code page + } + + if((m_format == KdbxFormat.PlainXml) && bProtected) + m_xmlWriter.WriteAttributeString(AttrProtectedInMemPlainXml, ValTrue); + + m_xmlWriter.WriteString(StrUtil.SafeXmlString(strValue)); + } + + m_xmlWriter.WriteEndElement(); // ElemValue + m_xmlWriter.WriteEndElement(); // ElemString + } + + private void WriteObject(string name, ProtectedBinary value, bool bAllowRef) + { + Debug.Assert(name != null); + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + + m_xmlWriter.WriteStartElement(ElemBinary); + m_xmlWriter.WriteStartElement(ElemKey); + m_xmlWriter.WriteString(StrUtil.SafeXmlString(name)); + m_xmlWriter.WriteEndElement(); + m_xmlWriter.WriteStartElement(ElemValue); + + string strRef = null; + if(bAllowRef) + { + int iRef = m_pbsBinaries.Find(value); + if(iRef >= 0) strRef = iRef.ToString(NumberFormatInfo.InvariantInfo); + else { Debug.Assert(false); } + } + if(strRef != null) + m_xmlWriter.WriteAttributeString(AttrRef, strRef); + else SubWriteValue(value); + + m_xmlWriter.WriteEndElement(); // ElemValue + m_xmlWriter.WriteEndElement(); // ElemBinary + } + + private void SubWriteValue(ProtectedBinary value) + { + if(value.IsProtected && (m_format == KdbxFormat.Default)) + { + m_xmlWriter.WriteAttributeString(AttrProtected, ValTrue); + + byte[] pbEnc = value.ReadXorredData(m_randomStream); + if(pbEnc.Length > 0) + m_xmlWriter.WriteBase64(pbEnc, 0, pbEnc.Length); + } + else + { + if(m_pwDatabase.Compression != PwCompressionAlgorithm.None) + { + m_xmlWriter.WriteAttributeString(AttrCompressed, ValTrue); + + byte[] pbRaw = value.ReadData(); + byte[] pbCmp = MemUtil.Compress(pbRaw); + m_xmlWriter.WriteBase64(pbCmp, 0, pbCmp.Length); + + if(value.IsProtected) + { + MemUtil.ZeroByteArray(pbRaw); + MemUtil.ZeroByteArray(pbCmp); + } + } + else + { + byte[] pbRaw = value.ReadData(); + m_xmlWriter.WriteBase64(pbRaw, 0, pbRaw.Length); + + if(value.IsProtected) MemUtil.ZeroByteArray(pbRaw); + } + } + } + + private void WriteObject(string name, PwDeletedObject value) + { + Debug.Assert(name != null); + Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value"); + + m_xmlWriter.WriteStartElement(name); + WriteObject(ElemUuid, value.Uuid); + WriteObject(ElemDeletionTime, value.DeletionTime); + m_xmlWriter.WriteEndElement(); + } + + private void WriteBinPool() + { + m_xmlWriter.WriteStartElement(ElemBinaries); + + ProtectedBinary[] v = m_pbsBinaries.ToArray(); + for(int i = 0; i < v.Length; ++i) + { + m_xmlWriter.WriteStartElement(ElemBinary); + m_xmlWriter.WriteAttributeString(AttrId, + i.ToString(NumberFormatInfo.InvariantInfo)); + SubWriteValue(v[i]); + m_xmlWriter.WriteEndElement(); + } + + m_xmlWriter.WriteEndElement(); + } + + [Obsolete] + public static bool WriteEntries(Stream msOutput, PwEntry[] vEntries) + { + return WriteEntries(msOutput, null, vEntries); + } + + public static bool WriteEntries(Stream msOutput, PwDatabase pdContext, + PwEntry[] vEntries) + { + if(msOutput == null) { Debug.Assert(false); return false; } + // pdContext may be null + if(vEntries == null) { Debug.Assert(false); return false; } + + /* KdbxFile f = new KdbxFile(pwDatabase); + f.m_format = KdbxFormat.PlainXml; + + XmlTextWriter xtw = null; + try { xtw = new XmlTextWriter(msOutput, StrUtil.Utf8); } + catch(Exception) { Debug.Assert(false); return false; } + if(xtw == null) { Debug.Assert(false); return false; } + + f.m_xmlWriter = xtw; + + xtw.Formatting = Formatting.Indented; + xtw.IndentChar = '\t'; + xtw.Indentation = 1; + + xtw.WriteStartDocument(true); + xtw.WriteStartElement(ElemRoot); + + foreach(PwEntry pe in vEntries) + f.WriteEntry(pe, false); + + xtw.WriteEndElement(); + xtw.WriteEndDocument(); + + xtw.Flush(); + xtw.Close(); + return true; */ + + PwDatabase pd = new PwDatabase(); + pd.New(new IOConnectionInfo(), new CompositeKey()); + + PwGroup pg = pd.RootGroup; + if(pg == null) { Debug.Assert(false); return false; } + + foreach(PwEntry pe in vEntries) + { + PwUuid pu = pe.CustomIconUuid; + if(!pu.Equals(PwUuid.Zero) && (pd.GetCustomIconIndex(pu) < 0)) + { + int i = -1; + if(pdContext != null) i = pdContext.GetCustomIconIndex(pu); + if(i >= 0) + { + PwCustomIcon ci = pdContext.CustomIcons[i]; + pd.CustomIcons.Add(ci); + } + else { Debug.Assert(pdContext == null); } + } + + PwEntry peCopy = pe.CloneDeep(); + pg.AddEntry(peCopy, true); + } + + KdbxFile f = new KdbxFile(pd); + f.Save(msOutput, null, KdbxFormat.PlainXml, null); + return true; + } + } +} diff --git a/ModernKeePassLib/Serialization/KdbxFile.cs b/ModernKeePassLib/Serialization/KdbxFile.cs new file mode 100644 index 0000000..190830c --- /dev/null +++ b/ModernKeePassLib/Serialization/KdbxFile.cs @@ -0,0 +1,544 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Globalization; +using System.IO; +using System.Security; +using System.Text; +using System.Xml; + +#if !KeePassUAP +using System.Security.Cryptography; +#endif + +#if ModernKeePassLib +using Windows.Storage; +#endif + +using ModernKeePassLib.Collections; +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Cryptography.Cipher; +using ModernKeePassLib.Cryptography.KeyDerivation; +using ModernKeePassLib.Delegates; +using ModernKeePassLib.Interfaces; +using ModernKeePassLib.Resources; +using ModernKeePassLib.Security; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Serialization +{ + /// + /// The KdbxFile class supports saving the data to various + /// formats. + /// + public enum KdbxFormat + { + /// + /// The default, encrypted file format. + /// + Default = 0, + + /// + /// Use this flag when exporting data to a plain-text XML file. + /// + PlainXml + } + + /// + /// Serialization to KeePass KDBX files. + /// + public sealed partial class KdbxFile + { + /// + /// File identifier, first 32-bit value. + /// + internal const uint FileSignature1 = 0x9AA2D903; + + /// + /// File identifier, second 32-bit value. + /// + internal const uint FileSignature2 = 0xB54BFB67; + + /// + /// File version of files saved by the current KdbxFile class. + /// KeePass 2.07 has version 1.01, 2.08 has 1.02, 2.09 has 2.00, + /// 2.10 has 2.02, 2.11 has 2.04, 2.15 has 3.00, 2.20 has 3.01. + /// The first 2 bytes are critical (i.e. loading will fail, if the + /// file version is too high), the last 2 bytes are informational. + /// + private const uint FileVersion32 = 0x00040000; + + internal const uint FileVersion32_4 = 0x00040000; // First of 4.x series + internal const uint FileVersion32_3 = 0x00030001; // Old format 3.1 + + private const uint FileVersionCriticalMask = 0xFFFF0000; + + // KeePass 1.x signature + internal const uint FileSignatureOld1 = 0x9AA2D903; + internal const uint FileSignatureOld2 = 0xB54BFB65; + // KeePass 2.x pre-release (alpha and beta) signature + internal const uint FileSignaturePreRelease1 = 0x9AA2D903; + internal const uint FileSignaturePreRelease2 = 0xB54BFB66; + + private const string ElemDocNode = "KeePassFile"; + private const string ElemMeta = "Meta"; + private const string ElemRoot = "Root"; + private const string ElemGroup = "Group"; + private const string ElemEntry = "Entry"; + + private const string ElemGenerator = "Generator"; + private const string ElemHeaderHash = "HeaderHash"; + private const string ElemSettingsChanged = "SettingsChanged"; + private const string ElemDbName = "DatabaseName"; + private const string ElemDbNameChanged = "DatabaseNameChanged"; + private const string ElemDbDesc = "DatabaseDescription"; + private const string ElemDbDescChanged = "DatabaseDescriptionChanged"; + private const string ElemDbDefaultUser = "DefaultUserName"; + private const string ElemDbDefaultUserChanged = "DefaultUserNameChanged"; + private const string ElemDbMntncHistoryDays = "MaintenanceHistoryDays"; + private const string ElemDbColor = "Color"; + private const string ElemDbKeyChanged = "MasterKeyChanged"; + private const string ElemDbKeyChangeRec = "MasterKeyChangeRec"; + private const string ElemDbKeyChangeForce = "MasterKeyChangeForce"; + private const string ElemDbKeyChangeForceOnce = "MasterKeyChangeForceOnce"; + private const string ElemRecycleBinEnabled = "RecycleBinEnabled"; + private const string ElemRecycleBinUuid = "RecycleBinUUID"; + private const string ElemRecycleBinChanged = "RecycleBinChanged"; + private const string ElemEntryTemplatesGroup = "EntryTemplatesGroup"; + private const string ElemEntryTemplatesGroupChanged = "EntryTemplatesGroupChanged"; + private const string ElemHistoryMaxItems = "HistoryMaxItems"; + private const string ElemHistoryMaxSize = "HistoryMaxSize"; + private const string ElemLastSelectedGroup = "LastSelectedGroup"; + private const string ElemLastTopVisibleGroup = "LastTopVisibleGroup"; + + private const string ElemMemoryProt = "MemoryProtection"; + private const string ElemProtTitle = "ProtectTitle"; + private const string ElemProtUserName = "ProtectUserName"; + private const string ElemProtPassword = "ProtectPassword"; + private const string ElemProtUrl = "ProtectURL"; + private const string ElemProtNotes = "ProtectNotes"; + // private const string ElemProtAutoHide = "AutoEnableVisualHiding"; + + private const string ElemCustomIcons = "CustomIcons"; + private const string ElemCustomIconItem = "Icon"; + private const string ElemCustomIconItemID = "UUID"; + private const string ElemCustomIconItemData = "Data"; + + private const string ElemAutoType = "AutoType"; + private const string ElemHistory = "History"; + + private const string ElemName = "Name"; + private const string ElemNotes = "Notes"; + private const string ElemUuid = "UUID"; + private const string ElemIcon = "IconID"; + private const string ElemCustomIconID = "CustomIconUUID"; + private const string ElemFgColor = "ForegroundColor"; + private const string ElemBgColor = "BackgroundColor"; + private const string ElemOverrideUrl = "OverrideURL"; + private const string ElemTimes = "Times"; + private const string ElemTags = "Tags"; + + private const string ElemCreationTime = "CreationTime"; + private const string ElemLastModTime = "LastModificationTime"; + private const string ElemLastAccessTime = "LastAccessTime"; + private const string ElemExpiryTime = "ExpiryTime"; + private const string ElemExpires = "Expires"; + private const string ElemUsageCount = "UsageCount"; + private const string ElemLocationChanged = "LocationChanged"; + + private const string ElemGroupDefaultAutoTypeSeq = "DefaultAutoTypeSequence"; + private const string ElemEnableAutoType = "EnableAutoType"; + private const string ElemEnableSearching = "EnableSearching"; + + private const string ElemString = "String"; + private const string ElemBinary = "Binary"; + private const string ElemKey = "Key"; + private const string ElemValue = "Value"; + + private const string ElemAutoTypeEnabled = "Enabled"; + private const string ElemAutoTypeObfuscation = "DataTransferObfuscation"; + private const string ElemAutoTypeDefaultSeq = "DefaultSequence"; + private const string ElemAutoTypeItem = "Association"; + private const string ElemWindow = "Window"; + private const string ElemKeystrokeSequence = "KeystrokeSequence"; + + private const string ElemBinaries = "Binaries"; + + private const string AttrId = "ID"; + private const string AttrRef = "Ref"; + private const string AttrProtected = "Protected"; + private const string AttrProtectedInMemPlainXml = "ProtectInMemory"; + private const string AttrCompressed = "Compressed"; + + private const string ElemIsExpanded = "IsExpanded"; + private const string ElemLastTopVisibleEntry = "LastTopVisibleEntry"; + + private const string ElemDeletedObjects = "DeletedObjects"; + private const string ElemDeletedObject = "DeletedObject"; + private const string ElemDeletionTime = "DeletionTime"; + + private const string ValFalse = "False"; + private const string ValTrue = "True"; + + private const string ElemCustomData = "CustomData"; + private const string ElemStringDictExItem = "Item"; + + private PwDatabase m_pwDatabase; // Not null, see constructor + private bool m_bUsedOnce = false; + + private XmlWriter m_xmlWriter = null; + private CryptoRandomStream m_randomStream = null; + private KdbxFormat m_format = KdbxFormat.Default; + private IStatusLogger m_slLogger = null; + + private uint m_uFileVersion = 0; + private byte[] m_pbMasterSeed = null; + // private byte[] m_pbTransformSeed = null; + private byte[] m_pbEncryptionIV = null; + private byte[] m_pbStreamStartBytes = null; + + // ArcFourVariant only for backward compatibility; KeePass defaults + // to a more secure algorithm when *writing* databases + private CrsAlgorithm m_craInnerRandomStream = CrsAlgorithm.ArcFourVariant; + private byte[] m_pbInnerRandomStreamKey = null; + + private ProtectedBinarySet m_pbsBinaries = new ProtectedBinarySet(); + + private byte[] m_pbHashOfHeader = null; + private byte[] m_pbHashOfFileOnDisk = null; + + private readonly DateTime m_dtNow = DateTime.UtcNow; // Cache current time + + private const uint NeutralLanguageOffset = 0x100000; // 2^20, see 32-bit Unicode specs + private const uint NeutralLanguageIDSec = 0x7DC5C; // See 32-bit Unicode specs + private const uint NeutralLanguageID = NeutralLanguageOffset + NeutralLanguageIDSec; + private static bool m_bLocalizedNames = false; + + private enum KdbxHeaderFieldID : byte + { + EndOfHeader = 0, + Comment = 1, + CipherID = 2, + CompressionFlags = 3, + MasterSeed = 4, + TransformSeed = 5, // KDBX 3.1, for backward compatibility only + TransformRounds = 6, // KDBX 3.1, for backward compatibility only + EncryptionIV = 7, + InnerRandomStreamKey = 8, // KDBX 3.1, for backward compatibility only + StreamStartBytes = 9, // KDBX 3.1, for backward compatibility only + InnerRandomStreamID = 10, // KDBX 3.1, for backward compatibility only + KdfParameters = 11, // KDBX 4, superseding Transform* + PublicCustomData = 12 // KDBX 4 + } + + // Inner header in KDBX >= 4 files + private enum KdbxInnerHeaderFieldID : byte + { + EndOfHeader = 0, + InnerRandomStreamID = 1, // Supersedes KdbxHeaderFieldID.InnerRandomStreamID + InnerRandomStreamKey = 2, // Supersedes KdbxHeaderFieldID.InnerRandomStreamKey + Binary = 3 + } + + [Flags] + private enum KdbxBinaryFlags : byte + { + None = 0, + Protected = 1 + } + + public byte[] HashOfFileOnDisk + { + get { return m_pbHashOfFileOnDisk; } + } + + private bool m_bRepairMode = false; + public bool RepairMode + { + get { return m_bRepairMode; } + set { m_bRepairMode = value; } + } + + private uint m_uForceVersion = 0; + internal uint ForceVersion + { + get { return m_uForceVersion; } + set { m_uForceVersion = value; } + } + + private string m_strDetachBins = null; + /// + /// Detach binaries when opening a file. If this isn't null, + /// all binaries are saved to the specified path and are removed + /// from the database. + /// + public string DetachBinaries + { + get { return m_strDetachBins; } + set { m_strDetachBins = value; } + } + + /// + /// Default constructor. + /// + /// The PwDatabase instance that the + /// class will load file data into or use to create a KDBX file. + public KdbxFile(PwDatabase pwDataStore) + { + Debug.Assert(pwDataStore != null); + if(pwDataStore == null) throw new ArgumentNullException("pwDataStore"); + + m_pwDatabase = pwDataStore; + } + + /// + /// Call this once to determine the current localization settings. + /// + public static void DetermineLanguageId() + { + // Test if localized names should be used. If localized names are used, + // the m_bLocalizedNames value must be set to true. By default, localized + // names should be used! (Otherwise characters could be corrupted + // because of different code pages). + unchecked + { + uint uTest = 0; + foreach(char ch in PwDatabase.LocalizedAppName) + uTest = uTest * 5 + ch; + + m_bLocalizedNames = (uTest != NeutralLanguageID); + } + } + + private uint GetMinKdbxVersion() + { + if(m_uForceVersion != 0) return m_uForceVersion; + + // See also KeePassKdb2x3.Export (KDBX 3.1 export module) + + AesKdf kdfAes = new AesKdf(); + if(!kdfAes.Uuid.Equals(m_pwDatabase.KdfParameters.KdfUuid)) + return FileVersion32; + + if(m_pwDatabase.PublicCustomData.Count > 0) + return FileVersion32; + + bool bCustomData = false; + GroupHandler gh = delegate(PwGroup pg) + { + if(pg == null) { Debug.Assert(false); return true; } + if(pg.CustomData.Count > 0) { bCustomData = true; return false; } + return true; + }; + EntryHandler eh = delegate(PwEntry pe) + { + if(pe == null) { Debug.Assert(false); return true; } + if(pe.CustomData.Count > 0) { bCustomData = true; return false; } + return true; + }; + gh(m_pwDatabase.RootGroup); + m_pwDatabase.RootGroup.TraverseTree(TraversalMethod.PreOrder, gh, eh); + if(bCustomData) return FileVersion32; + + return FileVersion32_3; // KDBX 3.1 is sufficient + } + + private void ComputeKeys(out byte[] pbCipherKey, int cbCipherKey, + out byte[] pbHmacKey64) + { + byte[] pbCmp = new byte[32 + 32 + 1]; + try + { + Debug.Assert(m_pbMasterSeed != null); + if(m_pbMasterSeed == null) + throw new ArgumentNullException("m_pbMasterSeed"); + Debug.Assert(m_pbMasterSeed.Length == 32); + if(m_pbMasterSeed.Length != 32) + throw new FormatException(KLRes.MasterSeedLengthInvalid); + Array.Copy(m_pbMasterSeed, 0, pbCmp, 0, 32); + + Debug.Assert(m_pwDatabase != null); + Debug.Assert(m_pwDatabase.MasterKey != null); + ProtectedBinary pbinUser = m_pwDatabase.MasterKey.GenerateKey32( + m_pwDatabase.KdfParameters); + Debug.Assert(pbinUser != null); + if(pbinUser == null) + throw new SecurityException(KLRes.InvalidCompositeKey); + byte[] pUserKey32 = pbinUser.ReadData(); + if((pUserKey32 == null) || (pUserKey32.Length != 32)) + throw new SecurityException(KLRes.InvalidCompositeKey); + Array.Copy(pUserKey32, 0, pbCmp, 32, 32); + MemUtil.ZeroByteArray(pUserKey32); + + pbCipherKey = CryptoUtil.ResizeKey(pbCmp, 0, 64, cbCipherKey); + + pbCmp[64] = 1; + using(SHA512Managed h = new SHA512Managed()) + { + pbHmacKey64 = h.ComputeHash(pbCmp); + } + } + finally { MemUtil.ZeroByteArray(pbCmp); } + } + + private ICipherEngine GetCipher(out int cbEncKey, out int cbEncIV) + { + PwUuid pu = m_pwDatabase.DataCipherUuid; + ICipherEngine iCipher = CipherPool.GlobalPool.GetCipher(pu); + if(iCipher == null) // CryptographicExceptions are translated to "file corrupted" + throw new Exception(KLRes.FileUnknownCipher + + MessageService.NewParagraph + KLRes.FileNewVerOrPlgReq + + MessageService.NewParagraph + "UUID: " + pu.ToHexString() + "."); + + ICipherEngine2 iCipher2 = (iCipher as ICipherEngine2); + if(iCipher2 != null) + { + cbEncKey = iCipher2.KeyLength; + if(cbEncKey < 0) throw new InvalidOperationException("EncKey.Length"); + + cbEncIV = iCipher2.IVLength; + if(cbEncIV < 0) throw new InvalidOperationException("EncIV.Length"); + } + else + { + cbEncKey = 32; + cbEncIV = 16; + } + + return iCipher; + } + + private Stream EncryptStream(Stream s, ICipherEngine iCipher, + byte[] pbKey, int cbIV, bool bEncrypt) + { + byte[] pbIV = (m_pbEncryptionIV ?? MemUtil.EmptyByteArray); + if(pbIV.Length != cbIV) + { + Debug.Assert(false); + throw new Exception(KLRes.FileCorrupted); + } + + if(bEncrypt) + return iCipher.EncryptStream(s, pbKey, pbIV); + return iCipher.DecryptStream(s, pbKey, pbIV); + } + + private byte[] ComputeHeaderHmac(byte[] pbHeader, byte[] pbKey) + { + byte[] pbHeaderHmac; + byte[] pbBlockKey = HmacBlockStream.GetHmacKey64( + pbKey, ulong.MaxValue); + using(HMACSHA256 h = new HMACSHA256(pbBlockKey)) + { + pbHeaderHmac = h.ComputeHash(pbHeader); + } + MemUtil.ZeroByteArray(pbBlockKey); + + return pbHeaderHmac; + } + + private void CloseStreams(List lStreams) + { + if(lStreams == null) { Debug.Assert(false); return; } + + // Typically, closing a stream also closes its base + // stream; however, there may be streams that do not + // do this (e.g. some cipher plugin), thus for safety + // we close all streams manually, from the innermost + // to the outermost + + for(int i = lStreams.Count - 1; i >= 0; --i) + { + // Check for duplicates + Debug.Assert((lStreams.IndexOf(lStreams[i]) == i) && + (lStreams.LastIndexOf(lStreams[i]) == i)); + + try { lStreams[i].Dispose(); } + catch(Exception) { Debug.Assert(false); } + } + + // Do not clear the list + } + + private void CleanUpInnerRandomStream() + { + if(m_randomStream != null) m_randomStream.Dispose(); + + if(m_pbInnerRandomStreamKey != null) + MemUtil.ZeroByteArray(m_pbInnerRandomStreamKey); + } + + private static void SaveBinary(string strName, ProtectedBinary pb, + string strSaveDir) + { + if(pb == null) { Debug.Assert(false); return; } + + if(string.IsNullOrEmpty(strName)) strName = "File.bin"; + + string strPath; + int iTry = 1; + do + { + strPath = UrlUtil.EnsureTerminatingSeparator(strSaveDir, false); + + string strExt = UrlUtil.GetExtension(strName); + string strDesc = UrlUtil.StripExtension(strName); + + strPath += strDesc; + if(iTry > 1) + strPath += " (" + iTry.ToString(NumberFormatInfo.InvariantInfo) + + ")"; + + if(!string.IsNullOrEmpty(strExt)) strPath += "." + strExt; + + ++iTry; + } +#if ModernKeePassLib + while (StorageFile.GetFileFromPathAsync(strPath).GetResults() != null); +#else + while(File.Exists(strPath)); +#endif + +#if ModernKeePassLib + byte[] pbData = pb.ReadData(); + /*var file = FileSystem.Current.GetFileFromPathAsync(strPath).Result; + using (var stream = file.OpenAsync(FileAccess.ReadAndWrite).Result) {*/ + var file = StorageFile.GetFileFromPathAsync(strPath).GetAwaiter().GetResult(); + using (var stream = file.OpenAsync(FileAccessMode.ReadWrite).GetAwaiter().GetResult().AsStream()) + { + stream.Write (pbData, 0, pbData.Length); + } + MemUtil.ZeroByteArray(pbData); +#elif !KeePassLibSD + byte[] pbData = pb.ReadData(); + File.WriteAllBytes(strPath, pbData); + MemUtil.ZeroByteArray(pbData); +#else + FileStream fs = new FileStream(strPath, FileMode.Create, + FileAccess.Write, FileShare.None); + byte[] pbData = pb.ReadData(); + try { File.WriteAllBytes(strPath, pbData); } + finally { if(pb.IsProtected) MemUtil.ZeroByteArray(pbData); } +#endif + } + } +} diff --git a/ModernKeePassLib/Serialization/OldFormatException.cs b/ModernKeePassLib/Serialization/OldFormatException.cs new file mode 100644 index 0000000..a66ab85 --- /dev/null +++ b/ModernKeePassLib/Serialization/OldFormatException.cs @@ -0,0 +1,66 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +using ModernKeePassLib.Resources; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Serialization +{ + public sealed class OldFormatException : Exception + { + private string m_strFormat = string.Empty; + private OldFormatType m_type = OldFormatType.Unknown; + + public enum OldFormatType + { + Unknown = 0, + KeePass1x = 1 + } + + public override string Message + { + get + { + string str = KLRes.OldFormat + ((m_strFormat.Length > 0) ? + (@" (" + m_strFormat + @")") : string.Empty) + "."; + + if(m_type == OldFormatType.KeePass1x) + str += MessageService.NewParagraph + KLRes.KeePass1xHint; + + return str; + } + } + + public OldFormatException(string strFormatName) + { + if(strFormatName != null) m_strFormat = strFormatName; + } + + public OldFormatException(string strFormatName, OldFormatType t) + { + if(strFormatName != null) m_strFormat = strFormatName; + + m_type = t; + } + } +} diff --git a/ModernKeePassLib/Translation/KPControlCustomization.cs b/ModernKeePassLib/Translation/KPControlCustomization.cs new file mode 100644 index 0000000..1f98346 --- /dev/null +++ b/ModernKeePassLib/Translation/KPControlCustomization.cs @@ -0,0 +1,400 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Globalization; +using System.Text; +using System.Xml.Serialization; + +#if !KeePassUAP +using System.Drawing; +using System.Windows.Forms; +#endif + +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Translation +{ + public sealed class KpccLayout + { + public enum LayoutParameterEx + { + X, Y, Width, Height + } + + private const string m_strControlRelative = @"%c"; + + internal const NumberStyles m_nsParser = (NumberStyles.AllowLeadingSign | + NumberStyles.AllowDecimalPoint); + internal static readonly CultureInfo m_lclInv = CultureInfo.InvariantCulture; + + private string m_strPosX = string.Empty; + [XmlAttribute] + [DefaultValue("")] + public string X + { + get { return m_strPosX; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strPosX = value; + } + } + + private string m_strPosY = string.Empty; + [XmlAttribute] + [DefaultValue("")] + public string Y + { + get { return m_strPosY; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strPosY = value; + } + } + + private string m_strSizeW = string.Empty; + [XmlAttribute] + [DefaultValue("")] + public string Width + { + get { return m_strSizeW; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strSizeW = value; + } + } + + private string m_strSizeH = string.Empty; + [XmlAttribute] + [DefaultValue("")] + public string Height + { + get { return m_strSizeH; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strSizeH = value; + } + } + + public void SetControlRelativeValue(LayoutParameterEx lp, string strValue) + { + Debug.Assert(strValue != null); + if(strValue == null) throw new ArgumentNullException("strValue"); + + if(strValue.Length > 0) strValue += m_strControlRelative; + + if(lp == LayoutParameterEx.X) m_strPosX = strValue; + else if(lp == LayoutParameterEx.Y) m_strPosY = strValue; + else if(lp == LayoutParameterEx.Width) m_strSizeW = strValue; + else if(lp == LayoutParameterEx.Height) m_strSizeH = strValue; + else { Debug.Assert(false); } + } + +#if (!KeePassLibSD && !KeePassUAP) + internal void ApplyTo(Control c) + { + Debug.Assert(c != null); if(c == null) return; + + int? v; + v = GetModControlParameter(c, LayoutParameterEx.X, m_strPosX); + if(v.HasValue) c.Left = v.Value; + v = GetModControlParameter(c, LayoutParameterEx.Y, m_strPosY); + if(v.HasValue) c.Top = v.Value; + v = GetModControlParameter(c, LayoutParameterEx.Width, m_strSizeW); + if(v.HasValue) c.Width = v.Value; + v = GetModControlParameter(c, LayoutParameterEx.Height, m_strSizeH); + if(v.HasValue) c.Height = v.Value; + } + + private static int? GetModControlParameter(Control c, LayoutParameterEx p, + string strModParam) + { + if(strModParam.Length == 0) return null; + + Debug.Assert(c.Left == c.Location.X); + Debug.Assert(c.Top == c.Location.Y); + Debug.Assert(c.Width == c.Size.Width); + Debug.Assert(c.Height == c.Size.Height); + + int iPrev; + if(p == LayoutParameterEx.X) iPrev = c.Left; + else if(p == LayoutParameterEx.Y) iPrev = c.Top; + else if(p == LayoutParameterEx.Width) iPrev = c.Width; + else if(p == LayoutParameterEx.Height) iPrev = c.Height; + else { Debug.Assert(false); return null; } + + double? dRel = ToControlRelativePercent(strModParam); + if(dRel.HasValue) + return (iPrev + (int)((dRel.Value * (double)iPrev) / 100.0)); + + Debug.Assert(false); + return null; + } + + public static double? ToControlRelativePercent(string strEncoded) + { + Debug.Assert(strEncoded != null); + if(strEncoded == null) throw new ArgumentNullException("strEncoded"); + + if(strEncoded.Length == 0) return null; + + if(strEncoded.EndsWith(m_strControlRelative)) + { + string strValue = strEncoded.Substring(0, strEncoded.Length - + m_strControlRelative.Length); + if((strValue.Length == 1) && (strValue == "-")) + strValue = "0"; + + double dRel; + if(double.TryParse(strValue, m_nsParser, m_lclInv, out dRel)) + { + return dRel; + } + else + { + Debug.Assert(false); + return null; + } + } + + Debug.Assert(false); + return null; + } +#endif + + public static string ToControlRelativeString(string strEncoded) + { + Debug.Assert(strEncoded != null); + if(strEncoded == null) throw new ArgumentNullException("strEncoded"); + + if(strEncoded.Length == 0) return string.Empty; + + if(strEncoded.EndsWith(m_strControlRelative)) + return strEncoded.Substring(0, strEncoded.Length - + m_strControlRelative.Length); + + Debug.Assert(false); + return string.Empty; + } + } + + public sealed class KPControlCustomization : IComparable + { + private string m_strMemberName = string.Empty; + /// + /// Member variable name of the control to be translated. + /// + [XmlAttribute] + public string Name + { + get { return m_strMemberName; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strMemberName = value; + } + } + + private string m_strHash = string.Empty; + [XmlAttribute] + public string BaseHash + { + get { return m_strHash; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strHash = value; + } + } + + private string m_strText = string.Empty; + [DefaultValue("")] + public string Text + { + get { return m_strText; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strText = value; + } + } + + private string m_strEngText = string.Empty; + [XmlIgnore] + public string TextEnglish + { + get { return m_strEngText; } + set { m_strEngText = value; } + } + + private KpccLayout m_layout = new KpccLayout(); + public KpccLayout Layout + { + get { return m_layout; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_layout = value; + } + } + + public int CompareTo(KPControlCustomization kpOther) + { + if(kpOther == null) { Debug.Assert(false); return 1; } + + return m_strMemberName.CompareTo(kpOther.Name); + } + +#if (!KeePassLibSD && !KeePassUAP) + private static readonly Type[] m_vTextControls = new Type[] { + typeof(MenuStrip), typeof(PictureBox), typeof(ListView), + typeof(TreeView), typeof(ToolStrip), typeof(WebBrowser), + typeof(Panel), typeof(StatusStrip), typeof(ProgressBar), + typeof(NumericUpDown), typeof(TabControl) + }; + + public static bool ControlSupportsText(object oControl) + { + if(oControl == null) return false; + + Type t = oControl.GetType(); + for(int i = 0; i < m_vTextControls.Length; ++i) + { + if(t == m_vTextControls[i]) return false; + } + + return true; + } + + // Name-unchecked (!) property application method + internal void ApplyTo(Control c) + { + if((m_strText.Length > 0) && ControlSupportsText(c) && + (c.Text.Length > 0)) + { + c.Text = m_strText; + } + + m_layout.ApplyTo(c); + } + + public static string HashControl(Control c) + { + if(c == null) { Debug.Assert(false); return string.Empty; } + + StringBuilder sb = new StringBuilder(); + WriteCpiParam(sb, c.Text); + + if(c is Form) + { + WriteCpiParam(sb, c.ClientSize.Width.ToString(KpccLayout.m_lclInv)); + WriteCpiParam(sb, c.ClientSize.Height.ToString(KpccLayout.m_lclInv)); + } + else // Normal control + { + WriteCpiParam(sb, c.Left.ToString(KpccLayout.m_lclInv)); + WriteCpiParam(sb, c.Top.ToString(KpccLayout.m_lclInv)); + WriteCpiParam(sb, c.Width.ToString(KpccLayout.m_lclInv)); + WriteCpiParam(sb, c.Height.ToString(KpccLayout.m_lclInv)); + WriteCpiParam(sb, c.Dock.ToString()); + } + + WriteCpiParam(sb, c.Font.Name); + WriteCpiParam(sb, c.Font.SizeInPoints.ToString(KpccLayout.m_lclInv)); + WriteCpiParam(sb, c.Font.Bold ? "B" : "N"); + WriteCpiParam(sb, c.Font.Italic ? "I" : "N"); + WriteCpiParam(sb, c.Font.Underline ? "U" : "N"); + WriteCpiParam(sb, c.Font.Strikeout ? "S" : "N"); + + WriteControlDependentParams(sb, c); + + byte[] pb = StrUtil.Utf8.GetBytes(sb.ToString()); + byte[] pbSha = CryptoUtil.HashSha256(pb); + + // See also MatchHash + return "v1:" + Convert.ToBase64String(pbSha, 0, 3, + Base64FormattingOptions.None); + } + + private static void WriteControlDependentParams(StringBuilder sb, Control c) + { + CheckBox cb = (c as CheckBox); + RadioButton rb = (c as RadioButton); + Button btn = (c as Button); + Label l = (c as Label); + LinkLabel ll = (c as LinkLabel); + + if(cb != null) + { + WriteCpiParam(sb, cb.AutoSize ? "A" : "F"); + WriteCpiParam(sb, cb.TextAlign.ToString()); + WriteCpiParam(sb, cb.TextImageRelation.ToString()); + WriteCpiParam(sb, cb.Appearance.ToString()); + WriteCpiParam(sb, cb.CheckAlign.ToString()); + } + else if(rb != null) + { + WriteCpiParam(sb, rb.AutoSize ? "A" : "F"); + WriteCpiParam(sb, rb.TextAlign.ToString()); + WriteCpiParam(sb, rb.TextImageRelation.ToString()); + WriteCpiParam(sb, rb.Appearance.ToString()); + WriteCpiParam(sb, rb.CheckAlign.ToString()); + } + else if(btn != null) + { + WriteCpiParam(sb, btn.AutoSize ? "A" : "F"); + WriteCpiParam(sb, btn.TextAlign.ToString()); + WriteCpiParam(sb, btn.TextImageRelation.ToString()); + } + else if(l != null) + { + WriteCpiParam(sb, l.AutoSize ? "A" : "F"); + WriteCpiParam(sb, l.TextAlign.ToString()); + } + else if(ll != null) + { + WriteCpiParam(sb, ll.AutoSize ? "A" : "F"); + WriteCpiParam(sb, ll.TextAlign.ToString()); + } + } + + private static void WriteCpiParam(StringBuilder sb, string strProp) + { + sb.Append('/'); + sb.Append(strProp); + } + + public bool MatchHash(string strHash) + { + if(strHash == null) throw new ArgumentNullException("strHash"); + + // Currently only v1: is supported, see HashControl + return (m_strHash == strHash); + } +#endif + } +} diff --git a/ModernKeePassLib/Translation/KPFormCustomization.cs b/ModernKeePassLib/Translation/KPFormCustomization.cs new file mode 100644 index 0000000..78a23c6 --- /dev/null +++ b/ModernKeePassLib/Translation/KPFormCustomization.cs @@ -0,0 +1,108 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; +using System.Xml.Serialization; + +#if !KeePassUAP +using System.Windows.Forms; +#endif + +namespace ModernKeePassLib.Translation +{ + public sealed class KPFormCustomization + { + private string m_strFQName = string.Empty; + /// + /// The fully qualified name of the form. + /// + [XmlAttribute] + public string FullName + { + get { return m_strFQName; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strFQName = value; + } + } + + private KPControlCustomization m_ccWindow = new KPControlCustomization(); + public KPControlCustomization Window + { + get { return m_ccWindow; } + set { m_ccWindow = value; } + } + + private List m_vControls = + new List(); + [XmlArray("ChildControls")] + [XmlArrayItem("Control")] + public List Controls + { + get { return m_vControls; } + set + { + if(value == null) throw new ArgumentNullException("value"); + + m_vControls = value; + } + } + +#if (!KeePassLibSD && !KeePassUAP) + private Form m_formEnglish = null; + [XmlIgnore] + public Form FormEnglish + { + get { return m_formEnglish; } + set { m_formEnglish = value; } + } + + public void ApplyTo(Form form) + { + Debug.Assert(form != null); if(form == null) throw new ArgumentNullException("form"); + + // Not supported by TrlUtil (preview form): + // Debug.Assert(form.GetType().FullName == m_strFQName); + + m_ccWindow.ApplyTo(form); + + if(m_vControls.Count == 0) return; + foreach(Control c in form.Controls) ApplyToControl(c); + } + + private void ApplyToControl(Control c) + { + foreach(KPControlCustomization cc in m_vControls) + { + if(c.Name == cc.Name) + { + cc.ApplyTo(c); + break; + } + } + + foreach(Control cSub in c.Controls) ApplyToControl(cSub); + } +#endif + } +} diff --git a/ModernKeePassLib/Translation/KPStringTable.cs b/ModernKeePassLib/Translation/KPStringTable.cs new file mode 100644 index 0000000..649d654 --- /dev/null +++ b/ModernKeePassLib/Translation/KPStringTable.cs @@ -0,0 +1,102 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; +using System.Xml.Serialization; + +#if !KeePassUAP +using System.Windows.Forms; +#endif + +namespace ModernKeePassLib.Translation +{ + public sealed class KPStringTable + { + private string m_strName = string.Empty; + [XmlAttribute] + public string Name + { + get { return m_strName; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strName = value; + } + } + + private List m_vItems = new List(); + + [XmlArrayItem("Data")] + public List Strings + { + get { return m_vItems; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_vItems = value; + } + } + + public Dictionary ToDictionary() + { + Dictionary dict = new Dictionary(); + + foreach(KPStringTableItem kpstItem in m_vItems) + { + if(kpstItem.Value.Length > 0) + dict[kpstItem.Name] = kpstItem.Value; + } + + return dict; + } + +#if (!KeePassLibSD && !KeePassUAP) + public void ApplyTo(ToolStripItemCollection tsic) + { + if(tsic == null) throw new ArgumentNullException("tsic"); + + Dictionary dict = this.ToDictionary(); + if(dict.Count == 0) return; + + this.ApplyTo(tsic, dict); + } + + private void ApplyTo(ToolStripItemCollection tsic, Dictionary dict) + { + if(tsic == null) return; + + foreach(ToolStripItem tsi in tsic) + { + if(tsi.Text.Length == 0) continue; + + string strTrl; + if(dict.TryGetValue(tsi.Name, out strTrl)) + tsi.Text = strTrl; + + ToolStripMenuItem tsmi = tsi as ToolStripMenuItem; + if((tsmi != null) && (tsmi.DropDownItems != null)) + this.ApplyTo(tsmi.DropDownItems); + } + } +#endif + } +} diff --git a/ModernKeePassLib/Translation/KPStringTableItem.cs b/ModernKeePassLib/Translation/KPStringTableItem.cs new file mode 100644 index 0000000..a32119c --- /dev/null +++ b/ModernKeePassLib/Translation/KPStringTableItem.cs @@ -0,0 +1,51 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; +using System.Xml.Serialization; + +namespace ModernKeePassLib.Translation +{ + public sealed class KPStringTableItem + { + private string m_strName = string.Empty; + public string Name + { + get { return m_strName; } + set { m_strName = value; } + } + + private string m_strValue = string.Empty; + public string Value + { + get { return m_strValue; } + set { m_strValue = value; } + } + + private string m_strEnglish = string.Empty; + [XmlIgnore] + public string ValueEnglish + { + get { return m_strEnglish; } + set { m_strEnglish = value; } + } + } +} diff --git a/ModernKeePassLib/Translation/KPTranslation.cs b/ModernKeePassLib/Translation/KPTranslation.cs new file mode 100644 index 0000000..a02b7d7 --- /dev/null +++ b/ModernKeePassLib/Translation/KPTranslation.cs @@ -0,0 +1,325 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; +using System.Xml.Serialization; + +#if !KeePassUAP +using System.Drawing; +using System.Windows.Forms; +#endif + +#if KeePassLibSD +using ICSharpCode.SharpZipLib.GZip; +#else +using System.IO.Compression; +#endif + +using ModernKeePassLib.Interfaces; +using ModernKeePassLib.Utility; + +namespace ModernKeePassLib.Translation +{ + [XmlRoot("Translation")] + public sealed class KPTranslation + { + public static readonly string FileExtension = "lngx"; + + private KPTranslationProperties m_props = new KPTranslationProperties(); + public KPTranslationProperties Properties + { + get { return m_props; } + set + { + if(value == null) throw new ArgumentNullException("value"); + + m_props = value; + } + } + + private List m_vStringTables = new List(); + + [XmlArrayItem("StringTable")] + public List StringTables + { + get { return m_vStringTables; } + set + { + if(value == null) throw new ArgumentNullException("value"); + + m_vStringTables = value; + } + } + + private List m_vForms = new List(); + + [XmlArrayItem("Form")] + public List Forms + { + get { return m_vForms; } + set + { + if(value == null) throw new ArgumentNullException("value"); + + m_vForms = value; + } + } + + private string m_strUnusedText = string.Empty; + [DefaultValue("")] + public string UnusedText + { + get { return m_strUnusedText; } + set + { + if(value == null) throw new ArgumentNullException("value"); + + m_strUnusedText = value; + } + } + + public static void Save(KPTranslation kpTrl, string strFileName, + IXmlSerializerEx xs) + { + using(FileStream fs = new FileStream(strFileName, FileMode.Create, + FileAccess.Write, FileShare.None)) + { + Save(kpTrl, fs, xs); + } + } + + public static void Save(KPTranslation kpTrl, Stream sOut, + IXmlSerializerEx xs) + { + if(xs == null) throw new ArgumentNullException("xs"); + +#if !KeePassLibSD + using(GZipStream gz = new GZipStream(sOut, CompressionMode.Compress)) +#else + using(GZipOutputStream gz = new GZipOutputStream(sOut)) +#endif + { + using(XmlWriter xw = XmlUtilEx.CreateXmlWriter(gz)) + { + xs.Serialize(xw, kpTrl); + } + } + + sOut.Close(); + } + + public static KPTranslation Load(string strFile, IXmlSerializerEx xs) + { + KPTranslation kpTrl = null; + + using(FileStream fs = new FileStream(strFile, FileMode.Open, + FileAccess.Read, FileShare.Read)) + { + kpTrl = Load(fs, xs); + } + + return kpTrl; + } + + public static KPTranslation Load(Stream s, IXmlSerializerEx xs) + { + if(xs == null) throw new ArgumentNullException("xs"); + + KPTranslation kpTrl = null; + +#if !KeePassLibSD + using(GZipStream gz = new GZipStream(s, CompressionMode.Decompress)) +#else + using(GZipInputStream gz = new GZipInputStream(s)) +#endif + { + kpTrl = (xs.Deserialize(gz) as KPTranslation); + } + + s.Close(); + return kpTrl; + } + + public Dictionary SafeGetStringTableDictionary( + string strTableName) + { + foreach(KPStringTable kpst in m_vStringTables) + { + if(kpst.Name == strTableName) return kpst.ToDictionary(); + } + + return new Dictionary(); + } + +#if (!KeePassLibSD && !KeePassUAP) + public void ApplyTo(Form form) + { + if(form == null) throw new ArgumentNullException("form"); + + if(m_props.RightToLeft) + { + try + { + form.RightToLeft = RightToLeft.Yes; + form.RightToLeftLayout = true; + } + catch(Exception) { Debug.Assert(false); } + } + + string strTypeName = form.GetType().FullName; + foreach(KPFormCustomization kpfc in m_vForms) + { + if(kpfc.FullName == strTypeName) + { + kpfc.ApplyTo(form); + break; + } + } + + if(m_props.RightToLeft) + { + try { RtlApplyToControls(form.Controls); } + catch(Exception) { Debug.Assert(false); } + } + } + + private static void RtlApplyToControls(Control.ControlCollection cc) + { + foreach(Control c in cc) + { + if(c.Controls.Count > 0) RtlApplyToControls(c.Controls); + + if(c is DateTimePicker) + ((DateTimePicker)c).RightToLeftLayout = true; + else if(c is ListView) + ((ListView)c).RightToLeftLayout = true; + else if(c is MonthCalendar) + ((MonthCalendar)c).RightToLeftLayout = true; + else if(c is ProgressBar) + ((ProgressBar)c).RightToLeftLayout = true; + else if(c is TabControl) + ((TabControl)c).RightToLeftLayout = true; + else if(c is TrackBar) + ((TrackBar)c).RightToLeftLayout = true; + else if(c is TreeView) + ((TreeView)c).RightToLeftLayout = true; + // else if(c is ToolStrip) + // RtlApplyToToolStripItems(((ToolStrip)c).Items); + /* else if(c is Button) // Also see Label + { + Button btn = (c as Button); + Image img = btn.Image; + if(img != null) + { + Image imgNew = (Image)img.Clone(); + imgNew.RotateFlip(RotateFlipType.RotateNoneFlipX); + btn.Image = imgNew; + } + } + else if(c is Label) // Also see Button + { + Label lbl = (c as Label); + Image img = lbl.Image; + if(img != null) + { + Image imgNew = (Image)img.Clone(); + imgNew.RotateFlip(RotateFlipType.RotateNoneFlipX); + lbl.Image = imgNew; + } + } */ + + if(IsRtlMoveChildsRequired(c)) RtlMoveChildControls(c); + } + } + + internal static bool IsRtlMoveChildsRequired(Control c) + { + if(c == null) { Debug.Assert(false); return false; } + + return ((c is GroupBox) || (c is Panel)); + } + + private static void RtlMoveChildControls(Control cParent) + { + int nParentWidth = cParent.Size.Width; + + foreach(Control c in cParent.Controls) + { + DockStyle ds = c.Dock; + if(ds == DockStyle.Left) + c.Dock = DockStyle.Right; + else if(ds == DockStyle.Right) + c.Dock = DockStyle.Left; + else + { + Point ptCur = c.Location; + c.Location = new Point(nParentWidth - c.Size.Width - ptCur.X, ptCur.Y); + } + } + } + + /* private static readonly string[] g_vRtlMirrorItemNames = new string[] { }; + private static void RtlApplyToToolStripItems(ToolStripItemCollection tsic) + { + foreach(ToolStripItem tsi in tsic) + { + if(tsi == null) { Debug.Assert(false); continue; } + + if(Array.IndexOf(g_vRtlMirrorItemNames, tsi.Name) >= 0) + tsi.RightToLeftAutoMirrorImage = true; + + ToolStripDropDownItem tsdd = (tsi as ToolStripDropDownItem); + if(tsdd != null) + RtlApplyToToolStripItems(tsdd.DropDownItems); + } + } */ + + public void ApplyTo(string strTableName, ToolStripItemCollection tsic) + { + if(tsic == null) throw new ArgumentNullException("tsic"); + + KPStringTable kpst = null; + foreach(KPStringTable kpstEnum in m_vStringTables) + { + if(kpstEnum.Name == strTableName) + { + kpst = kpstEnum; + break; + } + } + + if(kpst != null) kpst.ApplyTo(tsic); + } +#endif + + internal bool IsFor(string strIso6391Code) + { + if(strIso6391Code == null) { Debug.Assert(false); return false; } + + return string.Equals(strIso6391Code, m_props.Iso6391Code, + StrUtil.CaseIgnoreCmp); + } + } +} diff --git a/ModernKeePassLib/Translation/KPTranslationProperties.cs b/ModernKeePassLib/Translation/KPTranslationProperties.cs new file mode 100644 index 0000000..904fe03 --- /dev/null +++ b/ModernKeePassLib/Translation/KPTranslationProperties.cs @@ -0,0 +1,105 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +namespace ModernKeePassLib.Translation +{ + public sealed class KPTranslationProperties + { + private string m_strApp = string.Empty; + public string Application + { + get { return m_strApp; } + set { m_strApp = value; } + } + + private string m_strForVersion = string.Empty; + public string ApplicationVersion + { + get { return m_strForVersion; } + set { m_strForVersion = value; } + } + + private string m_strNameEnglish = string.Empty; + public string NameEnglish + { + get { return m_strNameEnglish; } + set { m_strNameEnglish = value; } + } + + private string m_strNameNative = string.Empty; + public string NameNative + { + get { return m_strNameNative; } + set { m_strNameNative = value; } + } + + private string m_strIso6391Code = string.Empty; + public string Iso6391Code + { + get { return m_strIso6391Code; } + set { m_strIso6391Code = value; } + } + + private bool m_bRtl = false; + public bool RightToLeft + { + get { return m_bRtl; } + set { m_bRtl = value; } + } + + private string m_strAuthorName = string.Empty; + public string AuthorName + { + get { return m_strAuthorName; } + set { m_strAuthorName = value; } + } + + private string m_strAuthorContact = string.Empty; + public string AuthorContact + { + get { return m_strAuthorContact; } + set { m_strAuthorContact = value; } + } + + private string m_strGen = string.Empty; + public string Generator + { + get { return m_strGen; } + set { m_strGen = value; } + } + + private string m_strUuid = string.Empty; + public string FileUuid + { + get { return m_strUuid; } + set { m_strUuid = value; } + } + + private string m_strLastModified = string.Empty; + public string LastModified + { + get { return m_strLastModified; } + set { m_strLastModified = value; } + } + } +} diff --git a/ModernKeePassLib/Utility/AppLogEx.cs b/ModernKeePassLib/Utility/AppLogEx.cs new file mode 100644 index 0000000..8208c79 --- /dev/null +++ b/ModernKeePassLib/Utility/AppLogEx.cs @@ -0,0 +1,103 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; + +#if !KeePassLibSD +using System.IO.Compression; +#endif + +namespace ModernKeePassLib.Utility +{ + /// + /// Application-wide logging services. + /// + public static class AppLogEx + { + private static StreamWriter m_swOut = null; + + public static void Open(string strPrefix) + { + // Logging is not enabled in normal builds of KeePass! + /* + AppLogEx.Close(); + + Debug.Assert(strPrefix != null); + if(strPrefix == null) strPrefix = "Log"; + + try + { + string strDirSep = string.Empty; + strDirSep += UrlUtil.LocalDirSepChar; + + string strTemp = UrlUtil.GetTempPath(); + if(!strTemp.EndsWith(strDirSep)) + strTemp += strDirSep; + + string strPath = strTemp + strPrefix + "-"; + Debug.Assert(strPath.IndexOf('/') < 0); + + DateTime dtNow = DateTime.UtcNow; + string strTime = dtNow.ToString("s"); + strTime = strTime.Replace('T', '-'); + strTime = strTime.Replace(':', '-'); + + strPath += strTime + "-" + Environment.TickCount.ToString( + NumberFormatInfo.InvariantInfo) + ".log.gz"; + + FileStream fsOut = new FileStream(strPath, FileMode.Create, + FileAccess.Write, FileShare.None); + GZipStream gz = new GZipStream(fsOut, CompressionMode.Compress); + m_swOut = new StreamWriter(gz); + + AppLogEx.Log("Started logging on " + dtNow.ToString("s") + "."); + } + catch(Exception) { Debug.Assert(false); } + */ + } + + public static void Close() + { + if(m_swOut == null) return; + + m_swOut.Dispose(); + m_swOut = null; + } + + public static void Log(string strText) + { + if(m_swOut == null) return; + + if(strText == null) m_swOut.WriteLine(); + else m_swOut.WriteLine(strText); + } + + public static void Log(Exception ex) + { + if(m_swOut == null) return; + + if(ex == null) m_swOut.WriteLine(); + else m_swOut.WriteLine(ex.ToString()); + } + } +} diff --git a/ModernKeePassLib/Utility/ColorTranslator.cs b/ModernKeePassLib/Utility/ColorTranslator.cs new file mode 100644 index 0000000..842345c --- /dev/null +++ b/ModernKeePassLib/Utility/ColorTranslator.cs @@ -0,0 +1,57 @@ +using System; +using System.Drawing; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace ModernKeePassLib.Utility +{ + /// + /// Replacement for System.Drawing.ColorTranslator. + /// + /// + /// Colors are stored in the kdbx database file in HTML format (#XXXXXX). + /// + public static class ColorTranslator + { + static Regex longForm = new Regex("^#([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})$"); + + /// + /// Converts an HTML color value to a Color. + /// + /// The Color. + /// HTML color code. + /// If htmlColor is null. + /// If htmlColor did not match the pattern "#XXXXXX". + /// + /// Currently only understands "#XXXXXX". "#XXX" or named colors will + /// throw and exception. + /// + public static Color FromHtml(string htmlColor) + { + if (htmlColor == null) + throw new ArgumentNullException("htmlColor"); + Match match = longForm.Match(htmlColor); + if (match.Success) { + var r = int.Parse(match.Groups[1].Value, NumberStyles.HexNumber); + var g = int.Parse(match.Groups[2].Value, NumberStyles.HexNumber); + var b = int.Parse(match.Groups[3].Value, NumberStyles.HexNumber); + return Color.FromArgb(r, g, b); + } + throw new ArgumentException(string.Format("Could not parse HTML color '{0}'.", htmlColor), "htmlColor"); + } + + /// + /// Converts a color to an HTML color code. + /// + /// String containing the color code. + /// The Color to convert + /// + /// The string is in the format "#XXXXXX" + /// + public static string ToHtml(Color htmlColor) + { + return string.Format("#{0:x2}{1:x2}{2:x2}", htmlColor.R, htmlColor.G, htmlColor.B); + } + } +} + diff --git a/ModernKeePassLib/Utility/GfxUtil.PCL.cs b/ModernKeePassLib/Utility/GfxUtil.PCL.cs new file mode 100644 index 0000000..b06a388 --- /dev/null +++ b/ModernKeePassLib/Utility/GfxUtil.PCL.cs @@ -0,0 +1,17 @@ +using Windows.UI.Xaml.Controls; + +namespace ModernKeePassLib.Utility +{ + public class GfxUtil + { + public static Image LoadImage(byte[] pb) + { + return null; + } + + public static Image ScaleImage(Image m_imgOrg, int? w, int? h, ScaleTransformFlags flags) + { + return null; + } + } +} diff --git a/ModernKeePassLib/Utility/GfxUtil.cs b/ModernKeePassLib/Utility/GfxUtil.cs new file mode 100644 index 0000000..9056a5a --- /dev/null +++ b/ModernKeePassLib/Utility/GfxUtil.cs @@ -0,0 +1,441 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; + +#if !KeePassUAP +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +#endif + +namespace ModernKeePassLib.Utility +{ + public static class GfxUtil + { +#if (!KeePassLibSD && !KeePassUAP) + private sealed class GfxImage + { + public byte[] Data; + + public int Width; + public int Height; + + public GfxImage(byte[] pbData, int w, int h) + { + this.Data = pbData; + this.Width = w; + this.Height = h; + } + +#if DEBUG + // For debugger display + public override string ToString() + { + return (this.Width.ToString() + "x" + this.Height.ToString()); + } +#endif + } +#endif + +#if KeePassUAP + public static Image LoadImage(byte[] pb) + { + if(pb == null) throw new ArgumentNullException("pb"); + + MemoryStream ms = new MemoryStream(pb, false); + try { return Image.FromStream(ms); } + finally { ms.Close(); } + } +#else + public static Image LoadImage(byte[] pb) + { + if(pb == null) throw new ArgumentNullException("pb"); + +#if !KeePassLibSD + // First try to load the data as ICO and afterwards as + // normal image, because trying to load an ICO using + // the normal image loading methods can result in a + // low resolution image + try + { + Image imgIco = ExtractBestImageFromIco(pb); + if(imgIco != null) return imgIco; + } + catch(Exception) { Debug.Assert(false); } +#endif + + MemoryStream ms = new MemoryStream(pb, false); + try { return LoadImagePriv(ms); } + finally { ms.Close(); } + } + + private static Image LoadImagePriv(Stream s) + { + // Image.FromStream wants the stream to be open during + // the whole lifetime of the image; as we can't guarantee + // this, we make a copy of the image + Image imgSrc = null; + try + { +#if !KeePassLibSD + imgSrc = Image.FromStream(s); + Bitmap bmp = new Bitmap(imgSrc.Width, imgSrc.Height, + PixelFormat.Format32bppArgb); + + try + { + bmp.SetResolution(imgSrc.HorizontalResolution, + imgSrc.VerticalResolution); + Debug.Assert(bmp.Size == imgSrc.Size); + } + catch(Exception) { Debug.Assert(false); } +#else + imgSrc = new Bitmap(s); + Bitmap bmp = new Bitmap(imgSrc.Width, imgSrc.Height); +#endif + + using(Graphics g = Graphics.FromImage(bmp)) + { + g.Clear(Color.Transparent); + +#if !KeePassLibSD + g.DrawImageUnscaled(imgSrc, 0, 0); +#else + g.DrawImage(imgSrc, 0, 0); +#endif + } + + return bmp; + } + finally { if(imgSrc != null) imgSrc.Dispose(); } + } + +#if !KeePassLibSD + private static Image ExtractBestImageFromIco(byte[] pb) + { + List l = UnpackIco(pb); + if((l == null) || (l.Count == 0)) return null; + + long qMax = 0; + foreach(GfxImage gi in l) + { + if(gi.Width == 0) gi.Width = 256; + if(gi.Height == 0) gi.Height = 256; + + qMax = Math.Max(qMax, (long)gi.Width * (long)gi.Height); + } + + byte[] pbHdrPng = new byte[] { + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A + }; + byte[] pbHdrJpeg = new byte[] { 0xFF, 0xD8, 0xFF }; + + Image imgBest = null; + int bppBest = -1; + + foreach(GfxImage gi in l) + { + if(((long)gi.Width * (long)gi.Height) < qMax) continue; + + byte[] pbImg = gi.Data; + Image img = null; + try + { + if((pbImg.Length > pbHdrPng.Length) && + MemUtil.ArraysEqual(pbHdrPng, + MemUtil.Mid(pbImg, 0, pbHdrPng.Length))) + img = GfxUtil.LoadImage(pbImg); + else if((pbImg.Length > pbHdrJpeg.Length) && + MemUtil.ArraysEqual(pbHdrJpeg, + MemUtil.Mid(pbImg, 0, pbHdrJpeg.Length))) + img = GfxUtil.LoadImage(pbImg); + else + { + using(MemoryStream ms = new MemoryStream(pb, false)) + { + using(Icon ico = new Icon(ms, gi.Width, gi.Height)) + { + img = ico.ToBitmap(); + } + } + } + } + catch(Exception) { Debug.Assert(false); } + + if(img == null) continue; + + if((img.Width < gi.Width) || (img.Height < gi.Height)) + { + Debug.Assert(false); + img.Dispose(); + continue; + } + + int bpp = GetBitsPerPixel(img.PixelFormat); + if(bpp > bppBest) + { + if(imgBest != null) imgBest.Dispose(); + + imgBest = img; + bppBest = bpp; + } + else img.Dispose(); + } + + return imgBest; + } + + private static List UnpackIco(byte[] pb) + { + if(pb == null) { Debug.Assert(false); return null; } + + const int SizeICONDIR = 6; + const int SizeICONDIRENTRY = 16; + + if(pb.Length < SizeICONDIR) return null; + if(MemUtil.BytesToUInt16(pb, 0) != 0) return null; // Reserved, 0 + if(MemUtil.BytesToUInt16(pb, 2) != 1) return null; // ICO type, 1 + + int n = MemUtil.BytesToUInt16(pb, 4); + if(n < 0) { Debug.Assert(false); return null; } + + int cbDir = SizeICONDIR + (n * SizeICONDIRENTRY); + if(pb.Length < cbDir) return null; + + List l = new List(); + int iOffset = SizeICONDIR; + for(int i = 0; i < n; ++i) + { + int w = pb[iOffset]; + int h = pb[iOffset + 1]; + if((w < 0) || (h < 0)) { Debug.Assert(false); return null; } + + int cb = MemUtil.BytesToInt32(pb, iOffset + 8); + if(cb <= 0) return null; // Data must have header (even BMP) + + int p = MemUtil.BytesToInt32(pb, iOffset + 12); + if(p < cbDir) return null; + if((p + cb) > pb.Length) return null; + + try + { + byte[] pbImage = MemUtil.Mid(pb, p, cb); + GfxImage img = new GfxImage(pbImage, w, h); + l.Add(img); + } + catch(Exception) { Debug.Assert(false); return null; } + + iOffset += SizeICONDIRENTRY; + } + + return l; + } + + private static int GetBitsPerPixel(PixelFormat f) + { + int bpp = 0; + switch(f) + { + case PixelFormat.Format1bppIndexed: + bpp = 1; + break; + + case PixelFormat.Format4bppIndexed: + bpp = 4; + break; + + case PixelFormat.Format8bppIndexed: + bpp = 8; + break; + + case PixelFormat.Format16bppArgb1555: + case PixelFormat.Format16bppGrayScale: + case PixelFormat.Format16bppRgb555: + case PixelFormat.Format16bppRgb565: + bpp = 16; + break; + + case PixelFormat.Format24bppRgb: + bpp = 24; + break; + + case PixelFormat.Format32bppArgb: + case PixelFormat.Format32bppPArgb: + case PixelFormat.Format32bppRgb: + bpp = 32; + break; + + case PixelFormat.Format48bppRgb: + bpp = 48; + break; + + case PixelFormat.Format64bppArgb: + case PixelFormat.Format64bppPArgb: + bpp = 64; + break; + + default: + Debug.Assert(false); + break; + } + + return bpp; + } + + public static Image ScaleImage(Image img, int w, int h) + { + return ScaleImage(img, w, h, ScaleTransformFlags.None); + } + + /// + /// Resize an image. + /// + /// Image to resize. + /// Width of the returned image. + /// Height of the returned image. + /// Flags to customize scaling behavior. + /// Resized image. This object is always different + /// from (i.e. they can be + /// disposed separately). + public static Image ScaleImage(Image img, int w, int h, + ScaleTransformFlags f) + { + if(img == null) throw new ArgumentNullException("img"); + if(w < 0) throw new ArgumentOutOfRangeException("w"); + if(h < 0) throw new ArgumentOutOfRangeException("h"); + + bool bUIIcon = ((f & ScaleTransformFlags.UIIcon) != + ScaleTransformFlags.None); + + // We must return a Bitmap object for UIUtil.CreateScaledImage + Bitmap bmp = new Bitmap(w, h, PixelFormat.Format32bppArgb); + using(Graphics g = Graphics.FromImage(bmp)) + { + g.Clear(Color.Transparent); + + g.SmoothingMode = SmoothingMode.HighQuality; + g.CompositingQuality = CompositingQuality.HighQuality; + + int wSrc = img.Width; + int hSrc = img.Height; + + InterpolationMode im = InterpolationMode.HighQualityBicubic; + if((wSrc > 0) && (hSrc > 0)) + { + if(bUIIcon && ((w % wSrc) == 0) && ((h % hSrc) == 0)) + im = InterpolationMode.NearestNeighbor; + // else if((w < wSrc) && (h < hSrc)) + // im = InterpolationMode.HighQualityBilinear; + } + else { Debug.Assert(false); } + g.InterpolationMode = im; + + RectangleF rSource = new RectangleF(0.0f, 0.0f, wSrc, hSrc); + RectangleF rDest = new RectangleF(0.0f, 0.0f, w, h); + AdjustScaleRects(ref rSource, ref rDest); + + g.DrawImage(img, rDest, rSource, GraphicsUnit.Pixel); + } + + return bmp; + } + + internal static void AdjustScaleRects(ref RectangleF rSource, + ref RectangleF rDest) + { + // When enlarging images, apply a -0.5 offset to avoid + // the scaled image being cropped on the top/left side; + // when shrinking images, do not apply a -0.5 offset, + // otherwise the image is cropped on the bottom/right + // side; this applies to all interpolation modes + if(rDest.Width > rSource.Width) + rSource.X = rSource.X - 0.5f; + if(rDest.Height > rSource.Height) + rSource.Y = rSource.Y - 0.5f; + + // When shrinking, apply a +0.5 offset, such that the + // scaled image is less cropped on the bottom/right side + if(rDest.Width < rSource.Width) + rSource.X = rSource.X + 0.5f; + if(rDest.Height < rSource.Height) + rSource.Y = rSource.Y + 0.5f; + } + +#if DEBUG + public static Image ScaleTest(Image[] vIcons) + { + Bitmap bmp = new Bitmap(1024, vIcons.Length * (256 + 12), + PixelFormat.Format32bppArgb); + + using(Graphics g = Graphics.FromImage(bmp)) + { + g.Clear(Color.White); + + int[] v = new int[] { 16, 24, 32, 48, 64, 128, 256 }; + + int x; + int y = 8; + + foreach(Image imgIcon in vIcons) + { + if(imgIcon == null) { Debug.Assert(false); continue; } + + x = 128; + + foreach(int q in v) + { + using(Image img = ScaleImage(imgIcon, q, q, + ScaleTransformFlags.UIIcon)) + { + g.DrawImageUnscaled(img, x, y); + } + + x += q + 8; + } + + y += v[v.Length - 1] + 8; + } + } + + return bmp; + } +#endif // DEBUG +#endif // !KeePassLibSD +#endif // KeePassUAP + + internal static string ImageToDataUri(Image img) + { + if(img == null) { Debug.Assert(false); return string.Empty; } + + byte[] pb = null; + using(MemoryStream ms = new MemoryStream()) + { + img.Save(ms, ImageFormat.Png); + pb = ms.ToArray(); + } + + return StrUtil.DataToDataUri(pb, "image/png"); + } + } +} diff --git a/ModernKeePassLib/Utility/MemUtil.cs b/ModernKeePassLib/Utility/MemUtil.cs new file mode 100644 index 0000000..adbe85b --- /dev/null +++ b/ModernKeePassLib/Utility/MemUtil.cs @@ -0,0 +1,885 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Runtime.CompilerServices; +using System.Text; + +#if KeePassLibSD +using KeePassLibSD; +#else +using System.IO.Compression; +#endif + +namespace ModernKeePassLib.Utility +{ + /// + /// Buffer manipulation and conversion routines. + /// + public static class MemUtil + { + public static readonly byte[] EmptyByteArray = new byte[0]; + + internal static readonly ArrayHelperEx ArrayHelperExOfChar = + new ArrayHelperEx(); + + private const MethodImplOptions MioNoOptimize = +#if KeePassLibSD + MethodImplOptions.NoInlining; +#else + (MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining); +#endif + + private static readonly uint[] m_vSBox = new uint[256] { + 0xCD2FACB3, 0xE78A7F5C, 0x6F0803FC, 0xBCF6E230, + 0x3A321712, 0x06403DB1, 0xD2F84B95, 0xDF22A6E4, + 0x07CE9E5B, 0x31788A0C, 0xF683F6F4, 0xEA061F49, + 0xFA5C2ACA, 0x4B9E494E, 0xB0AB25BA, 0x767731FC, + 0x261893A7, 0x2B09F2CE, 0x046261E4, 0x41367B4B, + 0x18A7F225, 0x8F923C0E, 0x5EF3A325, 0x28D0435E, + 0x84C22919, 0xED66873C, 0x8CEDE444, 0x7FC47C24, + 0xFCFC6BA3, 0x676F928D, 0xB4147187, 0xD8FB126E, + 0x7D798D17, 0xFF82E424, 0x1712FA5B, 0xABB09DD5, + 0x8156BA63, 0x84E4D969, 0xC937FB9A, 0x2F1E5BFC, + 0x178ECA11, 0x0E71CD5F, 0x52AAC6F4, 0x71EEFC8F, + 0x7090D749, 0x21CACA31, 0x92996378, 0x0939A8A8, + 0xE9EE1934, 0xD2718616, 0xF2500543, 0xB911873C, + 0xD3CB3EEC, 0x2BA0DBEB, 0xB42D0A27, 0xECE67C0F, + 0x302925F0, 0x6114F839, 0xD39E6307, 0xE28970D6, + 0xEB982F99, 0x941B4CDF, 0xC540E550, 0x8124FC45, + 0x98B025C7, 0xE2BF90EA, 0x4F57C976, 0xCF546FE4, + 0x59566DC8, 0xE3F4360D, 0xF5F9D231, 0xD6180B22, + 0xB54E088A, 0xB5DFE6A6, 0x3637A36F, 0x056E9284, + 0xAFF8FBC5, 0x19E01648, 0x8611F043, 0xDAE44337, + 0xF61B6A1C, 0x257ACD9E, 0xDD35F507, 0xEF05CAFA, + 0x05EB4A83, 0xFC25CA92, 0x0A4728E6, 0x9CF150EF, + 0xAEEF67DE, 0xA9472337, 0x57C81EFE, 0x3E5E009F, + 0x02CB03BB, 0x2BA85674, 0xF21DC251, 0x78C34A34, + 0xABB1F5BF, 0xB95A2FBD, 0x1FB47777, 0x9A96E8AC, + 0x5D2D2838, 0x55AAC92A, 0x99EE324E, 0x10F6214B, + 0x58ABDFB1, 0x2008794D, 0xBEC880F0, 0xE75E5341, + 0x88015C34, 0x352D8FBF, 0x622B7F6C, 0xF5C59EA2, + 0x1F759D8E, 0xADE56159, 0xCC7B4C25, 0x5B8BC48C, + 0xB6BD15AF, 0x3C5B5110, 0xE74A7C3D, 0xEE613161, + 0x156A1C67, 0x72C06817, 0xEA0A6F69, 0x4CECF993, + 0xCA9D554C, 0x8E20361F, 0x42D396B9, 0x595DE578, + 0x749D7955, 0xFD1BA5FD, 0x81FC160E, 0xDB97E28C, + 0x7CF148F7, 0x0B0B3CF5, 0x534DE605, 0x46421066, + 0xD4B68DD1, 0x9E479CE6, 0xAE667A9D, 0xBC082082, + 0xB06DD6EF, 0x20F0F23F, 0xB99E1551, 0xF47A2E3A, + 0x71DA50C6, 0x67B65779, 0x2A8CB376, 0x1EA71EEE, + 0x29ABCD50, 0xB6EB0C6B, 0x23C10511, 0x6F3F2144, + 0x6AF23012, 0xF696BD9E, 0xB94099D8, 0xAD5A9C81, + 0x7A0794FA, 0x7EDF59D6, 0x1E72E574, 0x8561913C, + 0x4E4D568F, 0xEECB9928, 0x9C124D2E, 0x0848B82C, + 0xF1CA395F, 0x9DAF43DC, 0xF77EC323, 0x394E9B59, + 0x7E200946, 0x8B811D68, 0x16DA3305, 0xAB8DE2C3, + 0xE6C53B64, 0x98C2D321, 0x88A97D81, 0xA7106419, + 0x8E52F7BF, 0x8ED262AF, 0x7CCA974E, 0xF0933241, + 0x040DD437, 0xE143B3D4, 0x3019F56F, 0xB741521D, + 0xF1745362, 0x4C435F9F, 0xB4214D0D, 0x0B0C348B, + 0x5051D189, 0x4C30447E, 0x7393D722, 0x95CEDD0B, + 0xDD994E80, 0xC3D22ED9, 0x739CD900, 0x131EB9C4, + 0xEF1062B2, 0x4F0DE436, 0x52920073, 0x9A7F3D80, + 0x896E7B1B, 0x2C8BBE5A, 0xBD304F8A, 0xA993E22C, + 0x134C41A0, 0xFA989E00, 0x39CE9726, 0xFB89FCCF, + 0xE8FBAC97, 0xD4063FFC, 0x935A2B5A, 0x44C8EE83, + 0xCB2BC7B6, 0x02989E92, 0x75478BEA, 0x144378D0, + 0xD853C087, 0x8897A34E, 0xDD23629D, 0xBDE2A2A2, + 0x581D8ECC, 0x5DA8AEE8, 0xFF8AAFD0, 0xBA2BCF6E, + 0x4BD98DAC, 0xF2EDB9E4, 0xFA2DC868, 0x47E84661, + 0xECEB1C7D, 0x41705CA4, 0x5982E4D4, 0xEB5204A1, + 0xD196CAFB, 0x6414804D, 0x3ABD4B46, 0x8B494C26, + 0xB432D52B, 0x39C5356B, 0x6EC80BF7, 0x71BE5483, + 0xCEC4A509, 0xE9411D61, 0x52F341E5, 0xD2E6197B, + 0x4F02826C, 0xA9E48838, 0xD1F8F247, 0xE4957FB3, + 0x586CCA99, 0x9A8B6A5B, 0x4998FBEA, 0xF762BE4C, + 0x90DFE33C, 0x9731511E, 0x88C6A82F, 0xDD65A4D4 + }; + + /// + /// Convert a hexadecimal string to a byte array. The input string must be + /// even (i.e. its length is a multiple of 2). + /// + /// String containing hexadecimal characters. + /// Returns a byte array. Returns null if the string parameter + /// was null or is an uneven string (i.e. if its length isn't a + /// multiple of 2). + /// Thrown if + /// is null. + public static byte[] HexStringToByteArray(string strHex) + { + if(strHex == null) { Debug.Assert(false); throw new ArgumentNullException("strHex"); } + + int nStrLen = strHex.Length; + if((nStrLen & 1) != 0) { Debug.Assert(false); return null; } + + byte[] pb = new byte[nStrLen / 2]; + byte bt; + char ch; + + for(int i = 0; i < nStrLen; i += 2) + { + ch = strHex[i]; + + if((ch >= '0') && (ch <= '9')) + bt = (byte)(ch - '0'); + else if((ch >= 'a') && (ch <= 'f')) + bt = (byte)(ch - 'a' + 10); + else if((ch >= 'A') && (ch <= 'F')) + bt = (byte)(ch - 'A' + 10); + else { Debug.Assert(false); bt = 0; } + + bt <<= 4; + + ch = strHex[i + 1]; + if((ch >= '0') && (ch <= '9')) + bt += (byte)(ch - '0'); + else if((ch >= 'a') && (ch <= 'f')) + bt += (byte)(ch - 'a' + 10); + else if((ch >= 'A') && (ch <= 'F')) + bt += (byte)(ch - 'A' + 10); + else { Debug.Assert(false); } + + pb[i >> 1] = bt; + } + + return pb; + } + + /// + /// Convert a byte array to a hexadecimal string. + /// + /// Input byte array. + /// Returns the hexadecimal string representing the byte + /// array. Returns null, if the input byte array was null. Returns + /// an empty string, if the input byte array has length 0. + public static string ByteArrayToHexString(byte[] pbArray) + { + if(pbArray == null) return null; + + int nLen = pbArray.Length; + if(nLen == 0) return string.Empty; + + StringBuilder sb = new StringBuilder(); + + byte bt, btHigh, btLow; + for(int i = 0; i < nLen; ++i) + { + bt = pbArray[i]; + btHigh = bt; btHigh >>= 4; + btLow = (byte)(bt & 0x0F); + + if(btHigh >= 10) sb.Append((char)('A' + btHigh - 10)); + else sb.Append((char)('0' + btHigh)); + + if(btLow >= 10) sb.Append((char)('A' + btLow - 10)); + else sb.Append((char)('0' + btLow)); + } + + return sb.ToString(); + } + + /// + /// Decode Base32 strings according to RFC 4648. + /// + public static byte[] ParseBase32(string str) + { + if((str == null) || ((str.Length % 8) != 0)) + { + Debug.Assert(false); + return null; + } + + ulong uMaxBits = (ulong)str.Length * 5UL; + List l = new List((int)(uMaxBits / 8UL) + 1); + Debug.Assert(l.Count == 0); + + for(int i = 0; i < str.Length; i += 8) + { + ulong u = 0; + int nBits = 0; + + for(int j = 0; j < 8; ++j) + { + char ch = str[i + j]; + if(ch == '=') break; + + ulong uValue; + if((ch >= 'A') && (ch <= 'Z')) + uValue = (ulong)(ch - 'A'); + else if((ch >= 'a') && (ch <= 'z')) + uValue = (ulong)(ch - 'a'); + else if((ch >= '2') && (ch <= '7')) + uValue = (ulong)(ch - '2') + 26UL; + else { Debug.Assert(false); return null; } + + u <<= 5; + u += uValue; + nBits += 5; + } + + int nBitsTooMany = (nBits % 8); + u >>= nBitsTooMany; + nBits -= nBitsTooMany; + Debug.Assert((nBits % 8) == 0); + + int idxNewBytes = l.Count; + while(nBits > 0) + { + l.Add((byte)(u & 0xFF)); + u >>= 8; + nBits -= 8; + } + l.Reverse(idxNewBytes, l.Count - idxNewBytes); + } + + return l.ToArray(); + } + + /// + /// Set all bytes in a byte array to zero. + /// + /// Input array. All bytes of this array + /// will be set to zero. + [MethodImpl(MioNoOptimize)] + public static void ZeroByteArray(byte[] pbArray) + { + Debug.Assert(pbArray != null); + if(pbArray == null) throw new ArgumentNullException("pbArray"); + + Array.Clear(pbArray, 0, pbArray.Length); + } + + /// + /// Set all elements of an array to the default value. + /// + /// Input array. + [MethodImpl(MioNoOptimize)] + public static void ZeroArray(T[] v) + { + if(v == null) { Debug.Assert(false); throw new ArgumentNullException("v"); } + + Array.Clear(v, 0, v.Length); + } + + /// + /// Convert 2 bytes to a 16-bit unsigned integer (little-endian). + /// + public static ushort BytesToUInt16(byte[] pb) + { + Debug.Assert((pb != null) && (pb.Length == 2)); + if(pb == null) throw new ArgumentNullException("pb"); + if(pb.Length != 2) throw new ArgumentOutOfRangeException("pb"); + + return (ushort)((ushort)pb[0] | ((ushort)pb[1] << 8)); + } + + /// + /// Convert 2 bytes to a 16-bit unsigned integer (little-endian). + /// + public static ushort BytesToUInt16(byte[] pb, int iOffset) + { + if(pb == null) { Debug.Assert(false); throw new ArgumentNullException("pb"); } + if((iOffset < 0) || ((iOffset + 1) >= pb.Length)) + { + Debug.Assert(false); + throw new ArgumentOutOfRangeException("iOffset"); + } + + return (ushort)((ushort)pb[iOffset] | ((ushort)pb[iOffset + 1] << 8)); + } + + /// + /// Convert 4 bytes to a 32-bit unsigned integer (little-endian). + /// + public static uint BytesToUInt32(byte[] pb) + { + Debug.Assert((pb != null) && (pb.Length == 4)); + if(pb == null) throw new ArgumentNullException("pb"); + if(pb.Length != 4) throw new ArgumentOutOfRangeException("pb"); + + return ((uint)pb[0] | ((uint)pb[1] << 8) | ((uint)pb[2] << 16) | + ((uint)pb[3] << 24)); + } + + /// + /// Convert 4 bytes to a 32-bit unsigned integer (little-endian). + /// + public static uint BytesToUInt32(byte[] pb, int iOffset) + { + if(pb == null) { Debug.Assert(false); throw new ArgumentNullException("pb"); } + if((iOffset < 0) || ((iOffset + 3) >= pb.Length)) + { + Debug.Assert(false); + throw new ArgumentOutOfRangeException("iOffset"); + } + + return ((uint)pb[iOffset] | ((uint)pb[iOffset + 1] << 8) | + ((uint)pb[iOffset + 2] << 16) | ((uint)pb[iOffset + 3] << 24)); + } + + /// + /// Convert 8 bytes to a 64-bit unsigned integer (little-endian). + /// + public static ulong BytesToUInt64(byte[] pb) + { + Debug.Assert((pb != null) && (pb.Length == 8)); + if(pb == null) throw new ArgumentNullException("pb"); + if(pb.Length != 8) throw new ArgumentOutOfRangeException("pb"); + + return ((ulong)pb[0] | ((ulong)pb[1] << 8) | ((ulong)pb[2] << 16) | + ((ulong)pb[3] << 24) | ((ulong)pb[4] << 32) | ((ulong)pb[5] << 40) | + ((ulong)pb[6] << 48) | ((ulong)pb[7] << 56)); + } + + /// + /// Convert 8 bytes to a 64-bit unsigned integer (little-endian). + /// + public static ulong BytesToUInt64(byte[] pb, int iOffset) + { + if(pb == null) { Debug.Assert(false); throw new ArgumentNullException("pb"); } + if((iOffset < 0) || ((iOffset + 7) >= pb.Length)) + { + Debug.Assert(false); + throw new ArgumentOutOfRangeException("iOffset"); + } + + // if(BitConverter.IsLittleEndian) + // return BitConverter.ToUInt64(pb, iOffset); + + return ((ulong)pb[iOffset] | ((ulong)pb[iOffset + 1] << 8) | + ((ulong)pb[iOffset + 2] << 16) | ((ulong)pb[iOffset + 3] << 24) | + ((ulong)pb[iOffset + 4] << 32) | ((ulong)pb[iOffset + 5] << 40) | + ((ulong)pb[iOffset + 6] << 48) | ((ulong)pb[iOffset + 7] << 56)); + } + + public static int BytesToInt32(byte[] pb) + { + return (int)BytesToUInt32(pb); + } + + public static int BytesToInt32(byte[] pb, int iOffset) + { + return (int)BytesToUInt32(pb, iOffset); + } + + public static long BytesToInt64(byte[] pb) + { + return (long)BytesToUInt64(pb); + } + + public static long BytesToInt64(byte[] pb, int iOffset) + { + return (long)BytesToUInt64(pb, iOffset); + } + + /// + /// Convert a 16-bit unsigned integer to 2 bytes (little-endian). + /// + public static byte[] UInt16ToBytes(ushort uValue) + { + byte[] pb = new byte[2]; + + unchecked + { + pb[0] = (byte)uValue; + pb[1] = (byte)(uValue >> 8); + } + + return pb; + } + + /// + /// Convert a 32-bit unsigned integer to 4 bytes (little-endian). + /// + public static byte[] UInt32ToBytes(uint uValue) + { + byte[] pb = new byte[4]; + + unchecked + { + pb[0] = (byte)uValue; + pb[1] = (byte)(uValue >> 8); + pb[2] = (byte)(uValue >> 16); + pb[3] = (byte)(uValue >> 24); + } + + return pb; + } + + /// + /// Convert a 32-bit unsigned integer to 4 bytes (little-endian). + /// + public static void UInt32ToBytesEx(uint uValue, byte[] pb, int iOffset) + { + if(pb == null) { Debug.Assert(false); throw new ArgumentNullException("pb"); } + if((iOffset < 0) || ((iOffset + 3) >= pb.Length)) + { + Debug.Assert(false); + throw new ArgumentOutOfRangeException("iOffset"); + } + + unchecked + { + pb[iOffset] = (byte)uValue; + pb[iOffset + 1] = (byte)(uValue >> 8); + pb[iOffset + 2] = (byte)(uValue >> 16); + pb[iOffset + 3] = (byte)(uValue >> 24); + } + } + + /// + /// Convert a 64-bit unsigned integer to 8 bytes (little-endian). + /// + public static byte[] UInt64ToBytes(ulong uValue) + { + byte[] pb = new byte[8]; + + unchecked + { + pb[0] = (byte)uValue; + pb[1] = (byte)(uValue >> 8); + pb[2] = (byte)(uValue >> 16); + pb[3] = (byte)(uValue >> 24); + pb[4] = (byte)(uValue >> 32); + pb[5] = (byte)(uValue >> 40); + pb[6] = (byte)(uValue >> 48); + pb[7] = (byte)(uValue >> 56); + } + + return pb; + } + + /// + /// Convert a 64-bit unsigned integer to 8 bytes (little-endian). + /// + public static void UInt64ToBytesEx(ulong uValue, byte[] pb, int iOffset) + { + if(pb == null) { Debug.Assert(false); throw new ArgumentNullException("pb"); } + if((iOffset < 0) || ((iOffset + 7) >= pb.Length)) + { + Debug.Assert(false); + throw new ArgumentOutOfRangeException("iOffset"); + } + + unchecked + { + pb[iOffset] = (byte)uValue; + pb[iOffset + 1] = (byte)(uValue >> 8); + pb[iOffset + 2] = (byte)(uValue >> 16); + pb[iOffset + 3] = (byte)(uValue >> 24); + pb[iOffset + 4] = (byte)(uValue >> 32); + pb[iOffset + 5] = (byte)(uValue >> 40); + pb[iOffset + 6] = (byte)(uValue >> 48); + pb[iOffset + 7] = (byte)(uValue >> 56); + } + } + + public static byte[] Int32ToBytes(int iValue) + { + return UInt32ToBytes((uint)iValue); + } + + public static void Int32ToBytesEx(int iValue, byte[] pb, int iOffset) + { + UInt32ToBytesEx((uint)iValue, pb, iOffset); + } + + public static byte[] Int64ToBytes(long lValue) + { + return UInt64ToBytes((ulong)lValue); + } + + public static void Int64ToBytesEx(long lValue, byte[] pb, int iOffset) + { + UInt64ToBytesEx((ulong)lValue, pb, iOffset); + } + + public static uint RotateLeft32(uint u, int nBits) + { + return ((u << nBits) | (u >> (32 - nBits))); + } + + public static uint RotateRight32(uint u, int nBits) + { + return ((u >> nBits) | (u << (32 - nBits))); + } + + public static ulong RotateLeft64(ulong u, int nBits) + { + return ((u << nBits) | (u >> (64 - nBits))); + } + + public static ulong RotateRight64(ulong u, int nBits) + { + return ((u >> nBits) | (u << (64 - nBits))); + } + + public static bool ArraysEqual(byte[] x, byte[] y) + { + // Return false if one of them is null (not comparable)! + if((x == null) || (y == null)) { Debug.Assert(false); return false; } + + if(x.Length != y.Length) return false; + + for(int i = 0; i < x.Length; ++i) + { + if(x[i] != y[i]) return false; + } + + return true; + } + + public static void XorArray(byte[] pbSource, int iSourceOffset, + byte[] pbBuffer, int iBufferOffset, int cb) + { + if(pbSource == null) throw new ArgumentNullException("pbSource"); + if(iSourceOffset < 0) throw new ArgumentOutOfRangeException("iSourceOffset"); + if(pbBuffer == null) throw new ArgumentNullException("pbBuffer"); + if(iBufferOffset < 0) throw new ArgumentOutOfRangeException("iBufferOffset"); + if(cb < 0) throw new ArgumentOutOfRangeException("cb"); + if(iSourceOffset > (pbSource.Length - cb)) + throw new ArgumentOutOfRangeException("cb"); + if(iBufferOffset > (pbBuffer.Length - cb)) + throw new ArgumentOutOfRangeException("cb"); + + for(int i = 0; i < cb; ++i) + pbBuffer[iBufferOffset + i] ^= pbSource[iSourceOffset + i]; + } + + /// + /// Fast hash that can be used e.g. for hash tables. + /// The algorithm might change in the future; do not store + /// the hashes for later use. + /// + public static uint Hash32(byte[] v, int iStart, int iLength) + { + uint u = 0x326F637B; + + if(v == null) { Debug.Assert(false); return u; } + if(iStart < 0) { Debug.Assert(false); return u; } + if(iLength < 0) { Debug.Assert(false); return u; } + + int m = iStart + iLength; + if(m > v.Length) { Debug.Assert(false); return u; } + + for(int i = iStart; i < m; ++i) + { + u ^= m_vSBox[v[i]]; + u *= 3; + } + + return u; + } + + public static void CopyStream(Stream sSource, Stream sTarget) + { + Debug.Assert((sSource != null) && (sTarget != null)); + if(sSource == null) throw new ArgumentNullException("sSource"); + if(sTarget == null) throw new ArgumentNullException("sTarget"); + + const int nBufSize = 4096; + byte[] pbBuf = new byte[nBufSize]; + + while(true) + { + int nRead = sSource.Read(pbBuf, 0, nBufSize); + if(nRead == 0) break; + + sTarget.Write(pbBuf, 0, nRead); + } + + // Do not close any of the streams + } + + public static byte[] Read(Stream s, int nCount) + { + if(s == null) throw new ArgumentNullException("s"); + if(nCount < 0) throw new ArgumentOutOfRangeException("nCount"); + + byte[] pb = new byte[nCount]; + int iOffset = 0; + while(nCount > 0) + { + int iRead = s.Read(pb, iOffset, nCount); + if(iRead == 0) break; + + iOffset += iRead; + nCount -= iRead; + } + + if(iOffset != pb.Length) + { + byte[] pbPart = new byte[iOffset]; + Array.Copy(pb, pbPart, iOffset); + return pbPart; + } + + return pb; + } + + public static void Write(Stream s, byte[] pbData) + { + if(s == null) { Debug.Assert(false); return; } + if(pbData == null) { Debug.Assert(false); return; } + + Debug.Assert(pbData.Length >= 0); + if(pbData.Length > 0) s.Write(pbData, 0, pbData.Length); + } + + public static byte[] Compress(byte[] pbData) + { + if(pbData == null) throw new ArgumentNullException("pbData"); + if(pbData.Length == 0) return pbData; + + byte[] pbCompressed; + using(MemoryStream msSource = new MemoryStream(pbData, false)) + { + using(MemoryStream msCompressed = new MemoryStream()) + { + using(GZipStream gz = new GZipStream(msCompressed, + CompressionMode.Compress)) + { + MemUtil.CopyStream(msSource, gz); + } + + pbCompressed = msCompressed.ToArray(); + } + } + + return pbCompressed; + } + + public static byte[] Decompress(byte[] pbCompressed) + { + if(pbCompressed == null) throw new ArgumentNullException("pbCompressed"); + if(pbCompressed.Length == 0) return pbCompressed; + + byte[] pbData; + using(MemoryStream msData = new MemoryStream()) + { + using(MemoryStream msCompressed = new MemoryStream(pbCompressed, false)) + { + using(GZipStream gz = new GZipStream(msCompressed, + CompressionMode.Decompress)) + { + MemUtil.CopyStream(gz, msData); + } + } + + pbData = msData.ToArray(); + } + + return pbData; + } + + public static int IndexOf(T[] vHaystack, T[] vNeedle) + where T : IEquatable + { + if(vHaystack == null) throw new ArgumentNullException("vHaystack"); + if(vNeedle == null) throw new ArgumentNullException("vNeedle"); + if(vNeedle.Length == 0) return 0; + + for(int i = 0; i <= (vHaystack.Length - vNeedle.Length); ++i) + { + bool bFound = true; + for(int m = 0; m < vNeedle.Length; ++m) + { + if(!vHaystack[i + m].Equals(vNeedle[m])) + { + bFound = false; + break; + } + } + if(bFound) return i; + } + + return -1; + } + + public static T[] Mid(T[] v, int iOffset, int iLength) + { + if(v == null) throw new ArgumentNullException("v"); + if(iOffset < 0) throw new ArgumentOutOfRangeException("iOffset"); + if(iLength < 0) throw new ArgumentOutOfRangeException("iLength"); + if((iOffset + iLength) > v.Length) throw new ArgumentException(); + + T[] r = new T[iLength]; + Array.Copy(v, iOffset, r, 0, iLength); + return r; + } + + public static IEnumerable Union(IEnumerable a, IEnumerable b, + IEqualityComparer cmp) + { + if(a == null) throw new ArgumentNullException("a"); + if(b == null) throw new ArgumentNullException("b"); + + Dictionary d = ((cmp != null) ? + (new Dictionary(cmp)) : (new Dictionary())); + + foreach(T ta in a) + { + if(d.ContainsKey(ta)) continue; // Prevent duplicates + + d[ta] = true; + yield return ta; + } + + foreach(T tb in b) + { + if(d.ContainsKey(tb)) continue; // Prevent duplicates + + d[tb] = true; + yield return tb; + } + + yield break; + } + + public static IEnumerable Intersect(IEnumerable a, IEnumerable b, + IEqualityComparer cmp) + { + if(a == null) throw new ArgumentNullException("a"); + if(b == null) throw new ArgumentNullException("b"); + + Dictionary d = ((cmp != null) ? + (new Dictionary(cmp)) : (new Dictionary())); + + foreach(T tb in b) { d[tb] = true; } + + foreach(T ta in a) + { + if(d.Remove(ta)) // Prevent duplicates + yield return ta; + } + + yield break; + } + + public static IEnumerable Except(IEnumerable a, IEnumerable b, + IEqualityComparer cmp) + { + if(a == null) throw new ArgumentNullException("a"); + if(b == null) throw new ArgumentNullException("b"); + + Dictionary d = ((cmp != null) ? + (new Dictionary(cmp)) : (new Dictionary())); + + foreach(T tb in b) { d[tb] = true; } + + foreach(T ta in a) + { + if(d.ContainsKey(ta)) continue; + + d[ta] = true; // Prevent duplicates + yield return ta; + } + + yield break; + } + + [MethodImpl(MioNoOptimize)] + internal static void DisposeIfPossible(object o) + { + if(o == null) { Debug.Assert(false); return; } + + IDisposable d = (o as IDisposable); + if(d != null) d.Dispose(); + } + } + + internal sealed class ArrayHelperEx : IEqualityComparer, IComparer + where T : IEquatable, IComparable + { + public int GetHashCode(T[] obj) + { + if(obj == null) throw new ArgumentNullException("obj"); + + uint h = 0xC17962B7U; + unchecked + { + int n = obj.Length; + for(int i = 0; i < n; ++i) + { + h += (uint)obj[i].GetHashCode(); + h = MemUtil.RotateLeft32(h * 0x5FC34C67U, 13); + } + } + + return (int)h; + } + + /* internal ulong GetHashCodeEx(T[] obj) + { + if(obj == null) throw new ArgumentNullException("obj"); + + ulong h = 0x207CAC8E509A3FC9UL; + unchecked + { + int n = obj.Length; + for(int i = 0; i < n; ++i) + { + h += (uint)obj[i].GetHashCode(); + h = MemUtil.RotateLeft64(h * 0x54724D3EA2860CBBUL, 29); + } + } + + return h; + } */ + + public bool Equals(T[] x, T[] y) + { + if(object.ReferenceEquals(x, y)) return true; + if((x == null) || (y == null)) return false; + + int n = x.Length; + if(n != y.Length) return false; + + for(int i = 0; i < n; ++i) + { + if(!x[i].Equals(y[i])) return false; + } + + return true; + } + + public int Compare(T[] x, T[] y) + { + if(object.ReferenceEquals(x, y)) return 0; + if(x == null) return -1; + if(y == null) return 1; + + int n = x.Length, m = y.Length; + if(n != m) return ((n < m) ? -1 : 1); + + for(int i = 0; i < n; ++i) + { + T tX = x[i], tY = y[i]; + if(!tX.Equals(tY)) return tX.CompareTo(tY); + } + + return 0; + } + } +} diff --git a/ModernKeePassLib/Utility/MessageService.cs b/ModernKeePassLib/Utility/MessageService.cs new file mode 100644 index 0000000..3f09a28 --- /dev/null +++ b/ModernKeePassLib/Utility/MessageService.cs @@ -0,0 +1,459 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Collections.Specialized; +using System.Diagnostics; +using System.Text; + +#if !ModernKeePassLib +using System.Windows.Forms; +#endif + +using ModernKeePassLib.Resources; +using ModernKeePassLib.Serialization; + +namespace ModernKeePassLib.Utility +{ + public sealed class MessageServiceEventArgs : EventArgs + { +#if !ModernKeePassLib + private string m_strTitle = string.Empty; + private string m_strText = string.Empty; + private MessageBoxButtons m_msgButtons = MessageBoxButtons.OK; + private MessageBoxIcon m_msgIcon = MessageBoxIcon.None; + + public string Title { get { return m_strTitle; } } + public string Text { get { return m_strText; } } + public MessageBoxButtons Buttons { get { return m_msgButtons; } } + public MessageBoxIcon Icon { get { return m_msgIcon; } } + + public MessageServiceEventArgs() { } + + public MessageServiceEventArgs(string strTitle, string strText, + MessageBoxButtons msgButtons, MessageBoxIcon msgIcon) + { + m_strTitle = (strTitle ?? string.Empty); + m_strText = (strText ?? string.Empty); + m_msgButtons = msgButtons; + m_msgIcon = msgIcon; + } +#endif + } + + public static class MessageService + { + private static volatile uint m_uCurrentMessageCount = 0; + +#if !ModernKeePassLib +#if !KeePassLibSD + private const MessageBoxIcon m_mbiInfo = MessageBoxIcon.Information; + private const MessageBoxIcon m_mbiWarning = MessageBoxIcon.Warning; + private const MessageBoxIcon m_mbiFatal = MessageBoxIcon.Error; + + private const MessageBoxOptions m_mboRtl = (MessageBoxOptions.RtlReading | + MessageBoxOptions.RightAlign); +#else + private const MessageBoxIcon m_mbiInfo = MessageBoxIcon.Asterisk; + private const MessageBoxIcon m_mbiWarning = MessageBoxIcon.Exclamation; + private const MessageBoxIcon m_mbiFatal = MessageBoxIcon.Hand; +#endif + private const MessageBoxIcon m_mbiQuestion = MessageBoxIcon.Question; +#endif + public static string NewLine + { +#if !KeePassLibSD + get { return Environment.NewLine; } +#else + get { return "\r\n"; } +#endif + } + + public static string NewParagraph + { +#if !KeePassLibSD + get { return Environment.NewLine + Environment.NewLine; } +#else + get { return "\r\n\r\n"; } +#endif + } + + public static uint CurrentMessageCount + { + get { return m_uCurrentMessageCount; } + } + +#if !KeePassUAP + public static event EventHandler MessageShowing; +#endif + + private static string ObjectsToMessage(object[] vLines) + { + return ObjectsToMessage(vLines, false); + } + + private static string ObjectsToMessage(object[] vLines, bool bFullExceptions) + { + if(vLines == null) return string.Empty; + + string strNewPara = MessageService.NewParagraph; + + StringBuilder sbText = new StringBuilder(); + bool bSeparator = false; + + foreach(object obj in vLines) + { + if(obj == null) continue; + + string strAppend = null; + + Exception exObj = (obj as Exception); + string strObj = (obj as string); +#if (!KeePassLibSD && !ModernKeePassLib) + StringCollection scObj = (obj as StringCollection); +#endif + + if(exObj != null) + { + if(bFullExceptions) + strAppend = StrUtil.FormatException(exObj); + else if(!string.IsNullOrEmpty(exObj.Message)) + strAppend = exObj.Message; + } +#if (!KeePassLibSD && !ModernKeePassLib) + else if(scObj != null) + { + StringBuilder sb = new StringBuilder(); + foreach(string strCollLine in scObj) + { + if(sb.Length > 0) sb.AppendLine(); + sb.Append(strCollLine.TrimEnd()); + } + strAppend = sb.ToString(); + } +#endif + else if(strObj != null) + strAppend = strObj; + else + strAppend = obj.ToString(); + + if(!string.IsNullOrEmpty(strAppend)) + { + if(bSeparator) sbText.Append(strNewPara); + else bSeparator = true; + + sbText.Append(strAppend); + } + } + + return sbText.ToString(); + } + +#if (!KeePassLibSD && !ModernKeePassLib) + internal static Form GetTopForm() + { + FormCollection fc = Application.OpenForms; + if((fc == null) || (fc.Count == 0)) return null; + + return fc[fc.Count - 1]; + } +#endif + +#if !ModernKeePassLib + internal static DialogResult SafeShowMessageBox(string strText, string strTitle, + MessageBoxButtons mb, MessageBoxIcon mi, MessageBoxDefaultButton mdb) + { +#if (KeePassLibSD || KeePassRT) + return MessageBox.Show(strText, strTitle, mb, mi, mdb); +#else + IWin32Window wnd = null; + try + { + Form f = GetTopForm(); + if((f != null) && f.InvokeRequired) + return (DialogResult)f.Invoke(new SafeShowMessageBoxInternalDelegate( + SafeShowMessageBoxInternal), f, strText, strTitle, mb, mi, mdb); + else wnd = f; + } + catch(Exception) { Debug.Assert(false); } + + if(wnd == null) + { + if(StrUtil.RightToLeft) + return MessageBox.Show(strText, strTitle, mb, mi, mdb, m_mboRtl); + return MessageBox.Show(strText, strTitle, mb, mi, mdb); + } + + try + { + if(StrUtil.RightToLeft) + return MessageBox.Show(wnd, strText, strTitle, mb, mi, mdb, m_mboRtl); + return MessageBox.Show(wnd, strText, strTitle, mb, mi, mdb); + } + catch(Exception) { Debug.Assert(false); } + + if(StrUtil.RightToLeft) + return MessageBox.Show(strText, strTitle, mb, mi, mdb, m_mboRtl); + return MessageBox.Show(strText, strTitle, mb, mi, mdb); +#endif + } + +#if (!KeePassLibSD && !KeePassRT) + internal delegate DialogResult SafeShowMessageBoxInternalDelegate(IWin32Window iParent, + string strText, string strTitle, MessageBoxButtons mb, MessageBoxIcon mi, + MessageBoxDefaultButton mdb); + + internal static DialogResult SafeShowMessageBoxInternal(IWin32Window iParent, + string strText, string strTitle, MessageBoxButtons mb, MessageBoxIcon mi, + MessageBoxDefaultButton mdb) + { + if(StrUtil.RightToLeft) + return MessageBox.Show(iParent, strText, strTitle, mb, mi, mdb, m_mboRtl); + return MessageBox.Show(iParent, strText, strTitle, mb, mi, mdb); + } +#endif + + public static void ShowInfo(params object[] vLines) + { + ShowInfoEx(null, vLines); + } + + public static void ShowInfoEx(string strTitle, params object[] vLines) + { + ++m_uCurrentMessageCount; + + strTitle = (strTitle ?? PwDefs.ShortProductName); + string strText = ObjectsToMessage(vLines); + + if(MessageService.MessageShowing != null) + MessageService.MessageShowing(null, new MessageServiceEventArgs( + strTitle, strText, MessageBoxButtons.OK, m_mbiInfo)); + + SafeShowMessageBox(strText, strTitle, MessageBoxButtons.OK, m_mbiInfo, + MessageBoxDefaultButton.Button1); + + --m_uCurrentMessageCount; + } + + public static void ShowWarning(params object[] vLines) + { + ShowWarningPriv(vLines, false); + } + + internal static void ShowWarningExcp(params object[] vLines) + { + ShowWarningPriv(vLines, true); + } + + private static void ShowWarningPriv(object[] vLines, bool bFullExceptions) + { + ++m_uCurrentMessageCount; + + string strTitle = PwDefs.ShortProductName; + string strText = ObjectsToMessage(vLines, bFullExceptions); + + if(MessageService.MessageShowing != null) + MessageService.MessageShowing(null, new MessageServiceEventArgs( + strTitle, strText, MessageBoxButtons.OK, m_mbiWarning)); + + SafeShowMessageBox(strText, strTitle, MessageBoxButtons.OK, m_mbiWarning, + MessageBoxDefaultButton.Button1); + + --m_uCurrentMessageCount; + } + + public static void ShowFatal(params object[] vLines) + { + ++m_uCurrentMessageCount; + + string strTitle = PwDefs.ShortProductName + " - " + KLRes.FatalError; + string strText = KLRes.FatalErrorText + MessageService.NewParagraph + + KLRes.ErrorInClipboard + MessageService.NewParagraph + + // Please send it to the KeePass developers. + // KLRes.ErrorFeedbackRequest + MessageService.NewParagraph + + ObjectsToMessage(vLines); + + try + { + string strDetails = ObjectsToMessage(vLines, true); + +#if KeePassLibSD + Clipboard.SetDataObject(strDetails); +#else + Clipboard.Clear(); + Clipboard.SetText(strDetails); +#endif + } + catch(Exception) { Debug.Assert(false); } + + if(MessageService.MessageShowing != null) + MessageService.MessageShowing(null, new MessageServiceEventArgs( + strTitle, strText, MessageBoxButtons.OK, m_mbiFatal)); + + SafeShowMessageBox(strText, strTitle, MessageBoxButtons.OK, m_mbiFatal, + MessageBoxDefaultButton.Button1); + + --m_uCurrentMessageCount; + } + + public static DialogResult Ask(string strText, string strTitle, + MessageBoxButtons mbb) + { + ++m_uCurrentMessageCount; + + string strTextEx = (strText ?? string.Empty); + string strTitleEx = (strTitle ?? PwDefs.ShortProductName); + + if(MessageService.MessageShowing != null) + MessageService.MessageShowing(null, new MessageServiceEventArgs( + strTitleEx, strTextEx, mbb, m_mbiQuestion)); + + DialogResult dr = SafeShowMessageBox(strTextEx, strTitleEx, mbb, + m_mbiQuestion, MessageBoxDefaultButton.Button1); + + --m_uCurrentMessageCount; + return dr; + } + + public static bool AskYesNo(string strText, string strTitle, bool bDefaultToYes, + MessageBoxIcon mbi) + { + ++m_uCurrentMessageCount; + + string strTextEx = (strText ?? string.Empty); + string strTitleEx = (strTitle ?? PwDefs.ShortProductName); + + if(MessageService.MessageShowing != null) + MessageService.MessageShowing(null, new MessageServiceEventArgs( + strTitleEx, strTextEx, MessageBoxButtons.YesNo, mbi)); + + DialogResult dr = SafeShowMessageBox(strTextEx, strTitleEx, + MessageBoxButtons.YesNo, mbi, bDefaultToYes ? + MessageBoxDefaultButton.Button1 : MessageBoxDefaultButton.Button2); + + --m_uCurrentMessageCount; + return (dr == DialogResult.Yes); + } + + public static bool AskYesNo(string strText, string strTitle, bool bDefaultToYes) + { + return AskYesNo(strText, strTitle, bDefaultToYes, m_mbiQuestion); + } + + public static bool AskYesNo(string strText, string strTitle) + { + return AskYesNo(strText, strTitle, true, m_mbiQuestion); + } + + public static bool AskYesNo(string strText) + { + return AskYesNo(strText, null, true, m_mbiQuestion); + } + + public static void ShowLoadWarning(string strFilePath, Exception ex) + { + ShowLoadWarning(strFilePath, ex, false); + } + + public static void ShowLoadWarning(string strFilePath, Exception ex, + bool bFullException) + { + ShowWarning(GetLoadWarningMessage(strFilePath, ex, bFullException)); + } + + public static void ShowLoadWarning(IOConnectionInfo ioConnection, Exception ex) + { + if(ioConnection != null) + ShowLoadWarning(ioConnection.GetDisplayName(), ex, false); + else ShowWarning(ex); + } + + public static void ShowSaveWarning(string strFilePath, Exception ex, + bool bCorruptionWarning) + { + FileLockException fl = (ex as FileLockException); + if(fl != null) + { + ShowWarning(fl.Message); + return; + } + + string str = GetSaveWarningMessage(strFilePath, ex, bCorruptionWarning); + ShowWarning(str); + } + + public static void ShowSaveWarning(IOConnectionInfo ioConnection, Exception ex, + bool bCorruptionWarning) + { + if(ioConnection != null) + ShowSaveWarning(ioConnection.GetDisplayName(), ex, bCorruptionWarning); + else ShowWarning(ex); + } +#endif // !KeePassUAP + + internal static string GetLoadWarningMessage(string strFilePath, + Exception ex, bool bFullException) + { + string str = string.Empty; + + if(!string.IsNullOrEmpty(strFilePath)) + str += strFilePath + MessageService.NewParagraph; + + str += KLRes.FileLoadFailed; + + if((ex != null) && !string.IsNullOrEmpty(ex.Message)) + { + str += MessageService.NewParagraph; + if(!bFullException) str += ex.Message; + else str += ObjectsToMessage(new object[] { ex }, true); + } + + return str; + } + + internal static string GetSaveWarningMessage(string strFilePath, + Exception ex, bool bCorruptionWarning) + { + string str = string.Empty; + if(!string.IsNullOrEmpty(strFilePath)) + str += strFilePath + MessageService.NewParagraph; + + str += KLRes.FileSaveFailed; + + if((ex != null) && !string.IsNullOrEmpty(ex.Message)) + str += MessageService.NewParagraph + ex.Message; + + if(bCorruptionWarning) + str += MessageService.NewParagraph + KLRes.FileSaveCorruptionWarning; + + return str; + } + + public static void ExternalIncrementMessageCount() + { + ++m_uCurrentMessageCount; + } + + public static void ExternalDecrementMessageCount() + { + --m_uCurrentMessageCount; + } + } +} diff --git a/ModernKeePassLib/Utility/MonoWorkaround.PCL.cs b/ModernKeePassLib/Utility/MonoWorkaround.PCL.cs new file mode 100644 index 0000000..f47b062 --- /dev/null +++ b/ModernKeePassLib/Utility/MonoWorkaround.PCL.cs @@ -0,0 +1,10 @@ +namespace ModernKeePassLib.Utility +{ + public static class MonoWorkarounds + { + public static bool IsRequired(int i) + { + return false; + } + } +} diff --git a/ModernKeePassLib/Utility/MonoWorkarounds.cs b/ModernKeePassLib/Utility/MonoWorkarounds.cs new file mode 100644 index 0000000..b26e328 --- /dev/null +++ b/ModernKeePassLib/Utility/MonoWorkarounds.cs @@ -0,0 +1,609 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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 +*/ + +#if DEBUG +// #define DEBUG_BREAKONFAIL +#endif + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Xml; + +#if !KeePassUAP +using System.Windows.Forms; +#endif + +using ModernKeePassLib.Native; + +namespace ModernKeePassLib.Utility +{ + public static class MonoWorkarounds + { + private const string AppXDoTool = "xdotool"; + + private static Dictionary g_dForceReq = new Dictionary(); + private static Thread g_thFixClip = null; + // private static Predicate g_fOwnWindow = null; + +#if DEBUG_BREAKONFAIL + private static DebugBreakTraceListener g_tlBreak = null; +#endif + + private static bool? g_bReq = null; + public static bool IsRequired() + { + if(!g_bReq.HasValue) g_bReq = NativeLib.IsUnix(); + return g_bReq.Value; + } + + // 106: + // Mono throws exceptions when no X server is running. + // https://sourceforge.net/p/keepass/patches/106/ + // 1219: + // Mono prepends byte order mark (BOM) to StdIn. + // https://sourceforge.net/p/keepass/bugs/1219/ + // 1245: + // Key events not raised while Alt is down, and nav keys out of order. + // https://sourceforge.net/p/keepass/bugs/1245/ + // 1254: + // NumericUpDown bug: text is drawn below up/down buttons. + // https://sourceforge.net/p/keepass/bugs/1254/ + // 1354: + // Finalizer of NotifyIcon throws on Unity. + // See also 1574. + // https://sourceforge.net/p/keepass/bugs/1354/ + // 1358: + // FileDialog crashes when ~/.recently-used is invalid. + // https://sourceforge.net/p/keepass/bugs/1358/ + // 1366: + // Drawing bug when scrolling a RichTextBox. + // https://sourceforge.net/p/keepass/bugs/1366/ + // 1378: + // Mono doesn't implement Microsoft.Win32.SystemEvents events. + // https://sourceforge.net/p/keepass/bugs/1378/ + // https://github.com/mono/mono/blob/master/mcs/class/System/Microsoft.Win32/SystemEvents.cs + // 1418: + // Minimizing a form while loading it doesn't work. + // https://sourceforge.net/p/keepass/bugs/1418/ + // 1468: + // Use LibGCrypt for AES-KDF, because Mono's implementations + // of RijndaelManaged and AesCryptoServiceProvider are slow. + // https://sourceforge.net/p/keepass/bugs/1468/ + // 1527: + // Timer causes 100% CPU load. + // https://sourceforge.net/p/keepass/bugs/1527/ + // 1530: + // Mono's clipboard functions don't work properly. + // https://sourceforge.net/p/keepass/bugs/1530/ + // 1574: + // Finalizer of NotifyIcon throws on Mac OS X. + // See also 1354. + // https://sourceforge.net/p/keepass/bugs/1574/ + // 1632: + // RichTextBox rendering bug for bold/italic text. + // https://sourceforge.net/p/keepass/bugs/1632/ + // 1690: + // Removing items from a list view doesn't work properly. + // https://sourceforge.net/p/keepass/bugs/1690/ + // 1710: + // Mono doesn't always raise the FormClosed event properly. + // https://sourceforge.net/p/keepass/bugs/1710/ + // 1716: + // 'Always on Top' doesn't work properly on the Cinnamon desktop. + // https://sourceforge.net/p/keepass/bugs/1716/ + // 1760: + // Input focus is not restored when activating a form. + // https://sourceforge.net/p/keepass/bugs/1760/ + // 2139: + // Shortcut keys are ignored. + // https://sourceforge.net/p/keepass/feature-requests/2139/ + // 2140: + // Explicit control focusing is ignored. + // https://sourceforge.net/p/keepass/feature-requests/2140/ + // 5795: + // Text in input field is incomplete. + // https://bugzilla.xamarin.com/show_bug.cgi?id=5795 + // https://sourceforge.net/p/keepass/discussion/329220/thread/d23dc88b/ + // 9604: + // Trying to resolve a non-existing metadata token crashes Mono. + // https://github.com/mono/mono/issues/9604 + // 10163: + // WebRequest GetResponse call missing, breaks WebDAV due to no PUT. + // https://bugzilla.xamarin.com/show_bug.cgi?id=10163 + // https://sourceforge.net/p/keepass/bugs/1117/ + // https://sourceforge.net/p/keepass/discussion/329221/thread/9422258c/ + // https://github.com/mono/mono/commit/8e67b8c2fc7cb66bff7816ebf7c1039fb8cfc43b + // https://bugzilla.xamarin.com/show_bug.cgi?id=1512 + // https://sourceforge.net/p/keepass/patches/89/ + // 12525: + // PictureBox not rendered when bitmap height >= control height. + // https://bugzilla.xamarin.com/show_bug.cgi?id=12525 + // https://sourceforge.net/p/keepass/discussion/329220/thread/54f61e9a/ + // 100001: + // Control locations/sizes are invalid/unexpected. + // [NoRef] + // 373134: + // Control.InvokeRequired doesn't always return the correct value. + // https://bugzilla.novell.com/show_bug.cgi?id=373134 + // 586901: + // RichTextBox doesn't handle Unicode string correctly. + // https://bugzilla.novell.com/show_bug.cgi?id=586901 + // 620618: + // ListView column headers not drawn. + // https://bugzilla.novell.com/show_bug.cgi?id=620618 + // 649266: + // Calling Control.Hide doesn't remove the application from taskbar. + // https://bugzilla.novell.com/show_bug.cgi?id=649266 + // 686017: + // Minimum sizes must be enforced. + // https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=686017 + // 801414: + // Mono recreates the main window incorrectly. + // https://bugs.launchpad.net/ubuntu/+source/keepass2/+bug/801414 + // 891029: + // Increase tab control height and don't use images on tabs. + // https://sourceforge.net/projects/keepass/forums/forum/329221/topic/4519750 + // https://bugs.launchpad.net/ubuntu/+source/keepass2/+bug/891029 + // https://sourceforge.net/p/keepass/bugs/1256/ + // https://sourceforge.net/p/keepass/bugs/1566/ + // https://sourceforge.net/p/keepass/bugs/1634/ + // 836428016: + // ListView group header selection unsupported. + // https://sourceforge.net/p/keepass/discussion/329221/thread/31dae0f0/ + // 2449941153: + // RichTextBox doesn't properly escape '}' when generating RTF data. + // https://sourceforge.net/p/keepass/discussion/329221/thread/920722a1/ + // 3471228285: + // Mono requires command line arguments to be encoded differently. + // https://sourceforge.net/p/keepass/discussion/329221/thread/cee6bd7d/ + // 3574233558: + // Problems with minimizing windows, no content rendered. + // https://sourceforge.net/p/keepass/discussion/329220/thread/d50a79d6/ + public static bool IsRequired(uint uBugID) + { + if(!MonoWorkarounds.IsRequired()) return false; + + bool bForce; + if(g_dForceReq.TryGetValue(uBugID, out bForce)) return bForce; + + ulong v = NativeLib.MonoVersion; + if(v != 0) + { + if(uBugID == 10163) + return (v >= 0x0002000B00000000UL); // >= 2.11 + } + + return true; + } + + internal static void SetEnabled(string strIDs, bool bEnabled) + { + if(string.IsNullOrEmpty(strIDs)) return; + + string[] vIDs = strIDs.Split(new char[] { ',' }); + foreach(string strID in vIDs) + { + if(string.IsNullOrEmpty(strID)) continue; + + uint uID; + if(StrUtil.TryParseUInt(strID.Trim(), out uID)) + g_dForceReq[uID] = bEnabled; + } + } + + internal static void Initialize() + { + Terminate(); + + // g_fOwnWindow = fOwnWindow; + + if(IsRequired(1530)) + { + try + { + ThreadStart ts = new ThreadStart(MonoWorkarounds.FixClipThread); + g_thFixClip = new Thread(ts); + g_thFixClip.Start(); + } + catch(Exception) { Debug.Assert(false); } + } + +#if DEBUG_BREAKONFAIL + if(IsRequired() && (g_tlBreak == null)) + { + g_tlBreak = new DebugBreakTraceListener(); + Debug.Listeners.Add(g_tlBreak); + } +#endif + } + + internal static void Terminate() + { + if(g_thFixClip != null) + { + try { g_thFixClip.Abort(); } + catch(Exception) { Debug.Assert(false); } + + g_thFixClip = null; + } + } + + private static void FixClipThread() + { + try + { +#if !KeePassUAP + const int msDelay = 250; + + string strTest = ClipboardU.GetText(); + if(strTest == null) return; // No clipboard support + + // Without XDoTool, the workaround would be applied to + // all applications, which may corrupt the clipboard + // when it doesn't contain simple text only; + // https://sourceforge.net/p/keepass/bugs/1603/#a113 + strTest = (NativeLib.RunConsoleApp(AppXDoTool, + "help") ?? string.Empty).Trim(); + if(strTest.Length == 0) return; + + Thread.Sleep(msDelay); + + string strLast = null; + while(true) + { + string str = ClipboardU.GetText(); + if(str == null) { Debug.Assert(false); } + else if(str != strLast) + { + if(NeedClipboardWorkaround()) + ClipboardU.SetText(str, true); + + strLast = str; + } + + Thread.Sleep(msDelay); + } +#endif + } + catch(ThreadAbortException) + { + try { Thread.ResetAbort(); } + catch(Exception) { Debug.Assert(false); } + } + catch(Exception) { Debug.Assert(false); } + finally { g_thFixClip = null; } + } + +#if !KeePassUAP + private static bool NeedClipboardWorkaround() + { + try + { + string strHandle = (NativeLib.RunConsoleApp(AppXDoTool, + "getactivewindow") ?? string.Empty).Trim(); + if(strHandle.Length == 0) { Debug.Assert(false); return false; } + + // IntPtr h = new IntPtr(long.Parse(strHandle)); + long.Parse(strHandle); // Validate + + // Detection of own windows based on Form.Handle + // comparisons doesn't work reliably (Mono's handles + // are usually off by 1) + // Predicate fOwnWindow = g_fOwnWindow; + // if(fOwnWindow != null) + // { + // if(fOwnWindow(h)) return true; + // } + // else { Debug.Assert(false); } + + string strWmClass = (NativeLib.RunConsoleApp("xprop", + "-id " + strHandle + " WM_CLASS") ?? string.Empty); + + if(strWmClass.IndexOf("\"" + PwDefs.ResClass + "\"", + StrUtil.CaseIgnoreCmp) >= 0) return true; + if(strWmClass.IndexOf("\"Remmina\"", + StrUtil.CaseIgnoreCmp) >= 0) return true; + } + catch(ThreadAbortException) { throw; } + catch(Exception) { Debug.Assert(false); } + + return false; + } + + public static void ApplyTo(Form f) + { + if(!MonoWorkarounds.IsRequired()) return; + if(f == null) { Debug.Assert(false); return; } + +#if (!KeePassLibSD && !KeePassRT) + f.HandleCreated += MonoWorkarounds.OnFormHandleCreated; + SetWmClass(f); + + ApplyToControlsRec(f.Controls, f, MonoWorkarounds.ApplyToControl); +#endif + } + + public static void Release(Form f) + { + if(!MonoWorkarounds.IsRequired()) return; + if(f == null) { Debug.Assert(false); return; } + +#if (!KeePassLibSD && !KeePassRT) + f.HandleCreated -= MonoWorkarounds.OnFormHandleCreated; + + ApplyToControlsRec(f.Controls, f, MonoWorkarounds.ReleaseControl); +#endif + } + +#if (!KeePassLibSD && !KeePassRT) + private delegate void MwaControlHandler(Control c, Form fContext); + + private static void ApplyToControlsRec(Control.ControlCollection cc, + Form fContext, MwaControlHandler fn) + { + if(cc == null) { Debug.Assert(false); return; } + + foreach(Control c in cc) + { + fn(c, fContext); + ApplyToControlsRec(c.Controls, fContext, fn); + } + } + + private static void ApplyToControl(Control c, Form fContext) + { + Button btn = (c as Button); + if(btn != null) ApplyToButton(btn, fContext); + + NumericUpDown nc = (c as NumericUpDown); + if((nc != null) && MonoWorkarounds.IsRequired(1254)) + { + if(nc.TextAlign == HorizontalAlignment.Right) + nc.TextAlign = HorizontalAlignment.Left; + } + } + + private sealed class MwaHandlerInfo + { + private readonly Delegate m_fnOrg; // May be null + public Delegate FunctionOriginal + { + get { return m_fnOrg; } + } + + private readonly Delegate m_fnOvr; + public Delegate FunctionOverride + { + get { return m_fnOvr; } + } + + private readonly DialogResult m_dr; + public DialogResult Result + { + get { return m_dr; } + } + + private readonly Form m_fContext; + public Form FormContext + { + get { return m_fContext; } + } + + public MwaHandlerInfo(Delegate fnOrg, Delegate fnOvr, DialogResult dr, + Form fContext) + { + m_fnOrg = fnOrg; + m_fnOvr = fnOvr; + m_dr = dr; + m_fContext = fContext; + } + } + + private static EventHandlerList GetEventHandlers(Component c, + out object objClickEvent) + { + FieldInfo fi = typeof(Control).GetField("ClickEvent", // Mono + BindingFlags.Static | BindingFlags.NonPublic); + if(fi == null) + fi = typeof(Control).GetField("EventClick", // .NET + BindingFlags.Static | BindingFlags.NonPublic); + if(fi == null) { Debug.Assert(false); objClickEvent = null; return null; } + + objClickEvent = fi.GetValue(null); + if(objClickEvent == null) { Debug.Assert(false); return null; } + + PropertyInfo pi = typeof(Component).GetProperty("Events", + BindingFlags.Instance | BindingFlags.NonPublic); + return (pi.GetValue(c, null) as EventHandlerList); + } + + private static Dictionary m_dictHandlers = + new Dictionary(); + private static void ApplyToButton(Button btn, Form fContext) + { + DialogResult dr = btn.DialogResult; + if(dr == DialogResult.None) return; // No workaround required + + object objClickEvent; + EventHandlerList ehl = GetEventHandlers(btn, out objClickEvent); + if(ehl == null) { Debug.Assert(false); return; } + Delegate fnClick = ehl[objClickEvent]; // May be null + + EventHandler fnOvr = new EventHandler(MonoWorkarounds.OnButtonClick); + m_dictHandlers[btn] = new MwaHandlerInfo(fnClick, fnOvr, dr, fContext); + + btn.DialogResult = DialogResult.None; + if(fnClick != null) ehl.RemoveHandler(objClickEvent, fnClick); + ehl[objClickEvent] = fnOvr; + } + + private static void ReleaseControl(Control c, Form fContext) + { + Button btn = (c as Button); + if(btn != null) ReleaseButton(btn, fContext); + } + + private static void ReleaseButton(Button btn, Form fContext) + { + MwaHandlerInfo hi; + m_dictHandlers.TryGetValue(btn, out hi); + if(hi == null) return; + + object objClickEvent; + EventHandlerList ehl = GetEventHandlers(btn, out objClickEvent); + if(ehl == null) { Debug.Assert(false); return; } + + ehl.RemoveHandler(objClickEvent, hi.FunctionOverride); + if(hi.FunctionOriginal != null) + ehl[objClickEvent] = hi.FunctionOriginal; + + btn.DialogResult = hi.Result; + m_dictHandlers.Remove(btn); + } + + private static void OnButtonClick(object sender, EventArgs e) + { + Button btn = (sender as Button); + if(btn == null) { Debug.Assert(false); return; } + + MwaHandlerInfo hi; + m_dictHandlers.TryGetValue(btn, out hi); + if(hi == null) { Debug.Assert(false); return; } + + Form f = hi.FormContext; + + // Set current dialog result by setting the form's private + // variable; the DialogResult property can't be used, + // because it raises close events + FieldInfo fiRes = typeof(Form).GetField("dialog_result", + BindingFlags.Instance | BindingFlags.NonPublic); + if(fiRes == null) { Debug.Assert(false); return; } + if(f != null) fiRes.SetValue(f, hi.Result); + + if(hi.FunctionOriginal != null) + hi.FunctionOriginal.Method.Invoke(hi.FunctionOriginal.Target, + new object[] { btn, e }); + + // Raise close events, if the click event handler hasn't + // reset the dialog result + if((f != null) && (f.DialogResult == hi.Result)) + f.DialogResult = hi.Result; // Raises close events + } + + private static void SetWmClass(Form f) + { + NativeMethods.SetWmClass(f, PwDefs.UnixName, PwDefs.ResClass); + } + + private static void OnFormHandleCreated(object sender, EventArgs e) + { + Form f = (sender as Form); + if(f == null) { Debug.Assert(false); return; } + + if(!f.IsHandleCreated) return; // Prevent infinite loop + + SetWmClass(f); + } + + /// + /// Set the value of the private shown_raised member + /// variable of a form. + /// + /// Previous shown_raised value. + internal static bool ExchangeFormShownRaised(Form f, bool bNewValue) + { + if(f == null) { Debug.Assert(false); return true; } + + try + { + FieldInfo fi = typeof(Form).GetField("shown_raised", + BindingFlags.Instance | BindingFlags.NonPublic); + if(fi == null) { Debug.Assert(false); return true; } + + bool bPrevious = (bool)fi.GetValue(f); + + fi.SetValue(f, bNewValue); + + return bPrevious; + } + catch(Exception) { Debug.Assert(false); } + + return true; + } +#endif + + /// + /// Ensure that the file ~/.recently-used is valid (in order to + /// prevent Mono's FileDialog from crashing). + /// + internal static void EnsureRecentlyUsedValid() + { + if(!MonoWorkarounds.IsRequired(1358)) return; + + try + { + string strFile = Environment.GetFolderPath( + Environment.SpecialFolder.Personal); + strFile = UrlUtil.EnsureTerminatingSeparator(strFile, false); + strFile += ".recently-used"; + + if(File.Exists(strFile)) + { + try + { + // Mono's WriteRecentlyUsedFiles method also loads the + // XML file using XmlDocument + XmlDocument xd = XmlUtilEx.CreateXmlDocument(); + xd.Load(strFile); + } + catch(Exception) // The XML file is invalid + { + File.Delete(strFile); + } + } + } + catch(Exception) { Debug.Assert(false); } + } +#endif // !KeePassUAP + +#if DEBUG_BREAKONFAIL + private sealed class DebugBreakTraceListener : TraceListener + { + public override void Fail(string message) + { + Debugger.Break(); + } + + public override void Fail(string message, string detailMessage) + { + Debugger.Break(); + } + + public override void Write(string message) { } + public override void WriteLine(string message) { } + } +#endif + } +} diff --git a/ModernKeePassLib/Utility/StrUtil.cs b/ModernKeePassLib/Utility/StrUtil.cs new file mode 100644 index 0000000..caa06bc --- /dev/null +++ b/ModernKeePassLib/Utility/StrUtil.cs @@ -0,0 +1,1900 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; + +#if !KeePassUAP +using System.Drawing; +using System.Security.Cryptography; +#endif + +using ModernKeePassLib.Collections; +using ModernKeePassLib.Cryptography; +using ModernKeePassLib.Cryptography.PasswordGenerator; +using ModernKeePassLib.Native; +using ModernKeePassLib.Security; + +namespace ModernKeePassLib.Utility +{ + /// + /// Character stream class. + /// + public sealed class CharStream + { + private string m_strString = string.Empty; + private int m_nPos = 0; + + public CharStream(string str) + { + Debug.Assert(str != null); + if(str == null) throw new ArgumentNullException("str"); + + m_strString = str; + } + + public void Seek(SeekOrigin org, int nSeek) + { + if(org == SeekOrigin.Begin) + m_nPos = nSeek; + else if(org == SeekOrigin.Current) + m_nPos += nSeek; + else if(org == SeekOrigin.End) + m_nPos = m_strString.Length + nSeek; + } + + public char ReadChar() + { + if(m_nPos < 0) return char.MinValue; + if(m_nPos >= m_strString.Length) return char.MinValue; + + char chRet = m_strString[m_nPos]; + ++m_nPos; + return chRet; + } + + public char ReadChar(bool bSkipWhiteSpace) + { + if(bSkipWhiteSpace == false) return ReadChar(); + + while(true) + { + char ch = ReadChar(); + + if((ch != ' ') && (ch != '\t') && (ch != '\r') && (ch != '\n')) + return ch; + } + } + + public char PeekChar() + { + if(m_nPos < 0) return char.MinValue; + if(m_nPos >= m_strString.Length) return char.MinValue; + + return m_strString[m_nPos]; + } + + public char PeekChar(bool bSkipWhiteSpace) + { + if(bSkipWhiteSpace == false) return PeekChar(); + + int iIndex = m_nPos; + while(true) + { + if(iIndex < 0) return char.MinValue; + if(iIndex >= m_strString.Length) return char.MinValue; + + char ch = m_strString[iIndex]; + + if((ch != ' ') && (ch != '\t') && (ch != '\r') && (ch != '\n')) + return ch; + + ++iIndex; + } + } + } + + public enum StrEncodingType + { + Unknown = 0, + Default, + Ascii, + Utf7, + Utf8, + Utf16LE, + Utf16BE, + Utf32LE, + Utf32BE + } + + public sealed class StrEncodingInfo + { + private readonly StrEncodingType m_type; + public StrEncodingType Type + { + get { return m_type; } + } + + private readonly string m_strName; + public string Name + { + get { return m_strName; } + } + + private readonly Encoding m_enc; + public Encoding Encoding + { + get { return m_enc; } + } + + private readonly uint m_cbCodePoint; + /// + /// Size of a character in bytes. + /// + public uint CodePointSize + { + get { return m_cbCodePoint; } + } + + private readonly byte[] m_vSig; + /// + /// Start signature of the text (byte order mark). + /// May be null or empty, if no signature is known. + /// + public byte[] StartSignature + { + get { return m_vSig; } + } + + public StrEncodingInfo(StrEncodingType t, string strName, Encoding enc, + uint cbCodePoint, byte[] vStartSig) + { + if(strName == null) throw new ArgumentNullException("strName"); + if(enc == null) throw new ArgumentNullException("enc"); + if(cbCodePoint <= 0) throw new ArgumentOutOfRangeException("cbCodePoint"); + + m_type = t; + m_strName = strName; + m_enc = enc; + m_cbCodePoint = cbCodePoint; + m_vSig = vStartSig; + } + } + + /// + /// A class containing various string helper methods. + /// + public static class StrUtil + { + public static readonly StringComparison CaseIgnoreCmp = StringComparison.OrdinalIgnoreCase; + + public static StringComparer CaseIgnoreComparer + { + get { return StringComparer.OrdinalIgnoreCase; } + } + + private static bool m_bRtl = false; + public static bool RightToLeft + { + get { return m_bRtl; } + set { m_bRtl = value; } + } + + private static UTF8Encoding m_encUtf8 = null; + public static UTF8Encoding Utf8 + { + get + { + if(m_encUtf8 == null) m_encUtf8 = new UTF8Encoding(false, false); + return m_encUtf8; + } + } + + private static List m_lEncs = null; + public static IEnumerable Encodings + { + get + { + if(m_lEncs != null) return m_lEncs; + + List l = new List(); + + l.Add(new StrEncodingInfo(StrEncodingType.Default, +#if ModernKeePassLib ||KeePassUAP + "Unicode (UTF-8)", StrUtil.Utf8, 1, new byte[] { 0xEF, 0xBB, 0xBF })); +#else +#if !KeePassLibSD + Encoding.Default.EncodingName, +#else + Encoding.Default.WebName, +#endif + Encoding.Default, + (uint)Encoding.Default.GetBytes("a").Length, null)); +#endif +#if !ModernKeePassLib && !KeePassRT + l.Add(new StrEncodingInfo(StrEncodingType.Ascii, + "ASCII", Encoding.ASCII, 1, null)); + l.Add(new StrEncodingInfo(StrEncodingType.Utf7, + "Unicode (UTF-7)", Encoding.UTF7, 1, null)); +#endif + l.Add(new StrEncodingInfo(StrEncodingType.Utf8, + "Unicode (UTF-8)", StrUtil.Utf8, 1, new byte[] { 0xEF, 0xBB, 0xBF })); + l.Add(new StrEncodingInfo(StrEncodingType.Utf16LE, + "Unicode (UTF-16 LE)", new UnicodeEncoding(false, false), + 2, new byte[] { 0xFF, 0xFE })); + l.Add(new StrEncodingInfo(StrEncodingType.Utf16BE, + "Unicode (UTF-16 BE)", new UnicodeEncoding(true, false), + 2, new byte[] { 0xFE, 0xFF })); +#if (!ModernKeePassLib && !KeePassLibSD && !KeePassRT) + l.Add(new StrEncodingInfo(StrEncodingType.Utf32LE, + "Unicode (UTF-32 LE)", new UTF32Encoding(false, false), + 4, new byte[] { 0xFF, 0xFE, 0x0, 0x0 })); + l.Add(new StrEncodingInfo(StrEncodingType.Utf32BE, + "Unicode (UTF-32 BE)", new UTF32Encoding(true, false), + 4, new byte[] { 0x0, 0x0, 0xFE, 0xFF })); +#endif + + m_lEncs = l; + return l; + } + } + + // public static string RtfPar + // { + // // get { return (m_bRtl ? "\\rtlpar " : "\\par "); } + // get { return "\\par "; } + // } + + // /// + // /// Convert a string into a valid RTF string. + // /// + // /// Any string. + // /// RTF-encoded string. + // public static string MakeRtfString(string str) + // { + // Debug.Assert(str != null); if(str == null) throw new ArgumentNullException("str"); + // str = str.Replace("\\", "\\\\"); + // str = str.Replace("\r", string.Empty); + // str = str.Replace("{", "\\{"); + // str = str.Replace("}", "\\}"); + // str = str.Replace("\n", StrUtil.RtfPar); + // StringBuilder sbEncoded = new StringBuilder(); + // for(int i = 0; i < str.Length; ++i) + // { + // char ch = str[i]; + // if((int)ch >= 256) + // sbEncoded.Append(StrUtil.RtfEncodeChar(ch)); + // else sbEncoded.Append(ch); + // } + // return sbEncoded.ToString(); + // } + + public static string RtfEncodeChar(char ch) + { + // Unicode character values must be encoded using + // 16-bit numbers (decimal); Unicode values greater + // than 32767 must be expressed as negative numbers + short sh = (short)ch; + return ("\\u" + sh.ToString(NumberFormatInfo.InvariantInfo) + "?"); + } + + internal static bool RtfIsURtf(string str) + { + if(str == null) { Debug.Assert(false); return false; } + + const string p = "{\\urtf"; // Typically "{\\urtf1\\ansi\\ansicpg65001" + return (str.StartsWith(p) && (str.Length > p.Length) && + char.IsDigit(str[p.Length])); + } + + public static string RtfFix(string strRtf) + { + if(strRtf == null) { Debug.Assert(false); return string.Empty; } + + string str = strRtf; + + // Workaround for .NET bug: the Rtf property of a RichTextBox + // can return an RTF text starting with "{\\urtf", but + // setting such an RTF text throws an exception (the setter + // checks for the RTF text to start with "{\\rtf"); + // https://sourceforge.net/p/keepass/discussion/329221/thread/7788872f/ + // https://www.microsoft.com/en-us/download/details.aspx?id=10725 + // https://msdn.microsoft.com/en-us/library/windows/desktop/bb774284.aspx + // https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/RichTextBox.cs + if(RtfIsURtf(str)) str = str.Remove(2, 1); // Remove the 'u' + + return str; + } + + internal static bool ContainsHighChar(string str) + { + if(str == null) { Debug.Assert(false); return false; } + + for(int i = 0; i < str.Length; ++i) + { + if(str[i] > '\u00FF') return true; + } + + return false; + } + + /// + /// Convert a string to a HTML sequence representing that string. + /// + /// String to convert. + /// String, HTML-encoded. + public static string StringToHtml(string str) + { + return StringToHtml(str, false); + } + + internal static string StringToHtml(string str, bool bNbsp) + { + Debug.Assert(str != null); if(str == null) throw new ArgumentNullException("str"); + + str = str.Replace(@"&", @"&"); // Must be first + str = str.Replace(@"<", @"<"); + str = str.Replace(@">", @">"); + str = str.Replace("\"", @"""); + str = str.Replace("\'", @"'"); + + if(bNbsp) str = str.Replace(" ", @" "); // Before
+ + str = NormalizeNewLines(str, false); + str = str.Replace("\n", @"
" + MessageService.NewLine); + + return str; + } + + public static string XmlToString(string str) + { + Debug.Assert(str != null); if(str == null) throw new ArgumentNullException("str"); + + str = str.Replace(@"&", @"&"); + str = str.Replace(@"<", @"<"); + str = str.Replace(@">", @">"); + str = str.Replace(@""", "\""); + str = str.Replace(@"'", "\'"); + + return str; + } + + public static string ReplaceCaseInsensitive(string strString, string strFind, + string strNew) + { + Debug.Assert(strString != null); if(strString == null) return strString; + Debug.Assert(strFind != null); if(strFind == null) return strString; + Debug.Assert(strNew != null); if(strNew == null) return strString; + + string str = strString; + + int nPos = 0; + while(nPos < str.Length) + { + nPos = str.IndexOf(strFind, nPos, StringComparison.OrdinalIgnoreCase); + if(nPos < 0) break; + + str = str.Remove(nPos, strFind.Length); + str = str.Insert(nPos, strNew); + + nPos += strNew.Length; + } + + return str; + } + + /// + /// Split up a command line into application and argument. + /// + /// Command line to split. + /// Application path. + /// Arguments. + public static void SplitCommandLine(string strCmdLine, out string strApp, out string strArgs) + { + Debug.Assert(strCmdLine != null); if(strCmdLine == null) throw new ArgumentNullException("strCmdLine"); + + string str = strCmdLine.Trim(); + + strApp = null; strArgs = null; + + if(str.StartsWith("\"")) + { + int nSecond = str.IndexOf('\"', 1); + if(nSecond >= 1) + { + strApp = str.Substring(1, nSecond - 1).Trim(); + strArgs = str.Remove(0, nSecond + 1).Trim(); + } + } + + if(strApp == null) + { + int nSpace = str.IndexOf(' '); + + if(nSpace >= 0) + { + strApp = str.Substring(0, nSpace); + strArgs = str.Remove(0, nSpace).Trim(); + } + else strApp = strCmdLine; + } + + if(strApp == null) strApp = string.Empty; + if(strArgs == null) strArgs = string.Empty; + } + + // /// + // /// Initialize an RTF document based on given font face and size. + // /// + // /// StringBuilder to put the generated RTF into. + // /// Face name of the font to use. + // /// Size of the font to use. + // public static void InitRtf(StringBuilder sb, string strFontFace, float fFontSize) + // { + // Debug.Assert(sb != null); if(sb == null) throw new ArgumentNullException("sb"); + // Debug.Assert(strFontFace != null); if(strFontFace == null) throw new ArgumentNullException("strFontFace"); + // sb.Append("{\\rtf1"); + // if(m_bRtl) sb.Append("\\fbidis"); + // sb.Append("\\ansi\\ansicpg"); + // sb.Append(Encoding.Default.CodePage); + // sb.Append("\\deff0{\\fonttbl{\\f0\\fswiss MS Sans Serif;}{\\f1\\froman\\fcharset2 Symbol;}{\\f2\\fswiss "); + // sb.Append(strFontFace); + // sb.Append(";}{\\f3\\fswiss Arial;}}"); + // sb.Append("{\\colortbl\\red0\\green0\\blue0;}"); + // if(m_bRtl) sb.Append("\\rtldoc"); + // sb.Append("\\deflang1031\\pard\\plain\\f2\\cf0 "); + // sb.Append("\\fs"); + // sb.Append((int)(fFontSize * 2)); + // if(m_bRtl) sb.Append("\\rtlpar\\qr\\rtlch "); + // } + + // /// + // /// Convert a simple HTML string to an RTF string. + // /// + // /// Input HTML string. + // /// RTF string representing the HTML input string. + // public static string SimpleHtmlToRtf(string strHtmlString) + // { + // StringBuilder sb = new StringBuilder(); + // StrUtil.InitRtf(sb, "Microsoft Sans Serif", 8.25f); + // sb.Append(" "); + // string str = MakeRtfString(strHtmlString); + // str = str.Replace("", "\\b "); + // str = str.Replace("", "\\b0 "); + // str = str.Replace("", "\\i "); + // str = str.Replace("", "\\i0 "); + // str = str.Replace("", "\\ul "); + // str = str.Replace("", "\\ul0 "); + // str = str.Replace("
", StrUtil.RtfPar); + // sb.Append(str); + // return sb.ToString(); + // } + + /// + /// Convert a Color to a HTML color identifier string. + /// + /// Color to convert. + /// If this is true, an empty string + /// is returned if the color is transparent. + /// HTML color identifier string. + public static string ColorToUnnamedHtml(Color color, bool bEmptyIfTransparent) + { + if(bEmptyIfTransparent && (color.A != 255)) + return string.Empty; + + StringBuilder sb = new StringBuilder(); + byte bt; + + sb.Append('#'); + + bt = (byte)(color.R >> 4); + if(bt < 10) sb.Append((char)('0' + bt)); else sb.Append((char)('A' - 10 + bt)); + bt = (byte)(color.R & 0x0F); + if(bt < 10) sb.Append((char)('0' + bt)); else sb.Append((char)('A' - 10 + bt)); + + bt = (byte)(color.G >> 4); + if(bt < 10) sb.Append((char)('0' + bt)); else sb.Append((char)('A' - 10 + bt)); + bt = (byte)(color.G & 0x0F); + if(bt < 10) sb.Append((char)('0' + bt)); else sb.Append((char)('A' - 10 + bt)); + + bt = (byte)(color.B >> 4); + if(bt < 10) sb.Append((char)('0' + bt)); else sb.Append((char)('A' - 10 + bt)); + bt = (byte)(color.B & 0x0F); + if(bt < 10) sb.Append((char)('0' + bt)); else sb.Append((char)('A' - 10 + bt)); + + return sb.ToString(); + } + + /// + /// Format an exception and convert it to a string. + /// + /// Exception to convert/format. + /// String representing the exception. + public static string FormatException(Exception excp) + { + string strText = string.Empty; + + if(!string.IsNullOrEmpty(excp.Message)) + strText += excp.Message + MessageService.NewLine; +#if !KeePassLibSD + if(!string.IsNullOrEmpty(excp.Source)) + strText += excp.Source + MessageService.NewLine; +#endif + if(!string.IsNullOrEmpty(excp.StackTrace)) + strText += excp.StackTrace + MessageService.NewLine; +#if !KeePassLibSD +#if !ModernKeePassLib && !KeePassRT + if(excp.TargetSite != null) + strText += excp.TargetSite.ToString() + MessageService.NewLine; +#endif + + if(excp.Data != null) + { + strText += MessageService.NewLine; + foreach(DictionaryEntry de in excp.Data) + strText += @"'" + de.Key + @"' -> '" + de.Value + @"'" + + MessageService.NewLine; + } +#endif + + if(excp.InnerException != null) + { + strText += MessageService.NewLine + "Inner:" + MessageService.NewLine; + if(!string.IsNullOrEmpty(excp.InnerException.Message)) + strText += excp.InnerException.Message + MessageService.NewLine; +#if !KeePassLibSD + if(!string.IsNullOrEmpty(excp.InnerException.Source)) + strText += excp.InnerException.Source + MessageService.NewLine; +#endif + if(!string.IsNullOrEmpty(excp.InnerException.StackTrace)) + strText += excp.InnerException.StackTrace + MessageService.NewLine; +#if !KeePassLibSD +#if !ModernKeePassLib && !KeePassRT + if(excp.InnerException.TargetSite != null) + strText += excp.InnerException.TargetSite.ToString(); +#endif + + if(excp.InnerException.Data != null) + { + strText += MessageService.NewLine; + foreach(DictionaryEntry de in excp.InnerException.Data) + strText += @"'" + de.Key + @"' -> '" + de.Value + @"'" + + MessageService.NewLine; + } +#endif + } + + return strText; + } + + public static bool TryParseUShort(string str, out ushort u) + { +#if !KeePassLibSD + return ushort.TryParse(str, out u); +#else + try { u = ushort.Parse(str); return true; } + catch(Exception) { u = 0; return false; } +#endif + } + + public static bool TryParseInt(string str, out int n) + { +#if !KeePassLibSD + return int.TryParse(str, out n); +#else + try { n = int.Parse(str); return true; } + catch(Exception) { n = 0; } + return false; +#endif + } + + public static bool TryParseIntInvariant(string str, out int n) + { +#if !KeePassLibSD + return int.TryParse(str, NumberStyles.Integer, + NumberFormatInfo.InvariantInfo, out n); +#else + try + { + n = int.Parse(str, NumberStyles.Integer, + NumberFormatInfo.InvariantInfo); + return true; + } + catch(Exception) { n = 0; } + return false; +#endif + } + + public static bool TryParseUInt(string str, out uint u) + { +#if !KeePassLibSD + return uint.TryParse(str, out u); +#else + try { u = uint.Parse(str); return true; } + catch(Exception) { u = 0; } + return false; +#endif + } + + public static bool TryParseUIntInvariant(string str, out uint u) + { +#if !KeePassLibSD + return uint.TryParse(str, NumberStyles.Integer, + NumberFormatInfo.InvariantInfo, out u); +#else + try + { + u = uint.Parse(str, NumberStyles.Integer, + NumberFormatInfo.InvariantInfo); + return true; + } + catch(Exception) { u = 0; } + return false; +#endif + } + + public static bool TryParseLong(string str, out long n) + { +#if !KeePassLibSD + return long.TryParse(str, out n); +#else + try { n = long.Parse(str); return true; } + catch(Exception) { n = 0; } + return false; +#endif + } + + public static bool TryParseLongInvariant(string str, out long n) + { +#if !KeePassLibSD + return long.TryParse(str, NumberStyles.Integer, + NumberFormatInfo.InvariantInfo, out n); +#else + try + { + n = long.Parse(str, NumberStyles.Integer, + NumberFormatInfo.InvariantInfo); + return true; + } + catch(Exception) { n = 0; } + return false; +#endif + } + + public static bool TryParseULong(string str, out ulong u) + { +#if !KeePassLibSD + return ulong.TryParse(str, out u); +#else + try { u = ulong.Parse(str); return true; } + catch(Exception) { u = 0; } + return false; +#endif + } + + public static bool TryParseULongInvariant(string str, out ulong u) + { +#if !KeePassLibSD + return ulong.TryParse(str, NumberStyles.Integer, + NumberFormatInfo.InvariantInfo, out u); +#else + try + { + u = ulong.Parse(str, NumberStyles.Integer, + NumberFormatInfo.InvariantInfo); + return true; + } + catch(Exception) { u = 0; } + return false; +#endif + } + + public static bool TryParseDateTime(string str, out DateTime dt) + { +#if !KeePassLibSD + return DateTime.TryParse(str, out dt); +#else + try { dt = DateTime.Parse(str); return true; } + catch(Exception) { dt = DateTime.UtcNow; } + return false; +#endif + } + + public static string CompactString3Dots(string strText, int cchMax) + { + Debug.Assert(strText != null); + if(strText == null) throw new ArgumentNullException("strText"); + Debug.Assert(cchMax >= 0); + if(cchMax < 0) throw new ArgumentOutOfRangeException("cchMax"); + + if(strText.Length <= cchMax) return strText; + + if(cchMax == 0) return string.Empty; + if(cchMax <= 3) return new string('.', cchMax); + + return (strText.Substring(0, cchMax - 3) + "..."); + } + + public static string GetStringBetween(string strText, int nStartIndex, + string strStart, string strEnd) + { + int nTemp; + return GetStringBetween(strText, nStartIndex, strStart, strEnd, out nTemp); + } + + public static string GetStringBetween(string strText, int nStartIndex, + string strStart, string strEnd, out int nInnerStartIndex) + { + if(strText == null) throw new ArgumentNullException("strText"); + if(strStart == null) throw new ArgumentNullException("strStart"); + if(strEnd == null) throw new ArgumentNullException("strEnd"); + + nInnerStartIndex = -1; + + int nIndex = strText.IndexOf(strStart, nStartIndex); + if(nIndex < 0) return string.Empty; + + nIndex += strStart.Length; + + int nEndIndex = strText.IndexOf(strEnd, nIndex); + if(nEndIndex < 0) return string.Empty; + + nInnerStartIndex = nIndex; + return strText.Substring(nIndex, nEndIndex - nIndex); + } + + /// + /// Removes all characters that are not valid XML characters, + /// according to https://www.w3.org/TR/xml/#charsets . + /// + /// Source text. + /// Text containing only valid XML characters. + public static string SafeXmlString(string strText) + { + Debug.Assert(strText != null); // No throw + if(string.IsNullOrEmpty(strText)) return strText; + + int nLength = strText.Length; + StringBuilder sb = new StringBuilder(nLength); + + for(int i = 0; i < nLength; ++i) + { + char ch = strText[i]; + + if(((ch >= '\u0020') && (ch <= '\uD7FF')) || + (ch == '\u0009') || (ch == '\u000A') || (ch == '\u000D') || + ((ch >= '\uE000') && (ch <= '\uFFFD'))) + sb.Append(ch); + else if((ch >= '\uD800') && (ch <= '\uDBFF')) // High surrogate + { + if((i + 1) < nLength) + { + char chLow = strText[i + 1]; + if((chLow >= '\uDC00') && (chLow <= '\uDFFF')) // Low sur. + { + sb.Append(ch); + sb.Append(chLow); + ++i; + } + else { Debug.Assert(false); } // Low sur. invalid + } + else { Debug.Assert(false); } // Low sur. missing + } + + Debug.Assert((ch < '\uDC00') || (ch > '\uDFFF')); // Lonely low sur. + } + + return sb.ToString(); + } + + /* private static Regex g_rxNaturalSplit = null; + public static int CompareNaturally(string strX, string strY) + { + Debug.Assert(strX != null); + if(strX == null) throw new ArgumentNullException("strX"); + Debug.Assert(strY != null); + if(strY == null) throw new ArgumentNullException("strY"); + + if(NativeMethods.SupportsStrCmpNaturally) + return NativeMethods.StrCmpNaturally(strX, strY); + + if(g_rxNaturalSplit == null) + g_rxNaturalSplit = new Regex(@"([0-9]+)", RegexOptions.Compiled); + + string[] vPartsX = g_rxNaturalSplit.Split(strX); + string[] vPartsY = g_rxNaturalSplit.Split(strY); + + int n = Math.Min(vPartsX.Length, vPartsY.Length); + for(int i = 0; i < n; ++i) + { + string strPartX = vPartsX[i], strPartY = vPartsY[i]; + int iPartCompare; + +#if KeePassLibSD + try + { + ulong uX = ulong.Parse(strPartX); + ulong uY = ulong.Parse(strPartY); + iPartCompare = uX.CompareTo(uY); + } + catch(Exception) { iPartCompare = string.Compare(strPartX, strPartY, true); } +#else + ulong uX, uY; + if(ulong.TryParse(strPartX, out uX) && ulong.TryParse(strPartY, out uY)) + iPartCompare = uX.CompareTo(uY); + else iPartCompare = string.Compare(strPartX, strPartY, true); +#endif + + if(iPartCompare != 0) return iPartCompare; + } + + if(vPartsX.Length == vPartsY.Length) return 0; + if(vPartsX.Length < vPartsY.Length) return -1; + return 1; + } */ + + public static int CompareNaturally(string strX, string strY) + { + Debug.Assert(strX != null); + if(strX == null) throw new ArgumentNullException("strX"); + Debug.Assert(strY != null); + if(strY == null) throw new ArgumentNullException("strY"); + +#if !ModernKeePassLib + if(NativeMethods.SupportsStrCmpNaturally) + return NativeMethods.StrCmpNaturally(strX, strY); +#endif + + int cX = strX.Length; + int cY = strY.Length; + if(cX == 0) return ((cY == 0) ? 0 : -1); + if(cY == 0) return 1; + + char chFirstX = strX[0]; + char chFirstY = strY[0]; + bool bExpNum = ((chFirstX >= '0') && (chFirstX <= '9')); + bool bExpNumY = ((chFirstY >= '0') && (chFirstY <= '9')); +#if ModernKeePassLib + if (bExpNum != bExpNumY) return StringComparer.OrdinalIgnoreCase.Compare(strX, strY); +#else + if(bExpNum != bExpNumY) return string.Compare(strX, strY, true); +#endif + + int pX = 0; + int pY = 0; + while((pX < cX) && (pY < cY)) + { + Debug.Assert(((strX[pX] >= '0') && (strX[pX] <= '9')) == bExpNum); + Debug.Assert(((strY[pY] >= '0') && (strY[pY] <= '9')) == bExpNum); + + int pExclX = pX + 1; + while(pExclX < cX) + { + char ch = strX[pExclX]; + bool bChNum = ((ch >= '0') && (ch <= '9')); + if(bChNum != bExpNum) break; + ++pExclX; + } + + int pExclY = pY + 1; + while(pExclY < cY) + { + char ch = strY[pExclY]; + bool bChNum = ((ch >= '0') && (ch <= '9')); + if(bChNum != bExpNum) break; + ++pExclY; + } + + string strPartX = strX.Substring(pX, pExclX - pX); + string strPartY = strY.Substring(pY, pExclY - pY); + + bool bStrCmp = true; + if(bExpNum) + { + // 2^64 - 1 = 18446744073709551615 has length 20 + if((strPartX.Length <= 19) && (strPartY.Length <= 19)) + { + ulong uX, uY; + if(ulong.TryParse(strPartX, out uX) && ulong.TryParse(strPartY, out uY)) + { + if(uX < uY) return -1; + if(uX > uY) return 1; + + bStrCmp = false; + } + else { Debug.Assert(false); } + } + else + { + double dX, dY; + if(double.TryParse(strPartX, out dX) && double.TryParse(strPartY, out dY)) + { + if(dX < dY) return -1; + if(dX > dY) return 1; + + bStrCmp = false; + } + else { Debug.Assert(false); } + } + } + if(bStrCmp) + { +#if ModernKeePassLib + int c = StringComparer.OrdinalIgnoreCase.Compare(strPartX, strPartY); +#else + int c = string.Compare(strPartX, strPartY, true); +#endif + if(c != 0) return c; + } + + bExpNum = !bExpNum; + pX = pExclX; + pY = pExclY; + } + + if(pX >= cX) + { + Debug.Assert(pX == cX); + if(pY >= cY) { Debug.Assert(pY == cY); return 0; } + return -1; + } + + Debug.Assert(pY == cY); + return 1; + } + + public static string RemoveAccelerator(string strMenuText) + { + if(strMenuText == null) throw new ArgumentNullException("strMenuText"); + + string str = strMenuText; + + for(char ch = 'A'; ch <= 'Z'; ++ch) + { + string strEnhAcc = @"(&" + ch.ToString() + @")"; + if(str.IndexOf(strEnhAcc) >= 0) + { + str = str.Replace(@" " + strEnhAcc, string.Empty); + str = str.Replace(strEnhAcc, string.Empty); + } + } + + str = str.Replace(@"&", string.Empty); + + return str; + } + + public static string AddAccelerator(string strMenuText, + List lAvailKeys) + { + if(strMenuText == null) { Debug.Assert(false); return null; } + if(lAvailKeys == null) { Debug.Assert(false); return strMenuText; } + + int xa = -1, xs = 0; + for(int i = 0; i < strMenuText.Length; ++i) + { + char ch = strMenuText[i]; + +#if KeePassLibSD + char chUpper = char.ToUpper(ch); +#else + char chUpper = char.ToUpperInvariant(ch); +#endif + xa = lAvailKeys.IndexOf(chUpper); + if(xa >= 0) { xs = i; break; } + +#if KeePassLibSD + char chLower = char.ToLower(ch); +#else + char chLower = char.ToLowerInvariant(ch); +#endif + xa = lAvailKeys.IndexOf(chLower); + if(xa >= 0) { xs = i; break; } + } + + if(xa < 0) return strMenuText; + + lAvailKeys.RemoveAt(xa); + return strMenuText.Insert(xs, @"&"); + } + + public static string EncodeMenuText(string strText) + { + if(strText == null) throw new ArgumentNullException("strText"); + + return strText.Replace(@"&", @"&&"); + } + + public static string EncodeToolTipText(string strText) + { + if(strText == null) throw new ArgumentNullException("strText"); + + return strText.Replace(@"&", @"&&&"); + } + + public static bool IsHexString(string str, bool bStrict) + { + if(str == null) throw new ArgumentNullException("str"); + + foreach(char ch in str) + { + if((ch >= '0') && (ch <= '9')) continue; + if((ch >= 'a') && (ch <= 'f')) continue; + if((ch >= 'A') && (ch <= 'F')) continue; + + if(bStrict) return false; + + if((ch == ' ') || (ch == '\t') || (ch == '\r') || (ch == '\n')) + continue; + + return false; + } + + return true; + } + + public static bool IsHexString(byte[] pbUtf8, bool bStrict) + { + if(pbUtf8 == null) throw new ArgumentNullException("pbUtf8"); + + for(int i = 0; i < pbUtf8.Length; ++i) + { + byte bt = pbUtf8[i]; + if((bt >= (byte)'0') && (bt <= (byte)'9')) continue; + if((bt >= (byte)'a') && (bt <= (byte)'f')) continue; + if((bt >= (byte)'A') && (bt <= (byte)'F')) continue; + + if(bStrict) return false; + + if((bt == (byte)' ') || (bt == (byte)'\t') || + (bt == (byte)'\r') || (bt == (byte)'\n')) + continue; + + return false; + } + + return true; + } + +#if !KeePassLibSD + private static readonly char[] m_vPatternPartsSep = new char[] { '*' }; + public static bool SimplePatternMatch(string strPattern, string strText, + StringComparison sc) + { + if(strPattern == null) throw new ArgumentNullException("strPattern"); + if(strText == null) throw new ArgumentNullException("strText"); + + if(strPattern.IndexOf('*') < 0) return strText.Equals(strPattern, sc); + + string[] vPatternParts = strPattern.Split(m_vPatternPartsSep, + StringSplitOptions.RemoveEmptyEntries); + if(vPatternParts == null) { Debug.Assert(false); return true; } + if(vPatternParts.Length == 0) return true; + + if(strText.Length == 0) return false; + + if(!strPattern.StartsWith(@"*") && !strText.StartsWith(vPatternParts[0], sc)) + { + return false; + } + + if(!strPattern.EndsWith(@"*") && !strText.EndsWith(vPatternParts[ + vPatternParts.Length - 1], sc)) + { + return false; + } + + int iOffset = 0; + for(int i = 0; i < vPatternParts.Length; ++i) + { + string strPart = vPatternParts[i]; + + int iFound = strText.IndexOf(strPart, iOffset, sc); + if(iFound < iOffset) return false; + + iOffset = iFound + strPart.Length; + if(iOffset == strText.Length) return (i == (vPatternParts.Length - 1)); + } + + return true; + } +#endif // !KeePassLibSD + + public static bool StringToBool(string str) + { + if(string.IsNullOrEmpty(str)) return false; // No assert + + string s = str.Trim().ToLower(); + if(s == "true") return true; + if(s == "yes") return true; + if(s == "1") return true; + if(s == "enabled") return true; + if(s == "checked") return true; + + return false; + } + + public static bool? StringToBoolEx(string str) + { + if(string.IsNullOrEmpty(str)) return null; + + string s = str.Trim().ToLower(); + if(s == "true") return true; + if(s == "false") return false; + + return null; + } + + public static string BoolToString(bool bValue) + { + return (bValue ? "true" : "false"); + } + + public static string BoolToStringEx(bool? bValue) + { + if(bValue.HasValue) return BoolToString(bValue.Value); + return "null"; + } + + /// + /// Normalize new line characters in a string. Input strings may + /// contain mixed new line character sequences from all commonly + /// used operating systems (i.e. \r\n from Windows, \n from Unix + /// and \r from Mac OS. + /// + /// String with mixed new line characters. + /// If true, new line characters + /// are normalized for Windows (\r\n); if false, new line + /// characters are normalized for Unix (\n). + /// String with normalized new line characters. + public static string NormalizeNewLines(string str, bool bWindows) + { + if(string.IsNullOrEmpty(str)) return str; + + str = str.Replace("\r\n", "\n"); + str = str.Replace("\r", "\n"); + + if(bWindows) str = str.Replace("\n", "\r\n"); + + return str; + } + + public static void NormalizeNewLines(ProtectedStringDictionary dict, + bool bWindows) + { + if(dict == null) { Debug.Assert(false); return; } + + List lKeys = dict.GetKeys(); + foreach(string strKey in lKeys) + { + ProtectedString ps = dict.Get(strKey); + if(ps == null) { Debug.Assert(false); continue; } + + char[] v = ps.ReadChars(); + if(!IsNewLineNormalized(v, bWindows)) + dict.Set(strKey, new ProtectedString(ps.IsProtected, + NormalizeNewLines(ps.ReadString(), bWindows))); + MemUtil.ZeroArray(v); + } + } + + internal static bool IsNewLineNormalized(char[] v, bool bWindows) + { + if(v == null) { Debug.Assert(false); return true; } + + if(bWindows) + { + int iFreeCr = -2; // Must be < -1 (for test "!= (i - 1)") + + for(int i = 0; i < v.Length; ++i) + { + char ch = v[i]; + + if(ch == '\r') + { + if(iFreeCr >= 0) return false; + iFreeCr = i; + } + else if(ch == '\n') + { + if(iFreeCr != (i - 1)) return false; + iFreeCr = -2; // Consume \r + } + } + + return (iFreeCr < 0); // Ensure no \r at end + } + + return (Array.IndexOf(v, '\r') < 0); + } + + public static string GetNewLineSeq(string str) + { + if(str == null) { Debug.Assert(false); return MessageService.NewLine; } + + int n = str.Length, nLf = 0, nCr = 0, nCrLf = 0; + char chLast = char.MinValue; + for(int i = 0; i < n; ++i) + { + char ch = str[i]; + + if(ch == '\r') ++nCr; + else if(ch == '\n') + { + ++nLf; + if(chLast == '\r') ++nCrLf; + } + + chLast = ch; + } + + nCr -= nCrLf; + nLf -= nCrLf; + + int nMax = Math.Max(nCrLf, Math.Max(nCr, nLf)); + if(nMax == 0) return MessageService.NewLine; + + if(nCrLf == nMax) return "\r\n"; + return ((nLf == nMax) ? "\n" : "\r"); + } + + public static string AlphaNumericOnly(string str) + { + if(string.IsNullOrEmpty(str)) return str; + + StringBuilder sb = new StringBuilder(); + for(int i = 0; i < str.Length; ++i) + { + char ch = str[i]; + if(((ch >= 'a') && (ch <= 'z')) || ((ch >= 'A') && (ch <= 'Z')) || + ((ch >= '0') && (ch <= '9'))) + sb.Append(ch); + } + + return sb.ToString(); + } + + public static string FormatDataSize(ulong uBytes) + { + const ulong uKB = 1024; + const ulong uMB = uKB * uKB; + const ulong uGB = uMB * uKB; + const ulong uTB = uGB * uKB; + + if(uBytes == 0) return "0 KB"; + if(uBytes <= uKB) return "1 KB"; + if(uBytes <= uMB) return (((uBytes - 1UL) / uKB) + 1UL).ToString() + " KB"; + if(uBytes <= uGB) return (((uBytes - 1UL) / uMB) + 1UL).ToString() + " MB"; + if(uBytes <= uTB) return (((uBytes - 1UL) / uGB) + 1UL).ToString() + " GB"; + + return (((uBytes - 1UL)/ uTB) + 1UL).ToString() + " TB"; + } + + public static string FormatDataSizeKB(ulong uBytes) + { + const ulong uKB = 1024; + + if(uBytes == 0) return "0 KB"; + if(uBytes <= uKB) return "1 KB"; + + return (((uBytes - 1UL) / uKB) + 1UL).ToString() + " KB"; + } + + private static readonly char[] m_vVersionSep = new char[]{ '.', ',' }; + public static ulong ParseVersion(string strVersion) + { + if(strVersion == null) { Debug.Assert(false); return 0; } + + string[] vVer = strVersion.Split(m_vVersionSep); + if((vVer == null) || (vVer.Length == 0)) { Debug.Assert(false); return 0; } + + ushort uPart; + StrUtil.TryParseUShort(vVer[0].Trim(), out uPart); + ulong uVer = ((ulong)uPart << 48); + + if(vVer.Length >= 2) + { + StrUtil.TryParseUShort(vVer[1].Trim(), out uPart); + uVer |= ((ulong)uPart << 32); + } + + if(vVer.Length >= 3) + { + StrUtil.TryParseUShort(vVer[2].Trim(), out uPart); + uVer |= ((ulong)uPart << 16); + } + + if(vVer.Length >= 4) + { + StrUtil.TryParseUShort(vVer[3].Trim(), out uPart); + uVer |= (ulong)uPart; + } + + return uVer; + } + + public static string VersionToString(ulong uVersion) + { + return VersionToString(uVersion, 1U); + } + + [Obsolete] + public static string VersionToString(ulong uVersion, + bool bEnsureAtLeastTwoComp) + { + return VersionToString(uVersion, (bEnsureAtLeastTwoComp ? 2U : 1U)); + } + + public static string VersionToString(ulong uVersion, uint uMinComp) + { + StringBuilder sb = new StringBuilder(); + uint uComp = 0; + + for(int i = 0; i < 4; ++i) + { + if(uVersion == 0UL) break; + + ushort us = (ushort)(uVersion >> 48); + + if(sb.Length > 0) sb.Append('.'); + + sb.Append(us.ToString(NumberFormatInfo.InvariantInfo)); + ++uComp; + + uVersion <<= 16; + } + + while(uComp < uMinComp) + { + if(sb.Length > 0) sb.Append('.'); + + sb.Append('0'); + ++uComp; + } + + return sb.ToString(); + } + + private static readonly byte[] m_pbOptEnt = { 0xA5, 0x74, 0x2E, 0xEC }; + + public static string EncryptString(string strPlainText) + { + if(string.IsNullOrEmpty(strPlainText)) return string.Empty; + + try + { + byte[] pbPlain = StrUtil.Utf8.GetBytes(strPlainText); + byte[] pbEnc = CryptoUtil.ProtectData(pbPlain, m_pbOptEnt, + DataProtectionScope.CurrentUser); + +#if (!ModernKeePassLib && !KeePassLibSD && !KeePassRT) + return Convert.ToBase64String(pbEnc, Base64FormattingOptions.None); +#else + return Convert.ToBase64String(pbEnc); +#endif + } + catch(Exception) { Debug.Assert(false); } + + return strPlainText; + } + + public static string DecryptString(string strCipherText) + { + if(string.IsNullOrEmpty(strCipherText)) return string.Empty; + + try + { + byte[] pbEnc = Convert.FromBase64String(strCipherText); + byte[] pbPlain = CryptoUtil.UnprotectData(pbEnc, m_pbOptEnt, + DataProtectionScope.CurrentUser); + + return StrUtil.Utf8.GetString(pbPlain, 0, pbPlain.Length); + } + catch(Exception) { Debug.Assert(false); } + + return strCipherText; + } + + public static string SerializeIntArray(int[] vNumbers) + { + if(vNumbers == null) throw new ArgumentNullException("vNumbers"); + + StringBuilder sb = new StringBuilder(); + for(int i = 0; i < vNumbers.Length; ++i) + { + if(i > 0) sb.Append(' '); + sb.Append(vNumbers[i].ToString(NumberFormatInfo.InvariantInfo)); + } + + return sb.ToString(); + } + + public static int[] DeserializeIntArray(string strSerialized) + { + if(strSerialized == null) throw new ArgumentNullException("strSerialized"); + if(strSerialized.Length == 0) return new int[0]; + + string[] vParts = strSerialized.Split(' '); + int[] v = new int[vParts.Length]; + + for(int i = 0; i < vParts.Length; ++i) + { + int n; + if(!TryParseIntInvariant(vParts[i], out n)) { Debug.Assert(false); } + v[i] = n; + } + + return v; + } + + private static readonly char[] m_vTagSep = new char[] { ',', ';', ':' }; + public static string TagsToString(List vTags, bool bForDisplay) + { + if(vTags == null) throw new ArgumentNullException("vTags"); + + StringBuilder sb = new StringBuilder(); + bool bFirst = true; + + foreach(string strTag in vTags) + { + if(string.IsNullOrEmpty(strTag)) { Debug.Assert(false); continue; } + Debug.Assert(strTag.IndexOfAny(m_vTagSep) < 0); + + if(!bFirst) + { + if(bForDisplay) sb.Append(", "); + else sb.Append(';'); + } + sb.Append(strTag); + + bFirst = false; + } + + return sb.ToString(); + } + + public static List StringToTags(string strTags) + { + if(strTags == null) throw new ArgumentNullException("strTags"); + + List lTags = new List(); + if(strTags.Length == 0) return lTags; + + string[] vTags = strTags.Split(m_vTagSep); + foreach(string strTag in vTags) + { + string strFlt = strTag.Trim(); + if(strFlt.Length > 0) lTags.Add(strFlt); + } + + return lTags; + } + + public static string Obfuscate(string strPlain) + { + if(strPlain == null) { Debug.Assert(false); return string.Empty; } + if(strPlain.Length == 0) return string.Empty; + + byte[] pb = StrUtil.Utf8.GetBytes(strPlain); + + Array.Reverse(pb); + for(int i = 0; i < pb.Length; ++i) pb[i] = (byte)(pb[i] ^ 0x65); + +#if (!ModernKeePassLib && !KeePassLibSD && !KeePassRT) + return Convert.ToBase64String(pb, Base64FormattingOptions.None); +#else + return Convert.ToBase64String(pb); +#endif + } + + public static string Deobfuscate(string strObf) + { + if(strObf == null) { Debug.Assert(false); return string.Empty; } + if(strObf.Length == 0) return string.Empty; + + try + { + byte[] pb = Convert.FromBase64String(strObf); + + for(int i = 0; i < pb.Length; ++i) pb[i] = (byte)(pb[i] ^ 0x65); + Array.Reverse(pb); + + return StrUtil.Utf8.GetString(pb, 0, pb.Length); + } + catch(Exception) { Debug.Assert(false); } + + return string.Empty; + } + + /// + /// Split a string and include the separators in the splitted array. + /// + /// String to split. + /// Separators. + /// Specifies whether separators are + /// matched case-sensitively or not. + /// Splitted string including separators. + public static List SplitWithSep(string str, string[] vSeps, + bool bCaseSensitive) + { + if(str == null) throw new ArgumentNullException("str"); + if(vSeps == null) throw new ArgumentNullException("vSeps"); + + List v = new List(); + while(true) + { + int minIndex = int.MaxValue, minSep = -1; + for(int i = 0; i < vSeps.Length; ++i) + { + string strSep = vSeps[i]; + if(string.IsNullOrEmpty(strSep)) { Debug.Assert(false); continue; } + + int iIndex = (bCaseSensitive ? str.IndexOf(strSep) : + str.IndexOf(strSep, StrUtil.CaseIgnoreCmp)); + if((iIndex >= 0) && (iIndex < minIndex)) + { + minIndex = iIndex; + minSep = i; + } + } + + if(minIndex == int.MaxValue) break; + + v.Add(str.Substring(0, minIndex)); + v.Add(vSeps[minSep]); + + str = str.Substring(minIndex + vSeps[minSep].Length); + } + + v.Add(str); + return v; + } + + public static string MultiToSingleLine(string strMulti) + { + if(strMulti == null) { Debug.Assert(false); return string.Empty; } + if(strMulti.Length == 0) return string.Empty; + + string str = strMulti; + str = str.Replace("\r\n", " "); + str = str.Replace("\r", " "); + str = str.Replace("\n", " "); + + return str; + } + + public static List SplitSearchTerms(string strSearch) + { + List l = new List(); + if(strSearch == null) { Debug.Assert(false); return l; } + + StringBuilder sbTerm = new StringBuilder(); + bool bQuoted = false; + + for(int i = 0; i < strSearch.Length; ++i) + { + char ch = strSearch[i]; + + if(((ch == ' ') || (ch == '\t') || (ch == '\r') || + (ch == '\n')) && !bQuoted) + { + if(sbTerm.Length > 0) l.Add(sbTerm.ToString()); + + sbTerm.Remove(0, sbTerm.Length); + } + else if(ch == '\"') bQuoted = !bQuoted; + else sbTerm.Append(ch); + } + if(sbTerm.Length > 0) l.Add(sbTerm.ToString()); + + return l; + } + + public static int CompareLengthGt(string x, string y) + { + if(x.Length == y.Length) return 0; + return ((x.Length > y.Length) ? -1 : 1); + } + + public static bool IsDataUri(string strUri) + { + return IsDataUri(strUri, null); + } + + public static bool IsDataUri(string strUri, string strReqMediaType) + { + if(strUri == null) { Debug.Assert(false); return false; } + // strReqMediaType may be null + + const string strPrefix = "data:"; + if(!strUri.StartsWith(strPrefix, StrUtil.CaseIgnoreCmp)) + return false; + + int iC = strUri.IndexOf(','); + if(iC < 0) return false; + + if(!string.IsNullOrEmpty(strReqMediaType)) + { + int iS = strUri.IndexOf(';', 0, iC); + int iTerm = ((iS >= 0) ? iS : iC); + + string strMedia = strUri.Substring(strPrefix.Length, + iTerm - strPrefix.Length); + if(!strMedia.Equals(strReqMediaType, StrUtil.CaseIgnoreCmp)) + return false; + } + + return true; + } + + /// + /// Create a data URI (according to RFC 2397). + /// + /// Data to encode. + /// Optional MIME type. If null, + /// an appropriate type is used. + /// Data URI. + public static string DataToDataUri(byte[] pbData, string strMediaType) + { + if(pbData == null) throw new ArgumentNullException("pbData"); + + if(strMediaType == null) strMediaType = "application/octet-stream"; + +#if (!ModernKeePassLib && !KeePassLibSD && !KeePassRT) + return ("data:" + strMediaType + ";base64," + Convert.ToBase64String( + pbData, Base64FormattingOptions.None)); +#else + return ("data:" + strMediaType + ";base64," + Convert.ToBase64String( + pbData)); +#endif + } + + /// + /// Convert a data URI (according to RFC 2397) to binary data. + /// + /// Data URI to decode. + /// Decoded binary data. + public static byte[] DataUriToData(string strDataUri) + { + if(strDataUri == null) throw new ArgumentNullException("strDataUri"); + if(!strDataUri.StartsWith("data:", StrUtil.CaseIgnoreCmp)) return null; + + int iSep = strDataUri.IndexOf(','); + if(iSep < 0) return null; + + string strDesc = strDataUri.Substring(5, iSep - 5); + bool bBase64 = strDesc.EndsWith(";base64", StrUtil.CaseIgnoreCmp); + + string strData = strDataUri.Substring(iSep + 1); + + if(bBase64) return Convert.FromBase64String(strData); + + MemoryStream ms = new MemoryStream(); + +#if ModernKeePassLib || KeePassRT + Encoding enc = StrUtil.Utf8; +#else + Encoding enc = Encoding.ASCII; +#endif + + string[] v = strData.Split('%'); + byte[] pb = enc.GetBytes(v[0]); + ms.Write(pb, 0, pb.Length); + for(int i = 1; i < v.Length; ++i) + { + ms.WriteByte(Convert.ToByte(v[i].Substring(0, 2), 16)); + pb = enc.GetBytes(v[i].Substring(2)); + ms.Write(pb, 0, pb.Length); + } + + pb = ms.ToArray(); + ms.Dispose(); + return pb; + } + + // https://www.iana.org/assignments/media-types/media-types.xhtml + private static readonly string[] g_vMediaTypePfx = new string[] { + "application/", "audio/", "example/", "font/", "image/", + "message/", "model/", "multipart/", "text/", "video/" + }; + internal static bool IsMediaType(string str) + { + if(str == null) { Debug.Assert(false); return false; } + if(str.Length == 0) return false; + + foreach(string strPfx in g_vMediaTypePfx) + { + if(str.StartsWith(strPfx, StrUtil.CaseIgnoreCmp)) + return true; + } + + return false; + } + + internal static string GetCustomMediaType(string strFormat) + { + if(strFormat == null) + { + Debug.Assert(false); + return "application/octet-stream"; + } + + if(IsMediaType(strFormat)) return strFormat; + + StringBuilder sb = new StringBuilder(); + for(int i = 0; i < strFormat.Length; ++i) + { + char ch = strFormat[i]; + + if(((ch >= 'A') && (ch <= 'Z')) || ((ch >= 'a') && (ch <= 'z')) || + ((ch >= '0') && (ch <= '9'))) + sb.Append(ch); + else if((sb.Length != 0) && ((ch == '-') || (ch == '_'))) + sb.Append(ch); + else { Debug.Assert(false); } + } + + if(sb.Length == 0) return "application/octet-stream"; + + return ("application/vnd." + PwDefs.ShortProductName + + "." + sb.ToString()); + } + + /// + /// Remove placeholders from a string (wrapped in '{' and '}'). + /// This doesn't remove environment variables (wrapped in '%'). + /// + public static string RemovePlaceholders(string str) + { + if(str == null) { Debug.Assert(false); return string.Empty; } + + while(true) + { + int iPlhStart = str.IndexOf('{'); + if(iPlhStart < 0) break; + + int iPlhEnd = str.IndexOf('}', iPlhStart); // '{' might be at end + if(iPlhEnd < 0) break; + + str = (str.Substring(0, iPlhStart) + str.Substring(iPlhEnd + 1)); + } + + return str; + } + + public static StrEncodingInfo GetEncoding(StrEncodingType t) + { + foreach(StrEncodingInfo sei in StrUtil.Encodings) + { + if(sei.Type == t) return sei; + } + + return null; + } + + public static StrEncodingInfo GetEncoding(string strName) + { + foreach(StrEncodingInfo sei in StrUtil.Encodings) + { + if(sei.Name == strName) return sei; + } + + return null; + } + + private static string[] m_vPrefSepChars = null; + /// + /// Find a character that does not occur within a given text. + /// + public static char GetUnusedChar(string strText) + { + if(strText == null) { Debug.Assert(false); return '@'; } + + if(m_vPrefSepChars == null) + m_vPrefSepChars = new string[5] { + "@!$%#/\\:;,.*-_?", + PwCharSet.UpperCase, PwCharSet.LowerCase, + PwCharSet.Digits, PwCharSet.PrintableAsciiSpecial + }; + + for(int i = 0; i < m_vPrefSepChars.Length; ++i) + { + foreach(char ch in m_vPrefSepChars[i]) + { + if(strText.IndexOf(ch) < 0) return ch; + } + } + + for(char ch = '\u00C0'; ch < char.MaxValue; ++ch) + { + if(strText.IndexOf(ch) < 0) return ch; + } + + return char.MinValue; + } + + public static char ByteToSafeChar(byte bt) + { + const char chDefault = '.'; + + // 00-1F are C0 control chars + if(bt < 0x20) return chDefault; + + // 20-7F are basic Latin; 7F is DEL + if(bt < 0x7F) return (char)bt; + + // 80-9F are C1 control chars + if(bt < 0xA0) return chDefault; + + // A0-FF are Latin-1 supplement; AD is soft hyphen + if(bt == 0xAD) return '-'; + return (char)bt; + } + + public static int Count(string str, string strNeedle) + { + if(str == null) { Debug.Assert(false); return 0; } + if(string.IsNullOrEmpty(strNeedle)) { Debug.Assert(false); return 0; } + + int iOffset = 0, iCount = 0; + while(iOffset < str.Length) + { + int p = str.IndexOf(strNeedle, iOffset); + if(p < 0) break; + + ++iCount; + iOffset = p + 1; + } + + return iCount; + } + + internal static string ReplaceNulls(string str) + { + if(str == null) { Debug.Assert(false); return null; } + + if(str.IndexOf('\0') < 0) return str; + + // Replacing null characters by spaces is the + // behavior of Notepad (on Windows 10) + return str.Replace('\0', ' '); + } + + // https://sourceforge.net/p/keepass/discussion/329220/thread/f98dece5/ + internal static string EnsureLtrPath(string strPath) + { + if(strPath == null) { Debug.Assert(false); return string.Empty; } + + string str = strPath; + + // U+200E = left-to-right mark + str = str.Replace("\\", "\\\u200E"); + str = str.Replace("/", "/\u200E"); + str = str.Replace("\u200E\u200E", "\u200E"); // Remove duplicates + + return str; + } + } +} diff --git a/ModernKeePassLib/Utility/StreamExtensions.cs b/ModernKeePassLib/Utility/StreamExtensions.cs new file mode 100644 index 0000000..499a4c2 --- /dev/null +++ b/ModernKeePassLib/Utility/StreamExtensions.cs @@ -0,0 +1,17 @@ +using System.IO; +using Windows.Storage.Streams; + +namespace ModernKeePassLibPCL.Utility +{ + public static class StreamExtensions + { + public static Stream AsStream(this IRandomAccessStream inputStream) + { + var reader = new DataReader(inputStream.GetInputStreamAt(0)); + var bytes = new byte[inputStream.Size]; + reader.LoadAsync((uint)inputStream.Size).GetResults(); + reader.ReadBytes(bytes); + return new MemoryStream(bytes); + } + } +} diff --git a/ModernKeePassLib/Utility/TimeUtil.cs b/ModernKeePassLib/Utility/TimeUtil.cs new file mode 100644 index 0000000..60ae625 --- /dev/null +++ b/ModernKeePassLib/Utility/TimeUtil.cs @@ -0,0 +1,482 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Globalization; +using System.Text; + +using ModernKeePassLib.Interfaces; + +namespace ModernKeePassLib.Utility +{ + /// + /// Contains various static time structure manipulation and conversion + /// routines. + /// + public static class TimeUtil + { + /// + /// Length of a compressed PW_TIME structure in bytes. + /// + public const int PwTimeLength = 7; + + public static readonly DateTime SafeMinValueUtc = new DateTime( + DateTime.MinValue.Ticks + TimeSpan.TicksPerDay, DateTimeKind.Utc); + public static readonly DateTime SafeMaxValueUtc = new DateTime( + DateTime.MaxValue.Ticks - TimeSpan.TicksPerDay, DateTimeKind.Utc); + +#if !KeePassLibSD + private static string m_strDtfStd = null; + private static string m_strDtfDate = null; +#endif + + // private static long m_lTicks2PowLess1s = 0; + + private static DateTime? m_odtUnixRoot = null; + public static DateTime UnixRoot + { + get + { + if(m_odtUnixRoot.HasValue) return m_odtUnixRoot.Value; + + DateTime dtRoot = new DateTime(1970, 1, 1, 0, 0, 0, 0, + DateTimeKind.Utc); + + m_odtUnixRoot = dtRoot; + return dtRoot; + } + } + + /// + /// Pack a DateTime object into 5 bytes. Layout: 2 zero bits, + /// year 12 bits, month 4 bits, day 5 bits, hour 5 bits, minute 6 + /// bits, second 6 bits. + /// + [Obsolete] + public static byte[] PackTime(DateTime dt) + { + dt = ToLocal(dt, true); + + byte[] pb = new byte[5]; + + // Pack time to 5 byte structure: + // Byte bits: 11111111 22222222 33333333 44444444 55555555 + // Contents : 00YYYYYY YYYYYYMM MMDDDDDH HHHHMMMM MMSSSSSS + pb[0] = (byte)((dt.Year >> 6) & 0x3F); + pb[1] = (byte)(((dt.Year & 0x3F) << 2) | ((dt.Month >> 2) & 0x03)); + pb[2] = (byte)(((dt.Month & 0x03) << 6) | ((dt.Day & 0x1F) << 1) | + ((dt.Hour >> 4) & 0x01)); + pb[3] = (byte)(((dt.Hour & 0x0F) << 4) | ((dt.Minute >> 2) & 0x0F)); + pb[4] = (byte)(((dt.Minute & 0x03) << 6) | (dt.Second & 0x3F)); + + return pb; + } + + /// + /// Unpack a packed time (5 bytes, packed by the PackTime + /// member function) to a DateTime object. + /// + /// Packed time, 5 bytes. + /// Unpacked DateTime object. + [Obsolete] + public static DateTime UnpackTime(byte[] pb) + { + Debug.Assert((pb != null) && (pb.Length == 5)); + if(pb == null) throw new ArgumentNullException("pb"); + if(pb.Length != 5) throw new ArgumentException(); + + int n1 = pb[0], n2 = pb[1], n3 = pb[2], n4 = pb[3], n5 = pb[4]; + + // Unpack 5 byte structure to date and time + int nYear = (n1 << 6) | (n2 >> 2); + int nMonth = ((n2 & 0x00000003) << 2) | (n3 >> 6); + int nDay = (n3 >> 1) & 0x0000001F; + int nHour = ((n3 & 0x00000001) << 4) | (n4 >> 4); + int nMinute = ((n4 & 0x0000000F) << 2) | (n5 >> 6); + int nSecond = n5 & 0x0000003F; + + return (new DateTime(nYear, nMonth, nDay, nHour, nMinute, + nSecond, DateTimeKind.Local)).ToUniversalTime(); + } + + /// + /// Pack a DateTime object into 7 bytes (PW_TIME). + /// + /// Object to be encoded. + /// Packed time, 7 bytes (PW_TIME). + [Obsolete] + public static byte[] PackPwTime(DateTime dt) + { + Debug.Assert(PwTimeLength == 7); + + dt = ToLocal(dt, true); + + byte[] pb = new byte[7]; + pb[0] = (byte)(dt.Year & 0xFF); + pb[1] = (byte)(dt.Year >> 8); + pb[2] = (byte)dt.Month; + pb[3] = (byte)dt.Day; + pb[4] = (byte)dt.Hour; + pb[5] = (byte)dt.Minute; + pb[6] = (byte)dt.Second; + + return pb; + } + + /// + /// Unpack a packed time (7 bytes, PW_TIME) to a DateTime object. + /// + /// Packed time, 7 bytes. + /// Unpacked DateTime object. + [Obsolete] + public static DateTime UnpackPwTime(byte[] pb) + { + Debug.Assert(PwTimeLength == 7); + + Debug.Assert(pb != null); if(pb == null) throw new ArgumentNullException("pb"); + Debug.Assert(pb.Length == 7); if(pb.Length != 7) throw new ArgumentException(); + + return (new DateTime(((int)pb[1] << 8) | (int)pb[0], (int)pb[2], (int)pb[3], + (int)pb[4], (int)pb[5], (int)pb[6], DateTimeKind.Local)).ToUniversalTime(); + } + + /// + /// Convert a DateTime object to a displayable string. + /// + /// DateTime object to convert to a string. + /// String representing the specified DateTime object. + public static string ToDisplayString(DateTime dt) + { + return ToLocal(dt, true).ToString(); + } + + public static string ToDisplayStringDateOnly(DateTime dt) + { + return ToLocal(dt, true).ToString("d"); + } + + public static DateTime FromDisplayString(string strDisplay) + { + DateTime dt; + if(FromDisplayStringEx(strDisplay, out dt)) return dt; + return DateTime.Now; + } + + public static bool FromDisplayStringEx(string strDisplay, out DateTime dt) + { +#if KeePassLibSD + try { dt = ToLocal(DateTime.Parse(strDisplay), true); return true; } + catch(Exception) { } +#else + if(DateTime.TryParse(strDisplay, out dt)) + { + dt = ToLocal(dt, true); + return true; + } + + // For some custom formats specified using the Control Panel, + // DateTime.ToString returns the correct string, but + // DateTime.TryParse fails (e.g. for "//dd/MMM/yyyy"); + // https://sourceforge.net/p/keepass/discussion/329221/thread/3a225b29/?limit=25&page=1#c6ae + if((m_strDtfStd == null) || (m_strDtfDate == null)) + { + DateTime dtUni = new DateTime(2111, 3, 4, 5, 6, 7, DateTimeKind.Local); + m_strDtfStd = DeriveCustomFormat(ToDisplayString(dtUni), dtUni); + m_strDtfDate = DeriveCustomFormat(ToDisplayStringDateOnly(dtUni), dtUni); + } + const DateTimeStyles dts = DateTimeStyles.AllowWhiteSpaces; + if(DateTime.TryParseExact(strDisplay, m_strDtfStd, null, dts, out dt)) + { + dt = ToLocal(dt, true); + return true; + } + if(DateTime.TryParseExact(strDisplay, m_strDtfDate, null, dts, out dt)) + { + dt = ToLocal(dt, true); + return true; + } +#endif + + Debug.Assert(false); + return false; + } + +#if !KeePassLibSD + private static string DeriveCustomFormat(string strDT, DateTime dt) + { + string[] vPlh = new string[] { + // Names, sorted by length + "MMMM", "dddd", + "MMM", "ddd", + "gg", "g", + + // Numbers, the ones with prefix '0' first + "yyyy", "yyy", "yy", "y", + "MM", "M", + "dd", "d", + "HH", "hh", "H", "h", + "mm", "m", + "ss", "s", + + "tt", "t" + }; + + List lValues = new List(); + foreach(string strPlh in vPlh) + { + string strEval = strPlh; + if(strEval.Length == 1) strEval = @"%" + strPlh; // Make custom + + lValues.Add(dt.ToString(strEval)); + } + + StringBuilder sbAll = new StringBuilder(); + sbAll.Append("dfFghHKmMstyz:/\"\'\\%"); + sbAll.Append(strDT); + foreach(string strVEnum in lValues) { sbAll.Append(strVEnum); } + + List lCodes = new List(); + for(int i = 0; i < vPlh.Length; ++i) + { + char ch = StrUtil.GetUnusedChar(sbAll.ToString()); + lCodes.Add(ch); + sbAll.Append(ch); + } + + string str = strDT; + for(int i = 0; i < vPlh.Length; ++i) + { + string strValue = lValues[i]; + if(string.IsNullOrEmpty(strValue)) continue; + + str = str.Replace(strValue, new string(lCodes[i], 1)); + } + + StringBuilder sbFmt = new StringBuilder(); + bool bInLiteral = false; + foreach(char ch in str) + { + int iCode = lCodes.IndexOf(ch); + + // The escape character doesn't work correctly (e.g. + // "dd\\.MM\\.yyyy\\ HH\\:mm\\:ss" doesn't work, but + // "dd'.'MM'.'yyyy' 'HH':'mm':'ss" does); use '' instead + + // if(iCode >= 0) sbFmt.Append(vPlh[iCode]); + // else // Literal + // { + // sbFmt.Append('\\'); + // sbFmt.Append(ch); + // } + + if(iCode >= 0) + { + if(bInLiteral) { sbFmt.Append('\''); bInLiteral = false; } + sbFmt.Append(vPlh[iCode]); + } + else // Literal + { + if(!bInLiteral) { sbFmt.Append('\''); bInLiteral = true; } + sbFmt.Append(ch); + } + } + if(bInLiteral) sbFmt.Append('\''); + + return sbFmt.ToString(); + } +#endif + + public static string SerializeUtc(DateTime dt) + { + Debug.Assert(dt.Kind != DateTimeKind.Unspecified); + + string str = ToUtc(dt, false).ToString("s"); + if(!str.EndsWith("Z")) str += "Z"; + return str; + } + + public static bool TryDeserializeUtc(string str, out DateTime dt) + { + if(str == null) throw new ArgumentNullException("str"); + + if(str.EndsWith("Z")) str = str.Substring(0, str.Length - 1); + + bool bResult = StrUtil.TryParseDateTime(str, out dt); + if(bResult) dt = ToUtc(dt, true); + return bResult; + } + + public static double SerializeUnix(DateTime dt) + { + return (ToUtc(dt, false) - TimeUtil.UnixRoot).TotalSeconds; + } + + public static DateTime ConvertUnixTime(double dtUnix) + { + try { return TimeUtil.UnixRoot.AddSeconds(dtUnix); } + catch(Exception) { Debug.Assert(false); } + + return DateTime.UtcNow; + } + +#if !KeePassLibSD + [Obsolete] + public static DateTime? ParseUSTextDate(string strDate) + { + return ParseUSTextDate(strDate, DateTimeKind.Unspecified); + } + + private static string[] m_vUSMonths = null; + /// + /// Parse a US textual date string, like e.g. "January 02, 2012". + /// + public static DateTime? ParseUSTextDate(string strDate, DateTimeKind k) + { + if(strDate == null) { Debug.Assert(false); return null; } + + if(m_vUSMonths == null) + m_vUSMonths = new string[] { "January", "February", "March", + "April", "May", "June", "July", "August", "September", + "October", "November", "December" }; + + string str = strDate.Trim(); + for(int i = 0; i < m_vUSMonths.Length; ++i) + { + if(str.StartsWith(m_vUSMonths[i], StrUtil.CaseIgnoreCmp)) + { + str = str.Substring(m_vUSMonths[i].Length); + string[] v = str.Split(new char[] { ',', ';' }); + if((v == null) || (v.Length != 2)) return null; + + string strDay = v[0].Trim().TrimStart('0'); + int iDay, iYear; + if(int.TryParse(strDay, out iDay) && + int.TryParse(v[1].Trim(), out iYear)) + return new DateTime(iYear, i + 1, iDay, 0, 0, 0, k); + else { Debug.Assert(false); return null; } + } + } + + return null; + } +#endif + + private static readonly DateTime m_dtInvMin = + new DateTime(2999, 12, 27, 23, 59, 59, DateTimeKind.Utc); + private static readonly DateTime m_dtInvMax = + new DateTime(2999, 12, 29, 23, 59, 59, DateTimeKind.Utc); + public static int Compare(DateTime dtA, DateTime dtB, bool bUnkIsPast) + { + Debug.Assert(dtA.Kind == dtB.Kind); + + if(bUnkIsPast) + { + // 2999-12-28 23:59:59 in KeePass 1.x means 'unknown'; + // expect time zone corruption (twice) + // bool bInvA = ((dtA.Year == 2999) && (dtA.Month == 12) && + // (dtA.Day >= 27) && (dtA.Day <= 29) && (dtA.Minute == 59) && + // (dtA.Second == 59)); + // bool bInvB = ((dtB.Year == 2999) && (dtB.Month == 12) && + // (dtB.Day >= 27) && (dtB.Day <= 29) && (dtB.Minute == 59) && + // (dtB.Second == 59)); + // Faster due to internal implementation of DateTime: + bool bInvA = ((dtA >= m_dtInvMin) && (dtA <= m_dtInvMax) && + (dtA.Minute == 59) && (dtA.Second == 59)); + bool bInvB = ((dtB >= m_dtInvMin) && (dtB <= m_dtInvMax) && + (dtB.Minute == 59) && (dtB.Second == 59)); + + if(bInvA) return (bInvB ? 0 : -1); + if(bInvB) return 1; + } + + return dtA.CompareTo(dtB); + } + + internal static int CompareLastMod(ITimeLogger tlA, ITimeLogger tlB, + bool bUnkIsPast) + { + if(tlA == null) { Debug.Assert(false); return ((tlB == null) ? 0 : -1); } + if(tlB == null) { Debug.Assert(false); return 1; } + + return Compare(tlA.LastModificationTime, tlB.LastModificationTime, + bUnkIsPast); + } + + public static DateTime ToUtc(DateTime dt, bool bUnspecifiedIsUtc) + { + DateTimeKind k = dt.Kind; + if(k == DateTimeKind.Utc) return dt; + if(k == DateTimeKind.Local) return dt.ToUniversalTime(); + + Debug.Assert(k == DateTimeKind.Unspecified); + if(bUnspecifiedIsUtc) + return new DateTime(dt.Ticks, DateTimeKind.Utc); + return dt.ToUniversalTime(); // Unspecified = local + } + + public static DateTime ToLocal(DateTime dt, bool bUnspecifiedIsLocal) + { + DateTimeKind k = dt.Kind; + if(k == DateTimeKind.Local) return dt; + if(k == DateTimeKind.Utc) return dt.ToLocalTime(); + + Debug.Assert(k == DateTimeKind.Unspecified); + if(bUnspecifiedIsLocal) + return new DateTime(dt.Ticks, DateTimeKind.Local); + return dt.ToLocalTime(); // Unspecified = UTC + } + + /* internal static DateTime RoundToMultOf2PowLess1s(DateTime dt) + { + long l2Pow = m_lTicks2PowLess1s; + if(l2Pow == 0) + { + l2Pow = 1; + while(true) + { + l2Pow <<= 1; + if(l2Pow >= TimeSpan.TicksPerSecond) break; + } + l2Pow >>= 1; + m_lTicks2PowLess1s = l2Pow; + + Debug.Assert(TimeSpan.TicksPerSecond == 10000000L); // .NET + Debug.Assert(l2Pow == (1L << 23)); // .NET + } + + long l = dt.Ticks; + if((l % l2Pow) == 0L) return dt; + + // Round down to full second + l /= TimeSpan.TicksPerSecond; + l *= TimeSpan.TicksPerSecond; + + // Round up to multiple of l2Pow + long l2PowM1 = l2Pow - 1L; + l = (l + l2PowM1) & ~l2PowM1; + DateTime dtRnd = new DateTime(l, dt.Kind); + + Debug.Assert((dtRnd.Ticks % l2Pow) == 0L); + Debug.Assert(dtRnd.ToString("u") == dt.ToString("u")); + return dtRnd; + } */ + } +} diff --git a/ModernKeePassLib/Utility/TypeOverridePool.cs b/ModernKeePassLib/Utility/TypeOverridePool.cs new file mode 100644 index 0000000..4038bbc --- /dev/null +++ b/ModernKeePassLib/Utility/TypeOverridePool.cs @@ -0,0 +1,65 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Text; + +using ModernKeePassLib.Delegates; + +namespace KeePassLib.Utility +{ + public static class TypeOverridePool + { + private static Dictionary> g_d = + new Dictionary>(); + + public static void Register(Type t, GFunc f) + { + if(t == null) throw new ArgumentNullException("t"); + if(f == null) throw new ArgumentNullException("f"); + + g_d[t] = f; + } + + public static void Unregister(Type t) + { + if(t == null) throw new ArgumentNullException("t"); + + g_d.Remove(t); + } + + public static bool IsRegistered(Type t) + { + if(t == null) throw new ArgumentNullException("t"); + + return g_d.ContainsKey(t); + } + + public static T CreateInstance() + where T : new() + { + GFunc f; + if(g_d.TryGetValue(typeof(T), out f)) + return (T)(f()); + + return new T(); + } + } +} diff --git a/ModernKeePassLib/Utility/UrlUtil.cs b/ModernKeePassLib/Utility/UrlUtil.cs new file mode 100644 index 0000000..1e89085 --- /dev/null +++ b/ModernKeePassLib/Utility/UrlUtil.cs @@ -0,0 +1,787 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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.Globalization; +using System.IO; +using System.Text; + +#if ModernKeePassLib +using Windows.Storage; +#endif + +using ModernKeePassLib.Native; + +namespace ModernKeePassLib.Utility +{ + /// + /// A class containing various static path utility helper methods (like + /// stripping extension from a file, etc.). + /// + public static class UrlUtil + { + private static readonly char[] m_vDirSeps = new char[] { + '\\', '/', UrlUtil.LocalDirSepChar }; +#if !ModernKeePassLib + private static readonly char[] m_vPathTrimCharsWs = new char[] { + '\"', ' ', '\t', '\r', '\n' }; +#endif + + public static char LocalDirSepChar + { +#if KeePassRT + get { return '\\'; } +#elif ModernKeePassLib + //get { return PortablePath.DirectorySeparatorChar; } + get { return '\\'; } +#else + get { return Path.DirectorySeparatorChar; } +#endif + } + + /// + /// Get the directory (path) of a file name. The returned string may be + /// terminated by a directory separator character. Example: + /// passing C:\\My Documents\\My File.kdb in + /// and true to + /// would produce this string: C:\\My Documents\\. + /// + /// Full path of a file. + /// Append a terminating directory separator + /// character to the returned path. + /// If true, the returned path + /// is guaranteed to be a valid directory path (for example X:\\ instead + /// of X:, overriding ). + /// This should only be set to true, if the returned path is directly + /// passed to some directory API. + /// Directory of the file. + public static string GetFileDirectory(string strFile, bool bAppendTerminatingChar, + bool bEnsureValidDirSpec) + { + Debug.Assert(strFile != null); + if(strFile == null) throw new ArgumentNullException("strFile"); + + int nLastSep = strFile.LastIndexOfAny(m_vDirSeps); + if(nLastSep < 0) return string.Empty; // No directory + + if(bEnsureValidDirSpec && (nLastSep == 2) && (strFile[1] == ':') && + (strFile[2] == '\\')) // Length >= 3 and Windows root directory + bAppendTerminatingChar = true; + + if(!bAppendTerminatingChar) return strFile.Substring(0, nLastSep); + return EnsureTerminatingSeparator(strFile.Substring(0, nLastSep), + (strFile[nLastSep] == '/')); + } + + /// + /// Gets the file name of the specified file (full path). Example: + /// if is C:\\My Documents\\My File.kdb + /// the returned string is My File.kdb. + /// + /// Full path of a file. + /// File name of the specified file. The return value is + /// an empty string ("") if the input parameter is null. + public static string GetFileName(string strPath) + { + Debug.Assert(strPath != null); if(strPath == null) throw new ArgumentNullException("strPath"); + + int nLastSep = strPath.LastIndexOfAny(m_vDirSeps); + + if(nLastSep < 0) return strPath; + if(nLastSep >= (strPath.Length - 1)) return string.Empty; + + return strPath.Substring(nLastSep + 1); + } + + /// + /// Strip the extension of a file. + /// + /// Full path of a file with extension. + /// File name without extension. + public static string StripExtension(string strPath) + { + Debug.Assert(strPath != null); if(strPath == null) throw new ArgumentNullException("strPath"); + + int nLastDirSep = strPath.LastIndexOfAny(m_vDirSeps); + int nLastExtDot = strPath.LastIndexOf('.'); + + if(nLastExtDot <= nLastDirSep) return strPath; + + return strPath.Substring(0, nLastExtDot); + } + + /// + /// Get the extension of a file. + /// + /// Full path of a file with extension. + /// Extension without prepending dot. + public static string GetExtension(string strPath) + { + Debug.Assert(strPath != null); if(strPath == null) throw new ArgumentNullException("strPath"); + + int nLastDirSep = strPath.LastIndexOfAny(m_vDirSeps); + int nLastExtDot = strPath.LastIndexOf('.'); + + if(nLastExtDot <= nLastDirSep) return string.Empty; + if(nLastExtDot == (strPath.Length - 1)) return string.Empty; + + return strPath.Substring(nLastExtDot + 1); + } + + /// + /// Ensure that a path is terminated with a directory separator character. + /// + /// Input path. + /// If true, a slash (/) is appended to + /// the string if it's not terminated already. If false, the + /// default system directory separator character is used. + /// Path having a directory separator as last character. + public static string EnsureTerminatingSeparator(string strPath, bool bUrl) + { + Debug.Assert(strPath != null); if(strPath == null) throw new ArgumentNullException("strPath"); + + int nLength = strPath.Length; + if(nLength <= 0) return string.Empty; + + char chLast = strPath[nLength - 1]; + + for(int i = 0; i < m_vDirSeps.Length; ++i) + { + if(chLast == m_vDirSeps[i]) return strPath; + } + + if(bUrl) return (strPath + '/'); + return (strPath + UrlUtil.LocalDirSepChar); + } + + /* /// + /// File access mode enumeration. Used by the FileAccessible + /// method. + /// + public enum FileAccessMode + { + /// + /// Opening a file in read mode. The specified file must exist. + /// + Read = 0, + + /// + /// Opening a file in create mode. If the file exists already, it + /// will be overwritten. If it doesn't exist, it will be created. + /// The return value is true, if data can be written to the + /// file. + /// + Create + } */ + + /* /// + /// Test if a specified path is accessible, either in read or write mode. + /// + /// Path to test. + /// Requested file access mode. + /// Returns true if the specified path is accessible in + /// the requested mode, otherwise the return value is false. + public static bool FileAccessible(string strFilePath, FileAccessMode fMode) + { + Debug.Assert(strFilePath != null); + if(strFilePath == null) throw new ArgumentNullException("strFilePath"); + + if(fMode == FileAccessMode.Read) + { + FileStream fs; + + try { fs = File.OpenRead(strFilePath); } + catch(Exception) { return false; } + if(fs == null) return false; + + fs.Close(); + return true; + } + else if(fMode == FileAccessMode.Create) + { + FileStream fs; + + try { fs = File.Create(strFilePath); } + catch(Exception) { return false; } + if(fs == null) return false; + + fs.Close(); + return true; + } + + return false; + } */ + + public static string GetQuotedAppPath(string strPath) + { + if(strPath == null) { Debug.Assert(false); return string.Empty; } + + // int nFirst = strPath.IndexOf('\"'); + // int nSecond = strPath.IndexOf('\"', nFirst + 1); + // if((nFirst >= 0) && (nSecond >= 0)) + // return strPath.Substring(nFirst + 1, nSecond - nFirst - 1); + // return strPath; + + string str = strPath.Trim(); + if(str.Length <= 1) return str; + if(str[0] != '\"') return str; + + int iSecond = str.IndexOf('\"', 1); + if(iSecond <= 0) return str; + + return str.Substring(1, iSecond - 1); + } + + public static string FileUrlToPath(string strUrl) + { + Debug.Assert(strUrl != null); + if(strUrl == null) throw new ArgumentNullException("strUrl"); + + string str = strUrl; + if(str.StartsWith(@"file:///", StrUtil.CaseIgnoreCmp)) + str = str.Substring(8, str.Length - 8); + + str = str.Replace('/', UrlUtil.LocalDirSepChar); + + return str; + } + + public static bool UnhideFile(string strFile) + { +#if (ModernKeePassLib || KeePassLibSD || KeePassRT) + return false; +#else + if(strFile == null) throw new ArgumentNullException("strFile"); + + try + { + FileAttributes fa = File.GetAttributes(strFile); + if((long)(fa & FileAttributes.Hidden) == 0) return false; + + return HideFile(strFile, false); + } + catch(Exception) { } + + return false; +#endif + } + + public static bool HideFile(string strFile, bool bHide) + { +#if (ModernKeePassLib || KeePassLibSD || KeePassRT) + return false; +#else + if(strFile == null) throw new ArgumentNullException("strFile"); + + try + { + FileAttributes fa = File.GetAttributes(strFile); + + if(bHide) fa = ((fa & ~FileAttributes.Normal) | FileAttributes.Hidden); + else // Unhide + { + fa &= ~FileAttributes.Hidden; + if((long)fa == 0) fa = FileAttributes.Normal; + } + + File.SetAttributes(strFile, fa); + return true; + } + catch(Exception) { } + + return false; +#endif + } + + public static string MakeRelativePath(string strBaseFile, string strTargetFile) + { + if(strBaseFile == null) throw new ArgumentNullException("strBasePath"); + if(strTargetFile == null) throw new ArgumentNullException("strTargetPath"); + if(strBaseFile.Length == 0) return strTargetFile; + if(strTargetFile.Length == 0) return string.Empty; + + // Test whether on different Windows drives + if((strBaseFile.Length >= 3) && (strTargetFile.Length >= 3)) + { + if((strBaseFile[1] == ':') && (strTargetFile[1] == ':') && + (strBaseFile[2] == '\\') && (strTargetFile[2] == '\\') && + (strBaseFile[0] != strTargetFile[0])) + return strTargetFile; + } + +#if (!KeePassLibSD && !KeePassUAP && !ModernKeePassLib) + if(NativeLib.IsUnix()) + { +#endif + bool bBaseUnc = IsUncPath(strBaseFile); + bool bTargetUnc = IsUncPath(strTargetFile); + if((!bBaseUnc && bTargetUnc) || (bBaseUnc && !bTargetUnc)) + return strTargetFile; + + string strBase = GetShortestAbsolutePath(strBaseFile); + string strTarget = GetShortestAbsolutePath(strTargetFile); + string[] vBase = strBase.Split(m_vDirSeps); + string[] vTarget = strTarget.Split(m_vDirSeps); + + int i = 0; + while((i < (vBase.Length - 1)) && (i < (vTarget.Length - 1)) && + (vBase[i] == vTarget[i])) { ++i; } + + StringBuilder sbRel = new StringBuilder(); + for(int j = i; j < (vBase.Length - 1); ++j) + { + if(sbRel.Length > 0) sbRel.Append(UrlUtil.LocalDirSepChar); + sbRel.Append(".."); + } + for(int k = i; k < vTarget.Length; ++k) + { + if(sbRel.Length > 0) sbRel.Append(UrlUtil.LocalDirSepChar); + sbRel.Append(vTarget[k]); + } + + return sbRel.ToString(); +#if (!KeePassLibSD && !KeePassUAP && !ModernKeePassLib) + } + + try // Windows + { + const int nMaxPath = NativeMethods.MAX_PATH * 2; + StringBuilder sb = new StringBuilder(nMaxPath + 2); + if(!NativeMethods.PathRelativePathTo(sb, strBaseFile, 0, + strTargetFile, 0)) + return strTargetFile; + + string str = sb.ToString(); + while(str.StartsWith(".\\")) str = str.Substring(2, str.Length - 2); + + return str; + } + catch(Exception) { Debug.Assert(false); } + return strTargetFile; +#endif + } + + public static string MakeAbsolutePath(string strBaseFile, string strTargetFile) + { + if(strBaseFile == null) throw new ArgumentNullException("strBasePath"); + if(strTargetFile == null) throw new ArgumentNullException("strTargetPath"); + if(strBaseFile.Length == 0) return strTargetFile; + if(strTargetFile.Length == 0) return string.Empty; + + if(IsAbsolutePath(strTargetFile)) return strTargetFile; + + string strBaseDir = GetFileDirectory(strBaseFile, true, false); + return GetShortestAbsolutePath(strBaseDir + strTargetFile); + } + + public static bool IsAbsolutePath(string strPath) + { + if(strPath == null) throw new ArgumentNullException("strPath"); + if(strPath.Length == 0) return false; + + if(IsUncPath(strPath)) return true; + + try { return Path.IsPathRooted(strPath); } + catch(Exception) { Debug.Assert(false); } + + return true; + } + + public static string GetShortestAbsolutePath(string strPath) + { + if(strPath == null) throw new ArgumentNullException("strPath"); + if(strPath.Length == 0) return string.Empty; + + // Path.GetFullPath is incompatible with UNC paths traversing over + // different server shares (which are created by PathRelativePathTo); + // we need to build the absolute path on our own... + if(IsUncPath(strPath)) + { + char chSep = strPath[0]; + Debug.Assert(Array.IndexOf(m_vDirSeps, chSep) >= 0); + + List l = new List(); +#if !KeePassLibSD + string[] v = strPath.Split(m_vDirSeps, StringSplitOptions.None); +#else + string[] v = strPath.Split(m_vDirSeps); +#endif + Debug.Assert((v.Length >= 3) && (v[0].Length == 0) && + (v[1].Length == 0)); + + foreach(string strPart in v) + { + if(strPart.Equals(".")) continue; + else if(strPart.Equals("..")) + { + if(l.Count > 0) l.RemoveAt(l.Count - 1); + else { Debug.Assert(false); } + } + else l.Add(strPart); // Do not ignore zero length parts + } + + StringBuilder sb = new StringBuilder(); + for(int i = 0; i < l.Count; ++i) + { + // Don't test length of sb, might be 0 due to initial UNC seps + if(i > 0) sb.Append(chSep); + + sb.Append(l[i]); + } + + return sb.ToString(); + } + + string str; + try + { +#if ModernKeePassLib + var dirT = StorageFolder.GetFolderFromPathAsync( + strPath).GetResults(); + str = dirT.Path; +#else + str = Path.GetFullPath(strPath); +#endif + } + catch(Exception) { Debug.Assert(false); return strPath; } + + Debug.Assert(str.IndexOf("\\..\\") < 0); + foreach(char ch in m_vDirSeps) + { + string strSep = new string(ch, 1); + str = str.Replace(strSep + "." + strSep, strSep); + } + + return str; + } + + public static int GetUrlLength(string strText, int nOffset) + { + if(strText == null) throw new ArgumentNullException("strText"); + if(nOffset > strText.Length) throw new ArgumentException(); // Not >= (0 len) + + int iPosition = nOffset, nLength = 0, nStrLen = strText.Length; + + while(iPosition < nStrLen) + { + char ch = strText[iPosition]; + ++iPosition; + + if((ch == ' ') || (ch == '\t') || (ch == '\r') || (ch == '\n')) + break; + + ++nLength; + } + + return nLength; + } + + public static string RemoveScheme(string strUrl) + { + if(string.IsNullOrEmpty(strUrl)) return string.Empty; + + int nNetScheme = strUrl.IndexOf(@"://", StrUtil.CaseIgnoreCmp); + int nShScheme = strUrl.IndexOf(@":/", StrUtil.CaseIgnoreCmp); + int nSmpScheme = strUrl.IndexOf(@":", StrUtil.CaseIgnoreCmp); + + if((nNetScheme < 0) && (nShScheme < 0) && (nSmpScheme < 0)) + return strUrl; // No scheme + + int nMin = Math.Min(Math.Min((nNetScheme >= 0) ? nNetScheme : int.MaxValue, + (nShScheme >= 0) ? nShScheme : int.MaxValue), + (nSmpScheme >= 0) ? nSmpScheme : int.MaxValue); + + if(nMin == nNetScheme) return strUrl.Substring(nMin + 3); + if(nMin == nShScheme) return strUrl.Substring(nMin + 2); + return strUrl.Substring(nMin + 1); + } + + public static string ConvertSeparators(string strPath) + { + return ConvertSeparators(strPath, UrlUtil.LocalDirSepChar); + } + + public static string ConvertSeparators(string strPath, char chSeparator) + { + if(string.IsNullOrEmpty(strPath)) return string.Empty; + + strPath = strPath.Replace('/', chSeparator); + strPath = strPath.Replace('\\', chSeparator); + + return strPath; + } + + public static bool IsUncPath(string strPath) + { + if(strPath == null) throw new ArgumentNullException("strPath"); + + return (strPath.StartsWith("\\\\") || strPath.StartsWith("//")); + } + + public static string FilterFileName(string strName) + { + if(strName == null) { Debug.Assert(false); return string.Empty; } + + string str = strName; + + str = str.Replace('/', '-'); + str = str.Replace('\\', '-'); + str = str.Replace(":", string.Empty); + str = str.Replace("*", string.Empty); + str = str.Replace("?", string.Empty); + str = str.Replace("\"", string.Empty); + str = str.Replace(@"'", string.Empty); + str = str.Replace('<', '('); + str = str.Replace('>', ')'); + str = str.Replace('|', '-'); + + return str; + } + + /// + /// Get the host component of an URL. + /// This method is faster and more fault-tolerant than creating + /// an Uri object and querying its Host + /// property. + /// + /// + /// For the input s://u:p@d.tld:p/p?q#f the return + /// value is d.tld. + /// + public static string GetHost(string strUrl) + { + if(strUrl == null) { Debug.Assert(false); return string.Empty; } + + StringBuilder sb = new StringBuilder(); + bool bInExtHost = false; + for(int i = 0; i < strUrl.Length; ++i) + { + char ch = strUrl[i]; + if(bInExtHost) + { + if(ch == '/') + { + if(sb.Length == 0) { } // Ignore leading '/'s + else break; + } + else sb.Append(ch); + } + else // !bInExtHost + { + if(ch == ':') bInExtHost = true; + } + } + + string str = sb.ToString(); + if(str.Length == 0) str = strUrl; + + // Remove the login part + int nLoginLen = str.IndexOf('@'); + if(nLoginLen >= 0) str = str.Substring(nLoginLen + 1); + + // Remove the port + int iPort = str.LastIndexOf(':'); + if(iPort >= 0) str = str.Substring(0, iPort); + + return str; + } + + public static bool AssemblyEquals(string strExt, string strShort) + { + if((strExt == null) || (strShort == null)) { Debug.Assert(false); return false; } + + if(strExt.Equals(strShort, StrUtil.CaseIgnoreCmp) || + strExt.StartsWith(strShort + ",", StrUtil.CaseIgnoreCmp)) + return true; + + if(!strShort.EndsWith(".dll", StrUtil.CaseIgnoreCmp)) + { + if(strExt.Equals(strShort + ".dll", StrUtil.CaseIgnoreCmp) || + strExt.StartsWith(strShort + ".dll,", StrUtil.CaseIgnoreCmp)) + return true; + } + + if(!strShort.EndsWith(".exe", StrUtil.CaseIgnoreCmp)) + { + if(strExt.Equals(strShort + ".exe", StrUtil.CaseIgnoreCmp) || + strExt.StartsWith(strShort + ".exe,", StrUtil.CaseIgnoreCmp)) + return true; + } + + return false; + } + + public static string GetTempPath() + { + string strDir; + if(NativeLib.IsUnix()) + strDir = NativeMethods.GetUserRuntimeDir(); +#if KeePassUAP || ModernKeePassLib + else strDir = Windows.Storage.ApplicationData.Current.TemporaryFolder.Path; +#else + else strDir = Path.GetTempPath(); + + try + { + if(!Directory.Exists(strDir)) Directory.CreateDirectory(strDir); + } + catch(Exception) { Debug.Assert(false); } + +#endif + return strDir; + } + +#if !ModernKeePassLib && !KeePassLibSD + // Structurally mostly equivalent to UrlUtil.GetFileInfos + public static List GetFilePaths(string strDir, string strPattern, + SearchOption opt) + { + List l = new List(); + if(strDir == null) { Debug.Assert(false); return l; } + if(strPattern == null) { Debug.Assert(false); return l; } + + string[] v = Directory.GetFiles(strDir, strPattern, opt); + if(v == null) { Debug.Assert(false); return l; } + + // Only accept files with the correct extension; GetFiles may + // return additional files, see GetFiles documentation + string strExt = GetExtension(strPattern); + if(!string.IsNullOrEmpty(strExt) && (strExt.IndexOf('*') < 0) && + (strExt.IndexOf('?') < 0)) + { + strExt = "." + strExt; + + foreach(string strPathRaw in v) + { + if(strPathRaw == null) { Debug.Assert(false); continue; } + string strPath = strPathRaw.Trim(m_vPathTrimCharsWs); + if(strPath.Length == 0) { Debug.Assert(false); continue; } + Debug.Assert(strPath == strPathRaw); + + if(strPath.EndsWith(strExt, StrUtil.CaseIgnoreCmp)) + l.Add(strPathRaw); + } + } + else l.AddRange(v); + + return l; + } + + // Structurally mostly equivalent to UrlUtil.GetFilePaths + public static List GetFileInfos(DirectoryInfo di, string strPattern, + SearchOption opt) + { + List l = new List(); + if(di == null) { Debug.Assert(false); return l; } + if(strPattern == null) { Debug.Assert(false); return l; } + + FileInfo[] v = di.GetFiles(strPattern, opt); + if(v == null) { Debug.Assert(false); return l; } + + // Only accept files with the correct extension; GetFiles may + // return additional files, see GetFiles documentation + string strExt = GetExtension(strPattern); + if(!string.IsNullOrEmpty(strExt) && (strExt.IndexOf('*') < 0) && + (strExt.IndexOf('?') < 0)) + { + strExt = "." + strExt; + + foreach(FileInfo fi in v) + { + if(fi == null) { Debug.Assert(false); continue; } + string strPathRaw = fi.FullName; + if(strPathRaw == null) { Debug.Assert(false); continue; } + string strPath = strPathRaw.Trim(m_vPathTrimCharsWs); + if(strPath.Length == 0) { Debug.Assert(false); continue; } + Debug.Assert(strPath == strPathRaw); + + if(strPath.EndsWith(strExt, StrUtil.CaseIgnoreCmp)) + l.Add(fi); + } + } + else l.AddRange(v); + + return l; + } +#endif + + /// + /// Expand shell variables in a string. + /// [0] is the value of %1, etc. + /// + public static string ExpandShellVariables(string strText, string[] vParams) + { + if(strText == null) { Debug.Assert(false); return string.Empty; } + if(vParams == null) { Debug.Assert(false); vParams = new string[0]; } + + string str = strText; + NumberFormatInfo nfi = NumberFormatInfo.InvariantInfo; + + for(int i = 0; i <= 9; ++i) + { + string strPlh = "%" + i.ToString(nfi); + + string strValue = string.Empty; + if((i > 0) && ((i - 1) < vParams.Length)) + strValue = (vParams[i - 1] ?? string.Empty); + + str = str.Replace(strPlh, strValue); + + if(i == 1) + { + // %L is replaced by the long version of %1; e.g. + // HKEY_CLASSES_ROOT\\IE.AssocFile.URL\\Shell\\Open\\Command + str = str.Replace("%L", strValue); + str = str.Replace("%l", strValue); + } + } + + if(str.IndexOf("%*") >= 0) + { + StringBuilder sb = new StringBuilder(); + foreach(string strValue in vParams) + { + if(!string.IsNullOrEmpty(strValue)) + { + if(sb.Length > 0) sb.Append(' '); + sb.Append(strValue); + } + } + + str = str.Replace("%*", sb.ToString()); + } + + return str; + } + + public static char GetDriveLetter(string strPath) + { + if(strPath == null) throw new ArgumentNullException("strPath"); + + Debug.Assert(default(char) == '\0'); + if(strPath.Length < 3) return '\0'; + if((strPath[1] != ':') || (strPath[2] != '\\')) return '\0'; + + char ch = char.ToUpperInvariant(strPath[0]); + return (((ch >= 'A') && (ch <= 'Z')) ? ch : '\0'); + } + } +} diff --git a/ModernKeePassLib/Utility/XmlUtilEx.cs b/ModernKeePassLib/Utility/XmlUtilEx.cs new file mode 100644 index 0000000..87a25e8 --- /dev/null +++ b/ModernKeePassLib/Utility/XmlUtilEx.cs @@ -0,0 +1,120 @@ +/* + KeePass Password Safe - The Open-Source Password Manager + Copyright (C) 2003-2019 Dominik Reichl + + 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; + +namespace ModernKeePassLib.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 void Serialize(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); + } + } + + public static T Deserialize(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; + } + } +}