├── keepass.version
├── Resources
└── Key.png
├── TestDatabases
├── A
│ ├── A.kdbx
│ └── A.key
├── B
│ ├── B.kdbx
│ └── B.key
└── C
│ └── C.kdbx
├── KeePassSubsetExport.Tests
├── ComparisonData
│ ├── Ae7RealData.cs
│ ├── Ae10RealData.cs
│ ├── Be1RealData.cs
│ ├── Be2RealData.cs
│ ├── Ae9RealData.cs
│ ├── Ae3RealData.cs
│ ├── Ae8RealData.cs
│ ├── AE4RealData.cs
│ ├── Ae5RealData.cs
│ ├── AE1RealData.cs
│ ├── Ae11RealData.cs
│ ├── Ae2RealData.cs
│ └── Ae6RealData.cs
├── packages.config
├── DataContainer
│ ├── TestGroupValues.cs
│ ├── TestEntryValues.cs
│ ├── TestSettings.cs
│ └── TestKdfValues.cs
├── Properties
│ └── AssemblyInfo.cs
├── DbHelper.cs
├── KeePassSubsetExport.Tests.csproj
└── MainTests.cs
├── Properties
├── AssemblyInfo.cs
├── Resources.Designer.cs
└── Resources.resx
├── LICENSE
├── KeePassSubsetExportExt.cs
├── KeePassSubsetExport.sln
├── FieldHelper.cs
├── KeePassSubsetExport.csproj
├── KeyHelper.cs
├── .gitignore
├── Settings.cs
├── README.md
└── Exporter.cs
/keepass.version:
--------------------------------------------------------------------------------
1 | :
2 | KeePassSubsetExport:0.7.0
3 | :
4 |
--------------------------------------------------------------------------------
/Resources/Key.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lukeIam/KeePassSubsetExport/HEAD/Resources/Key.png
--------------------------------------------------------------------------------
/TestDatabases/A/A.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lukeIam/KeePassSubsetExport/HEAD/TestDatabases/A/A.kdbx
--------------------------------------------------------------------------------
/TestDatabases/B/B.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lukeIam/KeePassSubsetExport/HEAD/TestDatabases/B/B.kdbx
--------------------------------------------------------------------------------
/TestDatabases/C/C.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lukeIam/KeePassSubsetExport/HEAD/TestDatabases/C/C.kdbx
--------------------------------------------------------------------------------
/TestDatabases/A/A.key:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1.00
5 |
6 |
7 | O5MEOlMRbVBiKI3y7H47HZx9xmP5FIeXnCmFBKfhGCg=
8 |
9 |
--------------------------------------------------------------------------------
/TestDatabases/B/B.key:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1.00
5 |
6 |
7 | O5MEOlMRbVBiKI3y7H47HZx9xmP5FIeXnCmFBKfhGCg=
8 |
9 |
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/ComparisonData/Ae7RealData.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace KeePassSubsetExport.Tests.ComparisonData
3 | {
4 | internal static class Ae7RealData
5 | {
6 | public static string Db => "A_E7.kdbx";
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/ComparisonData/Ae10RealData.cs:
--------------------------------------------------------------------------------
1 | using KeePassSubsetExport.Tests.DataContainer;
2 |
3 | namespace KeePassSubsetExport.Tests.ComparisonData
4 | {
5 | internal static class Ae10RealData
6 | {
7 | public static string Db => "A_E10.kdbx";
8 |
9 | public static TestKdfValues Kdf => Ae1RealData.Kdf;
10 | public static TestGroupValues Data => Ae1RealData.Data;
11 | }
12 | }
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/DataContainer/TestGroupValues.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace KeePassSubsetExport.Tests.DataContainer
4 | {
5 | internal class TestGroupValues
6 | {
7 | public string Uuid { get; set; }
8 | public string Name { get; set; }
9 | public List Entries { get; set; }
10 | public List SubGroups { get; set; }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/DataContainer/TestEntryValues.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace KeePassSubsetExport.Tests.DataContainer
3 | {
4 | internal class TestEntryValues
5 | {
6 | public string Uuid { get; set; }
7 | public string Title { get; set; }
8 | public string UserName { get; set; }
9 | public string Password { get; set; }
10 | public string Url { get; set; }
11 | public string Note { get; set; } = "";
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/ComparisonData/Be1RealData.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using KeePassSubsetExport.Tests.DataContainer;
3 |
4 | namespace KeePassSubsetExport.Tests.ComparisonData
5 | {
6 | internal static class Be1RealData
7 | {
8 | public static string Db => "B_E1.kdbx";
9 |
10 | public static TestKdfValues Kdf => new TestKdfValues()
11 | {
12 | KdfUuid = TestKdfValues.UuidArgon2,
13 | Argon2Iterations = 2,
14 | Argon2Memory = 1024*1024,
15 | Argon2Parallelism = 2
16 | };
17 |
18 | public static TestGroupValues Data => Ae2RealData.Data;
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/ComparisonData/Be2RealData.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using KeePassSubsetExport.Tests.DataContainer;
3 |
4 | namespace KeePassSubsetExport.Tests.ComparisonData
5 | {
6 | internal static class Be2RealData
7 | {
8 | public static string Db => "B_E2.kdbx";
9 |
10 | public static TestKdfValues Kdf => new TestKdfValues()
11 | {
12 | KdfUuid = TestKdfValues.UuidArgon2,
13 | Argon2Iterations = 3,
14 | Argon2Memory = 1024*1024*3,
15 | Argon2Parallelism = 3
16 | };
17 |
18 | public static TestGroupValues Data => Ae2RealData.Data;
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | [assembly: AssemblyTitle("KeePassSubsetExport.Tests")]
6 | [assembly: AssemblyDescription("")]
7 | [assembly: AssemblyConfiguration("")]
8 | [assembly: AssemblyCompany("")]
9 | [assembly: AssemblyProduct("KeePassSubsetExport.Tests")]
10 | [assembly: AssemblyCopyright("Copyright © 2018")]
11 | [assembly: AssemblyTrademark("")]
12 | [assembly: AssemblyCulture("")]
13 |
14 | [assembly: ComVisible(false)]
15 |
16 | [assembly: Guid("7ff2fce6-3f03-4bfd-8e02-b55ee50266e3")]
17 |
18 | // [assembly: AssemblyVersion("1.0.*")]
19 | [assembly: AssemblyVersion("1.0.0.0")]
20 | [assembly: AssemblyFileVersion("1.0.0.0")]
21 |
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/DataContainer/TestSettings.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace KeePassSubsetExport.Tests.DataContainer
3 | {
4 | internal class TestSettings
5 | {
6 | internal string RootPath { get; set; }
7 | internal string DbAFilesPath { get; set; }
8 | internal string DbBFilesPath { get; set; }
9 | internal string DbCFilesPath { get; set; }
10 |
11 | internal string DbAPath { get; set; }
12 | internal string DbBPath { get; set; }
13 | internal string DbCPath { get; set; }
14 | internal string DbMainPw { get; set; }
15 |
16 |
17 | internal string KeyTestAPath { get; set; }
18 | internal string KeyTestBPath { get; set; }
19 | internal string KeyTestCPath { get; set; }
20 |
21 | internal string DbTestPw { get; set; }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | [assembly: AssemblyTitle("KeePassSubsetExport")]
6 | [assembly: AssemblyDescription("KeePassSubsetExport is a KeePass2 plugin which automatically exports a subset of entries (tag/group based) to new databases (with different keys).")]
7 | [assembly: AssemblyConfiguration("")]
8 | [assembly: AssemblyCompany("lukeIam")]
9 | [assembly: AssemblyProduct("KeePass Plugin")]
10 | [assembly: AssemblyCopyright("")]
11 | [assembly: AssemblyTrademark("")]
12 | [assembly: AssemblyCulture("")]
13 |
14 | [assembly: ComVisible(false)]
15 |
16 |
17 | [assembly: Guid("3463A091-5683-487E-843F-47086294C5C1")]
18 |
19 | [assembly: AssemblyVersion("0.7.0.0")]
20 | [assembly: AssemblyFileVersion("0.7.0.0")]
21 |
22 | [assembly: InternalsVisibleTo("KeePassSubsetExport.Tests")]
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/DataContainer/TestKdfValues.cs:
--------------------------------------------------------------------------------
1 | using KeePassLib;
2 |
3 | namespace KeePassSubsetExport.Tests.DataContainer
4 | {
5 | public class TestKdfValues
6 | {
7 | public PwUuid KdfUuid { get; set; }
8 | public uint AesKeyTransformationRounds { get; set; }
9 | public uint Argon2Iterations { get; set; }
10 | public uint Argon2Memory { get; set; }
11 | public uint Argon2Parallelism { get; set; }
12 |
13 | public static readonly PwUuid UuidAes = new PwUuid(new byte[] {
14 | 0xC9, 0xD9, 0xF3, 0x9A, 0x62, 0x8A, 0x44, 0x60,
15 | 0xBF, 0x74, 0x0D, 0x08, 0xC1, 0x8A, 0x4F, 0xEA });
16 |
17 | public static readonly PwUuid UuidArgon2 = new PwUuid(new byte[] {
18 | 0xEF, 0x63, 0x6D, 0xDF, 0x8C, 0x29, 0x44, 0x4B,
19 | 0x91, 0xF7, 0xA9, 0xA4, 0x03, 0xE3, 0x0A, 0x0C });
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/KeePassSubsetExportExt.cs:
--------------------------------------------------------------------------------
1 | using System.Drawing;
2 | using KeePass.Forms;
3 | using KeePass.Plugins;
4 |
5 | namespace KeePassSubsetExport
6 | {
7 | public class KeePassSubsetExportExt : Plugin
8 | {
9 | private IPluginHost _host = null;
10 |
11 | public override Image SmallIcon
12 | {
13 | get { return Properties.Resources.Key; }
14 | }
15 |
16 | public override string UpdateUrl
17 | {
18 | get { return "https://github.com/lukeIam/KeePassSubsetExport/raw/master/keepass.version"; }
19 | }
20 |
21 | public override bool Initialize(IPluginHost host)
22 | {
23 | _host = host;
24 |
25 | _host.MainWindow.FileSaved += StartExport;
26 |
27 | return true;
28 | }
29 |
30 | private void StartExport(object sender, FileSavedEventArgs args)
31 | {
32 | Exporter.Export(args.Database);
33 | }
34 |
35 | public override void Terminate()
36 | {
37 | if (_host != null)
38 | {
39 | _host.MainWindow.FileSaved -= StartExport;
40 | }
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/DbHelper.cs:
--------------------------------------------------------------------------------
1 | using KeePassLib;
2 | using KeePassLib.Interfaces;
3 | using KeePassLib.Keys;
4 | using KeePassLib.Serialization;
5 |
6 | namespace KeePassSubsetExport.Tests
7 | {
8 | internal class DbHelper
9 | {
10 | ///
11 | /// Opens a database.
12 | ///
13 | /// Path to the datebase.
14 | /// Password of the database (optional).
15 | /// Keyfile for the database (optional).
16 | ///
17 | internal static PwDatabase OpenDatabase(string path, string password = null, string keyPath = null)
18 | {
19 | IOConnectionInfo ioConnInfo = new IOConnectionInfo { Path = path };
20 | CompositeKey compositeKey = new CompositeKey();
21 |
22 | if (password != null)
23 | {
24 | KeyHelper.AddPasswordToKey(password, compositeKey);
25 | }
26 |
27 | if (keyPath != null)
28 | {
29 | KeyHelper.AddKeyfileToKey(keyPath, compositeKey, ioConnInfo);
30 | }
31 |
32 | PwDatabase db = new PwDatabase();
33 | db.Open(ioConnInfo, compositeKey, new NullStatusLogger());
34 | return db;
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/KeePassSubsetExport.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.27703.2047
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KeePassSubsetExport", "KeePassSubsetExport.csproj", "{2E861274-C7D4-4774-B9A0-A746E0774A88}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KeePassSubsetExport.Tests", "KeePassSubsetExport.Tests\KeePassSubsetExport.Tests.csproj", "{7FF2FCE6-3F03-4BFD-8E02-B55EE50266E3}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {2E861274-C7D4-4774-B9A0-A746E0774A88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {2E861274-C7D4-4774-B9A0-A746E0774A88}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {2E861274-C7D4-4774-B9A0-A746E0774A88}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {2E861274-C7D4-4774-B9A0-A746E0774A88}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {7FF2FCE6-3F03-4BFD-8E02-B55EE50266E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {7FF2FCE6-3F03-4BFD-8E02-B55EE50266E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {7FF2FCE6-3F03-4BFD-8E02-B55EE50266E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {7FF2FCE6-3F03-4BFD-8E02-B55EE50266E3}.Release|Any CPU.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ExtensibilityGlobals) = postSolution
29 | SolutionGuid = {967A2DD2-03C4-4B1A-9BE1-40CE4DA3CDCD}
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/FieldHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using KeePass.Util.Spr;
3 | using KeePassLib;
4 | using KeePassLib.Security;
5 | using KeePassLib.Utility;
6 |
7 | namespace KeePassSubsetExport
8 | {
9 | public static class FieldHelper
10 | {
11 | private static readonly byte[] RefStringByteArray = new byte[]
12 | {
13 | 123, 82, 69, 70, 58
14 | };
15 |
16 | public static ProtectedString GetFieldWRef(PwEntry entry, PwDatabase sourceDb, string fieldName)
17 | {
18 | ProtectedString orgValue = entry.Strings.GetSafe(fieldName);
19 |
20 | byte[] orgValueByteArray = orgValue.ReadUtf8();
21 |
22 | // Check if the protected string begins with the ref marker
23 | bool isRef = orgValueByteArray.Take(5).SequenceEqual(RefStringByteArray);
24 |
25 | MemUtil.ZeroByteArray(orgValueByteArray);
26 |
27 | if (!isRef)
28 | {
29 | // The protected string is not a ref -> return the protected string directly
30 | return orgValue;
31 | }
32 |
33 |
34 | SprContext ctx = new SprContext(entry, sourceDb,
35 | SprCompileFlags.All, false, false);
36 |
37 | // the protected string is a reference -> decode it and look it up
38 | return new ProtectedString(true, SprEngine.Compile(
39 | orgValue.ReadString(), ctx));
40 | }
41 |
42 | public static string GetFieldWRefUnprotected(PwEntry entry, PwDatabase sourceDb, string fieldName)
43 | {
44 | string orgValue = entry.Strings.ReadSafe(fieldName);
45 |
46 | // Check if the string begins with the ref marker or contains a %
47 | if (!orgValue.StartsWith("{REF") && !orgValue.Contains("%"))
48 | {
49 | // The string is not a ref -> return the string directly
50 | return orgValue;
51 | }
52 |
53 | SprContext ctx = new SprContext(entry, sourceDb,
54 | SprCompileFlags.All, false, false);
55 |
56 | // the string is a reference -> decode it and look it up
57 | return SprEngine.Compile(orgValue, ctx);
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/ComparisonData/Ae9RealData.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using KeePassSubsetExport.Tests.DataContainer;
3 |
4 | namespace KeePassSubsetExport.Tests.ComparisonData
5 | {
6 | internal static class Ae9RealData
7 | {
8 | public static string Db => "A_E9.kdbx";
9 |
10 | public static TestKdfValues Kdf => new TestKdfValues()
11 | {
12 | KdfUuid = TestKdfValues.UuidAes,
13 | AesKeyTransformationRounds = 60000
14 | };
15 |
16 | public static TestGroupValues Data =>
17 | new TestGroupValues()
18 | {
19 | Uuid = "713CC7FA6D348E44AB570CD7CAB45DE0",
20 | Name = "A",
21 | SubGroups = new List()
22 | {
23 | new TestGroupValues()
24 | {
25 | Uuid = "875668C19091E94CAA50D846132EFAD8",
26 | Name = "A G4",
27 | Entries = new List()
28 | {
29 | new TestEntryValues()
30 | {
31 | Uuid = "E2A3ED500A1E884886F4146E755D2129",
32 | Title = "A_G3_E1",
33 | UserName = "user",
34 | Password = "dummy",
35 | Url = "",
36 | Note = ""
37 | }
38 | }
39 | },
40 | new TestGroupValues()
41 | {
42 | Uuid = "DDABC265A89D284792200CB7EE23F59E",
43 | Name = "A G5",
44 | Entries = new List()
45 | {
46 | new TestEntryValues()
47 | {
48 | Uuid = "75D3341FFA2FE04284F21633065B3690",
49 | Title = "A_G5_E1",
50 | UserName = "user",
51 | Password = "dummy",
52 | Url = ""
53 | }
54 | }
55 | }
56 | }
57 | };
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/ComparisonData/Ae3RealData.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using KeePassSubsetExport.Tests.DataContainer;
3 |
4 | namespace KeePassSubsetExport.Tests.ComparisonData
5 | {
6 | internal static class Ae3RealData
7 | {
8 | public static string Db => "A_E3.kdbx";
9 |
10 | public static TestKdfValues Kdf => new TestKdfValues()
11 | {
12 | KdfUuid = TestKdfValues.UuidAes,
13 | AesKeyTransformationRounds = 60000
14 | };
15 |
16 | public static TestGroupValues Data =>
17 | new TestGroupValues()
18 | {
19 | Uuid = "713CC7FA6D348E44AB570CD7CAB45DE0",
20 | Name = "A",
21 |
22 | Entries = new List()
23 | {
24 | new TestEntryValues()
25 | {
26 | Uuid = "743328509472674E821FB1FB778BC2BD",
27 | Title = "A_G1_E2",
28 | UserName = "user",
29 | Password = "dummy",
30 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
31 | },
32 | new TestEntryValues()
33 | {
34 | Uuid = "A3226A9877AD924F87182B66530B1BDF",
35 | Title = "A_G1_E3",
36 | UserName = "user",
37 | Password = "dummy",
38 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
39 | },
40 | new TestEntryValues()
41 | {
42 | Uuid = "7A9C95AC44FE2041B663C46549C21369",
43 | Title = "A_G2_E2",
44 | UserName = "user",
45 | Password = "dummy",
46 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
47 | },
48 | new TestEntryValues()
49 | {
50 | Uuid = "DEB4B7620CC7B24096A87F45F8CEA919",
51 | Title = "A_G2_E3",
52 | UserName = "user",
53 | Password = "dummy",
54 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
55 | }
56 | }
57 | };
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/ComparisonData/Ae8RealData.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using KeePassSubsetExport.Tests.DataContainer;
3 |
4 | namespace KeePassSubsetExport.Tests.ComparisonData
5 | {
6 | internal static class Ae8RealData
7 | {
8 | public static string Db => "A_E8.kdbx";
9 |
10 | public static TestKdfValues Kdf => new TestKdfValues()
11 | {
12 | KdfUuid = TestKdfValues.UuidAes,
13 | AesKeyTransformationRounds = 60000
14 | };
15 |
16 | public static TestGroupValues Data =>
17 | new TestGroupValues()
18 | {
19 | Uuid = "713CC7FA6D348E44AB570CD7CAB45DE0",
20 | Name = "A",
21 | SubGroups = new List()
22 | {
23 | new TestGroupValues()
24 | {
25 | Uuid = "875668C19091E94CAA50D846132EFAD8",
26 | Name = "A G4",
27 | Entries = new List()
28 | {
29 | new TestEntryValues()
30 | {
31 | Uuid = "E2A3ED500A1E884886F4146E755D2129",
32 | Title = "A_G3_E1",
33 | UserName = "user",
34 | Password = "dummy",
35 | Url = "https://github.com/lukeIam/KeePassSubsetExport",
36 | Note = "Note"
37 | }
38 | }
39 | },
40 | new TestGroupValues()
41 | {
42 | Uuid = "DDABC265A89D284792200CB7EE23F59E",
43 | Name = "A G5",
44 | Entries = new List()
45 | {
46 | new TestEntryValues()
47 | {
48 | Uuid = "75D3341FFA2FE04284F21633065B3690",
49 | Title = "A_G5_E1",
50 | UserName = "user",
51 | Password = "dummy",
52 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
53 | }
54 | }
55 | }
56 |
57 | }
58 | };
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/ComparisonData/AE4RealData.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using KeePassSubsetExport.Tests.DataContainer;
3 |
4 | namespace KeePassSubsetExport.Tests.ComparisonData
5 | {
6 | internal static class Ae4RealData
7 | {
8 | public static string Db => "A_E4.kdbx";
9 |
10 | public static TestKdfValues Kdf => new TestKdfValues()
11 | {
12 | KdfUuid = TestKdfValues.UuidAes,
13 | AesKeyTransformationRounds = 60000
14 | };
15 |
16 | public static TestGroupValues Data =>
17 | new TestGroupValues()
18 | {
19 | Uuid = "713CC7FA6D348E44AB570CD7CAB45DE0",
20 | Name = "A",
21 | SubGroups = new List()
22 | {
23 | new TestGroupValues()
24 | {
25 | Uuid = "3CABE6BBA6D7A048A2FBC37CA88C0819",
26 | Name = "A_G1",
27 | Entries = new List()
28 | {
29 | new TestEntryValues()
30 | {
31 | Uuid = "017718804448F249ACFEF68C87D075C6",
32 | Title = "A_G1_E1",
33 | UserName = "user",
34 | Password = "dummy",
35 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
36 | }
37 | },
38 | SubGroups = new List()
39 | {
40 | new TestGroupValues()
41 | {
42 | Uuid = "C1FA77802458964681E18C642978A3C3",
43 | Name = "A_G2",
44 | Entries = new List()
45 | {
46 | new TestEntryValues()
47 | {
48 | Uuid = "DEB4B7620CC7B24096A87F45F8CEA919",
49 | Title = "A_G2_E3",
50 | UserName = "user",
51 | Password = "dummy",
52 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
53 | }
54 | }
55 | }
56 | }
57 | }
58 | }
59 | };
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/ComparisonData/Ae5RealData.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using KeePassSubsetExport.Tests.DataContainer;
3 |
4 | namespace KeePassSubsetExport.Tests.ComparisonData
5 | {
6 | internal static class Ae5RealData
7 | {
8 | public static string Db => "A_E5.kdbx";
9 |
10 | public static TestKdfValues Kdf => new TestKdfValues()
11 | {
12 | KdfUuid = TestKdfValues.UuidAes,
13 | AesKeyTransformationRounds = 60000
14 | };
15 |
16 | public static TestGroupValues Data =>
17 | new TestGroupValues()
18 | {
19 | Uuid = "713CC7FA6D348E44AB570CD7CAB45DE0",
20 | Name = "A",
21 | SubGroups = new List()
22 | {
23 | new TestGroupValues()
24 | {
25 | Uuid = "3CABE6BBA6D7A048A2FBC37CA88C0819",
26 | Name = "A_G1",
27 | Entries = new List()
28 | {
29 | new TestEntryValues()
30 | {
31 | Uuid = "017718804448F249ACFEF68C87D075C6",
32 | Title = "A_G1_E1",
33 | UserName = "user",
34 | Password = "dummy",
35 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
36 | }
37 | },
38 | SubGroups = new List()
39 | {
40 | new TestGroupValues()
41 | {
42 | Uuid = "C1FA77802458964681E18C642978A3C3",
43 | Name = "A_G2",
44 | Entries = new List()
45 | {
46 | new TestEntryValues()
47 | {
48 | Uuid = "DEB4B7620CC7B24096A87F45F8CEA919",
49 | Title = "A_G2_E3",
50 | UserName = "user",
51 | Password = "dummy",
52 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
53 | }
54 | }
55 | }
56 | }
57 | }
58 | }
59 | };
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace KeePassSubsetExport.Properties {
12 | using System;
13 |
14 |
15 | ///
16 | /// A strongly-typed resource class, for looking up localized strings, etc.
17 | ///
18 | // This class was auto-generated by the StronglyTypedResourceBuilder
19 | // class via a tool like ResGen or Visual Studio.
20 | // To add or remove a member, edit your .ResX file then rerun ResGen
21 | // with the /str option, or rebuild your VS project.
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | internal class Resources {
26 |
27 | private static global::System.Resources.ResourceManager resourceMan;
28 |
29 | private static global::System.Globalization.CultureInfo resourceCulture;
30 |
31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
32 | internal Resources() {
33 | }
34 |
35 | ///
36 | /// Returns the cached ResourceManager instance used by this class.
37 | ///
38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
39 | internal static global::System.Resources.ResourceManager ResourceManager {
40 | get {
41 | if (object.ReferenceEquals(resourceMan, null)) {
42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KeePassSubsetExport.Properties.Resources", typeof(Resources).Assembly);
43 | resourceMan = temp;
44 | }
45 | return resourceMan;
46 | }
47 | }
48 |
49 | ///
50 | /// Overrides the current thread's CurrentUICulture property for all
51 | /// resource lookups using this strongly typed resource class.
52 | ///
53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
54 | internal static global::System.Globalization.CultureInfo Culture {
55 | get {
56 | return resourceCulture;
57 | }
58 | set {
59 | resourceCulture = value;
60 | }
61 | }
62 |
63 | ///
64 | /// Looks up a localized resource of type System.Drawing.Bitmap.
65 | ///
66 | internal static System.Drawing.Bitmap Key {
67 | get {
68 | object obj = ResourceManager.GetObject("Key", resourceCulture);
69 | return ((System.Drawing.Bitmap)(obj));
70 | }
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/KeePassSubsetExport.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {2E861274-C7D4-4774-B9A0-A746E0774A88}
8 | Library
9 | Properties
10 | KeePassSubsetExport
11 | KeePassSubsetExport
12 | v4.7.2
13 | 512
14 |
15 |
16 |
17 | true
18 | full
19 | false
20 | bin\Debug\
21 | DEBUG;TRACE
22 | prompt
23 | 4
24 | false
25 |
26 |
27 | pdbonly
28 | true
29 | bin\Release\
30 | TRACE
31 | prompt
32 | 4
33 | false
34 |
35 |
36 |
37 | $(KeePassPath)KeePass.exe
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | True
53 | True
54 | Resources.resx
55 |
56 |
57 |
58 |
59 |
60 | ResXFileCodeGenerator
61 | Resources.Designer.cs
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | copy /Y "$(TargetDir)$(ProjectName).dll" "$(KeePassPath)$(ProjectName).dll"
73 |
74 |
81 |
--------------------------------------------------------------------------------
/KeyHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using KeePass;
4 | using KeePass.Resources;
5 | using KeePassLib.Keys;
6 | using KeePassLib.Serialization;
7 | using KeePassLib.Utility;
8 |
9 | namespace KeePassSubsetExport
10 | {
11 | public static class KeyHelper
12 | {
13 | ///
14 | /// Adds a password to a .
15 | ///
16 | /// The password as byte array to add to the .
17 | /// The to add the password to.
18 | /// true if sucessfull, false otherwise.
19 | public static bool AddPasswordToKey(byte[] passwordByteArray, CompositeKey key)
20 | {
21 | if (passwordByteArray.Length == 0)
22 | {
23 | return false;
24 | }
25 |
26 | key.AddUserKey(new KcpPassword(passwordByteArray));
27 | return true;
28 | }
29 |
30 | ///
31 | /// Adds a password to a .
32 | ///
33 | /// The password to add to the .
34 | /// The to add the password to.
35 | /// true if sucessfull, false otherwise.
36 | public static bool AddPasswordToKey(string password, CompositeKey key)
37 | {
38 | if (password == "")
39 | {
40 | return false;
41 | }
42 |
43 | key.AddUserKey(new KcpPassword(password));
44 | return true;
45 | }
46 |
47 | ///
48 | /// Adds a keyfile to a .
49 | ///
50 | /// The path to the keyfile to add to the .
51 | /// The to add the keyfile to.
52 | /// The object of the database (required for ).
53 | /// true if sucessfull, false otherwise.
54 | public static bool AddKeyfileToKey(string keyFilePath, CompositeKey key, IOConnectionInfo connectionInfo)
55 | {
56 | bool success = false;
57 |
58 | if (!File.Exists(keyFilePath))
59 | {
60 | return false;
61 | }
62 |
63 | bool bIsKeyProv = Program.KeyProviderPool.IsKeyProvider(keyFilePath);
64 |
65 | if (!bIsKeyProv)
66 | {
67 | try
68 | {
69 | key.AddUserKey(new KcpKeyFile(keyFilePath, true));
70 | success = true;
71 | }
72 | catch (InvalidDataException exId)
73 | {
74 | MessageService.ShowWarning(keyFilePath, exId);
75 | }
76 | catch (Exception exKf)
77 | {
78 | MessageService.ShowWarning(keyFilePath, KPRes.KeyFileError, exKf);
79 | }
80 | }
81 | else
82 | {
83 | KeyProviderQueryContext ctxKp = new KeyProviderQueryContext(connectionInfo, true, false);
84 |
85 | KeyProvider prov = Program.KeyProviderPool.Get(keyFilePath);
86 | bool bPerformHash = !prov.DirectKey;
87 | byte[] pbCustomKey = prov.GetKey(ctxKp);
88 |
89 | if ((pbCustomKey != null) && (pbCustomKey.Length > 0))
90 | {
91 | try
92 | {
93 | key.AddUserKey(new KcpCustomKey(keyFilePath, pbCustomKey, bPerformHash));
94 | success = true;
95 | }
96 | catch (Exception exCkp)
97 | {
98 | MessageService.ShowWarning(exCkp);
99 | }
100 |
101 | MemUtil.ZeroByteArray(pbCustomKey);
102 | }
103 | }
104 |
105 | return success;
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/ComparisonData/AE1RealData.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using KeePassSubsetExport.Tests.DataContainer;
3 |
4 | namespace KeePassSubsetExport.Tests.ComparisonData
5 | {
6 | internal static class Ae1RealData
7 | {
8 | public static string Db => "A_E1.kdbx";
9 |
10 | public static TestKdfValues Kdf => new TestKdfValues()
11 | {
12 | KdfUuid = TestKdfValues.UuidAes,
13 | AesKeyTransformationRounds = 60000
14 | };
15 |
16 | public static TestGroupValues Data =>
17 | new TestGroupValues()
18 | {
19 | Uuid = "713CC7FA6D348E44AB570CD7CAB45DE0",
20 | Name = "A",
21 | Entries = new List()
22 | {
23 | new TestEntryValues()
24 | {
25 | Uuid = "50A6AAE7ABF82F4C9FA8533143988C60",
26 | Title = "A_E2",
27 | UserName = "user",
28 | Password = "dummy",
29 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
30 | },
31 | new TestEntryValues()
32 | {
33 | Uuid = "1569F3F77FA4C44F9D8FC9DEEF141629",
34 | Title = "A_E3",
35 | UserName = "user",
36 | Password = "dummy",
37 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
38 | }
39 | },
40 | SubGroups = new List()
41 | {
42 | new TestGroupValues()
43 | {
44 | Uuid = "3CABE6BBA6D7A048A2FBC37CA88C0819",
45 | Name = "A_G1",
46 | Entries = new List()
47 | {
48 | new TestEntryValues()
49 | {
50 | Uuid = "743328509472674E821FB1FB778BC2BD",
51 | Title = "A_G1_E2",
52 | UserName = "user",
53 | Password = "dummy",
54 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
55 | },
56 | new TestEntryValues()
57 | {
58 | Uuid = "A3226A9877AD924F87182B66530B1BDF",
59 | Title = "A_G1_E3",
60 | UserName = "user",
61 | Password = "dummy",
62 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
63 | }
64 | },
65 | SubGroups = new List()
66 | {
67 | new TestGroupValues()
68 | {
69 | Uuid = "C1FA77802458964681E18C642978A3C3",
70 | Name = "A_G2",
71 | Entries = new List()
72 | {
73 | new TestEntryValues()
74 | {
75 | Uuid = "7A9C95AC44FE2041B663C46549C21369",
76 | Title = "A_G2_E2",
77 | UserName = "user",
78 | Password = "dummy",
79 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
80 | },
81 | new TestEntryValues()
82 | {
83 | Uuid = "DEB4B7620CC7B24096A87F45F8CEA919",
84 | Title = "A_G2_E3",
85 | UserName = "user",
86 | Password = "dummy",
87 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
88 | }
89 | }
90 | }
91 | }
92 | }
93 | }
94 | };
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/ComparisonData/Ae11RealData.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using KeePassSubsetExport.Tests.DataContainer;
3 |
4 | namespace KeePassSubsetExport.Tests.ComparisonData
5 | {
6 | internal static class Ae11_RealData
7 | {
8 | public static string Db => "A_E11_1.kdbx";
9 | public static string Db2 => "A_E11_2.kdbx";
10 |
11 | public static TestKdfValues Kdf => new TestKdfValues()
12 | {
13 | KdfUuid = TestKdfValues.UuidAes,
14 | AesKeyTransformationRounds = 60000
15 | };
16 |
17 | public static TestGroupValues Data =>
18 | new TestGroupValues()
19 | {
20 | Uuid = "713CC7FA6D348E44AB570CD7CAB45DE0",
21 | Name = "A",
22 | Entries = new List()
23 | {
24 | new TestEntryValues()
25 | {
26 | Uuid = "50A6AAE7ABF82F4C9FA8533143988C60",
27 | Title = "A_E2",
28 | UserName = "user",
29 | Password = "dummy",
30 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
31 | },
32 | new TestEntryValues()
33 | {
34 | Uuid = "1569F3F77FA4C44F9D8FC9DEEF141629",
35 | Title = "A_E3",
36 | UserName = "user",
37 | Password = "dummy",
38 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
39 | }
40 | },
41 | SubGroups = new List()
42 | {
43 | new TestGroupValues()
44 | {
45 | Uuid = "3CABE6BBA6D7A048A2FBC37CA88C0819",
46 | Name = "A_G1",
47 | Entries = new List()
48 | {
49 | new TestEntryValues()
50 | {
51 | Uuid = "743328509472674E821FB1FB778BC2BD",
52 | Title = "A_G1_E2",
53 | UserName = "user",
54 | Password = "dummy",
55 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
56 | },
57 | new TestEntryValues()
58 | {
59 | Uuid = "A3226A9877AD924F87182B66530B1BDF",
60 | Title = "A_G1_E3",
61 | UserName = "user",
62 | Password = "dummy",
63 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
64 | }
65 | },
66 | SubGroups = new List()
67 | {
68 | new TestGroupValues()
69 | {
70 | Uuid = "C1FA77802458964681E18C642978A3C3",
71 | Name = "A_G2",
72 | Entries = new List()
73 | {
74 | new TestEntryValues()
75 | {
76 | Uuid = "7A9C95AC44FE2041B663C46549C21369",
77 | Title = "A_G2_E2",
78 | UserName = "user",
79 | Password = "dummy",
80 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
81 | },
82 | new TestEntryValues()
83 | {
84 | Uuid = "DEB4B7620CC7B24096A87F45F8CEA919",
85 | Title = "A_G2_E3",
86 | UserName = "user",
87 | Password = "dummy",
88 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
89 | }
90 | }
91 | }
92 | }
93 | }
94 | }
95 | };
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/ComparisonData/Ae2RealData.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using KeePassSubsetExport.Tests.DataContainer;
3 |
4 | namespace KeePassSubsetExport.Tests.ComparisonData
5 | {
6 | internal static class Ae2RealData
7 | {
8 | public static string Db => "A_E2.kdbx";
9 |
10 | public static TestKdfValues Kdf => new TestKdfValues()
11 | {
12 | KdfUuid = TestKdfValues.UuidAes,
13 | AesKeyTransformationRounds = 10000001
14 | };
15 |
16 | public static TestGroupValues Data =>
17 | new TestGroupValues()
18 | {
19 | Uuid = "713CC7FA6D348E44AB570CD7CAB45DE0",
20 | Name = "NewRoot",
21 | SubGroups = new List()
22 | {
23 | new TestGroupValues()
24 | {
25 | Uuid = "3CABE6BBA6D7A048A2FBC37CA88C0819",
26 | Name = "A_G1",
27 | Entries = new List()
28 | {
29 | new TestEntryValues()
30 | {
31 | Uuid = "017718804448F249ACFEF68C87D075C6",
32 | Title = "A_G1_E1",
33 | UserName = "user",
34 | Password = "dummy",
35 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
36 | },
37 | new TestEntryValues()
38 | {
39 | Uuid = "743328509472674E821FB1FB778BC2BD",
40 | Title = "A_G1_E2",
41 | UserName = "user",
42 | Password = "dummy",
43 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
44 | },
45 | new TestEntryValues()
46 | {
47 | Uuid = "A3226A9877AD924F87182B66530B1BDF",
48 | Title = "A_G1_E3",
49 | UserName = "user",
50 | Password = "dummy",
51 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
52 | }
53 | },
54 | SubGroups = new List()
55 | {
56 | new TestGroupValues()
57 | {
58 | Uuid = "C1FA77802458964681E18C642978A3C3",
59 | Name = "A_G2",
60 | Entries = new List()
61 | {
62 | new TestEntryValues()
63 | {
64 | Uuid = "29C4322F054A804D9F37A7A67558C5BF",
65 | Title = "A_G2_E1",
66 | UserName = "user",
67 | Password = "dummy",
68 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
69 | },
70 | new TestEntryValues()
71 | {
72 | Uuid = "7A9C95AC44FE2041B663C46549C21369",
73 | Title = "A_G2_E2",
74 | UserName = "user",
75 | Password = "dummy",
76 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
77 | },
78 | new TestEntryValues()
79 | {
80 | Uuid = "DEB4B7620CC7B24096A87F45F8CEA919",
81 | Title = "A_G2_E3",
82 | UserName = "user",
83 | Password = "dummy",
84 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
85 | }
86 | }
87 | }
88 | }
89 | }
90 | }
91 | };
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/ComparisonData/Ae6RealData.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using KeePassSubsetExport.Tests.DataContainer;
3 |
4 | namespace KeePassSubsetExport.Tests.ComparisonData
5 | {
6 | internal static class Ae6RealData
7 | {
8 | public static string Db => "A_E6.kdbx";
9 |
10 | public static TestKdfValues Kdf => new TestKdfValues()
11 | {
12 | KdfUuid = TestKdfValues.UuidAes,
13 | AesKeyTransformationRounds = 60000
14 | };
15 |
16 | public static TestGroupValues Data =>
17 | new TestGroupValues()
18 | {
19 | Uuid = "713CC7FA6D348E44AB570CD7CAB45DE0",
20 | Name = "A",
21 | SubGroups = new List()
22 | {
23 | new TestGroupValues()
24 | {
25 | Uuid = "3CABE6BBA6D7A048A2FBC37CA88C0819",
26 | Name = "A_G1",
27 | Entries = new List()
28 | {
29 | new TestEntryValues()
30 | {
31 | Uuid = "017718804448F249ACFEF68C87D075C6",
32 | Title = "A_G1_E1",
33 | UserName = "user",
34 | Password = "dummy",
35 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
36 | },
37 | new TestEntryValues()
38 | {
39 | Uuid = "743328509472674E821FB1FB778BC2BD",
40 | Title = "A_G1_E2",
41 | UserName = "user",
42 | Password = "dummy",
43 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
44 | },
45 | new TestEntryValues()
46 | {
47 | Uuid = "A3226A9877AD924F87182B66530B1BDF",
48 | Title = "A_G1_E3",
49 | UserName = "user",
50 | Password = "dummy",
51 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
52 | }
53 | },
54 | SubGroups = new List()
55 | {
56 | new TestGroupValues()
57 | {
58 | Uuid = "C1FA77802458964681E18C642978A3C3",
59 | Name = "A_G2",
60 | Entries = new List()
61 | {
62 | new TestEntryValues()
63 | {
64 | Uuid = "29C4322F054A804D9F37A7A67558C5BF",
65 | Title = "A_G2_E1",
66 | UserName = "user",
67 | Password = "dummy",
68 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
69 | },
70 | new TestEntryValues()
71 | {
72 | Uuid = "7A9C95AC44FE2041B663C46549C21369",
73 | Title = "A_G2_E2",
74 | UserName = "user",
75 | Password = "dummy",
76 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
77 | },
78 | new TestEntryValues()
79 | {
80 | Uuid = "DEB4B7620CC7B24096A87F45F8CEA919",
81 | Title = "A_G2_E3",
82 | UserName = "user",
83 | Password = "dummy",
84 | Url = "https://github.com/lukeIam/KeePassSubsetExport"
85 | }
86 | }
87 | }
88 | }
89 | },
90 | new TestGroupValues()
91 | {
92 | Uuid = "91B698D237EB794A90CAED5876884D5E",
93 | Name = "A_G3",
94 | Entries = new List()
95 | {
96 | new TestEntryValues()
97 | {
98 | Uuid = "A12712EE01C2C84D810EE7E424CEEE37",
99 | Title = "A_G3_E1",
100 | UserName = "user",
101 | Password = "dummy",
102 | Url = "https://github.com/lukeIam/KeePassSubsetExport",
103 | Note = "Note"
104 | }
105 | }
106 | }
107 | }
108 | };
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/KeePassSubsetExport.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {7FF2FCE6-3F03-4BFD-8E02-B55EE50266E3}
8 | Library
9 | Properties
10 | KeePassSubsetExport.Tests
11 | KeePassSubsetExport.Tests
12 | v4.7.2
13 | 512
14 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
15 | 15.0
16 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
17 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages
18 | False
19 | UnitTest
20 |
21 |
22 |
23 |
24 |
25 | true
26 | full
27 | false
28 | bin\Debug\
29 | DEBUG;TRACE
30 | prompt
31 | 4
32 |
33 |
34 | pdbonly
35 | true
36 | bin\Release\
37 | TRACE
38 | prompt
39 | 4
40 |
41 |
42 |
43 | $(KeePassPath)KeePass.exe
44 |
45 |
46 | ..\packages\MSTest.TestFramework.1.2.1\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll
47 |
48 |
49 | ..\packages\MSTest.TestFramework.1.2.1\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | {2e861274-c7d4-4774-b9a0-a746e0774a88}
82 | KeePassSubsetExport
83 |
84 |
85 |
86 |
87 |
88 |
89 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
90 |
91 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # MSTest test Results
33 | [Tt]est[Rr]esult*/
34 | [Bb]uild[Ll]og.*
35 |
36 | # NUNIT
37 | *.VisualState.xml
38 | TestResult.xml
39 |
40 | # Build Results of an ATL Project
41 | [Dd]ebugPS/
42 | [Rr]eleasePS/
43 | dlldata.c
44 |
45 | # .NET Core
46 | project.lock.json
47 | project.fragment.lock.json
48 | artifacts/
49 | **/Properties/launchSettings.json
50 |
51 | *_i.c
52 | *_p.c
53 | *_i.h
54 | *.ilk
55 | *.meta
56 | *.obj
57 | *.pch
58 | *.pdb
59 | *.pgc
60 | *.pgd
61 | *.rsp
62 | *.sbr
63 | *.tlb
64 | *.tli
65 | *.tlh
66 | *.tmp
67 | *.tmp_proj
68 | *.log
69 | *.vspscc
70 | *.vssscc
71 | .builds
72 | *.pidb
73 | *.svclog
74 | *.scc
75 |
76 | # Chutzpah Test files
77 | _Chutzpah*
78 |
79 | # Visual C++ cache files
80 | ipch/
81 | *.aps
82 | *.ncb
83 | *.opendb
84 | *.opensdf
85 | *.sdf
86 | *.cachefile
87 | *.VC.db
88 | *.VC.VC.opendb
89 |
90 | # Visual Studio profiler
91 | *.psess
92 | *.vsp
93 | *.vspx
94 | *.sap
95 |
96 | # TFS 2012 Local Workspace
97 | $tf/
98 |
99 | # Guidance Automation Toolkit
100 | *.gpState
101 |
102 | # ReSharper is a .NET coding add-in
103 | _ReSharper*/
104 | *.[Rr]e[Ss]harper
105 | *.DotSettings.user
106 |
107 | # JustCode is a .NET coding add-in
108 | .JustCode
109 |
110 | # TeamCity is a build add-in
111 | _TeamCity*
112 |
113 | # DotCover is a Code Coverage Tool
114 | *.dotCover
115 |
116 | # Visual Studio code coverage results
117 | *.coverage
118 | *.coveragexml
119 |
120 | # NCrunch
121 | _NCrunch_*
122 | .*crunch*.local.xml
123 | nCrunchTemp_*
124 |
125 | # MightyMoose
126 | *.mm.*
127 | AutoTest.Net/
128 |
129 | # Web workbench (sass)
130 | .sass-cache/
131 |
132 | # Installshield output folder
133 | [Ee]xpress/
134 |
135 | # DocProject is a documentation generator add-in
136 | DocProject/buildhelp/
137 | DocProject/Help/*.HxT
138 | DocProject/Help/*.HxC
139 | DocProject/Help/*.hhc
140 | DocProject/Help/*.hhk
141 | DocProject/Help/*.hhp
142 | DocProject/Help/Html2
143 | DocProject/Help/html
144 |
145 | # Click-Once directory
146 | publish/
147 |
148 | # Publish Web Output
149 | *.[Pp]ublish.xml
150 | *.azurePubxml
151 | # TODO: Comment the next line if you want to checkin your web deploy settings
152 | # but database connection strings (with potential passwords) will be unencrypted
153 | *.pubxml
154 | *.publishproj
155 |
156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
157 | # checkin your Azure Web App publish settings, but sensitive information contained
158 | # in these scripts will be unencrypted
159 | PublishScripts/
160 |
161 | # NuGet Packages
162 | *.nupkg
163 | # The packages folder can be ignored because of Package Restore
164 | **/packages/*
165 | # except build/, which is used as an MSBuild target.
166 | !**/packages/build/
167 | # Uncomment if necessary however generally it will be regenerated when needed
168 | #!**/packages/repositories.config
169 | # NuGet v3's project.json files produces more ignorable files
170 | *.nuget.props
171 | *.nuget.targets
172 |
173 | # Microsoft Azure Build Output
174 | csx/
175 | *.build.csdef
176 |
177 | # Microsoft Azure Emulator
178 | ecf/
179 | rcf/
180 |
181 | # Windows Store app package directories and files
182 | AppPackages/
183 | BundleArtifacts/
184 | Package.StoreAssociation.xml
185 | _pkginfo.txt
186 |
187 | # Visual Studio cache files
188 | # files ending in .cache can be ignored
189 | *.[Cc]ache
190 | # but keep track of directories ending in .cache
191 | !*.[Cc]ache/
192 |
193 | # Others
194 | ClientBin/
195 | ~$*
196 | *~
197 | *.dbmdl
198 | *.dbproj.schemaview
199 | *.jfm
200 | *.pfx
201 | *.publishsettings
202 | orleans.codegen.cs
203 |
204 | # Since there are multiple workflows, uncomment next line to ignore bower_components
205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
206 | #bower_components/
207 |
208 | # RIA/Silverlight projects
209 | Generated_Code/
210 |
211 | # Backup & report files from converting an old project file
212 | # to a newer Visual Studio version. Backup files are not needed,
213 | # because we have git ;-)
214 | _UpgradeReport_Files/
215 | Backup*/
216 | UpgradeLog*.XML
217 | UpgradeLog*.htm
218 |
219 | # SQL Server files
220 | *.mdf
221 | *.ldf
222 | *.ndf
223 |
224 | # Business Intelligence projects
225 | *.rdl.data
226 | *.bim.layout
227 | *.bim_*.settings
228 |
229 | # Microsoft Fakes
230 | FakesAssemblies/
231 |
232 | # GhostDoc plugin setting file
233 | *.GhostDoc.xml
234 |
235 | # Node.js Tools for Visual Studio
236 | .ntvs_analysis.dat
237 | node_modules/
238 |
239 | # Typescript v1 declaration files
240 | typings/
241 |
242 | # Visual Studio 6 build log
243 | *.plg
244 |
245 | # Visual Studio 6 workspace options file
246 | *.opt
247 |
248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
249 | *.vbw
250 |
251 | # Visual Studio LightSwitch build output
252 | **/*.HTMLClient/GeneratedArtifacts
253 | **/*.DesktopClient/GeneratedArtifacts
254 | **/*.DesktopClient/ModelManifest.xml
255 | **/*.Server/GeneratedArtifacts
256 | **/*.Server/ModelManifest.xml
257 | _Pvt_Extensions
258 |
259 | # Paket dependency manager
260 | .paket/paket.exe
261 | paket-files/
262 |
263 | # FAKE - F# Make
264 | .fake/
265 |
266 | # JetBrains Rider
267 | .idea/
268 | *.sln.iml
269 |
270 | # CodeRush
271 | .cr/
272 |
273 | # Python Tools for Visual Studio (PTVS)
274 | __pycache__/
275 | *.pyc
276 |
277 | # Cake - Uncomment if you are using it
278 | # tools/**
279 | # !tools/packages.config
280 |
281 | # Telerik's JustMock configuration file
282 | *.jmconfig
283 |
284 | # BizTalk build output
285 | *.btp.cs
286 | *.btm.cs
287 | *.odx.cs
288 | *.xsd.cs
289 | /KeePass
290 |
--------------------------------------------------------------------------------
/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
121 |
122 | ..\Resources\Key.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
123 |
124 |
--------------------------------------------------------------------------------
/Settings.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using KeePassLib;
3 | using KeePassLib.Security;
4 | using KeePassLib.Utility;
5 |
6 | namespace KeePassSubsetExport
7 | {
8 | ///
9 | /// Contains all settings for a job.
10 | ///
11 | public class Settings
12 | {
13 | ///
14 | /// The password to protect the target database(optional if is set)
15 | ///
16 | public ProtectedString Password { get; private set; }
17 | ///
18 | /// The path for the target database.
19 | ///
20 | public string TargetFilePath { get; set; }
21 | ///
22 | /// The path to a key file to protect the target database (optional if is set).
23 | ///
24 | public string KeyFilePath { get; set; }
25 | ///
26 | /// Tag to export (optional if is set).
27 | ///
28 | public string Tag { get; private set; }
29 | ///
30 | /// The parsed KeyTransformationRounds for KdfAes.
31 | ///
32 | public ulong KeyTransformationRounds { get; set; }
33 | ///
34 | /// The parsed number of interations for KdfArgon2.
35 | ///
36 | public ulong Argon2ParamIterations { get; set; }
37 | ///
38 | /// The parsed memory amount for KdfArgon2.
39 | ///
40 | public ulong Argon2ParamMemory { get; set; }
41 | ///
42 | /// The parsed count of parallelism for KdfArgon2.
43 | ///
44 | public uint Argon2ParamParallelism { get; set; }
45 | ///
46 | /// The new name for the root group (optional).
47 | ///
48 | public string RootGroupName { get; private set; }
49 | ///
50 | /// The name of the group to export (optional if is set).
51 | ///
52 | public string Group { get; private set; }
53 | ///
54 | /// If true, the export progress will ignore groups/folders, false otherwise (optional, defaults to false).
55 | ///
56 | public bool FlatExport { get; private set; }
57 | ///
58 | /// If true, the target database will be overriden, otherwise the entries will added to the target database (optional, defaults to true).
59 | ///
60 | public bool OverrideTargetDatabase { get; private set; }
61 | ///
62 | /// If true, only newer entries will overrides older entries (only works with == false).
63 | ///
64 | public bool OverrideEntryOnlyNewer { get; private set; }
65 | ///
66 | /// If true, the entire group of the target database will be overwritten (only works with == false).
67 | ///
68 | public bool OverrideEntireGroup { get; private set; }
69 | ///
70 | /// If true, this export job will be ignored.
71 | ///
72 | public bool Disabled { get; private set; }
73 | ///
74 | /// If true, only Username and Password will be exported to the target database.
75 | ///
76 | public bool ExportUserAndPassOnly { get; private set; }
77 | ///
78 | /// If true, the settings of the source database will be exported to the target database.
79 | ///
80 | public bool ExportDatebaseSettings { get; private set; }
81 |
82 | // Private constructor
83 | private Settings()
84 | {
85 | }
86 |
87 | private static ulong GetUlongValue(string key, PwEntry settingsEntry)
88 | {
89 | ulong result = 0;
90 | string value = settingsEntry.Strings.ReadSafe(key);
91 | if (!string.IsNullOrEmpty(value) && !ulong.TryParse(value, out result))
92 | {
93 | MessageService.ShowWarning("SubsetExport: " + key + " is given but can not be parsed as ulog for: " +
94 | settingsEntry.Strings.ReadSafe("Title"));
95 | }
96 |
97 | return result;
98 | }
99 |
100 | private static uint GetUIntValue(string key, PwEntry settingsEntry)
101 | {
102 | uint result = 0;
103 | string value = settingsEntry.Strings.ReadSafe(key);
104 | if (!string.IsNullOrEmpty(value) && !uint.TryParse(value, out result))
105 | {
106 | MessageService.ShowWarning("SubsetExport: " + key + " is given but can not be parsed as uint for: " +
107 | settingsEntry.Strings.ReadSafe("Title"));
108 | }
109 |
110 | return result;
111 | }
112 |
113 | ///
114 | /// Read all job settings from an entry.
115 | ///
116 | /// The entry to read the settings from.
117 | /// A database to resolve refs in the password field.
118 | /// A settings object containing all the settings for this job.
119 | public static Settings Parse(PwEntry settingsEntry, PwDatabase sourceDb = null)
120 | {
121 | return new Settings()
122 | {
123 | Password = FieldHelper.GetFieldWRef(settingsEntry, sourceDb, PwDefs.PasswordField),
124 | TargetFilePath = FieldHelper.GetFieldWRefUnprotected(settingsEntry, sourceDb, "SubsetExport_TargetFilePath"),
125 | KeyFilePath = FieldHelper.GetFieldWRefUnprotected(settingsEntry, sourceDb, "SubsetExport_KeyFilePath"),
126 | Tag = settingsEntry.Strings.ReadSafe("SubsetExport_Tag"),
127 | KeyTransformationRounds = GetUlongValue("SubsetExport_KeyTransformationRounds", settingsEntry),
128 | RootGroupName = settingsEntry.Strings.ReadSafe("SubsetExport_RootGroupName"),
129 | Group = settingsEntry.Strings.ReadSafe("SubsetExport_Group"),
130 | FlatExport = settingsEntry.Strings.ReadSafe("SubsetExport_FlatExport").ToLower().Trim() == "true",
131 | OverrideTargetDatabase = settingsEntry.Strings.ReadSafe("SubsetExport_OverrideTargetDatabase").ToLower().Trim() != "false",
132 | OverrideEntryOnlyNewer = settingsEntry.Strings.ReadSafe("SubsetExport_OverrideEntryOnlyNewer").ToLower().Trim() == "true",
133 | OverrideEntireGroup = settingsEntry.Strings.ReadSafe("SubsetExport_OverrideEntireGroup").ToLower().Trim() == "true",
134 | Argon2ParamIterations = GetUlongValue("SubsetExport_Argon2ParamIterations", settingsEntry),
135 | Argon2ParamMemory = GetUlongValue("SubsetExport_Argon2ParamMemory", settingsEntry),
136 | Argon2ParamParallelism = GetUIntValue("SubsetExport_Argon2ParamParallelism", settingsEntry),
137 | Disabled = (settingsEntry.Expires && DateTime.Now > settingsEntry.ExpiryTime),
138 | ExportUserAndPassOnly = settingsEntry.Strings.ReadSafe("SubsetExport_ExportUserAndPassOnly").ToLower().Trim() == "true",
139 | ExportDatebaseSettings = settingsEntry.Strings.ReadSafe("SubsetExport_ExportDatebaseSettings").ToLower().Trim() != "false",
140 | };
141 | }
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # KeePassSubsetExport
2 | KeePassSubsetExport is a [KeePass2](https://keepass.info) plugin which automatically exports a subset of entries (tag based) to new databases with different keys.
3 |
4 | [](https://lukeiam.visualstudio.com/KeePassSubsetExport/_build/latest?definitionId=1)
5 | [](https://sonarcloud.io/dashboard?id=KeePassSubsetExport)
6 | [](https://sonarcloud.io/component_measures?id=KeePassSubsetExport&metric=coverage)
7 | [](https://github.com/lukeIam/KeePassSubsetExport/releases/latest)
8 | [](https://github.com/lukeIam/KeePassSubsetExport/releases)
9 | [](https://github.com/lukeIam/KeePassSubsetExport/blob/master/LICENSE)
10 |
11 | ## Why?
12 | I'm using the plugin to export some entries of my main database to another database which is [synced](https://syncthing.net) to my mobile devices.
13 | Additionally, I'm sharing some other entries with my family.
14 |
15 | ## Disclaimer
16 | This is my first KeePass plugin and I tried not to compromise security - but I can't guarantee it.
17 | **So use this plugin at your own risk.**
18 | If you have more experience with KeePass plugins, I would be very grateful if you have a look on the code.
19 |
20 | ## How to install?
21 | - Download the latest release from [here](https://github.com/lukeIam/KeePassSubsetExport/releases)
22 | - Place KeePassSubsetExport.plgx in the KeePass program directory
23 | - Start KeePass and the plugin is automatically loaded (check the Plugin menu)
24 |
25 | ## How to use?
26 | - Open the database containing the entries that should be exported
27 | - Create a folder `SubsetExportSettings` under the root folder
28 | - For each export job (target database) create a new entry:
29 |
30 | | Setting | Description | Optional | Example |
31 | | --------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------ | --------------------------------------- |
32 | | `Title` | Name of the job | No | `SubsetExport_MobilePhone` |
33 | | `Password` | The password for the target database | Yes, if `SubsetExport_KeyFilePath` is set | `SecurePW!` |
34 | | `Expires` | If the entry is expired the job is disabled and won't be executed | `-` | `-` |
35 | | `SubsetExport_KeyFilePath`
[string field] | Path to a key file | Yes, if `Password` is set | `C:\keys\mobile.key` |
36 | | `SubsetExport_TargetFilePath`
[string field] | Path to the target database.
(Absolute, or relative to source database parent folder.) (`,` to delimit multiple target dbs) | No | `C:\sync\mobile.kdbx`
or
`mobile.kdbx`
or
`..\mobile.kdbx` |
37 | | `SubsetExport_Tag`
[string field] | Tag(s) for filtering (`,` to delimit multiple tags - `,` is not allowed in tag names)| Yes, if `SubsetExport_Group` is set | `MobileSync` |
38 | | `SubsetExport_Group`
[string field] | Group(s) for filtering (`,` to delimit multiple groups - `,` is not allowed in group names)| Yes, if `SubsetExport_Tag` is set | `MobileGroup` |
39 | | `SubsetExport_KeyTransformationRounds`
[string field] | Overwrite the number of KeyTransformationRounds for AesKdf | Yes | `10000000` |
40 | | `SubsetExport_RootGroupName`
[string field] | Overwrite the name of the root group in the target database | Yes | `NewRootGroupName` |
41 | | `SubsetExport_FlatExport`
[string field] | If `True` no groups will be created in the target database (flat export)| Yes (defaults to `False`) | `True` |
42 | | `SubsetExport_OverrideTargetDatabase`
[string field] | If `True` the traget database will be overriden, otherwise the enries will added to the target database | Yes (defaults to `True`) | `True` |
43 | | `SubsetExport_OverrideEntryOnlyNewer`
[string field] | If `True` only newer entries will overrides older entries (`OverrideTargetDatabase` is `False`)| Yes (defaults to `False`) | `True` |
44 | | `SubsetExport_OverrideEntireGroup`
[string field] | If `True` will override entire group in target Database (`OverrideTargetDatabase` is `False`)| Yes (defaults to `False`) | `True` |
45 | | `SubsetExport_Argon2ParamIterations`
[string field] | Overwrite the number of iterations of Argon2Kdf | Yes | `2` |
46 | | `SubsetExport_Argon2ParamMemory`
[string field] | Overwrite the memory parameter of Argon2Kdf | Yes | `1048576` = 1MB |
47 | | `SubsetExport_Argon2ParamParallelism`
[string field] | Overwrite the parallelism parameter of Argon2Kdf
Typical parallelism values should be less or equal than to two times the number of available processor cores (less if increasing does not result in a performance increase) | Yes | `2` |
48 | | `SubsetExport_ExportUserAndPassOnly`
[string field] | If `True` only the username and password will be exported to the target database. | Yes (defaults to `False`) | `True` |
49 |
50 | - Every time the (source) database is saved the target databases will be recreated automatically
51 | - To disable an export job temporarily just move its entry to another folder
52 | - If both `SubsetExport_Tag` and `SubsetExport_Group` are set, only entries matching *both* will be exported
53 |
54 | 
55 |
56 | ## KeePassSubsetExport vs Partial KeePass Database Export
57 | I started developing KeePassSubsetExport before [Partial KeePass Database Export](https://github.com/heinrich-ulbricht/partial-keepass-database-export) was published, so the basic functionality is similar.
58 | But KeePassSubsetExport has some advantages:
59 | - The folder structure is copied to the target database
60 | - Multiple export jobs are supported
61 | - Key-File protection of the target databases is supported
62 | - KeyTransformationRounds of the target database is set to the number of the source database (can be overwritten)
63 | - Exports can be limited to a folder (can be combined with a tag filter)
64 | - Field references support (in the export job password field and the following entry fields: Title, Username, Url and Password)
65 |
--------------------------------------------------------------------------------
/KeePassSubsetExport.Tests/MainTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using KeePassLib;
5 | using KeePassLib.Cryptography.KeyDerivation;
6 | using KeePassSubsetExport.Tests.ComparisonData;
7 | using KeePassSubsetExport.Tests.DataContainer;
8 | using Microsoft.VisualStudio.TestTools.UnitTesting;
9 |
10 | namespace KeePassSubsetExport.Tests
11 | {
12 | [TestClass]
13 | public class MainTests
14 | {
15 | private static TestSettings _settings;
16 |
17 |
18 | private static void InitalizeSettings(TestContext testContext)
19 | {
20 | _settings = new TestSettings();
21 |
22 |
23 | string testDirPath = testContext.TestDir;
24 |
25 | // Check if test is running on AzureDevops
26 | if (testDirPath.Contains("_temp"))
27 | {
28 | // Running on AzureDevOps
29 | _settings.RootPath = @"D:\a\1\s\";
30 | }
31 | else
32 | {
33 | // Local test run
34 | _settings.RootPath =
35 | Path.GetDirectoryName(Path.GetDirectoryName(testDirPath)) ?? throw new InvalidOperationException();
36 | }
37 |
38 | _settings.DbAFilesPath = Path.Combine(_settings.RootPath, @"TestDatabases\A\");
39 | _settings.DbBFilesPath = Path.Combine(_settings.RootPath, @"TestDatabases\B\");
40 | _settings.DbCFilesPath = Path.Combine(_settings.RootPath, @"TestDatabases\C\");
41 |
42 | _settings.DbMainPw = "Test";
43 | _settings.DbAPath = Path.Combine(_settings.DbAFilesPath, "A.kdbx");
44 | _settings.DbBPath = Path.Combine(_settings.DbBFilesPath, "B.kdbx");
45 | _settings.DbCPath = Path.Combine(_settings.DbCFilesPath, "C.kdbx");
46 |
47 | _settings.DbTestPw = "TargetPw";
48 | _settings.KeyTestAPath = Path.Combine(_settings.DbAFilesPath, "A.key");
49 | _settings.KeyTestBPath = Path.Combine(_settings.DbBFilesPath, "B.key");
50 |
51 | // Delete old files
52 | foreach (var filePathToDelete in Directory.GetFiles(_settings.DbAFilesPath, "*_*.kdbx"))
53 | {
54 | File.Delete(filePathToDelete);
55 | }
56 | }
57 |
58 | [ClassInitialize()]
59 | public static void Initalize(TestContext testContext)
60 | {
61 | InitalizeSettings(testContext);
62 |
63 | Environment.SetEnvironmentVariable("KeePassSubsetExport_Test_Ae10_TargetPath", Ae10RealData.Db);
64 | Environment.SetEnvironmentVariable("KeePassSubsetExport_Test_Ae10_KeyPath", "A.key");
65 |
66 | PwDatabase db = DbHelper.OpenDatabase(_settings.DbAPath, _settings.DbMainPw);
67 | Exporter.Export(db);
68 |
69 | Environment.SetEnvironmentVariable("KeePassSubsetExport_Test_Ae10_TargetPath", null);
70 | Environment.SetEnvironmentVariable("KeePassSubsetExport_Test_Ae10_KeyPath", null);
71 |
72 | db.Close();
73 |
74 | db = DbHelper.OpenDatabase(_settings.DbBPath, _settings.DbMainPw);
75 |
76 | Exporter.Export(db);
77 |
78 | db.Close();
79 |
80 | db = DbHelper.OpenDatabase(_settings.DbCPath, _settings.DbMainPw);
81 |
82 | Exporter.Export(db);
83 |
84 | db.Close();
85 | }
86 |
87 | [TestMethod]
88 | public void Ae1Test()
89 | {
90 | PwDatabase db = DbHelper.OpenDatabase(Path.Combine(_settings.DbAFilesPath, Ae1RealData.Db), password:_settings.DbTestPw,
91 | keyPath:_settings.KeyTestAPath);
92 |
93 | var group = db.RootGroup;
94 |
95 | CheckKdf(db.KdfParameters, Ae1RealData.Kdf);
96 |
97 | CheckGroup(group, Ae1RealData.Data);
98 |
99 | db.Close();
100 | }
101 |
102 | [TestMethod]
103 | public void Ae2Test()
104 | {
105 | PwDatabase db = DbHelper.OpenDatabase(Path.Combine(_settings.DbAFilesPath, Ae2RealData.Db), keyPath: _settings.KeyTestAPath);
106 |
107 | var group = db.RootGroup;
108 |
109 | CheckKdf(db.KdfParameters, Ae2RealData.Kdf);
110 |
111 | CheckGroup(group, Ae2RealData.Data);
112 |
113 | db.Close();
114 | }
115 |
116 | [TestMethod]
117 | public void Ae3Test()
118 | {
119 | PwDatabase db = DbHelper.OpenDatabase(Path.Combine(_settings.DbAFilesPath, Ae3RealData.Db), password: _settings.DbTestPw);
120 |
121 | var group = db.RootGroup;
122 |
123 | CheckKdf(db.KdfParameters, Ae3RealData.Kdf);
124 |
125 | CheckGroup(group, Ae3RealData.Data);
126 |
127 | db.Close();
128 | }
129 |
130 | [TestMethod]
131 | public void Ae4Test()
132 | {
133 | PwDatabase db = DbHelper.OpenDatabase(Path.Combine(_settings.DbAFilesPath, Ae4RealData.Db), password: _settings.DbTestPw);
134 |
135 | var group = db.RootGroup;
136 |
137 | CheckKdf(db.KdfParameters, Ae4RealData.Kdf);
138 |
139 | CheckGroup(group, Ae4RealData.Data);
140 |
141 | db.Close();
142 | }
143 |
144 | [TestMethod]
145 | public void Ae5Test()
146 | {
147 | PwDatabase db = DbHelper.OpenDatabase(Path.Combine(_settings.DbAFilesPath, Ae5RealData.Db), password: _settings.DbTestPw);
148 |
149 | var group = db.RootGroup;
150 |
151 | CheckKdf(db.KdfParameters, Ae5RealData.Kdf);
152 |
153 | CheckGroup(group, Ae5RealData.Data);
154 |
155 | db.Close();
156 | }
157 |
158 | [TestMethod]
159 | public void Ae6Test()
160 | {
161 | PwDatabase db = DbHelper.OpenDatabase(Path.Combine(_settings.DbAFilesPath, Ae6RealData.Db), password: _settings.DbTestPw);
162 |
163 | var group = db.RootGroup;
164 |
165 | CheckKdf(db.KdfParameters, Ae6RealData.Kdf);
166 |
167 | CheckGroup(group, Ae6RealData.Data);
168 |
169 | db.Close();
170 | }
171 |
172 | [TestMethod]
173 | public void Ae7Test()
174 | {
175 | Assert.IsFalse(File.Exists(Path.Combine(_settings.DbCFilesPath, Ae7RealData.Db)), "An disabled/expired job was executed.");
176 | }
177 |
178 | [TestMethod]
179 | public void Ae8Test()
180 | {
181 | PwDatabase db = DbHelper.OpenDatabase(Path.Combine(_settings.DbAFilesPath, Ae8RealData.Db), password: _settings.DbTestPw,
182 | keyPath: _settings.KeyTestAPath);
183 |
184 | var group = db.RootGroup;
185 |
186 | CheckKdf(db.KdfParameters, Ae8RealData.Kdf);
187 |
188 | CheckGroup(group, Ae8RealData.Data);
189 |
190 | db.Close();
191 | }
192 |
193 | [TestMethod]
194 | public void Ae9Test()
195 | {
196 | PwDatabase db = DbHelper.OpenDatabase(Path.Combine(_settings.DbAFilesPath, Ae9RealData.Db), password: _settings.DbTestPw,
197 | keyPath: _settings.KeyTestAPath);
198 |
199 | var group = db.RootGroup;
200 |
201 | CheckKdf(db.KdfParameters, Ae9RealData.Kdf);
202 |
203 | CheckGroup(group, Ae9RealData.Data);
204 |
205 | db.Close();
206 | }
207 |
208 | [TestMethod]
209 | public void Ae10Test()
210 | {
211 | PwDatabase db = DbHelper.OpenDatabase(Path.Combine(_settings.DbAFilesPath, Ae10RealData.Db), password: _settings.DbTestPw,
212 | keyPath: _settings.KeyTestAPath);
213 |
214 | var group = db.RootGroup;
215 |
216 | CheckKdf(db.KdfParameters, Ae10RealData.Kdf);
217 |
218 | CheckGroup(group, Ae10RealData.Data);
219 |
220 | db.Close();
221 | }
222 |
223 | [TestMethod]
224 | public void Ae11Test()
225 | {
226 | PwDatabase db = DbHelper.OpenDatabase(Path.Combine(_settings.DbAFilesPath, Ae11_RealData.Db), password: _settings.DbTestPw,
227 | keyPath: _settings.KeyTestAPath);
228 |
229 | var group = db.RootGroup;
230 |
231 | CheckKdf(db.KdfParameters, Ae1RealData.Kdf);
232 |
233 | CheckGroup(group, Ae1RealData.Data);
234 |
235 | db.Close();
236 |
237 | db = DbHelper.OpenDatabase(Path.Combine(_settings.DbAFilesPath, Ae11_RealData.Db2), password: _settings.DbTestPw,
238 | keyPath: _settings.KeyTestAPath);
239 |
240 | group = db.RootGroup;
241 |
242 | CheckKdf(db.KdfParameters, Ae1RealData.Kdf);
243 |
244 | CheckGroup(group, Ae1RealData.Data);
245 |
246 | db.Close();
247 | }
248 |
249 | [TestMethod]
250 | public void Be1Test()
251 | {
252 | PwDatabase db = DbHelper.OpenDatabase(Path.Combine(_settings.DbBFilesPath, Be1RealData.Db), keyPath: _settings.KeyTestBPath);
253 |
254 | var group = db.RootGroup;
255 |
256 | CheckKdf(db.KdfParameters, Be1RealData.Kdf);
257 |
258 | CheckGroup(group, Be1RealData.Data);
259 |
260 | db.Close();
261 | }
262 |
263 | [TestMethod]
264 | public void Be2Test()
265 | {
266 | PwDatabase db = DbHelper.OpenDatabase(Path.Combine(_settings.DbBFilesPath, Be2RealData.Db), keyPath: _settings.KeyTestAPath);
267 |
268 | var group = db.RootGroup;
269 |
270 | CheckKdf(db.KdfParameters, Be2RealData.Kdf);
271 |
272 | CheckGroup(group, Be2RealData.Data);
273 |
274 | db.Close();
275 | }
276 |
277 | #region Content test functions
278 |
279 | private static void CheckGroup(PwGroup group, TestGroupValues data)
280 | {
281 | Assert.IsNotNull(group);
282 | Assert.AreEqual(data.Uuid, group.Uuid.ToHexString());
283 | Assert.AreEqual(data.Name, group.Name);
284 | CollectionAssert.AreEquivalent(data.SubGroups?.Select(x => x.Uuid).ToArray() ?? (new string[0]), group.Groups?.Select(x => x.Uuid.ToHexString()).ToArray() ??(new string[0]));
285 | CollectionAssert.AreEquivalent(data.Entries?.Select(x => x.Uuid).ToArray() ?? (new string[0]), group.Entries?.Select(x => x.Uuid.ToHexString()).ToArray() ?? (new string[0]));
286 |
287 | if (group.Entries != null)
288 | {
289 | foreach (PwEntry entry in group.Entries)
290 | {
291 | CheckEntry(entry, (data.Entries ?? throw new InvalidOperationException()).First(x => x.Uuid == entry.Uuid.ToHexString()));
292 | }
293 | }
294 |
295 | if (group.Groups != null)
296 | {
297 | foreach (PwGroup subGroup in group.Groups)
298 | {
299 | CheckGroup(subGroup, (data.SubGroups ?? throw new InvalidOperationException()).First(x => x.Uuid == subGroup.Uuid.ToHexString()));
300 | }
301 | }
302 | }
303 |
304 | private static void CheckEntry(PwEntry entry, TestEntryValues testEntryValues)
305 | {
306 | Assert.IsNotNull(entry);
307 | Assert.AreEqual(testEntryValues.Title, entry.Strings.ReadSafe("Title"));
308 | Assert.AreEqual(testEntryValues.UserName, entry.Strings.ReadSafe("UserName"));
309 | Assert.AreEqual(testEntryValues.Password, entry.Strings.ReadSafe("Password"));
310 | Assert.AreEqual(testEntryValues.Url, entry.Strings.ReadSafe("URL"));
311 | Assert.AreEqual(testEntryValues.Note, entry.Strings.ReadSafe("Notes"));
312 | }
313 |
314 | private static void CheckKdf(KdfParameters param, TestKdfValues testEntryValues)
315 | {
316 | Assert.AreEqual(testEntryValues.KdfUuid, param.KdfUuid);
317 | if (param.KdfUuid.Equals(TestKdfValues.UuidAes))
318 | {
319 | Assert.AreEqual(testEntryValues.AesKeyTransformationRounds, param.GetUInt64(AesKdf.ParamRounds, 0));
320 | }
321 | else if(param.KdfUuid.Equals(TestKdfValues.UuidArgon2))
322 | {
323 | Assert.AreEqual(testEntryValues.Argon2Iterations, param.GetUInt64(Argon2Kdf.ParamIterations, 0));
324 | Assert.AreEqual(testEntryValues.Argon2Memory, param.GetUInt64(Argon2Kdf.ParamMemory, 0));
325 | Assert.AreEqual(testEntryValues.Argon2Parallelism, param.GetUInt32(Argon2Kdf.ParamParallelism, 0));
326 | }
327 | else
328 | {
329 | Assert.Fail("Kdf is not Aes or Argon2");
330 | }
331 | }
332 |
333 | #endregion
334 |
335 | }
336 | }
337 |
--------------------------------------------------------------------------------
/Exporter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using KeePassLib;
6 | using KeePassLib.Collections;
7 | using KeePassLib.Cryptography.KeyDerivation;
8 | using KeePassLib.Interfaces;
9 | using KeePassLib.Keys;
10 | using KeePassLib.Serialization;
11 | using KeePassLib.Utility;
12 | using System.Text.RegularExpressions;
13 |
14 | namespace KeePassSubsetExport
15 | {
16 | internal static class Exporter
17 | {
18 | private static readonly PwUuid UuidAes = new PwUuid(new byte[] {
19 | 0xC9, 0xD9, 0xF3, 0x9A, 0x62, 0x8A, 0x44, 0x60,
20 | 0xBF, 0x74, 0x0D, 0x08, 0xC1, 0x8A, 0x4F, 0xEA });
21 |
22 | private static readonly PwUuid UuidArgon2 = new PwUuid(new byte[] {
23 | 0xEF, 0x63, 0x6D, 0xDF, 0x8C, 0x29, 0x44, 0x4B,
24 | 0x91, 0xF7, 0xA9, 0xA4, 0x03, 0xE3, 0x0A, 0x0C });
25 |
26 | private static readonly IOConnectionInfo ConnectionInfo = new IOConnectionInfo();
27 |
28 | ///
29 | /// Exports all entries with the given tag to a new database at the given path (multiple jobs possible).
30 | /// Each job is an entry in the "SubsetExportSettings" folder with a title "SubsetExport_*".
31 | /// Password == password field of the entry
32 | /// keyFilePath == "SubsetExport_KeyFilePath" string field on the entry
33 | /// targetFilePath == "SubsetExport_TargetFilePath" string field on the entry
34 | /// tag (filter) == "SubsetExport_Tag" string field on the entry
35 | ///
36 | /// The source database to run the exports on.
37 | internal static void Export(PwDatabase sourceDb)
38 | {
39 | // Get all entries out of the group "SubsetExportSettings" which start with "SubsetExport_"
40 | PwGroup settingsGroup = FindSettingsGroup(sourceDb);
41 | if (settingsGroup == null)
42 | {
43 | return;
44 | }
45 | IEnumerable jobSettings = settingsGroup.Entries;
46 |
47 | // Loop through all found entries - each on is a export job
48 | foreach (var settingsEntry in jobSettings)
49 | {
50 | // Load settings for this job
51 | var settings = Settings.Parse(settingsEntry, sourceDb);
52 |
53 | // Skip disabled/expired jobs
54 | if (settings.Disabled)
55 | continue;
56 |
57 | if (CheckKeyFile(sourceDb, settings, settingsEntry))
58 | continue;
59 |
60 | if (CheckTagOrGroup(settings, settingsEntry))
61 | continue;
62 |
63 | if (CheckTargetFilePath(settings, settingsEntry))
64 | continue;
65 |
66 | if (CheckPasswordOrKeyfile(settings, settingsEntry))
67 | continue;
68 |
69 | try
70 | {
71 | // Execute the export
72 | CopyToNewDb(sourceDb, settings);
73 | }
74 | catch (Exception e)
75 | {
76 | MessageService.ShowWarning("SubsetExport failed:", e);
77 | }
78 | }
79 | }
80 |
81 | private static PwGroup FindSettingsGroup(PwDatabase sourceDb, string settingsGroupName = "SubsetExportSettings")
82 | {
83 | var settingsGroup = sourceDb.RootGroup.Groups.FirstOrDefault(g => g.Name == settingsGroupName);
84 | if (settingsGroup != null)
85 | {
86 | return settingsGroup;
87 | }
88 |
89 | return FindGroupRecursive(sourceDb.RootGroup, settingsGroupName);
90 | }
91 |
92 | private static PwGroup FindGroupRecursive(PwGroup startGroup, string groupName)
93 | {
94 | if (startGroup.Name == groupName)
95 | {
96 | return startGroup;
97 | }
98 |
99 | foreach (PwGroup group in startGroup.Groups)
100 | {
101 | PwGroup result = FindGroupRecursive(group, groupName);
102 | if (result != null)
103 | {
104 | return result;
105 | }
106 | }
107 |
108 | return null;
109 | }
110 |
111 | private static bool CheckPasswordOrKeyfile(Settings settings, PwEntry settingsEntry)
112 | {
113 | // Require at least one of Password or KeyFilePath.
114 | if (settings.Password.IsEmpty && !File.Exists(settings.KeyFilePath))
115 | {
116 | MessageService.ShowWarning("SubsetExport: Missing Password or valid KeyFilePath for: " +
117 | settingsEntry.Strings.ReadSafe("Title"));
118 | return true;
119 | }
120 |
121 | return false;
122 | }
123 |
124 | private static bool CheckTargetFilePath(Settings settings, PwEntry settingsEntry)
125 | {
126 | // Require targetFilePath
127 | if (string.IsNullOrEmpty(settings.TargetFilePath))
128 | {
129 | MessageService.ShowWarning("SubsetExport: Missing TargetFilePath for: " +
130 | settingsEntry.Strings.ReadSafe("Title"));
131 | return true;
132 | }
133 |
134 | return false;
135 | }
136 |
137 | private static bool CheckTagOrGroup(Settings settings, PwEntry settingsEntry)
138 | {
139 | // Require at least one of Tag or Group
140 | if (string.IsNullOrEmpty(settings.Tag) && string.IsNullOrEmpty(settings.Group))
141 | {
142 | MessageService.ShowWarning("SubsetExport: Missing Tag or Group for: " +
143 | settingsEntry.Strings.ReadSafe("Title"));
144 | return true;
145 | }
146 |
147 | return false;
148 | }
149 |
150 | private static Boolean CheckKeyFile(PwDatabase sourceDb, Settings settings, PwEntry settingsEntry)
151 | {
152 | // If a key file is given it must exist.
153 | if (!string.IsNullOrEmpty(settings.KeyFilePath))
154 | {
155 | // Default to same folder as sourceDb for the keyfile if no directory is specified
156 | if (!Path.IsPathRooted(settings.KeyFilePath))
157 | {
158 | string sourceDbPath = Path.GetDirectoryName(sourceDb.IOConnectionInfo.Path);
159 | if (sourceDbPath != null)
160 | {
161 | settings.KeyFilePath = Path.Combine(sourceDbPath, settings.KeyFilePath);
162 | }
163 | }
164 |
165 | if (!File.Exists(settings.KeyFilePath))
166 | {
167 | MessageService.ShowWarning("SubsetExport: Keyfile is given but could not be found for: " +
168 | settingsEntry.Strings.ReadSafe("Title"), settings.KeyFilePath);
169 | return true;
170 | }
171 | }
172 |
173 | return false;
174 | }
175 |
176 | ///
177 | /// Exports all entries with the given tag to a new database at the given path.
178 | ///
179 | /// The source database.
180 | /// The settings for this job.
181 | private static void CopyToNewDb(PwDatabase sourceDb, Settings settings)
182 | {
183 | // Create a key for the target database
184 | CompositeKey key = CreateCompositeKey(settings);
185 |
186 | // Trigger an export for multiple target dbs (as we could also write to en existing db coping is not an option)
187 | foreach (string targetFilePathLoopVar in settings.TargetFilePath.Split(','))
188 | {
189 | string targetFilePath = targetFilePathLoopVar;
190 | // Create or open the target database
191 | PwDatabase targetDatabase = CreateTargetDatabase(sourceDb, settings, key, ref targetFilePath);
192 |
193 | if (settings.ExportDatebaseSettings)
194 | {
195 | // Copy database settings
196 | CopyDatabaseSettings(sourceDb, targetDatabase);
197 | }
198 |
199 | // Copy key derivation function parameters
200 | CopyKdfSettings(sourceDb, settings, targetDatabase);
201 |
202 | // Assign the properties of the source root group to the target root group
203 | targetDatabase.RootGroup.AssignProperties(sourceDb.RootGroup, false, true);
204 | HandleCustomIcon(targetDatabase, sourceDb, sourceDb.RootGroup);
205 |
206 | // Overwrite the root group name if requested
207 | if (!string.IsNullOrEmpty(settings.RootGroupName))
208 | {
209 | targetDatabase.RootGroup.Name = settings.RootGroupName;
210 | }
211 |
212 | // Find all entries matching the tag
213 | PwObjectList entries = GetMatching(sourceDb, settings);
214 |
215 | // Copy all entries to the new database
216 | CopyEntriesAndGroups(sourceDb, settings, entries, targetDatabase);
217 |
218 | // Save new database
219 | SaveTargetDatabase(targetFilePath, targetDatabase, settings.OverrideTargetDatabase);
220 | }
221 | }
222 |
223 | private static PwObjectList GetMatching(PwDatabase sourceDb, Settings settings)
224 | {
225 | PwObjectList entries;
226 |
227 | if (!string.IsNullOrEmpty(settings.Tag) && string.IsNullOrEmpty(settings.Group))
228 | {
229 | // Tag only export
230 | // Support multiple tags (Tag1,Tag2)
231 | entries = FindEntriesByTag(sourceDb, settings.Tag);
232 | }
233 | else if (string.IsNullOrEmpty(settings.Tag) && !string.IsNullOrEmpty(settings.Group))
234 | {
235 | // Group only export
236 | // Support multiple groups (Group1,Group2)
237 | entries = FindEntriesByGroup(sourceDb, settings.Group);
238 | }
239 | else if (!string.IsNullOrEmpty(settings.Tag) && !string.IsNullOrEmpty(settings.Group))
240 | {
241 | // Group and Tag export
242 | // Support multiple groups (Group1,Group2)
243 | // Support multiple tags (Tag1,Tag2)
244 | entries = FindEntriesByGroupAndTag(sourceDb, settings.Group, settings.Tag);
245 | }
246 | else
247 | {
248 | throw new ArgumentException("At least one of Tag or ExportFolderName must be set.");
249 | }
250 |
251 | return entries;
252 | }
253 |
254 | ///
255 | /// Finds all entries with a given group and tag (or multiple)
256 | ///
257 | /// Database to search for the entries.
258 | /// Groups to search for (multiple separated by ,).
259 | /// Tag to search for (multiple separated by ,).
260 | /// A PwObjectList with all metching entries.
261 | private static PwObjectList FindEntriesByGroupAndTag(PwDatabase sourceDb, string groups, string tags)
262 | {
263 | PwObjectList entries = new PwObjectList();
264 |
265 | // Tag and group export
266 | foreach (string group in groups.Split(',').Select(x => x.Trim()))
267 | {
268 | PwGroup groupToExport = sourceDb.RootGroup.GetFlatGroupList().FirstOrDefault(g => g.Name == group);
269 |
270 | if (groupToExport == null)
271 | {
272 | throw new ArgumentException("No group with the name of the Group-Setting found.");
273 | }
274 |
275 | foreach (string tag in tags.Split(',').Select(x => x.Trim()))
276 | {
277 | PwObjectList tagEntries = new PwObjectList();
278 | groupToExport.FindEntriesByTag(tag, tagEntries, true);
279 | // Prevent duplicated entries
280 | IEnumerable existingUuids = entries.Select(x => x.Uuid);
281 | List entriesToAdd = tagEntries.Where(x => !existingUuids.Contains(x.Uuid)).ToList();
282 | entries.Add(entriesToAdd);
283 | }
284 | }
285 |
286 | return entries;
287 | }
288 |
289 | ///
290 | /// Finds all entries with a given group (or multiple)
291 | ///
292 | /// Database to search for the entries.
293 | /// Groups to search for (multiple separated by ,).
294 | /// A PwObjectList with all metching entries.
295 | private static PwObjectList FindEntriesByGroup(PwDatabase sourceDb, string groups)
296 | {
297 | PwObjectList entries = new PwObjectList();
298 |
299 | foreach (string group in groups.Split(',').Select(x => x.Trim()))
300 | {
301 | // Tag and group export
302 | PwGroup groupToExport = sourceDb.RootGroup.GetFlatGroupList().FirstOrDefault(g => g.Name == group);
303 |
304 | if (groupToExport == null)
305 | {
306 | throw new ArgumentException("No group with the name of the Group-Setting found.");
307 | }
308 |
309 | PwObjectList groupEntries = groupToExport.GetEntries(true);
310 | // Prevent duplicated entries
311 | IEnumerable existingUuids = entries.Select(x => x.Uuid);
312 | List entriesToAdd = groupEntries.Where(x => !existingUuids.Contains(x.Uuid)).ToList();
313 | entries.Add(entriesToAdd);
314 | }
315 |
316 | return entries;
317 | }
318 |
319 | ///
320 | /// Finds all entries with a given tag (or multiple)
321 | ///
322 | /// Database to search for the entries.
323 | /// Tag to search for (multiple separated by ,).
324 | /// A PwObjectList with all metching entries.
325 | private static PwObjectList FindEntriesByTag(PwDatabase sourceDb, string tags)
326 | {
327 | PwObjectList entries = new PwObjectList();
328 |
329 | foreach (string tag in tags.Split(',').Select(x => x.Trim()))
330 | {
331 | PwObjectList tagEntries = new PwObjectList();
332 | sourceDb.RootGroup.FindEntriesByTag(tag, tagEntries, true);
333 | // Prevent duplicated entries
334 | IEnumerable existingUuids = entries.Select(x => x.Uuid);
335 | List entriesToAdd = tagEntries.Where(x => !existingUuids.Contains(x.Uuid)).ToList();
336 | entries.Add(entriesToAdd);
337 | }
338 |
339 | return entries;
340 | }
341 |
342 | private static void CopyEntriesAndGroups(PwDatabase sourceDb, Settings settings, PwObjectList entries,
343 | PwDatabase targetDatabase)
344 | {
345 | //If OverrideEntireGroup is set to true
346 | if (!settings.OverrideTargetDatabase && !settings.FlatExport &&
347 | settings.OverrideEntireGroup && !string.IsNullOrEmpty(settings.Group))
348 | {
349 | //Delete every entry in target database' groups to override them
350 | IEnumerable groupsToDelete = entries.Select(x => x.ParentGroup).Distinct();
351 | DeleteTargetGroupsInDatabase(groupsToDelete, targetDatabase);
352 | }
353 |
354 | foreach (PwEntry entry in entries)
355 | {
356 | // Get or create the target group in the target database (including hierarchy)
357 | PwGroup targetGroup = settings.FlatExport
358 | ? targetDatabase.RootGroup
359 | : CreateTargetGroupInDatebase(entry, targetDatabase, sourceDb);
360 |
361 | PwEntry peNew = null;
362 | if (!settings.OverrideTargetDatabase)
363 | {
364 | peNew = targetGroup.FindEntry(entry.Uuid, bSearchRecursive: false);
365 |
366 | // Check if the target entry is newer than the source entry
367 | if (settings.OverrideEntryOnlyNewer && peNew != null &&
368 | peNew.LastModificationTime > entry.LastModificationTime)
369 | {
370 | // Yes -> skip this entry
371 | continue;
372 | }
373 | }
374 |
375 | // Was no existing entry in the target database found?
376 | if (peNew == null)
377 | {
378 | // Create a new entry
379 | peNew = new PwEntry(false, false);
380 | peNew.Uuid = entry.Uuid;
381 |
382 | // Add entry to the target group in the new database
383 | targetGroup.AddEntry(peNew, true);
384 | }
385 |
386 | // Clone entry properties if ExportUserAndPassOnly is false
387 | if (!settings.ExportUserAndPassOnly)
388 | {
389 | peNew.AssignProperties(entry, false, true, true);
390 | peNew.Strings.Set(PwDefs.UrlField,
391 | FieldHelper.GetFieldWRef(entry, sourceDb, PwDefs.UrlField));
392 | peNew.Strings.Set(PwDefs.NotesField,
393 | FieldHelper.GetFieldWRef(entry, sourceDb, PwDefs.NotesField));
394 | }
395 | else
396 | {
397 | // Copy visual stuff even if settings.ExportUserAndPassOnly is set
398 | peNew.IconId = entry.IconId;
399 | peNew.CustomIconUuid = entry.CustomIconUuid;
400 | peNew.BackgroundColor = entry.BackgroundColor;
401 | }
402 |
403 | // Copy/override some supported fields with ref resolving values
404 | peNew.Strings.Set(PwDefs.TitleField,
405 | FieldHelper.GetFieldWRef(entry, sourceDb, PwDefs.TitleField));
406 | peNew.Strings.Set(PwDefs.UserNameField,
407 | FieldHelper.GetFieldWRef(entry, sourceDb, PwDefs.UserNameField));
408 | peNew.Strings.Set(PwDefs.PasswordField,
409 | FieldHelper.GetFieldWRef(entry, sourceDb, PwDefs.PasswordField));
410 |
411 | // Handle custom icon
412 | HandleCustomIcon(targetDatabase, sourceDb, entry);
413 | }
414 | }
415 |
416 | private static void SaveTargetDatabase(string targetFilePath, PwDatabase targetDatabase, bool overrideTargetDatabase)
417 | {
418 | Regex rg = new Regex(@".+://.+", RegexOptions.None, TimeSpan.FromMilliseconds(200));
419 | if (!rg.IsMatch(targetFilePath))
420 | {
421 | // local file path
422 | if (!overrideTargetDatabase && File.Exists(targetFilePath))
423 | {
424 | // Save changes to existing target database
425 | targetDatabase.Save(new NullStatusLogger());
426 | }
427 | else
428 | {
429 | // Create target folder (if not exist)
430 | string targetFolder = Path.GetDirectoryName(targetFilePath);
431 |
432 | if (targetFolder == null)
433 | {
434 | throw new ArgumentException("Can't get target folder.");
435 | }
436 |
437 | Directory.CreateDirectory(targetFolder);
438 |
439 | // Save the new database under the target path
440 | KdbxFile kdbx = new KdbxFile(targetDatabase);
441 |
442 | using (FileStream outputStream = new FileStream(targetFilePath, FileMode.Create))
443 | {
444 | kdbx.Save(outputStream, null, KdbxFormat.Default, new NullStatusLogger());
445 | }
446 | }
447 | }
448 | else
449 | {
450 | // Non local file (ftp, webdav, ...)
451 | Uri targetUrl = new Uri(targetFilePath);
452 | string[] userAndPw = targetUrl.UserInfo.Split(':');
453 | IOConnectionInfo conInfo = new IOConnectionInfo
454 | {
455 | Path = Regex.Replace(targetUrl.AbsoluteUri, @"(?<=//)[^@]+@", "", RegexOptions.None, TimeSpan.FromMilliseconds(200)),
456 | CredSaveMode = IOCredSaveMode.NoSave,
457 | UserName = userAndPw[0],
458 | Password = userAndPw[1]
459 | };
460 |
461 | targetDatabase.SaveAs(conInfo, false, new NullStatusLogger());
462 | }
463 | }
464 |
465 | private static void CopyKdfSettings(PwDatabase sourceDb, Settings settings, PwDatabase targetDatabase)
466 | {
467 | // Create a clone of the KdfParameters object. As cloning is not supportet serialize and deserialize
468 | targetDatabase.KdfParameters = KdfParameters.DeserializeExt(KdfParameters.SerializeExt(sourceDb.KdfParameters));
469 |
470 | if (Equals(targetDatabase.KdfParameters.KdfUuid, UuidAes))
471 | {
472 | // Allow override of AesKdf transformation rounds
473 | if (settings.KeyTransformationRounds != 0)
474 | {
475 | // Set keyTransformationRounds (min PwDefs.DefaultKeyEncryptionRounds)
476 | targetDatabase.KdfParameters.SetUInt64(AesKdf.ParamRounds,
477 | Math.Max(PwDefs.DefaultKeyEncryptionRounds, settings.KeyTransformationRounds));
478 | }
479 | }
480 | else if (Equals(targetDatabase.KdfParameters.KdfUuid, UuidArgon2))
481 | {
482 | // Allow override of Agon2Kdf transformation rounds
483 | if (settings.Argon2ParamIterations != 0)
484 | {
485 | // Set paramIterations (min default value == 2)
486 | targetDatabase.KdfParameters.SetUInt64(Argon2Kdf.ParamIterations,
487 | Math.Max(2, settings.Argon2ParamIterations));
488 | }
489 |
490 | // Allow override of Agon2Kdf memory setting
491 | if (settings.Argon2ParamMemory != 0)
492 | {
493 | // Set ParamMemory (min default value == 1048576 == 1 MB)
494 | targetDatabase.KdfParameters.SetUInt64(Argon2Kdf.ParamMemory,
495 | Math.Max(1048576, settings.Argon2ParamMemory));
496 | }
497 |
498 | // Allow override of Agon2Kdf parallelism setting
499 | if (settings.Argon2ParamParallelism != 0)
500 | {
501 | // Set ParamParallelism (min default value == 2 MB)
502 | targetDatabase.KdfParameters.SetUInt32(Argon2Kdf.ParamParallelism, settings.Argon2ParamParallelism);
503 | }
504 | }
505 | }
506 |
507 | private static void CopyDatabaseSettings(PwDatabase sourceDb, PwDatabase targetDatabase)
508 | {
509 | targetDatabase.Color = sourceDb.Color;
510 | targetDatabase.Compression = sourceDb.Compression;
511 | targetDatabase.DataCipherUuid = sourceDb.DataCipherUuid;
512 | targetDatabase.DefaultUserName = sourceDb.DefaultUserName;
513 | targetDatabase.Description = sourceDb.Description;
514 | targetDatabase.HistoryMaxItems = sourceDb.HistoryMaxItems;
515 | targetDatabase.HistoryMaxSize = sourceDb.HistoryMaxSize;
516 | targetDatabase.MaintenanceHistoryDays = sourceDb.MaintenanceHistoryDays;
517 | targetDatabase.MasterKeyChangeForce = sourceDb.MasterKeyChangeForce;
518 | targetDatabase.MasterKeyChangeRec = sourceDb.MasterKeyChangeRec;
519 | targetDatabase.Name = sourceDb.Name;
520 | targetDatabase.RecycleBinEnabled = sourceDb.RecycleBinEnabled;
521 | }
522 |
523 | private static PwDatabase CreateTargetDatabase(PwDatabase sourceDb, Settings settings, CompositeKey key, ref string targetFilePath)
524 | {
525 | Regex rg = new Regex(@".+://.+", RegexOptions.None, TimeSpan.FromMilliseconds(200));
526 | if (rg.IsMatch(targetFilePath))
527 | {
528 | // Non local file (ftp, webdav, ...)
529 | // Create a new database
530 | PwDatabase targetDatabaseForUri = new PwDatabase();
531 |
532 | // Apply the created key to the new database
533 | targetDatabaseForUri.New(new IOConnectionInfo(), key);
534 |
535 | return targetDatabaseForUri;
536 | }
537 |
538 | // Default to same folder as sourceDb for target if no directory is specified
539 | if (!Path.IsPathRooted(targetFilePath))
540 | {
541 | string sourceDbPath = Path.GetDirectoryName(sourceDb.IOConnectionInfo.Path);
542 | if (sourceDbPath != null)
543 | {
544 | targetFilePath = Path.Combine(sourceDbPath, targetFilePath);
545 | }
546 | }
547 |
548 | // Create a new database
549 | PwDatabase targetDatabase = new PwDatabase();
550 |
551 | if (!settings.OverrideTargetDatabase && File.Exists(targetFilePath))
552 | {
553 | // Connect the database object to the existing database
554 | targetDatabase.Open(new IOConnectionInfo()
555 | {
556 | Path = targetFilePath
557 | }, key, new NullStatusLogger());
558 | }
559 | else
560 | {
561 | // Apply the created key to the new database
562 | targetDatabase.New(new IOConnectionInfo(), key);
563 | }
564 |
565 | return targetDatabase;
566 | }
567 |
568 | private static CompositeKey CreateCompositeKey(Settings settings)
569 | {
570 | CompositeKey key = new CompositeKey();
571 |
572 | bool hasPassword = false;
573 | bool hasKeyFile = false;
574 |
575 | if (!settings.Password.IsEmpty)
576 | {
577 | byte[] passwordByteArray = settings.Password.ReadUtf8();
578 | hasPassword = KeyHelper.AddPasswordToKey(passwordByteArray, key);
579 | MemUtil.ZeroByteArray(passwordByteArray);
580 | }
581 |
582 | // Load a keyfile for the target database if requested (and add it to the key)
583 | if (!string.IsNullOrEmpty(settings.KeyFilePath))
584 | {
585 | hasKeyFile = KeyHelper.AddKeyfileToKey(settings.KeyFilePath, key, ConnectionInfo);
586 | }
587 |
588 | // Check if at least a password or a keyfile have been added to the key object
589 | if (!hasPassword && !hasKeyFile)
590 | {
591 | // Fail if not
592 | throw new InvalidOperationException("For the target database at least a password or a keyfile is required.");
593 | }
594 |
595 | return key;
596 | }
597 |
598 | ///
599 | /// Get or create the target group of an entry in the target database (including hierarchy).
600 | ///
601 | /// An entry wich is located in the folder with the target structure.
602 | /// The target database in which the folder structure should be created.
603 | /// The source database from which the folder properties should be taken.
604 | /// The target folder in the target database.
605 | private static PwGroup CreateTargetGroupInDatebase(PwEntry entry, PwDatabase targetDatabase, PwDatabase sourceDatabase)
606 | {
607 | // Collect all group names from the entry up to the root group
608 | PwGroup group = entry.ParentGroup;
609 | List list = new List();
610 |
611 | while (group != null)
612 | {
613 | list.Add(group.Uuid);
614 | group = group.ParentGroup;
615 | }
616 |
617 | // Remove root group (we already changed the root group name)
618 | list.RemoveAt(list.Count - 1);
619 | // groups are in a bottom-up oder -> reverse to get top-down
620 | list.Reverse();
621 |
622 | // Create group structure for the new entry (copying group properties)
623 | PwGroup lastGroup = targetDatabase.RootGroup;
624 | foreach (PwUuid id in list)
625 | {
626 | // Does the target group already exist?
627 | PwGroup newGroup = lastGroup.FindGroup(id, false);
628 | if (newGroup != null)
629 | {
630 | lastGroup = newGroup;
631 | continue;
632 | }
633 |
634 | // Get the source group
635 | PwGroup sourceGroup = sourceDatabase.RootGroup.FindGroup(id, true);
636 |
637 | // Create a new group and assign all properties from the source group
638 | newGroup = new PwGroup();
639 | newGroup.AssignProperties(sourceGroup, false, true);
640 | HandleCustomIcon(targetDatabase, sourceDatabase, sourceGroup);
641 |
642 | // Add the new group at the right position in the target database
643 | lastGroup.AddGroup(newGroup, true);
644 |
645 | lastGroup = newGroup;
646 | }
647 |
648 | // Return the target folder (leaf folder)
649 | return lastGroup;
650 | }
651 |
652 | ///
653 | /// Delete every entry in the target group.
654 | ///
655 | /// Collection of groups which counterparts should be deleted in the target database.
656 | /// The target database in which the folder structure should be created.
657 | private static void DeleteTargetGroupsInDatabase(IEnumerable sourceGroups, PwDatabase targetDatabase)
658 | {
659 | // Get the target groups ID based
660 | foreach (PwGroup targetGroup in sourceGroups.Select(x => targetDatabase.RootGroup.FindGroup(x.Uuid, false)))
661 | {
662 | // If group exists in target database, delete its entries, otherwise show a warning
663 | if (targetGroup != null)
664 | {
665 | targetGroup.DeleteAllObjects(targetDatabase);
666 | }
667 | }
668 | }
669 | ///
670 | /// Copies the custom icons required for this group to the target database.
671 | ///
672 | /// The target database where to add the icons.
673 | /// The source database where to get the icons from.
674 | /// The source group which icon should be copied (if it is custom).
675 | private static void HandleCustomIcon(PwDatabase targetDatabase, PwDatabase sourceDatabase, PwGroup sourceGroup)
676 | {
677 | // Does the group not use a custom icon or is it already in the target database
678 | if (sourceGroup.CustomIconUuid.Equals(PwUuid.Zero) ||
679 | targetDatabase.GetCustomIconIndex(sourceGroup.CustomIconUuid) != -1)
680 | {
681 | return;
682 | }
683 |
684 | // Check if the custom icon really is in the source database
685 | int iconIndex = sourceDatabase.GetCustomIconIndex(sourceGroup.CustomIconUuid);
686 | if (iconIndex < 0 || iconIndex > sourceDatabase.CustomIcons.Count - 1)
687 | {
688 | MessageService.ShowWarning("Can't locate custom icon (" + sourceGroup.CustomIconUuid.ToHexString() +
689 | ") for group " + sourceGroup.Name);
690 | }
691 |
692 | // Get the custom icon from the source database
693 | PwCustomIcon customIcon = sourceDatabase.CustomIcons[iconIndex];
694 |
695 | // Copy the custom icon to the target database
696 | targetDatabase.CustomIcons.Add(customIcon);
697 | }
698 |
699 | ///
700 | /// Copies the custom icons required for this group to the target database.
701 | ///
702 | /// The target database where to add the icons.
703 | /// The source database where to get the icons from.
704 | /// The entry which icon should be copied (if it is custom).
705 | private static void HandleCustomIcon(PwDatabase targetDatabase, PwDatabase sourceDb, PwEntry entry)
706 | {
707 | // Does the entry not use a custom icon or is it already in the target database
708 | if (entry.CustomIconUuid.Equals(PwUuid.Zero) ||
709 | targetDatabase.GetCustomIconIndex(entry.CustomIconUuid) != -1)
710 | {
711 | return;
712 | }
713 |
714 | // Check if the custom icon really is in the source database
715 | int iconIndex = sourceDb.GetCustomIconIndex(entry.CustomIconUuid);
716 | if (iconIndex < 0 || iconIndex > sourceDb.CustomIcons.Count - 1)
717 | {
718 | MessageService.ShowWarning("Can't locate custom icon (" + entry.CustomIconUuid.ToHexString() +
719 | ") for entry " + entry.Strings.ReadSafe("Title"));
720 | }
721 |
722 | // Get the custom icon from the source database
723 | PwCustomIcon customIcon = sourceDb.CustomIcons[iconIndex];
724 |
725 | // Copy the custom icon to the target database
726 | targetDatabase.CustomIcons.Add(customIcon);
727 | }
728 | }
729 | }
730 |
--------------------------------------------------------------------------------