├── 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 | [![Build Status](https://lukeiam.visualstudio.com/KeePassSubsetExport/_apis/build/status/KeePassSubsetExport-Build "View build on VisualStudio online")](https://lukeiam.visualstudio.com/KeePassSubsetExport/_build/latest?definitionId=1) 5 | [![Quality Status](https://sonarcloud.io/api/project_badges/measure?project=KeePassSubsetExport&metric=alert_status "View project on SonarCloud")](https://sonarcloud.io/dashboard?id=KeePassSubsetExport) 6 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=KeePassSubsetExport&metric=coverage "View coverage on SonarCloud")](https://sonarcloud.io/component_measures?id=KeePassSubsetExport&metric=coverage) 7 | [![Latest release](https://img.shields.io/github/release/lukeiam/KeePassSubsetExport.svg?label=latest%20release)](https://github.com/lukeIam/KeePassSubsetExport/releases/latest) 8 | [![Github All Releases](https://img.shields.io/github/downloads/lukeIam/KeePassSubsetExport/total.svg)](https://github.com/lukeIam/KeePassSubsetExport/releases) 9 | [![License](https://img.shields.io/github/license/lukeIam/KeePassSubsetExport.svg)](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 | ![create](https://user-images.githubusercontent.com/5115160/38439682-da51a266-39de-11e8-9cc4-744d5a3f0dae.png) 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 | --------------------------------------------------------------------------------