[PM-192] Refactor forwarded email providers (#2579)

* PM-192 Refactor Forwarded email providers to use better patterns and code reuse.

* PM-192 fix format
This commit is contained in:
Federico Maccaroni 2023-06-27 18:49:38 -03:00 committed by GitHub
parent 3506269811
commit 1014563c75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 547 additions and 459 deletions

View File

@ -249,27 +249,25 @@
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
StyleClass="box-value"
AutomationId="ServiceTypePicker" />
<!--ANONADDY OPTIONS-->
<Grid IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
Grid.RowDefinitions="Auto,*"
Grid.ColumnDefinitions="*,Auto">
<Grid
Grid.RowDefinitions="Auto,*"
Grid.ColumnDefinitions="*,Auto">
<Label
Margin="0,10,0,0"
Text="{u:I18n APIAccessToken}"
Text="{Binding ForwardedEmailApiSecretLabel}"
StyleClass="box-label"/>
<Entry
x:Name="_anonAddyApiAccessTokenEntry"
Text="{Binding AnonAddyApiAccessToken}"
IsPassword="{Binding ShowAnonAddyApiAccessToken, Converter={StaticResource inverseBool}}"
Text="{Binding ForwardedEmailApiSecret}"
IsPassword="{Binding ShowForwardedEmailApiSecret, Converter={StaticResource inverseBool}}"
Grid.Row="1"
AutomationId="AnonAddyApiAccessTokenEntry" />
AutomationId="ForwardedEmailApiSecretEntry" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowAnonAddyApiAccessToken, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Text="{Binding ShowForwardedEmailApiSecret, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"
AutomationId="ShowAnonAddyApiAccessTokenButton" />
AutomationId="ShowForwardedEmailApiSecretButton" />
</Grid>
<Label IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
Text="{u:I18n DomainNameRequiredParenthesis}"
@ -280,98 +278,6 @@
Text="{Binding AnonAddyDomainName}"
StyleClass="box-value"
AutomationId="AnonAddyDomainNameEntry" />
<!--FIREFOX RELAY OPTIONS-->
<Grid StyleClass="box-row, box-row-input"
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.FirefoxRelay}}"
Grid.RowDefinitions="Auto,*"
Grid.ColumnDefinitions="*,Auto">
<Label
Text="{u:I18n APIAccessToken}"
StyleClass="box-label"/>
<Entry
x:Name="_firefoxRelayApiAccessTokenEntry"
Text="{Binding FirefoxRelayApiAccessToken}"
StyleClass="box-value"
Grid.Row="1"
IsPassword="{Binding ShowFirefoxRelayApiAccessToken, Converter={StaticResource inverseBool}}"
AutomationId="FirefoxRelayApiAccessTokenEntry" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowFirefoxRelayApiAccessToken, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"
AutomationId="ShowFirefoxRelayApiAccessTokenButton" />
</Grid>
<!--SIMPLELOGIN OPTIONS-->
<Grid StyleClass="box-row, box-row-input"
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.SimpleLogin}}"
Grid.RowDefinitions="Auto,*"
Grid.ColumnDefinitions="*,Auto">
<Label
Text="{u:I18n APIKeyRequiredParenthesis}"
StyleClass="box-label"/>
<Entry
x:Name="_simpleLoginApiKeyEntry"
Text="{Binding SimpleLoginApiKey}"
StyleClass="box-value"
Grid.Row="1"
IsPassword="{Binding ShowSimpleLoginApiKey, Converter={StaticResource inverseBool}}"
AutomationId="SimpleLoginApiKeyEntry" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowSimpleLoginApiKey, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"
AutomationId="ShowSimpleLoginApiKeyButton" />
</Grid>
<!--DUCKDUCKGO OPTIONS-->
<Grid StyleClass="box-row, box-row-input"
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.DuckDuckGo}}"
Grid.RowDefinitions="Auto,*"
Grid.ColumnDefinitions="*,Auto">
<Label
Text="{u:I18n APIKeyRequiredParenthesis}"
StyleClass="box-label"/>
<Entry
x:Name="_duckDuckGoApiAccessTokenEntry"
Text="{Binding DuckDuckGoApiKey}"
StyleClass="box-value"
Grid.Row="1"
IsPassword="{Binding ShowDuckDuckGoApiKey, Converter={StaticResource inverseBool}}"
AutomationId="DuckDuckGoApiKeyEntry" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowDuckDuckGoApiKey, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"
AutomationId="ShowDuckDuckGoApiKeyButton" />
</Grid>
<!--FASTMAIL OPTIONS-->
<Grid StyleClass="box-row, box-row-input"
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.Fastmail}}"
Grid.RowDefinitions="Auto,*"
Grid.ColumnDefinitions="*,Auto">
<Label
Text="{u:I18n APIKeyRequiredParenthesis}"
StyleClass="box-label"/>
<Entry
x:Name="_fastmailApiAccessTokenEntry"
Text="{Binding FastmailApiKey}"
StyleClass="box-value"
Grid.Row="1"
IsPassword="{Binding ShowFastmailApiKey, Converter={StaticResource inverseBool}}"
AutomationId="FastmailApiKeyEntry" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowFastmailApiKey, Converter={StaticResource iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"
AutomationId="ShowFastmailApiKeyButton" />
</Grid>
</StackLayout>
<!--RANDOM WORD OPTIONS-->
<Grid IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}">

View File

@ -8,6 +8,7 @@ using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Domain;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
@ -23,7 +24,7 @@ namespace Bit.App.Pages
private readonly IUsernameGenerationService _usernameGenerationService;
private readonly ITokenService _tokenService;
private readonly IDeviceActionService _deviceActionService;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
private PasswordGenerationOptions _options;
private UsernameGenerationOptions _usernameOptions;
@ -49,11 +50,7 @@ namespace Bit.App.Pages
private bool _doneIniting;
private bool _showTypePicker;
private string _emailWebsite;
private bool _showFirefoxRelayApiAccessToken;
private bool _showAnonAddyApiAccessToken;
private bool _showSimpleLoginApiKey;
private bool _showDuckDuckGoApiKey;
private bool _showFastmailApiKey;
private bool _showForwardedEmailApiSecret;
private bool _editMode;
public GeneratorPageViewModel()
@ -96,7 +93,7 @@ namespace Bit.App.Pages
UsernameTypePromptHelpCommand = new Command(UsernameTypePromptHelp);
RegenerateCommand = new AsyncCommand(RegenerateAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
RegenerateUsernameCommand = new AsyncCommand(RegenerateUsernameAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
ToggleForwardedEmailHiddenValueCommand = new AsyncCommand(ToggleForwardedEmailHiddenValueAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false);
ToggleForwardedEmailHiddenValueCommand = new Command(() => ShowForwardedEmailApiSecret = !ShowForwardedEmailApiSecret);
CopyCommand = new AsyncCommand(CopyAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false);
CloseCommand = new AsyncCommand(CloseAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false);
}
@ -415,7 +412,6 @@ namespace Bit.App.Pages
public string UsernameTypeDescriptionLabel => GetUsernameTypeLabelDescription(UsernameTypeSelected);
public ForwardedEmailServiceType ForwardedEmailServiceSelected
{
get => _usernameOptions.ServiceType;
@ -425,7 +421,11 @@ namespace Bit.App.Pages
{
_usernameOptions.ServiceType = value;
Username = Constants.DefaultUsernameGenerated;
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected));
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected), new string[]
{
nameof(ForwardedEmailApiSecret),
nameof(ForwardedEmailApiSecretLabel)
});
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
@ -445,27 +445,104 @@ namespace Bit.App.Pages
}
}
public string AnonAddyApiAccessToken
public string ForwardedEmailApiSecret
{
get => _usernameOptions.AnonAddyApiAccessToken;
get
{
switch (ForwardedEmailServiceSelected)
{
case ForwardedEmailServiceType.AnonAddy:
return _usernameOptions.AnonAddyApiAccessToken;
case ForwardedEmailServiceType.DuckDuckGo:
return _usernameOptions.DuckDuckGoApiKey;
case ForwardedEmailServiceType.Fastmail:
return _usernameOptions.FastMailApiKey;
case ForwardedEmailServiceType.FirefoxRelay:
return _usernameOptions.FirefoxRelayApiAccessToken;
case ForwardedEmailServiceType.SimpleLogin:
return _usernameOptions.SimpleLoginApiKey;
default:
return null;
}
}
set
{
if (_usernameOptions.AnonAddyApiAccessToken != value)
bool changed = false;
switch (ForwardedEmailServiceSelected)
{
_usernameOptions.AnonAddyApiAccessToken = value;
TriggerPropertyChanged(nameof(AnonAddyApiAccessToken));
case ForwardedEmailServiceType.AnonAddy:
if (_usernameOptions.AnonAddyApiAccessToken != value)
{
_usernameOptions.AnonAddyApiAccessToken = value;
changed = true;
}
break;
case ForwardedEmailServiceType.DuckDuckGo:
if (_usernameOptions.DuckDuckGoApiKey != value)
{
_usernameOptions.DuckDuckGoApiKey = value;
changed = true;
}
break;
case ForwardedEmailServiceType.Fastmail:
if (_usernameOptions.FastMailApiKey != value)
{
_usernameOptions.FastMailApiKey = value;
changed = true;
}
break;
case ForwardedEmailServiceType.FirefoxRelay:
if (_usernameOptions.FirefoxRelayApiAccessToken != value)
{
_usernameOptions.FirefoxRelayApiAccessToken = value;
changed = true;
}
break;
case ForwardedEmailServiceType.SimpleLogin:
if (_usernameOptions.SimpleLoginApiKey != value)
{
_usernameOptions.SimpleLoginApiKey = value;
changed = true;
}
break;
default:
break;
}
if (changed)
{
TriggerPropertyChanged(nameof(ForwardedEmailApiSecret));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public bool ShowAnonAddyApiAccessToken
public string ForwardedEmailApiSecretLabel
{
get
{
return _showAnonAddyApiAccessToken;
switch (ForwardedEmailServiceSelected)
{
case ForwardedEmailServiceType.AnonAddy:
case ForwardedEmailServiceType.FirefoxRelay:
return AppResources.APIAccessToken;
case ForwardedEmailServiceType.DuckDuckGo:
case ForwardedEmailServiceType.Fastmail:
case ForwardedEmailServiceType.SimpleLogin:
return AppResources.APIKeyRequiredParenthesis;
default:
return null;
}
}
set => SetProperty(ref _showAnonAddyApiAccessToken, value);
}
public bool ShowForwardedEmailApiSecret
{
get
{
return _showForwardedEmailApiSecret;
}
set => SetProperty(ref _showForwardedEmailApiSecret, value);
}
public string AnonAddyDomainName
@ -482,99 +559,6 @@ namespace Bit.App.Pages
}
}
public string FirefoxRelayApiAccessToken
{
get => _usernameOptions.FirefoxRelayApiAccessToken;
set
{
if (_usernameOptions.FirefoxRelayApiAccessToken != value)
{
_usernameOptions.FirefoxRelayApiAccessToken = value;
TriggerPropertyChanged(nameof(FirefoxRelayApiAccessToken));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public bool ShowFirefoxRelayApiAccessToken
{
get
{
return _showFirefoxRelayApiAccessToken;
}
set => SetProperty(ref _showFirefoxRelayApiAccessToken, value);
}
public string SimpleLoginApiKey
{
get => _usernameOptions.SimpleLoginApiKey;
set
{
if (_usernameOptions.SimpleLoginApiKey != value)
{
_usernameOptions.SimpleLoginApiKey = value;
TriggerPropertyChanged(nameof(SimpleLoginApiKey));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public bool ShowSimpleLoginApiKey
{
get
{
return _showSimpleLoginApiKey;
}
set => SetProperty(ref _showSimpleLoginApiKey, value);
}
public string DuckDuckGoApiKey
{
get => _usernameOptions.DuckDuckGoApiKey;
set
{
if (_usernameOptions.DuckDuckGoApiKey != value)
{
_usernameOptions.DuckDuckGoApiKey = value;
TriggerPropertyChanged(nameof(DuckDuckGoApiKey));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public bool ShowDuckDuckGoApiKey
{
get
{
return _showDuckDuckGoApiKey;
}
set => SetProperty(ref _showDuckDuckGoApiKey, value);
}
public string FastmailApiKey
{
get => _usernameOptions.FastMailApiKey;
set
{
if (_usernameOptions.FastMailApiKey != value)
{
_usernameOptions.FastMailApiKey = value;
TriggerPropertyChanged(nameof(FastmailApiKey));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public bool ShowFastmailApiKey
{
get
{
return _showFastmailApiKey;
}
set => SetProperty(ref _showFastmailApiKey, value);
}
public bool CapitalizeRandomWordUsername
{
get => _usernameOptions.CapitalizeRandomWordUsername;
@ -807,12 +791,9 @@ namespace Bit.App.Pages
TriggerPropertyChanged(nameof(PlusAddressedEmailTypeSelected));
TriggerPropertyChanged(nameof(IncludeNumberRandomWordUsername));
TriggerPropertyChanged(nameof(CapitalizeRandomWordUsername));
TriggerPropertyChanged(nameof(SimpleLoginApiKey));
TriggerPropertyChanged(nameof(FirefoxRelayApiAccessToken));
TriggerPropertyChanged(nameof(ForwardedEmailApiSecret));
TriggerPropertyChanged(nameof(ForwardedEmailApiSecretLabel));
TriggerPropertyChanged(nameof(AnonAddyDomainName));
TriggerPropertyChanged(nameof(AnonAddyApiAccessToken));
TriggerPropertyChanged(nameof(DuckDuckGoApiKey));
TriggerPropertyChanged(nameof(FastmailApiKey));
TriggerPropertyChanged(nameof(CatchAllEmailDomain));
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected));
TriggerPropertyChanged(nameof(UsernameTypeSelected));
@ -845,15 +826,23 @@ namespace Bit.App.Pages
{
_logger.Value.Exception(ex);
string message = AppResources.GenericErrorMessage;
if (IsUsername && UsernameTypeSelected == UsernameType.ForwardedEmailAlias)
{
await Device.InvokeOnMainThreadAsync(() => Page.DisplayAlert(
AppResources.AnErrorHasOccurred, string.Format(AppResources.UnknownXErrorMessage, ForwardedEmailServiceSelected), AppResources.Ok));
}
else
{
await Device.InvokeOnMainThreadAsync(() => Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok));
if (ex is ForwardedEmailInvalidSecretException)
{
message = ForwardedEmailServiceSelected == ForwardedEmailServiceType.AnonAddy || ForwardedEmailServiceSelected == ForwardedEmailServiceType.FirefoxRelay
? AppResources.InvalidAPIToken
: AppResources.InvalidAPIKey;
}
else
{
message = string.Format(AppResources.UnknownXErrorMessage, ForwardedEmailServiceSelected);
}
}
await Device.InvokeOnMainThreadAsync(() => Page.DisplayAlert(AppResources.AnErrorHasOccurred, message, AppResources.Ok));
}
private string GetUsernameTypeLabelDescription(UsernameType value)
@ -870,27 +859,5 @@ namespace Bit.App.Pages
return string.Empty;
}
}
private async Task ToggleForwardedEmailHiddenValueAsync()
{
switch (ForwardedEmailServiceSelected)
{
case ForwardedEmailServiceType.AnonAddy:
ShowAnonAddyApiAccessToken = !ShowAnonAddyApiAccessToken;
break;
case ForwardedEmailServiceType.FirefoxRelay:
ShowFirefoxRelayApiAccessToken = !ShowFirefoxRelayApiAccessToken;
break;
case ForwardedEmailServiceType.SimpleLogin:
ShowSimpleLoginApiKey = !ShowSimpleLoginApiKey;
break;
case ForwardedEmailServiceType.DuckDuckGo:
ShowDuckDuckGoApiKey = !ShowDuckDuckGoApiKey;
break;
case ForwardedEmailServiceType.Fastmail:
ShowFastmailApiKey = !ShowFastmailApiKey;
break;
}
}
}
}

View File

@ -3262,6 +3262,24 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Invalid API key.
/// </summary>
public static string InvalidAPIKey {
get {
return ResourceManager.GetString("InvalidAPIKey", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Invalid API token.
/// </summary>
public static string InvalidAPIToken {
get {
return ResourceManager.GetString("InvalidAPIToken", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Invalid email address..
/// </summary>

View File

@ -2637,4 +2637,10 @@ Do you want to switch to this account?</value>
<data name="UnlockingMayFailDueToInsufficientMemoryDecreaseYourKDFMemorySettingsToResolve" xml:space="preserve">
<value>Unlocking may fail due to insufficient memory. Decrease your KDF memory settings to resolve.</value>
</data>
<data name="InvalidAPIKey" xml:space="preserve">
<value>Invalid API key</value>
</data>
<data name="InvalidAPIToken" xml:space="preserve">
<value>Invalid API token</value>
</data>
</root>

View File

@ -1,8 +1,9 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Request;
using Bit.Core.Models.Response;
@ -48,6 +49,7 @@ namespace Bit.Core.Abstractions
Task<SsoPrevalidateResponse> PreValidateSso(string identifier);
Task<TResponse> SendAsync<TRequest, TResponse>(HttpMethod method, string path,
TRequest body, bool authed, bool hasResponse, Action<HttpRequestMessage> alterRequest, bool logoutOnUnauthorized = true);
Task<HttpResponseMessage> SendAsync(HttpRequestMessage requestMessage, CancellationToken cancellationToken = default);
void SetUrls(EnvironmentUrls urls);
[Obsolete("Mar 25 2021: This method has been deprecated in favor of direct uploads. This method still exists for backward compatibility with old server versions.")]
Task<CipherResponse> PostCipherAttachmentLegacyAsync(string id, MultipartFormDataContent data);
@ -89,9 +91,9 @@ namespace Bit.Core.Abstractions
Task<PasswordlessLoginResponse> GetAuthResponseAsync(string id, string accessCode);
Task<PasswordlessLoginResponse> PutAuthRequestAsync(string id, string key, string masterPasswordHash, string deviceIdentifier, bool requestApproved);
Task<PasswordlessLoginResponse> PostCreateRequestAsync(PasswordlessCreateLoginRequest passwordlessCreateLoginRequest);
Task<string> GetUsernameFromAsync(ForwardedEmailServiceType service, UsernameGeneratorConfig config);
Task<bool> GetKnownDeviceAsync(string email, string deviceIdentifier);
Task<OrganizationDomainSsoDetailsResponse> GetOrgDomainSsoDetailsAsync(string email);
Task<ConfigResponse> GetConfigsAsync();
Task<string> GetFastmailAccountIdAsync(string apiKey);
}
}

View File

@ -43,6 +43,7 @@ namespace Bit.Core.Abstractions
Task<Tuple<EncString, SymmetricCryptoKey>> MakeShareKeyAsync();
Task<SymmetricCryptoKey> MakeSendKeyAsync(byte[] keyMaterial);
Task<int> RandomNumberAsync(int min, int max);
Task<string> RandomStringAsync(int length);
Task<Tuple<SymmetricCryptoKey, EncString>> RemakeEncKeyAsync(SymmetricCryptoKey key);
Task<EncString> RsaEncryptAsync(byte[] data, byte[] publicKey = null);
Task<byte[]> RsaDecryptAsync(string encValue, byte[] privateKey = null);

View File

@ -20,6 +20,7 @@
<None Remove="Attributes\" />
<None Remove="MessagePack" />
<None Remove="MessagePack.MSBuild.Tasks" />
<None Remove="Services\EmailForwarders\" />
</ItemGroup>
<ItemGroup>
@ -44,5 +45,6 @@
<ItemGroup>
<Folder Include="Services\Logging\" />
<Folder Include="Attributes\" />
<Folder Include="Services\EmailForwarders\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,11 @@
using System;
namespace Bit.Core.Exceptions
{
public class ForwardedEmailInvalidSecretException : Exception
{
public ForwardedEmailInvalidSecretException(Exception innerEx)
: base("Invalid API Secret", innerEx)
{
}
}
}

View File

@ -1,4 +1,5 @@
using Bit.Core.Enums;
using Bit.Core.Services.EmailForwarders;
namespace Bit.Core.Models.Domain
{
@ -24,5 +25,33 @@ namespace Bit.Core.Models.Domain
public string AnonAddyApiAccessToken { get; set; }
public string AnonAddyDomainName { get; set; }
public string EmailWebsite { get; set; }
public ForwarderOptions GetForwarderOptions()
{
if (Type != UsernameType.ForwardedEmailAlias)
{
return null;
}
switch (ServiceType)
{
case ForwardedEmailServiceType.AnonAddy:
return new AnonAddyForwarderOptions
{
ApiKey = AnonAddyApiAccessToken,
DomainName = AnonAddyDomainName
};
case ForwardedEmailServiceType.DuckDuckGo:
return new ForwarderOptions { ApiKey = DuckDuckGoApiKey };
case ForwardedEmailServiceType.Fastmail:
return new ForwarderOptions { ApiKey = FastMailApiKey };
case ForwardedEmailServiceType.FirefoxRelay:
return new ForwarderOptions { ApiKey = FirefoxRelayApiAccessToken };
case ForwardedEmailServiceType.SimpleLogin:
return new ForwarderOptions { ApiKey = SimpleLoginApiKey };
default:
return null;
}
}
}
}

View File

@ -1,9 +0,0 @@
namespace Bit.Core.Models.Domain
{
public class UsernameGeneratorConfig
{
public string ApiToken { get; set; }
public string Domain { get; set; }
public string Url { get; set; }
}
}

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
@ -763,111 +764,29 @@ namespace Bit.Core.Services
}
}
public async Task<string> GetUsernameFromAsync(ForwardedEmailServiceType service, UsernameGeneratorConfig config)
public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage requestMessage, CancellationToken cancellationToken = default)
{
using (var requestMessage = new HttpRequestMessage())
HttpResponseMessage response;
try
{
requestMessage.Version = new Version(1, 0);
requestMessage.Method = HttpMethod.Post;
requestMessage.RequestUri = new Uri(config.Url);
requestMessage.Headers.Add("Accept", "application/json");
switch (service)
{
case ForwardedEmailServiceType.AnonAddy:
requestMessage.Headers.Add("Authorization", $"Bearer {config.ApiToken}");
requestMessage.Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["domain"] = config.Domain
});
break;
case ForwardedEmailServiceType.FirefoxRelay:
requestMessage.Headers.Add("Authorization", $"Token {config.ApiToken}");
requestMessage.Content = new StringContent(JsonConvert.SerializeObject(
new
{
enabled = true,
description = "Generated by Bitwarden."
}), Encoding.UTF8, "application/json");
break;
case ForwardedEmailServiceType.SimpleLogin:
requestMessage.Headers.Add("Authentication", config.ApiToken);
break;
case ForwardedEmailServiceType.DuckDuckGo:
requestMessage.Headers.Add("Authorization", $"Bearer {config.ApiToken}");
break;
case ForwardedEmailServiceType.Fastmail:
requestMessage.Headers.Add("Authorization", $"Bearer {config.ApiToken}");
requestMessage.Content = new StringContent(await CreateFastmailRequest(config.ApiToken),
Encoding.UTF8, "application/json");
break;
}
HttpResponseMessage response;
try
{
response = await _httpClient.SendAsync(requestMessage);
}
catch (Exception e)
{
throw new ApiException(HandleWebError(e));
}
if (!response.IsSuccessStatusCode)
{
throw new ApiException(new ErrorResponse
{
StatusCode = response.StatusCode,
Message = $"{service} error: {(int)response.StatusCode} {response.ReasonPhrase}."
});
}
var responseJsonString = await response.Content.ReadAsStringAsync();
var result = JObject.Parse(responseJsonString);
switch (service)
{
case ForwardedEmailServiceType.AnonAddy:
return result["data"]?["email"]?.ToString();
case ForwardedEmailServiceType.FirefoxRelay:
return result["full_address"]?.ToString();
case ForwardedEmailServiceType.SimpleLogin:
return result["alias"]?.ToString();
case ForwardedEmailServiceType.DuckDuckGo:
return $"{result["address"]?.ToString()}@duck.com";
case ForwardedEmailServiceType.Fastmail:
return HandleFastMailResponse(result);
default:
return string.Empty;
}
response = await _httpClient.SendAsync(requestMessage, cancellationToken);
}
catch (Exception e)
{
throw new ApiException(HandleWebError(e));
}
if (!response.IsSuccessStatusCode)
{
throw new ApiException(new ErrorResponse
{
StatusCode = response.StatusCode,
Message = $"{requestMessage.RequestUri} error: {(int)response.StatusCode} {response.ReasonPhrase}."
});
}
return response;
}
private string HandleFastMailResponse(JObject result)
{
if (result["methodResponses"] == null || !result["methodResponses"].HasValues ||
!result["methodResponses"][0].HasValues)
{
throw new Exception("Fastmail error: could not parse response.");
}
if (result["methodResponses"][0][0].ToString() == "MaskedEmail/set")
{
if (result["methodResponses"][0][1]?["created"]?["new-masked-email"] != null)
{
return result["methodResponses"][0][1]?["created"]?["new-masked-email"]?["email"].ToString();
}
if (result["methodResponses"][0][1]?["notCreated"]?["new-masked-email"] != null)
{
throw new Exception("Fastmail error: " +
result["methodResponses"][0][1]?["created"]?["new-masked-email"]?["description"].ToString());
}
}
else if (result["methodResponses"][0][0].ToString() == "error")
{
throw new Exception("Fastmail error: " + result["methodResponses"][0][1]?["description"].ToString());
}
throw new Exception("Fastmail error: could not parse response.");
}
private async Task<string> CreateFastmailRequest(string apiKey)
public async Task<string> GetFastmailAccountIdAsync(string apiKey)
{
using (var httpclient = new HttpClient())
{
@ -891,36 +810,7 @@ namespace Bit.Core.Services
});
}
var result = JObject.Parse(await response.Content.ReadAsStringAsync());
var accountId = result["primaryAccounts"]?["https://www.fastmail.com/dev/maskedemail"]?.ToString();
var requestJObj = new JObject
{
new JProperty("using",
new JArray { "https://www.fastmail.com/dev/maskedemail", "urn:ietf:params:jmap:core" }),
new JProperty("methodCalls",
new JArray
{
new JArray
{
"MaskedEmail/set",
new JObject
{
["accountId"] = accountId,
["create"] = new JObject
{
["new-masked-email"] = new JObject
{
["state"] = "enabled",
["description"] = "",
["url"] = "",
["emailPrefix"] = ""
}
}
},
"0"
}
})
};
return requestJObj.ToString();
return result["primaryAccounts"]?["https://www.fastmail.com/dev/maskedemail"]?.ToString();
}
}

View File

@ -14,6 +14,8 @@ namespace Bit.Core.Services
{
public class CryptoService : ICryptoService
{
private const string RANDOM_STRING_CHARSET = "abcdefghijklmnopqrstuvwxyz1234567890";
private readonly IStateService _stateService;
private readonly ICryptoFunctionService _cryptoFunctionService;
@ -633,6 +635,22 @@ namespace Bit.Core.Services
return (int)(min + (ui % diff));
}
/// <summary>
/// Makes random string with length <paramref name="length"/> based on the charset <see cref="RANDOM_STRING_CHARSET"/>
/// </summary>
public async Task<string> RandomStringAsync(int length)
{
var sb = new StringBuilder();
for (var i = 0; i < length; i++)
{
var randomCharIndex = await RandomNumberAsync(0, RANDOM_STRING_CHARSET.Length - 1);
sb.Append(RANDOM_STRING_CHARSET[randomCharIndex]);
}
return sb.ToString();
}
// Helpers
private async Task<EncryptedObject> AesEncryptAsync(byte[] data, SymmetricCryptoKey key)

View File

@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Newtonsoft.Json.Linq;
namespace Bit.Core.Services.EmailForwarders
{
public class AnonAddyForwarderOptions : ForwarderOptions
{
public string DomainName { get; set; }
}
public class AnonAddyForwarder : BaseForwarder<AnonAddyForwarderOptions>
{
protected override string RequestUri => "https://app.anonaddy.com/api/v1/aliases";
protected override bool CanGenerate(AnonAddyForwarderOptions options)
{
return !string.IsNullOrWhiteSpace(options.ApiKey) && !string.IsNullOrWhiteSpace(options.DomainName);
}
protected override void ConfigureHeaders(HttpRequestHeaders headers, AnonAddyForwarderOptions options)
{
headers.Add("Authorization", $"Bearer {options.ApiKey}");
}
protected override Task<HttpContent> GetContentAsync(IApiService apiService, AnonAddyForwarderOptions options)
{
return Task.FromResult<HttpContent>(new FormUrlEncodedContent(new Dictionary<string, string>
{
["domain"] = options.DomainName
}));
}
protected override string HandleResponse(JObject result)
{
return result["data"]?["email"]?.ToString();
}
}
}

View File

@ -0,0 +1,63 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
using Newtonsoft.Json.Linq;
namespace Bit.Core.Services.EmailForwarders
{
public abstract class BaseForwarder<T>
where T : ForwarderOptions
{
protected abstract string RequestUri { get; }
public async Task<string> GenerateAsync(IApiService apiService, T options)
{
if (!CanGenerate(options))
{
return Constants.DefaultUsernameGenerated;
}
using (var requestMessage = new HttpRequestMessage())
{
requestMessage.Version = new Version(1, 0);
requestMessage.Method = HttpMethod.Post;
requestMessage.RequestUri = new Uri(RequestUri);
requestMessage.Headers.Add("Accept", "application/json");
ConfigureHeaders(requestMessage.Headers, options);
requestMessage.Content = await GetContentAsync(apiService, options);
try
{
var response = await apiService.SendAsync(requestMessage);
var responseJsonString = await response.Content.ReadAsStringAsync();
return HandleResponse(JObject.Parse(responseJsonString));
}
catch (ApiException ex)
{
if (IsRequestSecretInvalid(ex))
{
throw new ForwardedEmailInvalidSecretException(ex);
}
throw;
}
}
}
protected virtual bool CanGenerate(T options) => !string.IsNullOrWhiteSpace(options.ApiKey);
protected abstract void ConfigureHeaders(HttpRequestHeaders headers, T options);
protected abstract Task<HttpContent> GetContentAsync(IApiService apiService, T options);
protected abstract string HandleResponse(JObject result);
protected virtual bool IsRequestSecretInvalid(ApiException ex) => ex.Error?.StatusCode == System.Net.HttpStatusCode.Unauthorized;
}
}

View File

@ -0,0 +1,25 @@
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Newtonsoft.Json.Linq;
namespace Bit.Core.Services.EmailForwarders
{
public class DuckDuckGoForwarder : BaseForwarder<ForwarderOptions>
{
protected override string RequestUri => "https://quack.duckduckgo.com/api/email/addresses";
protected override void ConfigureHeaders(HttpRequestHeaders headers, ForwarderOptions options)
{
headers.Add("Authorization", $"Bearer {options.ApiKey}");
}
protected override Task<HttpContent> GetContentAsync(IApiService apiService, ForwarderOptions options) => Task.FromResult<HttpContent>(null);
protected override string HandleResponse(JObject result)
{
return $"{result["address"]?.ToString()}@duck.com";
}
}
}

View File

@ -0,0 +1,98 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
using Newtonsoft.Json.Linq;
namespace Bit.Core.Services.EmailForwarders
{
public class FastmailForwarder : BaseForwarder<ForwarderOptions>
{
protected override string RequestUri => "https://api.fastmail.com/jmap/api/";
protected override void ConfigureHeaders(HttpRequestHeaders headers, ForwarderOptions options)
{
headers.Add("Authorization", $"Bearer {options.ApiKey}");
}
protected override async Task<HttpContent> GetContentAsync(IApiService apiService, ForwarderOptions options)
{
string accountId = null;
try
{
accountId = await apiService.GetFastmailAccountIdAsync(options.ApiKey);
}
catch (ApiException ex)
{
if (IsRequestSecretInvalid(ex))
{
throw new ForwardedEmailInvalidSecretException(ex);
}
throw;
}
var requestJObj = new JObject
{
new JProperty("using",
new JArray { "https://www.fastmail.com/dev/maskedemail", "urn:ietf:params:jmap:core" }),
new JProperty("methodCalls",
new JArray
{
new JArray
{
"MaskedEmail/set",
new JObject
{
["accountId"] = accountId,
["create"] = new JObject
{
["new-masked-email"] = new JObject
{
["state"] = "enabled",
["description"] = "",
["url"] = "",
["emailPrefix"] = ""
}
}
},
"0"
}
})
};
return new StringContent(requestJObj.ToString(), Encoding.UTF8, "application/json");
}
protected override string HandleResponse(JObject result)
{
if (result["methodResponses"] == null || !result["methodResponses"].HasValues ||
!result["methodResponses"][0].HasValues)
{
throw new Exception("Fastmail error: could not parse response.");
}
if (result["methodResponses"][0][0].ToString() == "MaskedEmail/set")
{
if (result["methodResponses"][0][1]?["created"]?["new-masked-email"] != null)
{
return result["methodResponses"][0][1]?["created"]?["new-masked-email"]?["email"].ToString();
}
if (result["methodResponses"][0][1]?["notCreated"]?["new-masked-email"] != null)
{
throw new Exception("Fastmail error: " +
result["methodResponses"][0][1]?["created"]?["new-masked-email"]?["description"].ToString());
}
}
else if (result["methodResponses"][0][0].ToString() == "error")
{
throw new Exception("Fastmail error: " + result["methodResponses"][0][1]?["description"].ToString());
}
throw new Exception("Fastmail error: could not parse response.");
}
protected override bool IsRequestSecretInvalid(ApiException ex) => base.IsRequestSecretInvalid(ex) || ex.Error?.StatusCode == System.Net.HttpStatusCode.Forbidden;
}
}

View File

@ -0,0 +1,36 @@
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Bit.Core.Services.EmailForwarders
{
public class FirefoxRelayForwarder : BaseForwarder<ForwarderOptions>
{
protected override string RequestUri => "https://relay.firefox.com/api/v1/relayaddresses/";
protected override void ConfigureHeaders(HttpRequestHeaders headers, ForwarderOptions options)
{
headers.Add("Authorization", $"Token {options.ApiKey}");
}
protected override Task<HttpContent> GetContentAsync(IApiService apiService, ForwarderOptions options)
{
return Task.FromResult<HttpContent>(new StringContent(
JsonConvert.SerializeObject(
new
{
enabled = true,
description = "Generated by Bitwarden."
}), Encoding.UTF8, "application/json"));
}
protected override string HandleResponse(JObject result)
{
return result["full_address"]?.ToString();
}
}
}

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Services.EmailForwarders
{
public class ForwarderOptions
{
public string ApiKey { get; set; }
}
}

View File

@ -0,0 +1,25 @@
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Newtonsoft.Json.Linq;
namespace Bit.Core.Services.EmailForwarders
{
public class SimpleLoginForwarder : BaseForwarder<ForwarderOptions>
{
protected override string RequestUri => "https://app.simplelogin.io/api/alias/random/new";
protected override void ConfigureHeaders(HttpRequestHeaders headers, ForwarderOptions options)
{
headers.Add("Authentication", options.ApiKey);
}
protected override Task<HttpContent> GetContentAsync(IApiService apiService, ForwarderOptions options) => Task.FromResult<HttpContent>(null);
protected override string HandleResponse(JObject result)
{
return result["alias"]?.ToString();
}
}
}

View File

@ -1,7 +1,9 @@
using System.Threading.Tasks;
using System.Text;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Services.EmailForwarders;
using Bit.Core.Utilities;
namespace Bit.Core.Services
@ -12,7 +14,7 @@ namespace Bit.Core.Services
private readonly ICryptoService _cryptoService;
private readonly IApiService _apiService;
private readonly IStateService _stateService;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
private UsernameGenerationOptions _optionsCache;
public UsernameGenerationService(
@ -104,7 +106,7 @@ namespace Bit.Core.Services
if (options.PlusAddressedEmailType == UsernameEmailType.Random)
{
var randomString = await RandomStringAsync(8);
var randomString = await _cryptoService.RandomStringAsync(8);
return options.PlusAddressedEmail.Insert(atIndex, $"+{randomString}");
}
else
@ -124,7 +126,7 @@ namespace Bit.Core.Services
if (options.CatchAllEmailType == UsernameEmailType.Random)
{
var randomString = await RandomStringAsync(8);
var randomString = await _cryptoService.RandomStringAsync(8);
return string.Format(CATCH_ALL_EMAIL_DOMAIN_FORMAT, randomString, catchAllEmailDomain);
}
@ -133,85 +135,34 @@ namespace Bit.Core.Services
private async Task<string> GenerateForwardedEmailAliasAsync(UsernameGenerationOptions options)
{
if (options.ServiceType == ForwardedEmailServiceType.AnonAddy)
{
return await new AnonAddyForwarder()
.GenerateAsync(_apiService, (AnonAddyForwarderOptions)options.GetForwarderOptions());
}
BaseForwarder<ForwarderOptions> simpleForwarder = null;
switch (options.ServiceType)
{
case ForwardedEmailServiceType.AnonAddy:
if (string.IsNullOrWhiteSpace(options.AnonAddyApiAccessToken) || string.IsNullOrWhiteSpace(options.AnonAddyDomainName))
{
return Constants.DefaultUsernameGenerated;
}
return await _apiService.GetUsernameFromAsync(ForwardedEmailServiceType.AnonAddy,
new UsernameGeneratorConfig()
{
ApiToken = options.AnonAddyApiAccessToken,
Domain = options.AnonAddyDomainName,
Url = "https://app.anonaddy.com/api/v1/aliases"
});
case ForwardedEmailServiceType.FirefoxRelay:
if (string.IsNullOrWhiteSpace(options.FirefoxRelayApiAccessToken))
{
return Constants.DefaultUsernameGenerated;
}
return await _apiService.GetUsernameFromAsync(ForwardedEmailServiceType.FirefoxRelay,
new UsernameGeneratorConfig()
{
ApiToken = options.FirefoxRelayApiAccessToken,
Url = "https://relay.firefox.com/api/v1/relayaddresses/"
});
simpleForwarder = new FirefoxRelayForwarder();
break;
case ForwardedEmailServiceType.SimpleLogin:
if (string.IsNullOrWhiteSpace(options.SimpleLoginApiKey))
{
return Constants.DefaultUsernameGenerated;
}
return await _apiService.GetUsernameFromAsync(ForwardedEmailServiceType.SimpleLogin,
new UsernameGeneratorConfig()
{
ApiToken = options.SimpleLoginApiKey,
Url = "https://app.simplelogin.io/api/alias/random/new"
});
simpleForwarder = new SimpleLoginForwarder();
break;
case ForwardedEmailServiceType.DuckDuckGo:
if (string.IsNullOrWhiteSpace(options.DuckDuckGoApiKey))
{
return Constants.DefaultUsernameGenerated;
}
return await _apiService.GetUsernameFromAsync(ForwardedEmailServiceType.DuckDuckGo,
new UsernameGeneratorConfig()
{
ApiToken = options.DuckDuckGoApiKey,
Url = "https://quack.duckduckgo.com/api/email/addresses"
});
simpleForwarder = new DuckDuckGoForwarder();
break;
case ForwardedEmailServiceType.Fastmail:
if (string.IsNullOrWhiteSpace(options.FastMailApiKey))
{
return Constants.DefaultUsernameGenerated;
}
return await _apiService.GetUsernameFromAsync(ForwardedEmailServiceType.Fastmail,
new UsernameGeneratorConfig()
{
ApiToken = options.FastMailApiKey,
Url = "https://api.fastmail.com/jmap/api/"
});
simpleForwarder = new FastmailForwarder();
break;
default:
_logger.Value.Error($"Error UsernameGenerationService: ForwardedEmailServiceType {options.ServiceType} not implemented.");
return Constants.DefaultUsernameGenerated;
}
}
private async Task<string> RandomStringAsync(int length)
{
var str = "";
var charSet = "abcdefghijklmnopqrstuvwxyz1234567890";
for (var i = 0; i < length; i++)
{
var randomCharIndex = await _cryptoService.RandomNumberAsync(0, charSet.Length - 1);
str += charSet[randomCharIndex];
}
return str;
return await simpleForwarder.GenerateAsync(_apiService, options.GetForwarderOptions());
}
private string Capitalize(string str)