Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for individual breach ignores #91

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 110 additions & 2 deletions HaveIBeenPwned/BreachCheckers/BreachedEntry.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
using KeePassLib;
using KeePass.Plugins;
using KeePassLib;
using System;
using System.Collections.Generic;
using System.Linq;

namespace HaveIBeenPwned.BreachCheckers
{
public class BreachedEntry
{
private IBreach breach;
private IPluginHost pluginHost;

public PwEntry Entry { get; private set; }

Expand Down Expand Up @@ -58,10 +62,114 @@ public string[] DataClasses
}
}

public BreachedEntry(PwEntry entry, IBreach breach)
public BreachedEntry(IPluginHost pluginHost, PwEntry entry, IBreach breach)
{
Entry = entry;
this.breach = breach;
this.pluginHost = pluginHost;
}

public bool IsIgnored
{
get
{
if (Entry == null)
{
// At the moment the only way a breach can have no entry is if it's a breached username
return GetIgnoredBreachForUserName().Contains(BreachName);
}

// Otherwise, see if this particular breach is ignored for this entry
return GetIngoredBreachesForThisEntry().Contains(BreachName);
}
}

private string[] GetIngoredBreachesForThisEntry()
{
if (Entry == null)
{
return new string[] { };
}

var items = Entry.CustomData.Get<string[]>(Resources.FieldNameIgnoredBreachs);
return items == null ? new string[] { } : items;
}

private void SetIngoredBreachForThisEntry(string[] value)
{
if (Entry == null)
{
throw new NotSupportedException();
}

var arrayValue = value.OrderBy(x => x).ToArray();
var modified = Entry.CustomData.Set(Resources.FieldNameIgnoredBreachs, arrayValue.Length > 0 ? arrayValue : null);
if (modified)
{
pluginHost.MarkAsModified();
}
}

public void IgnoreForThisEntry()
{
var items = GetIngoredBreachesForThisEntry().ToList();
items.Add(BreachName);
SetIngoredBreachForThisEntry(items.Distinct().ToArray());
}

public void StopIgnoringBreachForThisEntry()
{
var items = GetIngoredBreachesForThisEntry().Where(x => x != BreachName).ToArray();
SetIngoredBreachForThisEntry(items);
}

public Dictionary<string, string[]> GetUserIgnoreList()
{
var items = pluginHost.Database.RootGroup.CustomData.Get<Dictionary<string, string[]>>(Resources.FieldNameIgnoredBreachs);
return items == null ? new Dictionary<string, string[]>() { } : items;
}

private void SetGloballyIgnoredBreachesByUser(string[] breaches)
{

var currentIgnores = GetUserIgnoreList();
if (breaches != null && breaches.Length > 0)
{
currentIgnores[BreachUsername] = breaches;
}
else
{
currentIgnores.Remove(BreachUsername);
}

var entry = pluginHost.Database.RootGroup;
var modified = entry.CustomData.Set(Resources.FieldNameIgnoredBreachs, currentIgnores.Count > 0 ? currentIgnores : null);
if (modified)
{
pluginHost.MarkAsModified();
}
}

public string[] GetIgnoredBreachForUserName()
{
string[] value;
if (!GetUserIgnoreList().TryGetValue(BreachUsername, out value))
{
value = new string[] { };
}

return value;
}

public void AddBreachToUserIgnoreList()
{
var value = GetIgnoredBreachForUserName().Concat(new[] { BreachName }).OrderBy(x => x).ToArray();
SetGloballyIgnoredBreachesByUser(value);
}

public void ClearUserIgnoreList()
{
SetGloballyIgnoredBreachesByUser(null);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,14 @@ public async override Task<List<BreachedEntry>> CheckGroup(PwGroup group, bool e
var domainBreaches = breaches.Where(b => url == b && (!oldEntriesOnly || lastModified < new DateTime(2017, 02, 17)));
if (domainBreaches.Any())
{
breachedEntries.Add(new BreachedEntry(entry, new CloudbleedSiteEntry(string.Empty, entry.GetUrlDomain())));
var item = new BreachedEntry(pluginHost, entry, new CloudbleedSiteEntry(string.Empty, entry.GetUrlDomain()));

if (item.IsIgnored)
{
continue;
}

breachedEntries.Add(item);
if (expireEntries)
{
ExpireEntry(entry);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,22 @@ public async override Task<List<BreachedEntry>> CheckGroup(PwGroup group, bool e
foreach (var breach in breaches)
{
var pwEntry = breach.Entry;
if(pwEntry != null)
var breachEntry = new BreachedEntry(pluginHost, pwEntry, breach);

if (breachEntry.IsIgnored)
{
continue;
}

if (pwEntry != null)
{
if (expireEntries)
{
ExpireEntry(pwEntry);
}
}

breachedEntries.Add(new BreachedEntry(pwEntry, breach));
breachedEntries.Add(breachEntry);
}
});

Expand All @@ -75,10 +82,11 @@ private async Task<List<HaveIBeenPwnedPasswordEntry>> GetBreaches(IProgress<Prog
{
break;
}

counter++;
progressIndicator.Report(new ProgressItem((uint)((double)counter / entries.Count() * 100), string.Format("Checking \"{0}\" for breaches", entry.Strings.ReadSafe(PwDefs.TitleField))));
if(entry.Strings.Get(PwDefs.PasswordField) == null || string.IsNullOrWhiteSpace(entry.Strings.ReadSafe(PwDefs.PasswordField)) || entry.Strings.ReadSafe(PwDefs.PasswordField).StartsWith("{REF:")) continue;

if (entry.Strings.Get(PwDefs.PasswordField) == null || string.IsNullOrWhiteSpace(entry.Strings.ReadSafe(PwDefs.PasswordField)) || entry.Strings.ReadSafe(PwDefs.PasswordField).StartsWith("{REF:")) continue;
var passwordHash = string.Join("", sha.ComputeHash(entry.Strings.Get(PwDefs.PasswordField).ReadUtf8()).Select(x => x.ToString("x2"))).ToUpperInvariant();
if (cache.ContainsKey(passwordHash))
{
Expand Down Expand Up @@ -113,6 +121,7 @@ private async Task<List<HaveIBeenPwnedPasswordEntry>> GetBreaches(IProgress<Prog
}
}
}

return allBreaches;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public HaveIBeenPwnedPasswordEntry(string username, string domain, PwEntry entry
public string Title {
get
{
return "HIBP Password Breach";
return Resources.PasswordBreachTitle;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,22 @@ public async override Task<List<BreachedEntry>> CheckGroup(PwGroup group, bool e
if (!string.IsNullOrEmpty(url))
{
var domainBreaches = breaches.Where(b => !string.IsNullOrWhiteSpace(b.Domain) && url == b.Domain && (!oldEntriesOnly || lastModified < b.BreachDate)).OrderBy(b => b.BreachDate);
if (domainBreaches.Any())

if (!domainBreaches.Any())
{
continue;
}

var breachEntry = new BreachedEntry(pluginHost, entry, domainBreaches.Last());

if (!breachEntry.IsIgnored)
{
breachedEntries.Add(breachEntry);
}

if (expireEntries)
{
breachedEntries.Add(new BreachedEntry(entry, domainBreaches.Last()));
if (expireEntries)
{
ExpireEntry(entry);
}
ExpireEntry(entry);
}
}
// this checker is so quick it probably doesn't need to report progress
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,16 @@ public override string BreachTitle

progressIndicator.Report(new ProgressItem(0, "Getting HaveIBeenPwned breach list..."));
var entries = group.GetEntries(true)
.Where(e => (!ignoreDeleted || !e.IsDeleted(pluginHost)) && (!ignoreExpired || !e.Expires)).ToArray();
var usernames = entries.Select(e => e.Strings.ReadSafe(PwDefs.UserNameField).Trim().ToLower()).Distinct();
.Where(e => (!ignoreDeleted || !e.IsDeleted(pluginHost)) && (!ignoreExpired || !e.Expires))
.ToArray();
var usernames =
entries.Select(e => e.Strings.ReadSafe(PwDefs.UserNameField).Trim().ToLower())
.Distinct();
var breaches = await this.GetBreaches(progressIndicator, usernames, canContinue);
var breachedEntries = new List<BreachedEntry>();

await Task.Run(() =>
{
{
foreach (var breachGrp in breaches.GroupBy(x => x.Username))
{
var username = breachGrp.Key;
Expand All @@ -74,28 +77,46 @@ public override string BreachTitle
{
continue;
}

var pwEntries =
string.IsNullOrWhiteSpace(breach.Domain) ? new PwEntry[] { } :
entries
.Where(e => e.GetUrlDomain() == breach.Domain && breach.Username == e.Strings.ReadSafe(PwDefs.UserNameField).Trim().ToLower())
.ToArray();

if (pwEntries.Length == 0)
{
var item = new BreachedEntry(pluginHost, null, breach);
if (item.IsIgnored)
{
continue;
}

breachedEntries.Add(item);
continue;
}

var pwEntry =
string.IsNullOrWhiteSpace(breach.Domain)
? null
: entries.FirstOrDefault(e =>
e.GetUrlDomain() == breach.Domain && breach.Username ==
e.Strings.ReadSafe(PwDefs.UserNameField).Trim().ToLower());
if (pwEntry != null)
foreach (var pwEntry in pwEntries)
{
var lastModified = pwEntry.GetPasswordLastModified();
if (oldEntriesOnly && lastModified >= breach.BreachDate)
{
continue;
}

var item = new BreachedEntry(pluginHost, pwEntry, breach);
if (item.IsIgnored)
{
continue;
}

if (expireEntries)
{
ExpireEntry(pwEntry);
}
}

breachedEntries.Add(new BreachedEntry(pwEntry, breach));
breachedEntries.Add(item);
}
}
}
});
Expand All @@ -117,10 +138,10 @@ public override string BreachTitle
}

counter++;
progressIndicator.Report(new ProgressItem((uint) ((double) counter / filteredUsernames.Count() * 100),
progressIndicator.Report(new ProgressItem((uint)((double)counter / filteredUsernames.Count() * 100),
string.Format("Checking \"{0}\" for breaches", username)));
try
{
{
var breaches = await GetBreachesForUserName(HttpUtility.UrlEncode(username), DefaultRetries);
if (breaches != null)
{
Expand Down Expand Up @@ -165,7 +186,7 @@ public override string BreachTitle
b.Username = HttpUtility.UrlDecode(username);
}
}
else if ((int) response.StatusCode == 429) // The Rate limit of our API Key was exceeded
else if ((int)response.StatusCode == 429) // The Rate limit of our API Key was exceeded
{
var whenToRetry = response.Headers.RetryAfter.Delta;

Expand Down
2 changes: 2 additions & 0 deletions HaveIBeenPwned/HaveIBeenPwned.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@
<Compile Include="BreachCheckers\HaveIBeenPwnedUsername\HaveIBeenPwnedUsernameChecker.cs" />
<Compile Include="BreachCheckers\HaveIBeenPwnedUsername\HaveIBeenPwnedUsernameEntry.cs" />
<Compile Include="DisplayAttribute.cs" />
<Compile Include="PwDatabaseExtensions.cs" />
<Compile Include="StringDictionaryExExtensions.cs" />
<Compile Include="UI\BreachedEntriesDialog.cs">
<SubType>Form</SubType>
</Compile>
Expand Down
Loading