Skip to content

Commit e9fb677

Browse files
authored
Custom wordlist (#34)
Truly, @SecurityCze has gone above and beyond by implementing what has to be the most desired feature request! This closes #1.
1 parent a54a0f6 commit e9fb677

13 files changed

Lines changed: 1421 additions & 492 deletions

Diceware.cs

Lines changed: 5 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ You should have received a copy of the GNU Affero General Public License
2121
using System;
2222
using System.Collections.Generic;
2323
using System.Diagnostics;
24-
using System.IO;
2524
using System.Linq;
26-
using System.Reflection;
2725
using System.Text;
2826

2927
using KeePassLib.Cryptography;
@@ -33,9 +31,6 @@ namespace KeePassDiceware
3331
{
3432
public class Diceware
3533
{
36-
37-
private const string WordListFileExtension = ".txt";
38-
3934
private static readonly char[] LookalikeCharacters = "I|1lO0".ToCharArray();
4035

4136
// Copyright (C) 2014-2021 Mark McGuill. All rights reserved.
@@ -84,8 +79,6 @@ public class Diceware
8479
{ 'Z', "2" },
8580
};
8681

87-
private static readonly Dictionary<WordLists, string[]> LoadedLists = new();
88-
8982
public static string Generate(Options options, PwProfile profile, CryptoRandomStream random)
9083
{
9184
Debug.Assert(options != null);
@@ -355,58 +348,16 @@ select saltOptions.SelectRandom(random))
355348
return result?.ToString() ?? string.Empty;
356349
}
357350

358-
public static IEnumerable<string> GetWordList(WordLists lists)
359-
{
360-
var selectedWordlists = new List<string[]>();
361-
362-
foreach (WordLists list in lists.GetFlags())
363-
{
364-
// none isn't a real list.
365-
if (list == WordLists.None)
366-
{
367-
continue;
368-
}
369-
370-
if (LoadedLists.ContainsKey(list))
371-
{
372-
selectedWordlists.Add(LoadedLists[list]);
373-
}
374-
else
375-
{
376-
string[] loadedList = ReadEmbeddedResource($"{list}{WordListFileExtension}").ToArray();
377-
LoadedLists.Add(list, loadedList);
378-
379-
// cache it for re-use.
380-
selectedWordlists.Add(loadedList);
381-
}
382-
}
383-
384-
return selectedWordlists.SelectMany(s => s);
385-
}
386-
387-
// via: https://stackoverflow.com/a/3314213/944605
388-
public static IEnumerable<string> ReadEmbeddedResource(string resourceName, Encoding encoding = null)
351+
public static IEnumerable<string> GetWordList(List<WordList> lists)
389352
{
390-
encoding ??= Encoding.UTF8;
391-
392-
var assembly = Assembly.GetAssembly(typeof(Diceware));
393-
string resourcePath = resourceName;
353+
HashSet<string> joinedWordList = new HashSet<string>();
394354

395-
// Format: "{Namespace}.{Folder}.{Filename}.{Extension}"
396-
if (!resourceName.StartsWith(nameof(KeePassDiceware)))
355+
foreach (WordList list in lists.Where(wl => wl.Enabled))
397356
{
398-
string[] manifestResourceNames = assembly.GetManifestResourceNames();
399-
resourcePath = manifestResourceNames.Single(str => str.EndsWith(resourceName));
357+
joinedWordList.UnionWith(list.Get());
400358
}
401359

402-
using Stream stream = assembly.GetManifestResourceStream(resourcePath);
403-
using var reader = new StreamReader(stream, encoding);
404-
string line;
405-
406-
while ((line = reader.ReadLine()) != null)
407-
{
408-
yield return line;
409-
}
360+
return joinedWordList;
410361
}
411362
}
412363
}

DicewareOptionsForm.Designer.cs

Lines changed: 377 additions & 310 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

DicewareOptionsForm.cs

Lines changed: 57 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ public Options Options
2222

2323
private List<SaltSource> _saltSources = new();
2424

25+
private List<WordList> _wordLists = new();
26+
private readonly ListViewGroup[] _wordListCategories = Enum.GetNames(typeof(WordList.CategoryEnum))
27+
.Select(wl => new ListViewGroup(wl, wl)).ToArray();
28+
29+
2530
public DicewareOptionsForm()
2631
{
2732
InitializeComponent();
@@ -49,24 +54,11 @@ private void InitializeEnumOptions()
4954
saltComboBox.SelectedIndex = 0;
5055

5156
_saltSources.Clear();
52-
activeSaltSourcesLabel.Text = string.Empty;
53-
54-
wordListsListView.Columns.Add("Word List");
55-
56-
ListViewGroup[] wordListCategories = EnumTools.GetCategories<WordLists>()
57-
.Select(c => new ListViewGroup(c, c))
58-
.ToArray();
57+
activeSaltSourcesListView.Columns.Add("Salt sources");
5958

60-
wordListsListView.Groups.AddRange(wordListCategories);
61-
62-
ListViewItem[] wordLists = EnumTools.GetDisplays<WordLists>()
63-
.Select(v =>
64-
new ListViewItem($"{v}", wordListCategories.First(c => c.Name == v.Category)))
65-
.ToArray();
66-
67-
wordListsListView.Items.AddRange(wordLists);
68-
69-
wordListsListView.Columns[0].AutoResize(ColumnHeaderAutoResizeStyle.ColumnContent);
59+
_wordLists.Clear();
60+
activeWordListsListView.Columns.Add("Word List");
61+
activeWordListsListView.Groups.AddRange(_wordListCategories);
7062
}
7163

7264
protected override void OnLoad(EventArgs e)
@@ -90,31 +82,40 @@ private void PopulateInterface()
9082
l33tSpeakComboBox.SelectedIndex = l33tSpeakComboBox.FindStringExact(Options.L33tSpeak.GetDescription());
9183
saltComboBox.SelectedIndex = saltComboBox.FindStringExact(Options.Salt.GetDescription());
9284
_saltSources = new(Options.SaltSources);
93-
UpdateSaltSourcesLabel();
94-
UpdateListView(wordListsListView, Options.WordLists);
85+
_wordLists = new(Options.WordLists);
86+
UpdateSaltSourcesListView();
87+
UpdateWordListsListView();
9588
}
9689

97-
private void UpdateSaltSourcesLabel()
90+
private void UpdateSaltSourcesListView()
9891
{
99-
IEnumerable<string> sources = _saltSources.Where(ss => ss.Enabled)
92+
string[] sources = _saltSources.Where(ss => ss.Enabled)
10093
.Select(ss => $"{ss.Name} "
10194
+ (ss.MinimumAmount == ss.MaximumAmount
10295
? $"({ss.MinimumAmount})"
10396
: $"({ss.MinimumAmount}-{ss.MaximumAmount})")
104-
);
97+
).ToArray();
10598

106-
activeSaltSourcesLabel.Text = string.Join(", ", sources);
99+
activeSaltSourcesListView.Items.Clear();
100+
foreach (string rowString in sources)
101+
{
102+
ListViewItem rowItem = new ListViewItem(rowString);
103+
activeSaltSourcesListView.Items.Add(rowItem);
104+
}
105+
activeSaltSourcesListView.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent);
107106
}
108107

109-
private void UpdateListView<T>(ListView view, T flags) where T : Enum
108+
private void UpdateWordListsListView()
110109
{
111-
foreach (ListViewItem i in view.Items)
112-
{
113-
T flag = EnumTools.FromDescription<T>(i.Text);
114-
i.Checked = flags.HasFlag(flag);
115-
}
110+
ListViewItem[] lists = _wordLists.Where(wl => wl.Enabled)
111+
.Select(wl => new ListViewItem(wl.Name,
112+
_wordListCategories.First(g => g.Name == Enum.GetName(typeof(WordList.CategoryEnum), wl.Category))))
113+
.ToArray();
114+
115+
activeWordListsListView.Items.Clear();
116+
activeWordListsListView.Items.AddRange(lists);
116117

117-
view.Refresh();
118+
activeWordListsListView.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent);
118119
}
119120

120121
private Options GetOptions()
@@ -128,32 +129,15 @@ private Options GetOptions()
128129
AdvancedStrategy = EnumTools.FromDescription<AdvancedStrategy>(advancedStrategyComboBox.SelectedItem.ToString()),
129130
Salt = EnumTools.FromDescription<SaltType>(saltComboBox.SelectedItem.ToString()),
130131
SaltSources = new(_saltSources),
131-
WordLists = GetListViewFlags<WordLists>(wordListsListView)
132+
WordLists = new(_wordLists)
132133
};
133134
}
134135

135-
private T GetListViewFlags<T>(ListView view) where T : Enum
136-
{
137-
int result = 0;
138-
foreach (ListViewItem i in view.Items)
139-
{
140-
T flagEnum = EnumTools.FromDescription<T>(i.Text);
141-
int flag = (int)Convert.ChangeType(flagEnum, typeof(int));
142-
143-
if (i.Checked)
144-
{
145-
result |= flag;
146-
}
147-
}
148-
149-
return (T)Enum.ToObject(typeof(T), result);
150-
}
151-
152136
private void OkButton_Click(object sender, System.EventArgs e)
153137
{
154138
Options = GetOptions();
155139

156-
if (Options.WordLists == WordLists.None)
140+
if (Options.WordLists.Count() == 0)
157141
{
158142
MessageBox.Show(this, $"No word lists were selected. This will prevent the plugin from generating passwords. Please select at least one list.", "Options Validation Failure", MessageBoxButtons.OK, MessageBoxIcon.Error);
159143
DialogResult = DialogResult.Cancel;
@@ -196,7 +180,30 @@ private void EditSaltSourcesButton_Click(object sender, EventArgs e)
196180
}
197181

198182
_saltSources = ssf.Result;
199-
UpdateSaltSourcesLabel();
183+
UpdateSaltSourcesListView();
184+
}
185+
186+
private void EditWordListsButton_Click(object sender, EventArgs e)
187+
{
188+
WordListsForm wlf = new(_wordLists);
189+
190+
if (wlf.ShowDialog(this) == DialogResult.Cancel)
191+
{
192+
return;
193+
}
194+
195+
_wordLists = wlf.Result;
196+
UpdateWordListsListView();
197+
}
198+
199+
private void ActiveSaltSourcesListView_SelectedIndexChanged(object sender, EventArgs e)
200+
{
201+
((ListView)sender).SelectedItems.Clear();
202+
}
203+
204+
private void WordListsListView_SelectedIndexChanged(object sender, EventArgs e)
205+
{
206+
((ListView)sender).SelectedItems.Clear();
200207
}
201208
}
202209
}

DicewareOptionsForm.resx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,4 @@
117117
<resheader name="writer">
118118
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
119119
</resheader>
120-
<data name="activeSaltSourcesLabel.Text" xml:space="preserve">
121-
<value>&lt;active salts&gt; &lt;active salts&gt; &lt;active salts&gt; &lt;active salts&gt; &lt;active salts&gt; &lt;active salts&gt; &lt;active salts&gt; &lt;active salts&gt; &lt;active salts&gt; &lt;active salts&gt; &lt;active salts&gt; &lt;active salts&gt; &lt;active salts&gt; &lt;active salts&gt; &lt;active salts&gt; &lt;active salts&gt;
122-
</value>
123-
</data>
124120
</root>

Extensions.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ internal static class Extensions
1010
/// <summary>
1111
/// Generates a pseudorandom value in range [0, <paramref name="maxInclusive"/>] (both ends are inclusive).
1212
/// </summary>
13-
/// <param name="maxInclusive"> maximal value (inclusive) of random number.</param>
13+
/// <param name="maxInclusive">maximal value (inclusive) of random number.</param>
1414
/// <returns>
1515
/// Pseudorandom value from [0, <paramref name="maxInclusive"/>] range.
1616
/// </returns>
@@ -37,7 +37,7 @@ public static ulong AtMost(this CryptoRandomStream random, ulong maxInclusive)
3737
/// <summary>
3838
/// Generates a pseudorandom value in range [0, <paramref name="maxInclusive"/>] (both ends are inclusive).
3939
/// </summary>
40-
/// <param name="maxInclusive"> maximal value (inclusive) of random number.</param>
40+
/// <param name="maxInclusive">maximal value (inclusive) of random number.</param>
4141
/// <returns>
4242
/// Pseudorandom value from [0, <paramref name="maxInclusive"/>] range.
4343
/// </returns>
@@ -56,8 +56,8 @@ public static int AtMost(this CryptoRandomStream random, int maxInclusive)
5656
/// <summary>
5757
/// Generates a pseudorandom value in range [<paramref name="minInclusive"/>, <paramref name="maxInclusive"/>] (both ends are inclusive).
5858
/// </summary>
59-
/// <param name="minInclusive"> minimal value (inclusive) of random number.</param>
60-
/// <param name="maxInclusive"> maximal value (inclusive) of random number.</param>
59+
/// <param name="minInclusive">minimal value (inclusive) of random number.</param>
60+
/// <param name="maxInclusive">maximal value (inclusive) of random number.</param>
6161
/// <returns>
6262
/// Pseudorandom value from [<paramref name="minInclusive"/>, <paramref name="maxInclusive"/>] range.
6363
/// </returns>
@@ -79,7 +79,7 @@ public static int Range(this CryptoRandomStream random, int minInclusive, int ma
7979
/// <summary>
8080
/// Selects a pseudorandom element from <paramref name="array"/>.
8181
/// </summary>
82-
/// <param name="array"> array from which to select the random element.</param>
82+
/// <param name="array">array from which to select the random element.</param>
8383
/// <returns>
8484
/// A pseudorandom element from <paramref name="array"/>.
8585
/// </returns>

Options.cs

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,7 @@ public class Options
3535
public L33tSpeakType L33tSpeak { get; set; } = L33tSpeakType.None;
3636
public SaltType Salt { get; set; } = SaltType.None;
3737
public List<SaltSource> SaltSources { get; set; } = new();
38-
public WordLists WordLists { get; set; } =
39-
WordLists.Diceware
40-
| WordLists.EffLarge
41-
| WordLists.Google;
38+
public List<WordList> WordLists { get; set; } = new();
4239
public AdvancedStrategy AdvancedStrategy { get; set; } = AdvancedStrategy.Drop;
4340

4441
public Options() { }
@@ -47,7 +44,8 @@ public static Options Default()
4744
{
4845
return new()
4946
{
50-
SaltSources = SaltSource.DefaultSources,
47+
SaltSources = SaltSource.Default,
48+
WordLists = WordListEmbeded.Default,
5149
};
5250
}
5351

@@ -58,9 +56,49 @@ internal static Options Deserialize(string serialized)
5856
XmlSerializer xml = new(typeof(Options));
5957
xml.UnknownElement += Xml_UnknownElement;
6058
Options result = xml.Deserialize(reader) as Options;
59+
MigrateWordListsIfNeeded(serialized, ref result);
6160
return result;
6261
}
6362

63+
/// <summary>
64+
/// Handles migration of WordList storage on versions <= 1.6.0. If a wordlists were set, it is replaced by the same configuration in the new system
65+
/// </summary>
66+
/// <param name="serialized">The serialized XML configuration</param>
67+
/// <param name="result">The resulting Options</param>
68+
private static void MigrateWordListsIfNeeded(string serialized, ref Options result)
69+
{
70+
using StringReader stream = new(serialized.Trim());
71+
using var reader = XmlReader.Create(stream);
72+
73+
// migrate from <= v1.6.0 word lists
74+
reader.ReadToDescendant("WordLists");
75+
76+
// They do not start as objects - it is simply a string stream
77+
if (reader.Read() && !reader.Value.StartsWith("<"))
78+
{
79+
// Start with a default wordlist with all disabled
80+
List<WordList> newWordLists = WordListEmbeded.Default;
81+
newWordLists.ForEach(wl => wl.Enabled = false);
82+
83+
string[] oldWordListNames = reader.Value.Split(new char[] { ' ' });
84+
85+
foreach (var currentWordListName in oldWordListNames)
86+
{
87+
int index = newWordLists.FindIndex(wl => ((WordListEmbeded)wl).GetBareFilename() == currentWordListName);
88+
89+
// If unexpected value (e.g. nonexistent wordlist) is found -> discard all changes
90+
if (index == -1)
91+
{
92+
return;
93+
}
94+
95+
newWordLists.ElementAt(index).Enabled = true;
96+
}
97+
98+
result.WordLists = newWordLists;
99+
}
100+
}
101+
64102
private static void Xml_UnknownElement(object sender, XmlElementEventArgs e)
65103
{
66104
if (e.ObjectBeingDeserialized is not Options opts)
@@ -74,7 +112,7 @@ private static void Xml_UnknownElement(object sender, XmlElementEventArgs e)
74112
string[] saltSourceNames = e.Element.InnerText.Split(new char[] { ' ' });
75113

76114
// start from the defaults
77-
opts.SaltSources = SaltSource.DefaultSources;
115+
opts.SaltSources = SaltSource.Default;
78116

79117
foreach (SaltSource ss in opts.SaltSources)
80118
{
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
This file is automatically generated by Visual Studio .Net. It is
4+
used to store generic object data source configuration information.
5+
Renaming the file extension or editing the content of this file may
6+
cause the file to be unrecognizable by the program.
7+
-->
8+
<GenericObjectDataSource DisplayName="WordList" Version="1.0" xmlns="urn:schemas-microsoft-com:xml-msdatasource">
9+
<TypeInfo>KeePassDiceware.WordList, KeePassDiceware, Version=1.6.0.0, Culture=neutral, PublicKeyToken=null</TypeInfo>
10+
</GenericObjectDataSource>

0 commit comments

Comments
 (0)