diff --git a/src/Core/Abstractions/IApiService.cs b/src/Core/Abstractions/IApiService.cs index 7bef3400b..049f01982 100644 --- a/src/Core/Abstractions/IApiService.cs +++ b/src/Core/Abstractions/IApiService.cs @@ -2,6 +2,7 @@ using Bit.Core.Models.Request; using Bit.Core.Models.Response; using System; +using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; @@ -42,5 +43,6 @@ namespace Bit.Core.Abstractions Task PostCipherAttachmentAsync(string id, MultipartFormDataContent data); Task PostShareCipherAttachmentAsync(string id, string attachmentId, MultipartFormDataContent data, string organizationId); + Task> GetHibpBreachAsync(string username); } } diff --git a/src/Core/Abstractions/IAuditService.cs b/src/Core/Abstractions/IAuditService.cs new file mode 100644 index 000000000..f28bb2e6f --- /dev/null +++ b/src/Core/Abstractions/IAuditService.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Response; + +namespace Bit.Core.Abstractions +{ + public interface IAuditService + { + Task> BreachedAccountsAsync(string username); + Task PasswordLeakedAsync(string password); + } +} \ No newline at end of file diff --git a/src/Core/Models/Response/BreachAccountResponse.cs b/src/Core/Models/Response/BreachAccountResponse.cs new file mode 100644 index 000000000..f32ba923a --- /dev/null +++ b/src/Core/Models/Response/BreachAccountResponse.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Bit.Core.Models.Response +{ + public class BreachAccountResponse + { + public string AddedDate { get; set; } + public string BreachDate { get; set; } + public List DataClasses { get; set; } + public string Description { get; set; } + public string Domain { get; set; } + public bool IsActive { get; set; } + public bool IsVerified { get; set; } + public string LogoPath { get; set; } + public string ModifiedDate { get; set; } + public string Name { get; set; } + public int PwnCount { get; set; } + public string Title { get; set; } + } +} diff --git a/src/Core/Services/ApiService.cs b/src/Core/Services/ApiService.cs index 8d5273397..077184122 100644 --- a/src/Core/Services/ApiService.cs +++ b/src/Core/Services/ApiService.cs @@ -261,6 +261,16 @@ namespace Bit.Core.Services #endregion + #region HIBP APIs + + public Task> GetHibpBreachAsync(string username) + { + return SendAsync>(HttpMethod.Get, + string.Concat("/hibp/breach?username=", username), null, true, true); + } + + #endregion + #region Helpers public async Task GetActiveBearerTokenAsync() diff --git a/src/Core/Services/AuditService.cs b/src/Core/Services/AuditService.cs new file mode 100644 index 000000000..53a0c2b1e --- /dev/null +++ b/src/Core/Services/AuditService.cs @@ -0,0 +1,62 @@ +using Bit.Core.Abstractions; +using Bit.Core.Exceptions; +using Bit.Core.Models.Response; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class AuditService : IAuditService + { + private const string PwnedPasswordsApi = "https://api.pwnedpasswords.com/range/"; + + private readonly ICryptoFunctionService _cryptoFunctionService; + private readonly IApiService _apiService; + + private HttpClient _httpClient; + + public AuditService( + ICryptoFunctionService cryptoFunctionService, + IApiService apiService) + { + _cryptoFunctionService = cryptoFunctionService; + _apiService = apiService; + } + + public async Task PasswordLeakedAsync(string password) + { + var hashBytes = await _cryptoFunctionService.HashAsync(password, Enums.CryptoHashAlgorithm.Sha1); + var hash = BitConverter.ToString(hashBytes).ToUpperInvariant(); + var hashStart = hash.Substring(0, 5); + var hashEnding = hash.Substring(5); + var response = await _httpClient.GetAsync(string.Concat(PwnedPasswordsApi, hashStart)); + var leakedHashes = await response.Content.ReadAsStringAsync(); + var match = leakedHashes.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None) + .FirstOrDefault(v => v.Split(':')[0] == hashEnding); + if(match != null && int.TryParse(match.Split(':')[1], out var matchCount)) + { + return matchCount; + } + return 0; + } + + public async Task> BreachedAccountsAsync(string username) + { + try + { + return await _apiService.GetHibpBreachAsync(username); + } + catch(ApiException e) + { + if(e.Error != null && e.Error.StatusCode == System.Net.HttpStatusCode.NotFound) + { + return new List(); + } + throw e; + } + } + } +}