From bcbd5ac2ae766b87b5aec5ed786d9c936d4a4ae1 Mon Sep 17 00:00:00 2001 From: rookiestyle Date: Tue, 24 Aug 2021 19:30:14 +0200 Subject: [PATCH] Initial commit --- .github/FUNDING.yml | 3 + .github/ISSUE_TEMPLATE/bug_report.md | 36 ++ .github/ISSUE_TEMPLATE/feature_request.md | 21 + .github/ISSUE_TEMPLATE/translation.md | 17 + .gitignore | 350 +++++++++++++++ LockAssist.cs | 254 +++++++++++ LockAssist.csproj | 76 ++++ LockAssist.sln | 67 +++ OptionsForm.Designer.cs | 371 ++++++++++++++++ OptionsForm.cs | 153 +++++++ PluginTools.cs | 321 +++++++++++++ PluginTranslation.cs | 246 ++++++++++ Properties/AssemblyInfo.cs | 36 ++ QuickUnlock.cs | 220 +++++++++ QuickUnlockKeyProv.cs | 231 ++++++++++ README.md | 51 +++ Translations/LockAssist.de.language.xml | 111 +++++ Translations/LockAssist.template.language.xml | 105 +++++ UnlockForm.Designer.cs | 135 ++++++ UnlockForm.cs | 54 +++ images/LockAssist - options.png | Bin 0 -> 138774 bytes images/LockAssist - quick unlock.png | Bin 0 -> 40845 bytes plgxcreate.cmd | 40 ++ plgxexclude.txt | 8 + src/LockAssist.cs | 101 +++++ src/LockAssist.csproj | 93 ++++ src/LockAssistConfig.cs | 128 ++++++ src/OptionsForm.Designer.cs | 239 ++++++++++ src/OptionsForm.cs | 137 ++++++ src/PluginTranslation.cs | 240 ++++++++++ src/Properties/AssemblyInfo.cs | 36 ++ src/QuickUnlock.cs | 263 +++++++++++ src/QuickUnlockKeyProv.cs | 271 +++++++++++ src/UnlockForm.Designer.cs | 127 ++++++ src/UnlockForm.cs | 44 ++ src/Utilities/Debug.cs | 420 ++++++++++++++++++ src/Utilities/EventHelper.cs | 374 ++++++++++++++++ src/Utilities/Tools_Controls.cs | 63 +++ src/Utilities/Tools_Main.cs | 93 ++++ src/Utilities/Tools_Options.cs | 375 ++++++++++++++++ translationcopy.cmd | 10 + version.info | 4 + 42 files changed, 5924 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/translation.md create mode 100644 .gitignore create mode 100644 LockAssist.cs create mode 100644 LockAssist.csproj create mode 100644 LockAssist.sln create mode 100644 OptionsForm.Designer.cs create mode 100644 OptionsForm.cs create mode 100644 PluginTools.cs create mode 100644 PluginTranslation.cs create mode 100644 Properties/AssemblyInfo.cs create mode 100644 QuickUnlock.cs create mode 100644 QuickUnlockKeyProv.cs create mode 100644 README.md create mode 100644 Translations/LockAssist.de.language.xml create mode 100644 Translations/LockAssist.template.language.xml create mode 100644 UnlockForm.Designer.cs create mode 100644 UnlockForm.cs create mode 100644 images/LockAssist - options.png create mode 100644 images/LockAssist - quick unlock.png create mode 100644 plgxcreate.cmd create mode 100644 plgxexclude.txt create mode 100644 src/LockAssist.cs create mode 100644 src/LockAssist.csproj create mode 100644 src/LockAssistConfig.cs create mode 100644 src/OptionsForm.Designer.cs create mode 100644 src/OptionsForm.cs create mode 100644 src/PluginTranslation.cs create mode 100644 src/Properties/AssemblyInfo.cs create mode 100644 src/QuickUnlock.cs create mode 100644 src/QuickUnlockKeyProv.cs create mode 100644 src/UnlockForm.Designer.cs create mode 100644 src/UnlockForm.cs create mode 100644 src/Utilities/Debug.cs create mode 100644 src/Utilities/EventHelper.cs create mode 100644 src/Utilities/Tools_Controls.cs create mode 100644 src/Utilities/Tools_Main.cs create mode 100644 src/Utilities/Tools_Options.cs create mode 100644 translationcopy.cmd create mode 100644 version.info diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..e02419e --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +custom: http://paypal.com/cgi-bin/webscr?cmd=_donations&business=rookiestyle%40gmx%2enet¤cy_code=EUR&lc=en_DE&item_name=LockAssist diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..4c06a48 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,36 @@ +--- +name: Bug report +about: Found a bug? Report it to get it fixed +title: '' +labels: bug +assignees: '' + +--- + +## Overview +[TIP]: # (DO NOT include screenshots of your actual database) +[TIP]: # (DO NOT post sensitive data of any kind) +[NOTE]: # (Give a BRIEF summary about your problem) + + +## Steps to Reproduce +[NOTE]: # (Provide a simple set of steps to reproduce this bug) +1. +2. +3. + +## Expected Behavior +[NOTE]: # (Describe how you expect the plugin to behave) + + +## Actual Behavior +[NOTE]: # (Describe how the plugin actually behaves) + + +## Context +[NOTE]: # (Provide any additional information you may have) +OS: +KeePass Version: +Plugin Version: + +[NOTE]: # (If possible, please attach a debug file. Have a look at the wiki for details.) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..173b389 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,21 @@ +--- +name: Feature request +about: Suggest an idea for this plugin +title: '' +labels: enhancement +assignees: '' + +--- + +## Summary +[TIP]: # (DO NOT include screenshots of your actual database) +[TIP]: # (DO NOT post sensitive data of any kind) +[NOTE]: # (Provide a brief overview of what the new feature is all about) + + +## Added value +[NOTE]: # (Describe how you and other users would benefit from this feature) + + +## Example +[NOTE]: # (Show a picture or a mock-up if applicable) diff --git a/.github/ISSUE_TEMPLATE/translation.md b/.github/ISSUE_TEMPLATE/translation.md new file mode 100644 index 0000000..0f1a671 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/translation.md @@ -0,0 +1,17 @@ +--- +name: Translation +about: New or updated translation +title: 'Translation: ' +labels: translation +assignees: '' + +--- + +[NOTE]: # (Please do not forget to attach the translation file) +Language: +- [ ] New translation +- [ ] Update existing translation +- [ ] If updated: Only added missing Texts +- [ ] All relevant texts translated + +[NOTE]: # (A text is relevant if the standard text in english language is NOT sufficient) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dfcfd56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,350 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ diff --git a/LockAssist.cs b/LockAssist.cs new file mode 100644 index 0000000..f2bcf58 --- /dev/null +++ b/LockAssist.cs @@ -0,0 +1,254 @@ +using KeePass.Plugins; +using KeePassLib; +using System.Collections.Generic; +using System.Windows.Forms; + +using PluginTranslation; +using PluginTools; +using System.Drawing; + +namespace LockAssist +{ + public class LockAssistOptions + { + public IPluginHost host; + public bool FirstTime = false; + public bool QUActive = false; + public bool DBSpecific = false; + public bool UsePassword = true; + public int Length = 4; + public bool FromEnd = true; + public bool SLMinimize = false; + public int SLSeconds = 60; + public bool SLIdle = true; + public bool SLIdleActive { get { return SLIdle && (SLSeconds > 0); } } + public List SLExcludeForms = new List() + { + "AboutForm", "AutoTypeCtxForm", "CharPickerForm", "ColumnsForm", + "HelpSourceForm", "KeyPromptForm", "LanguageForm", "PluginsForm", + "PwGeneratorForm", "UpdateCheckForm" + }; + public bool LockWorkspace = false; + + public bool ConfigChanged(LockAssistOptions comp, bool CheckDBSpecific) + { + if (QUActive != comp.QUActive) return true; + if (CheckDBSpecific && (DBSpecific != comp.DBSpecific)) return true; + if (UsePassword != comp.UsePassword) return true; + if (Length != comp.Length) return true; + if (FromEnd != comp.FromEnd) return true; + if (SLMinimize != comp.SLMinimize) return true; + if (SLIdle != comp.SLIdle) return true; + if (SLSeconds != comp.SLSeconds) return true; + if (LockWorkspace != comp.LockWorkspace) return true; + return false; + } + + public bool CopyFrom(LockAssistOptions NewOptions) + { + bool SwitchToNoDBSpecific = DBSpecific && !NewOptions.DBSpecific; + DBSpecific = NewOptions.DBSpecific; + QUActive = NewOptions.QUActive; + UsePassword = NewOptions.UsePassword; + Length = NewOptions.Length; + FromEnd = NewOptions.FromEnd; + SLMinimize = NewOptions.SLMinimize; + SLIdle = NewOptions.SLIdle; + SLSeconds = NewOptions.SLSeconds; + LockWorkspace = NewOptions.LockWorkspace; + return SwitchToNoDBSpecific; + } + + public void ReadConfig() + { + ReadConfig(host.Database); + } + + public void ReadConfig(PwDatabase db) + { + FirstTime = host.CustomConfig.GetBool("LockAssist.FirstTime", true); + DBSpecific = (db != null) && (db.IsOpen) && db.CustomData.Exists("LockAssist.UsePassword"); + if (DBSpecific) + { + QUActive = db.CustomData.Get("LockAssist.Active") == "true"; + UsePassword = db.CustomData.Get("LockAssist.UsePassword") == "true"; + if (!int.TryParse(db.CustomData.Get("LockAssist.KeyLength"), out Length)) Length = 4; + FromEnd = db.CustomData.Get("LockAssist.KeyFromEnd") == "false"; + } + else + { + QUActive = host.CustomConfig.GetBool("LockAssist.Active", false); + UsePassword = host.CustomConfig.GetBool("LockAssist.UsePassword", true); + Length = (int)host.CustomConfig.GetLong("LockAssist.KeyLength", 4); + FromEnd = host.CustomConfig.GetBool("LockAssist.KeyFromEnd", true); + } + + SLIdle = host.CustomConfig.GetBool("LockAssist.SoftlockMode.HideOnIdle", true); + SLSeconds = (int)host.CustomConfig.GetLong("LockAssist.SoftlockMode.Seconds", 0); + SLMinimize = host.CustomConfig.GetBool("LockAssist.SoftlockMode.HideOnMinimize", false); + string excludeForms = host.CustomConfig.GetString("LockAssist.SoftlockMode.ExcludeForms", ""); + if (!string.IsNullOrEmpty(excludeForms)) + { + SLExcludeForms.Clear(); + SLExcludeForms.AddRange(excludeForms.Split(new char[1] { ',' })); + } + LockWorkspace = host.CustomConfig.GetBool("LockAssist.LockWorkspace", false); + } + + public void WriteConfig() + { + WriteConfig(host.Database); + } + + public void WriteConfig(PwDatabase db) + { + if (DBSpecific) + { + host.Database.CustomData.Set("LockAssist.Active", QUActive ? "true" : "false"); + host.Database.CustomData.Set("LockAssist.UsePassword", UsePassword ? "true" : "false"); + host.Database.CustomData.Set("LockAssist.KeyLength", Length.ToString()); + host.Database.CustomData.Set("LockAssist.KeyFromEnd", FromEnd ? "true" : "false"); + } + else + { + host.CustomConfig.SetBool("LockAssist.Active", QUActive); + host.CustomConfig.SetBool("LockAssist.UsePassword", UsePassword); + host.CustomConfig.SetLong("LockAssist.KeyLength", Length); + host.CustomConfig.SetBool("LockAssist.KeyFromEnd", FromEnd); + DeleteDBConfig(); + } + host.CustomConfig.SetBool("LockAssist.SoftlockMode.HideOnIdle", SLIdle); + host.CustomConfig.SetLong("LockAssist.SoftlockMode.Seconds", SLSeconds); + host.CustomConfig.SetBool("LockAssist.SoftlockMode.HideOnMinimize", SLMinimize); + string excludeForms = string.Empty; + foreach (string form in SLExcludeForms) + excludeForms += "," + form; + excludeForms = excludeForms.Substring(1); + host.CustomConfig.SetString("LockAssist.SoftlockMode.ExcludeForms", excludeForms); + host.CustomConfig.SetBool("LockAssist.LockWorkspace", LockWorkspace); + } + + public void DeleteDBConfig() + { + bool deleted = host.Database.CustomData.Remove("LockAssist.Active"); + deleted |= deleted = host.Database.CustomData.Remove("LockAssist.UsePassword"); + deleted |= host.Database.CustomData.Remove("LockAssist.KeyLength"); + deleted |= host.Database.CustomData.Remove("LockAssist.KeyFromEnd"); + if (deleted) + { + host.MainWindow.UpdateUI(false, null, false, null, false, null, true); + host.Database.Modified = true; + } + } + } + + public partial class LockAssistExt : Plugin, IMessageFilter + { + private static IPluginHost m_host = null; + private QuickUnlockKeyProv m_kp = null; + private ToolStripMenuItem m_menu = null; + private static bool Terminated { get { return m_host == null; } } + + private LockAssistOptions m_options; + + public override bool Initialize(IPluginHost host) + { + if (m_host != null) Terminate(); + m_host = host; + + PluginTranslate.Init(this, KeePass.Program.Translation.Properties.Iso6391Code); + Tools.DefaultCaption = PluginTranslate.PluginName; + + m_menu = new ToolStripMenuItem(); + m_menu.Text = PluginTranslate.PluginName + "..."; + m_menu.Click += (o, e) => Tools.ShowOptions(); + m_menu.Image = m_host.MainWindow.ClientIcons.Images[51]; + m_host.MainWindow.ToolsMenu.DropDownItems.Add(m_menu); + + Tools.OptionsFormShown += OptionsFormShown; + Tools.OptionsFormClosed += OptionsFormClosed; + m_options = new LockAssistOptions(); + m_options.host = m_host; + m_options.ReadConfig(); + + QuickUnlock_Init(); + Softlock_Init(); + LockWorkspace_Init(); + + return true; + } + + public override void Terminate() + { + Application.RemoveMessageFilter(this); + Tools.OptionsFormShown -= OptionsFormShown; + Tools.OptionsFormClosed -= OptionsFormClosed; + + QuickUnlock_Terminate(); + Softlock_Terminate(); + LockWorkspace_Terminate(); + + m_host.MainWindow.ToolsMenu.DropDownItems.Remove(m_menu); + m_host = null; + } + + #region Options + private void OptionsFormShown(object sender, Tools.OptionsFormsEventArgs e) + { + m_options.ReadConfig(); + OptionsForm options = new OptionsForm(); + options.SetOptions(m_options); + Tools.AddPluginToOptionsForm(this, options); + } + + private void OptionsFormClosed(object sender, Tools.OptionsFormsEventArgs e) + { + if (e.form.DialogResult != DialogResult.OK) return; + bool shown = false; + OptionsForm options = (OptionsForm)Tools.GetPluginFromOptions(this, out shown); + if (!shown) return; + LockAssistOptions NewOptions = options.GetOptions(); + bool changedConfig = m_options.ConfigChanged(NewOptions, false); + bool changedConfigTotal = m_options.ConfigChanged(NewOptions, true); + if (m_SLHide) + { + if (changedConfig) Tools.ShowError(PluginTranslate.OptionsNotSavedSoftlock); + return; + } + if (m_options.LockWorkspace != NewOptions.LockWorkspace) + ActivateNewLockWorkspace(NewOptions.LockWorkspace); + bool SwitchToNoDBSpecific = m_options.CopyFrom(NewOptions); + CheckSoftlockMode(); + if (SwitchToNoDBSpecific) + { + if (Tools.AskYesNo(PluginTranslate.OptionsSwitchDBToGeneral) == DialogResult.Yes) + { + //Remove DB specific configuration + m_options.DeleteDBConfig(); + m_options.ReadConfig(); + } + else + { + //Make current configuration the new global configuration + m_options.WriteConfig(); + } + } + else + m_options.WriteConfig(); + CheckSoftlockMode(); + if ((changedConfigTotal && m_options.DBSpecific) || SwitchToNoDBSpecific) + { + m_host.MainWindow.UpdateUI(false, null, false, null, false, null, true); + m_host.Database.Modified = true; + } + } + #endregion + + public override string UpdateUrl + { + get { return "https://firebasestorage.googleapis.com/v0/b/rookiestyle-43398.appspot.com/o/versioninfo.txt?alt=media&token=89da60c0-f331-4334-94bb-0dccf617818f"; } + } + + public override Image SmallIcon { get { return m_menu.Image; } } + } +} \ No newline at end of file diff --git a/LockAssist.csproj b/LockAssist.csproj new file mode 100644 index 0000000..c550771 --- /dev/null +++ b/LockAssist.csproj @@ -0,0 +1,76 @@ + + + + + Release + AnyCPU + {4712D887-6685-4CB1-B67B-E981119FE3D2} + Library + Properties + LockAssist + LockAssist + v2.0 + 512 + true + + + + true + full + false + ..\..\KeePass\source\Build\KeePass\Debug\Plugins\ + TRACE;DEBUG + prompt + 4 + true + false + + + pdbonly + true + ..\..\KeePass\source\Build\KeePass\Release\Plugins\ + TRACE + prompt + 4 + false + + + + + + + + + + + + + + + Form + + + UnlockForm.cs + + + + + + UserControl + + + OptionsForm.cs + + + + + {10938016-dee2-4a25-9a5a-8fd3444379ca} + KeePass + + + + + call "$(ProjectDir)plgxcreate.cmd" +call "$(ProjectDir)translationcopy.cmd" + + \ No newline at end of file diff --git a/LockAssist.sln b/LockAssist.sln new file mode 100644 index 0000000..c702dc7 --- /dev/null +++ b/LockAssist.sln @@ -0,0 +1,67 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28010.2048 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LockAssist", "src\LockAssist.csproj", "{4712D887-6685-4CB1-B67B-E981119FE3D2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KeePass", "..\_KeePass_Source\KeePass\KeePass.csproj", "{10938016-DEE2-4A25-9A5A-8FD3444379CA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E2ACAE5D-E1C7-459F-B770-A5A8AEFE0BEB}" + ProjectSection(SolutionItems) = preProject + plgxcreate.cmd = plgxcreate.cmd + version.info = version.info + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|Mixed Platforms = Debug|Mixed Platforms + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + ReleasePlgx|Any CPU = ReleasePlgx|Any CPU + ReleasePlgx|Mixed Platforms = ReleasePlgx|Mixed Platforms + ReleasePlgx|Win32 = ReleasePlgx|Win32 + ReleasePlgx|x64 = ReleasePlgx|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4712D887-6685-4CB1-B67B-E981119FE3D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4712D887-6685-4CB1-B67B-E981119FE3D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4712D887-6685-4CB1-B67B-E981119FE3D2}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {4712D887-6685-4CB1-B67B-E981119FE3D2}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {4712D887-6685-4CB1-B67B-E981119FE3D2}.Debug|Win32.ActiveCfg = Debug|Any CPU + {4712D887-6685-4CB1-B67B-E981119FE3D2}.Debug|Win32.Build.0 = Debug|Any CPU + {4712D887-6685-4CB1-B67B-E981119FE3D2}.Debug|x64.ActiveCfg = Debug|Any CPU + {4712D887-6685-4CB1-B67B-E981119FE3D2}.Debug|x64.Build.0 = Debug|Any CPU + {4712D887-6685-4CB1-B67B-E981119FE3D2}.ReleasePlgx|Any CPU.ActiveCfg = ReleasePlgx|Any CPU + {4712D887-6685-4CB1-B67B-E981119FE3D2}.ReleasePlgx|Any CPU.Build.0 = ReleasePlgx|Any CPU + {4712D887-6685-4CB1-B67B-E981119FE3D2}.ReleasePlgx|Mixed Platforms.ActiveCfg = ReleasePlgx|Any CPU + {4712D887-6685-4CB1-B67B-E981119FE3D2}.ReleasePlgx|Mixed Platforms.Build.0 = ReleasePlgx|Any CPU + {4712D887-6685-4CB1-B67B-E981119FE3D2}.ReleasePlgx|Win32.ActiveCfg = ReleasePlgx|Any CPU + {4712D887-6685-4CB1-B67B-E981119FE3D2}.ReleasePlgx|Win32.Build.0 = ReleasePlgx|Any CPU + {4712D887-6685-4CB1-B67B-E981119FE3D2}.ReleasePlgx|x64.ActiveCfg = ReleasePlgx|Any CPU + {4712D887-6685-4CB1-B67B-E981119FE3D2}.ReleasePlgx|x64.Build.0 = ReleasePlgx|Any CPU + {10938016-DEE2-4A25-9A5A-8FD3444379CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10938016-DEE2-4A25-9A5A-8FD3444379CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10938016-DEE2-4A25-9A5A-8FD3444379CA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {10938016-DEE2-4A25-9A5A-8FD3444379CA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {10938016-DEE2-4A25-9A5A-8FD3444379CA}.Debug|Win32.ActiveCfg = Debug|Any CPU + {10938016-DEE2-4A25-9A5A-8FD3444379CA}.Debug|Win32.Build.0 = Debug|Any CPU + {10938016-DEE2-4A25-9A5A-8FD3444379CA}.Debug|x64.ActiveCfg = Debug|Any CPU + {10938016-DEE2-4A25-9A5A-8FD3444379CA}.Debug|x64.Build.0 = Debug|Any CPU + {10938016-DEE2-4A25-9A5A-8FD3444379CA}.ReleasePlgx|Any CPU.ActiveCfg = Release|Any CPU + {10938016-DEE2-4A25-9A5A-8FD3444379CA}.ReleasePlgx|Any CPU.Build.0 = Release|Any CPU + {10938016-DEE2-4A25-9A5A-8FD3444379CA}.ReleasePlgx|Mixed Platforms.ActiveCfg = Release|Any CPU + {10938016-DEE2-4A25-9A5A-8FD3444379CA}.ReleasePlgx|Mixed Platforms.Build.0 = Release|Any CPU + {10938016-DEE2-4A25-9A5A-8FD3444379CA}.ReleasePlgx|Win32.ActiveCfg = Release|Any CPU + {10938016-DEE2-4A25-9A5A-8FD3444379CA}.ReleasePlgx|Win32.Build.0 = Release|Any CPU + {10938016-DEE2-4A25-9A5A-8FD3444379CA}.ReleasePlgx|x64.ActiveCfg = Release|Any CPU + {10938016-DEE2-4A25-9A5A-8FD3444379CA}.ReleasePlgx|x64.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6D5548B5-0461-4635-862A-3283E03A5485} + EndGlobalSection +EndGlobal diff --git a/OptionsForm.Designer.cs b/OptionsForm.Designer.cs new file mode 100644 index 0000000..c12502a --- /dev/null +++ b/OptionsForm.Designer.cs @@ -0,0 +1,371 @@ +namespace LockAssist +{ + partial class OptionsForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.tabControl1 = new System.Windows.Forms.TabControl(); + this.tabQuickUnlock = new System.Windows.Forms.TabPage(); + this.cbPINDBSpecific = new System.Windows.Forms.CheckBox(); + this.tbModeExplain = new System.Windows.Forms.TextBox(); + this.panel2 = new System.Windows.Forms.Panel(); + this.cbActive = new System.Windows.Forms.CheckBox(); + this.lQUPINLength = new System.Windows.Forms.Label(); + this.lQUMode = new System.Windows.Forms.Label(); + this.rbPINEnd = new System.Windows.Forms.RadioButton(); + this.rbPINFront = new System.Windows.Forms.RadioButton(); + this.tbPINLength = new System.Windows.Forms.TextBox(); + this.cbPINMode = new System.Windows.Forms.ComboBox(); + this.tabSoftlock = new System.Windows.Forms.TabPage(); + this.tbSoftlockExplain = new System.Windows.Forms.TextBox(); + this.panel1 = new System.Windows.Forms.Panel(); + this.cbSLIdle = new System.Windows.Forms.CheckBox(); + this.cbSLOnMinimize = new System.Windows.Forms.CheckBox(); + this.tSLIdleSeconds = new System.Windows.Forms.TextBox(); + this.tabAdditional = new System.Windows.Forms.TabPage(); + this.tbLockWorkspace = new System.Windows.Forms.TextBox(); + this.panel3 = new System.Windows.Forms.Panel(); + this.cbLockWorkspace = new System.Windows.Forms.CheckBox(); + this.tabControl1.SuspendLayout(); + this.tabQuickUnlock.SuspendLayout(); + this.panel2.SuspendLayout(); + this.tabSoftlock.SuspendLayout(); + this.panel1.SuspendLayout(); + this.tabAdditional.SuspendLayout(); + this.panel3.SuspendLayout(); + this.SuspendLayout(); + // + // tabControl1 + // + this.tabControl1.Controls.Add(this.tabQuickUnlock); + this.tabControl1.Controls.Add(this.tabSoftlock); + this.tabControl1.Controls.Add(this.tabAdditional); + this.tabControl1.Dock = System.Windows.Forms.DockStyle.Fill; + this.tabControl1.Location = new System.Drawing.Point(0, 0); + this.tabControl1.Name = "tabControl1"; + this.tabControl1.SelectedIndex = 0; + this.tabControl1.Size = new System.Drawing.Size(480, 450); + this.tabControl1.TabIndex = 6; + // + // tabQuickUnlock + // + this.tabQuickUnlock.BackColor = System.Drawing.Color.Transparent; + this.tabQuickUnlock.Controls.Add(this.cbPINDBSpecific); + this.tabQuickUnlock.Controls.Add(this.tbModeExplain); + this.tabQuickUnlock.Controls.Add(this.panel2); + this.tabQuickUnlock.Location = new System.Drawing.Point(4, 29); + this.tabQuickUnlock.Name = "tabQuickUnlock"; + this.tabQuickUnlock.Padding = new System.Windows.Forms.Padding(3); + this.tabQuickUnlock.Size = new System.Drawing.Size(472, 417); + this.tabQuickUnlock.TabIndex = 0; + this.tabQuickUnlock.Text = "Quick Unlock settings"; + this.tabQuickUnlock.UseVisualStyleBackColor = true; + // + // cbPINDBSpecific + // + this.cbPINDBSpecific.AutoSize = true; + this.cbPINDBSpecific.Location = new System.Drawing.Point(11, 316); + this.cbPINDBSpecific.Name = "cbPINDBSpecific"; + this.cbPINDBSpecific.Size = new System.Drawing.Size(205, 24); + this.cbPINDBSpecific.TabIndex = 5; + this.cbPINDBSpecific.Text = "Settings are DB specific"; + this.cbPINDBSpecific.TextAlign = System.Drawing.ContentAlignment.BottomCenter; + this.cbPINDBSpecific.UseVisualStyleBackColor = true; + // + // tbModeExplain + // + this.tbModeExplain.Dock = System.Windows.Forms.DockStyle.Top; + this.tbModeExplain.ForeColor = System.Drawing.SystemColors.WindowText; + this.tbModeExplain.Location = new System.Drawing.Point(3, 183); + this.tbModeExplain.Multiline = true; + this.tbModeExplain.Name = "tbModeExplain"; + this.tbModeExplain.ReadOnly = true; + this.tbModeExplain.Size = new System.Drawing.Size(466, 116); + this.tbModeExplain.TabIndex = 34; + this.tbModeExplain.TabStop = false; + this.tbModeExplain.Text = "Requirements for mode \'Database password\'\r\n - Database masterkey contains a passw" + + "ord\r\n - Option \'Remember master password\' is active\r\n\r\nQuick Unlock Entry will b" + + "e used as fallback"; + // + // panel2 + // + this.panel2.Controls.Add(this.cbActive); + this.panel2.Controls.Add(this.lQUPINLength); + this.panel2.Controls.Add(this.lQUMode); + this.panel2.Controls.Add(this.rbPINEnd); + this.panel2.Controls.Add(this.rbPINFront); + this.panel2.Controls.Add(this.tbPINLength); + this.panel2.Controls.Add(this.cbPINMode); + this.panel2.Dock = System.Windows.Forms.DockStyle.Top; + this.panel2.Location = new System.Drawing.Point(3, 3); + this.panel2.Name = "panel2"; + this.panel2.Size = new System.Drawing.Size(466, 180); + this.panel2.TabIndex = 35; + // + // cbActive + // + this.cbActive.AutoSize = true; + this.cbActive.Location = new System.Drawing.Point(8, 11); + this.cbActive.Name = "cbActive"; + this.cbActive.Size = new System.Drawing.Size(182, 24); + this.cbActive.TabIndex = 42; + this.cbActive.Text = "Enable Quick Unlock"; + this.cbActive.TextAlign = System.Drawing.ContentAlignment.BottomCenter; + this.cbActive.UseVisualStyleBackColor = true; + // + // lQUPINLength + // + this.lQUPINLength.AutoSize = true; + this.lQUPINLength.Location = new System.Drawing.Point(4, 86); + this.lQUPINLength.Name = "lQUPINLength"; + this.lQUPINLength.Size = new System.Drawing.Size(87, 20); + this.lQUPINLength.TabIndex = 41; + this.lQUPINLength.Text = "PIN length:"; + // + // lQUMode + // + this.lQUMode.AutoSize = true; + this.lQUMode.Location = new System.Drawing.Point(4, 44); + this.lQUMode.Name = "lQUMode"; + this.lQUMode.Size = new System.Drawing.Size(53, 20); + this.lQUMode.TabIndex = 40; + this.lQUMode.Text = "Mode:"; + // + // rbPINEnd + // + this.rbPINEnd.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.rbPINEnd.AutoSize = true; + this.rbPINEnd.CheckAlign = System.Drawing.ContentAlignment.MiddleRight; + this.rbPINEnd.Location = new System.Drawing.Point(268, 145); + this.rbPINEnd.Name = "rbPINEnd"; + this.rbPINEnd.Size = new System.Drawing.Size(194, 24); + this.rbPINEnd.TabIndex = 39; + this.rbPINEnd.TabStop = true; + this.rbPINEnd.Text = "Use {0} last characters"; + this.rbPINEnd.UseVisualStyleBackColor = true; + // + // rbPINFront + // + this.rbPINFront.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.rbPINFront.AutoSize = true; + this.rbPINFront.CheckAlign = System.Drawing.ContentAlignment.MiddleRight; + this.rbPINFront.Location = new System.Drawing.Point(268, 115); + this.rbPINFront.Name = "rbPINFront"; + this.rbPINFront.Size = new System.Drawing.Size(195, 24); + this.rbPINFront.TabIndex = 38; + this.rbPINFront.TabStop = true; + this.rbPINFront.Text = "Use {0} first characters"; + this.rbPINFront.UseVisualStyleBackColor = true; + // + // tbPINLength + // + this.tbPINLength.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.tbPINLength.Location = new System.Drawing.Point(362, 83); + this.tbPINLength.MaxLength = 3; + this.tbPINLength.Name = "tbPINLength"; + this.tbPINLength.Size = new System.Drawing.Size(100, 26); + this.tbPINLength.TabIndex = 37; + this.tbPINLength.Tag = "32"; + this.tbPINLength.TextAlign = System.Windows.Forms.HorizontalAlignment.Right; + this.tbPINLength.TextChanged += new System.EventHandler(this.tbPINLength_TextChanged); + // + // cbPINMode + // + this.cbPINMode.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.cbPINMode.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cbPINMode.FormattingEnabled = true; + this.cbPINMode.Items.AddRange(new object[] { + "Quick Unlock entry only", + "Database password"}); + this.cbPINMode.Location = new System.Drawing.Point(109, 41); + this.cbPINMode.Name = "cbPINMode"; + this.cbPINMode.Size = new System.Drawing.Size(353, 28); + this.cbPINMode.TabIndex = 36; + this.cbPINMode.SelectedIndexChanged += new System.EventHandler(this.cbPINMode_SelectedIndexChanged); + // + // tabSoftlock + // + this.tabSoftlock.Controls.Add(this.tbSoftlockExplain); + this.tabSoftlock.Controls.Add(this.panel1); + this.tabSoftlock.Location = new System.Drawing.Point(4, 29); + this.tabSoftlock.Name = "tabSoftlock"; + this.tabSoftlock.Padding = new System.Windows.Forms.Padding(3); + this.tabSoftlock.Size = new System.Drawing.Size(472, 417); + this.tabSoftlock.TabIndex = 1; + this.tabSoftlock.Text = "Privacy Mode configuration"; + this.tabSoftlock.UseVisualStyleBackColor = true; + // + // tbSoftlockExplain + // + this.tbSoftlockExplain.Dock = System.Windows.Forms.DockStyle.Top; + this.tbSoftlockExplain.Location = new System.Drawing.Point(3, 73); + this.tbSoftlockExplain.Multiline = true; + this.tbSoftlockExplain.Name = "tbSoftlockExplain"; + this.tbSoftlockExplain.ReadOnly = true; + this.tbSoftlockExplain.Size = new System.Drawing.Size(466, 210); + this.tbSoftlockExplain.TabIndex = 35; + this.tbSoftlockExplain.TabStop = false; + this.tbSoftlockExplain.Text = "Soft Lock explanation"; + // + // panel1 + // + this.panel1.Controls.Add(this.cbSLIdle); + this.panel1.Controls.Add(this.cbSLOnMinimize); + this.panel1.Controls.Add(this.tSLIdleSeconds); + this.panel1.Dock = System.Windows.Forms.DockStyle.Top; + this.panel1.Location = new System.Drawing.Point(3, 3); + this.panel1.Margin = new System.Windows.Forms.Padding(0); + this.panel1.Name = "panel1"; + this.panel1.Size = new System.Drawing.Size(466, 70); + this.panel1.TabIndex = 36; + // + // cbSLIdle + // + this.cbSLIdle.AutoSize = true; + this.cbSLIdle.Location = new System.Drawing.Point(8, 11); + this.cbSLIdle.Name = "cbSLIdle"; + this.cbSLIdle.Size = new System.Drawing.Size(179, 24); + this.cbSLIdle.TabIndex = 17; + this.cbSLIdle.Text = "Seconds of inactivity"; + this.cbSLIdle.UseVisualStyleBackColor = true; + // + // cbSLOnMinimize + // + this.cbSLOnMinimize.AutoSize = true; + this.cbSLOnMinimize.Location = new System.Drawing.Point(8, 41); + this.cbSLOnMinimize.Name = "cbSLOnMinimize"; + this.cbSLOnMinimize.Size = new System.Drawing.Size(215, 24); + this.cbSLOnMinimize.TabIndex = 16; + this.cbSLOnMinimize.Text = "Privacy mode on Minimize"; + this.cbSLOnMinimize.UseVisualStyleBackColor = true; + // + // tSLIdleSeconds + // + this.tSLIdleSeconds.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.tSLIdleSeconds.Location = new System.Drawing.Point(363, 7); + this.tSLIdleSeconds.MaxLength = 5; + this.tSLIdleSeconds.Name = "tSLIdleSeconds"; + this.tSLIdleSeconds.Size = new System.Drawing.Size(100, 26); + this.tSLIdleSeconds.TabIndex = 14; + this.tSLIdleSeconds.Tag = "9999"; + this.tSLIdleSeconds.TextAlign = System.Windows.Forms.HorizontalAlignment.Right; + // + // tabAdditional + // + this.tabAdditional.Controls.Add(this.tbLockWorkspace); + this.tabAdditional.Controls.Add(this.panel3); + this.tabAdditional.Location = new System.Drawing.Point(4, 29); + this.tabAdditional.Name = "tabAdditional"; + this.tabAdditional.Padding = new System.Windows.Forms.Padding(3); + this.tabAdditional.Size = new System.Drawing.Size(472, 417); + this.tabAdditional.TabIndex = 2; + this.tabAdditional.Text = "Additional"; + this.tabAdditional.UseVisualStyleBackColor = true; + // + // tbLockWorkspace + // + this.tbLockWorkspace.Dock = System.Windows.Forms.DockStyle.Top; + this.tbLockWorkspace.ForeColor = System.Drawing.SystemColors.WindowText; + this.tbLockWorkspace.Location = new System.Drawing.Point(3, 53); + this.tbLockWorkspace.Multiline = true; + this.tbLockWorkspace.Name = "tbLockWorkspace"; + this.tbLockWorkspace.ReadOnly = true; + this.tbLockWorkspace.Size = new System.Drawing.Size(466, 237); + this.tbLockWorkspace.TabIndex = 36; + this.tbLockWorkspace.TabStop = false; + // + // panel3 + // + this.panel3.Controls.Add(this.cbLockWorkspace); + this.panel3.Dock = System.Windows.Forms.DockStyle.Top; + this.panel3.Location = new System.Drawing.Point(3, 3); + this.panel3.Name = "panel3"; + this.panel3.Size = new System.Drawing.Size(466, 50); + this.panel3.TabIndex = 37; + // + // cbLockWorkspace + // + this.cbLockWorkspace.AutoSize = true; + this.cbLockWorkspace.Location = new System.Drawing.Point(8, 11); + this.cbLockWorkspace.Name = "cbLockWorkspace"; + this.cbLockWorkspace.Size = new System.Drawing.Size(203, 24); + this.cbLockWorkspace.TabIndex = 42; + this.cbLockWorkspace.Text = "Global Lock Workspace"; + this.cbLockWorkspace.TextAlign = System.Drawing.ContentAlignment.BottomLeft; + this.cbLockWorkspace.UseVisualStyleBackColor = true; + // + // OptionsForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.BackColor = System.Drawing.Color.Transparent; + this.Controls.Add(this.tabControl1); + this.Name = "OptionsForm"; + this.Size = new System.Drawing.Size(480, 450); + this.Load += new System.EventHandler(this.UnlockOptions_Load); + this.tabControl1.ResumeLayout(false); + this.tabQuickUnlock.ResumeLayout(false); + this.tabQuickUnlock.PerformLayout(); + this.panel2.ResumeLayout(false); + this.panel2.PerformLayout(); + this.tabSoftlock.ResumeLayout(false); + this.tabSoftlock.PerformLayout(); + this.panel1.ResumeLayout(false); + this.panel1.PerformLayout(); + this.tabAdditional.ResumeLayout(false); + this.tabAdditional.PerformLayout(); + this.panel3.ResumeLayout(false); + this.panel3.PerformLayout(); + this.ResumeLayout(false); + + } + + #endregion + private System.Windows.Forms.TabControl tabControl1; + private System.Windows.Forms.TabPage tabQuickUnlock; + private System.Windows.Forms.CheckBox cbPINDBSpecific; + private System.Windows.Forms.TabPage tabSoftlock; + private System.Windows.Forms.TextBox tbSoftlockExplain; + private System.Windows.Forms.TextBox tbModeExplain; + private System.Windows.Forms.Panel panel1; + public System.Windows.Forms.TextBox tSLIdleSeconds; + public System.Windows.Forms.CheckBox cbSLIdle; + public System.Windows.Forms.CheckBox cbSLOnMinimize; + private System.Windows.Forms.Panel panel2; + private System.Windows.Forms.CheckBox cbActive; + private System.Windows.Forms.Label lQUPINLength; + private System.Windows.Forms.Label lQUMode; + private System.Windows.Forms.RadioButton rbPINEnd; + private System.Windows.Forms.RadioButton rbPINFront; + private System.Windows.Forms.TextBox tbPINLength; + internal System.Windows.Forms.ComboBox cbPINMode; + private System.Windows.Forms.TabPage tabAdditional; + private System.Windows.Forms.TextBox tbLockWorkspace; + private System.Windows.Forms.Panel panel3; + internal System.Windows.Forms.CheckBox cbLockWorkspace; + } +} \ No newline at end of file diff --git a/OptionsForm.cs b/OptionsForm.cs new file mode 100644 index 0000000..3b60ded --- /dev/null +++ b/OptionsForm.cs @@ -0,0 +1,153 @@ +using System; +using System.Windows.Forms; + +using KeePass; +using KeePassLib; +using KeePass.UI; + +using PluginTranslation; +using PluginTools; + +namespace LockAssist +{ + public partial class OptionsForm : UserControl + { + private bool FirstTime = false; + + public OptionsForm() + { + InitializeComponent(); + + Text = PluginTranslate.PluginName; + cbActive.Text = PluginTranslate.Active; + tabQuickUnlock.Text = PluginTranslate.OptionsQUSettings; + lQUMode.Text = PluginTranslate.OptionsQUMode; + lQUPINLength.Text = PluginTranslate.OptionsQUPINLength; + tbModeExplain.Lines = PluginTranslate.OptionsQUReqInfoDB.Split(new string[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); ; + cbPINDBSpecific.Text = PluginTranslate.OptionsQUSettingsPerDB; + cbPINMode.Items.Clear(); + cbPINMode.Items.AddRange(new string[] { PluginTranslate.OptionsQUModeEntry, PluginTranslate.OptionsQUModeDatabasePW }); + tabSoftlock.Text = PluginTranslate.OptionsSLSettings; + cbSLIdle.Text = PluginTranslate.OptionsSLSecondsToActivate; + cbSLOnMinimize.Text = PluginTranslate.OptionsSLMinimize; + tbSoftlockExplain.Lines = PluginTranslate.OptionsSLInfo.Split(new string[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); ; + tabAdditional.Text = KeePass.Resources.KPRes.LockWorkspace; + cbLockWorkspace.Text = string.Format(PluginTranslate.OptionsLockWorkspace, KeePass.Resources.KPRes.LockWorkspace, KeePass.Resources.KPRes.LockMenuUnlock); + tbLockWorkspace.Lines = PluginTranslate.OptionsLockWorkspaceDesc.Split(new string[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); ; + } + + public void SetOptions(LockAssistOptions options) + { + cbActive.Checked = options.QUActive; + cbPINMode.SelectedIndex = options.UsePassword ? 1 : 0; + tbPINLength.Text = options.Length.ToString(); + rbPINEnd.Checked = options.FromEnd; + rbPINFront.Checked = !options.FromEnd; + cbPINDBSpecific.Checked = options.DBSpecific; + cbSLIdle.Checked = options.SLIdle && (options.SLSeconds > 0); + if (options.SLSeconds < 1) options.SLSeconds = 60; + //tSLIdleSeconds.Text = "60"; + tSLIdleSeconds.Text = options.SLSeconds.ToString(); + cbSLOnMinimize.Checked = options.SLMinimize; + FirstTime = options.FirstTime; + cbLockWorkspace.Checked = options.LockWorkspace; + } + + public LockAssistOptions GetOptions() + { + LockAssistOptions options = new LockAssistOptions(); + options.QUActive = cbActive.Checked; + options.UsePassword = cbPINMode.SelectedIndex == 1; + if (!int.TryParse(tbPINLength.Text, out options.Length)) options.Length = 4; + options.FromEnd = rbPINEnd.Checked; + options.DBSpecific = cbPINDBSpecific.Checked; + if (!int.TryParse(tSLIdleSeconds.Text, out options.SLSeconds)) options.SLSeconds = 60; + if (options.SLSeconds <= 0) options.SLSeconds = 60; + options.SLIdle = cbSLIdle.Checked; + options.SLMinimize = cbSLOnMinimize.Checked; + options.LockWorkspace = cbLockWorkspace.Checked; + return options; + } + + private void tbPINLength_TextChanged(object sender, EventArgs e) + { + int len = 0; + if (!int.TryParse(tbPINLength.Text, out len)) len = 4; + if (len < 1) len = 1; + if (len > 32) len = 32; + rbPINFront.Text = string.Format(PluginTranslate.OptionsQUUseFirst, len.ToString()); + rbPINEnd.Text = string.Format(PluginTranslate.OptionsQUUseLast, len.ToString()); + } + + private void cbPINMode_SelectedIndexChanged(object sender, EventArgs e) + { + lQUMode.ForeColor = System.Drawing.SystemColors.ControlText; + if (cbPINMode.SelectedIndex == 0) + { + PwEntry check = LockAssistExt.GetQuickUnlockEntry(Program.MainForm.ActiveDatabase); + if (check != null) return; + if (!FirstTime && (Tools.AskYesNo(PluginTranslate.OptionsQUEntryCreate) == DialogResult.No)) + { + cbPINMode.SelectedIndex = 1; + } + else + { + check = new PwEntry(true, true); + Program.MainForm.ActiveDatabase.RootGroup.AddEntry(check, true); + check.Strings.Set(PwDefs.TitleField, new KeePassLib.Security.ProtectedString(false, QuickUnlockKeyProv.KeyProviderName)); + Tools.ShowInfo(PluginTranslate.OptionsQUEntryCreated); + ShowQuickUnlockEntry(Program.MainForm.ActiveDatabase, check.Uuid); + } + return; + } + if (Program.Config.Security.MasterPassword.RememberWhileOpen) return; + lQUMode.ForeColor = System.Drawing.Color.Red; + if (FirstTime || (cbPINMode.SelectedIndex == 0)) return; + Tools.ShowInfo(PluginTranslate.OptionsQUInfoRememberPassword); + } + + private void tbValidating(object sender, System.ComponentModel.CancelEventArgs e) + { + int len = 0; + if (!int.TryParse((sender as TextBox).Text, out len)) + { + if ((sender as TextBox).Name == "tbPinLength") len = 4; + else len = 60; + } + if ((sender as TextBox).Name == "tbPinLength") len = Math.Max(1, len); + if ((sender as TextBox).Name != "tbPinLength") len = Math.Max(0, len); + int max = int.Parse((string)(sender as TextBox).Tag); + if (len > max) len = max; + if ((sender as TextBox).Text != len.ToString()) (sender as TextBox).Text = len.ToString(); + } + + private void ShowQuickUnlockEntry(PwDatabase db, PwUuid qu) + { + Program.MainForm.UpdateUI(false, null, false, db.RootGroup, true, db.RootGroup, true); + Program.MainForm.EnsureVisibleEntry(qu); + + ListView lv = (Program.MainForm.Controls.Find("m_lvEntries", true)[0] as ListView); + foreach (ListViewItem lvi in lv.Items) + { + PwListItem li = (lvi.Tag as PwListItem); + if (li == null) continue; + + PwEntry pe = li.Entry; + if (pe.Uuid != qu) continue; + lv.FocusedItem = lvi; + ToolStripItem[] tsmi = Program.MainForm.EntryContextMenu.Items.Find("m_ctxEntryEdit", false); + if (tsmi != null) tsmi[0].PerformClick(); + break; + } + } + + private void UnlockOptions_Load(object sender, EventArgs e) + { + if ((Program.MainForm.ActiveDatabase == null) || !Program.MainForm.ActiveDatabase.IsOpen) + { + cbPINDBSpecific.Enabled = false; + cbPINDBSpecific.Checked = false; + } + } + } +} diff --git a/PluginTools.cs b/PluginTools.cs new file mode 100644 index 0000000..3832747 --- /dev/null +++ b/PluginTools.cs @@ -0,0 +1,321 @@ +using KeePass.Forms; +using KeePass.UI; +using KeePassLib.Utility; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Reflection; +using System.Windows.Forms; + +namespace PluginTools +{ + public static class Tools + { + private static Version m_KPVersion = null; + public static Version KeePassVersion { get { return m_KPVersion; } } + public static string DefaultCaption = string.Empty; + public static string PluginURL = string.Empty; + + static Tools() + { + KeePass.UI.GlobalWindowManager.WindowAdded += OnWindowAdded; + KeePass.UI.GlobalWindowManager.WindowRemoved += OnWindowRemoved; + m_KPVersion = typeof(KeePass.Program).Assembly.GetName().Version; + } + + #region Form and field handling + public static object GetField(string field, object obj) + { + BindingFlags bf = BindingFlags.Instance | BindingFlags.NonPublic; + return GetField(field, obj, bf); + } + + public static object GetField(string field, object obj, BindingFlags bf) + { + if (obj == null) return null; + FieldInfo fi = obj.GetType().GetField(field, bf); + if (fi == null) return null; + return fi.GetValue(obj); + } + + public static Control GetControl(string control) + { + return GetControl(control, KeePass.Program.MainForm); + } + + public static Control GetControl(string control, Control form) + { + if (form == null) return null; + if (string.IsNullOrEmpty(control)) return null; + Control[] cntrls = form.Controls.Find(control, true); + if (cntrls.Length == 0) return null; + return cntrls[0]; + } + #endregion + + #region Plugin options and instance + public static object GetPluginInstance(string PluginName) + { + string comp = PluginName + "." + PluginName + "Ext"; + BindingFlags bf = BindingFlags.Instance | BindingFlags.NonPublic; + try + { + var PluginManager = GetField("m_pluginManager", KeePass.Program.MainForm); + var PluginList = GetField("m_vPlugins", PluginManager); + MethodInfo IteratorMethod = PluginList.GetType().GetMethod("System.Collections.Generic.IEnumerable.GetEnumerator", bf); + IEnumerator PluginIterator = (IEnumerator)(IteratorMethod.Invoke(PluginList, null)); + while (PluginIterator.MoveNext()) + { + object result = GetField("m_pluginInterface", PluginIterator.Current); + if (comp == result.GetType().ToString()) return result; + } + } + + catch (Exception) { } + return null; + } + + public static event EventHandler OptionsFormShown; + public static event EventHandler OptionsFormClosed; + + private static bool OptionsEnabled = (KeePass.Program.Config.UI.UIFlags & (ulong)KeePass.App.Configuration.AceUIFlags.DisableOptions) != (ulong)KeePass.App.Configuration.AceUIFlags.DisableOptions; + private static bool m_ActivatePluginTab = false; + private static OptionsForm m_of = null; + private const string c_tabRookiestyle = "m_tabRookiestyle"; + private const string c_tabControlRookiestyle = "m_tabControlRookiestyle"; + private static string m_TabPageName = string.Empty; + private static bool m_OptionsShown = false; + private static bool m_PluginContainerShown = false; + + public static void AddPluginToOptionsForm(KeePass.Plugins.Plugin p, UserControl uc) + { + m_OptionsShown = m_PluginContainerShown = false; + TabPage tPlugin = new TabPage(DefaultCaption); + tPlugin.CreateControl(); + tPlugin.Name = m_TabPageName = c_tabRookiestyle + p.GetType().Name; + uc.Dock = DockStyle.Fill; + uc.Padding = new Padding(15, 10, 15, 10); + tPlugin.Controls.Add(uc); + TabControl tcPlugins = AddPluginTabContainer(); + int i = 0; + bool insert = false; + for (int j = 0; j < tcPlugins.TabPages.Count; j++) + { + if (string.Compare(tPlugin.Text, tcPlugins.TabPages[j].Text, StringComparison.CurrentCultureIgnoreCase) < 0) + { + i = j; + insert = true; + break; + } + } + if (!insert) i = tcPlugins.TabPages.Count; + tcPlugins.TabPages.Insert(i, tPlugin); + if (p.SmallIcon != null) + { + tcPlugins.ImageList.Images.Add(tPlugin.Name, p.SmallIcon); + tPlugin.ImageKey = tPlugin.Name; + } + TabControl tcMain = Tools.GetControl("m_tabMain", m_of) as TabControl; + if (m_ActivatePluginTab) + { + tcMain.SelectedIndex = tcMain.TabPages.IndexOfKey(c_tabRookiestyle); + KeePass.Program.Config.Defaults.OptionsTabIndex = (uint)tcMain.SelectedIndex; + tcPlugins.SelectedIndex = tcPlugins.TabPages.IndexOf(tPlugin); + } + m_ActivatePluginTab = false; + if (!string.IsNullOrEmpty(PluginURL)) + AddPluginLink(uc); + } + + private static void OnPluginTabsSelected(object sender, TabControlEventArgs e) + { + m_OptionsShown |= (e.TabPage.Name == m_TabPageName); + m_PluginContainerShown |= (m_OptionsShown || (e.TabPage.Name == c_tabRookiestyle)); + } + + public static UserControl GetPluginFromOptions(KeePass.Plugins.Plugin p, out bool PluginOptionsShown) + { + PluginOptionsShown = m_OptionsShown && m_PluginContainerShown; + TabPage tPlugin = Tools.GetControl(c_tabRookiestyle + p.GetType().Name, m_of) as TabPage; + if (tPlugin == null) return null; + return tPlugin.Controls[0] as UserControl; + } + + public static void ShowOptions() + { + m_ActivatePluginTab = true; + if (OptionsEnabled) + KeePass.Program.MainForm.ToolsMenu.DropDownItems["m_menuToolsOptions"].PerformClick(); + else + { + m_of = new OptionsForm(); + m_of.InitEx(KeePass.Program.MainForm.ClientIcons); + m_of.ShowDialog(); + } + } + + private static void AddPluginLink(UserControl uc) + { + LinkLabel llUrl = new LinkLabel(); + llUrl.Links.Add(0, 0, PluginURL); + llUrl.Text = PluginURL; + uc.Controls.Add(llUrl); + llUrl.Dock = DockStyle.Bottom; + llUrl.LinkClicked += new LinkLabelLinkClickedEventHandler(PluginURLClicked); + } + + private static void PluginURLClicked(object sender, LinkLabelLinkClickedEventArgs e) + { + string target = e.Link.LinkData as string; + System.Diagnostics.Process.Start(target); + } + + private static void OnOptionsFormShown(object sender, EventArgs e) + { + m_of.Shown -= OnOptionsFormShown; + TabControl tcMain = Tools.GetControl("m_tabMain", m_of) as TabControl; + if (!tcMain.TabPages.ContainsKey(c_tabRookiestyle)) return; + TabPage tPlugins = tcMain.TabPages[c_tabRookiestyle]; + TabControl tcPlugins = Tools.GetControl(c_tabControlRookiestyle, tPlugins) as TabControl; + tcMain.Selected += OnPluginTabsSelected; + tcPlugins.Selected += OnPluginTabsSelected; + tcMain.ImageList.Images.Add(c_tabRookiestyle + "Icon", (Image)KeePass.Program.Resources.GetObject("B16x16_BlockDevice")); + tPlugins.ImageKey = c_tabRookiestyle + "Icon"; + m_PluginContainerShown |= tcMain.SelectedTab == tPlugins; + m_OptionsShown |= (tcPlugins.SelectedTab.Name == m_TabPageName); + } + + private static void OnWindowAdded(object sender, KeePass.UI.GwmWindowEventArgs e) + { + if (OptionsFormShown == null) return; + if (e.Form is OptionsForm) + { + m_of = e.Form as OptionsForm; + m_of.Shown += OnOptionsFormShown; + OptionsFormsEventArgs o = new OptionsFormsEventArgs(m_of); + OptionsFormShown(sender, o); + } + } + + private static void OnWindowRemoved(object sender, KeePass.UI.GwmWindowEventArgs e) + { + if (OptionsFormClosed == null) return; + if (e.Form is OptionsForm) + { + OptionsFormsEventArgs o = new OptionsFormsEventArgs(m_of); + OptionsFormClosed(sender, o); + } + } + + private static TabControl AddPluginTabContainer() + { + TabControl tcMain = Tools.GetControl("m_tabMain", m_of) as TabControl; + TabPage tPlugins = null; + TabControl tcPlugins = null; + if (tcMain.TabPages.ContainsKey(c_tabRookiestyle)) + { + tPlugins = tcMain.TabPages[c_tabRookiestyle]; + tcPlugins = (TabControl)tPlugins.Controls[c_tabControlRookiestyle]; + } + else + { + tPlugins = new TabPage(KeePass.Resources.KPRes.Plugin + " " + m_of.Text); + tPlugins.Name = c_tabRookiestyle; + tPlugins.CreateControl(); + if (!OptionsEnabled) + { + while (tcMain.TabCount > 0) + tcMain.TabPages.RemoveAt(0); + } + tcMain.TabPages.Add(tPlugins); + tcPlugins = new TabControl(); + tcPlugins.Name = c_tabControlRookiestyle; + tcPlugins.Dock = DockStyle.Fill; + tcPlugins.Multiline = true; + tcPlugins.CreateControl(); + if (tcPlugins.ImageList == null) + tcPlugins.ImageList = new ImageList(); + tPlugins.Controls.Add(tcPlugins); + } + return tcPlugins; + } + + public class OptionsFormsEventArgs : EventArgs + { + public Form form; + + public OptionsFormsEventArgs(Form form) + { + this.form = form; + } + } + #endregion + + #region MessageBox shortcuts + public static DialogResult ShowError(string msg) + { + return ShowError(msg, DefaultCaption); + } + + public static DialogResult ShowInfo(string msg) + { + return ShowInfo(msg, DefaultCaption); + } + + public static DialogResult AskYesNo(string msg) + { + return AskYesNo(msg, DefaultCaption); + } + + public static DialogResult ShowError(string msg, string caption) + { + return MessageBox.Show(msg, caption, MessageBoxButtons.OK, MessageBoxIcon.Error); + } + + public static DialogResult ShowInfo(string msg, string caption) + { + return MessageBox.Show(msg, caption, MessageBoxButtons.OK, MessageBoxIcon.Information); + } + + public static DialogResult AskYesNo(string msg, string caption) + { + return MessageBox.Show(msg, caption, MessageBoxButtons.YesNo, MessageBoxIcon.Question); + } + #endregion + + #region GlobalWindowManager + public static void GlobalWindowManager(Form form) + { + if ((form == null) || (form.IsDisposed)) return; + form.Load += FormLoaded; + form.FormClosed += FormClosed; + } + + private static void FormLoaded(object sender, EventArgs e) + { + KeePass.UI.GlobalWindowManager.AddWindow(sender as Form); + } + + private static void FormClosed(object sender, FormClosedEventArgs e) + { + KeePass.UI.GlobalWindowManager.RemoveWindow(sender as Form); + } + #endregion + } + + public static class DPIAwareness + { + public static readonly Size Size16 = new Size(DpiUtil.ScaleIntX(16), DpiUtil.ScaleIntY(16)); + + public static Image Scale16x16(Image img) + { + return Scale(img, 16, 16); + } + + public static Image Scale(Image img, int x, int y) + { + if (img == null) return null; + return GfxUtil.ScaleImage(img, DpiUtil.ScaleIntX(x), DpiUtil.ScaleIntY(y)); + } + } +} \ No newline at end of file diff --git a/PluginTranslation.cs b/PluginTranslation.cs new file mode 100644 index 0000000..d14b7b4 --- /dev/null +++ b/PluginTranslation.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections.Generic; +using System.Xml; +using System.Xml.Serialization; +using System.IO; + +using KeePass.Plugins; +using KeePass.Util; +using KeePassLib.Utility; + +namespace PluginTranslation +{ + public static class PluginTranslate + { + #region Definitions of translated texts go here + public const string PluginName = "LockAssist"; + public static string FirstTimeInfo + { + get { return TryGet("FirstTimeInfo", @"Quick Unlock offers two operation modes. +Please choose your preferred way of working."); } + } + public static string OptionsQUMode + { + get { return TryGet("OptionsQUMode", @"Mode:"); } + } + public static string Active + { + get { return TryGet("Active", @"Enable Quick Unlock"); } + } + public static string OptionsSLMinimize + { + get { return TryGet("OptionsSLMinimize", @"Softlock on minimize"); } + } + public static string KeyProvNoQuickUnlock + { + get { return TryGet("KeyProvNoQuickUnlock", @"No Quick Unlock key found. Quick Unlock is not possible."); } + } + public static string OptionsNotSavedSoftlock + { + get { return TryGet("OptionsNotSavedSoftlock", @"Softlock active. Settings have NOT been saved."); } + } + public static string OptionsQUReqInfoDB + { + get { return TryGet("OptionsQUReqInfoDB", @"Prerequsites for mode 'database password': +- Database masterkey contains a password +- Option 'Remember master password' is active + +Quick Unlock entry will be used as fallback"); } + } + public static string ButtonCancel + { + get { return TryGet("ButtonCancel", @"Cancel"); } + } + public static string OptionsQUSettings + { + get { return TryGet("OptionsQUSettings", @"Quick Unlock"); } + } + public static string OptionsQUModeEntry + { + get { return TryGet("OptionsQUModeEntry", @"Quick Unlock entry only"); } + } + public static string OptionsQUModeDatabasePW + { + get { return TryGet("OptionsQUModeDatabasePW", @"Database password"); } + } + public static string OptionsSLInfo + { + get { return TryGet("OptionsSLInfo", @"Softlock hides following sensitive information while still allowing Auto-Type as well as other integration: +- group list +- entry list +- entry view +- all forms NOT mentioned in config file property QuickUnlock.SoftLock.ExcludeForms + +Valid Quick Unlock settings required, Quick Unlock does NOT need to be active"); } + } + public static string OptionsQUEntryCreated + { + get { return TryGet("OptionsQUEntryCreated", @"Quick Unlock entry created. Please edit and set Quick Unlock PIN as password"); } + } + public static string OptionsSLSettings + { + get { return TryGet("OptionsSLSettings", @"Softlock"); } + } + public static string OptionsQUSettingsPerDB + { + get { return TryGet("OptionsQUSettingsPerDB", @"Settings are DB specific"); } + } + public static string SoftlockModeUnhide + { + get { return TryGet("SoftlockModeUnhide", @"Softlock active. Click to deactivate"); } + } + public static string OptionsSwitchDBToGeneral + { + get { return TryGet("OptionsSwitchDBToGeneral", @"Database specific settings switched off. +Revert back to general settings? +'No' will set current settings as global settings"); } + } + public static string OptionsQUPINLength + { + get { return TryGet("OptionsQUPINLength", @"PIN length:"); } + } + public static string ButtonUnlock + { + get { return TryGet("ButtonUnlock", @"Unlock"); } + } + public static string UnlockLabel + { + get { return TryGet("UnlockLabel", @"Quick Unlock PIN:"); } + } + public static string OptionsQUEntryCreate + { + get { return TryGet("OptionsQUEntryCreate", @"Quick Unlock entry could not be found. Create it now?"); } + } + public static string KeyProvNoCreate + { + get { return TryGet("KeyProvNoCreate", @"This key provider cannot be used to create keys."); } + } + public static string OptionsSLSecondsToActivate + { + get { return TryGet("OptionsSLSecondsToActivate", @"Softlock after inactivity (seconds):"); } + } + public static string ButtonOK + { + get { return TryGet("ButtonOK", @"OK"); } + } + public static string OptionsQUInfoRememberPassword + { + get { return TryGet("OptionsQUInfoRememberPassword", @"'Remember master password' needs to be active in Options -> Security. +Please don't forget to activate"); } + } + public static string OptionsQUUseLast + { + get { return TryGet("OptionsQUUseLast", @"Use last {0} characters as PIN"); } + } + public static string OptionsQUUseFirst + { + get { return TryGet("OptionsQUUseFirst", @"Use first {0} characters as PIN"); } + } + public static string SoftlockModeUnhideForms + { + get { return TryGet("SoftlockModeUnhideForms", @"Softlock active. Click topmost form to deactivate"); } + } + public static string WrongPIN + { + get { return TryGet("WrongPIN", @"The entered PIN was not correct. +Database stays locked and can only be unlocked with the original masterkey"); } + } + public static string OptionsLockWorkspace + { + get { return TryGet("OptionsLockWorkspace", @"Global '{0} / {1}'"); } + } + public static string OptionsLockWorkspaceDesc + { + get { return TryGet("OptionsLockWorkspaceDesc", @"This option changes the behaviour of 'Lock Workspace' for both the menu entry as well as the toolbar button. + +If it's active ALL loaded databases are locked / unlocked by using these commands. +In this case it depends on the active document's state whether a global lock or global unlock is performed. + +If the [Shift] key is pressed while using these commands only the active document is processed."); } + } + #endregion + + #region NO changes in this area + private static bool Debug = KeePass.Program.CommandLineArgs[KeePass.App.AppDefs.CommandLineOptions.Debug] != null; + private static StringDictionary m_translation = new StringDictionary(); + + public static void Init(Plugin plugin, string LanguageCodeIso6391) + { + try + { + string filename = GetFilename(plugin.GetType().Namespace, LanguageCodeIso6391); + + string translation = File.ReadAllText(filename); + + XmlSerializer xs = new XmlSerializer(m_translation.GetType()); + m_translation = (StringDictionary)xs.Deserialize(new StringReader(translation)); + } + catch (Exception) { } + } + + public static string TryGet(string key, string def) + { + string result = string.Empty; + if (m_translation.TryGetValue(key, out result)) + return result; + else + return def; + } + + private static string GetFilename(string plugin, string lang) + { + string filename = UrlUtil.GetFileDirectory(WinUtil.GetExecutable(), true, true); + filename += KeePass.App.AppDefs.PluginsDir + UrlUtil.LocalDirSepChar + "Translations" + UrlUtil.LocalDirSepChar; + filename += plugin + "." + lang + ".language.xml"; + return filename; + } + #endregion + } + + #region NO changes in this area + [XmlRoot("Translation")] + public class StringDictionary : Dictionary, IXmlSerializable + { + public System.Xml.Schema.XmlSchema GetSchema() + { + return null; + } + + public void ReadXml(XmlReader reader) + { + bool wasEmpty = reader.IsEmptyElement; + reader.Read(); + if (wasEmpty) return; + while (reader.NodeType != XmlNodeType.EndElement) + { + reader.ReadStartElement("item"); + reader.ReadStartElement("key"); + string key = reader.ReadContentAsString(); + reader.ReadEndElement(); + reader.ReadStartElement("value"); + string value = reader.ReadContentAsString(); + reader.ReadEndElement(); + this.Add(key, value); + reader.ReadEndElement(); + reader.MoveToContent(); + } + reader.ReadEndElement(); + } + + public void WriteXml(XmlWriter writer) + { + foreach (string key in this.Keys) + { + writer.WriteStartElement("item"); + writer.WriteStartElement("key"); + writer.WriteString(key); + writer.WriteEndElement(); + writer.WriteStartElement("value"); + writer.WriteString(this[key]); + writer.WriteEndElement(); + writer.WriteEndElement(); + } + } + } + #endregion +} \ No newline at end of file diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..e091b8b --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// Allgemeine Informationen über eine Assembly werden über die folgenden +// Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, +// die einer Assembly zugeordnet sind. +[assembly: AssemblyTitle("LockAssist")] +[assembly: AssemblyDescription("Adds a quick unlock option as key provider and hides KeePass screens after defined inactivity")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("rookiestyle")] +[assembly: AssemblyProduct("KeePass Plugin")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Durch Festlegen von ComVisible auf FALSE werden die Typen in dieser Assembly +// für COM-Komponenten unsichtbar. Wenn Sie auf einen Typ in dieser Assembly von +// COM aus zugreifen müssen, sollten Sie das ComVisible-Attribut für diesen Typ auf "True" festlegen. +[assembly: ComVisible(true)] + +// Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird +[assembly: Guid("4712d887-6685-4cb1-b67b-e981119fe3d2")] + +// Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten: +// +// Hauptversion +// Nebenversion +// Buildnummer +// Revision +// +// Sie können alle Werte angeben oder Standardwerte für die Build- und Revisionsnummern verwenden, +// indem Sie "*" wie unten gezeigt eingeben: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("0.3")] +[assembly: AssemblyFileVersion("0.3")] diff --git a/QuickUnlock.cs b/QuickUnlock.cs new file mode 100644 index 0000000..6ef5f1d --- /dev/null +++ b/QuickUnlock.cs @@ -0,0 +1,220 @@ +using KeePass; +using KeePass.Forms; +using KeePass.Plugins; +using KeePass.UI; +using KeePassLib; +using KeePassLib.Collections; +using KeePassLib.Keys; +using KeePassLib.Security; +using KeePassLib.Serialization; +using System; +using System.Collections.Generic; +using System.Windows.Forms; + +using PluginTranslation; +using PluginTools; + +namespace LockAssist +{ + public partial class LockAssistExt : Plugin, IMessageFilter + { + private void QuickUnlock_Init() + { + m_kp = new QuickUnlockKeyProv(); + m_host.KeyProviderPool.Add(m_kp); + m_host.MainWindow.FileClosingPre += OnFileClosePre; + m_host.MainWindow.FileOpened += OnFileOpened; + GlobalWindowManager.WindowAdded += OnWindowAdded; + } + + #region Eventhandler for opening and closing a DB + private void OnFileOpened(object sender, FileOpenedEventArgs e) + { + CheckSoftlockMode(); + m_options.ReadConfig(e.Database); + if (m_options.FirstTime && + (!m_options.UsePassword && (GetQuickUnlockEntry(e.Database) == null) + || (m_options.UsePassword && !Program.Config.Security.MasterPassword.RememberWhileOpen))) + { + Tools.ShowInfo(PluginTranslate.FirstTimeInfo); + Tools.ShowOptions(); + m_host.CustomConfig.SetBool("LockAssist.FirstTime", false); + } + if (!m_options.QUActive) return; + //Restore previously stored information about the masterkey + QuickUnlockOldKeyInfo ok = m_kp.GetOldKey(e.Database); + if (ok == null) return; + KcpCustomKey ck = (KcpCustomKey)e.Database.MasterKey.GetUserKey(typeof(KcpCustomKey)); + if ((ck == null) || (ck.Name != QuickUnlockKeyProv.KeyProviderName)) return; + + e.Database.MasterKey.RemoveUserKey(ck); + if (ok.pwHash != null) + { + KcpPassword p = m_kp.DeserializePassword(ok.pwHash, Program.Config.Security.MasterPassword.RememberWhileOpen); + e.Database.MasterKey.AddUserKey(p); + } + if (!string.IsNullOrEmpty(ok.keyFile)) e.Database.MasterKey.AddUserKey(new KcpKeyFile(ok.keyFile)); + if (ok.account) e.Database.MasterKey.AddUserKey(new KcpUserAccount()); + Program.Config.Defaults.SetKeySources(e.Database.IOConnectionInfo, e.Database.MasterKey); + } + + private void OnFileClosePre(object sender, FileClosingEventArgs e) + { + //Do quick unlock only in case of locking + //Do NOT do quick unlock in case of closing the database + if (e.Flags != FileEventFlags.Locking) return; + m_options.ReadConfig(e.Database); + if (!m_options.QUActive) return; + ProtectedString QuickUnlockKey = null; + if (Program.Config.Security.MasterPassword.RememberWhileOpen && m_options.UsePassword) + QuickUnlockKey = GetQuickUnlockKeyFromMasterKey(e.Database); + if (QuickUnlockKey == null) + QuickUnlockKey = GetQuickUnlockKeyFromEntry(e.Database); + QuickUnlockKey = TrimQuickUnlockKey(QuickUnlockKey); + if ((QuickUnlockKey == null) || QuickUnlockKey.IsEmpty) return; + m_kp.AddDb(e.Database, QuickUnlockKey, Program.Config.Security.MasterPassword.RememberWhileOpen && m_options.UsePassword); + } + #endregion + + #region Unlock / KeyPromptForm + private void OnWindowAdded(object sender, GwmWindowEventArgs e) + { + if (!(e.Form is KeyPromptForm) && !(e.Form is KeyCreationForm)) return; + e.Form.Shown += (o, x) => OnKeyFormShown(o, false); + } + + public static void OnKeyFormShown(object sender, bool resetFile) + { + if (CheckGlobalUnlock()) + { + GlobalWindowManager.RemoveWindow(sender as Form); + (sender as Form).Close();(sender as Form).Dispose(); + GlobalUnlock(sender, null); + return; + } + Form keyform = (sender as Form); + try + { + ComboBox cmbKeyFile = (ComboBox)Tools.GetControl("m_cmbKeyFile", keyform); + if (cmbKeyFile == null) return; + int index = cmbKeyFile.Items.IndexOf(QuickUnlockKeyProv.KeyProviderName); + //Quick Unlock cannot be used to create a key ==> Remove it from list of key providers + if (keyform is KeyCreationForm) + { + if (index == -1) return; + cmbKeyFile.Items.RemoveAt(index); + List keyfiles = (List)Tools.GetField("m_lKeyFileNames", keyform); + if (keyfiles != null) keyfiles.Remove(QuickUnlockKeyProv.KeyProviderName); + return; + } + + //Key prompt form is shown + IOConnectionInfo dbIOInfo = (IOConnectionInfo)Tools.GetField("m_ioInfo", keyform); + if (m_bContinueUnlock) + { + CheckBox cbContinueUnlock = new CheckBox(); + cbContinueUnlock.AutoSize = true; + cbContinueUnlock.Text = KeePass.Resources.KPRes.LockMenuUnlock; + cbContinueUnlock.Checked = true; + cbContinueUnlock.Name = c_LockAssistContinueUnlockWorkbench; + cbContinueUnlock.CheckedChanged += (o, e) => { m_bContinueUnlock = cbContinueUnlock.Checked; }; + cbContinueUnlock.Checked = cbContinueUnlock.Checked; + CheckBox cbPassword = (CheckBox)Tools.GetControl("m_cbPassword", keyform); + CheckBox cbAccount = (CheckBox)Tools.GetControl("m_cbUserAccount", keyform); + cbContinueUnlock.Left = cbAccount.Left; + cbContinueUnlock.Top = cbAccount.Top + 30; + cbAccount.Parent.Controls.Add(cbContinueUnlock); + } + //If Quick Unlock is possible show the Quick Unlock form + if ((index != -1) && (dbIOInfo != null) && QuickUnlockKeyProv.HasDB(dbIOInfo.Path)) + { + cmbKeyFile.SelectedIndex = index; + CheckBox cbPassword = (CheckBox)Tools.GetControl("m_cbPassword", keyform); + CheckBox cbAccount = (CheckBox)Tools.GetControl("m_cbUserAccount", keyform); + Button bOK = (Button)Tools.GetControl("m_btnOK", keyform); + if ((bOK != null) && (cbPassword != null) && (cbAccount != null)) + { + UIUtil.SetChecked(cbPassword, false); + UIUtil.SetChecked(cbAccount, false); + bOK.PerformClick(); + } + return; + } + + //Quick Unlock is not possible => Remove it from list of key providers + if ((resetFile || ((dbIOInfo != null) && !QuickUnlockKeyProv.HasDB(dbIOInfo.Path))) && (index != -1)) + { + cmbKeyFile.Items.RemoveAt(index); + List keyfiles = (List)Tools.GetField("m_lKeyFileNames", keyform); + if (keyfiles != null) keyfiles.Remove(QuickUnlockKeyProv.KeyProviderName); + if (resetFile) cmbKeyFile.SelectedIndex = 0; + } + } + catch (Exception) { } + } + #endregion + + #region QuickUnlockKey handling + private ProtectedString GetQuickUnlockKeyFromMasterKey(PwDatabase db) + { + /* + * Try to create QuickUnlockKey based on password + * + * If no password is contained in MasterKey there + * EITHER is no password at all + * OR the database was unlocked with Quick Unlock + * In these case ask our key provider for the original password + */ + ProtectedString QuickUnlockKey = null; + try + { + KcpPassword pw = (KcpPassword)db.MasterKey.GetUserKey(typeof(KcpPassword)); + if (pw != null) + QuickUnlockKey = pw.Password; + } + catch (Exception) { } + if ((QuickUnlockKey != null) && (QuickUnlockKey.Length > 0)) return QuickUnlockKey; + return null; + } + + private ProtectedString GetQuickUnlockKeyFromEntry(PwDatabase db) + { + PwEntry QuickUnlockEntry = GetQuickUnlockEntry(db); + if (QuickUnlockEntry == null) return null; + return QuickUnlockEntry.Strings.GetSafe(PwDefs.PasswordField); + } + + public static PwEntry GetQuickUnlockEntry(PwDatabase db) + { + if ((db == null) || !db.IsOpen) return null; + SearchParameters sp = new SearchParameters(); + sp.SearchInTitles = true; + sp.ExcludeExpired = true; + sp.SearchString = QuickUnlockKeyProv.KeyProviderName; + PwObjectList entries = new PwObjectList(); + db.RootGroup.SearchEntries(sp, entries); + if ((entries == null) || (entries.UCount == 0)) return null; + return entries.GetAt(0); + } + + private ProtectedString TrimQuickUnlockKey(ProtectedString QuickUnlockKey) + { + if ((QuickUnlockKey == null) || (QuickUnlockKey.Length <= m_options.Length)) return QuickUnlockKey; + int startIndex = 0; + if (!m_options.FromEnd) + startIndex = m_options.Length; + QuickUnlockKey = QuickUnlockKey.Remove(startIndex, QuickUnlockKey.Length - m_options.Length); + return QuickUnlockKey; + } + #endregion + + private void QuickUnlock_Terminate() + { + m_host.KeyProviderPool.Remove(m_kp); + m_kp = null; + m_host.MainWindow.FileClosingPre -= OnFileClosePre; + m_host.MainWindow.FileOpened -= OnFileOpened; + GlobalWindowManager.WindowAdded -= OnWindowAdded; + } + } +} diff --git a/QuickUnlockKeyProv.cs b/QuickUnlockKeyProv.cs new file mode 100644 index 0000000..0fe7644 --- /dev/null +++ b/QuickUnlockKeyProv.cs @@ -0,0 +1,231 @@ +using System; +using System.Reflection; +using System.Collections.Generic; +using System.Windows.Forms; +using System.Security.Cryptography; + +using KeePassLib.Keys; +using KeePassLib; +using KeePassLib.Cryptography; +using KeePassLib.Security; +using KeePassLib.Utility; +using KeePassLib.Cryptography.Cipher; + +using PluginTranslation; +using PluginTools; + +namespace LockAssist +{ + public class QuickUnlockOldKeyInfo + { + public ProtectedString QuickUnlockKey = ProtectedString.EmptyEx; + public ProtectedBinary pwHash = null; + public string keyFile = string.Empty; + public bool account = false; + public ProtectedBinary PINCheck = null; + } + + public class QuickUnlockKeyProv : KeyProvider + { + public static string KeyProviderName = PluginTranslate.PluginName + " - Quick Unlock"; + private static byte[] m_PINCheck = StrUtil.Utf8.GetBytes(KeyProviderName); + public override string Name { get { return KeyProviderName; } } + public override bool SecureDesktopCompatible { get { return true; } } + public override bool Exclusive { get { return true; } } + public override bool DirectKey { get { return true; } } + public override bool GetKeyMightShowGui { get { return true; } } + + private static Dictionary m_hashedKey = new Dictionary(); + private static Dictionary m_originalKey = new Dictionary(); + + public override byte[] GetKey(KeyProviderQueryContext ctx) + { + if (ctx.CreatingNewKey) //should not happen but you never know + { + Tools.ShowError(PluginTranslate.KeyProvNoCreate); + return null; + } + ProtectedBinary encryptedKey = new ProtectedBinary(); + if (!m_hashedKey.TryGetValue(ctx.DatabasePath, out encryptedKey)) + { + Tools.ShowError(PluginTranslate.KeyProvNoQuickUnlock); + return null; + } + + var uForm = new UnlockForm(); + if (uForm.ShowDialog() != DialogResult.OK) return null; + ProtectedString QuickUnlockKey = uForm.GetQuickUnlockKey(); + uForm.Close(); + + m_hashedKey.Remove(ctx.DatabasePath); + if (KeePass.UI.GlobalWindowManager.TopWindow is KeePass.Forms.KeyPromptForm) + { + QuickUnlockOldKeyInfo ok = null; + if (m_originalKey.TryGetValue(ctx.DatabasePath, out ok)) + { + byte[] comparePIN = DecryptKey(QuickUnlockKey, ok.PINCheck).ReadData(); + if (StrUtil.Utf8.GetString(comparePIN) != KeyProviderName) + { + LockAssistExt.OnKeyFormShown(KeePass.UI.GlobalWindowManager.TopWindow, true); + Tools.ShowError(PluginTranslate.WrongPIN); + return null; + } + } + } + return DecryptKey(QuickUnlockKey, encryptedKey).ReadData(); + } + + public QuickUnlockOldKeyInfo GetOldKey(PwDatabase db) + { + QuickUnlockOldKeyInfo ok = new QuickUnlockOldKeyInfo(); + if (m_originalKey.TryGetValue(db.IOConnectionInfo.Path, out ok)) + { + if ((ok.pwHash != null) && (ok.pwHash.Length != 0)) + ok.pwHash = DecryptKey(ok.QuickUnlockKey, ok.pwHash); + m_originalKey.Remove(db.IOConnectionInfo.Path); + return ok; + } + return null; + } + + public void AddDb(PwDatabase db, ProtectedString QuickUnlockKey, bool savePw) + { + RemoveDb(db); + ProtectedBinary pbKey = CreateMasterKeyHash(db.MasterKey); + m_hashedKey.Add(db.IOConnectionInfo.Path, EncryptKey(QuickUnlockKey, pbKey)); + AddOldMasterKey(db, QuickUnlockKey, savePw); + } + + private void AddOldMasterKey(PwDatabase db, ProtectedString QuickUnlockKey, bool savePw) + { + QuickUnlockOldKeyInfo ok = new QuickUnlockOldKeyInfo(); + ok.QuickUnlockKey = QuickUnlockKey; + if (db.MasterKey.ContainsType(typeof(KcpPassword))) + ok.pwHash = EncryptKey(QuickUnlockKey, SerializePassword(db.MasterKey.GetUserKey(typeof(KcpPassword)) as KcpPassword, savePw)); + if (db.MasterKey.ContainsType(typeof(KcpKeyFile))) + ok.keyFile = (db.MasterKey.GetUserKey(typeof(KcpKeyFile)) as KcpKeyFile).Path; + ok.account = db.MasterKey.ContainsType(typeof(KcpUserAccount)); + ok.PINCheck = EncryptKey(QuickUnlockKey, new ProtectedBinary(true, m_PINCheck)); + m_originalKey.Add(db.IOConnectionInfo.Path, ok); + } + + public static bool HasDB(string db) + { + if (m_hashedKey.ContainsKey(db)) return true; + m_originalKey.Remove(db); + return false; + } + + private void RemoveDb(PwDatabase db) + { + m_hashedKey.Remove(db.IOConnectionInfo.Path); + m_originalKey.Remove(db.IOConnectionInfo.Path); + } + + private ProtectedBinary CreateMasterKeyHash(CompositeKey mk) + { + List keys = new List(); + int keysLength = 0; + foreach (var key in mk.UserKeys) //Hopefully we never need to consider the sequence... + { + ProtectedBinary pb = key.KeyData; + if (pb != null) + { + var pbArray = pb.ReadData(); + keys.Add(pbArray); + keysLength += pbArray.Length; + } + } + + byte[] allKeys = new byte[keysLength]; + int index = 0; + foreach (byte[] key in keys) + { + Array.Copy(key, 0, allKeys, index, key.Length); + index += key.Length; + MemUtil.ZeroByteArray(key); + } + + var result = new ProtectedBinary(true, allKeys); + MemUtil.ZeroByteArray(allKeys); + return result; + } + + private ProtectedBinary SerializePassword(KcpPassword p, bool savePassword) + { + //returned array always contains password hash + //password is contained only if requested + //check for p.Password != null as the user might disable Program.Config.Security.MasterPassword.RememberWhileOpen anytime + if (savePassword && (p.Password != null) && !p.Password.IsEmpty) + { + byte[] result = new byte[p.KeyData.Length + p.Password.ReadUtf8().Length]; + Array.Copy(p.KeyData.ReadData(), result, p.KeyData.Length); + Array.Copy(p.Password.ReadUtf8(), 0, result, p.KeyData.Length, p.Password.ReadUtf8().Length); + return new ProtectedBinary(true, result); + } + return p.KeyData; + } + + public KcpPassword DeserializePassword(ProtectedBinary serialized, bool setPassword) + { + //if password is stored and should be retrieved + //simply create a new instance + //instead of messing around with private members + KcpPassword p = new KcpPassword(string.Empty); + if (setPassword && (serialized.Length > p.KeyData.Length)) + { + byte[] pw = new byte[serialized.Length - p.KeyData.Length]; + Array.Copy(serialized.ReadData(), p.KeyData.Length, pw, 0, pw.Length); + ProtectedString pws = new ProtectedString(true, pw); + return new KcpPassword(pws.ReadString()); + } + + if (serialized.Length != p.KeyData.Length) + return null; + ProtectedBinary pb = new ProtectedBinary(true, serialized.ReadData()); + MemUtil.ZeroByteArray(serialized.ReadData()); + typeof(KcpPassword).GetField("m_pbKeyData", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(p, pb); + typeof(KcpPassword).GetField("m_psPassword", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(p, null); + return p; + } + + private ProtectedBinary EncryptKey(ProtectedString QuickUnlockKey, ProtectedBinary pbKey) + { + byte[] iv = CryptoRandom.Instance.GetRandomBytes(12); + ChaCha20Cipher cipher = new ChaCha20Cipher(AdjustQuickUnlockKey(QuickUnlockKey), iv); + + byte[] bKey = pbKey.ReadData(); + cipher.Encrypt(bKey, 0, bKey.Length); + + byte[] result = new byte[iv.Length + bKey.Length]; + iv.CopyTo(result, 0); + bKey.CopyTo(result, iv.Length); + + return new ProtectedBinary(true, result); + } + + private ProtectedBinary DecryptKey(ProtectedString QuickUnlockKey, ProtectedBinary pbCrypted) + { + byte[] crypted = pbCrypted.ReadData(); + byte[] iv = new byte[12]; + Array.Copy(crypted, iv, iv.Length); + + byte[] cryptedKey = new byte[crypted.Length - iv.Length]; + Array.Copy(crypted, iv.Length, cryptedKey, 0, cryptedKey.Length); + + ChaCha20Cipher cipher = new ChaCha20Cipher(AdjustQuickUnlockKey(QuickUnlockKey), iv); + byte[] bDecrypted = pbCrypted.ReadData(); + cipher.Decrypt(cryptedKey, 0, cryptedKey.Length); + ProtectedBinary pbDecrypted = new ProtectedBinary(true, cryptedKey); + MemUtil.ZeroByteArray(cryptedKey); + return pbDecrypted; + } + + private byte[] AdjustQuickUnlockKey(ProtectedString QuickUnlockKey) + { + byte[] result = QuickUnlockKey.ReadUtf8(); + SHA256Managed sha = new SHA256Managed(); + return sha.ComputeHash(result); + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..5e9a16b --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# LockAssist +[![Version](https://img.shields.io/github/release/rookiestyle/lockassist)](https://github.com/rookiestyle/lockassist/releases/latest) +[![Releasedate](https://img.shields.io/github/release-date/rookiestyle/lockassist)](https://github.com/rookiestyle/lockassist/releases/latest) +[![Downloads](https://img.shields.io/github/downloads/rookiestyle/lockassist/total?color=%2300cc00)](https://github.com/rookiestyle/lockassist/releases/latest/download/lockassist.plgx)\ +[![License: GPL v3](https://img.shields.io/github/license/rookiestyle/lockassist)](https://www.gnu.org/licenses/gpl-3.0) + +LockAssist extends KeePass' lock/unlock mechanism and currently offers a Quick Unlock option. +Additional features will follow soon. + +# Table of Contents +- [Configuration](#configuration) +- [Usage](#usage) +- [Translations](#translations) +- [Download and Requirements](#download-and-requirements) + +# Configuration +LockAssist integrates into KeePass' options form.\ +Options + +# Usage +You'll find more details in the [Wiki](https://github.com/rookiestyle/lockassist/wiki) + +## Quick Unlock +Quick Unlock lets you unlock a previously opened database without the need to enter the complete password. +You can unlock your database with a only a few characters instead - your quick unlock key. +Have a look at the [Wiki](https://github.com/rookiestyle/lockassist/wiki/quick-unlock) to learn why this does not impose security risks. + +Options + +# Translations +LockAssist is provided with english language built-in and allows usage of translation files. +These translation files need to be placed in a folder called *Translations* inside in your plugin folder. +If a text is missing in the translation file, it is backfilled with the english text. +You're welcome to add additional translation files by creating a pull request. + +Naming convention for translation files: `lockassist..language.xml`\ +Example: `lockassist.de.language.xml` + +The language identifier in the filename must match the language identifier inside the KeePass language that you can select using *View -> Change language...*\ +This identifier is shown there as well, if you have [EarlyUpdateCheck](https://github.com/rookiestyle/earlyupdatecheck) installed + +# Download and Requirements +## Download +Please follow these links to download the plugin file itself. +- [Download newest release](https://github.com/rookiestyle/lockassist/releases/latest/download/LockAssist.plgx) +- [Download history](https://github.com/rookiestyle/lockassist/releases) + +If you're interested in any of the available translations in addition, please download them from the [Translations](Translations) folder. +## Requirements +* KeePass: 2.41 + diff --git a/Translations/LockAssist.de.language.xml b/Translations/LockAssist.de.language.xml new file mode 100644 index 0000000..0bf6118 --- /dev/null +++ b/Translations/LockAssist.de.language.xml @@ -0,0 +1,111 @@ + + + + 1 + + FirstTimeInfo + Quick Unlock bietet verschiedene Konfigurationsmöglichkeiten. +Bitte wähle eine der Möglichkeiten aus. + + + OptionsQUMode + Modus: + + + Active + Quick Unlock aktiv + + + KeyProvNoQuickUnlock + Keine Quick Unlock Daten vorhanden, + +Quick Unlock nicht möglich. + + + OptionsQUReqInfoDB + Voraussetzungen für Modus 'Datenbankpasswort': +- Hauptschlüssel der Datenbank enthält ein Passwort +- Option 'Hauptpasswort einer Datenbank merken' ist aktiv + +Ein vorhandener Quick Unlock Eintrag wird als Fallback genutzt. + + + OptionsQUSettings + Quick Unlock + + + OptionsQUModeEntry + NUR Quick Unlock Eintrag + + + OptionsQUModeDatabasePW + Datenbankpasswort + + + OptionsQUEntryCreated + Quick Unlock Eintrag erstellt. + +Bitte bearbeiten und Quick Unlock PIN als Password setzen. + + + OptionsQUSettingsPerDB + Konfiguration nur für aktive Datenbank + + + OptionsSwitchDBToGeneral + Datenbankspezifische Konfiguration deaktiviert. + +Klicke '{0}', um für diese Datenbank die globalen Einstellungen zu nutzen. +Klicke '{1}', um die Einstellungen dieser Datenbank als neue globalen Einstellungen zu verwenden. + + + OptionsQUPINLength + PIN Länge: + + + ButtonUnlock + Entsperren + + + UnlockLabel + Quick Unlock PIN: + + + OptionsQUEntryCreate + Quick Unlock Eintrag nicht gefunden. + +Anlegen? + + + KeyProvNoCreate + Dieser Schlüsselprovider kann nicht zum Erstellen von Hauptschlüsseln verwendet werden. + + + OptionsQUInfoRememberPassword + 'Hauptpasswort einer Datenbank merken' muss aktiv sein (Optionen -> Sicherheit) + +Bitte aktiviere diese Einstellung! + + + OptionsQUUseLast + Die letzten {0} Zeichen als PIN nutzen + + + OptionsQUUseFirst + Die ersten {0} Zeichen als PIN nutzen + + + WrongPIN + Die eingegebe PIN war nicht korrekt. + +Die Datenbank ist weiterhin gesperrt und kann nur mit dem vollständigen Hauptschlüssel entsperrt werden. + + + FirstTimeInfoRememberPassword + Quick Unlock bietet verschiedene Konfigurationsmöglichkeiten. +Bitte wähle eine der Möglichkeiten aus. + + \ No newline at end of file diff --git a/Translations/LockAssist.template.language.xml b/Translations/LockAssist.template.language.xml new file mode 100644 index 0000000..2ed3b7e --- /dev/null +++ b/Translations/LockAssist.template.language.xml @@ -0,0 +1,105 @@ + + + + 0 + + FirstTimeInfo + Quick Unlock offers two operation modes. +Please choose your preferred way of working. + + + OptionsQUMode + Mode: + + + Active + Quick Unlock active + + + KeyProvNoQuickUnlock + No Quick Unlock key found. + +Quick Unlock is not possible. + + + OptionsQUReqInfoDB + Prerequsites for mode 'database password': +- Database masterkey contains a password +- Option 'Remember master password' is active + +An existing Quick Unlock entry will be used as fallback + + + OptionsQUSettings + Quick Unlock + + + OptionsQUModeEntry + Quick Unlock entry only + + + OptionsQUModeDatabasePW + Database password + + + OptionsQUEntryCreated + Quick Unlock entry created. + +Please edit and set Quick Unlock PIN as password + + + OptionsQUSettingsPerDB + Settings are DB specific + + + OptionsSwitchDBToGeneral + Database specific settings switched off. + +Click '{0}' to use the global settings for this database. +Click '{1}' to make this database's settings the new global settings. + + + OptionsQUPINLength + PIN length: + + + ButtonUnlock + Unlock + + + UnlockLabel + Quick Unlock PIN: + + + OptionsQUEntryCreate + Quick Unlock entry could not be found. + +Create it now? + + + KeyProvNoCreate + This key provider cannot be used to create keys. + + + OptionsQUInfoRememberPassword + 'Remember master password' needs to be active in Options -> Security. +Please don't forget to activate this setting. + + + OptionsQUUseLast + Use last {0} characters as PIN + + + OptionsQUUseFirst + Use first {0} characters as PIN + + + WrongPIN + The entered PIN was not correct. + +The database stays locked and can only be unlocked with the original masterkey + + \ No newline at end of file diff --git a/UnlockForm.Designer.cs b/UnlockForm.Designer.cs new file mode 100644 index 0000000..d8fd0b7 --- /dev/null +++ b/UnlockForm.Designer.cs @@ -0,0 +1,135 @@ +using KeePass.UI; +namespace LockAssist +{ + partial class UnlockForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.lLabel = new System.Windows.Forms.Label(); + this.bUnlock = new System.Windows.Forms.Button(); + this.bCancel = new System.Windows.Forms.Button(); + this.cbTogglePin = new System.Windows.Forms.CheckBox(); + this.stbPIN = new KeePass.UI.SecureTextBoxEx(); + this.cbContinueUnlock = new System.Windows.Forms.CheckBox(); + this.SuspendLayout(); + // + // lLabel + // + this.lLabel.AutoSize = true; + this.lLabel.Location = new System.Drawing.Point(42, 43); + this.lLabel.Name = "lLabel"; + this.lLabel.Size = new System.Drawing.Size(136, 20); + this.lLabel.TabIndex = 1; + this.lLabel.Text = "Quick Unlock PIN:"; + // + // bUnlock + // + this.bUnlock.DialogResult = System.Windows.Forms.DialogResult.OK; + this.bUnlock.Location = new System.Drawing.Point(195, 117); + this.bUnlock.Name = "bUnlock"; + this.bUnlock.Size = new System.Drawing.Size(100, 30); + this.bUnlock.TabIndex = 2; + this.bUnlock.Text = "Unlock"; + this.bUnlock.UseVisualStyleBackColor = true; + // + // bCancel + // + this.bCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.bCancel.Location = new System.Drawing.Point(301, 117); + this.bCancel.Name = "bCancel"; + this.bCancel.Size = new System.Drawing.Size(100, 30); + this.bCancel.TabIndex = 3; + this.bCancel.Text = "Cancel"; + this.bCancel.UseVisualStyleBackColor = true; + // + // cbTogglePin + // + this.cbTogglePin.Appearance = System.Windows.Forms.Appearance.Button; + this.cbTogglePin.AutoSize = true; + this.cbTogglePin.CheckAlign = System.Drawing.ContentAlignment.MiddleCenter; + this.cbTogglePin.Checked = true; + this.cbTogglePin.CheckState = System.Windows.Forms.CheckState.Checked; + this.cbTogglePin.Location = new System.Drawing.Point(364, 38); + this.cbTogglePin.Name = "cbTogglePin"; + this.cbTogglePin.Size = new System.Drawing.Size(37, 30); + this.cbTogglePin.TabIndex = 1; + this.cbTogglePin.Text = "***"; + this.cbTogglePin.UseVisualStyleBackColor = true; + this.cbTogglePin.CheckedChanged += new System.EventHandler(this.togglePIN_CheckedChanged); + // + // stbPIN + // + this.stbPIN.Location = new System.Drawing.Point(197, 40); + this.stbPIN.Name = "stbPIN"; + this.stbPIN.Size = new System.Drawing.Size(155, 26); + this.stbPIN.TabIndex = 0; + // + // cbContinueUnlock + // + this.cbContinueUnlock.AutoSize = true; + this.cbContinueUnlock.Location = new System.Drawing.Point(46, 78); + this.cbContinueUnlock.Name = "cbContinueUnlock"; + this.cbContinueUnlock.Size = new System.Drawing.Size(149, 24); + this.cbContinueUnlock.TabIndex = 4; + this.cbContinueUnlock.Text = "Continue unlock"; + this.cbContinueUnlock.UseVisualStyleBackColor = true; + this.cbContinueUnlock.CheckedChanged += new System.EventHandler(this.cbContinueUnlock_CheckedChanged); + // + // UnlockForm + // + this.AcceptButton = this.bUnlock; + this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.bCancel; + this.ClientSize = new System.Drawing.Size(413, 187); + this.Controls.Add(this.cbContinueUnlock); + this.Controls.Add(this.cbTogglePin); + this.Controls.Add(this.bCancel); + this.Controls.Add(this.bUnlock); + this.Controls.Add(this.lLabel); + this.Controls.Add(this.stbPIN); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "UnlockForm"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Quick Unlock"; + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private SecureTextBoxEx stbPIN; + private System.Windows.Forms.Label lLabel; + private System.Windows.Forms.Button bUnlock; + private System.Windows.Forms.Button bCancel; + private System.Windows.Forms.CheckBox cbTogglePin; + private System.Windows.Forms.CheckBox cbContinueUnlock; + } +} \ No newline at end of file diff --git a/UnlockForm.cs b/UnlockForm.cs new file mode 100644 index 0000000..c965e71 --- /dev/null +++ b/UnlockForm.cs @@ -0,0 +1,54 @@ +using System.Windows.Forms; +using System.Drawing; +using KeePassLib.Security; + +using PluginTranslation; +using PluginTools; + +namespace LockAssist +{ + public partial class UnlockForm : Form + { + public UnlockForm() + { + InitializeComponent(); + cbTogglePin.Image = (Image)KeePass.Program.Resources.GetObject("B19x07_3BlackDots"); + if (cbTogglePin.Image != null) + { + cbTogglePin.AutoSize = false; + cbTogglePin.Text = string.Empty; + if (KeePass.UI.UIUtil.IsDarkTheme) + cbTogglePin.Image = KeePass.UI.UIUtil.InvertImage(cbTogglePin.Image); + } + + Text = QuickUnlockKeyProv.KeyProviderName; + lLabel.Text = PluginTranslate.UnlockLabel; + bUnlock.Text = PluginTranslate.ButtonUnlock; + bCancel.Text = PluginTranslate.ButtonCancel; + + KeePass.UI.SecureTextBoxEx.InitEx(ref stbPIN); + cbTogglePin.Checked = true; + stbPIN.EnableProtection(cbTogglePin.Checked); + + cbContinueUnlock.Text = KeePass.Resources.KPRes.LockMenuUnlock; + cbContinueUnlock.Visible = cbContinueUnlock.Checked = LockAssistExt.m_bContinueUnlock; + } + + public ProtectedString GetQuickUnlockKey() + { + return stbPIN.TextEx; + } + + private void togglePIN_CheckedChanged(object sender, System.EventArgs e) + { + stbPIN.EnableProtection(cbTogglePin.Checked); + } + + private void cbContinueUnlock_CheckedChanged(object sender, System.EventArgs e) + { + CheckBox cbContinue = (CheckBox)Tools.GetControl(LockAssistExt.c_LockAssistContinueUnlockWorkbench, KeePass.UI.GlobalWindowManager.TopWindow); + if (cbContinue != null) + cbContinue.Checked = cbContinueUnlock.Checked; + } + } +} diff --git a/images/LockAssist - options.png b/images/LockAssist - options.png new file mode 100644 index 0000000000000000000000000000000000000000..e0858170d7941b0c3607ce3592ed84479517a89b GIT binary patch literal 138774 zcmcG#WmH>h*9D40fFi+NgBCCDE&+*Cf2JRT-7{Fx! z+&L))g^2r!=nzFY6u@JXpJzIs zuMA(V4E?f+GPHl$LRe5gtWKfHmp`ozX^&$6)_r>qM z6)B{X3b@~{u5FA}4o2UQs!s~imFzQ@?~XC=%M9DafXg~R>*1wOxN}r6aa}zp7Cf*; z909aRK7>(doLLN;*N(of1e8NQNkdnc5t*FokLAVr$_xbFSFyFNh>rfAZr^bi z1&QI%=X`zrr6qLI6>C4WU$@n5*P^mhL5u4Tc*ZL2k2i1An>Wks2YlGv7yB%uG8b=7 zc$2Ul%RrQsOovhnA?dbXIrxt?oHZw%ZB{%^$^4#KR_?~q<7wX(5y=0!^{V>Za#Ia` zh^uw0jkT zf3WlMM)+IZ4G*z_=eEwl@Mwj!iB7LweIcH6qFUq0yx029%O%SvCR0T()<5f_y(P3Z z&eCP?Y-7;Z*Y~9DV(hK&d3|*E2&!1OKH6gNK9_u}+o~6$wU!Hac?NMZhho1MP z?fv-N<%WJ{S3@2H1~0*zWp)H0 zLV)2V{y=)=#rU)d3y*N;B*PMo zZ;sfZd{1Pv{ab5he`03m-4R)Ccp*4h%rVdCPdnHv3JaGA-t5+GZ{wad;~_+>VY(}M z`mz!=fYCt@rFfZ8zfAfk6SDo z8yk|{RWTvszIawQ$A=DQsegdM;oNMFXtvJ@qVVI<&kYot%}LTV9}< z+|t^rucx={c~D{btBCV!y{oferKNZHf;*mu^G>2`CNlUo+3<0Y$iD6DiR_Wp$O0}t zDNb3(O4>5QcEhK}C2^to#3z<1^I^sB<)o~w5|P;YT5xgGqxz?dAr&!J9Y6a;x>QUS zDtvQGi&$=+dz6L}#xeiHl3Qvu)hnT~>sKro^jYyCe`P;kUN6{Z^LUiDJ}7UrOMjSFF|c&gP@c&WXY+efZG3(F^1RV$Alvrk zn+|Yd0>I2f^05WavvSSvX^Dk=j{oK9MgI_+xe&Co^oq!p>k!SF{`Cp+yvL!m@cmiq zNe4lpx&xseA_o=A4 zxvS>QMkHT!z4MnG4)>c7vZwGTwZ)T{%Lok}tN00^-NsW@r+L(d@9oNR*4O*`oo*UD zNgbWmc3%YtA5T%u$;8W-`z0?WJ#+ISy5$7RghDrqhx^blJS3Ez_|!h8roDx1nD`X> zsVa}SKLFvX+fywjtFs>NFISykUHL$AYkMqn2GUp-@2d++iSYVz>l5M@YFsR4{T|B5 z#M6Y^Zkj!-K+H@_8xtzv-Mgeu9N%B=(?vfWhP{l(T2%3r$bhe`zs^)GYMkIJxt+^d z%V|8_M%0l3zR$#$M0Ws8lVczgRxlC>H zsC=mmi<}OOxVCH+^V6W;@3Jo37k-Ap=1itr@ux4knwq5YxWksO>MWE ziTL&@KUFl0qmgvzbRl`Ioqx&4q6Iueva`ys7~s}HT(^%-_V#ImmZOM;|7bNcWMZPW zLS9E_Q@@q6n)Ji<<-&vT^Pu6&Hg_jxzqJB`RQvquP|Kts;s3BCmdw5S0z5jVfWJ!e z`M&Dq-a`1{p>Bt{e#uWwiRM~I%#xnD-J~yN?&ZEr`0-rR@3z15WzF#U)$Nn;%fZRZ zUUKKl9gFWvY***wGuiXj&C5-;-_6a-WotIH@XON5)5gj3xbX9_-%Ibw^8w4Rt@1+2 z<%UMm2C%m^#rRR{_;Xd~Q)}m4`fpR^Q=2BO1Ef7fs95Op;+uvvegbfizxA<<$$lQ0 zHSNeF=ZJ||Y21+g@%phkQ=^aleC$wT>wab2i$XX{`oGEL^`k-sUAu@S0K!PyTP}kj z#B4|S53*yupQvF$@)u4M^9;uUo;*h$YC*L`}R$n}sH`^nAk<$~<_ zG%U}(Vb3&oIfdRsHZfXzO@?V*AhShn)p~94NIpUm!KFmx%)pukg-sXzbKrktpbdFC zc8WpHUxi`mLeIXh;&hPzt*0_!5lvF3#Se7%eCa_X^lZE+ogMoyYO(SBxYvY^5bgD{ z;dtWn<~m3;Ug**I9x;32aW9^})5q_d{W8&0_DRfhtZ?-66q)qX1DVv*!ilsGt9xR* zAyaaa)ppbLw^=>G@rrF7xxr)cpMEzSzxLxOb5l~uO(*09^YzjN(<8X$S3ln%_Y}&T z=tYWNW8$fqOX;&03(Xtf3Q##pvp~`Je>107i%q>zz|4K4gOen>5B#rgJZJrmrJ*I5 zabql_{m0_l$#c{Lq$#=8!*>02N#d_1rutEt8{>7!yJ6!QGCP&{~ zb3j&oh_Jse_|W8&_iF~p?E*^h`1_OLKzT2l?Z$4wjW~HBW2w16u~I=^4(yOX%OYVU z_E+uQLx?|wl@qq?h*&@5PqryG)MI1LJll72wX+}hvY5LcRb2`$zUf{2)Kt*$a)s_E zW@$;2oJ3<=oOPfPpR}^I-M-VAJ~yTNS>s+opZ&`q9l-gf-z@%Zs*1a(hL8NTOp8Tn zR*@dnA4qh_$<7xBld^&AOm9 zRCu}_Eg|z^yBg1a`*d1YLgvl4)h?=9#5}*&+&OSzw6XrZrt{(byx)_@yuDDyeKwi= zQ|wLA^L~5D%k`I%6W^nGd-?YyrNeR*DZ`~1Y6{dnN#dp`Q|xQW;KY+4W=W%YE%mfw_sD|MJ_98=geqQ2w-q_ zOMAZjBK&kaXivuDTwAjxDy9*ZkQ8HJ^JF~R=644{)4P?GmoI)^FFP;y&%)1fXXu^o zey4p*L}Cowhvi?jNzdF~9=l)ezCh4-eC1`q&-=yisrTXK#n1i4?|J3M_wI|fiVk68 z+lQm;`}#JtldgQfD@|cv2zAX-Qf?c-_9#Id9xxh#C8Q|1*^lFr%$_VHEnvlhzf#Z# z4oK3xPSLJ{mHe-~kh(_JW=YK7VflLxzuBC={tBdv04dAEAV$F=0mN&E17OwTytO;~5qg1Ve`EwilDe7rIop4kBq6tQh zj*bxK%(CpULkrQL*6F0nV~0Gry#<9nYd6nZnq(b!+qyJ&F_O>Q7AI@vg%mM~b`OMc zwfy4f`u9I$SMIi0{0PX%Sdh86BngabBP4zL1A(9m)-P=-KSLlU6_M^~8>k5UPyXf?Mj1V>d@}8h zGIsFtAH1)ODAs4cS-m&|Jg#%5yGB?!Xx|=fnO?~SFWsboHsk;!KUIXw7kD9z| zsSt1}En$kwZH;i~k-PIwK}98wZlaC82k`YTqKZd{yBy^cb{R`VT^i`%F*+HKumm8Z z_m8`KRdiJGqP7O3woUQ zLFz8kZQ0}D$Su%~_;ty9@VEV0yk*KlkS5lIN~W2mWu)rWadpxH!Hr(~&C{;5eUK=o z(kHr!Jc1{Qw9oDQ3mMgU#o)SLPQLh&WloN@LArK%3n_gV4kY;j^K-YQk5^O;1S_1H z0M?15Kc;3`Umyx!M<2!-BJrCX)_zO;*q7V!)@x!GX8X5edjj|#6x!fz(!YBS9KQSki)qlb0re5 zK^KTbc4KUT=sef3hH^mTYbG`q%kS6sDBKH>a*TJH-(l9f$R}%mr9C%0E7F+fZ(;wx zBSk?JaqSXB7eIMsrQKRj$nW{XLfFGsKLOuuIdfmtcOIIaqB9YU6$Xgu8a8Ey>$#S-Sp1R7sy0z0Wy(mg`7GbGoKdmhzHjNBnT=Ks;Xx!D`uo9+kh~+hRqn= zJrYX~eS(n8uPWB#5=9<}q)+CvePmC!nO`B&MYHo>T|@-R6vXahAoT}bBOsWoV}*?6 z3Yh~S2~Ew*>#;Xc_*=sN25U0NeCFk<^=9C0MuT@l*_V~&Pl7wwq%q4*bbNG|(2ZC7oUFJ2kLlvd3k%cm6aVpwCzJX zM5#t|FW)}j_?`ZxO^#Nc4=wy&oL2pwj>exKUT&?MkiL?4L#8P^5KxEAlKc!!&laQq zCnIVt-qA5?e__u3zqziN+1VsBgUn&-DMrMwb6^ z6r@T2e>Hgi-@sEHMCF!C{O>un4gde1nFIG#l!jyj$&voKWx+2rW{bcC#Udh*1#L`| z3y^Mg-Lc7Iv~6r&lYH_2l=(&6HEXWs;M>@bx~|Hlolidm1DCwUZ>YSkWqT%v;Bvj7 z9H26Iv1$*#U1xp?a?4PeI=#JKdcPJYx$s+p%;2zy@ZSFeS7Q zTPUwrd~_tj&nceKGDV!-`#dqUAnz$nVy6=8$#N0K3~f0A$*8eYYq$+TI?O;RlQ zMlBLu(mC6%Nx4ZaLDMDXG6B@|#@GESI^|FG93Y(|C3h4Pj(pC?fTs>ut1CbN28_eF z1N~1;b%PSTQ3H(SUhzQHs6;qYY5+;!=x@mxj@4O~nHKbATAp`3f>lFjiqq zq}w`>hv4DA!nD6SkdqPwYkt37`xW|zMRqX|Zni1{wgtgL~hk_K=;t{neA&4)oo z*3(+skNBa0*8P*)pG5hM=pOXMMFcNTndEE!^*n%;Jd7kWW@Lnh{XT!|wM_7B;1HY= zWkMwqo&2wNF4*e)BL^YsSR(D!f%53aaTo)l{TjX`RK3a{R=WX4(kuQ!BWF2P!omnf z?3iyrc4CQGjN47$s6Ttgqh^+lh^dN~3^AOFCM3BYYas6M;4O7=YjBdnF~>XVe|&hI zNAcuWD5@=64Bn5#{8wtgon1O?!P9OKvlmlCBR=(ERytfkyWmRFDZ|n9mJt+8c%0mur@_xw)pF;eRck@gW;stbe8Lz2?IJ@~Km&RqGor`b+d zVZB24^UPVtL!U}x-v%Y%q@eYv1G^UNg@xx%Ho_PH=^MRQm|RN9F!2(!9F9LFD&C}u zAk%5oP^lSz%7AJ^nG=gp;&TV$2Yb0Qx}fp|e?Vstm48}mK_@nY^90DLJvwF)Wmjz; z4F-nZu6~w8??qw_Q5?V^hP`oLL}ia3&Y^F>ol3AGRFvKqq+?oJ1aZ|cZcm_DBe@rg+i@=mIT7n7qQiFhRU0)J95aw zam1vkSSu?eelz79spY{1p<1J)QXSlVq z(AW21s~Po7=EDMEK2A8&K)i0HME5zzm~xH7nYt*1mcB%b+&!$1oEO?1k6qN#(i+21 zX2@dh)42Hh*;`0Jbo4>M)VLqq`@j zxW{>IKy92-a>2oWM1l1aVVJ<0iM#6UYuQ79BkYIc^g;ike{7C=*+$?fQ&1}Vki~@U5L@=mtngp;;^hv znN4Bl#6(97#Jqbx11aB<(J(nCBAStF90*CHlO-70$u)Wjwq1)rEwx{tQ=!vz_L%b9 zV;|;-rtWv+jnusAWHZL6B)}@1&P!WxgQETXDf4%qtpNrAIokR$80+s#A{H^^IQLw0K7O4_S|NGqGVKU1(%xN>pQFJ*>hjj4H| zX+X4CaJG20D99irzYH^^)Ca^!7Xv-*uGOKmW95BDI2sN9{Gm&PyC#WHWq>(dEG_XC zV8qqSffTDR9>5oAG}znoi4AffLz(Jy)9j?~DJH=i^>VGITRS;Arih#pPrPZg9n&)` z=Df3!b!l-r*4HCJESPkR^!vb*AO8;BS?LCV%E{hwy}torD;)2x55txD?Iu&B+N<%z z_@@*i^I33;R@a#khqY^~7fdc{tP*#uzWed;WTBwJvR9kW_1*0;q#>X|`934!piw+S z3u07XRmW5WNc(r$#^nz+=41hog+f=Qx3rTeHor~xvEdv-J=}4DG~^Vtqff)9lA%2a zSsAtAVrhdvORDg$17O9X(fsX~F;x6;-x3s71caH%J14$cX6B8tLEmFK$0Xo>4HJnn zI}&-l;Yp017M>0iM8cd#+i09o#Sv_akp>Zn<7MNajX+R8&H(tYo%WR>C6lg(lii$Q z@@G_^@uH2j2P+1<5wLP?yKPCzP?4_9%wckF_1D%q{%dRdgboQXVLE$C z(G#hP)P+Af%;z!C|nbh8$Ya$Dx;1h=2o(iADt+b(eidc1gaG@FtBW#P0kY* z#SRj`%2B6FhYo4+JcLQvU#sjY#L$e*gyKV8)ewbRi3xi;B?uH|o8~^nJA>-12%FuW z4hZzO4SPTkhoD}^00K|*8ohRsRMNo0#Vw17)3 z%R`KmY4H$Kla!>!MZz-h{$cso0*Yw-E>9cxB)zEbk7kwH%( z2ug+Ft%rz4tG-~0T@PC^QIfJXnZx3Gqe-1L5|-$qSV9CB|Li0Y$l_EXl3c|^WmMir z1jLbBK*>g@pphGwGz-VbwK0IWPGj6t))qe#J_UY7v&&-ISUl}083B(=*)|tKrd_;7 z**dB*Oh1awR*g@9iZl%_71j-+9`KCEDL%*Kg=PQv9FRxh-ij13ypYT^9E{fGNuQk& z(j81SDaMOugkdUTfS{zIG6`~s`HwA;+@8~2B`R(48G3Fr&sarZ1y`}I=fN6A6_g*X zJ2?oPg~#rGFt>sq*Y?qm^=zKToxuo2;kR?08xyY0k78tA1!|YH^(+O>TG691C)Wjs zzzO-<9K1L53pMP5hHvB<^(&h?MRb&%5h5GPuGlet=ZuGAG4-z6b9l;6FsuwcJkVLx zb*Wji2W6b1JnTyhC9++i^2zd0O~$JJJygA zE+z(2|FAndS&k)n)^dlJwm|C0a4VTwe7}N3oO-arkY}T^58+=~>M8qg$yEh6qQURL zJg^X12`$aDQmEpv`Cf0AN9FtHoW9=fgcPQpb7iWqd%jgZe^10nk}=&)wiGZJ6;Cr- z%im~k7j!dRqR^m{OKooGlNHS8dPvx*X=3nw~XR8)??HL_0Di5SUD7h@^pmK`8 z8wGl(g|uvLQK>0Dmjx|i|3*ZWMbN*?E*UccSg(01flN^9gn;Vv ziVu^4$9*!AXCwm?jrvm_uvAkn%iR=>ZmSjxew)bO$%3nuHCnuy7wHO3AP+mQGMu=u z%#5bPsZQdyM5?`f+!Us36tN$>=nGw4YD#q;FumOWfLsh2`c`on15^wn5z75a!e8B% z|AlTpy!&(F5exZSxWJO&Wu55u>}Fdug%I+vtR}oOk%*=; z@*2|jQPkBsXl;^+FJF~@I55QLykZ+-~+H$Q9(&XLb~q6=Ld(?y-Rb4VuBpH(B|vX_+MlGpy!@^R9T+5r;_ zzW|g=eDp~t?lrvJn?L|`@p}gssv%3&RjD0Y2ADJ4qRnTbMFfW&wCX%{xp)nm31}hf zPdEuiM@lB^rIApDlx1Cr&BpK^Vveg&=m|vGwQvnaS3SrG?+!#vn{o!$^ULfz{XcKM zvxo%3e0H0!Pwe+oU8UKvT?FA8IN*2>q|m{{i<5;EzKIRA_z9y=+`R z`uio)_4^Fwjl-zSU}@Z9l^O%Set9Qs59lNvach6)l)Q?`HG~%llmnL|C4ifZDPwsA zg7ek5l~#>xoVOhcg8=elqW&BLO}y?LJmrBXZx(Hfq9U3#nJc}sZE)<4TkB81kK%Os zk+j@P#s6n)v6R7K z@Btnwmx1agY4CX3J9ezDB9`2)%`s;JHQia3@DgVbTD%dI4Aih*Vl!Az$2bxwja|^x zO+$A^^%41Os2=r;0*{WVlLe3bNf#D`ecH_zO{Y-XKx2#VD^U2;w?{;od3ITZ%DDiG zIcW-y(~ycUiwwF=;0f}uYvWHEsHZ?kNY*4ELYuaaba)Sbw;$(5s_kd-p~~Hi>3Ut6 zSw(~OXG!I~u~}W6QPu> zXAxj;1>#@EN}B6+Nd-klDk|NWLS&mx0TJUPj>=p&KTE+jn=ET4zQj0$rf6z#*!qA% z=tjAyJ(!{bkce{fBuu<^v@9djm6SdZMzymTOvjo_80ci%n4(oMQ1;MjuX?=w@V#h)t%YRk5 zelj>b=1U@*sgVfHE;)pY~hue{7r+S&-OvTN`>Ec(fn~;scsw`for_EtTM0Clc zWhWu(&CTg96QK^oMM1ILSTT!!)U9;!e>Go$SKdk$t>NNa_|xo6L8An8)9>r?C567EHy0& zCa&`h%% zn#4F{xQHNZB(#h<-q0<}F7F(2W?BkgDAb)b@Ai0Myp+DAeGJBr5r^otv^n1j=oC;r z`ia@i$X0p7W5T);9fjkjxCyZ*sVR zxbfj83=y8>;(WQzW7L&4tFwjYUrn(`XS0b=xCaUMEl;t+>g6l?X?$vY{5-}9U@2Hd z0V5D*1{-x1hj{bCQ7AsifpQqPgB^{NggH}2KZ4iaV9)y(J+piH2v**9EoOg9cwYnK1 zA@S^QcPv<%{77^0wVuAO)Y@XRf8I~6OJXuS#;o1A{GQ&twZ(LGb#!%QtFHbX1ag57 zQ71?(tf?Uwj5VBZJSr|*P<+k`r=k59S1LCTdO_FArsW1+K7c%@M>wS*u`uBB@>kZO3Z;lzE#KY zI6jGQWS9g&AL{Meg{|e#veo-4SzToH*cKCGFOj$Chu%Sotl_}>GpDtW5l^RiCc0*_ zH>Bc%-u%E=#ks_4tiW0E0c3B$0TpeoIX;!9z}Rqa}pvDEsuUho|R47=)h3hjZzgRpxyI<_2vk&bJQE ztBo7XmATe4@UIQ`Y9RT#$3bj9rqf}Uxw1y3OpuDh#Dq>Ne%e%a@J@O#XnQ** zFmOMLEh0D9Vrfx4H+rW0V&i)i;|&go;YODuiO4MBRubr(FUvDCB6Uw z3MV=-8CrQkQAx-InaM$tiM6BvRLNW;=#NptXjur7=~|5h23eG0&QSLdByRRSPBJLS z->*T7nrf+4kHFv;FXy9SE`;mI9|F4ru6r6y6iRZwF+tKfrb5reDe4yUt=76ho~II> z+<~`d)s@Ct5MD^v=H@tMC*bB9S9Ku=SuXj<6ZA3saaJ=ZBj{R zJ|$h(=gD*6XMQpaY^=TGT#aY+IM8e7uJIs=uj%&oFe*Hpg}L(J(EIf!irw2R?}yX1 zjndNX+1bl>uU@?+*9x7D)(;Kbe!9+uoyIo5!Z~rv>A7TH4pBr8PM6}P0RN=FI}S## z<&h*?k_IlKWj#)U+9}-?c0=6yzFkW7#x{j0g)CxgziKUFh#jTI0Hje9(_xbs5h7=| zc8|{~-_eb&74y*lCrnQuYTDXrL61Kn;i%co6(Wwi%=m zsms4qLU68sT=Rda z8YPUGVHc%0HBrbTuMx(>fvDrF;T|!QU^SCdoMGndjEUB(*9-gu>BP`8P}pRa0~3gI zaZ9MFLv{4mVX%HBek=e@CgL}V$-%2GHtm{+N2pM47NJKenyUm%O~Ou{U}mYvfc;U? zZu6T43DI`|NTi-LmEN`jZHsipPn$B*ToXT_}#vTiqDNl0Re2#@iw)mB;(ad2HX zPS=XX!+K&#q9P(&GE-7C)zlUyC(l*`%#VTidJv!Ek!Aaw8Wp53RN^qUCo|n>)r>gx zGwjYScQF`6kMDEZY|&;oKqtDKB`4SduJX(oMk9XEM6pr`Ptn>;73>+l0Zk8*vdXR2 zsh2U6aLbAOAM5A?qhq1eQ_-A#MmKiM3!YSMOv|Y5HN&PLbAa#0$Bup76?Zlm@*L>V zbL*F1iwTUBUJYV<#4TV$5)8*qq;6MAer#@`-lT5W&NB*+s5vf5u>O%^>Rq4XU4(eB zOhnFRjs`CpA;A7&Jq}w=31IvxlD5>T2yRU}#>OW?CSvFIb#nZhJRn&_l#_3iAbcU% z1>h}Cf@t;1j!{rz=H5sAn%P8xke+up;)O3~377m8P=Hp%P?QJVB@6^;(#nBNYf!$#o0``WV9?*%1PLdNj zH@7v4z9gHQCTwl0;o{BAOfFhm?j7#B-=40;)6UO#*PPYT0apJ zFD3D{K%vD{jwZEM>20;UX09N7>AE6n6#Kdzhs99hDQqYZAT^<)7%UXs$K&WD=FhCq ziTRmn#7M)>r}IB5!KH7A00eudj8@Pr(|w8E0e+*Zg33N3)d-Ds+_Bk~da~FCa*sk; z7#-+WmzFs(its>t%?MT7?~^>cEUPEbGtrzAG=_w28_vl8T2Oq>M=$8Y95y`z7UUg9 z)2PsdWxBhGzRE0MOhqba!$R69I_0^T7I}0Z%rE#(D`DbpT1`Nx$cQM9D98m;9kbOe zII*aG!QZ=~vva51Q3&S=)3iXSYY}VVN0o9PX&p4Psu7iCbZSOm$JZoy!}5nLzVnfF zNPZ@}#w}v5sXp)wuiSeGJv18FRbj7wPs$fgbf8AZM89S{rKYJ7B_#1%92c}u=LxT# z>h13e*grEYz*|pO-7M5>*r=(^A1(B2Yj5G;DBK*oxVj4M@48PeKc7B7g^`e;FD$gK z8LO&Fn{4>-G&Z_+UsYBPqLWsCZ-e~K4_66A9)d^#m+pTS7V+Q0I6fV0h9aQn3)`491?`#|a z2zq+ZOYaRQF7&e^hCW$~$Rn)xJ6=g6$-?m7g?|Sd8+qoEl^|{68)BYXpQfD{l2b!U8wkX^l}a< z%mWtsj$dc$8iR`Z5Y%-t_Gn67likiJTYDOCVBJZw0~KL%@(P5M&x6rmDbC>$yRc3X z+XB0cNxCiGPiM7+nVo7MQt(Nv{K^Ga1*1_8^fvq?(}NFd2GYXxSW`0;W(*#&R%L+J41%alxx9Ou9IGB|yJb zk%S)?hk(HC?!*4xFHLdRjNoM3e!18!M!m$!ms$)5{nlu=F?h(g>GYHigjRePoNddP zMLToo3!m=|{(~>=SAXU}O_)4AFotXOjOKp9VI|}Bk)_SgiKENj%k97shDz4h;Vij^-jc2UT1wj4kA73a06G{($|lVN_dr2-eXRil&DHiw^lh|(6j>-=t6 zygv6D5)%9UQErv`qw)$&4D=<|@83h9tj(>euA!l#X_fD z=1FAY@u+6gnVZOrvH>>V=i=2Ap#YBm#~lmnkW5_piDHe7!Jog!a4L} z@znN{l?l9SYma1chIn{ZRywtt8ZZA$R@@t%YKLjz(|2Z3bAMgn5)=Ad zGt5tx)TcS>HuI>dtXzEJ?Ky$IU7obX>}rc%-}y4Wh16*Z-0^m8?-`kzIwTB*ugai% zh0PMk<&`DKCiLNBaS=$rju8)tP%u0<7o!*xTQ7wp&4pe4%*mY`onV(kz`{uNzKw38 z+R4|H9q|%ta%oyj^K3ihzS1oa>OM$Vi_TH3ZjDQOf>TwRqL+5fneN2;h3(T*!5wv!@aY&mexcF zimbBo@K-ytoh@|@O>S<^C21FcrL_r1N=pl=n_CpTV=O6KA)ue8gn939IG$Et-v+2E zDr#nBCHmomfrf^xwzfVbVwIKBnVGQ%`|}eM$E3W#t)n^%2#9PkE|MfSJJ4SFY{5Io z=VMG6F66&8iV)P~JlH^vvBbL7R~?UBcY|HO^5P0G3oFS6z9;#m>(iDUHz7f9{*$*n zg@M~de%nvJY}m0*@6DiI;cLEsNCGV3S2M=No z$RE<1_y#dA>PRGmNc!x6)-qGNb7@NvN*2mmXO0|SQ}6yRsU4^iW{CWlOmw)8^nJ?V zDfa%dVb#Yvl&b)I?ycpLG(n;0?*qGN)V7hF3U(rocqCnlcmQ;`Y8iZU^6iW)&t z)k?sslwA`bOybo{)}qUKU&0JMA&c5ty2q4%fQ~xn%KjFo{D`OJ06~occ(yhw>^K7K z+R3l}+NIuxMBMB3j!j~m>~Cp}#!8&r%3QC@ZQU4j6sT@Jb7!~U`ndbkdS?ujA)Jj( znuSf2-tT5;wfZZW?|A9J#`0Z#qie%g;oU>;$*HU2)^tmGVjjOsh|-Xb%@Xh}Us=#4 zq<(LI0{Io8y^T#6Dg|O{ZP|gNiwjs!?>bj}jWnxuy8P;c-Zl)Wv71K?@l-K&wC2+l z;Pr-AgR;N~(?d#OYf{DYzTH*bsHaM@cVfc43UbT{@tZ@g3P#Ismv#4#O#DeCMP7PF zcl2-63;$2EtTzNie6D6*-X&(K+rV+=%*`^U;2yFs`97q_iBhOYs^`0%9N5^@1 zGW-p@?(6=*LrSteSQQ|fAyET~jhMenM=@OT55n~(7 z#Fk(fhy?8`gnX3-pe*}sP_&^Zr=VC_4TruQpQh=ISV_nDqIpxEnH0JR5{s`woS%w> zhcuErO7jZPs3`5|>E|Rt?1yepZ0rKB<=+Pp(yL1~Lm9rp+k&gY?>T4x>%zt+zX0p1 zs9T-etiX!v)W_88x*Z;7{g%1!OwQw3;lI9Y^z_8#=c}%-e{lN1(9|+Hzv$grSQtc= zGz5tU#O2$%>@Gc5+uN==g$Hz}5yr443gf^8J1~hHRK}6j!*NCnn~vl&oo(D)f%VAdar1 z%R|LVad(hMmiK|h!FYu~yWI7w&btY{n7PJw=&nfF+lP21gJ=rmYl~Ah8cs`jcA8xW_*UI001D*4Yas zWbD918%;4L@5T%6By8c{h5w66Pm*@-iaLA3H1`d=c3bH#OkIPy?dP$viM{V< zYYx`dh9N_dqC%Xdc3oBdrNtjVrY9$tC!IT+Q@9O#ynrj3psfaR^L!PzMTeF5FgN@>&QKCVWKF=;U5gD-T`0`Ou=)yPyHd749FF4pS_^k6$Z z9rj`-*4r&glrbYK=d4HwkrVXsK4Wa8i_?MlEq#M7sf$ z2y!woQ(oN$ubyjnslWVl(fu}YVEG*fac;J#pPBBKJ(AY=^|zaaditSJYkQfYImqBy zzb(ZTGDP+DRW~y;@p)cfYTU%#SMRFLE11H5^P zik80*(T#ntpkuDAsI&|1)7MA8(`hDsLnCtvfx6Jp2Lgh^FK-Pm&ga{$8g5j=zJrta zShPpHec#>g*5^7wYR5^DDsyu1uJ}0ZhuO!dA9p$5MU(xWru0j7VKQ5sXD9T3xzuMA zaX0)Z9Olbw@sh|Fw6p`%vmJsN|EZv|A#Y9%j zKbu3zM#U7qaSe@9-T#%h*I|e8CmhcR#l&jl*J@vv;@HgqF1Ty~A=!1SM&2LcX)4($ z5G?a&hNR)xOWUP0DfWfvN*r=jY}Vs4TB$i{x(Zt0hTul1%BS1bn+BV6(dp2l&`vJ} zMYnTGGex9YrK|Ht$RR)P{Eo=!vRQXMSrv+QS5!RFhW!^uz9Z~h=UHHK)v1185ww9z z`=vri=s{deF*r19u5M?s!)FJbIGo3EUCz{Wy+W5aHPx)J@T#HVhLBK)h;Y8Ci`tyE zyT7(Jw5_eQzJ5J8*jvVXLrTNu^(O!=x4n7);0`s-_DZu8H}}`P^74+3j)wO3gEet* zdG^&uU}uD{1`VNv+7n`3st@hXpyq{vW2!I;!h#dE0b@ zgfvPCNJ}?JOCu@qr9-+qq&uX$L%JJ8K)SoTyHh~l?Q_m^e((P*Sd01WJu~-x&207q z_LS=qJUj}wHwxOug{2wqm0R3skpH9L7-U0jkr7~qvS0~WZ{jbk6kr5N$-d17op+Q) zHsZ8snzFRJ>CZDHrYN7Ce#6iaDE<*d$lbkB{?}OMZ^8hTF%&5+3G|fyP~2V z{JX2GzvtwXx3n}D6>$oDeT5t9y_lA(bTGeo-8{buy$=zz=GsJG9Q zhHA&u=p{a$+T1jIW4)8DuT%)ExyRG}QdAV8lhfVhB^|S_Jdfks$tBaV#roB#q3pFb zIDxI^5*gkV$v`TuOnJ!UZn~40lT5G#>}38igt5*}kzR2zeMHLgzuNG{Vn>jO=R89J zl_`nLRjbzaZ}NBNL0CcUQiG8n*0+p+HK4+f{qZ#nK|)lTd{A2(gleB`a32j;@;kJ`ubMF!cO3t(${yg zv$N9B5CtIsb(5T&jMd5aU+$d##Vsulrz@$`)2-^2g~5S6S=@Fv<2m6#B_`#4>2z;9 z9Ezhpn$b!~nBw|izgTZUSY4e0j?Zd)2I>JS3Y~>{>H``z&JUV9pzG|KuvnO#t-gB^ zonbNS$^?!}TQ~BGTDyWSrXbhM=1`-C={2(Zu56)evevU4^9-bt9(|LywoSA|OVe3f ztukt=DQDov=^(}oqi>*6T_X|GBY`zxPI<^+@ajx%qpISt2>ZWjMz7YaTHvSkKyS}2 zRJx4{f@IoB!icCM#NH@;YdNX%`cwO)aGDV?zkKU^6qQHm0h=xLiiT3}>rUHYwlqY>N0h6p0jaGb-u$ss8 zffw{|RVcp_6Z+xi$LD8BhS%^9yd3V5m8ca}g~^`JViHQ>>DAnX8C^|peboHhM~Gj# z^U-l^!uxU=gbI)l-nYM%6no?GA5i1o;K$@Vq;mrDKTMsSA8G@t~qJx!^z1oCWthT_tRhnWDkVi z!#${_<>bmg$6#pCYG zMacd7(>G=T*K-2vf&$!3L9dx1i@U*u*WpQ)+I6l!b9>~+86E3gvG%EO^WX$F%ICgK zy9h8_IHvf6u!%646%rJ)0p~nPRHYFPPnxF)a%Qp$-BI@9dud=G-M;-h&Y`IPP8M`S z8Ro5EcOVCe+lncgXc{oj_Fa&3;VULKGwODFH<>-4yfC~ET7$W5wYT&)9hw-ub2a;6}Qr>8NH06R~}SO^d%rj~?+AWSwHUE3(23 z&Iohe4k2ihRxiyqO;loDGoD0@c2aBkFim}#oz>S9PqF4kczR^TL&eJK{i>>4#hu1p zj&W6o{(ZPMA>+6Q#2xjm&Y)uwy&(B9_ zHO^x7o46-*4HzJhBgPk2)0??wrQL<`|VecidVj;oA*nz z4XMF`J*I{D_1~)dL+x?t>80}xI8k~&~Ln^OrBo;mnK1bpIiaw-xTP4guiI@39jmk&sqluH9;kBEupNX*D{_; zQX*sU1UHH-+Ec%;mXe3TWthq*N1^}~M8#H?!ag{q&->`SF}YlNv=*~|rR(pwqg$|5 z#pX_}-33r!M*}#XJ~SqXao>Rq4C%UsM^{DfGvD zB|Gu%Qbi`ql>TX(D!qEkK1fs9 zp)x<8oTqvHc;N^dIyn0dL~A7Nhx_RA4mhj|wHlUeUIW*JGJB(1)t@I8cZMJhmdGe5 zg6@~w3CYRR#9TFYUfR~y;^4luu$aq}Byu{~_}Ld3d17V;52976dnS-&+&byJ!J9|d z_KrPvuo0al^A}vA-H-f~ggCqLu9Y^}5-ncG!Kl`a*#k7Y1dWvfy@!x)5!&5a4&UbP z@1^3l5o!=tX|i#I<$wc&5C5(3V587cPm=5E^d?$BVqk_#9uW}7i8}LdgPQJNU%0xZsi)LL~R!2(1?vxhbi?(#uDW&5|mSSKJW`#1|`Aj3ccmhQ=%Zdeb+_C&k;mrX8x|>IfTw30~WzC z8mi_b6G4?8b(@hvoY%)UA?H+JUOcDTmV<OtCBMo|wk7A@jr+(pl4dWI_b zjPPCTpVII8amG0F6ZLe|SQonvpO3~0%R8-a0*3#6} zRa1K*2~Yhlr$q(gb#}Va0{-aU^qE0yezj?5m*p496yg1d zuab&gubyz?sNG{oduO-0O^bSblEE(^dUq1e?XYX2qGE4psb*jx?CvflFYo5@8KK@Yztk zWU)$JX}YMDj7a8mA4Vfi6OW2Y^YXfT&4=f3pfq^0G=@$5v}i%BkLo+P=`o9pI5bG4 z2q`JE92h8Wb7f)SzJKeO%HxnYmfY;P`lQ;T^fy-;7XHkaYRKywCg2z}8H|U=z({*~ zzRMEyV&vpxUZ2h0uuw6Ia%{#p<*o4p--{{AbeTM*7ux60}t< z>o%h!!Qqgq5Y%JNzLPd1D>{2F6JZ?@C+U(}=t#crDw8TkVI9RK{738W)BoI%8o^1y zp!Qf`T-{#ZZ!k|dTdePolNmC1k7O@0+aOP zgO;ew(tjy?SY!enkf(`~ZNjTqup;j9!l)SJ!|`pjyKeOih)}G-P>lYY7yU79n&uh} z%f=5VKE>p3*=P@g{$UeMUi+izV`rG?qwV@n#-`W25vOXvpD}324$i6?$!xw3f|-(F zdS|ss7u50#3Dpl9F-Y2e(#J0zp@y19SRdp~n> zH40?vAZkNgDs@Tsn}*Tb4LUrW+|v$ZsO3_HtQKPfQRjI05aAKC`0cmtKYbEWQ89OP z)HE^ziinbm%H__Gi}mWTyi{{qobpGN?&*Tb$zPzE0QAn~Wnp@H$xxEAh^T_~{JMo{ zqxb3)#}%|Ht@-XKgvV{rPygq)2ofGEQ0*`=2?xKBg#5_F;Bjy&t}Q*9$dgN`f_cfZch+Y(k*))E)@($QHsSxVU((_Lx4Dk{|)uQ47)#p`N3q=V3o5zifIdL1m; zu*vUFeTcxB({Y(7*l7CfNXRyDyBE^>`IkK`v-vb3j*gkkz=G+4wT=Dwx9apCei}k| zW{#!5!YeMZa2Lspz_;)}+a%oWN_{M&(foBsBYJ0s=)Eg$(DJ+6UP4)J={|k_h&`u} z4T0Ra6h(buJ24arI*32HkS3I}W_PyO{x~vTuO0@s@qis6H`;GeNcOC(x`Bh%arZCE zW%i&U*|+bi*R$VM1lRxf1M`CXus|t)Rlzg;G=FOAI9?I_{>6lI- znF|pbzUH}(7s_Q6f5nxTZG4=hz;b)Y|Bh7Qzibl$L5B}&GMmkgwqCnlCL?j+NH959 z)>QR))N=sdZ@fo?$H;U!y7MsT=7ak0{%lp#So6nK5gSdOs5O>>s5PEcqTY2-g>@mN zJZfb%0mJ$WVT|iM4i6cQEx?ZO2?!Jk4I(E) z`c0I9mooW=B83j;eAy?$;cPpJr<;@rhFF|4uB7_9!)YmS zym1cd3jf2o)B8_<8q37NAjaRyDNN-FCS~ldQK!vys%GbunA7s+a`lJD-GjrP^-5EW z$D?!S&X8GO6l4{E;JtF4LrEU<3szM-X;umu4Mx|=8gbN&KG7Dl_f4&5*EcKpB%YpU ztLK!G73sWATgMAcS9>1zy=Uns%CnUy4#&K_Z>y84=hg~$1NC(inY zC^t%q)b|0d`K>;M)Kp$2Wz-j>?d#jyh8C^%v&Y7YuOL{=FowkiG(+6h@u9wq_;STE zCgs0#!vzuFGq=A@tX6n4o*4UG{gM0I-3{x8DDx#E%=KaHVMQ}tGV7G^f9E(tpkbBc zcgxe+T;SiiWOCcv%61pptk2ekAI~l^u6EE|hPcV%jmsHy4e)WFNys;TluOa?kH+hW z$730Gs7APGO0Tb*79Q4ADrBgZVnVU3HRTDF67Z5z^$LyfH<6EX=ZvF$=bQX&gpnef zGH!&ur6KPH=a-jl7Vj;|87#IcqL8gaK!U^|rZuYRhqqBqsfE%~@oCFs$fZnu>|;$2 zvNVDkbc?#^YhB!bF6{OtW=y2gjEp${yrDymb}emZI7h4bcP@id1)>>vqhbTEY z%^h&I@6QXAzgYuf`^$!ifsxTAkl&=F0v8r`0s}do?zWOURCHE9sm;bGsS^^Xd9Qao zFfuJA&hw3=)OyZZN>;E`pK%+SjA;n!cP(R47Rsiw+uWGnQA$!MDTiB_FP^P^xnA#V z1SHnMsrN7qi-7lah@7?(v8SckfwUYWx9j=SuN$4KUB)b)A!zd)`c3ib=Ehh>uWP*L zT3+`I+~kMl2JNXafz8Br_5Vcoxy;w+b6oRhV5xcy2yG=Z37?5fIDGi8z`Z#Gk4pvB ztBe%Uyp8+=e#;yRVU?TPv;@UmR!Nm5SlTpZF-!+#VgT&a}ct=-;~m4({;RN^QI=Cb@`1_|jEzo6#Q*h}JKR*l9(BRWDtRV^l!Q%+R3@x0A|Co|G6$1`C)N4k4ogo)7}p6m#gtnzCE+{e zV1hdAKNfc7T{uiTAKn5_`KOJIHlF#hv++}by|HwVl-(svOf`>8MRMc5jQ32YD8VMoc4}tA_ z1e!JR@Y&JS6K;pZpTinawI<9{4`^#%oKr~=uh|Um&j*wX7kPOe()kHt7dY7+9i0Wi zZM}4Q+6Q{smR8b%0~vGQgNZyEC#P~dJHnitKJ{|@6oEedURjUdUL~|U;tmXudkdq3 zz6k6z6Nfs9q=md66bxRKzlAAfxhb<)nZ$3hF}_R@DWA-Y{EA0#Px`@W>Oj7j#)!RN zF6dtS(cs?;8#CKGpyOQH7rp+zD%>kw0MQ4ILEujY)$+L(5_=a_BqV>uMEnj$<>d;{>JorN9Q9Y#zY39}g)v+}u^I>OuuDXJbP>&~sAp^wcKeI|pK<7}q^CeAVeKlt$Su~nk+eocP*=HM zE6K`Ao11_9_z_}dwe;Ns0^kt9ldS#tQNb<6A&~pzdP6a&<{{aN`;KD$OzdMZvi%* z=67th49t}!RaLvE5D6(MFAWVtGc!?PVL1bX)rYIOMk~HHlX4nvPK$=0Krr?>9E#Eh z3RFO-gJe`Xr}J?HJgVBm!%4d z13JCY+0FgW94#cnP@_IF#J*HA9j51`=8bs0IhN1UPpTdKoQ;82o})q)4aPF1VkE0evDVz@uN7ZM<7%p4C~L>cCwc!|!@^20*U9^qt{$d8^@6wV16d}`f52IPjzwJ{w zLxPsZfS|s(UJf7*t6Ts^UrU}m&w@Jm$`X3V;QY(zn?~m zin=Z4PM6y=65``C5)hsJEyMl(zS@r?~~F@927 zqFQ=_YG#tUs-hy~6Vf_bVaw!XP;4whllPy!^6eG!c;~E zk%W);b;k;rjA&~MtExI$UFADDnFF2pe6IB?8VLl@Yhi}&AT&LZWt?dLAzoModg4{9C6D#V|hxgzuV1cs4P zgR;Hvtx;0f-(JLg#NNAPqR3(mmZf#*NT%0UH&k+R*Hlcd4H5kPy-${}#y&@afC&kv zn{W~J?xR6%V)C>fB*W>LknOnIq=Gy8DZ$m@Gwb)Q( zg_V^TJTJz1`S`$=M;x5EXc8XaeEnSVn5%%8j%NKJ#7_#h*tEB&b8w*Fcq8;fDWy}T zJhM|pQ86yn9k^WaI5dXNCw{WdHVZXQKM^W9ESBfW>>TVbo$VIzFssfwe5>~Nmx_zw z&s9W4AHn^<&*;+h2)-%X$?A&qu6s(zCg;#EpBb=q0c)_#*D}88xXZ-e?aq)GYJac@AhelUXxrW-?fdMuD z^{Y^sWAz`3ntm7&&kH8MuxCCV9Bu~OIPloLh^}P%$3Rj4-OTE=xEUH}`AuG{&UWLM zT$-QD3GbI*j@n{kKm_{*=nGJj0=r92?oLaKPfM%yYXfKD9JoZg3rs=fLCQaVmKb?^ zzVQ=0H*tU84T~rbCX3GR);!Mj?t8+)uGByt9$YuKndasMCB0a%)8{p4!8sBWIPh?1 zj7`ntVI2uU zL7e?4gVHkeQu0jFQnYe%j0)0p?Z5%up9nuaCC9)3(1O#pLz2#WdVa)oAkP2a>;MA6 zyY+y)5yxkz_g+{*%SaeM9*|oxqQ{J~I|3F_aFSnI1&$bY70a4i!SB47=6e}w2KiA$ zOlKwH&qRob!uo-(_d_4vf^XbD)(pf9SvouWvm5iph_jH#)}u>Ds9igw63 zJb&1F8Q|{P$Ax&RNXH>SNHT@y*-0RADLpC3D`slUYmi3cgf4G2nZm#yj>-H z>A2k;Iy6_YA|>}dEdvr0rwruSPW--&Ybx4zERn@UMGg7+nW(aW=!gWW{#P=#T6U^} z_1lwUtwvU#?(XG?2m{xW+}wip-`>i~rZT8YYRJMV$Q9FbjR}dfrp5_0H2A%JdwqTQ zZ{ED5dh=5EM<#fF2L0Up#=P;NVaQI#<~@F{FLbfYzgPxQu38LWlg6eMzuLzV3EB;i zG2Ub|6~|*tO~z_6SK(!}XGDoL7} zydUc~!bfcrNYXl|SDrup-WJkyadB~T>xm@32*$`jL7G1T20i=r@Ir0%TtxxMA8XCZ z=_wjPTFl0#KVxFzDB}ykY-6hndYUHy#X(+BS8GsFVF!~n35hk3l=V+KtJ)B+%8H6t z>U$hqF6d#!A`YlKrt$Tkkmq(n7WG@yTt!q!U)fz5zNadC z=ZpO9WSx%3I43<`C18>~koYA>e$6 zuZgZC)BVXv6LK*r0lnSE)S)4=XCKX+1$MJ(#nr^9ctQtDHp$-5gfdw_2bPG5Hn*_* zRJ@YQHFx)~fa}=$N;8dNjw0-{zO_vCnQ}EQ=MOP8lU#~9qHG2NgT(y`bOF>qRYBS6 zmC@##C7@o)rW&=HC#ZkuPr2H|`S~-Y#qrki>EWsoU`61o2FgJIhv#<0 z$9#0&leaTB$MPvrwzf7FhSnkthEWEP4d@Szjb)siEac?0tgJjB5D%~CXYhJf5JJO+ zva&Mb8g$8D_6a~xIXSqaol>@y&CPkaxh#z3@95|@z_8j%>lnb?RvXWrd|~lH*RZjR zx4qwFcWb%tjGB{m;K%lLKKo-5s|yj;zZ)cFS5?IrR;&P&(>pf4(HfEg{gTdHYkBz+)#um67lkLe#M(_ z48;XhCuK}{B(F(t!wtiR-x%b6mc_0~DPxzyewO;RT+yH^EN{e(*~9LwoDW_E(HBvU6-;xRn@o1>=y8HHedyM-42uOpU|1D{ITjk|T6u~EpDgFCm|t#SQ+MX z2?GeMh50E5$BK-0-NI>etIcY>I|ZNp zU;b>lIZa>R4h!pog+&UGf7H~wAYD*VuSSLsHG2ze4JNRYadK|mo}>e6O+%x=%?)>u zj%PWvxT3DEy{#=LH@CW^1Uy;71c5{F3Ebck z+ALf#pO=uKgM@}sYjX)ZNg>JBny~pwQ5A+D^Jy=>|0s%t(#tFIhvG`|8A%mEEC#~h zBZ&HdaOAg0IG1oeiH+i5e%?aCM#xT0Ncft>U&IJI1)DU!{|I}>xp!4%*JS# zHngCsKK{uc5P>M|vJkE&`9l9C1ylK*CTdMkx?L6xxaBiz^qvBm^CT2VS?mv!S=da6 zZ9layk4!n(eEvLNZ8!&B&S>)F5#VXA%FZsSsVN4yX3Uu>96JZ{Mxp8xg_gR51LB*a zGRumh&vcCC%nW7H3d~K82eYf+`<7Q4d3oGgTM!TtNrgPQe+{S1S5-BgpWpBAchjp+ zS}*svIMLtrMjiv>5mYCUVbZ{hYyw9U%nhV*vBkw+va!irUDDYhD1XFail6!{yG{(X zb6McFi%F zrvqEw{%@mgTbQh0XYGV|A=3gwCZ|{E2wI0XFVk7-&O5`zt>FdpM8eLBu~bSBNd`Dr z#p#ZW@*QkOaqf575b_zJ{$V>t0_M~QpVW^E*<(ua&|;Cc!XzCtREXLr!CGn;k?tJ* zu44`4B7Eb+JXh5)bAamOMSFbTR+KJVyse_qisf6Qg6}sf_G1vQ{E?* zGAt|SDKK2k)mO5h;3;oB zFk^2#3*Q%WPMzdfh{!N#H#tw_)&qwV#Bj#UFdNL$j*nO2&{RlDQ1|uhfbuy$iR+R_ z*a{uJ+*2M?85Ad{Yps2W{T!UDmBr2KYO{)piajohbl)Amz2)H%;^aIzUKnb1esys^ zzq`HNY?oJ=G*^+qr|?S1o0frriIw$YvnPHi=?sd61*|(36EhGKgTW?D!F-ir`(Qr^ zr0%D=oxumNl@mQ~ITYj^Sra;@ir{EltS>eg@*OF_er(~jy%9V;Sy}yE)eK$;SxF7Y z-}>Gr0+ec2$NBj&K~i4V{(uXd-QrgKf43QR(~ooyxKaf1a362Sp&b{>fpk_ZGiA!# z!L1!8Ez8N;MaD>I!kB_7s5qy1P4wMQuN4XlrAwKTHU1&b1Z>Hmt^9E3jy{c4N}W&D zE~5BBMoB)lRZJ#cOUWd5Xh#-qgg`b_;h;k+ryvZ@oJeaQWqE2~1&GSt3lV7{}e=5yQR;sBiK; znL?!aSTUHm3H_$;sB~JLX;@f<*lJ~ehmn}ef_*L<(mFcEpt5mxc9W1mzWfq<50AHj!>~qDI3Z5w>>ETyaQt_iO0hFwSts513@r zR_v4??`ukgFq;RSs+)MO)IKO+W@LRA9Ukkd8csifB8Mp)3$U%`(dBoHDCWeJLLX$D znSRH~EL*;I5QE$_+2?~e(|^QTz=YUQgo5x61CtOAwg5iYl!#D=svjGILE*qyM%*9t z4pE_Za;VJ6P9oCU&#eK;0h!eoEoTrDGXy&TgUFA0-T-ye^$(Xj-+VKp4#|gqYR#ez zHsh!6hb@}n4BipzfmpfI(;UPXIuQ}Fe>=^?89uOCM^cgohJ|GSB(cK$m%BTvprxIi zUjAuSYu7_q0x0YgfUOS>9s&jtDE}3@V{MMK9MsfQ)YQ20;CgIzWuam?p1s(@2krLh z$*VTIyuDvH)@fXih;YYijb~j0adUvpcKYRb3^-q~1jWxk8Vmp%+@01+Ms)*hhmxQF z^;{nW#FdSv8p8#zq{TyM4U)=I?$?$LupNq7e@_AEJI=FjnRtBZzN5=+eMSY7K z@JOs}J~!zqPo-w*Iz0c%0LQ;f=Y-WO=p?zC)3A4Bx+nPG!gF)R_(_aErBYkpa8$|@ zVJf~@8&Mx7?)wN@JfmX%*Vq94b3lfqEI6QN*FY7$fejTd->0XbAJ$+r+SiWV48P$f z3(JQ6SR?5qMZ?IlBZ8`m~;sA`}RQqs}k*1%9u#^vQI zG3m77;5g9m@;-D0YV}2lpUm6MPqBTTqK-xmrOzA#*EPneiR%VQjjtzd?Y>yba1q+iQd$6xbg$gz4b>mmjeGH797q z1d|yP1I6>$l&lVzU(yB_3n@V+52QU|bzuj=^H2x46z_FZSlNdIx@N+$hv8xriffhl zr$gkh3nH)4^I>ktBHInIRi%G!v6?alo#u39FAO-i;Xft}tEtz)=L@wdCi1+fd{#-L z8XwEyi6;~|#w8HK^11V4v4-&wKIiTL%U4Qda=XU-Shh@}35?n7COunwh?iGH!9Y+{ zn6#WM!{}HgBfSM3vke`+1p{rdy7!~*aS8;&_$4*6SOzLR9%v9o@nG|ixY!%8Lu~8* z!a_1iThRW^?zw(%wV^ujWduBKXTfj~Ku+Dy^*-62lPio_5p%hhY3()W``_)3_yR6Q ztHE-mrf06Fr*9V0U?5)83pOlC=fhPBmOr1FK%$+n>uL@=9>ngYxmQZ;0b8GY^y{5ItD zD+hew{%R~2%zma_^B1z3M4X9F*6e%f*W#1aB z271Vr&7Zkh6tm*CHWr$1PrD<>0Dm}HBW?D2lma%~>(@Ef*5ph~OAtt7YwOwN%@V*R zEYDU^L7e&R-?*Qx*82KBE-%AyTH5N?_BuVgE0~%Jx!k?~^5^R-zFT0)eCdl))LkpD zsAy+m(t3Wr1Hb$A>rC4#6L|vzed)m!(BH7J4cR-;$44g8fk}+5t%bVc{r&C!{_dip z{Qdp>aQrv}18gwE)aJ~{At*@Ccw!Bl2@?}zY3YTXp#wyW02@2xx_XG9+u*X@mZ|9b z-l(j+aP0q8Jf&z+cJnbWxEIEWf)+eaCq)D)B?fWczJs<8sQidhhlJe0szmm6IOUr} zCDt{@1-D7aXf~a%d7#Rt>3(EEr4W|D4|yJVAE`v;BtM80J)RFp%gS$7&t$)qJ6w?E zRpjaN3}7(El4W=ebQlEx1x{95_C%}UzNy6;6`9BG zbh(_E%N`pW1>|4AwJIxzv$9HqxU#YiDk+`zg56|Im@e1*Jpvx1r;ASJXRB9|(GppF zE|heKw>v|YcG{kvoZKnt+K&IgI|tzFy1rg+c06bSR$wF{pV#obM$7&GR{}*4_WgcH zCw=&l7j2p+N1%Dd(!f|E^VGM6R}2p8$q-gM!IIW{!bvvAMp;&m3ZCm&UqUhW;Yb-| zm}%Pl28x+PQ>a5}=dgDFnCX3K9GbB?zd3SrQAXT>@WVbDIbnFk=NRaRw;IsNxbzT4 zHt0!Jw4Un}q(g2B?YC>mB2$ICSO>;INQnkm6YqIn(gbEfNz2!eMU5hI1Wl5274g}S z719fr-TRF{;B@o1Hy=AClj+3S;h0?E@MEz5W7>G)^HYLnfmw|9^RXzujb=1?ZRc^7>>+IXx%2M0?Q*d9C-WLKVlmL$7v+H5#Tn-Jacb@ssov#vtwO9u3wj z_%9vF*std&^7OH==C-z`uC8Qcqz4@w91IOBUvb-xudMVfEYQ3=A77~DvRNGl3b?c^ zgP1QZV>vft`5T_*{ocqjHd6ypktnNG>?VVrUE?9na;?3coqC6{%-X$C$PdR*#^Uhs zq3@toU969&s?q{QwSyh9W{uHzFu*ZyHzgUB$?JYB3-SC{A|TWGC=s0jA3=ynz!XI+ zgx{FDqs6=+ul8)etU|ophe&hQ@irrJCKBQN*(W|lmYk+1Sv0@9%;c>H`2de(6D}6n z9Zl?c78-2#YY7v~0mm^SO45xVwv|u)0*_Rzc%PuY4SW`k6n#?_A{;jf=`2&3v!f(` zn~EH$P5&|~CkvP+6FT~UQhW5Z3+`&pp`qI*Npsdl> z))5oy0vEts#Z9uH*F=9zoJ2%AyV;~RU+WA=0k@|D#CXoAhfX)5a@{F~5%gXlMxqvHibjLL?$nc+AoGqF#qrb+VDFkG&Go&s0fLOjvh^JEZ0w z#W(brDQXv*7?t>Fh?z6o5j)^{&w!hwmYwWZ1LJ3oibzv+K4?!Zr=y3qs>Igw6*i49 zKYKK#(}DAEzrRdVeF&i?Snuwu+(@PB0NU4;*H#@{RmwGs9 zlKUKmm}b)&*>=M(OE)_%F8JGClxT1+laB0nJIcM;a*_`uq*+N{6~;^^kVAtJ!~+9XTJ&S0|5;PrZJM5fR7k=%Td#%Xk~U2?NiEKQD#bkaT`BTlu*X`fjZDPXtj$ zxWv~J$wIo;db?7Xsc(>g(I;_(X8eW3_w;nCgN^;Iq{-KV6b~iE_hZ#~P-@cqsAW;I z*B|pqRlb~yMaQDiz*RfMM{-jrC=*QPZTf3shY{`i!l8l3cHVBr2n&JdAT|?uzx%x` z54c)SuvBhWlV@BGX_CqhF~lt)`PA?!z%u+BH2xiQ*w3Jh{0L!E6>-sWjqq~>V-}zx zIrFFWP{QN?_Uo|lLn(9B-G2D*M`082+qC;_8o4M9Iw_0OVO<~?g+f4hEnv5)2}B)7 z$IF@013)(w7rOvW4){q|mnER-2n#a@ix*0_=X09}2MbeEJ%CC9I+B(4443QA(|Ac8 z%~y%hL;_oBCL)GkTvX%>|r(c{#?x zk@A3au+_rCg81qZg@Q5<7RGP3Vx>mk-{pNrr$6Ff+Sz+UMBCAE6Ckplyh?{)EE&+! z3_5Kl-65f0eh-;`x!=lKdtASI@#m@MTNe5&9{bBSHyLmx*t)y`W`2wLOya}Ut3HKf z!TYpoT~D6P@Jb1ELQe6$y$(vnrvw+8@%==+ow3es#XK#z3(tAi3L=jj@G?bvkieB{ zvHg$xhcbE#$Zvr!`X%`V+hlpIV`x3qugiCr?VoGG&%2@js6*Z~h6^&Jlo%lp#Evr1 z4ic3EW1skaEYBibrorX0X)T`**;~!tRW<${9=rgx>=Lgco4zAhmprIFQT5%lkjb1t0KfyI>ZHlhzwiQe{fTS>d8hWxQ!?dVr$G$_$hx zqi>i?fG30Y=Rh>=5({A5f}xgY^^NU4kB6a;Pr|T9qM5NOVK4ox%|RerswgnZ+Wk~b zo|#!Z1vjY=Ilw+Zz#DmlQc16eXXmvsNH1fzxc)Tzr+J>v2j6J>^Q|+11wJhDUoNuy z2mY@teab@b=aGAm`$y8vemG~i0F+Jnn*k*SHYH_TZeAR-ZaWTf8sIL{;#GWv{^%+y zI)TZ5J3B@&DD-!-G7T9n%X2VSAnSU60`!XE;mOHKg#PBjLKdMv*0RG1Crd6BT8+j# z(tC1gPi@~_z#`*??~lVdqF4aD1=9+PF_bL8py9MaN#+{UA5NwPYB9(DRF1zO@$Z3e zKLua2Z%>T{p^>uK4Nf=LnlPpc8vedVo(e|KI-i-#`|;$?uJWb7YGC(?oOOyyTv?DGWnu^A;&rYop0v;3iEG1i^ zG+rc=tdkJROJ7SAH(8VGHKX^zBh*vcB)r^5FDEf3G;%{gG5c0+4>tQgdjc<|qT5)^ z@{}d4g5A9C7rg#Dn7+mQS?-JYS5Q&E8fkG zjYfU{p&<0rG|q>nCOB7DxW`{XzaGzE!6(w%49~~o#-!W6I%?gM&U1TS1Mb0_p~_yg zUzXi{{_PLPSNh;9xf%I7;s1m1W=EY;@X_ENUpMl%N&g#L(Y^Wi#-*2iYii(AaHjo& zamv~vjyVP!?7+=m!P6@H#Uk5a_r_CIr}y7v!sw;Xq%0-lE8&vG^8JBYeKp&rbd=`+ zB%j*sxi5N{a2MLX`WU7q_5tBj5)pM9Q|x-ljLy27VFUcy!v#8jdcO3?J}m-SM0~1n zNkGBUgn=_y7FGwk2@2(ltsIk-Kam|zC@pIG!l2rSrNGNI{a|sZZ*~{R3@xFrwS)Bf zua7S#WWx-Ee>Umw5Bnm<RtKwpD;DIXX@nf>Nn$^66nO3ys72S;LLdiiF&&5O96J zZkdT}^!AF~wP?6y@~JL;vT0Y(w+AT=cIxegsow7z@+!t*@+qi6=Cy;CEa(iw-t(C+ z_IA0)und=;4pghDWqS&Hnz19cx4dMs=&rBW!J{@{B@FJV={-t4vaTnG`EgTg>9oCNH@|5(%p@Oblr4!gLI3ONJj7>uC>lQ;^3gRs34x3wXW0Eoe zMQbmjthDJSMo0Ue@0hbxDm~;Y{JQ>^+v7iug0iIj)_Ro2)^qYFsABnfdEwzmgj}ev z=gQMsTaUuRK*}nZni{UHjW{@vgGVPpMOy=)ys?py^|7&m*;%gYHxoO+h7; z=VD=$d%vydD>tHGj#58;fGZ+^u67jjJolk*^!To%HR5S&zow$h?Jo7HTMri)@YFVB zAh$hGYtrlDY-V*xU_2>SzI47r*lOy=q3Qzvqi<1W5%^qoJG52HjG8=2X<(C}SD}sn zr=!U-Vc6r^}eV`im7yq~rsY>4D?;hiKYWN&6SQtnD&gpq%Y= z@(Sy4aCQh9R7()vS(=|>6>)z=0JsoJUB-^_QbnRjTL~IBY*5LAF48brYOs#_h(zXV zd32p%bHQ~QenG7e%z2C8<-Gzsh;%+<&Op2^8voHSA_-g!3~RLhMf~Is{iK`ateInf zwou7(yMB;X`7@#x?6;FOlhCqlri!0It21fvzkzn!e+!k8!9!{z^-(Dm^Y@>x?tX7K zXQNq=Gf<5FwV5%G)(2Am9z4hcLNCrc3soF{78BOs71A1U36im}V=@05t#W~IA|P>PJOT%5U?*=j2R9**1DXfi-s z6ED|T0x4x~|I$ce zmEoiJTznm~?=;g^Q9^>+d?p3BCWgKmyi417LL!$iZO6)wcu7TF zpo1bCnqg zB8Lc87OmSHY8Qt}P}hL6k2>vFpi4^Q7uXL?SRTqB1w+Uc5KqZVYbJvIb(VVK zsrI0h1u|23Br;wydUYsG^#_?d$&V2F*HV$KHi#MV2OPZm(#f?{Tb;9l!SwI8aPve4 z)K(`N;mNx#T{b9^LNzq~=Mv@7{{REn&IT9^hi_8{F!=w`FJ}K5wiEClUa&}3$%vU+ zFltwK&AYFI83VJPbVQi!_H?2O1qHxg#q{)GZ<0HnnJks-HW3g|0ioJQnjQSUK9d{3 zb?pC7YvbhiI%&O=Y3GW^NcQyUjjCbAcd{O8->0!a5g=S~I+<&iu;2jX{_#mjfJl*n z_TmhXTbpw3C_Xfhm-ms8@h~s|G5+LNtJ00`$oq&fR+vAy~tK0}0hy*6df&Hm+#(LsH-A(0_6qE&6&!kkXhl?$ z@9KZjSFxqQO|{__oU_FmthZ}$i)Y@qE>1nn@zp67PuujEG@&Quv9RRs*8f_SP@VdC zoL|T?etB=BATjhj-!mi9rz^8LS!8@*reje{6B!+gPEU!cz zCExh`SUgV@3Q4%^CTdAKul zY&pKd@B4fmgly|X48Z@M+mg73#F~B`q=k=xdVtkIp`@h+N@K@o@3PEw1Nd+90N=k6 zPsAS)`aa>#;(+pWCpW6&?*16yGMJs*P=#FfJUuqRLl4TnACZyEaZJaqmRm9aB+n(ok$e_0>FG^}`X6GN& zUSp5b-p!&%t?yZ*kRpC2GGtaPtAq3_kF$S? z`X_zc8r$@srcC;1iKF)A(IHl<2)9xRfw0@%;N{b`{dMs~D#L2Y$B`9?`*yl*h3i@8~_kJzRNDE~OZo?)8U088P%;|4}ks()VBnT;|F+nMGsL!xkCO zlmBsL9_e4`%sc-3)IGlrF-jSy5fgebnJJp&=CEMaHaV&Nl-`i4D-F8oU%zVpi8}D( zcs+Xy%qPX<ihD=(sCdG6kWn!P<(l7Yh3}d3&05FktSZ+Y*e|z{$?p$`qdoo;=FXuRkzG;GjnT!Zm%nKSnHsC zdek8VLv`0W%~w*8fc3)jn$OipaaXZJd{)+S6u@ID?oBcc6V@a?Op{;Zp)eC&L4&C2 zS9dt(io-NzEY@3wk{C*iU+_LyJ#@^(4*pl3@4fqUZG+&ohrML}O`S^)f&I`im%4zM zn7xIGJsLxbIX0z1T^KW4HXHxqD@43m6@*q%h-?5-9r@j&2>bLO#c# z6bXglpEE>j9l0_p4ko|GHWq~&xAO1o{(;C~9S?sP*-1$;VQQZwnRg(IB2)}N1w7!= z>U!mNy2GNK14Afqh$UJkKXM{1%;oekeN9e65>mi5?HXv?NydU#-a@gw2o}%!B}PIX z@GUI%pin%H%C7}Wn;*S0tt@aUv*Iw>q^OoR!)QC~0vdf-HmteV9qq`lokSqR?(^kP zbd=Du#jh-5!!KOc3#T&J)T1dJ18b8`+x?Nx0J7=+A7pdZWq+%uCpITXrP5$bT6$1X zk!1)h6&TB!yc%#fIFi5^ns9i8=wz)NkVL{lLe|@#ZkydQT=x0E>>b&2>X&gWoG-<- zFgu*R&vW0PB3x*boW1?>L}tg2bp6F&jvX}@X94i*pWJ)ShI+m!k_g@>giCj4I0n3I zNj2)Ky0CCxf6rXEf8SDvy*SY?e`gW0=xoS<;yZIa6;n?{qWRK9%|bsYycF|lr|*xf z)#-j?Ymt$gy@k^AsJXABjXhm{=|zK95z6;a`l9WuEMBXUPyCGWHamrjgwW?KCu+Vu z5(JjpQjh|k0Ck0HzL>U8Q;^T{bs?_(;v7Sxvv*0u;$3P@Q+Jk&v8YbdfCrI%%;I}b zRr0i`h%!!Oe|2ZhHhbBRuOqGjJErD_qO}Y=$CI2~ks}h^FtMo;#{KtNF?B&e;}eva zXX&~;_Nrn*L;XZ5;}c!`kXvP8S)-Fk+acmBn_Ocz-6R*up?z&14uO_6X$%->VvnKi z4Tp`bynj={x7^857lq&PtpZe~Z%y*Gn+*p@g+;O+IiL0Jrf*}Zh0&=MtGE!?;)RFM zN&J_859|%lswO!0BWZeP6(||jvtmfji$Ng-kqJ4D`(eYOL{be)@EUhIVpVQ5svz!( z<4$BV`bvE8!_sb|kbGt9kN*(Vt{iM7zarD0RK>U_@Y~;Mo9q^a;8R&H{X0Tx`C!;+ zl&PNWXd*MT{0)m8txL|8B)!$%R&%nCzp^j{V?*}1o9&@opx*K5pJun)79{1+8t?qb z?$KIT9SDP$4IC^Y4dZnbcdAwjWqeEW4f4~~twrp3u;ANucY&|nf2&Jbyc5NZvAeAi zw#+a5kFkou&vz__9WPyYq|bNZBAIA+7K00C+m_=QZLW*eOn?8r|A#R?ni&I-*QBKP z^Ocs=)VKgjytk);`$o;huNcmpT8~syXDK?^nkhmE+@iDVrm^)zr%FNWhR+O4hNgoKLm@qZLv!0kJ~a*Ar~ z?^UDZ3q@2Q5r%6IGWB}s$;nwdIQZ-ywQ+Z$8S(bL4ZdpK4qmTKWacAGd1AsF#!P+Z zf)v9NAv=dbC5fNsczNEvw_(u7E*#%ts_AV*@0&YrN7~Ovtv$}}G`ha-#>wV}w6w{k z9a%Ph9Fo!B!#$9qDUZyPp&5H)Oins7p44n(SEG|^5Sef3mSnBh+5GD&QTPc<{W!hq zcZRd3$0F0d1ARtYxlqoI(eUWLkzILtR7_I^xwelEN;a2a#9Z)5qq}xzN$*}&1&o%$ zdIzfUk^Qw9p=XSwGl6GpBgZIf>Lq+FbjgEI?mhAcqEUc&@sO4nTqSF4*bR;+^IWd^dz4SsRtR83rpi?>>)mFpF^X#S+a0J6; zD+(#lroG;DM(HIsKEjg1c5$nvk)Pvm!deM3izXongrE#Bh5Yblj{jw!)SowJuau~K z%%vmlLZpu;tqpIy+K=H~z)(e}FFI}tz26L1;(VS~>MUx-9w=SzW&(**DEbl0%Lu*P zAu}1T70BgD&Ivm#Q>J%^#OfP|x5M=xI-=Oq!}lCa(>iXQQP!8edI$7O?+E}d+O~RSW%%)to|%=*Hl7#>LPdq@ z$OGVDjKBM@|45~$J>oL!aJU`$vzZp6ld`>TS&Amx?l6Yn^V@ELK?hi*y>bVl#I;~g z+SwU^iOB<&0RZ%*tNV{53%o=Y)}8@>k=OE-D$oiUS7^M=DladstHb^dlqGwlq?HVP z0if{rP=0}-(QK}#WM6w93&)ch(nUzl<8@^HOCkhqv+a77j)6O zY(pr02FDn~6&ZXBnc4I>uRccw(Ia}Qzn6x}Dnwg) z-DSmdJ$5Xwc6j;uywEUbDVIamf9(o8>!u5~%+Av^ZYz-LzDt4%{%jK8CN+yizQAR) zEO}xYf7_8xv+*h}9;9zmJNTc`YfFFU#qJAF6s*fe##mmTsAU-Bw@~%i7x2iIMZy_@ zVmL3h$W-k@(>tWF!5PWzL86xppy!TOQ$D@1K826s-A zlmBFM0b>f@mTPuk{>^`bexf~s^Ic6ni+k@G%giX^%fr(S@5?KifZzxc!P#;9^r(q; zfH2l?iUD)}%ot}Lz+I;)Bxk8)XQ>QBh7_>@iBbs(lWz{Z-K)L5k0BwAZC(@2Ee?iN zCgFelai$BGe)_9z(oH`y51S0z|@}^)OoC4(r;u9Fw)xty5Zi;@Mp^0UhDH7hb=u*@nQi!0ywm-@epTWMy6gskS;lF)DL@kIyYOHhq zLFtyY*i*oU8WNY4Ls?O3j&_QgrndKcffm~`u%ee?#CmMIGY^&O&F|d0ETNnhgrCR1 z1I@!tKgV%{Bx6p^9WTihKlEFNuOP#y$h_8grVJ?vWB!yvR&Kk$Cm^RKhselWOBqbweE^i`j(@D`HGvsanx3e=V+!dkS1hf8SSC zbqFLHR_l%fzYSbp9k&q^r{gV_*VO5A`932pzb;K!sMKwm>RVj&J)ZLcKuv>|goJc` z9rf9jm6d1e6_E5#Ue@yo#`N6r}Yf4XQlU@~V&@@|f zNN@6rqq0za?K)970qM0TUOovs^yvNigY(0QIh&*vrcc=$9<_BoxHxi8?$NoW%LoJC zSR!NnMn++i64z3!)XpU|BQl=}yYjKIr5wMEu!G9+;z!c*^Gn%PrbV2(ml}@#rH@b= z@Oks=Mo+&ZiVHRc>>MaV{5tUryD@_=Kl_UvpBeg&)uIzmn-rp1* zudUv(?LcXZP>7C4zw8+IP}lH@5c869mwgkRm4;UfQKGnIH1l7DbKbijH*0UeH00>* zVeUDfzUfN^>kZ4BFsy%zP6@ce&@|w@pbx079#m0 za@;#wD*w*F5V+8-9bt+4{>s`U^${>T-KYYaLbp$73oSg1^z>|G+0k-x@l8$YfXob9 zXFUrO*;wmPV(g?-njOGsj+Br9P~vzX;Gt(DkzwE4)8pdMlV@gXIeFYyTILs*mJkOL622$k@}Af`SRQW!*!|J*^1Bzn zKF`Z*X>h=!q{gMD!Gm^ky2E2Bc+3Ch50Ebyis1sxRT6#_{^WN5N~WhQ=HwTbUkJ!i zmt;KQA;Rr_q=`8x_kM;GL4>H(OoArYEINW38X8aVRg)A2NIuUBLQ&EwXDhs!Bx#5L zj$teYMuSS@XEltL?7E9l{L0T^!Muj%_gAL--pofa+=L&{qwwp3Lcv18`+hO` z%SkgV+Xe=OHL-*5)m3Ac_Xt@9Gh0=FJkDWIb6yA<#Qei-&l}O3AM8pUM#}oH20CTb z5(}K&WETHgvg#%WVQbAeK{ft*H|_UA{Ni zd9-DuqEE;HH=A$Oe}shu>4{WhvYYHjV3L6s@{bbJh$b@&H{M^?IZLWJbS7sMV9(`c z6i-urbINd~{=bdwCOqTmJWigom*)DBx=yPn(?poUBRAl&6#Tk?NXD8yHM zRMfL{YYxW3K(Vy-&wtIWAb&39fxa+wEm=&|F@I?1QUOo@6y6?rz^CYq+;($2Y^~Ic z$zum+E+2$M?KkkQZak4(q2)(VUxO|M;WXgHm(w#!CgkohK+?REc9Xdg=1NwKs;c8A z{90#cT%k+2KRq!n&{83X0rQ#-t(%O8Xef7~OY)pG!V<=<9Ru;E#rQNmp6m3jBffV$ zvFUGKmykL&?>>!=F3oS7^sI2WU4obl-JLI+6pj5|wZSrt$~=Zx2oxgnw2g3-m6w?D zO5{5+6ANqXKmogOAIerBiXo&pmUDfEmK z!cP&mF_r#xXYT|M(%4?_0>JOh7 zyRon~)BDlB9i0>iLHu3Cu1Tn_9tQAQKrn&X+B$bwXNCn#mnKIt)65a-{}h~+%c?TPbS>`F?@mSrm2=JT{ck;FFyOO zO_5!=RH#lVQ<83`m+5;|Pg}8n(+Y}~LNO=OTAdoX5q;_YiCNU*wHW1pjcd9CKV;Xm zoTE)<9bIs7^w=uB;px9QpBg(?f0R~9*`)qRsMm4c>(Sbf55YQ#onRo%EoijJ#-T;T z9DazGfp_0oy!#lY9?E32D6`#?FqL`)WqmnueasWO8>W?fucnenNec2 zWq;Dq1sd1f6-G~c36MOY1Mqc5j2{yX>0ZTJ-^a*IibTqK{u)FLdzT~0@?AhMtcX&` z3A@DI7}|tYUMMpWyIS7tOydA2~ld^N=q;kgD7t?oKr$7MTR6NZM1Sr zm){cbA$3Xe)V}|U$3`F&?u>3GbW6mo8gcv{!-a&DiW&edOnw!j@NSa!Nstu%X!ZSm z?|-96LS9dRJ#OkR;E|=Jk3qs;rO%_F(XFu;^t}u6dBHsQU5*5lZQMM;zdnL;`~3G; z3zEx2$B!r3Vgt(yjCw6|9F#k~fVkAt<6zjD1f0Nr#Krjn>>-d<1LahAAGJWW2V|I% zx?bozGQ>7s&&tZj!^8A{8TDL_l_e#0%_+*N(>j{-8iwm@V47Ej>36}MRrSN~XsXTY zZFxCZjidv*s1m3TwI?TKHq_0`L;-{lz}e<2%q1oA0SJ6?!XYzvyP#Or4k$~*K0#J9 z5qd~{!Nl2C^_h3sqOs^vz8{Y7JV*R$ztVY9jT)=eld_U&`>DNw%!x#)PYf zQ`0$1ktC+7z>e>vwF7}^u^yG|R>I$0wB3pVB{?UJr^WJ)`~f7I2Mn3gbWp+-doy0* zPLG*^*@<&IsaLhNV`;m+`N91NX=$$Y@PyC}w(7dFthu=-Rn<@mVfB%lD&gcbUcql^ zjhQ8JqjHDp3}TMZx=r?%6sRywSzWF8g;!INYpzh8CILFjPmmxT_tN>*p{0+d%!K z+dew0@vGyiNF>I)H%{UvB7xPfB-F4-Ju*a3paxD0hi~I>>0IzNEJVuH*)VCI5awkA zO~}pTCKjY1?2Z*lZ#jW$)l4t_;>xt!i~ z?A?$~>I|&f!cGYsrQ}qi<`AJc*9z4+4f~m}Tl%)Cs5kySiEhYGMpsI7A$sl4Z;ZU! z_Wr{)ySZjyW}*;YCw6#WEL!#qL}easZ0rCG>EMqfpbel82v(SwzGo&TwziTcCV*q* zq=Ac_#Qu*1APe|aGI9(6pxEpJUy%q1eCJJajp7ycMMVXTjS0vQRABP}0FYc<<#ly{ z%lGH%>IO%-7-;aLI_CeAcc`eq`Tjvn%tlo;HHX=)uGUpUjWa)g74V*Uy?H$WPp`#2 zbFoAC_rc@=HVyMXAQQ2-8IU1ra`___nQ?w)M9p+af19$+^!!2Ynj55Hr8_xhB_E%z zV*1v{KMm?Mcqz>W9`=4@ZHEv_jlDUF5rr^e+GMKRgTF{>K_vlMo^yKf>htleIaX;r z-;$lXy|WXx%HozM6jBsznPW8GTlKrd-r*ya3)Ddj8h%0G_SNq$nYzEy zzp;=#)K^RZvZ@Ue*)U#&zGaamR>JDzTGNV+$$uTFQz5z^GgDQ*#Y8Q+yoLKVt6W`= zSSG)YJOD9rYeb0&tA7_eUoLBld(8hK3bh2Otl8CNwbJ)qz{IVVX{Vr9=?@M$L($U| zYh?W7&w8d&KGS9cAJ=|f-r%zPB_N0;zpIE*$?OdkB}HsnQ$>Z+7q2e84PS#=sBDwu zZ&Me&Ns(k=iuHVKM5u(Z*|FId%^r(%O=0+$*m3{ex*7|{Wp66xc9xZzsY+YUq>@S~ zv+pNw_<+dQu0z8-lWv0Np?3v&9~R=bdOJznFqqR%+h&1I76gFH=Oxtpl{ARAJ25@6 zhZToYcn819*s7d~@CEmbE6<7=BiAv(zW+)Xn=WDy2z&ZwrxRwpUcqt-Yu@v-JR3C# zwb}T=oqXhCfmTp!60_{s){vrss$v<07h@ql(#T@vN6hrj0p~p$LNzM`oBseW%#u%N zoEL^d_j^z1LNC8-Ho3WxdF(IOTW%f?r+EM=_$#=qdX0_@)YNDFkr6vP{+E{~Gx4DR z1-W%=+XbZ8Pynx%_1C3pT$8tk08||VopEdu@U)m10)rGyAR(h(H!}luV{J2A`^wEt z)YRJ6*2dmGkpURGr%X?Ub@yxonaV$hO)k^azBDwXmiy&^;TCWa&(N^2#IKOV>oWMu z>DRI=4rN@2O>Ao2 zie|+q&JMk9mV%gOuMEe2FNu>~foPlOJAq61IGXmGtm+5He^=1uE(uX*m(OAPPHVO- zAx(-gDCb7dql+(6HGbcA@v6Pn;pNs;Uv2E(YAD&DK9>_S)*!#(+MKOG(=cgGCP{i8 z2`uK+551n`D|(h3wXcUu3LXCi);K@(n&)cmD>$bK(Zn-{LjO7WVCnrgnB5)^Tg*`?T*Cwd z$)9h=872o}J9#K?&G=EVO_YfqI*BP$mf{P|_Yo@5!>=XTP-O9F#a!O((rK6Pi&AM3 zs9uvtQGF?iNp_T@UWDD?KoV4$R+dnQ1=nmN+b9gnza}lOHgNghWUq&q6fTW5GIRuj z-8f1aM@`2q^HaE}Iq-wQH_qFvxCGXXUoD?U^V4`tFrwh10z!rcGCL6iGB?~lb*q!b zB`5U<-qFfpPsgzOetDf5U#!e17%tqOyc?yx8G5cQ7i${EGo#-7KF056z?CCg#5@)vea$i@J+Ki2r0}`YMVBn08KZoO%XRw)0&J@>o#?f8$D<*MS&%}|5 z^?!MOtqO6&p=|9X{U~GbFJ0pxIyi7O_bHwK&~WS4$SfFuP_~Vf<1J+1_%2e^B5zh9<*b$G z4x#1G7&Cp_h&XKRp~NfIU7>*t+h}^Tg@?8Fr*n%d{*r{;nQvcm(vUfhTgSzIwzo0% z)6V|hL{@!k84)$4yW;uL-=N?m-;}zFS}(+DIaCT_tZrgDgPJ!oY)0$3nAP_Ci~pCS zi-_mLG&naIQkmqAif!#YbQV*GC4bXh9<9u@$xLbUhK%#_6iU(f1-{3 z*zW)1U25ayp(StW>w(#OjH~zfE<;kJ=<4Hae)wy-o;kIzfQs8e7qt@noR)>?&J-V1 zgA6+bQ+7pfFXOmY`v~taMAT}_B=%P-MqK1C(%4QW$PlUJWbml+jfg&C*RW{0hZz9W z@X$m14YIxymyKJ%#5!%~_r^G>E#f@O{VEkj8V{$QG8-MB0(*QH$o8Ki0^BhHbos+G zyy5d^6sgbU5MiBBc1c|(!AIAmBQC503dE#K z%J9qM*@j3!FbW~3+G<0{e9&DHvwC`xgKPL_{j)a#F_enAy5gDHMOYKTP!i$@tj$;c z9#8j=Bg?q3BA0OHE6l+w-bQqJkrTqCeQuF#hCV?ZiBbNqtl?v!(=w;zlu&;R`gkjm zZeoS{I-4%PwwGNq zkCYcl$zn@3Hc&=E&n+eWb-5An0-=(qieeF9*Z-5YA-eUF8m)ZJ|_A-UELqQ(7H*}_8F@q7)~?gPOCCctqP5FitH{3PD{2dJyc zCB~Tk6WMYG-kCtP(e(%mFb5ht1Ce(*H>UX7#jvN79B-(yn(lxfttQZ522JOVq#*h{F zbz1#Rc zDpo4F32A|AW+>DFmjdv~*|Pdu*x!}(RgfWC?&^;1KOXN`9DBnTO#3i(i3^H@K=*0Cg~bhKcJ^j!3P~|7Wa(#@O?TiT zsn6r?$(NUB(wEywc+xM3b-!lAKMP{`BCTJ~=+nUQXAD>Wz`oX4A!ig*rZRxCmJoA4 zUId?`2ifl5=Ro88nAk&(pWxd)|LLbThxfLbka_>l2dux{7pO1tRfWa~FNehI|Q2l7xC6d~Y?RzzX*+BPJrSB(Of7$u~8L--{~LdIa9Esh4ON3~?2FW@nU zpUwC`QwU0!o8MnQ`Md1vS~R`!~!DG3YqPgBpEwTw*l*iwWLIE5Uzqm>>hMqk=w<;srrB9hBTxv!r=-3IzkutD$QCdO@bvhT z@_ycUFq#c;tM);W(DyR1=FzFCTxR#UaHUguwRT)M?_7SVCVHYz=kG6zjhN-%mMwh} zSYniS!;ImHoGpeWO7r1BkdiDf8ur!bjlv?|vZ9PL*2dEd$u?h0O%2D%Xo5%5D#ZiU z5!y75OW!e$$+rW|Yu%eb^} z=oS5;f4nhNDd*=&ItY*$o!t2J)%!#dIes9HMkZB8c9&Dx$#hV)Se^>Qr7Ud@i=iW_ zyk`~F{4Yv66)cV>F>2(A#xIoy(fr{nZQWmc8@PxLn0vll!LP)sj_0<8Y4$H4v@X*~ zV(0R(t!MB>hs&w{GI!NWx(`{w%vQiWNq#L~Q{s$62x*tC!)QNBg))EB5Fgl*hi2GStdj4!J3NnsQi6q{RqPzk zKbGoy{&dqO8Ii>9vUfx!m!nw`p<8FYABF>D&~*35Bh$F;?skUc3KaCYEyq(N!nv!G z<=KFumJFm+b3jt!4=7B2WeCy$Eq|}u)4M>}BPurcP$4RKUoh-*-{?#h^iue};=#!L zSIhP{rgVqLMSCRCjqPH!`@nZGc(gP#xM}n2V269cg5KU*mwjf^_X)M81Ii2Azp}9b zwtcDAY9ys@tyrh-3yzqZQbPm(@Fdjk@0IWOuto31D0)8(!h&yw48B!W9CHJ3*ok)t z^f4&;wfrhbbXO$)SW^X7%n4sb!+e@QscWW@nk#87*G2t^vsCxy$@5C6NOApPx>Y0q zmn5{kCRC<@_;UO`schQ=^8tlYPq11fp--ASm#y?`k8}MGoAL8vjhO3Gay>TP3vcttdU9v@g>zKdxwn`0UzP$<`y< zBJ+KH$MDfaX_`N}>6e#Yoej~fIFiYM>^Q1beTs*os}8nXVsB~Vp>A{OeAZmd@abujvGMl9$ zcDhaMo`04aQHic4q)69l_RYK>!F2TCl4_4RUD$Jm&jxYs?}8I2Kfm$Abs(78+bz`! z(_8N^)v;wo61n&F5=TS{dpzDsTrZuBSWXmV2qZh-o(84AkE+yfu{oMCSNf`u-tMqe zYhJ#Dn(SL#ihRVa318Ua`bk0RNoOWKzl$x0g*GkuxMq6q4?_g~u)8xqVX(T;n`_`a zKMCI6u-t5^mP`oxNO6~V;b^NID&95GpSlH`q+{9%v$KKf6-_~l1L?y#f5KKVn#r$g z9*M_2?nEkFwwKCleS3%AupYzkT}gG6)XaX?v?pIIzQf(b7uCUbo&EV(mC?*ZuTg^R zdnY=_?AjbTY_t_Ax=OG}EDLotX)~iGjfkMwiQS{|^X?(b^D5gX)&F*dyx^wMdfuvA zy|iATb$Y05W!%F*Kvb0wY?WKQk5x< zuAcK1*lcW44R&%1)w`z!@@GWczat|eblW^1O4OIW z9v*fhTf;~p>bWAgmor}TVHy4FZIcyKXH~bpmdx}is*OE_3#mX?ZdH=LGOB%5qeq_) zqmx;&<_k7?hq+oxHujc|iIJ#3-Kyw6&Z_gktG>NOE_MCWtuM@04!HvBA~BL?-0FK& zBE%u8DV?b%IkNL3;+}-d0oj-w6W|4w4-@UgUK?lTbi`4n%hQjMPBcpJ?krCgml&qzWpFc}>0c;+|y=X_Df93HZOMbP9mk`Pc; zCH3jAK)u7dOnjH?-$6|NP&%RK(F*;6+}tSJMdy#(lK0ClKZlm;-p`kX>eo|P@CKh(wfOtPP0am%)nm5Mz%W2+%H&2s$j#qbz12N#KV8D@ez!n(wtF!#LK0z#i^NqLgu zWFY5#TI8P9e@7LtTpBlOs?!J|Du^-ZktTF*0@TPA<8d>Z&|(Ab=0mb`Hotb+frzoVa|D#<#7RXK{I zo#?fr+GH*k`wbJ%VAx_SE&uXW7TPZ!JWTaUgIIK-kF#4npH8^!v_{Pd>+hruL)c@-D4%44$|2>&<$8U5v>*-4xEesRhE3SGRGFFq>? zOb^L&ZtTo|cYx)rX4wLuu2(-lN%@}j-HfI9=#|d3#B0IN#*_Z{EdKjKe<^2!MTk|I zZjt4TKA;77aC;O&qnwvo>qqF+6?gRtP)Dlw_D0gkouST7Q9;ph$EJ<-GLik|+?5d! z8*F0sNpAT32KlIunpxRBR2WsW$r+}uGA}ZI0v45QSpp$Jt*Db`(s!3RWvP1PZxxh6ZBcoxS%q*-FH|&Oq zG0x5ipjRsL$N8^F`1`$GS-MF_q-%EgR{avaNUPp99tp=d0puKcDnhBP_shxJ=On2qJKxex>)j>g0~>$6fZ#u zPt4;@xPqYxbFepBsYQ7~*Z`$1wS;#la#t|_Ar>u3tfNfQsNw8RP-9(hW>3tV%GFV1 ziMm5CNq5d)tW-LRLdG*=3e@n4*YDLE!at^*hjpnHJHuGBs{7y#S3;P*qFyU~=6R>> zWW`ZT=|hE4m+*U)N;g1eRGGal;lqj!d!t-yyp@YsRny`?ikOyM8B^exW=4vvTjO@W z7u_(=R(*_PqL%w*@o?}A3R=ZqQl%!>yl-R z(#0ww>LoM40%QaXq@JH{?yrt-?3NQS-ujp=*NZLJ_c<(oa(uWJ13BRRC*tL*?c2Sv zGr2_jR8BgRUs+BakL;(Z4quL^cK01R{!Hc$oC-fDv3Xq|<;yjIcd}AzbpmcUz&ABo zP5W`#d;)(%aFyx)%`wmYab=6oqtgT231_KJxxpP$3VZJV75|GEMkpvuGJCVedxaEuB+!b zjkzN53j(rBS$C4-1%8#SR#^hIZuCmaK?IOXIzFoxwt!+fo}>@eTlA?zbmf(>F>_dn zYx=JUP^sw-sSaUF^Xgz@{$k-dm@g8~7EKS7MlriOd>>vdI&4g!Re6Ca@T`sUIuAzz4s8qm3j&2bxXb47FS@ zH~%c7JG&za4fImlE`RzwmXZ#9QwiD4RuT=LmvZ_Fua0=i^&eK<*PpWf1)ooUge^u# z`CR78v@qMgJPBRP7PO5dN&yci=bb@1;is7n{g&}aqJ|>n8j;=MUtBh4sx<$<3k)sB zQE+scxJdiX!uTfvB)*ihNJV1QXluf_Sb&;EJ-5KXIX-zzoa!m0HN>*d;2qO%l=~^F zYpt|%;)J9|%EFGFdF>ckruYVo(~#rjpWzCsg1H8j6vH$>myrdFL=9d#cZAVH7;f& zqm^{_+2wb;Caz!-lYDu`k&8n?qSI_MANdXg7m>{4uzG*>kdqtL-%nzSKJeCLPUY1{ za66Tiz3>P+xDf^Uw#$6c}%we&uQr+9yd zyuNnTqIwQ?urgM2fHiUG#^sJxo}#b*QCtUG?VPA~k<^;FG+v}m^r=vQKr>P(l4_s1 zcz=WzPSseOsM@Wug|byR-|ZHCNCZUIRVsw`Nqv+0AE9-<`RqxJfn&W?g4A>gHEUKe zF`r`Mmei*9Ms)+Z;g8X6x3F9i7j$P2dqKyBLjX{`|8-rI4<^d0;L5RonbD^=Y`-5M zbw8Ltxmm2ytTcGr;AHv;Vr9L3{!+Uj8TFnPWOQ;Y6`k3T#O=T#~! z+?0o+10FK121vZKHZw1cyM^n%>l}=>-5`ciL$r8p7kWv3f3G&X4ZxwMfM7i>oF#fV zMfP(b%jk$1lxhE`VxVudb;D0#QxE8HhQszk8{rEGo%mrqVSHxTyFW2Jxd`zVjV(`c zlzAAEq)!bk`IKG|R)m^;l`4QcAySo4lndSGA{3eLc^Kn4j6!MwpBU7^skd~1Q)_mN za1yoTJ8G<~^R0uV6uLIt+w+HnA}p%)SlU~w*EU9gO5HE?4GHQ+{cM zFxm52&En@J$$#1ce=hy+>M)WC6%KjPB|E|(UyI%#^aarKu2L1&sS%NP`s8mI@lWmj z*d#ko{(O9Dp;GtV5h}Z=O#g_kasVrencgY4&f@v|WpJK{8CCL>3iB#eQy4P0@m$Q3 zeh%Ni0~AfpUjA^`RM(r5#motP6W>^z$G+rE`0`$FMFZcL~6s|%8tEH$J~t|WSz8L ze90fK3m6flzP#K&F>C^7XVM2VeEV9nLhoYLJ=N#&tdR7}0dlR zr{PQBzdqrAFJOG{vh!ldkQJ++ku*At7yjmj6}#S}Us>3e6}u&8AL?xOWf-wjh|I{|2VvCW`VB)A6tjrjyTJ86HAl5Vh&eT z()oWPN4g?@xH|Gnk?F#s#j!zKYFDzDgS~W<`_+JrSPf=cy>Fc3| z=H1fv6h*hLq z(=y}E?3!1Ek&V_fwxju8%B}{z1>%!s)-!?noqw|gK#Fup+)2&5gVFY$LwT8`zi&~; zz(x3B&3OcKNQ7|Bj6!{nLcxiYsV&)qR<%26ikTa)PK`~lkG(4+DIMo6r*Q}UVo*@Pz+gLID7&vfm#7% zv9l9tcUZkxFW+J;_wc9% zep6seO6yoEh+Tpj7UojU)2+Z{fNHq=e|UTAsHnO&Y}`N)DMdmg6$WG|=@Jl;8hR*c zB&EAcq#K5Ai2;U`ZV<^qLOP_o1nGv~8GPRN@%OH8eSdyyxsC&znX~tGU-xz0d(YYX z__lOT39P-;8^!nfj{i1|*A*}^(cfy*BT>SHH8&$smz)=$z>BJYAnmmWV>D|q&h=Jb z%(xdwm+E-}i4;Tsc#Bg08BY!L^ub7ha)C~+>>}U`!$la=xZ#ebOzV5Mg;n4XU^tYo zxr=WY5`W(x5B^cuF$Oj}q;KYg=p_;R3Bz$r=hD>*qe<1FKRxG^JT^o`y4{L|%Vz=H6zjJ#p32i|*%-?72>DzhxrZ{*0C8n&+d!n{h5M&5^ZP3V8uz`H@vkc1kO-Z~mxfXjCPNf?sUEgbj%6-H^p#@D zaGqGLmgiQVY$k5xYojr1_h&!$QhuE5zv%v6al{n5&@OH6AASr={Bp6rtvhy5GB z3;UxCrmz1EDnT~{Bo$k(B7uY*&_~1IAJ`;%QfVKCo?c$mF3Euv%7cDNx3r9`nbS6Q zzV%uOy&O#Zl9;VT@i>0cWbi}=I_O-cZV5_+X#iXrx+CF)&^%QK>QX6kvb_7{{n(@U zMNLlv3aF6_SvqxA0+*p z^&DZz_h9)5<6;>%a1<>=D81ikBEd}>s-7GqWz1#5NXNX*w@&nlfV*4v6=?Qeb=+&P zQ&3;d{5HOw00;WxWQa&XVbY8!h#K?LJ4AC5q&gNBai39wbuXcQNmUJ#F`bHb8a@da zzlwZi{z!J`Qbi*6f|&)r^$xvP+c7W|`RFJ(mTK;y#d{_j;s`;9uDXW>4)?Cgj7TVB zSlUtZQS=x>fALD5BJh%?Iw+x+CWTh;0h4kPLdoUm7P%KpYLm5;n0=Sq%g#sARhmdP zddM7_LR;y(X+T}{%#c1VVJC1Wi3q*BV|GJkwm#c7K89{FuTrVQgbDN92g4f!jiz-C zYWp~{r_F?^wAt{2D*qvLn|a_Ep=gF+blU-D3pj@2YZ6ei7(Pqt1!!DQtIfoO%D1h> z@1(X8dG42;C=0GnDd=E6zm@SqVbDi6b5CJP1v&-d*;DwdfY?(3On9>`4kMmXjR-AW zDh_*IPiW{Z&ZDak=?kr4Kp8%LTGPn$tADHWRtrn1$=#y(0pZ4)QmLL+F(kH7sb4#m zp`IZwZAGp0`4;h!Y>2_G-VRuv{K(bh>>XJNt%Fxhxk^4+$j8V~$sl~qK4-90ipjgF z6aNFuw-1+)X!W1nyao1jyH#D;%@F7jG%+NQKC|1L`B1oEo~bed9UU>2MEfPmQUebS zp08Oeh(@MPq@LJG@|Rjvsk(F}gVK+{`d2;7rBM}$jLNlT^{@$* z!uxIkY(G8;V`k*>Inh&nW@Ast8*2r=fy-f)3Un2bcd`tW*zD~go0qDod>bvv9lT0i z1d>l=dnj_o;OV(ly`dDfB=khQwaW*6IiOAnaVKB3GGCo9Uc5R^D!v0!SM=&(%!9T} znXp$c*zwq^wzQs%toPn)gbW=PGL5xC zd)_y4br==#m)|Nifo~m*YW!AZoL__jMSnue+tukj$|wenI6)^Od?;3%0Id-_(s;Qc z&bdk!V!<^5*oz0Vk>~EZ)QFFd3^wh``ER<#?8+sMgcwYS`%zeJO(mLM@{eYBJHs>l z>bMhq8@&bCs8;3?84G*K9rlESv~aq?SL|IhDoB#|Q>_bvQ3!)OBeY6if~)U)QOO<- zXE-lKVnoX=>b=KgR$)vyelbxm-*hunN5kO#JJ0Xsa?c)Mv|y}o4A_{I>e=HzW|ygv zdUN^6a0ADq)yev$$C~tRLY@+ZA_a&MOm9M38`I+(>r31-tY!OD`T`_u>VtTTY4Q?t zU&0t{gG5MQ%=0v19_PLeu0*S!8-=+p;HO@Lra>=?7@J#WcO{;(2TB$$OtA6lFQ?UH zy%pN8nv=l>sTP9H>Ok%4`CspJQH#+r2#f2b249JuYTu0Ngu4DtZSc#}#$9y6bLmca zkM0A#QOwi`#X1g72tkPNm9M7!!%A}i|8^W&qxhwGU)6Sy1$DgMx(ROKs|IINC&?xc zM43BJxLn;lFf)+Pko`8u)^;04nn{1^Z)z5Vf#cY+*g?yT)R|qBBObKf(<{d~%L}5W z+})Qq#R>{xUUgwZK5U^djFf46{36KDnc+8WToXPXZHR#=(QAsjGC*}2FRQ-9&A5it z#ZC74??u%V`EKL?g}6ISyH+OpkcQRi<`kT5YnOR>g86-vgi}y5?i2%32vb}Hvg$N0 zVVOxFy$U?q#NE!gZJpI7S<;1`^AM7trAMFkksmUpGv6Zj;3+;1Bibt5Ki5!Qbj)dOimZ7*_z zb!OI~x0icVmM?=$c!E1)nr$aZpy0(;KP3%JGhmO8ik>N=_YqwPpb(DHbUFG=c0Ix# z)@cB?1hXpWNS4O6vD>`TtP7NMl2j2FP>`@mdIcML0|h8*uFM)WPq<)VJm7BsIPv4X zNXRn7z?!g3M)xbKEiEaazQxoDav`e_CpjbSCcd%K@kil*Cz$YhOr>?X59h%c%11D^@^{N(8R(2&+R#U2uft{aB~~gT2?mz)4>ovy$Xf>lN^(&$B@{VN z`LbegLN%lwK3}$Z(^%P7r$};Auxa&THV`$}s`o#slnuQ;Pd~w?9^{_+Jya(J+?VAb zSEH55Lp#Wkf#**6(H=cOaf)eAf=xqPx3a!X3*RyZn2>W}UME{812G>JSZi|NNXY@r zW#UccA!0CP0&X(s*IHoF8Vux>St*l6I7Zd9ffL^m-UgfC@AACJ_zW!?l%Sn+h zx+kgK zLtmHb2?O-qug0=*3Oc2=g@n)OcdM=o;L@)@b^$wF6^^?)1@Y5P>3-@4XA-VnMG4#1 zN??<+=>%y=J#`8amn0HP64ZS5EN&2vX7ZRPjHXsTC#L>x=^AEwpyb=vkjifQ+Bl5h zP9XR}PU#HI55;FON=_iM5h6cl<(nR(Ob+dTn)n&NqyhTLW3E+U<6hnr{58-}CS^#2 zYY@qcVXybwn1@dZ^q+gdk9o2gVpw4OdgzHSWooQ4*%I1*SU%o#~#ZAGBn7i8se5MdoX z^nkkZ40k3=227{*<@k`l+Za0w?hyL=;$TFzwuNSexm1-!!3pt>HMhQvGF(wc=Z-Qo zr*Wy|Ao@f0P)sv)N2zZ};esHyV8Q=`-gf)P;fZ{w++^uC&?Qwnp)J!iEtJiBDhvO$ z2e~q#J*3zWvt|t#key^%C*{spD8^mPI^TV(Q;Mb)D5H`&ioU77apk{PiSkQ$yf)bX z?|i=6SdKr{F}?j_8$Y$aO}laFcqRFp7FfH1(d`ib(&7rb<#a|E5i}`1r5)7)5k#8U zY#U68Qf%zb@Y$YfLc9=7jhz^iutC_%@A3(bV>w0#HLUJ|Bfjm@OC8KZcy%z(2zRu~nls#4P^bmnw}ehF)n%;ljo=h( z#ui6yYN{%CKdaT802@Y`X{su9XV*RGmZwhzW5nbP8p3*G^Vjf|iBy9#4Q%}wkcgfT zO>Xwber(Kn_Gw^pd)WcC42~ZpqPYlWA6bO`o)LHqGd6s-q0K=Wo~1?*9!!?RvXAREu zleJPcAswOrp(3i0C;N%3pdx4k?L!uXG%wt)NR>d;A=E52QzAc0(~PhvgYR3V`T#f| ztvEJVMV@>`z4!yk$g^U2aNHD6SLHb3$NlQdvDIKRXB7$P<}|utW|Oo zwj=1xnQY~h8j#MP)?_D;$(?NHlo?RUPt{Z;kjV*NOv)Z?+vKmI&ErS|6LHAGOULwU zuAuZO;HL+<5dFL=yD*glj1jKlS;DZ)$!<>Q!2NtC%~nF3tjQiuC50y=w8hVY(G@M0 zo@RfTJH*kvo1#9eqaY;eQ6i4HEOz)veN01vB|}19PE;Uo^iv(fS(qt~Dz|7ev}ol_ zR6#XgSyPDNEXWiz77^_ES-s`35i+~Xyn3brx3Z15@hBQpt(B^6QbI^=;}`b@H+5+- z9@b{|+fvu?(QhQwORP<1;=aKWQ8tyUymf%cbX4;45mZ1hjkwE`#;WTicaA{dabqjC z9)*MAV=LAkc~GqUiO(xwkJp@72ThRPJR@69;`FV4JmvyXy(%Hri6z(e)8`Djq z{N{s!jcB{CSVPgEhEl7r71-6s^vT#}uy)qd?n5Eb5~!a|W9FJ+{xeM$`ZJah-C||w>I%B+`5YI z=%ofY(o7&;7nUm^w=wj{VUDsB3c`h(dzM>UBZcYvxI0_dTY+-{{`(T|J8^{qQ#uLw zyHDmD_e1JQ zY(r+L8JXr*-cn_R7S4Q`1iVS!Q=H|q(uNecei(`jVF}cVK`a?QZDXD~c+SkSM&%fk z8}H__j!~*{2ka94)sobAsnOTgXTD*1jcG?@DK%)r@m`yyF4xDUFr4~+Z&C`d%XO<| zz*PmZ~SZ$UaYhj5El6Omga=BvBS*^a_LXVi)LXrwStS4Nm z_Uo&E>p0tGMU{FSZSBSzH)9G#^2<50bX!4EkMkZQ=8j?^GreW!8lEMa4TtwiHS~Z6 zVe3`*9uTR|$oh=qa5YZEanDdWipYYj1}0?cBO9VsEq7A6FTB1hd?X{zL&u`xa_6}Z zYkk1g{c#>WmdxWz%YE{MF5w4(INEtj*63~iyh^rnclAV`@iD?gXdaUCDIi-20&z;# zNqXJ|(T(olmyo==671vi*UfM=cEI-Kuk3nDx56Dz2NbIh8ani z+tvH2VH2lw)4XxUS#D2O6p;!-`?s0xB)vZJqEGQz^I--z&T)|SVG@6W?1Bi&-%JuK z$!dtO@|C?Fywo+i&ncEI`R!FS8S^^)rIX)JMp!ON*gn=|&FQsgpB7I)vV*bZTl zZZ?dYiN6gL&)N0BZ4f44wNnTqabro_QSNc_x3+0^SQQrke0jlOn*mW`F{ldFxd$)FZqkP6NO~A@9>$IrjkfU7TaqReX`HZUG@r0{R&~%i>8D!628#rZoboK+-d+CcX zF3oTj)?*#XBosbj+6PVeq^oNlAg{TM`6aM^Yc`9r`B*P{MW;sO}BF$UJ>e$N?A1F3G021|a%HkdO#*Y=U)X$eUxC z7<`z*c@IW-fZB;zk7I|6g9nuEkjQ`{fqtij1BdMh;2`+OE_}-!PF&u+mIf{XTnWIGf*!yM zssF%0ADjpv^NtG$y~{12r?EoAB$& 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 F`VWz=8@~Vm literal 0 HcmV?d00001 diff --git a/plgxcreate.cmd b/plgxcreate.cmd new file mode 100644 index 0000000..01a045b --- /dev/null +++ b/plgxcreate.cmd @@ -0,0 +1,40 @@ +@echo off +set plgxnet=%1 +set plgxkp=%2 +set plgxos=%3 + +cls +cd %~dp0 + +for %%* in (.) do set CurrDirName=%%~nx* +echo Processing %CurrDirName% + +echo Deleting existing PlgX folder +rmdir /s /q plgx + +echo Creating PlgX folder +mkdir plgx + +echo Copying files +xcopy src plgx /s /e /exclude:plgxexclude.txt > nul + +echo Compiling PlgX +cd.. +cd _KeePass_Release +KeePass.exe --plgx-create "%~dp0plgx" -plgx-prereq-net:%plgxnet% -plgx-prereq-kp:%plgxkp% -plgx-prereq-os:%plgxos% +cd .. +cd %CurrDirName% + +echo Copying PlgX to KeePass plugin folder +copy plgx.plgx "..\_KeePass_Release\Plugins\%CurrDirName%.plgx" + +echo Releasing PlgX +move /y plgx.plgx "..\_Releases\%CurrDirName%.plgx" + +echo Cleaning up +rmdir /s /q plgx + +echo Compiled with following minimum requirements: +echo .NET = %plgxnet% +echo KeePass = %plgxkp% +echo OS = %plgxos% \ No newline at end of file diff --git a/plgxexclude.txt b/plgxexclude.txt new file mode 100644 index 0000000..94c84d5 --- /dev/null +++ b/plgxexclude.txt @@ -0,0 +1,8 @@ +bin\ +obj\ +.vs +.git +.user +.sln +.suo +.pdb \ No newline at end of file diff --git a/src/LockAssist.cs b/src/LockAssist.cs new file mode 100644 index 0000000..52c2b87 --- /dev/null +++ b/src/LockAssist.cs @@ -0,0 +1,101 @@ +using KeePass.Plugins; +using KeePassLib; +using System.Collections.Generic; +using System.Windows.Forms; + +using PluginTranslation; +using PluginTools; +using System.Drawing; +using System; + +namespace LockAssist +{ + public partial class LockAssistExt : Plugin + { + private static IPluginHost m_host = null; + private ToolStripMenuItem m_menu = null; + private static bool Terminated { get { return m_host == null; } } + + private QuickUnlock _qu = null; + + //private static LockAssistOptions m_options; + + public override bool Initialize(IPluginHost host) + { + if (m_host != null) Terminate(); + m_host = host; + + PluginTranslate.Init(this, KeePass.Program.Translation.Properties.Iso6391Code); + Tools.DefaultCaption = PluginTranslate.PluginName; + + m_menu = new ToolStripMenuItem(); + m_menu.Text = PluginTranslate.PluginName + "..."; + m_menu.Click += (o, e) => Tools.ShowOptions(); + m_menu.Image = m_host.MainWindow.ClientIcons.Images[(int)PwIcon.LockOpen]; + m_host.MainWindow.ToolsMenu.DropDownItems.Add(m_menu); + + Tools.OptionsFormShown += OptionsFormShown; + Tools.OptionsFormClosed += OptionsFormClosed; + + _qu = new QuickUnlock(); + + return true; + } + + public override void Terminate() + { + Tools.OptionsFormShown -= OptionsFormShown; + Tools.OptionsFormClosed -= OptionsFormClosed; + + _qu.Clear(); + _qu = null; + + m_host.MainWindow.ToolsMenu.DropDownItems.Remove(m_menu); + PluginDebug.SaveOrShow(); + m_host = null; + } + + #region Options + private void OptionsFormShown(object sender, Tools.OptionsFormsEventArgs e) + { + PluginDebug.AddInfo("Show options", 0); + OptionsForm options = new OptionsForm(); + options.InitEx(LockAssistConfig.GetOptions(m_host.Database)); + Tools.AddPluginToOptionsForm(this, options); + } + + private void OptionsFormClosed(object sender, Tools.OptionsFormsEventArgs e) + { + if (e.form.DialogResult != DialogResult.OK) return; + bool shown = false; + OptionsForm options = (OptionsForm)Tools.GetPluginFromOptions(this, out shown); + if (!shown) return; + var MyOptions = LockAssistConfig.GetOptions(m_host.Database); + LockAssistConfig NewOptions = options.GetOptions(); + bool changedConfig = MyOptions.ConfigChanged(NewOptions, false); + bool changedConfigTotal = MyOptions.ConfigChanged(NewOptions, true); + PluginDebug.AddInfo("Options form closed", 0, "Config changed: " + changedConfig.ToString(), "Config total changed:" + changedConfigTotal.ToString()); + + bool SwitchToNoDBSpecific = MyOptions.CopyFrom(NewOptions); + + if (SwitchToNoDBSpecific) + { + string sQuestion = string.Format(PluginTranslate.OptionsSwitchDBToGeneral, DialogResult.Yes.ToString(), DialogResult.No.ToString()); + if (Tools.AskYesNo(sQuestion) == DialogResult.No) + //Make current configuration the new global configuration + MyOptions.WriteConfig(null); + //Remove DB specific configuration + MyOptions.DeleteDBConfig(m_host.Database); + } + else MyOptions.WriteConfig(m_host.Database); + } +#endregion + + public override string UpdateUrl + { + get { return "https://raw.githubusercontent.com/rookiestyle/lockassist/master/version.info"; } + } + + public override Image SmallIcon { get { return m_menu.Image; } } + } +} \ No newline at end of file diff --git a/src/LockAssist.csproj b/src/LockAssist.csproj new file mode 100644 index 0000000..ff04158 --- /dev/null +++ b/src/LockAssist.csproj @@ -0,0 +1,93 @@ + + + + + 5 + + + 2.41 + + + + + Release + AnyCPU + {4712D887-6685-4CB1-B67B-E981119FE3D2} + Library + Properties + LockAssist + LockAssist + v3.5 + 512 + true + + + + true + full + false + ..\..\_KeePass_Debug\Plugins\ + TRACE;DEBUG + prompt + 4 + true + false + 5 + + + none + true + ..\..\_KeePass_Release\Plugins\ + + + prompt + 4 + false + 5 + + + + + + + + + + + + + Form + + + UnlockForm.cs + + + + + + UserControl + + + OptionsForm.cs + + + + + + + + + + {10938016-dee2-4a25-9a5a-8fd3444379ca} + KeePass + + + + + + + + + + + \ No newline at end of file diff --git a/src/LockAssistConfig.cs b/src/LockAssistConfig.cs new file mode 100644 index 0000000..c5967d0 --- /dev/null +++ b/src/LockAssistConfig.cs @@ -0,0 +1,128 @@ +using KeePassLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace LockAssist +{ + internal class LockAssistConfig + { + public static KeePass.App.Configuration.AceCustomConfig _config = KeePass.Program.Config.CustomConfig; + public static LockAssistConfig GetOptions(PwDatabase db) + { + LockAssistConfig conf = new LockAssistConfig(); + conf.GetOptionsInternal(db); + return conf; + } + + private void GetOptionsInternal(PwDatabase db) + { + QU_DBSpecific = (db != null) && (db.IsOpen) && db.CustomData.Exists(LockAssistQuickUnlockDBSpecific); + if (QU_DBSpecific) + { + QU_Active = db.CustomData.Get(LockAssistActive) == "true"; + QU_UsePassword = db.CustomData.Get(LockAssistUsePassword) == "true"; + if (!int.TryParse(db.CustomData.Get(LockAssistKeyLength), out QU_PINLength)) QU_PINLength = 4; + QU_UsePasswordFromEnd = db.CustomData.Get(LockAssistKeyFromEnd) == "false"; + } + else + { + QU_Active = _config.GetBool(LockAssistActive, false); + QU_UsePassword = _config.GetBool(LockAssistUsePassword, true); + QU_PINLength = (int)_config.GetLong(LockAssistKeyLength, 4); + QU_UsePasswordFromEnd = _config.GetBool(LockAssistKeyFromEnd, true); + } + } + + public static bool FirstTime + { + get { return _config.GetBool(LockAssistFirstTime, true); } + set { _config.SetBool(LockAssistFirstTime, value); } + } + + public bool QU_Active = false; + public bool QU_DBSpecific = false; + public bool QU_UsePassword = true; + public bool QU_UsePasswordFromEnd = true; + public int QU_PINLength = 4; + + public bool ConfigChanged(LockAssistConfig comp, bool CheckDBSpecific) + { + if (QU_Active != comp.QU_Active) return true; + if (CheckDBSpecific && (QU_DBSpecific != comp.QU_DBSpecific)) return true; + if (QU_UsePassword != comp.QU_UsePassword) return true; + if (QU_PINLength != comp.QU_PINLength) return true; + if (QU_UsePasswordFromEnd != comp.QU_UsePasswordFromEnd) return true; + return false; + } + + public bool CopyFrom(LockAssistConfig NewOptions) + { + bool SwitchToNoDBSpecific = QU_DBSpecific && !NewOptions.QU_DBSpecific; + QU_DBSpecific = NewOptions.QU_DBSpecific; + QU_Active = NewOptions.QU_Active; + QU_UsePassword = NewOptions.QU_UsePassword; + QU_PINLength = NewOptions.QU_PINLength; + QU_UsePasswordFromEnd = NewOptions.QU_UsePasswordFromEnd; + return SwitchToNoDBSpecific; + } + + public void WriteConfig() + { + QU_DBSpecific = false; + WriteConfig(null); + } + + public void WriteConfig(PwDatabase db) + { + if (QU_DBSpecific) + { + if (db == null || !db.IsOpen) return; + db.CustomData.Set(LockAssistActive, QU_Active ? "true" : "false"); + db.CustomData.Set(LockAssistUsePassword, QU_UsePassword ? "true" : "false"); + db.CustomData.Set(LockAssistKeyLength, QU_PINLength.ToString()); + db.CustomData.Set(LockAssistKeyFromEnd, QU_UsePasswordFromEnd ? "true" : "false"); + db.CustomData.Set(LockAssistQuickUnlockDBSpecific, "true"); + FlagDBChanged(db); + } + else + { + _config.SetBool(LockAssistActive, QU_Active); + _config.SetBool(LockAssistUsePassword, QU_UsePassword); + _config.SetLong(LockAssistKeyLength, QU_PINLength); + _config.SetBool(LockAssistKeyFromEnd, QU_UsePasswordFromEnd); + DeleteDBConfig(db); + } + } + + private void FlagDBChanged(PwDatabase db) + { + db.Modified = true; + db.SettingsChanged = DateTime.UtcNow; + KeePass.Program.MainForm.UpdateUI(false, KeePass.Program.MainForm.DocumentManager.FindDocument(db), false, null, false, null, true); + } + + public void DeleteDBConfig(PwDatabase db) + { + if (db == null || !db.IsOpen) return; + bool deleted = db.CustomData.Remove(LockAssistActive); + deleted |= deleted = db.CustomData.Remove(LockAssistUsePassword); + deleted |= db.CustomData.Remove(LockAssistKeyLength); + deleted |= db.CustomData.Remove(LockAssistKeyFromEnd); + deleted |= db.CustomData.Remove(LockAssistQuickUnlockDBSpecific); + if (deleted) + { + FlagDBChanged(db); + GetOptionsInternal(db); + } + } + + private const string LockAssistActive = "LockAssist.Active"; + private const string LockAssistUsePassword = "LockAssist.UsePassword"; + private const string LockAssistKeyLength = "LockAssist.KeyLength"; + private const string LockAssistKeyFromEnd = "LockAssist.KeyFromEnd"; + private const string LockAssistFirstTime = "LockAssist.FirstTime"; + private const string LockAssistQuickUnlockDBSpecific = "LockAssist.QuickUnlockDBSpecific"; + } +} diff --git a/src/OptionsForm.Designer.cs b/src/OptionsForm.Designer.cs new file mode 100644 index 0000000..b0ed922 --- /dev/null +++ b/src/OptionsForm.Designer.cs @@ -0,0 +1,239 @@ +namespace LockAssist +{ + partial class OptionsForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.tcLockAssistOptions = new System.Windows.Forms.TabControl(); + this.tabQuickUnlock = new System.Windows.Forms.TabPage(); + this.cbPINDBSpecific = new System.Windows.Forms.CheckBox(); + this.tbModeExplain = new System.Windows.Forms.TextBox(); + this.panel2 = new System.Windows.Forms.Panel(); + this.cbActive = new System.Windows.Forms.CheckBox(); + this.lQUPINLength = new System.Windows.Forms.Label(); + this.lQUMode = new System.Windows.Forms.Label(); + this.rbPINEnd = new System.Windows.Forms.RadioButton(); + this.rbPINFront = new System.Windows.Forms.RadioButton(); + this.tbPINLength = new System.Windows.Forms.TextBox(); + this.cbPINMode = new System.Windows.Forms.ComboBox(); + this.tcLockAssistOptions.SuspendLayout(); + this.tabQuickUnlock.SuspendLayout(); + this.panel2.SuspendLayout(); + this.SuspendLayout(); + // + // tcLockAssistOptions + // + this.tcLockAssistOptions.Controls.Add(this.tabQuickUnlock); + this.tcLockAssistOptions.Dock = System.Windows.Forms.DockStyle.Fill; + this.tcLockAssistOptions.Location = new System.Drawing.Point(0, 0); + this.tcLockAssistOptions.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); + this.tcLockAssistOptions.Name = "tcLockAssistOptions"; + this.tcLockAssistOptions.SelectedIndex = 0; + this.tcLockAssistOptions.Size = new System.Drawing.Size(853, 697); + this.tcLockAssistOptions.TabIndex = 6; + // + // tabQuickUnlock + // + this.tabQuickUnlock.BackColor = System.Drawing.Color.Transparent; + this.tabQuickUnlock.Controls.Add(this.cbPINDBSpecific); + this.tabQuickUnlock.Controls.Add(this.tbModeExplain); + this.tabQuickUnlock.Controls.Add(this.panel2); + this.tabQuickUnlock.Location = new System.Drawing.Point(10, 48); + this.tabQuickUnlock.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); + this.tabQuickUnlock.Name = "tabQuickUnlock"; + this.tabQuickUnlock.Padding = new System.Windows.Forms.Padding(5, 5, 5, 5); + this.tabQuickUnlock.Size = new System.Drawing.Size(833, 639); + this.tabQuickUnlock.TabIndex = 0; + this.tabQuickUnlock.Text = "Quick Unlock settings"; + this.tabQuickUnlock.UseVisualStyleBackColor = true; + // + // cbPINDBSpecific + // + this.cbPINDBSpecific.AutoSize = true; + this.cbPINDBSpecific.Location = new System.Drawing.Point(20, 490); + this.cbPINDBSpecific.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); + this.cbPINDBSpecific.Name = "cbPINDBSpecific"; + this.cbPINDBSpecific.Size = new System.Drawing.Size(354, 36); + this.cbPINDBSpecific.TabIndex = 5; + this.cbPINDBSpecific.Text = "Settings are DB specific"; + this.cbPINDBSpecific.TextAlign = System.Drawing.ContentAlignment.BottomCenter; + this.cbPINDBSpecific.UseVisualStyleBackColor = true; + // + // tbModeExplain + // + this.tbModeExplain.Dock = System.Windows.Forms.DockStyle.Top; + this.tbModeExplain.ForeColor = System.Drawing.SystemColors.WindowText; + this.tbModeExplain.Location = new System.Drawing.Point(5, 284); + this.tbModeExplain.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); + this.tbModeExplain.Multiline = true; + this.tbModeExplain.Name = "tbModeExplain"; + this.tbModeExplain.ReadOnly = true; + this.tbModeExplain.Size = new System.Drawing.Size(823, 178); + this.tbModeExplain.TabIndex = 34; + this.tbModeExplain.TabStop = false; + this.tbModeExplain.Text = "Requirements for mode \'Database password\'\r\n - Database masterkey contains a passw" + + "ord\r\n - Option \'Remember master password\' is active\r\n\r\nQuick Unlock Entry will b" + + "e used as fallback"; + // + // panel2 + // + this.panel2.Controls.Add(this.cbActive); + this.panel2.Controls.Add(this.lQUPINLength); + this.panel2.Controls.Add(this.lQUMode); + this.panel2.Controls.Add(this.rbPINEnd); + this.panel2.Controls.Add(this.rbPINFront); + this.panel2.Controls.Add(this.tbPINLength); + this.panel2.Controls.Add(this.cbPINMode); + this.panel2.Dock = System.Windows.Forms.DockStyle.Top; + this.panel2.Location = new System.Drawing.Point(5, 5); + this.panel2.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); + this.panel2.Name = "panel2"; + this.panel2.Size = new System.Drawing.Size(823, 279); + this.panel2.TabIndex = 35; + // + // cbActive + // + this.cbActive.AutoSize = true; + this.cbActive.Location = new System.Drawing.Point(14, 17); + this.cbActive.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); + this.cbActive.Name = "cbActive"; + this.cbActive.Size = new System.Drawing.Size(317, 36); + this.cbActive.TabIndex = 42; + this.cbActive.Text = "Enable Quick Unlock"; + this.cbActive.TextAlign = System.Drawing.ContentAlignment.BottomCenter; + this.cbActive.UseVisualStyleBackColor = true; + // + // lQUPINLength + // + this.lQUPINLength.AutoSize = true; + this.lQUPINLength.Location = new System.Drawing.Point(7, 133); + this.lQUPINLength.Margin = new System.Windows.Forms.Padding(5, 0, 5, 0); + this.lQUPINLength.Name = "lQUPINLength"; + this.lQUPINLength.Size = new System.Drawing.Size(155, 32); + this.lQUPINLength.TabIndex = 41; + this.lQUPINLength.Text = "PIN length:"; + // + // lQUMode + // + this.lQUMode.AutoSize = true; + this.lQUMode.Location = new System.Drawing.Point(7, 68); + this.lQUMode.Margin = new System.Windows.Forms.Padding(5, 0, 5, 0); + this.lQUMode.Name = "lQUMode"; + this.lQUMode.Size = new System.Drawing.Size(94, 32); + this.lQUMode.TabIndex = 40; + this.lQUMode.Text = "Mode:"; + // + // rbPINEnd + // + this.rbPINEnd.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.rbPINEnd.AutoSize = true; + this.rbPINEnd.CheckAlign = System.Drawing.ContentAlignment.MiddleRight; + this.rbPINEnd.Location = new System.Drawing.Point(483, 225); + this.rbPINEnd.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); + this.rbPINEnd.Name = "rbPINEnd"; + this.rbPINEnd.Size = new System.Drawing.Size(334, 36); + this.rbPINEnd.TabIndex = 39; + this.rbPINEnd.TabStop = true; + this.rbPINEnd.Text = "Use {0} last characters"; + this.rbPINEnd.UseVisualStyleBackColor = true; + // + // rbPINFront + // + this.rbPINFront.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.rbPINFront.AutoSize = true; + this.rbPINFront.CheckAlign = System.Drawing.ContentAlignment.MiddleRight; + this.rbPINFront.Location = new System.Drawing.Point(484, 178); + this.rbPINFront.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); + this.rbPINFront.Name = "rbPINFront"; + this.rbPINFront.Size = new System.Drawing.Size(335, 36); + this.rbPINFront.TabIndex = 38; + this.rbPINFront.TabStop = true; + this.rbPINFront.Text = "Use {0} first characters"; + this.rbPINFront.UseVisualStyleBackColor = true; + // + // tbPINLength + // + this.tbPINLength.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.tbPINLength.Location = new System.Drawing.Point(639, 129); + this.tbPINLength.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); + this.tbPINLength.MaxLength = 3; + this.tbPINLength.Name = "tbPINLength"; + this.tbPINLength.Size = new System.Drawing.Size(175, 38); + this.tbPINLength.TabIndex = 37; + this.tbPINLength.Tag = "32"; + this.tbPINLength.TextAlign = System.Windows.Forms.HorizontalAlignment.Right; + this.tbPINLength.TextChanged += new System.EventHandler(this.tbPINLength_TextChanged); + // + // cbPINMode + // + this.cbPINMode.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.cbPINMode.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cbPINMode.FormattingEnabled = true; + this.cbPINMode.Items.AddRange(new object[] { + "Quick Unlock entry only", + "Database password"}); + this.cbPINMode.Location = new System.Drawing.Point(189, 64); + this.cbPINMode.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); + this.cbPINMode.Name = "cbPINMode"; + this.cbPINMode.Size = new System.Drawing.Size(624, 39); + this.cbPINMode.TabIndex = 36; + this.cbPINMode.SelectedIndexChanged += new System.EventHandler(this.cbPINMode_SelectedIndexChanged); + // + // OptionsForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(16F, 31F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.BackColor = System.Drawing.Color.Transparent; + this.Controls.Add(this.tcLockAssistOptions); + this.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); + this.Name = "OptionsForm"; + this.Size = new System.Drawing.Size(853, 697); + this.Load += new System.EventHandler(this.UnlockOptions_Load); + this.tcLockAssistOptions.ResumeLayout(false); + this.tabQuickUnlock.ResumeLayout(false); + this.tabQuickUnlock.PerformLayout(); + this.panel2.ResumeLayout(false); + this.panel2.PerformLayout(); + this.ResumeLayout(false); + + } + + #endregion + private System.Windows.Forms.TabControl tcLockAssistOptions; + private System.Windows.Forms.TabPage tabQuickUnlock; + private System.Windows.Forms.CheckBox cbPINDBSpecific; + private System.Windows.Forms.TextBox tbModeExplain; + private System.Windows.Forms.Panel panel2; + private System.Windows.Forms.CheckBox cbActive; + private System.Windows.Forms.Label lQUPINLength; + private System.Windows.Forms.Label lQUMode; + private System.Windows.Forms.RadioButton rbPINEnd; + private System.Windows.Forms.RadioButton rbPINFront; + private System.Windows.Forms.TextBox tbPINLength; + internal System.Windows.Forms.ComboBox cbPINMode; + } +} \ No newline at end of file diff --git a/src/OptionsForm.cs b/src/OptionsForm.cs new file mode 100644 index 0000000..16fe6b7 --- /dev/null +++ b/src/OptionsForm.cs @@ -0,0 +1,137 @@ +using System; +using System.Windows.Forms; + +using KeePass; +using KeePassLib; +using KeePass.UI; + +using PluginTranslation; +using PluginTools; + +namespace LockAssist +{ + public partial class OptionsForm : UserControl + { + private bool FirstTime = false; + + public OptionsForm() + { + InitializeComponent(); + + Text = PluginTranslate.PluginName; + + cbActive.Text = PluginTranslate.Active; + tabQuickUnlock.Text = PluginTranslate.OptionsQUSettings; + lQUMode.Text = PluginTranslate.OptionsQUMode; + lQUPINLength.Text = PluginTranslate.OptionsQUPINLength; + tbModeExplain.Lines = PluginTranslate.OptionsQUReqInfoDB.Split(new string[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); ; + cbPINDBSpecific.Text = PluginTranslate.OptionsQUSettingsPerDB; + cbPINMode.Items.Clear(); + cbPINMode.Items.AddRange(new string[] { PluginTranslate.OptionsQUModeEntry, PluginTranslate.OptionsQUModeDatabasePW }); + } + + internal void InitEx(LockAssistConfig options) + { + cbActive.Checked = options.QU_Active; + cbPINMode.SelectedIndex = options.QU_UsePassword ? 1 : 0; + tbPINLength.Text = options.QU_PINLength.ToString(); + rbPINEnd.Checked = options.QU_UsePasswordFromEnd; + rbPINFront.Checked = !options.QU_UsePasswordFromEnd; + cbPINDBSpecific.Checked = options.QU_DBSpecific; + FirstTime = LockAssistConfig.FirstTime; + } + + internal LockAssistConfig GetOptions() + { + LockAssistConfig options = new LockAssistConfig(); + options.QU_Active = cbActive.Checked; + options.QU_UsePassword = cbPINMode.SelectedIndex == 1; + if (!int.TryParse(tbPINLength.Text, out options.QU_PINLength)) options.QU_PINLength = 4; + options.QU_UsePasswordFromEnd = rbPINEnd.Checked; + options.QU_DBSpecific = cbPINDBSpecific.Checked; + + return options; + } + + private void tbPINLength_TextChanged(object sender, EventArgs e) + { + int len = 0; + if (!int.TryParse(tbPINLength.Text, out len)) len = 4; + if (len < 1) len = 1; + if (len > 32) len = 32; + rbPINFront.Text = string.Format(PluginTranslate.OptionsQUUseFirst, len.ToString()); + rbPINEnd.Text = string.Format(PluginTranslate.OptionsQUUseLast, len.ToString()); + } + + private void cbPINMode_SelectedIndexChanged(object sender, EventArgs e) + { + lQUMode.ForeColor = System.Drawing.SystemColors.ControlText; + if (cbPINMode.SelectedIndex == 0) + { + PwEntry check = QuickUnlock.GetQuickUnlockEntry(Program.MainForm.ActiveDatabase); + if (check != null) return; + if (!FirstTime && (Tools.AskYesNo(PluginTranslate.OptionsQUEntryCreate) == DialogResult.No)) + { + cbPINMode.SelectedIndex = 1; + } + else + { + check = new PwEntry(true, true); + Program.MainForm.ActiveDatabase.RootGroup.AddEntry(check, true); + check.Strings.Set(PwDefs.TitleField, new KeePassLib.Security.ProtectedString(false, QuickUnlockKeyProv.KeyProviderName)); + Tools.ShowInfo(PluginTranslate.OptionsQUEntryCreated); + ShowQuickUnlockEntry(Program.MainForm.ActiveDatabase, check.Uuid); + } + return; + } + if (Program.Config.Security.MasterPassword.RememberWhileOpen) return; + lQUMode.ForeColor = System.Drawing.Color.Red; + if (FirstTime || (cbPINMode.SelectedIndex == 0)) return; + Tools.ShowInfo(PluginTranslate.OptionsQUInfoRememberPassword); + } + + private void tbValidating(object sender, System.ComponentModel.CancelEventArgs e) + { + int len = 0; + if (!int.TryParse((sender as TextBox).Text, out len)) + { + if ((sender as TextBox).Name == "tbPinLength") len = 4; + else len = 60; + } + if ((sender as TextBox).Name == "tbPinLength") len = Math.Max(1, len); + if ((sender as TextBox).Name != "tbPinLength") len = Math.Max(0, len); + int max = int.Parse((string)(sender as TextBox).Tag); + if (len > max) len = max; + if ((sender as TextBox).Text != len.ToString()) (sender as TextBox).Text = len.ToString(); + } + + private void ShowQuickUnlockEntry(PwDatabase db, PwUuid qu) + { + Program.MainForm.UpdateUI(false, null, false, db.RootGroup, true, db.RootGroup, true); + Program.MainForm.EnsureVisibleEntry(qu); + + ListView lv = (Program.MainForm.Controls.Find("m_lvEntries", true)[0] as ListView); + foreach (ListViewItem lvi in lv.Items) + { + PwListItem li = (lvi.Tag as PwListItem); + if (li == null) continue; + + PwEntry pe = li.Entry; + if (pe.Uuid != qu) continue; + lv.FocusedItem = lvi; + ToolStripItem[] tsmi = Program.MainForm.EntryContextMenu.Items.Find("m_ctxEntryEdit", false); + if (tsmi != null) tsmi[0].PerformClick(); + break; + } + } + + private void UnlockOptions_Load(object sender, EventArgs e) + { + if ((Program.MainForm.ActiveDatabase == null) || !Program.MainForm.ActiveDatabase.IsOpen) + { + cbPINDBSpecific.Enabled = false; + cbPINDBSpecific.Checked = false; + } + } + } +} diff --git a/src/PluginTranslation.cs b/src/PluginTranslation.cs new file mode 100644 index 0000000..3fd31d2 --- /dev/null +++ b/src/PluginTranslation.cs @@ -0,0 +1,240 @@ +using System; +using System.Collections.Generic; +using System.Xml; +using System.Xml.Serialization; +using System.IO; +using System.Reflection; + +using KeePass.Plugins; +using KeePass.Util; +using KeePassLib.Utility; + +using PluginTools; +using System.Windows.Forms; + +namespace PluginTranslation +{ + public class TranslationChangedEventArgs: EventArgs + { + public string OldLanguageIso6391 = string.Empty; + public string NewLanguageIso6391 = string.Empty; + + public TranslationChangedEventArgs(string OldLanguageIso6391, string NewLanguageIso6391) + { + this.OldLanguageIso6391 = OldLanguageIso6391; + this.NewLanguageIso6391 = NewLanguageIso6391; + } + } + + public static class PluginTranslate + { + public static long TranslationVersion = 0; + public static event EventHandler TranslationChanged = null; + private static string LanguageIso6391 = string.Empty; + #region Definitions of translated texts go here + public const string PluginName = "Lock Assist"; + public static readonly string FirstTimeInfo = @"Quick Unlock offers two operation modes. +Please choose your preferred way of working."; + public static readonly string OptionsQUMode = @"Mode:"; + public static readonly string Active = @"Quick Unlock active"; + public static readonly string KeyProvNoQuickUnlock = @"No Quick Unlock key found. + +Quick Unlock is not possible."; + public static readonly string OptionsQUReqInfoDB = @"Prerequsites for mode 'database password': +- Database masterkey contains a password +- Option 'Remember master password' is active + +An existing Quick Unlock entry will be used as fallback"; + public static readonly string OptionsQUSettings = @"Quick Unlock"; + public static readonly string OptionsQUModeEntry = @"Quick Unlock entry only"; + public static readonly string OptionsQUModeDatabasePW = @"Database password"; + public static readonly string OptionsQUEntryCreated = @"Quick Unlock entry created. + +Please edit and set Quick Unlock PIN as password"; + public static readonly string OptionsQUSettingsPerDB = @"Settings are DB specific"; + public static readonly string OptionsSwitchDBToGeneral = @"Database specific settings switched off. + +Click '{0}' to use the global settings for this database. +Click '{1}' to make this database's settings the new global settings."; + public static readonly string OptionsQUPINLength = @"PIN length:"; + public static readonly string ButtonUnlock = @"Unlock"; + public static readonly string UnlockLabel = @"Quick Unlock PIN:"; + public static readonly string OptionsQUEntryCreate = @"Quick Unlock entry could not be found. + +Create it now?"; + public static readonly string KeyProvNoCreate = @"This key provider cannot be used to create keys."; + public static readonly string OptionsQUInfoRememberPassword = @"'Remember master password' needs to be active in Options -> Security. +Please don't forget to activate this setting"; + public static readonly string OptionsQUUseLast = @"Use last {0} characters as PIN"; + public static readonly string OptionsQUUseFirst = @"Use first {0} characters as PIN"; + public static readonly string WrongPIN = @"The entered PIN was not correct. + +The database stays locked and can only be unlocked with the original masterkey"; + #endregion + + #region NO changes in this area + private static StringDictionary m_translation = new StringDictionary(); + + public static void Init(Plugin plugin, string LanguageCodeIso6391) + { + List lDebugStrings = new List(); + m_translation.Clear(); + bool bError = true; + LanguageCodeIso6391 = InitTranslation(plugin, lDebugStrings, LanguageCodeIso6391, out bError); + if (bError && (LanguageCodeIso6391.Length > 2)) + { + LanguageCodeIso6391 = LanguageCodeIso6391.Substring(0, 2); + lDebugStrings.Add("Trying fallback: " + LanguageCodeIso6391); + LanguageCodeIso6391 = InitTranslation(plugin, lDebugStrings, LanguageCodeIso6391, out bError); + } + if (bError) + { + PluginDebug.AddError("Reading translation failed", 0, lDebugStrings.ToArray()); + LanguageCodeIso6391 = "en"; + } + else + { + List lTranslatable = new List( + typeof(PluginTranslate).GetFields(BindingFlags.Static | BindingFlags.Public) + ).FindAll(x => x.IsInitOnly); + lDebugStrings.Add("Parsing complete"); + lDebugStrings.Add("Translated texts read: " + m_translation.Count.ToString()); + lDebugStrings.Add("Translatable texts: " + lTranslatable.Count.ToString()); + foreach (FieldInfo f in lTranslatable) + { + if (m_translation.ContainsKey(f.Name)) + { + lDebugStrings.Add("Key found: " + f.Name); + f.SetValue(null, m_translation[f.Name]); + } + else + lDebugStrings.Add("Key not found: " + f.Name); + } + PluginDebug.AddInfo("Reading translations finished", 0, lDebugStrings.ToArray()); + } + if (TranslationChanged != null) + { + TranslationChanged(null, new TranslationChangedEventArgs(LanguageIso6391, LanguageCodeIso6391)); + } + LanguageIso6391 = LanguageCodeIso6391; + lDebugStrings.Clear(); + } + + private static string InitTranslation(Plugin plugin, List lDebugStrings, string LanguageCodeIso6391, out bool bError) + { + if (string.IsNullOrEmpty(LanguageCodeIso6391)) + { + lDebugStrings.Add("No language identifier supplied, using 'en' as fallback"); + LanguageCodeIso6391 = "en"; + } + string filename = GetFilename(plugin.GetType().Namespace, LanguageCodeIso6391); + lDebugStrings.Add("Translation file: " + filename); + + if (!File.Exists(filename)) //If e. g. 'plugin.zh-tw.language.xml' does not exist, try 'plugin.zh.language.xml' + { + lDebugStrings.Add("File does not exist"); + bError = true; + return LanguageCodeIso6391; + } + else + { + string translation = string.Empty; + try { translation = File.ReadAllText(filename); } + catch (Exception ex) + { + lDebugStrings.Add("Error reading file: " + ex.Message); + LanguageCodeIso6391 = "en"; + bError = true; + return LanguageCodeIso6391; + } + XmlSerializer xs = new XmlSerializer(m_translation.GetType()); + lDebugStrings.Add("File read, parsing content"); + try + { + m_translation = (StringDictionary)xs.Deserialize(new StringReader(translation)); + } + catch (Exception ex) + { + lDebugStrings.Add("Error parsing file: " + ex.Message); + LanguageCodeIso6391 = "en"; + MessageBox.Show("Error parsing translation file\n" + ex.Message, PluginName, MessageBoxButtons.OK, MessageBoxIcon.Error); + bError = true; + return LanguageCodeIso6391; + } + bError = false; + return LanguageCodeIso6391; + } + } + + private static string GetFilename(string plugin, string lang) + { + string filename = UrlUtil.GetFileDirectory(WinUtil.GetExecutable(), true, true); + filename += KeePass.App.AppDefs.PluginsDir + UrlUtil.LocalDirSepChar + "Translations" + UrlUtil.LocalDirSepChar; + filename += plugin + "." + lang + ".language.xml"; + return filename; + } + #endregion + } + + #region NO changes in this area + [XmlRoot("Translation")] + public class StringDictionary : Dictionary, IXmlSerializable + { + public System.Xml.Schema.XmlSchema GetSchema() + { + return null; + } + + public void ReadXml(XmlReader reader) + { + bool wasEmpty = reader.IsEmptyElement; + reader.Read(); + if (wasEmpty) return; + bool bFirst = true; + while (reader.NodeType != XmlNodeType.EndElement) + { + if (bFirst) + { + bFirst = false; + try + { + reader.ReadStartElement("TranslationVersion"); + PluginTranslate.TranslationVersion = reader.ReadContentAsLong(); + reader.ReadEndElement(); + } + catch { } + } + reader.ReadStartElement("item"); + reader.ReadStartElement("key"); + string key = reader.ReadContentAsString(); + reader.ReadEndElement(); + reader.ReadStartElement("value"); + string value = reader.ReadContentAsString(); + reader.ReadEndElement(); + this.Add(key, value); + reader.ReadEndElement(); + reader.MoveToContent(); + } + reader.ReadEndElement(); + } + + public void WriteXml(XmlWriter writer) + { + writer.WriteStartElement("TranslationVersion"); + writer.WriteString(PluginTranslate.TranslationVersion.ToString()); + writer.WriteEndElement(); + foreach (string key in this.Keys) + { + writer.WriteStartElement("item"); + writer.WriteStartElement("key"); + writer.WriteString(key); + writer.WriteEndElement(); + writer.WriteStartElement("value"); + writer.WriteString(this[key]); + writer.WriteEndElement(); + writer.WriteEndElement(); + } + } + } + #endregion +} \ No newline at end of file diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..1fb4362 --- /dev/null +++ b/src/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// Allgemeine Informationen über eine Assembly werden über die folgenden +// Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, +// die einer Assembly zugeordnet sind. +[assembly: AssemblyTitle("LockAssist")] +[assembly: AssemblyDescription("Enhance KeePass database locking & unlocking")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Rookiestyle")] +[assembly: AssemblyProduct("KeePass Plugin")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Durch Festlegen von ComVisible auf FALSE werden die Typen in dieser Assembly +// für COM-Komponenten unsichtbar. Wenn Sie auf einen Typ in dieser Assembly von +// COM aus zugreifen müssen, sollten Sie das ComVisible-Attribut für diesen Typ auf "True" festlegen. +[assembly: ComVisible(true)] + +// Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird +[assembly: Guid("4712d887-6685-4cb1-b67b-e981119fe3d2")] + +// Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten: +// +// Hauptversion +// Nebenversion +// Buildnummer +// Revision +// +// Sie können alle Werte angeben oder Standardwerte für die Build- und Revisionsnummern verwenden, +// indem Sie "*" wie unten gezeigt eingeben: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0")] +[assembly: AssemblyFileVersion("1.0.0")] diff --git a/src/QuickUnlock.cs b/src/QuickUnlock.cs new file mode 100644 index 0000000..d98906d --- /dev/null +++ b/src/QuickUnlock.cs @@ -0,0 +1,263 @@ +using KeePass; +using KeePass.Forms; +using KeePass.Plugins; +using KeePass.UI; +using KeePassLib; +using KeePassLib.Collections; +using KeePassLib.Keys; +using KeePassLib.Security; +using KeePassLib.Serialization; +using System; +using System.Collections.Generic; +using System.Windows.Forms; + +using PluginTranslation; +using PluginTools; + +namespace LockAssist +{ + /* + * QuickUnlock part is inspired by KeePass2Android's QuickUnlock and https://github.com/JanisEst/KeePassQuickUnlock + * Additional features added: + * - DB specific settings + * - Restore previously used masterkey (allow QuickUnlock multiple times in a row, allow printing of emergency sheets, ...) + * - Show explicit error message if wrong QuickUnlock key is entered + * - Automate creation of QuickUnlock entry + * - Additional smaller adjustments + */ + internal partial class QuickUnlock + { + private QuickUnlockKeyProv m_kp = null; + + public QuickUnlock() + { + Init(); + } + private void Init() + { + m_kp = new QuickUnlockKeyProv(); + Program.KeyProviderPool.Add(m_kp); + Program.MainForm.FileClosingPre += OnFileClosePre_QU; + Program.MainForm.FileOpened += OnFileOpened_QU; + GlobalWindowManager.WindowAdded += OnWindowAdded_QU; + PluginDebug.AddInfo("Quick Unlock: Initialized", 0); + } + + #region Eventhandler for opening and closing a DB + private void OnFileOpened_QU(object sender, FileOpenedEventArgs e) + { + var MyOptions = LockAssistConfig.GetOptions(e.Database); + if (LockAssistConfig.FirstTime && + (!MyOptions.QU_UsePassword && (GetQuickUnlockEntry(e.Database) == null) + || (MyOptions.QU_UsePassword && !Program.Config.Security.MasterPassword.RememberWhileOpen))) + { + Tools.ShowInfo(PluginTranslate.FirstTimeInfo); + Tools.ShowOptions(); + LockAssistConfig.FirstTime = false; + } + if (!MyOptions.QU_Active) return; + //Restore previously stored information about the masterkey + QuickUnlockOldKeyInfo quOldKey = QuickUnlockKeyProv.GetOldKey(e.Database); + if (quOldKey == null) + { + PluginDebug.AddInfo("Quick Unlock: DB opened, no encrypted master key available to restore"); + return; + } + PluginDebug.AddInfo("Quick Unlock: DB opened, restore encrypted master key"); + KcpCustomKey ck = (KcpCustomKey)e.Database.MasterKey.GetUserKey(typeof(KcpCustomKey)); + if ((ck == null) || (ck.Name != QuickUnlockKeyProv.KeyProviderName)) + { + //Quick Unlock was not used + return; + } + e.Database.MasterKey.RemoveUserKey(ck); + if (quOldKey.pwHash != null) + { + KcpPassword p; + p = QuickUnlockKeyProv.DeserializePassword(quOldKey.pwHash, Program.Config.Security.MasterPassword.RememberWhileOpen); + if (p.Password == null && quOldKey.HasPassword) p = new KcpPassword(new byte[0] { }, Program.Config.Security.MasterPassword.RememberWhileOpen); + e.Database.MasterKey.AddUserKey(p); + } + if (!string.IsNullOrEmpty(quOldKey.keyFile)) e.Database.MasterKey.AddUserKey(new KcpKeyFile(quOldKey.keyFile)); + if (quOldKey.account) e.Database.MasterKey.AddUserKey(new KcpUserAccount()); + Program.Config.Defaults.SetKeySources(e.Database.IOConnectionInfo, e.Database.MasterKey); + } + + private void OnFileClosePre_QU(object sender, FileClosingEventArgs e) + { + //Do quick unlock only in case of locking + //Do NOT do quick unlock in case of closing the database + PluginDebug.AddInfo("Quick Unlock: File closing = " + e.Flags.ToString()); + if (e.Flags != FileEventFlags.Locking) + { + QuickUnlockKeyProv.RemoveDb(e.Database); + return; + } + var MyOptions = LockAssistConfig.GetOptions(e.Database); + if (!MyOptions.QU_Active) return; + ProtectedString QuickUnlockKey = null; + if (Program.Config.Security.MasterPassword.RememberWhileOpen && MyOptions.QU_UsePassword) + QuickUnlockKey = GetQuickUnlockKeyFromMasterKey(e.Database); + if (QuickUnlockKey == null) + QuickUnlockKey = GetQuickUnlockKeyFromEntry(e.Database); + QuickUnlockKey = TrimQuickUnlockKey(QuickUnlockKey, MyOptions); + if (QuickUnlockKey == null) + { + PluginDebug.AddError("Quick Unlock: Can't derive key, Quick Unlock not possible"); + return; + } + QuickUnlockKeyProv.AddDb(e.Database, QuickUnlockKey, Program.Config.Security.MasterPassword.RememberWhileOpen && MyOptions.QU_UsePassword); + PluginDebug.AddInfo("Quick Unlock: key added"); + } + #endregion + + #region Unlock / KeyPromptForm + private void OnWindowAdded_QU(object sender, GwmWindowEventArgs e) + { + if (!(e.Form is KeyPromptForm) && !(e.Form is KeyCreationForm)) return; + PluginDebug.AddInfo(e.Form.GetType().Name + " added", 0); + e.Form.Shown += (o, x) => OnKeyFormShown_QU(o, false); + } + + public static void OnKeyFormShown_QU(object sender, bool resetFile) + { + Form keyform = (sender as Form); + try + { + ComboBox cmbKeyFile = (ComboBox)Tools.GetControl("m_cmbKeyFile", keyform); + if (cmbKeyFile == null) + { + PluginDebug.AddError("Cant't find m_cmbKeyFile'", 0, "Form: " + keyform.GetType().Name); + return; + } + int index = cmbKeyFile.Items.IndexOf(QuickUnlockKeyProv.KeyProviderName); + //Quick Unlock cannot be used to create a key ==> Remove it from list of key providers + if (keyform is KeyCreationForm) + { + PluginDebug.AddInfo("Removing Quick Unlock from key providers", 0); + if (index == -1) return; + cmbKeyFile.Items.RemoveAt(index); + List keyfiles = (List)Tools.GetField("m_lKeyFileNames", keyform); + if (keyfiles != null) keyfiles.Remove(QuickUnlockKeyProv.KeyProviderName); + return; + } + + //Key prompt form is shown + IOConnectionInfo dbIOInfo = (IOConnectionInfo)Tools.GetField("m_ioInfo", keyform); + //If Quick Unlock is possible show the Quick Unlock form + if ((index != -1) && (dbIOInfo != null) && QuickUnlockKeyProv.HasDB(dbIOInfo.Path)) + { + cmbKeyFile.SelectedIndex = index; + CheckBox cbPassword = (CheckBox)Tools.GetControl("m_cbPassword", keyform); + CheckBox cbAccount = (CheckBox)Tools.GetControl("m_cbUserAccount", keyform); + Button bOK = (Button)Tools.GetControl("m_btnOK", keyform); + if ((bOK != null) && (cbPassword != null) && (cbAccount != null)) + { + UIUtil.SetChecked(cbPassword, false); + UIUtil.SetChecked(cbAccount, false); + bOK.PerformClick(); + } + else + { + PluginDebug.AddError("Quick Unlock form cannot be shown", 0, + "Form: "+keyform.GetType().Name, + "Password checkbox: " + (cbPassword == null ? "null" : cbPassword.Name + " / " + cbPassword.GetType().Name), + "Account checkbox: " + (cbAccount == null ? "null" : cbAccount.Name + " / " + cbAccount.GetType().Name), + "OK button: " + (bOK == null ? "null" : bOK.Name + " / " + bOK.GetType().Name) + ); + } + return; + } + + //Quick Unlock is not possible => Remove it from list of key providers + if ((resetFile || ((dbIOInfo != null) && !QuickUnlockKeyProv.HasDB(dbIOInfo.Path))) && (index != -1)) + { + cmbKeyFile.Items.RemoveAt(index); + List keyfiles = (List)Tools.GetField("m_lKeyFileNames", keyform); + if (keyfiles != null) keyfiles.Remove(QuickUnlockKeyProv.KeyProviderName); + if (resetFile) cmbKeyFile.SelectedIndex = 0; + } + } + catch (Exception ex) + { + PluginDebug.AddError(ex.Message); + } + } + #endregion + + #region QuickUnlockKey handling + private ProtectedString GetQuickUnlockKeyFromMasterKey(PwDatabase db) + { + /* + * Try to create QuickUnlockKey based on password + * + * If no password is contained in MasterKey there is + * EITHER no password at all + * OR the database was unlocked with Quick Unlock + * In these case ask our key provider for the original password + */ + ProtectedString QuickUnlockKey = null; + try + { + KcpPassword pw = (KcpPassword)db.MasterKey.GetUserKey(typeof(KcpPassword)); + if (pw != null) + QuickUnlockKey = pw.Password; + } + catch (Exception ex) { PluginDebug.AddError("Quick Unlock: " + ex.Message); } + if (QuickUnlockKey != null) //Do NOT check QuickUnlockKey.Length, an empty string is treated like no password otherwise + { + PluginDebug.AddInfo("Quick Unlock: Quick Unlock key found", 0); + return QuickUnlockKey; + } + PluginDebug.AddError("Quick Unlock: Quick Unlock key NOT found", 0, + "MasterPassword.RememberWhileOpen: " + Program.Config.Security.MasterPassword.RememberWhileOpen.ToString()); + return null; + } + + private ProtectedString GetQuickUnlockKeyFromEntry(PwDatabase db) + { + PwEntry QuickUnlockEntry = GetQuickUnlockEntry(db); + if (QuickUnlockEntry == null) + { + PluginDebug.AddInfo("Quick Unlock: Quick Unlock entry NOT found", 0); + return null; + } + PluginDebug.AddInfo("Quick Unlock: Quick Unlock entry found", 0); + return QuickUnlockEntry.Strings.GetSafe(PwDefs.PasswordField); + } + + public static PwEntry GetQuickUnlockEntry(PwDatabase db) + { + if ((db == null) || !db.IsOpen) return null; + SearchParameters sp = new SearchParameters(); + sp.SearchInTitles = true; + sp.ExcludeExpired = true; + sp.SearchString = QuickUnlockKeyProv.KeyProviderName; + PwObjectList entries = new PwObjectList(); + db.RootGroup.SearchEntries(sp, entries); + if ((entries == null) || (entries.UCount == 0)) return null; + return entries.GetAt(0); + } + + private ProtectedString TrimQuickUnlockKey(ProtectedString QuickUnlockKey, LockAssistConfig lac) + { + if ((QuickUnlockKey == null) || (QuickUnlockKey.Length <= lac.QU_PINLength)) return QuickUnlockKey; + int startIndex = 0; + if (!lac.QU_UsePasswordFromEnd) startIndex = lac.QU_PINLength; + QuickUnlockKey = QuickUnlockKey.Remove(startIndex, QuickUnlockKey.Length - lac.QU_PINLength); + return QuickUnlockKey; + } + #endregion + + internal void Clear() + { + Program.KeyProviderPool.Remove(m_kp); + m_kp = null; + QuickUnlockKeyProv.Clear(); + Program.MainForm.FileClosingPre -= OnFileClosePre_QU; + Program.MainForm.FileOpened -= OnFileOpened_QU; + GlobalWindowManager.WindowAdded -= OnWindowAdded_QU; + PluginDebug.AddInfo("Quick Unlock: Terminated", 0); + } + } +} diff --git a/src/QuickUnlockKeyProv.cs b/src/QuickUnlockKeyProv.cs new file mode 100644 index 0000000..621b550 --- /dev/null +++ b/src/QuickUnlockKeyProv.cs @@ -0,0 +1,271 @@ +using System; +using System.Reflection; +using System.Collections.Generic; +using System.Windows.Forms; +using System.Security.Cryptography; + +using KeePassLib.Keys; +using KeePassLib; +using KeePassLib.Cryptography; +using KeePassLib.Security; +using KeePassLib.Utility; +using KeePassLib.Cryptography.Cipher; + +using PluginTranslation; +using PluginTools; + +namespace LockAssist +{ + /* + * QuickUnlock part is inspired by KeePass2Android's QuickUnlock and https://github.com/JanisEst/KeePassQuickUnlock + * Additional features added: + * - DB specific settings + * - Restore previously used masterkey (allow QuickUnlock multiple times in a row, allow printing of emergency sheets, ...) + * - Show explicit error message if wrong QuickUnlock key is entered + * - Automate creation of QuickUnlock entry + * - Additional smaller adjustments + */ + public class QuickUnlockOldKeyInfo + { + public ProtectedString QuickUnlockKey = ProtectedString.EmptyEx; + public ProtectedBinary pwHash = null; + public string keyFile = string.Empty; + public bool account = false; + public ProtectedBinary PINCheck = null; + public bool HasPassword = false; + } + + public class QuickUnlockKeyProv : KeyProvider + { + public static string KeyProviderName = PluginTranslate.PluginName + " - Quick Unlock"; + private static byte[] m_PINCheck = StrUtil.Utf8.GetBytes(KeyProviderName); + public override string Name { get { return KeyProviderName; } } + public override bool SecureDesktopCompatible { get { return true; } } + public override bool Exclusive { get { return true; } } + public override bool DirectKey { get { return true; } } + public override bool GetKeyMightShowGui { get { return true; } } + + private static Dictionary m_hashedKey = new Dictionary(); + private static Dictionary m_originalKey = new Dictionary(); + + public override byte[] GetKey(KeyProviderQueryContext ctx) + { + if (ctx.CreatingNewKey) //should not happen but you never know + { + Tools.ShowError(PluginTranslate.KeyProvNoCreate); + return null; + } + + //Check for existing Quick Unlock data + ProtectedBinary encryptedKey = new ProtectedBinary(); + if (!m_hashedKey.TryGetValue(ctx.DatabasePath, out encryptedKey)) + { + Tools.ShowError(PluginTranslate.KeyProvNoQuickUnlock); + return null; + } + + var fQuickUnlock = new UnlockForm(); + if (KeePass.UI.UIUtil.ShowDialogNotValue(fQuickUnlock, DialogResult.OK)) return null; + ProtectedString psQuickUnlockKey = fQuickUnlock.QuickUnlockKey; + KeePass.UI.UIUtil.DestroyForm(fQuickUnlock); + + //Remove Quick Unlock data - there is only one attempt + m_hashedKey.Remove(ctx.DatabasePath); + if (KeePass.UI.GlobalWindowManager.TopWindow is KeePass.Forms.KeyPromptForm && !VerifyPin(ctx, psQuickUnlockKey)) + { + QuickUnlock.OnKeyFormShown_QU(KeePass.UI.GlobalWindowManager.TopWindow, true); + Tools.ShowError(PluginTranslate.WrongPIN); + return null; + } + return DecryptKey(psQuickUnlockKey, encryptedKey).ReadData(); + } + + private bool VerifyPin(KeyProviderQueryContext ctx, ProtectedString psQuickUnlockKey) + { + //Verify Quick Unlock PIN + QuickUnlockOldKeyInfo quOldKey = null; + if (!m_originalKey.TryGetValue(ctx.DatabasePath, out quOldKey)) return false; + byte[] comparePIN = DecryptKey(psQuickUnlockKey, quOldKey.PINCheck).ReadData(); + return StrUtil.Utf8.GetString(comparePIN) == KeyProviderName; + } + + public static QuickUnlockOldKeyInfo GetOldKey(PwDatabase db) + { + QuickUnlockOldKeyInfo quOldKey = null; + if (!m_originalKey.TryGetValue(db.IOConnectionInfo.Path, out quOldKey)) return null; + if ((quOldKey.pwHash != null) && (quOldKey.pwHash.Length != 0)) + quOldKey.pwHash = DecryptKey(quOldKey.QuickUnlockKey, quOldKey.pwHash); + m_originalKey.Remove(db.IOConnectionInfo.Path); + return quOldKey; + } + + public static void AddDb(PwDatabase db, ProtectedString QuickUnlockKey, bool savePw) + { + RemoveDb(db); + ProtectedBinary pbKey = CreateMasterKeyHash(db.MasterKey); + m_hashedKey.Add(db.IOConnectionInfo.Path, EncryptKey(QuickUnlockKey, pbKey)); + AddOldMasterKey(db, QuickUnlockKey, savePw); + } + + private static void AddOldMasterKey(PwDatabase db, ProtectedString QuickUnlockKey, bool savePw) + { + QuickUnlockOldKeyInfo quOldKey = new QuickUnlockOldKeyInfo(); + quOldKey.QuickUnlockKey = QuickUnlockKey; + quOldKey.HasPassword = db.MasterKey.ContainsType(typeof(KcpPassword)); + if (quOldKey.HasPassword) + { + var pbPasswordSerialized = SerializePassword(db.MasterKey.GetUserKey(typeof(KcpPassword)) as KcpPassword, savePw); + quOldKey.pwHash = EncryptKey(QuickUnlockKey, pbPasswordSerialized); + } + if (db.MasterKey.ContainsType(typeof(KcpKeyFile))) + quOldKey.keyFile = (db.MasterKey.GetUserKey(typeof(KcpKeyFile)) as KcpKeyFile).Path; + quOldKey.account = db.MasterKey.ContainsType(typeof(KcpUserAccount)); + quOldKey.PINCheck = EncryptKey(QuickUnlockKey, new ProtectedBinary(true, m_PINCheck)); + m_originalKey.Add(db.IOConnectionInfo.Path, quOldKey); + } + + public static void Clear() + { + m_hashedKey.Clear(); + m_originalKey.Clear(); + } + + public static bool HasDB(string db) + { + if (m_hashedKey.ContainsKey(db)) return true; + m_originalKey.Remove(db); + return false; + } + + public static void RemoveDb(PwDatabase db) + { + if (db == null) return; + if (!string.IsNullOrEmpty(db.IOConnectionInfo.Path)) + { + RemoveDb(db.IOConnectionInfo.Path); + return; + } + KeePass.UI.PwDocument doc = KeePass.Program.MainForm.DocumentManager.FindDocument(db); + if ((doc == null) || string.IsNullOrEmpty(doc.LockedIoc.Path)) return; + RemoveDb(doc.LockedIoc.Path); + } + + public static void RemoveDb(string ioc) + { + bool bRemoved = m_hashedKey.Remove(ioc); + bRemoved |= m_originalKey.Remove(ioc); + if (bRemoved) PluginDebug.AddInfo("Quick Unlock - Removed Quick Unlock data", 10, "Database: " + ioc); + } + + private static ProtectedBinary CreateMasterKeyHash(CompositeKey mk) + { + List keys = new List(); + int keysLength = 0; + foreach (var key in mk.UserKeys) //Hopefully we never need to consider the sequence... + { + ProtectedBinary pb = key.KeyData; + if (pb != null) + { + var pbArray = pb.ReadData(); + keys.Add(pbArray); + keysLength += pbArray.Length; + MemUtil.ZeroByteArray(pbArray); + } + } + + byte[] allKeys = new byte[keysLength]; + int index = 0; + foreach (byte[] key in keys) + { + Array.Copy(key, 0, allKeys, index, key.Length); + index += key.Length; + MemUtil.ZeroByteArray(key); + } + + var result = new ProtectedBinary(true, allKeys); + MemUtil.ZeroByteArray(allKeys); + return result; + } + + private static ProtectedBinary SerializePassword(KcpPassword p, bool savePassword) + { + //returned array always contains password hash + //password is contained only if requested + //check for p.Password != null as the user might disable Program.Config.Security.MasterPassword.RememberWhileOpen anytime + if (savePassword && (p.Password != null) && !p.Password.IsEmpty) + { + byte[] result = new byte[p.KeyData.Length + p.Password.ReadUtf8().Length]; + Array.Copy(p.KeyData.ReadData(), result, p.KeyData.Length); + Array.Copy(p.Password.ReadUtf8(), 0, result, p.KeyData.Length, p.Password.ReadUtf8().Length); + return new ProtectedBinary(true, result); + } + return p.KeyData; + } + + public static KcpPassword DeserializePassword(ProtectedBinary serialized, bool setPassword) + { + //if password is stored and should be retrieved + //simply create a new instance + //instead of messing around with private members + KcpPassword p = new KcpPassword(string.Empty); + if (setPassword && (serialized.Length > p.KeyData.Length)) + { + byte[] pw = new byte[serialized.Length - p.KeyData.Length]; + Array.Copy(serialized.ReadData(), p.KeyData.Length, pw, 0, pw.Length); + ProtectedString pws = new ProtectedString(true, pw); + MemUtil.ZeroByteArray(pw); + return new KcpPassword(pws.ReadString()); + } + + if (serialized.Length != p.KeyData.Length) return null; + + ProtectedBinary pb = new ProtectedBinary(true, serialized.ReadData()); + typeof(KcpPassword).GetField("m_pbKeyData", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(p, pb); + typeof(KcpPassword).GetField("m_psPassword", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(p, null); + return p; + } + + private static ProtectedBinary EncryptKey(ProtectedString QuickUnlockKey, ProtectedBinary pbKey) + { + byte[] iv = CryptoRandom.Instance.GetRandomBytes(12); + ChaCha20Cipher cipher = new ChaCha20Cipher(AdjustQuickUnlockKey(QuickUnlockKey), iv); + + byte[] bKey = pbKey.ReadData(); + cipher.Encrypt(bKey, 0, bKey.Length); + + byte[] result = new byte[iv.Length + bKey.Length]; + iv.CopyTo(result, 0); + bKey.CopyTo(result, iv.Length); + MemUtil.ZeroByteArray(bKey); + + var pbResult = new ProtectedBinary(true, result); + MemUtil.ZeroByteArray(result); + return pbResult; + } + + private static ProtectedBinary DecryptKey(ProtectedString QuickUnlockKey, ProtectedBinary pbCrypted) + { + byte[] crypted = pbCrypted.ReadData(); + byte[] iv = new byte[12]; + Array.Copy(crypted, iv, iv.Length); + + byte[] cryptedKey = new byte[crypted.Length - iv.Length]; + Array.Copy(crypted, iv.Length, cryptedKey, 0, cryptedKey.Length); + + ChaCha20Cipher cipher = new ChaCha20Cipher(AdjustQuickUnlockKey(QuickUnlockKey), iv); + cipher.Decrypt(cryptedKey, 0, cryptedKey.Length); + ProtectedBinary pbDecrypted = new ProtectedBinary(true, cryptedKey); + MemUtil.ZeroByteArray(cryptedKey); + return pbDecrypted; + } + + private static byte[] AdjustQuickUnlockKey(ProtectedString QuickUnlockKey) + { + byte[] bUtf8 = QuickUnlockKey.ReadUtf8(); + SHA256Managed sha = new SHA256Managed(); + byte[] result = sha.ComputeHash(bUtf8); + MemUtil.ZeroByteArray(bUtf8); + return result; + } + } +} diff --git a/src/UnlockForm.Designer.cs b/src/UnlockForm.Designer.cs new file mode 100644 index 0000000..496784c --- /dev/null +++ b/src/UnlockForm.Designer.cs @@ -0,0 +1,127 @@ +using KeePass.UI; +namespace LockAssist +{ + partial class UnlockForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.lLabel = new System.Windows.Forms.Label(); + this.bUnlock = new System.Windows.Forms.Button(); + this.bCancel = new System.Windows.Forms.Button(); + this.cbTogglePin = new System.Windows.Forms.CheckBox(); + this.stbPIN = new KeePass.UI.SecureTextBoxEx(); + this.SuspendLayout(); + // + // lLabel + // + this.lLabel.AutoSize = true; + this.lLabel.Location = new System.Drawing.Point(75, 67); + this.lLabel.Margin = new System.Windows.Forms.Padding(5, 0, 5, 0); + this.lLabel.Name = "lLabel"; + this.lLabel.Size = new System.Drawing.Size(243, 32); + this.lLabel.TabIndex = 1; + this.lLabel.Text = "Quick Unlock PIN:"; + // + // bUnlock + // + this.bUnlock.DialogResult = System.Windows.Forms.DialogResult.OK; + this.bUnlock.Location = new System.Drawing.Point(347, 181); + this.bUnlock.Margin = new System.Windows.Forms.Padding(5); + this.bUnlock.Name = "bUnlock"; + this.bUnlock.Size = new System.Drawing.Size(178, 55); + this.bUnlock.TabIndex = 2; + this.bUnlock.Text = "Unlock"; + this.bUnlock.UseVisualStyleBackColor = true; + // + // bCancel + // + this.bCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.bCancel.Location = new System.Drawing.Point(535, 181); + this.bCancel.Margin = new System.Windows.Forms.Padding(5); + this.bCancel.Name = "bCancel"; + this.bCancel.Size = new System.Drawing.Size(178, 55); + this.bCancel.TabIndex = 3; + this.bCancel.Text = "Cancel"; + this.bCancel.UseVisualStyleBackColor = true; + // + // cbTogglePin + // + this.cbTogglePin.Appearance = System.Windows.Forms.Appearance.Button; + this.cbTogglePin.AutoSize = true; + this.cbTogglePin.CheckAlign = System.Drawing.ContentAlignment.MiddleCenter; + this.cbTogglePin.Checked = true; + this.cbTogglePin.CheckState = System.Windows.Forms.CheckState.Checked; + this.cbTogglePin.Location = new System.Drawing.Point(647, 59); + this.cbTogglePin.Margin = new System.Windows.Forms.Padding(5); + this.cbTogglePin.Name = "cbTogglePin"; + this.cbTogglePin.Size = new System.Drawing.Size(58, 42); + this.cbTogglePin.TabIndex = 1; + this.cbTogglePin.Text = "***"; + this.cbTogglePin.UseVisualStyleBackColor = true; + this.cbTogglePin.CheckedChanged += new System.EventHandler(this.togglePIN_CheckedChanged); + // + // stbPIN + // + this.stbPIN.Location = new System.Drawing.Point(350, 62); + this.stbPIN.Margin = new System.Windows.Forms.Padding(5); + this.stbPIN.Name = "stbPIN"; + this.stbPIN.Size = new System.Drawing.Size(272, 38); + this.stbPIN.TabIndex = 0; + // + // UnlockForm + // + this.AcceptButton = this.bUnlock; + this.AutoScaleDimensions = new System.Drawing.SizeF(16F, 31F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.bCancel; + this.ClientSize = new System.Drawing.Size(734, 290); + this.Controls.Add(this.cbTogglePin); + this.Controls.Add(this.bCancel); + this.Controls.Add(this.bUnlock); + this.Controls.Add(this.lLabel); + this.Controls.Add(this.stbPIN); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.Margin = new System.Windows.Forms.Padding(5); + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "UnlockForm"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Quick Unlock"; + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private SecureTextBoxEx stbPIN; + private System.Windows.Forms.Label lLabel; + private System.Windows.Forms.Button bUnlock; + private System.Windows.Forms.Button bCancel; + private System.Windows.Forms.CheckBox cbTogglePin; + } +} \ No newline at end of file diff --git a/src/UnlockForm.cs b/src/UnlockForm.cs new file mode 100644 index 0000000..e392c18 --- /dev/null +++ b/src/UnlockForm.cs @@ -0,0 +1,44 @@ +using System.Windows.Forms; +using System.Drawing; +using KeePassLib.Security; + +using PluginTranslation; +using PluginTools; + +namespace LockAssist +{ + public partial class UnlockForm : Form + { + public UnlockForm() + { + InitializeComponent(); + cbTogglePin.Image = (Image)KeePass.Program.Resources.GetObject("B19x07_3BlackDots"); + if (cbTogglePin.Image != null) + { + cbTogglePin.AutoSize = false; + cbTogglePin.Text = string.Empty; + if (KeePass.UI.UIUtil.IsDarkTheme) + cbTogglePin.Image = KeePass.UI.UIUtil.InvertImage(cbTogglePin.Image); + } + + Text = QuickUnlockKeyProv.KeyProviderName; + lLabel.Text = PluginTranslate.UnlockLabel; + bUnlock.Text = PluginTranslate.ButtonUnlock; + bCancel.Text = KeePass.Resources.KPRes.Cancel; + + KeePass.UI.SecureTextBoxEx.InitEx(ref stbPIN); + cbTogglePin.Checked = true; + stbPIN.EnableProtection(cbTogglePin.Checked); + } + + public ProtectedString QuickUnlockKey + { + get { return stbPIN.TextEx; } + } + + private void togglePIN_CheckedChanged(object sender, System.EventArgs e) + { + stbPIN.EnableProtection(cbTogglePin.Checked); + } + } +} diff --git a/src/Utilities/Debug.cs b/src/Utilities/Debug.cs new file mode 100644 index 0000000..498a1d8 --- /dev/null +++ b/src/Utilities/Debug.cs @@ -0,0 +1,420 @@ +using KeePass.Forms; +using KeePass.UI; +using KeePassLib.Utility; +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Reflection; +using System.Windows.Forms; + +namespace PluginTools +{ + public static class PluginDebug + { + [Flags] + public enum LogLevelFlags + { + None = 0, + Info = 1, + Warning = 2, + Error = 4, + Success = 8, + All = Info | Warning | Error | Success + } + + public static string DebugFile { get; private set; } + public static LogLevelFlags LogLevel = LogLevelFlags.All; + + private static bool AutoSave = false; + private static bool AutoOpen = false; + private static bool AskOpen = true; + private static List m_DebugEntries = new List(); + private static string PluginName = string.Empty; + private static string PluginVersion; + private static bool m_DebugMode = false; + public static bool DebugMode + { + get { return m_DebugMode; } + set { m_DebugMode = value; } + } + private static Dictionary m_plugins = new Dictionary(); + public static Version DotNetVersion { get; private set; } + private static int m_DotNetRelease = 0; + + private static DateTime m_Start = DateTime.UtcNow; + + //Init + static PluginDebug() + { + PluginName = Assembly.GetExecutingAssembly().GetName().Name; + PluginVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(); + + ulong uInst = KeePass.Util.WinUtil.GetMaxNetFrameworkVersion(); + DotNetVersion = new Version(StrUtil.VersionToString(uInst)); + try + { + RegistryKey rkRel = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full", false); + m_DotNetRelease = (int)rkRel.GetValue("Release"); + if (rkRel != null) rkRel.Close(); + } + catch { } + + DebugFile = System.IO.Path.GetTempPath() + "Debug_" + PluginName + "_" + m_Start.ToString("yyyyMMddTHHmmssZ") + ".xml"; + + string level = KeePass.Program.CommandLineArgs["debuglevel"]; + if (string.IsNullOrEmpty(level)) + level = LogLevelFlags.All.ToString(); + try + { + LogLevel = (LogLevelFlags)Enum.Parse(LogLevel.GetType(), level); + } + catch { } + AutoSave = KeePass.Program.CommandLineArgs["debugautosave"] != null; + AutoOpen = KeePass.Program.CommandLineArgs["debugautoopen"] != null; + AskOpen = KeePass.Program.CommandLineArgs["debugsaveonly"] == null; + + DebugMode = KeePass.Program.CommandLineArgs[KeePass.App.AppDefs.CommandLineOptions.Debug] != null; + if (!DebugMode) + { + try + { + string[] plugins = KeePass.Program.CommandLineArgs["debugplugin"].ToLowerInvariant().Split(new char[] { ',' }); + DebugMode |= Array.Find(plugins, x => x.Trim() == PluginName.ToLowerInvariant()) != null; + DebugMode |= Array.Find(plugins, x => x.Trim() == "all") != null; + } + catch { } + } + KeePass.Program.MainForm.FormLoadPost += LoadPluginNames; + if (AutoSave) + AddInfo("AutoSave mode active", 0); + } + + #region Handle debug messages + public static void AddInfo(string msg) + { + AddMessage(LogLevelFlags.Info, msg, 5, null); + } + + public static void AddInfo(string msg, params string[] parameters) + { + AddMessage(LogLevelFlags.Info, msg, 5, parameters); + } + + public static void AddInfo(string msg, int CallstackFrames) + { + AddMessage(LogLevelFlags.Info, msg, CallstackFrames, null); + } + + public static void AddInfo(string msg, int CallstackFrames, params string[] parameters) + { + AddMessage(LogLevelFlags.Info, msg, CallstackFrames, parameters); + } + + public static void AddWarning(string msg) + { + AddMessage(LogLevelFlags.Warning, msg, 5, null); + } + + public static void AddWarning(string msg, params string[] parameters) + { + AddMessage(LogLevelFlags.Warning, msg, 5, parameters); + } + + public static void AddWarning(string msg, int CallstackFrames) + { + AddMessage(LogLevelFlags.Warning, msg, CallstackFrames, null); + } + + public static void AddWarning(string msg, int CallstackFrames, params string[] parameters) + { + AddMessage(LogLevelFlags.Warning, msg, CallstackFrames, parameters); + } + + public static void AddError(string msg) + { + AddMessage(LogLevelFlags.Error, msg, 5, null); + } + + public static void AddError(string msg, params string[] parameters) + { + AddMessage(LogLevelFlags.Error, msg, 5, parameters); + } + + public static void AddError(string msg, int CallstackFrames) + { + AddMessage(LogLevelFlags.Error, msg, CallstackFrames, null); + } + + public static void AddError(string msg, int CallstackFrames, params string[] parameters) + { + AddMessage(LogLevelFlags.Error, msg, CallstackFrames, parameters); + } + + public static void AddSuccess(string msg) + { + AddMessage(LogLevelFlags.Success, msg, 5, null); + } + + public static void AddSuccess(string msg, params string[] parameters) + { + AddMessage(LogLevelFlags.Success, msg, 5, parameters); + } + + public static void AddSuccess(string msg, int CallstackFrames) + { + AddMessage(LogLevelFlags.Success, msg, CallstackFrames, null); + } + + public static void AddSuccess(string msg, int CallstackFrames, params string[] parameters) + { + AddMessage(LogLevelFlags.Success, msg, CallstackFrames, parameters); + } + + private static void AddMessage(LogLevelFlags severity, string msg, int CallstackFrames, string[] parameters) + { + if (m_Saving || !DebugMode || ((severity & LogLevel) != severity)) return; + if (m_DebugEntries.Count > 0) + { + DebugEntry prev = m_DebugEntries[m_DebugEntries.Count - 1]; + if ((prev.severity == severity) && (prev.msg == msg) && ParamsEqual(prev.parameters, parameters)) + { + m_DebugEntries[m_DebugEntries.Count - 1].counter++; + return; + } + } + DebugEntry m = new DebugEntry(); + m.severity = severity; + m.msg = msg; + m.utc = DateTime.UtcNow; + m.counter = 1; + m.parameters = parameters; + if (CallstackFrames != 0) + { + System.Diagnostics.StackTrace st = new System.Diagnostics.StackTrace(true); + for (int i = 0; i < st.FrameCount; i++) + { + if (m.sf.Count == CallstackFrames) break; + System.Diagnostics.StackFrame sf = st.GetFrame(i); + if (sf.GetMethod().DeclaringType.FullName != "PluginTools.PluginDebug") + m.sf.Add(sf); + } + } + m_DebugEntries.Add(m); + if (AutoSave) SaveDebugMessages(); + } + + private static bool ParamsEqual(string[] a, string[] b) + { + if ((a == null) && (b == null)) return true; + if ((a == null) && (b != null)) return false; + if ((a != null) && (b == null)) return false; + if (a.Length != b.Length) return false; + for (int i = 0; i < a.Length; i++) + if (a[i] != b[i]) return false; + return true; + } + + public static bool HasMessage(LogLevelFlags severity, string msg) + { + return m_DebugEntries.Find(x => (x.severity == severity) && (x.msg == msg)) != null; + } + #endregion + + public static void SaveOrShow() + { + if (m_DebugEntries.Count == 0) return; + SaveDebugMessages(); + if (AutoOpen || (AskOpen && Tools.AskYesNo("DebugFile: " + DebugFile + "\n\nOpen debug file?") == DialogResult.Yes)) + { + try + { + System.Diagnostics.Process.Start(DebugFile); + } + catch + { + if (KeePassLib.Native.NativeLib.IsUnix()) //The above is broken on mono + { + System.Diagnostics.ProcessStartInfo psi = new System.Diagnostics.ProcessStartInfo(); + psi.Arguments = DebugFile; + psi.FileName = "xdg-open"; + System.Diagnostics.Process.Start(psi); + } + } + } + } + + private static System.Xml.XmlWriter m_xw = null; + private static System.IO.StringWriter m_sw = null; + private static void StartXml() + { + m_sw = new System.IO.StringWriter(); + System.Xml.XmlWriterSettings ws = new System.Xml.XmlWriterSettings(); + ws.OmitXmlDeclaration = true; + ws.Indent = true; + ws.IndentChars = "\t"; + m_xw = System.Xml.XmlWriter.Create(m_sw, ws); + } + + private static string Xml + { + get + { + if (m_xw == null) return string.Empty; + m_sw.Flush(); + m_xw.Flush(); + string s = m_sw.ToString(); + m_xw = null; + m_sw = null; + return s; + } + } + + public static string DebugMessages + { + get + { + StartXml(); + LoadPluginNames(null, null); + string sEncoding = "\n"; + m_xw.WriteStartElement("DebugInfo"); + #region General info + m_xw.WriteStartElement("General"); + m_xw.WriteStartElement("Plugin"); + m_xw.WriteElementString("PluginName", PluginName); + m_xw.WriteElementString("PluginVersion", PluginVersion); + m_xw.WriteEndElement(); + m_xw.WriteStartElement("DebugTime"); + m_xw.WriteElementString("DebugStart", m_Start.ToString("yyyyMMddTHHmmssZ")); + m_xw.WriteElementString("DebugEnd", DateTime.UtcNow.ToString("yyyyMMddTHHmmssZ")); + m_xw.WriteEndElement(); + m_xw.WriteElementString("LogLevel", LogLevel.ToString()); + + #region Add OS info + string os = string.Empty; + if (KeePass.Util.WinUtil.IsWindows9x) + os = "Windows 9x"; + else if (KeePass.Util.WinUtil.IsWindows2000) + os = "Windows 2000"; + else if (KeePass.Util.WinUtil.IsWindowsXP) + os = "Windows XP"; + else + { + if (KeePass.Util.WinUtil.IsAtLeastWindows10) + os = ">= Windows 10"; + else if (KeePass.Util.WinUtil.IsAtLeastWindows8) + os = ">= Windows 8"; + else if (KeePass.Util.WinUtil.IsAtLeastWindows7) + os = ">= Windows 7"; + else if (KeePass.Util.WinUtil.IsAtLeastWindowsVista) + os = ">= Windows Vista"; + else if (KeePass.Util.WinUtil.IsAtLeastWindows2000) + os = ">= Windows 2000"; + else os = "Unknown"; + } + if (KeePass.Util.WinUtil.IsAppX) + os += " (AppX)"; + os += " - " + Environment.OSVersion.ToString(); + m_xw.WriteElementString("OS", KeePass.Util.WinUtil.GetOSStr() + " " + os); + #endregion + m_xw.WriteElementString("DotNet", DotNetVersion.ToString() + (m_DotNetRelease > 0 ? " (" + m_DotNetRelease.ToString() + ")" : string.Empty)); + m_xw.WriteElementString("KeePass", Tools.KeePassVersion.ToString()); + + m_xw.WriteStartElement("LoadedPlugins"); + foreach (KeyValuePair kvp in m_plugins) + { + m_xw.WriteStartElement("Plugin"); + m_xw.WriteElementString("PluginName", kvp.Key); + m_xw.WriteElementString("PluginVersion", kvp.Value.ToString()); + m_xw.WriteEndElement(); + } + m_xw.WriteEndElement(); + m_xw.WriteEndElement(); + #endregion + + if (m_DebugEntries.Count == 0) + m_xw.WriteElementString("DebugMessages", null); + else + { + m_xw.WriteStartElement("DebugMessages"); + foreach (var m in m_DebugEntries) + m.GetXml(m_xw); + m_xw.WriteEndElement(); + } + + m_xw.WriteEndElement(); + return sEncoding + Xml; + } + } + + private static bool m_Saving = false; + public static void SaveDebugMessages() + { + if (m_Saving) return; + m_Saving = true; + try + { + System.IO.File.WriteAllText(DebugFile, DebugMessages); + } + catch (Exception ex) + { + Tools.ShowError("Can't save debug file: " + DebugFile + "\n\n" + ex.Message); + } + m_Saving = false; + } + + private static bool m_bAllPluginsLoaded = false; + private static void LoadPluginNames(object sender, EventArgs e) + { + if (m_bAllPluginsLoaded) return; + m_plugins = Tools.GetLoadedPluginsName(); + if (sender == null) return; + m_bAllPluginsLoaded = true; + KeePass.Program.MainForm.FormLoadPost -= LoadPluginNames; + } + + private class DebugEntry + { + public LogLevelFlags severity; + public string msg; + public DateTime utc; + public int counter; + public List sf = new List(); + public string[] parameters = null; + + public void GetXml(System.Xml.XmlWriter xw) + { + xw.WriteStartElement("DebugEntry"); + xw.WriteElementString("Message", msg); + xw.WriteElementString("Counter", counter.ToString()); + xw.WriteElementString("Severity", severity.ToString()); + xw.WriteElementString("DateTimeUtc", utc.ToString("yyyyMMddTHHmmssZ")); + if ((parameters == null) || parameters.Length == 0) + xw.WriteElementString("Parameters", null); + else + { + xw.WriteStartElement("Parameters"); + foreach (string p in parameters) + xw.WriteElementString("Param", p); + xw.WriteEndElement(); + } + if (sf.Count == 0) + xw.WriteElementString("StackFrames", null); + else + { + xw.WriteStartElement("StackFrames"); + foreach (var f in sf) + { + xw.WriteStartElement("StackFrame"); + xw.WriteElementString("Method", f.GetMethod().Name + " (" + f.GetMethod().DeclaringType.FullName + ")"); + xw.WriteElementString("FileName", System.IO.Path.GetFileName(f.GetFileName())); + xw.WriteElementString("Line", f.GetFileLineNumber().ToString()); + xw.WriteEndElement(); + } + xw.WriteEndElement(); + } + xw.WriteEndElement(); + } + } + } +} \ No newline at end of file diff --git a/src/Utilities/EventHelper.cs b/src/Utilities/EventHelper.cs new file mode 100644 index 0000000..c935249 --- /dev/null +++ b/src/Utilities/EventHelper.cs @@ -0,0 +1,374 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; +using System.Windows.Forms; + +namespace PluginTools +{ + public static class EventHelper + { + public static object DoInvoke(object sender, bool bUnwrapMonoWorkaround, List handlers, params object[] parameters) + { + if (handlers == null) return null; + List lDelegates = bUnwrapMonoWorkaround ? UnwrapMonoWorkaround(sender, handlers) : handlers; + if (lDelegates.Count == 1) + { + return lDelegates[0].DynamicInvoke(parameters) as object; + } + foreach (Delegate d in lDelegates) + d.DynamicInvoke(parameters); + return true; + } + + public static List GetEventHandlers(this object obj, string EventName) + { + List result = new List(); + if (obj == null) return result; + + Type t = obj.GetType(); + List event_fields = GetTypeEventFields(t); + EventHandlerList static_event_handlers = null; + + foreach (FieldInfo fi in event_fields) + { + if (!CheckEvent(fi, EventName)) continue; + + if (fi.IsStatic) + { + + if (static_event_handlers == null) + static_event_handlers = GetStaticEventHandlerList(t, obj); + + object idx = fi.GetValue(obj); + + + Delegate eh = static_event_handlers[idx]; + if (eh == null) + { + var head = GetHead(static_event_handlers); + List lDel = new List(); + CollectEventHandler(head, lDel); + if (lDel.Count == 0) continue; + result.AddRange(lDel); + } + else + { + Delegate[] dels = eh.GetInvocationList(); + if (dels == null) continue; + result.AddRange(dels); + } + } + else + { + EventInfo ei = t.GetEvent(fi.Name, AllBindings); + if (ei == null) + ei = t.GetEvent(EventName, AllBindings); + if (ei == null) + ei = fi.DeclaringType.GetEvent(fi.Name, AllBindings); + if (ei == null) + ei = fi.DeclaringType.GetEvent(EventName, AllBindings); + if (ei != null) + { + object val = fi.GetValue(obj); + Delegate mdel = (val as Delegate); + if (mdel != null) + result.AddRange(mdel.GetInvocationList()); + } + } + } + return result; + } + + public static void RemoveEventHandlers(this object obj, string EventName, List handlers) + { + if (obj == null) return; + if (handlers == null) return; + + Type t = obj.GetType(); + List event_fields = GetTypeEventFields(t); + + foreach (FieldInfo fi in event_fields) + { + if (!CheckEvent(fi, EventName)) continue; + + if (fi.IsStatic) + { + EventInfo ei = t.GetEvent(fi.Name, AllBindings); + if (ei == null) + ei = t.GetEvent(EventName, AllBindings); + if (ei == null) + ei = fi.DeclaringType.GetEvent(fi.Name, AllBindings); + if (ei == null) + ei = fi.DeclaringType.GetEvent(EventName, AllBindings); + if (ei == null) continue; + + foreach (Delegate del in handlers) + ei.RemoveEventHandler(obj, del); + } + else + { + EventInfo ei = t.GetEvent(fi.Name, AllBindings); + if (ei == null) + ei = t.GetEvent(EventName, AllBindings); + if (ei == null) + ei = fi.DeclaringType.GetEvent(fi.Name, AllBindings); + if (ei == null) + ei = fi.DeclaringType.GetEvent(EventName, AllBindings); + if (ei != null) + { + foreach (Delegate del in handlers) + ei.RemoveEventHandler(obj, del); + } + } + } + } + + public static bool AddEventHandlers(this object obj, string EventName, List handlers) + { + if (obj == null) return false; + if (handlers == null) return false; + + Type t = obj.GetType(); + List event_fields = GetTypeEventFields(t); + + bool added = false; + foreach (FieldInfo fi in event_fields) + { + if (!CheckEvent(fi, EventName)) continue; + + if (fi.IsStatic) + { + EventInfo ei = t.GetEvent(fi.Name, AllBindings); + if (ei == null) + ei = t.GetEvent(EventName, AllBindings); + if (ei == null) + ei = fi.DeclaringType.GetEvent(fi.Name, AllBindings); + if (ei == null) + ei = fi.DeclaringType.GetEvent(EventName, AllBindings); + if (ei == null) continue; + + foreach (var del in handlers) + ei.AddEventHandler(obj, del); + added = true; + } + else + { + EventInfo ei = t.GetEvent(fi.Name, AllBindings); + if (ei == null) + ei = t.GetEvent(EventName, AllBindings); + if (ei == null) + ei = fi.DeclaringType.GetEvent(fi.Name, AllBindings); + if (ei == null) + ei = fi.DeclaringType.GetEvent(EventName, AllBindings); + if (ei != null) + { + foreach (var del in handlers) + ei.AddEventHandler(obj, del); + added = true; + } + } + } + return added; + } + + private static Dictionary> m_dicEventFieldInfos = new Dictionary>(); + + private static BindingFlags AllBindings + { + get { return BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; } + } + + private static List GetTypeEventFields(Type t) + { + if (m_dicEventFieldInfos.ContainsKey(t)) return m_dicEventFieldInfos[t]; + + List lst = new List(); + BuildEventFields(t, lst); + m_dicEventFieldInfos[t] = lst; + return lst; + } + + private static void BuildEventFields(Type t, List lst) + { + // Type.GetEvent(s) gets all Events for the type AND it's ancestors + // Type.GetField(s) gets only Fields for the exact type. + // (BindingFlags.FlattenHierarchy only works on PROTECTED & PUBLIC + // doesn't work because Fieds are PRIVATE) + var eil = t.GetEvents(AllBindings); + lst.Clear(); + Dictionary> dBuffer = new Dictionary>(); + foreach (EventInfo ei in eil) + { + Type dt = ei.DeclaringType; + if (!dBuffer.ContainsKey(dt)) + { + dBuffer[dt] = new List(); + dBuffer[dt].AddRange(dt.GetFields(AllBindings)); + } + FieldInfo fi = t.GetField(ei.Name, AllBindings); + if (fi == null) + fi = dt.GetField(ei.Name, AllBindings); + if (fi == null) + fi = t.GetField("Event" + ei.Name, AllBindings); + if (fi == null) + fi = dt.GetField("Event" + ei.Name, AllBindings); + if (fi == null) + fi = t.GetField(ei.Name + "Event", AllBindings); + if (fi == null) + fi = dt.GetField(ei.Name + "Event", AllBindings); + if ((fi == null)) // && (dt.Name == "ListView")) + { + fi = dBuffer[dt].Find(x => x.Name.ToLowerInvariant() == "event_" + ei.Name.ToLowerInvariant()); + if (fi == null) fi = dBuffer[dt].Find(x => x.Name.ToLowerInvariant() == ei.Name.ToLowerInvariant() + "_event"); + } + if (fi != null) + lst.Add(fi); + } + } + + private static EventHandlerList GetStaticEventHandlerList(Type t, object obj) + { + MethodInfo mi = t.GetMethod("get_Events", AllBindings); + while ((mi == null) & (t.BaseType != null)) + { + t = t.BaseType; + mi = t.GetMethod("get_Events", AllBindings); + } + if (mi == null) return null; + return (EventHandlerList)mi.Invoke(obj, new object[] { }); + } + + private static bool CheckEvent(FieldInfo fi, string sEventName) + { + if (string.IsNullOrEmpty(sEventName)) + return false; + if (string.Compare(sEventName, fi.Name, true) == 0) + return true; + if (string.Compare("Event" + sEventName, fi.Name, true) == 0) + return true; + if (string.Compare(sEventName + "Event", fi.Name, true) == 0) + return true; + if (string.Compare("Event_" + sEventName, fi.Name, true) == 0) + return true; + if (string.Compare(sEventName + "_Event", fi.Name, true) == 0) + return true; + return false; + } + + private static void CollectEventHandler(object obj, List lDel) + { + try + { + if (obj == null) return; + FieldInfo fHandler = obj.GetType().GetField("handler", AllBindings); + if (fHandler == null) fHandler = obj.GetType().GetField("_handler", AllBindings); + if (fHandler == null) return; + Delegate d = (Delegate)fHandler.GetValue(obj); + Delegate[] d2 = d.GetInvocationList(); + lDel.AddRange(d2); + } + catch { } + } + + private static object GetHead(EventHandlerList eh) + { + try + { + FieldInfo fHead = eh.GetType().GetField("head", AllBindings); + if (fHead == null) fHead = eh.GetType().GetField("_head", AllBindings); + return fHead.GetValue(eh); + } + catch { } + return null; + } + + private static List UnwrapMonoWorkaround(object sender, List handlers) + { + if (!handlers[0].Method.DeclaringType.Name.Contains("MonoWorkaround")) + { + Tools.ShowInfo("No unwrapping required"); + return handlers; + } + List lHandlers = new List(); + FieldInfo fiHandlers = typeof(KeePassLib.Utility.MonoWorkarounds).GetField("m_dictHandlers", BindingFlags.Static | BindingFlags.NonPublic); + if (fiHandlers == null) + { + Tools.ShowError("No unwrapping possible - fiHandlers null"); + return handlers; + } + var dictHandler = fiHandlers.GetValue(null); + MethodInfo miTryGetValue = dictHandler.GetType().GetMethod("TryGetValue", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + object[] o = new object[] { sender, null }; + miTryGetValue.Invoke(dictHandler, o); + if ((o == null) || (o.Length < 1)) + { + Tools.ShowError("No unwrapping possible - object not found"); + return handlers; + } + Delegate dUnwrapped = Tools.GetField("m_fnOrg", o[1]) as Delegate; + Tools.ShowInfo("Wrapped\n\nOld: " + handlers[0].Method.Name + " - " + handlers[0].Method.DeclaringType.Name + "\n" + dUnwrapped.Method.Name + " - " + dUnwrapped.Method.DeclaringType.Name); + lHandlers.Add(dUnwrapped); + return lHandlers; + } + } + + public class MonoWorkaroundDialogResult : IDisposable + { + private bool m_bRequired = false; + private DialogResult m_dr = DialogResult.None; + + private object m_oMwaHandlerInfo = null; + private FieldInfo m_fiDialogResult = null; + + public MonoWorkaroundDialogResult(object sender) + { + if (sender == null) return; + if (!KeePassLib.Native.NativeLib.IsUnix()) return; + + GetDialogResultObject(sender); + + if (m_oMwaHandlerInfo == null) return; + + SetDialogResult(DialogResult.None); + } + + public void Dispose() + { + if (!m_bRequired) return; + SetDialogResult(m_dr); + } + + private void GetDialogResultObject(object sender) + { + if (sender == null) return; + + FieldInfo fiHandlers = typeof(KeePassLib.Utility.MonoWorkarounds).GetField("m_dictHandlers", BindingFlags.Static | BindingFlags.NonPublic); + object dictHandler = null; + if (fiHandlers != null) dictHandler = fiHandlers.GetValue(null); + + //Do NOT set bRequired, spmething went wrong and we will stick to KeePass standard behaviour + if (dictHandler == null) return; + + MethodInfo miTryGetValue = dictHandler.GetType().GetMethod("TryGetValue", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + //Do NOT set bRequired, spmething went wrong and we will stick to KeePass standard behaviour + if (miTryGetValue == null) return; + + object[] o = new object[] { sender, null }; + miTryGetValue.Invoke(dictHandler, o); + //Do NOT set bRequired, spmething went wrong and we will stick to KeePass standard behaviour + if ((o == null) || (o.Length < 1)) return; + + m_oMwaHandlerInfo = o[1]; + m_fiDialogResult = m_oMwaHandlerInfo.GetType().GetField("m_dr", BindingFlags.Instance | BindingFlags.NonPublic); + + m_bRequired = true; + m_dr = (DialogResult)m_fiDialogResult.GetValue(m_oMwaHandlerInfo); + } + + private void SetDialogResult(DialogResult dr) + { + m_fiDialogResult.SetValue(m_oMwaHandlerInfo, dr); + } + } +} diff --git a/src/Utilities/Tools_Controls.cs b/src/Utilities/Tools_Controls.cs new file mode 100644 index 0000000..8f432c6 --- /dev/null +++ b/src/Utilities/Tools_Controls.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Reflection; +using System.Windows.Forms; + +namespace PluginTools +{ + public static partial class Tools + { + public static object GetField(string field, object obj) + { + BindingFlags bf = BindingFlags.Instance | BindingFlags.NonPublic; + return GetField(field, obj, bf); + } + + public static object GetField(string field, object obj, BindingFlags bf) + { + if (obj == null) return null; + FieldInfo fi = obj.GetType().GetField(field, bf); + if (fi == null) return null; + return fi.GetValue(obj); + } + + public static Control GetControl(string control) + { + return GetControl(control, KeePass.Program.MainForm); + } + + public static Control GetControl(string control, Control form) + { + if (form == null) return null; + if (string.IsNullOrEmpty(control)) return null; + Control[] cntrls = form.Controls.Find(control, true); + if (cntrls.Length == 0) return null; + return cntrls[0]; + } + + public static ToolStripMenuItem FindToolStripMenuItem(ToolStripItemCollection tsic, string key, bool searchAllChildren) + { + if (tsic == null) return null; + ToolStripItem[] tsi = FindToolStripMenuItems(tsic, key, searchAllChildren); + if (tsi.Length > 0) return tsi[0] as ToolStripMenuItem; + return null; + } + + public static ToolStripItem[] FindToolStripMenuItems(ToolStripItemCollection tsic, string key, bool searchAllChildren) + { + if (tsic == null) return new ToolStripItem[] { }; + ToolStripItem[] tsi = tsic.Find(key, searchAllChildren); + if (!MonoWorkaroundRequired || !searchAllChildren) return tsi; + + //Mono does not support 'searchAllChildren' for ToolStripItemCollection + //Iterate over all items and search for given item + List lItems = new List(tsi); + foreach (var item in tsic) + { + ToolStripMenuItem tsmi = item as ToolStripMenuItem; + if (tsmi == null) continue; + lItems.AddRange(FindToolStripMenuItems(tsmi.DropDownItems, key, searchAllChildren)); + } + return lItems.ToArray(); + } + } +} \ No newline at end of file diff --git a/src/Utilities/Tools_Main.cs b/src/Utilities/Tools_Main.cs new file mode 100644 index 0000000..67c123a --- /dev/null +++ b/src/Utilities/Tools_Main.cs @@ -0,0 +1,93 @@ +using KeePass.UI; +using System; +using System.Windows.Forms; + +namespace PluginTools +{ + public static partial class Tools + { + public static Version KeePassVersion { get; private set; } + public static string DefaultCaption = string.Empty; + public static string PluginURL = string.Empty; + public static string KeePassLanguageIso6391 { get; private set; } + + private static bool MonoWorkaroundRequired = KeePassLib.Native.NativeLib.IsUnix(); + + static Tools() + { + KeePass.UI.GlobalWindowManager.WindowAdded += OnWindowAdded; + KeePass.UI.GlobalWindowManager.WindowRemoved += OnWindowRemoved; + KeePassVersion = typeof(KeePass.Program).Assembly.GetName().Version; + KeePassLanguageIso6391 = KeePass.Program.Translation.Properties.Iso6391Code; + if (string.IsNullOrEmpty(KeePassLanguageIso6391)) KeePassLanguageIso6391 = "en"; + m_sPluginClassname = typeof(Tools).Assembly.GetName().Name + "Ext"; + } + + public static void OpenUrl(string sURL) + { + OpenUrl(sURL, null); + } + + public static void OpenUrl(string sURL, KeePassLib.PwEntry pe) + { + //Use KeePass built-in logic instead of System.Diagnostics.Process.Start + //For details see: https://sourceforge.net/p/keepass/discussion/329221/thread/f399b6d74b/#4801 + KeePass.Util.WinUtil.OpenUrl(sURL, pe, true); + } + + #region MessageBox shortcuts + public static DialogResult ShowError(string msg) + { + return ShowError(msg, DefaultCaption); + } + + public static DialogResult ShowInfo(string msg) + { + return ShowInfo(msg, DefaultCaption); + } + + public static DialogResult AskYesNo(string msg) + { + return AskYesNo(msg, DefaultCaption); + } + + public static DialogResult ShowError(string msg, string caption) + { + PluginDebug.AddError("Show error", 6, caption, msg); + return MessageBox.Show(msg, caption, MessageBoxButtons.OK, MessageBoxIcon.Error); + } + + public static DialogResult ShowInfo(string msg, string caption) + { + PluginDebug.AddInfo("Show info", 6, caption, msg); + return MessageBox.Show(msg, caption, MessageBoxButtons.OK, MessageBoxIcon.Information); + } + + public static DialogResult AskYesNo(string msg, string caption) + { + DialogResult result = MessageBox.Show(msg, caption, MessageBoxButtons.YesNo, MessageBoxIcon.Question); + PluginDebug.AddInfo("Ask question", 6, caption, msg, "Result: " + result.ToString()); + return result; + } + #endregion + + #region GlobalWindowManager + public static void GlobalWindowManager(Form form) + { + if ((form == null) || (form.IsDisposed)) return; + form.Load += FormLoaded; + form.FormClosed += FormClosed; + } + + private static void FormLoaded(object sender, EventArgs e) + { + KeePass.UI.GlobalWindowManager.AddWindow(sender as Form, sender as IGwmWindow); + } + + private static void FormClosed(object sender, FormClosedEventArgs e) + { + KeePass.UI.GlobalWindowManager.RemoveWindow(sender as Form); + } + #endregion + } +} \ No newline at end of file diff --git a/src/Utilities/Tools_Options.cs b/src/Utilities/Tools_Options.cs new file mode 100644 index 0000000..51eb9fe --- /dev/null +++ b/src/Utilities/Tools_Options.cs @@ -0,0 +1,375 @@ +using KeePass.Forms; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Reflection; +using System.Windows.Forms; + +namespace PluginTools +{ + public static partial class Tools + { + public static object GetPluginInstance(string PluginName) + { + string comp = PluginName + "." + PluginName + "Ext"; + BindingFlags bf = BindingFlags.Instance | BindingFlags.NonPublic; + try + { + var PluginManager = GetField("m_pluginManager", KeePass.Program.MainForm); + var PluginList = GetField("m_vPlugins", PluginManager); + MethodInfo IteratorMethod = PluginList.GetType().GetMethod("System.Collections.Generic.IEnumerable.GetEnumerator", bf); + IEnumerator PluginIterator = (IEnumerator)(IteratorMethod.Invoke(PluginList, null)); + while (PluginIterator.MoveNext()) + { + object result = GetField("m_pluginInterface", PluginIterator.Current); + if (comp == result.GetType().ToString()) return result; + } + } + + catch (Exception) { } + return null; + } + + public static Dictionary GetLoadedPluginsName() + { + Dictionary dPlugins = new Dictionary(); + BindingFlags bf = BindingFlags.Instance | BindingFlags.NonPublic; + try + { + var PluginManager = GetField("m_pluginManager", KeePass.Program.MainForm); + var PluginList = GetField("m_vPlugins", PluginManager); + MethodInfo IteratorMethod = PluginList.GetType().GetMethod("System.Collections.Generic.IEnumerable.GetEnumerator", bf); + IEnumerator PluginIterator = (IEnumerator)(IteratorMethod.Invoke(PluginList, null)); + while (PluginIterator.MoveNext()) + { + object result = GetField("m_pluginInterface", PluginIterator.Current); + var x = result.GetType().Assembly; + object[] v = x.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), true); + Version ver = null; + if ((v != null) && (v.Length > 0)) + ver = new Version(((AssemblyFileVersionAttribute)v[0]).Version); + else + ver = result.GetType().Assembly.GetName().Version; + if (ver.Build < 0) ver = new Version(ver.Major, ver.Minor, 0, 0); + if (ver.Revision < 0) ver = new Version(ver.Major, ver.Minor, ver.Build, 0); + dPlugins[result.GetType().FullName] = ver; + } + } + catch (Exception) { } + return dPlugins; + } + + public static event EventHandler OptionsFormShown; + public static event EventHandler OptionsFormClosed; + + private static bool OptionsEnabled = (KeePass.Program.Config.UI.UIFlags & (ulong)KeePass.App.Configuration.AceUIFlags.DisableOptions) != (ulong)KeePass.App.Configuration.AceUIFlags.DisableOptions; + private static bool m_ActivatePluginTab = false; + private static string m_sPluginClassname = string.Empty; + private static OptionsForm m_of = null; + private const string c_tabRookiestyle = "m_tabRookiestyle"; + private const string c_tabControlRookiestyle = "m_tabControlRookiestyle"; + private static string m_TabPageName = string.Empty; + private static bool m_OptionsShown = false; + private static bool m_PluginContainerShown = false; + + public static void AddPluginToOptionsForm(KeePass.Plugins.Plugin p, UserControl uc) + { + m_OptionsShown = m_PluginContainerShown = false; + TabPage tPlugin = new TabPage(DefaultCaption); + tPlugin.CreateControl(); + tPlugin.Name = m_TabPageName = c_tabRookiestyle + p.GetType().Name; + uc.Dock = DockStyle.Fill; + uc.Padding = new Padding(15, 10, 15, 10); + tPlugin.Controls.Add(uc); + PluginDebug.AddInfo("Adding/Searching " + c_tabControlRookiestyle); + TabControl tcPlugins = AddPluginTabContainer(); + int i = 0; + bool insert = false; + for (int j = 0; j < tcPlugins.TabPages.Count; j++) + { + if (string.Compare(tPlugin.Text, tcPlugins.TabPages[j].Text, StringComparison.CurrentCultureIgnoreCase) < 0) + { + i = j; + insert = true; + break; + } + } + if (!insert) + { + i = tcPlugins.TabPages.Count; + PluginDebug.AddInfo(p.GetType().Name + " tab index : " + i.ToString() + " - insert!", 0); + } + else PluginDebug.AddInfo(p.GetType().Name + " tab index : " + i.ToString(), 0); + tcPlugins.TabPages.Insert(i, tPlugin); + AddPluginToOverview(tPlugin.Name.Replace(c_tabRookiestyle, string.Empty), tcPlugins); + if (p.SmallIcon != null) + { + tcPlugins.ImageList.Images.Add(tPlugin.Name, p.SmallIcon); + tPlugin.ImageKey = tPlugin.Name; + } + TabControl tcMain = Tools.GetControl("m_tabMain", m_of) as TabControl; + if (!string.IsNullOrEmpty(PluginURL)) AddPluginLink(uc); + } + + public static void AddPluginToOverview(string sPluginName) + { + AddPluginToOverview(sPluginName, null); + } + + private static void AddPluginToOverview(string sPluginName, TabControl tcPlugins) + { + if (tcPlugins == null) tcPlugins = AddPluginTabContainer(); + TabPage tpOverview = null; + ListView lv = null; + string sTabName = c_tabRookiestyle + "_PluginOverview"; + string sListViewName = c_tabRookiestyle + "_PluginOverviewListView"; + if (tcPlugins.TabPages.ContainsKey(sTabName)) + { + tpOverview = tcPlugins.TabPages[sTabName]; + lv = (ListView)tpOverview.Controls.Find(sListViewName, true)[0]; + PluginDebug.AddInfo("Found " + sTabName, 0, "Listview: " + (lv == null ? "null" : lv.Items.Count.ToString() + " /" + lv.Name.ToString())); + } + else + { + tpOverview = new TabPage("Overview"); + tpOverview.CreateControl(); + tpOverview.Name = sTabName; + UserControl uc = new UserControl(); + uc.Dock = DockStyle.Fill; + uc.Padding = new Padding(15, 10, 15, 10); + tpOverview.Controls.Add(uc); + lv = new ListView(); + lv.Name = sListViewName; + lv.Dock = DockStyle.Fill; + lv.View = View.Details; + lv.Columns.Add("Plugin"); + lv.Columns.Add("Version"); + lv.CheckBoxes = true; + tpOverview.Layout += TpOverview_Layout; + Label lInfo = new Label(); + lInfo.AutoSize = true; + lInfo.Text = "Use the checkbox to activate/deactivate debug mode"; + lInfo.Dock = DockStyle.Bottom; + uc.Controls.Add(lv); + uc.Controls.Add(lInfo); + } + lv.ItemCheck += Lv_ItemCheck; + lv.Sorting = SortOrder.Ascending; + lv.FullRowSelect = true; + ListViewItem lvi = new ListViewItem(); + lvi.Name = sPluginName; + lvi.Checked = PluginDebug.DebugMode; + lvi.Text = DefaultCaption; + Version v = new Version(0, 0); + GetLoadedPluginsName().TryGetValue(sPluginName.Replace("Ext", string.Empty) + "." + sPluginName, out v); + if (v == null) PluginDebug.AddError("Could not get loaded plugins' data", 0); + string ver = (v == null) ? "???" : v.ToString(); + if (ver.EndsWith(".0")) ver = ver.Substring(0, ver.Length - 2); + else ver += " (Dev)"; + lvi.SubItems.Add(ver); + lv.Items.Add(lvi); + tcPlugins.TabPages.Remove(tpOverview); + tcPlugins.TabPages.Add(tpOverview); + PluginDebug.AddInfo("Added " + sTabName, 0, "Listview: " + (lv == null ? "null" : lv.Items.Count.ToString() + " /" + lv.Name.ToString())); + } + + private static void TpOverview_Layout(object sender, LayoutEventArgs e) + { + string sListViewName = c_tabRookiestyle + "_PluginOverviewListView"; + ListView lv = (sender as TabPage).Controls.Find(sListViewName, true)[0] as ListView; + lv.BeginUpdate(); + lv.Columns[1].DisplayIndex = 0; + lv.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent); + int w = lv.Columns[1].Width; + lv.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize); + lv.Columns[1].Width = Math.Max(w, lv.Columns[1].Width); + lv.Columns[0].Width = lv.ClientSize.Width - lv.Columns[1].Width; + if (lv.Columns[0].Width < 150) + { + lv.Columns[1].Width = 100; + lv.Columns[0].Width = lv.ClientSize.Width - lv.Columns[1].Width; + } + lv.Columns[1].DisplayIndex = 1; + lv.EndUpdate(); + } + + private static void Lv_ItemCheck(object sender, ItemCheckEventArgs e) + { + ListViewItem lvi = (sender as ListView).Items[e.Index]; + if (lvi == null) return; + if (lvi.Text != DefaultCaption) return; + PluginDebug.DebugMode = e.NewValue == CheckState.Checked; + } + + private static void OnPluginTabsSelected(object sender, TabControlEventArgs e) + { + m_OptionsShown |= (e.TabPage.Name == m_TabPageName); + m_PluginContainerShown |= (m_OptionsShown || (e.TabPage.Name == c_tabRookiestyle)); + } + + public static UserControl GetPluginFromOptions(KeePass.Plugins.Plugin p, out bool PluginOptionsShown) + { + PluginOptionsShown = m_OptionsShown && m_PluginContainerShown; + TabPage tPlugin = Tools.GetControl(c_tabRookiestyle + p.GetType().Name, m_of) as TabPage; + if (tPlugin == null) return null; + return tPlugin.Controls[0] as UserControl; + } + + public static void ShowOptions() + { + m_ActivatePluginTab = true; + if (OptionsEnabled) + KeePass.Program.MainForm.ToolsMenu.DropDownItems["m_menuToolsOptions"].PerformClick(); + else + { + m_of = new OptionsForm(); + m_of.InitEx(KeePass.Program.MainForm.ClientIcons); + m_of.ShowDialog(); + } + } + + private static void AddPluginLink(UserControl uc) + { + LinkLabel llUrl = new LinkLabel(); + llUrl.Links.Add(0, PluginURL.Length, PluginURL); + llUrl.Text = PluginURL; + uc.Controls.Add(llUrl); + llUrl.Dock = DockStyle.Bottom; + llUrl.AutoSize = true; + llUrl.LinkClicked += new LinkLabelLinkClickedEventHandler(PluginURLClicked); + } + + private static void PluginURLClicked(object sender, LinkLabelLinkClickedEventArgs e) + { + string target = e.Link.LinkData as string; + OpenUrl(target); + } + + private static void OnOptionsFormShown(object sender, EventArgs e) + { + m_of.Shown -= OnOptionsFormShown; + TabControl tcMain = Tools.GetControl("m_tabMain", m_of) as TabControl; + if (!tcMain.TabPages.ContainsKey(c_tabRookiestyle)) return; + TabPage tPlugins = tcMain.TabPages[c_tabRookiestyle]; + TabControl tcPlugins = Tools.GetControl(c_tabControlRookiestyle, tPlugins) as TabControl; + if (m_ActivatePluginTab) + { + tcMain.SelectedIndex = tcMain.TabPages.IndexOfKey(c_tabRookiestyle); + KeePass.Program.Config.Defaults.OptionsTabIndex = (uint)tcMain.SelectedIndex; + tcPlugins.SelectedIndex = tcPlugins.TabPages.IndexOfKey(c_tabRookiestyle + m_sPluginClassname); + } + m_ActivatePluginTab = false; + tcMain.Selected += OnPluginTabsSelected; + tcPlugins.Selected += OnPluginTabsSelected; + tcMain.ImageList.Images.Add(c_tabRookiestyle + "Icon", (Image)KeePass.Program.Resources.GetObject("B16x16_BlockDevice")); + tPlugins.ImageKey = c_tabRookiestyle + "Icon"; + m_PluginContainerShown |= tcMain.SelectedTab == tPlugins; + m_OptionsShown |= (tcPlugins.SelectedTab.Name == m_TabPageName); + CheckKeeTheme(tPlugins); + } + + private static void CheckKeeTheme(Control c) + { + Control check = GetControl("Rookiestyle_KeeTheme_Check", m_of); + if (check != null) return; + PluginDebug.AddInfo("Checking for KeeTheme"); + check = new Control(); + check.Name = "Rookiestyle_KeeTheme_Check"; + check.Visible = false; + m_of.Controls.Add(check); + KeePass.Plugins.Plugin p = (KeePass.Plugins.Plugin)GetPluginInstance("KeeTheme"); + if (p == null) return; + var t = GetField("_theme", p); + if (t == null) return; + bool bKeeThemeEnabled = (bool)t.GetType().GetProperty("Enabled").GetValue(t, null); + if (!bKeeThemeEnabled) return; + var v = GetField("_controlVisitor", p); + if (v == null) return; + MethodInfo miVisit = v.GetType().GetMethod("Visit", new Type[] { typeof(Control) }); + if (miVisit == null) return; + miVisit.Invoke(v, new object[] { c }); + } + + private static void OnWindowAdded(object sender, KeePass.UI.GwmWindowEventArgs e) + { + if (OptionsFormShown == null) return; + if (e.Form is OptionsForm) + { + m_of = e.Form as OptionsForm; + m_of.Shown += OnOptionsFormShown; + OptionsFormsEventArgs o = new OptionsFormsEventArgs(m_of); + OptionsFormShown(sender, o); + } + } + + private static void OnWindowRemoved(object sender, KeePass.UI.GwmWindowEventArgs e) + { + if (OptionsFormClosed == null) return; + if (e.Form is OptionsForm) + { + OptionsFormsEventArgs o = new OptionsFormsEventArgs(m_of); + OptionsFormClosed(sender, o); + } + } + + private static TabControl AddPluginTabContainer() + { + if (m_of == null) + { + PluginDebug.AddError("Could not identify KeePass options form", 0); + return null; + } + TabControl tcMain = Tools.GetControl("m_tabMain", m_of) as TabControl; + if (tcMain == null) + { + PluginDebug.AddError("Could not locate m_tabMain", 0); + return null; + } + TabPage tPlugins = null; + TabControl tcPlugins = null; + if (tcMain.TabPages.ContainsKey(c_tabRookiestyle)) + { + tPlugins = tcMain.TabPages[c_tabRookiestyle]; + tcPlugins = (TabControl)tPlugins.Controls[c_tabControlRookiestyle]; + if (tcPlugins == null) + { + PluginDebug.AddError("Could not locate " + c_tabControlRookiestyle, 0); + return null; + } + tcPlugins.Multiline = false; //Older version of PluginTools might still be used by other plugins + PluginDebug.AddInfo("Found " + c_tabControlRookiestyle, 0); + return tcPlugins; + } + tPlugins = new TabPage(KeePass.Resources.KPRes.Plugin + " " + m_of.Text); + tPlugins.Name = c_tabRookiestyle; + tPlugins.CreateControl(); + if (!OptionsEnabled) + { + while (tcMain.TabCount > 0) + tcMain.TabPages.RemoveAt(0); + PluginDebug.AddInfo("Removed tab pages from KeePass options form", 0); + } + tcMain.TabPages.Add(tPlugins); + tcPlugins = new TabControl(); + tcPlugins.Name = c_tabControlRookiestyle; + tcPlugins.Dock = DockStyle.Fill; + tcPlugins.Multiline = false; + tcPlugins.CreateControl(); + if (tcPlugins.ImageList == null) + tcPlugins.ImageList = new ImageList(); + tPlugins.Controls.Add(tcPlugins); + PluginDebug.AddInfo("Added " + c_tabControlRookiestyle, 0); + return tcPlugins; + } + + public class OptionsFormsEventArgs : EventArgs + { + public Form form; + + public OptionsFormsEventArgs(Form form) + { + this.form = form; + } + } + } +} \ No newline at end of file diff --git a/translationcopy.cmd b/translationcopy.cmd new file mode 100644 index 0000000..e7fcba3 --- /dev/null +++ b/translationcopy.cmd @@ -0,0 +1,10 @@ +@echo off +cd %~dp0 + +if "%1" == "Debug" ( + xcopy /y /c /i Translations\*.xml "..\_KeePass_Debug\Plugins\Translations" /EXCLUDE:..\translationsnocopy.txt +) +if "%1" == "ReleasePlgx" ( + xcopy /y /c /i Translations\*.xml "..\_KeePass_Release\Plugins\Translations" /EXCLUDE:..\translationsnocopy.txt + xcopy /y /c /i Translations\*.xml "..\_Releases\Translations" /EXCLUDE:..\translationsnocopy.txt +) \ No newline at end of file diff --git a/version.info b/version.info new file mode 100644 index 0000000..fa4d989 --- /dev/null +++ b/version.info @@ -0,0 +1,4 @@ +: +LockAssist:1.0 +LockAssist!de:1 +: \ No newline at end of file