[SG-516] Additional forwarded email providers for username generator - mobile (#2304)

* [SG-516] Added DuckDuckGo provider

* [SG-516] Add Fastmail as generator provider

* [SG-516] code clean up

* [SG-516] Default to service empty if first time on screen. Order services by alphabetic order.

* [SG-516] Removed unnecessary prop.

* [PS-2278] Fixed inverted eye bug.

* [SG-516] Add icon glyph converter

* [SG-516] Fixed enum default value and ordering
This commit is contained in:
André Bispo 2023-01-26 13:53:48 +00:00 committed by GitHub
parent b8d53b0f81
commit 68a6449339
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 264 additions and 26 deletions

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8" ?>
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
@ -20,6 +20,7 @@
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:LocalizableEnumConverter x:Key="localizableEnum" />
<u:IconGlyphConverter x:Key="iconGlyphConverter" />
<xct:EnumToBoolConverter x:Key="enumToBool"/>
<ToolbarItem Text="{u:I18n Cancel}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1"
x:Name="_closeItem" x:Key="closeItem" />
@ -251,7 +252,7 @@
Grid.Row="1"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowAnonAddyHiddenValueIcon}"
Text="{Binding ShowAnonAddyApiAccessToken, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"/>
@ -280,7 +281,7 @@
IsPassword="{Binding ShowFirefoxRelayApiAccessToken, Converter={StaticResource inverseBool}}"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowFirefoxRelayHiddenValueIcon}"
Text="{Binding ShowFirefoxRelayApiAccessToken, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"/>
@ -301,7 +302,49 @@
IsPassword="{Binding ShowSimpleLoginApiKey, Converter={StaticResource inverseBool}}"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowSimpleLoginHiddenValueIcon}"
Text="{Binding ShowSimpleLoginApiKey, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"/>
</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}}"/>
<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"/>
</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}}"/>
<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"/>

View File

@ -52,6 +52,8 @@ namespace Bit.App.Pages
private bool _showFirefoxRelayApiAccessToken;
private bool _showAnonAddyApiAccessToken;
private bool _showSimpleLoginApiKey;
private bool _showDuckDuckGoApiKey;
private bool _showFastmailApiKey;
private bool _editMode;
public GeneratorPageViewModel()
@ -79,6 +81,8 @@ namespace Bit.App.Pages
ForwardedEmailServiceTypeOptions = new List<ForwardedEmailServiceType> {
ForwardedEmailServiceType.AnonAddy,
ForwardedEmailServiceType.DuckDuckGo,
ForwardedEmailServiceType.Fastmail,
ForwardedEmailServiceType.FirefoxRelay,
ForwardedEmailServiceType.SimpleLogin
};
@ -461,15 +465,9 @@ namespace Bit.App.Pages
{
return _showAnonAddyApiAccessToken;
}
set => SetProperty(ref _showAnonAddyApiAccessToken, value,
additionalPropertyNames: new string[]
{
nameof(ShowAnonAddyHiddenValueIcon)
});
set => SetProperty(ref _showAnonAddyApiAccessToken, value);
}
public string ShowAnonAddyHiddenValueIcon => _showAnonAddyApiAccessToken ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string AnonAddyDomainName
{
get => _usernameOptions.AnonAddyDomainName;
@ -504,15 +502,9 @@ namespace Bit.App.Pages
{
return _showFirefoxRelayApiAccessToken;
}
set => SetProperty(ref _showFirefoxRelayApiAccessToken, value,
additionalPropertyNames: new string[]
{
nameof(ShowFirefoxRelayHiddenValueIcon)
});
set => SetProperty(ref _showFirefoxRelayApiAccessToken, value);
}
public string ShowFirefoxRelayHiddenValueIcon => _showFirefoxRelayApiAccessToken ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string SimpleLoginApiKey
{
get => _usernameOptions.SimpleLoginApiKey;
@ -533,14 +525,55 @@ namespace Bit.App.Pages
{
return _showSimpleLoginApiKey;
}
set => SetProperty(ref _showSimpleLoginApiKey, value,
additionalPropertyNames: new string[]
{
nameof(ShowSimpleLoginHiddenValueIcon)
});
set => SetProperty(ref _showSimpleLoginApiKey, value);
}
public string ShowSimpleLoginHiddenValueIcon => _showSimpleLoginApiKey ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
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
{
@ -778,6 +811,8 @@ namespace Bit.App.Pages
TriggerPropertyChanged(nameof(FirefoxRelayApiAccessToken));
TriggerPropertyChanged(nameof(AnonAddyDomainName));
TriggerPropertyChanged(nameof(AnonAddyApiAccessToken));
TriggerPropertyChanged(nameof(DuckDuckGoApiKey));
TriggerPropertyChanged(nameof(FastmailApiKey));
TriggerPropertyChanged(nameof(CatchAllEmailDomain));
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected));
TriggerPropertyChanged(nameof(UsernameTypeSelected));
@ -849,6 +884,12 @@ namespace Bit.App.Pages
case ForwardedEmailServiceType.SimpleLogin:
ShowSimpleLoginApiKey = !ShowSimpleLoginApiKey;
break;
case ForwardedEmailServiceType.DuckDuckGo:
ShowDuckDuckGoApiKey = !ShowDuckDuckGoApiKey;
break;
case ForwardedEmailServiceType.Fastmail:
ShowFastmailApiKey = !ShowFastmailApiKey;
break;
}
}
}

View File

@ -2047,6 +2047,15 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to DuckDuckGo.
/// </summary>
public static string DuckDuckGo {
get {
return ResourceManager.GetString("DuckDuckGo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Edit.
/// </summary>
@ -2542,6 +2551,15 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Fastmail.
/// </summary>
public static string Fastmail {
get {
return ResourceManager.GetString("Fastmail", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Favorite.
/// </summary>

View File

@ -2423,6 +2423,14 @@ select Add TOTP to store the key safely</value>
<value>SimpleLogin</value>
<comment>"SimpleLogin" is the product name and should not be translated.</comment>
</data>
<data name="DuckDuckGo" xml:space="preserve">
<value>DuckDuckGo</value>
<comment>"DuckDuckGo" is the product name and should not be translated.</comment>
</data>
<data name="Fastmail" xml:space="preserve">
<value>Fastmail</value>
<comment>"Fastmail" is the product name and should not be translated.</comment>
</data>
<data name="APIAccessToken" xml:space="preserve">
<value>API access token</value>
</data>

View File

@ -54,7 +54,7 @@ namespace Bit.App.Utilities
case BooleanGlyphType.Checkbox:
return value ? BitwardenIcons.CheckSquare : BitwardenIcons.Square;
case BooleanGlyphType.Eye:
return value ? BitwardenIcons.Eye : BitwardenIcons.EyeSlash;
return value ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
default:
return "";
}

View File

@ -4,11 +4,16 @@ namespace Bit.Core.Enums
{
public enum ForwardedEmailServiceType
{
None = -1,
[LocalizableEnum("AnonAddy")]
AnonAddy = 0,
[LocalizableEnum("FirefoxRelay")]
FirefoxRelay = 1,
[LocalizableEnum("SimpleLogin")]
SimpleLogin = 2,
[LocalizableEnum("DuckDuckGo")]
DuckDuckGo = 3,
[LocalizableEnum("Fastmail")]
Fastmail = 4,
}
}

View File

@ -4,7 +4,10 @@ namespace Bit.Core.Models.Domain
{
public class UsernameGenerationOptions
{
public UsernameGenerationOptions() { }
public UsernameGenerationOptions()
{
ServiceType = ForwardedEmailServiceType.None;
}
public UsernameType Type { get; set; }
public ForwardedEmailServiceType ServiceType { get; set; }
@ -16,6 +19,8 @@ namespace Bit.Core.Models.Domain
public string CatchAllEmailDomain { get; set; }
public string FirefoxRelayApiAccessToken { get; set; }
public string SimpleLoginApiKey { get; set; }
public string DuckDuckGoApiKey { get; set; }
public string FastMailApiKey { get; set; }
public string AnonAddyApiAccessToken { get; set; }
public string AnonAddyDomainName { get; set; }
public string EmailWebsite { get; set; }

View File

@ -760,6 +760,14 @@ namespace Bit.Core.Services
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;
@ -790,12 +798,99 @@ namespace Bit.Core.Services
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;
}
}
}
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)
{
using (var httpclient = new HttpClient())
{
HttpResponseMessage response;
try
{
httpclient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
httpclient.DefaultRequestHeaders.Add("Accept", "application/json");
response = await httpclient.GetAsync(new Uri("https://api.fastmail.com/jmap/session"));
}
catch (Exception e)
{
throw new ApiException(HandleWebError(e));
}
if (!response.IsSuccessStatusCode)
{
throw new ApiException(new ErrorResponse
{
StatusCode = response.StatusCode,
Message = $"Fastmail error: {(int)response.StatusCode} {response.ReasonPhrase}."
});
}
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();
}
}
private ErrorResponse HandleWebError(Exception e)
{
return new ErrorResponse

View File

@ -171,6 +171,29 @@ namespace Bit.Core.Services
ApiToken = options.SimpleLoginApiKey,
Url = "https://app.simplelogin.io/api/alias/random/new"
});
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"
});
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/"
});
default:
_logger.Value.Error($"Error UsernameGenerationService: ForwardedEmailServiceType {options.ServiceType} not implemented.");
return Constants.DefaultUsernameGenerated;