1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-18 17:23:18 +00:00

Merge branch 'master' into EC-295-swipe-to-copy-logins

This commit is contained in:
Federico Andrés Maccaroni
2022-09-07 17:41:31 -03:00
29 changed files with 1540 additions and 122 deletions

View File

@@ -22,7 +22,7 @@
## Before you submit ## Before you submit
- [ ] I have checked for formatting errors (`dotnet tool run dotnet-format --check`) (required) - Please check for formatting errors (`dotnet format --verify-no-changes`) (required)
- [ ] I have added **unit tests** where it makes sense to do so (encouraged but not required) - Please add **unit tests** where it makes sense to do so (encouraged but not required)
- [ ] This change requires a **documentation update** (notify the documentation team) - If this change requires a **documentation update** - notify the documentation team
- [ ] This change has particular **deployment requirements** (notify the DevOps team) - If this change has particular **deployment requirements** - notify the DevOps team

View File

@@ -441,10 +441,17 @@ jobs:
- name: Retrieve secrets - name: Retrieve secrets
id: retrieve-secrets id: retrieve-secrets
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f env:
with: KEYVAULT: bitwarden-prod-kv
keyvault: "bitwarden-prod-kv" SECRETS: |
secrets: "appcenter-ios-token" appcenter-ios-token
run: |
for i in ${SECRETS//,/ }
do
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv)
echo "::add-mask::$VALUE"
echo "::set-output name=$i::$VALUE"
done
- name: Decrypt secrets - name: Decrypt secrets
env: env:
@@ -635,10 +642,17 @@ jobs:
- name: Retrieve secrets - name: Retrieve secrets
id: retrieve-secrets id: retrieve-secrets
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f env:
with: KEYVAULT: bitwarden-prod-kv
keyvault: "bitwarden-prod-kv" SECRETS: |
secrets: "crowdin-api-token" crowdin-api-token
run: |
for i in ${SECRETS//,/ }
do
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv)
echo "::add-mask::$VALUE"
echo "::set-output name=$i::$VALUE"
done
- name: Upload Sources - name: Upload Sources
uses: crowdin/github-action@9237b4cb361788dfce63feb2e2f15c09e2fe7415 uses: crowdin/github-action@9237b4cb361788dfce63feb2e2f15c09e2fe7415
@@ -695,11 +709,18 @@ jobs:
- name: Retrieve secrets - name: Retrieve secrets
id: retrieve-secrets id: retrieve-secrets
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
if: failure() if: failure()
with: env:
keyvault: "bitwarden-prod-kv" KEYVAULT: bitwarden-prod-kv
secrets: "devops-alerts-slack-webhook-url" SECRETS: |
devops-alerts-slack-webhook-url
run: |
for i in ${SECRETS//,/ }
do
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv)
echo "::add-mask::$VALUE"
echo "::set-output name=$i::$VALUE"
done
- name: Notify Slack on failure - name: Notify Slack on failure
uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33 uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33

View File

@@ -24,10 +24,17 @@ jobs:
- name: Retrieve secrets - name: Retrieve secrets
id: retrieve-secrets id: retrieve-secrets
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403 env:
with: KEYVAULT: bitwarden-prod-kv
keyvault: "bitwarden-prod-kv" SECRETS: |
secrets: "crowdin-api-token" crowdin-api-token
run: |
for i in ${SECRETS//,/ }
do
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv)
echo "::add-mask::$VALUE"
echo "::set-output name=$i::$VALUE"
done
- name: Download translations - name: Download translations
uses: crowdin/github-action@12143a68c213f3c6d9913c9e5023224f7231face uses: crowdin/github-action@12143a68c213f3c6d9913c9e5023224f7231face

View File

@@ -5,6 +5,7 @@ using System.Threading.Tasks;
using Android.App; using Android.App;
using Android.Content; using Android.Content;
using Android.Content.PM; using Android.Content.PM;
using Android.Content.Res;
using Android.Nfc; using Android.Nfc;
using Android.OS; using Android.OS;
using Android.Runtime; using Android.Runtime;
@@ -18,7 +19,9 @@ using Bit.Core.Enums;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Droid.Receivers; using Bit.Droid.Receivers;
using Bit.Droid.Utilities; using Bit.Droid.Utilities;
using Xamarin.Essentials;
using ZXing.Net.Mobile.Android; using ZXing.Net.Mobile.Android;
using FileProvider = AndroidX.Core.Content.FileProvider;
namespace Bit.Droid namespace Bit.Droid
{ {
@@ -35,6 +38,7 @@ namespace Bit.Droid
private IStateService _stateService; private IStateService _stateService;
private IAppIdService _appIdService; private IAppIdService _appIdService;
private IEventService _eventService; private IEventService _eventService;
private ILogger _logger;
private PendingIntent _eventUploadPendingIntent; private PendingIntent _eventUploadPendingIntent;
private AppOptions _appOptions; private AppOptions _appOptions;
private string _activityKey = $"{nameof(MainActivity)}_{Java.Lang.JavaSystem.CurrentTimeMillis().ToString()}"; private string _activityKey = $"{nameof(MainActivity)}_{Java.Lang.JavaSystem.CurrentTimeMillis().ToString()}";
@@ -56,6 +60,7 @@ namespace Bit.Droid
_stateService = ServiceContainer.Resolve<IStateService>("stateService"); _stateService = ServiceContainer.Resolve<IStateService>("stateService");
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService"); _appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
_eventService = ServiceContainer.Resolve<IEventService>("eventService"); _eventService = ServiceContainer.Resolve<IEventService>("eventService");
_logger = ServiceContainer.Resolve<ILogger>("logger");
TabLayoutResource = Resource.Layout.Tabbar; TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar; ToolbarResource = Resource.Layout.Toolbar;
@@ -70,7 +75,7 @@ namespace Bit.Droid
Window.AddFlags(Android.Views.WindowManagerFlags.Secure); Window.AddFlags(Android.Views.WindowManagerFlags.Secure);
}); });
ServiceContainer.Resolve<ILogger>("logger").InitAsync(); _logger.InitAsync();
var toplayout = Window?.DecorView?.RootView; var toplayout = Window?.DecorView?.RootView;
if (toplayout != null) if (toplayout != null)
@@ -82,7 +87,7 @@ namespace Bit.Droid
Xamarin.Forms.Forms.Init(this, savedInstanceState); Xamarin.Forms.Forms.Init(this, savedInstanceState);
_appOptions = GetOptions(); _appOptions = GetOptions();
LoadApplication(new App.App(_appOptions)); LoadApplication(new App.App(_appOptions));
DisableAndroidFontScale();
_broadcasterService.Subscribe(_activityKey, (message) => _broadcasterService.Subscribe(_activityKey, (message) =>
{ {
@@ -401,5 +406,19 @@ namespace Bit.Droid
alarmManager.Cancel(_eventUploadPendingIntent); alarmManager.Cancel(_eventUploadPendingIntent);
await _eventService.UploadEventsAsync(); await _eventService.UploadEventsAsync();
} }
private void DisableAndroidFontScale()
{
try
{
//As we are using NamedSizes the xamarin will change the font size. So we are disabling the Android scaling.
Resources.Configuration.FontScale = 1f;
BaseContext.Resources.DisplayMetrics.ScaledDensity = Resources.Configuration.FontScale * (float)DeviceDisplay.MainDisplayInfo.Density;
}
catch (Exception e)
{
_logger.Exception(e);
}
}
} }
} }

View File

@@ -1,12 +1,15 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<pages:BaseContentPage <pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms" xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
x:Class="Bit.App.Pages.GeneratorPage" x:Class="Bit.App.Pages.GeneratorPage"
xmlns:pages="clr-namespace:Bit.App.Pages" xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:controls="clr-namespace:Bit.App.Controls" xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities" xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:effects="clr-namespace:Bit.App.Effects" xmlns:effects="clr-namespace:Bit.App.Effects"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
xmlns:enums="clr-namespace:Bit.Core.Enums;assembly=BitwardenCore"
x:DataType="pages:GeneratorPageViewModel" x:DataType="pages:GeneratorPageViewModel"
Title="{Binding PageTitle}"> Title="{Binding PageTitle}">
<ContentPage.BindingContext> <ContentPage.BindingContext>
@@ -16,6 +19,8 @@
<ContentPage.Resources> <ContentPage.Resources>
<ResourceDictionary> <ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" /> <u:InverseBoolConverter x:Key="inverseBool" />
<u:LocalizableEnumConverter x:Key="localizableEnum" />
<xct:EnumToBoolConverter x:Key="enumToBool"/>
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" <ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
x:Name="_closeItem" x:Key="closeItem" /> x:Name="_closeItem" x:Key="closeItem" />
<ToolbarItem Text="{u:I18n Select}" <ToolbarItem Text="{u:I18n Select}"
@@ -42,60 +47,302 @@
in ContentView.--> in ContentView.-->
<ContentView> <ContentView>
<ScrollView Padding="0, 0, 0, 20"> <ScrollView Padding="0, 0, 0, 20">
<StackLayout Spacing="0" Padding="0"> <StackLayout Spacing="0"
<StackLayout StyleClass="box"> Padding="10,0">
<Grid IsVisible="{Binding IsPolicyInEffect}" <Grid IsVisible="{Binding IsPolicyInEffect}"
Margin="0, 12, 0, 0" Margin="0, 12, 0, 0"
RowSpacing="0" Padding="10,0"
ColumnSpacing="0"> RowSpacing="0"
<Grid.RowDefinitions> ColumnSpacing="0">
<RowDefinition Height="Auto" /> <Grid.RowDefinitions>
<RowDefinition Height="*" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> <RowDefinition Height="*" />
<Grid.ColumnDefinitions> </Grid.RowDefinitions>
<ColumnDefinition Width="*" /> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" />
<Frame Padding="10" </Grid.ColumnDefinitions>
Margin="0" <Frame Padding="10"
HasShadow="False" Margin="0"
BackgroundColor="Transparent" HasShadow="False"
BorderColor="Accent"> BackgroundColor="Transparent"
<Label BorderColor="Accent">
Text="{u:I18n PasswordGeneratorPolicyInEffect}" <Label
StyleClass="text-muted, text-sm, text-bold" Text="{u:I18n PasswordGeneratorPolicyInEffect}"
HorizontalTextAlignment="Center" /> StyleClass="text-muted, text-sm, text-bold"
</Frame> HorizontalTextAlignment="Center" />
</Grid> </Frame>
</Grid>
<Grid IsVisible="{Binding IsUsername, Converter={StaticResource inverseBool}}"
StyleClass="box-row"
RowDefinitions="Auto"
ColumnDefinitions="*,Auto,Auto">
<controls:MonoLabel <controls:MonoLabel
x:Name="lblPassword" x:Name="lblPassword"
StyleClass="text-lg, text-html" StyleClass="text-lg, text-html"
Text="{Binding ColoredPassword, Mode=OneWay}" Text="{Binding ColoredPassword, Mode=OneWay}"
Margin="0, 20" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyCommand}"
Grid.Column="1"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyPassword}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
Command="{Binding RegenerateCommand}"
Grid.Column="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n GeneratePassword}" />
</Grid>
<Grid IsVisible="{Binding IsUsername}"
StyleClass="box-row"
RowDefinitions="Auto"
ColumnDefinitions="*,Auto,Auto">
<controls:MonoLabel
x:Name="lblUsername"
StyleClass="text-lg, text-html"
Text="{Binding ColoredUsername, Mode=OneWay}"
Margin="0, 20" Margin="0, 20"
HorizontalTextAlignment="Center" HorizontalOptions="Start" />
HorizontalOptions="CenterAndExpand" <controls:IconButton
LineBreakMode="CharacterWrap" /> StyleClass="box-row-button, box-row-button-platform"
<Button Text="{u:I18n RegeneratePassword}" Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
StyleClass="btn-primary" Command="{Binding CopyCommand}"
HorizontalOptions="FillAndExpand" Grid.Column="1"
Clicked="Regenerate_Clicked"></Button> AutomationProperties.IsInAccessibleTree="True"
<Button Text="{u:I18n CopyPassword}" AutomationProperties.Name="{u:I18n CopyUsername}" />
HorizontalOptions="FillAndExpand" <controls:IconButton
Clicked="Copy_Clicked"></Button> StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
Command="{Binding RegenerateUsernameCommand}"
Grid.Column="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n GenerateUsername}" />
</Grid>
<BoxView StyleClass="box-row-separator"/>
<StackLayout StyleClass="box"
IsVisible="{Binding ShowTypePicker}"
Padding="0,10">
<Label
Text="{u:I18n WhatWouldYouLikeToGenerate}"
StyleClass="box-label" />
<Picker
x:Name="_typePicker"
ItemsSource="{Binding GeneratorTypeOptions, Mode=OneTime}"
SelectedItem="{Binding GeneratorTypeSelected}"
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
StyleClass="box-value" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box"> <Label Text="{u:I18n Options, Header=True}"
<StackLayout StyleClass="box-row-header"> StyleClass="box-header, box-header-platform"
<Label Text="{u:I18n Options, Header=True}" Margin="0,10,0,0"/>
StyleClass="box-header, box-header-platform" /> <!--USERNAME OPTIONS-->
</StackLayout> <StackLayout IsVisible="{Binding IsUsername}">
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout Orientation="Horizontal">
<Label <Label
Text="{u:I18n Type}" Text="{u:I18n UsernameType}"
StyleClass="box-label"
VerticalOptions="Center"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.QuestionCircle}}"
Command="{Binding UsernameTypePromptHelpCommand}"
TextColor="{DynamicResource HyperlinkColor}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n UsernamePromptHelpLink}"
VerticalOptions="Center"/>
</StackLayout>
<Picker
x:Name="_usernameTypePicker"
ItemsSource="{Binding UsernameTypeOptions, Mode=OneTime}"
SelectedItem="{Binding UsernameTypeSelected}"
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
StyleClass="box-value" />
<Label
StyleClass="box-footer-label"
Text="{Binding UsernameTypeDescriptionLabel}" />
<!--PLUS ADDRESSED EMAIL OPTIONS-->
<StackLayout StyleClass="box-row, box-row-input"
IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.PlusAddressedEmail}}">
<Label Text="{u:I18n EmailRequiredParenthesis}"
StyleClass="box-label" />
<Entry x:Name="_plusAddressedEmailEntry"
Text="{Binding PlusAddressedEmail}"
StyleClass="box-value" />
<Label IsVisible="{Binding ShowUsernameEmailType}"
Text="{u:I18n EmailType}"
StyleClass="box-label"
Margin="0,10,0,0"/>
<Picker IsVisible="{Binding ShowUsernameEmailType}"
x:Name="_plusAddressedEmailTypePicker"
ItemsSource="{Binding UsernameEmailTypeOptions, Mode=OneTime}"
SelectedItem="{Binding PlusAddressedEmailTypeSelected}"
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
StyleClass="box-value" />
<Label IsVisible="{Binding ShowUsernameEmailType}"
Text="{u:I18n Website}"
StyleClass="box-label"
Margin="0,10,0,0" />
<Label IsVisible="{Binding ShowUsernameEmailType}"
Text="{Binding EmailWebsite}"
StyleClass="box-value" />
<BoxView IsVisible="{Binding ShowUsernameEmailType}"
StyleClass="box-row-separator"
Margin="0,10,0,0" />
</StackLayout>
<!--CATCH-ALL EMAIL OPTIONS-->
<StackLayout StyleClass="box-row, box-row-input"
IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.CatchAllEmail}}">
<Label
Text="{u:I18n DomainNameRequiredParenthesis}"
StyleClass="box-label" />
<Entry
x:Name="_catchAllEmailDomainNameEntry"
Text="{Binding CatchAllEmailDomain}"
StyleClass="box-value" />
<Label IsVisible="{Binding ShowUsernameEmailType}"
Text="{u:I18n EmailType}"
StyleClass="box-label"
Margin="0,10,0,0"/>
<Picker IsVisible="{Binding ShowUsernameEmailType}"
x:Name="_catchallEmailTypePicker"
ItemsSource="{Binding UsernameEmailTypeOptions, Mode=OneTime}"
SelectedItem="{Binding CatchAllEmailTypeSelected}"
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
StyleClass="box-value" />
<Label IsVisible="{Binding ShowUsernameEmailType}"
Text="{u:I18n Website}"
StyleClass="box-label"
Margin="0,10,0,0" />
<Label IsVisible="{Binding ShowUsernameEmailType}"
Text="{Binding EmailWebsite}"
StyleClass="box-value"/>
<BoxView IsVisible="{Binding ShowUsernameEmailType}"
StyleClass="box-row-separator"
Margin="0,10,0,0"/>
</StackLayout>
<!--FORWARDED EMAIL OPTIONS-->
<StackLayout StyleClass="box-row, box-row-input"
IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.ForwardedEmailAlias}}">
<Label
Text="{u:I18n Service}"
StyleClass="box-label" /> StyleClass="box-label" />
<Picker <Picker
x:Name="_typePicker" x:Name="_serviceTypePicker"
ItemsSource="{Binding TypeOptions, Mode=OneTime}" ItemsSource="{Binding ForwardedEmailServiceTypeOptions, Mode=OneTime}"
SelectedIndex="{Binding TypeSelectedIndex}" SelectedItem="{Binding ForwardedEmailServiceSelected}"
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
StyleClass="box-value" />
<!--ANONADDY OPTIONS-->
<Grid IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
Grid.RowDefinitions="Auto,*"
Grid.ColumnDefinitions="*,Auto">
<Label
Margin="0,10,0,0"
Text="{u:I18n APIAccessToken}"
StyleClass="box-label"/>
<Entry
x:Name="_anonAddyApiAccessTokenEntry"
Text="{Binding AnonAddyApiAccessToken}"
IsPassword="{Binding ShowAnonAddyApiAccessToken, Converter={StaticResource inverseBool}}"
Grid.Row="1"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowAnonAddyHiddenValueIcon}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"/>
</Grid>
<Label IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
Text="{u:I18n DomainNameRequiredParenthesis}"
StyleClass="box-label"
Margin="0,10,0,0"/>
<Entry IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
x:Name="_anonAddyDomainNameEntry"
Text="{Binding AnonAddyDomainName}"
StyleClass="box-value"/>
<!--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}}"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowFirefoxRelayHiddenValueIcon}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"/>
</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}}"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowSimpleLoginHiddenValueIcon}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"/>
</Grid>
</StackLayout>
<!--RANDOM WORD OPTIONS-->
<Grid IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}">
<Label
Text="{u:I18n Capitalize}"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding CapitalizeRandomWordUsername}"
StyleClass="box-value"
HorizontalOptions="End" />
</Grid>
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
StyleClass="box-row-separator" />
<Grid IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}">
<Label
Text="{u:I18n IncludeNumber}"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding IncludeNumberRandomWordUsername}"
StyleClass="box-value"
HorizontalOptions="End" />
</Grid>
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
StyleClass="box-row-separator" />
</StackLayout>
<!--PASSWORD OPTIONS-->
<StackLayout IsVisible="{Binding IsUsername, Converter={StaticResource inverseBool}}">
<StackLayout StyleClass="box-row, box-row-input">
<Label
Text="{u:I18n PasswordType}"
StyleClass="box-label" />
<Picker
x:Name="_passwordTypePicker"
ItemsSource="{Binding PasswordTypeOptions, Mode=OneTime}"
SelectedIndex="{Binding PasswordTypeSelectedIndex}"
StyleClass="box-value" /> StyleClass="box-value" />
</StackLayout> </StackLayout>
<StackLayout Spacing="0" <StackLayout Spacing="0"

View File

@@ -18,7 +18,7 @@ namespace Bit.App.Pages
private readonly Action<string> _selectAction; private readonly Action<string> _selectAction;
private readonly TabsPage _tabsPage; private readonly TabsPage _tabsPage;
public GeneratorPage(bool fromTabPage, Action<string> selectAction = null, TabsPage tabsPage = null) public GeneratorPage(bool fromTabPage, Action<string> selectAction = null, TabsPage tabsPage = null, bool isUsernameGenerator = false, string emailWebsite = null, bool editMode = false)
{ {
_tabsPage = tabsPage; _tabsPage = tabsPage;
InitializeComponent(); InitializeComponent();
@@ -27,6 +27,10 @@ namespace Bit.App.Pages
_vm.Page = this; _vm.Page = this;
_fromTabPage = fromTabPage; _fromTabPage = fromTabPage;
_selectAction = selectAction; _selectAction = selectAction;
_vm.ShowTypePicker = fromTabPage;
_vm.IsUsername = isUsernameGenerator;
_vm.EmailWebsite = emailWebsite;
_vm.EditMode = editMode;
var isIos = Device.RuntimePlatform == Device.iOS; var isIos = Device.RuntimePlatform == Device.iOS;
if (selectAction != null) if (selectAction != null)
{ {
@@ -47,10 +51,12 @@ namespace Bit.App.Pages
ToolbarItems.Add(_historyItem); ToolbarItems.Add(_historyItem);
} }
} }
if (isIos) _typePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
{ _passwordTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_typePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished); _usernameTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
} _serviceTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_plusAddressedEmailTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_catchallEmailTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
} }
public async Task InitAsync() public async Task InitAsync()
@@ -97,16 +103,6 @@ namespace Bit.App.Pages
return base.OnBackButtonPressed(); return base.OnBackButtonPressed();
} }
private async void Regenerate_Clicked(object sender, EventArgs e)
{
await _vm.RegenerateAsync();
}
private async void Copy_Clicked(object sender, EventArgs e)
{
await _vm.CopyAsync();
}
private async void More_Clicked(object sender, EventArgs e) private async void More_Clicked(object sender, EventArgs e)
{ {
if (!DoOnce()) if (!DoOnce())
@@ -124,7 +120,7 @@ namespace Bit.App.Pages
private void Select_Clicked(object sender, EventArgs e) private void Select_Clicked(object sender, EventArgs e)
{ {
_selectAction?.Invoke(_vm.Password); _selectAction?.Invoke(_vm.IsUsername ? _vm.Username : _vm.Password);
} }
private async void History_Clicked(object sender, EventArgs e) private async void History_Clicked(object sender, EventArgs e)
@@ -150,7 +146,20 @@ namespace Bit.App.Pages
{ {
await base.UpdateOnThemeChanged(); await base.UpdateOnThemeChanged();
await Device.InvokeOnMainThreadAsync(() => _vm?.RedrawPassword()); await Device.InvokeOnMainThreadAsync(() =>
{
if (_vm != null)
{
if (_vm.IsUsername)
{
_vm.RedrawUsername();
}
else
{
_vm.RedrawPassword();
}
}
});
} }
} }
} }

View File

@@ -1,10 +1,16 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.App.Utilities; using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain; using Bit.Core.Models.Domain;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
@@ -13,11 +19,15 @@ namespace Bit.App.Pages
private readonly IPasswordGenerationService _passwordGenerationService; private readonly IPasswordGenerationService _passwordGenerationService;
private readonly IPlatformUtilsService _platformUtilsService; private readonly IPlatformUtilsService _platformUtilsService;
private readonly IClipboardService _clipboardService; private readonly IClipboardService _clipboardService;
private readonly IUsernameGenerationService _usernameGenerationService;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private PasswordGenerationOptions _options; private PasswordGenerationOptions _options;
private UsernameGenerationOptions _usernameOptions;
private PasswordGeneratorPolicyOptions _enforcedPolicyOptions; private PasswordGeneratorPolicyOptions _enforcedPolicyOptions;
private string _password; private string _password;
private bool _isPassword; private bool _isPassword;
private bool _isUsername;
private bool _uppercase; private bool _uppercase;
private bool _lowercase; private bool _lowercase;
private bool _number; private bool _number;
@@ -30,21 +40,70 @@ namespace Bit.App.Pages
private string _wordSeparator; private string _wordSeparator;
private bool _capitalize; private bool _capitalize;
private bool _includeNumber; private bool _includeNumber;
private int _typeSelectedIndex; private string _username;
private GeneratorType _generatorTypeSelected;
private int _passwordTypeSelectedIndex;
private bool _doneIniting; private bool _doneIniting;
private bool _showTypePicker;
private string _emailWebsite;
private bool _showFirefoxRelayApiAccessToken;
private bool _showAnonAddyApiAccessToken;
private bool _showSimpleLoginApiKey;
private UsernameEmailType _catchAllEmailTypeSelected;
private UsernameEmailType _plusAddressedEmailTypeSelected;
private bool _editMode;
public GeneratorPageViewModel() public GeneratorPageViewModel()
{ {
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>( _passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>();
"passwordGenerationService"); _platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"); _clipboardService = ServiceContainer.Resolve<IClipboardService>();
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService"); _usernameGenerationService = ServiceContainer.Resolve<IUsernameGenerationService>();
PageTitle = AppResources.PasswordGenerator; PageTitle = AppResources.Generator;
TypeOptions = new List<string> { AppResources.Password, AppResources.Passphrase }; GeneratorTypeOptions = new List<GeneratorType> {
GeneratorType.Password,
GeneratorType.Username
};
PasswordTypeOptions = new List<string> { AppResources.Password, AppResources.Passphrase };
UsernameTypeOptions = new List<UsernameType> {
UsernameType.PlusAddressedEmail,
UsernameType.CatchAllEmail,
UsernameType.ForwardedEmailAlias,
UsernameType.RandomWord
};
ForwardedEmailServiceTypeOptions = new List<ForwardedEmailServiceType> {
ForwardedEmailServiceType.AnonAddy,
ForwardedEmailServiceType.FirefoxRelay,
ForwardedEmailServiceType.SimpleLogin
};
UsernameEmailTypeOptions = new List<UsernameEmailType>
{
UsernameEmailType.Random,
UsernameEmailType.Website
};
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 => OnSubmitException(ex), allowsMultipleExecutions: false);
CopyCommand = new AsyncCommand(CopyAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
} }
public List<string> TypeOptions { get; set; } public List<GeneratorType> GeneratorTypeOptions { get; set; }
public List<string> PasswordTypeOptions { get; set; }
public List<UsernameType> UsernameTypeOptions { get; set; }
public List<ForwardedEmailServiceType> ForwardedEmailServiceTypeOptions { get; set; }
public List<UsernameEmailType> UsernameEmailTypeOptions { get; set; }
public Command UsernameTypePromptHelpCommand { get; set; }
public ICommand RegenerateCommand { get; set; }
public ICommand RegenerateUsernameCommand { get; set; }
public ICommand ToggleForwardedEmailHiddenValueCommand { get; set; }
public ICommand CopyCommand { get; set; }
public string Password public string Password
{ {
@@ -56,7 +115,18 @@ namespace Bit.App.Pages
}); });
} }
public string ColoredPassword => PasswordFormatter.FormatPassword(Password); public string Username
{
get => _username;
set => SetProperty(ref _username, value,
additionalPropertyNames: new string[]
{
nameof(ColoredUsername)
});
}
public string ColoredPassword => GeneratedValueFormatter.Format(Password);
public string ColoredUsername => GeneratedValueFormatter.Format(Username);
public bool IsPassword public bool IsPassword
{ {
@@ -64,6 +134,32 @@ namespace Bit.App.Pages
set => SetProperty(ref _isPassword, value); set => SetProperty(ref _isPassword, value);
} }
public bool IsUsername
{
get => _isUsername;
set => SetProperty(ref _isUsername, value);
}
public bool ShowTypePicker
{
get => _showTypePicker;
set => SetProperty(ref _showTypePicker, value);
}
public bool EditMode
{
get => _editMode;
set => SetProperty(ref _editMode, value, additionalPropertyNames: new string[]
{
nameof(ShowUsernameEmailType)
});
}
public bool ShowUsernameEmailType
{
get => !string.IsNullOrWhiteSpace(EmailWebsite) || EditMode;
}
public int Length public int Length
{ {
get => _length; get => _length;
@@ -235,6 +331,20 @@ namespace Bit.App.Pages
} }
} }
public string PlusAddressedEmail
{
get => _usernameOptions.PlusAddressedEmail;
set
{
if (_usernameOptions.PlusAddressedEmail != value)
{
_usernameOptions.PlusAddressedEmail = value;
TriggerPropertyChanged(nameof(PlusAddressedEmail));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public PasswordGeneratorPolicyOptions EnforcedPolicyOptions public PasswordGeneratorPolicyOptions EnforcedPolicyOptions
{ {
get => _enforcedPolicyOptions; get => _enforcedPolicyOptions;
@@ -247,24 +357,261 @@ namespace Bit.App.Pages
public bool IsPolicyInEffect => _enforcedPolicyOptions.InEffect(); public bool IsPolicyInEffect => _enforcedPolicyOptions.InEffect();
public int TypeSelectedIndex public GeneratorType GeneratorTypeSelected
{ {
get => _typeSelectedIndex; get => _generatorTypeSelected;
set set
{ {
if (SetProperty(ref _typeSelectedIndex, value)) if (SetProperty(ref _generatorTypeSelected, value))
{ {
IsPassword = value == 0; IsUsername = value == GeneratorType.Username;
var task = SaveOptionsAsync(); TriggerPropertyChanged(nameof(GeneratorTypeSelected));
SaveOptionsAsync().FireAndForget();
SaveUsernameOptionsAsync(false).FireAndForget();
} }
} }
} }
public int PasswordTypeSelectedIndex
{
get => _passwordTypeSelectedIndex;
set
{
if (SetProperty(ref _passwordTypeSelectedIndex, value))
{
IsPassword = value == 0;
TriggerPropertyChanged(nameof(PasswordTypeSelectedIndex));
SaveOptionsAsync().FireAndForget();
}
}
}
public UsernameType UsernameTypeSelected
{
get => _usernameOptions.Type;
set
{
if (_usernameOptions.Type != value)
{
_usernameOptions.Type = value;
Username = Constants.DefaultUsernameGenerated;
TriggerPropertyChanged(nameof(UsernameTypeSelected), new string[] { nameof(UsernameTypeDescriptionLabel) });
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public string UsernameTypeDescriptionLabel => GetUsernameTypeLabelDescription(UsernameTypeSelected);
public ForwardedEmailServiceType ForwardedEmailServiceSelected
{
get => _usernameOptions.ServiceType;
set
{
if (_usernameOptions.ServiceType != value)
{
_usernameOptions.ServiceType = value;
Username = Constants.DefaultUsernameGenerated;
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public string CatchAllEmailDomain
{
get => _usernameOptions.CatchAllEmailDomain;
set
{
if (_usernameOptions.CatchAllEmailDomain != value)
{
_usernameOptions.CatchAllEmailDomain = value;
TriggerPropertyChanged(nameof(CatchAllEmailDomain));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public string AnonAddyApiAccessToken
{
get => _usernameOptions.AnonAddyApiAccessToken;
set
{
if (_usernameOptions.AnonAddyApiAccessToken != value)
{
_usernameOptions.AnonAddyApiAccessToken = value;
TriggerPropertyChanged(nameof(AnonAddyApiAccessToken));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public bool ShowAnonAddyApiAccessToken
{
get
{
return _showAnonAddyApiAccessToken;
}
set => SetProperty(ref _showAnonAddyApiAccessToken, value,
additionalPropertyNames: new string[]
{
nameof(ShowAnonAddyHiddenValueIcon)
});
}
public string ShowAnonAddyHiddenValueIcon => _showAnonAddyApiAccessToken ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string AnonAddyDomainName
{
get => _usernameOptions.AnonAddyDomainName;
set
{
if (_usernameOptions.AnonAddyDomainName != value)
{
_usernameOptions.AnonAddyDomainName = value;
TriggerPropertyChanged(nameof(AnonAddyDomainName));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
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,
additionalPropertyNames: new string[]
{
nameof(ShowFirefoxRelayHiddenValueIcon)
});
}
public string ShowFirefoxRelayHiddenValueIcon => _showFirefoxRelayApiAccessToken ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
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,
additionalPropertyNames: new string[]
{
nameof(ShowSimpleLoginHiddenValueIcon)
});
}
public string ShowSimpleLoginHiddenValueIcon => _showSimpleLoginApiKey ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public bool CapitalizeRandomWordUsername
{
get => _usernameOptions.CapitalizeRandomWordUsername;
set
{
if (_usernameOptions.CapitalizeRandomWordUsername != value)
{
_usernameOptions.CapitalizeRandomWordUsername = value;
TriggerPropertyChanged(nameof(CapitalizeRandomWordUsername));
SaveUsernameOptionsAsync().FireAndForget();
}
}
}
public bool IncludeNumberRandomWordUsername
{
get => _usernameOptions.IncludeNumberRandomWordUsername;
set
{
if (_usernameOptions.IncludeNumberRandomWordUsername != value)
{
_usernameOptions.IncludeNumberRandomWordUsername = value;
TriggerPropertyChanged(nameof(IncludeNumberRandomWordUsername));
SaveUsernameOptionsAsync().FireAndForget();
}
}
}
public UsernameEmailType PlusAddressedEmailTypeSelected
{
get => _plusAddressedEmailTypeSelected;
set
{
if (SetProperty(ref _plusAddressedEmailTypeSelected, value))
{
_usernameOptions.PlusAddressedEmailType = value;
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public UsernameEmailType CatchAllEmailTypeSelected
{
get => _catchAllEmailTypeSelected;
set
{
if (SetProperty(ref _catchAllEmailTypeSelected, value))
{
_usernameOptions.CatchAllEmailType = value;
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public string EmailWebsite
{
get => _emailWebsite;
set => SetProperty(ref _emailWebsite, value, additionalPropertyNames: new string[]
{
nameof(ShowUsernameEmailType)
});
}
public async Task InitAsync() public async Task InitAsync()
{ {
(_options, EnforcedPolicyOptions) = await _passwordGenerationService.GetOptionsAsync(); (_options, EnforcedPolicyOptions) = await _passwordGenerationService.GetOptionsAsync();
LoadFromOptions(); LoadFromOptions();
await RegenerateAsync(); await RegenerateAsync();
_usernameOptions = await _usernameGenerationService.GetOptionsAsync();
if (!EditMode)
{
_usernameOptions.CatchAllEmailType = _usernameOptions.PlusAddressedEmailType = UsernameEmailType.Random;
}
TriggerUsernamePropertiesChanged();
Username = Constants.DefaultUsernameGenerated;
_doneIniting = true; _doneIniting = true;
} }
@@ -274,6 +621,11 @@ namespace Bit.App.Pages
await _passwordGenerationService.AddHistoryAsync(Password); await _passwordGenerationService.AddHistoryAsync(Password);
} }
public async Task RegenerateUsernameAsync()
{
Username = await _usernameGenerationService.GenerateAsync(_usernameOptions);
}
public void RedrawPassword() public void RedrawPassword()
{ {
if (!string.IsNullOrEmpty(_password)) if (!string.IsNullOrEmpty(_password))
@@ -282,6 +634,14 @@ namespace Bit.App.Pages
} }
} }
public void RedrawUsername()
{
if (!string.IsNullOrEmpty(_username))
{
TriggerPropertyChanged(nameof(ColoredUsername));
}
}
public async Task SaveOptionsAsync(bool regenerate = true) public async Task SaveOptionsAsync(bool regenerate = true)
{ {
if (!_doneIniting) if (!_doneIniting)
@@ -291,6 +651,7 @@ namespace Bit.App.Pages
SetOptions(); SetOptions();
_passwordGenerationService.NormalizeOptions(_options, _enforcedPolicyOptions); _passwordGenerationService.NormalizeOptions(_options, _enforcedPolicyOptions);
await _passwordGenerationService.SaveOptionsAsync(_options); await _passwordGenerationService.SaveOptionsAsync(_options);
LoadFromOptions(); LoadFromOptions();
if (regenerate) if (regenerate)
{ {
@@ -298,6 +659,21 @@ namespace Bit.App.Pages
} }
} }
public async Task SaveUsernameOptionsAsync(bool regenerate = true)
{
if (!_doneIniting)
{
return;
}
await _usernameGenerationService.SaveOptionsAsync(_usernameOptions);
if (regenerate)
{
await RegenerateUsernameAsync();
}
}
public async Task SliderChangedAsync() public async Task SliderChangedAsync()
{ {
await SaveOptionsAsync(false); await SaveOptionsAsync(false);
@@ -317,15 +693,28 @@ namespace Bit.App.Pages
public async Task CopyAsync() public async Task CopyAsync()
{ {
await _clipboardService.CopyTextAsync(Password); await _clipboardService.CopyTextAsync(IsUsername ? Username : Password);
_platformUtilsService.ShowToastForCopiedValue(AppResources.Password); _platformUtilsService.ShowToastForCopiedValue(IsUsername ? AppResources.Username : AppResources.Password);
}
public void UsernameTypePromptHelp()
{
try
{
_platformUtilsService.LaunchUri("https://bitwarden.com/help/generator/#username-types");
}
catch (Exception ex)
{
_logger.Value.Exception(ex);
Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
}
} }
private void LoadFromOptions() private void LoadFromOptions()
{ {
AllowAmbiguousChars = _options.AllowAmbiguousChar.GetValueOrDefault(); AllowAmbiguousChars = _options.AllowAmbiguousChar.GetValueOrDefault();
TypeSelectedIndex = _options.Type == "passphrase" ? 1 : 0; PasswordTypeSelectedIndex = _options.Type == "passphrase" ? 1 : 0;
IsPassword = TypeSelectedIndex == 0; IsPassword = PasswordTypeSelectedIndex == 0;
MinNumber = _options.MinNumber.GetValueOrDefault(); MinNumber = _options.MinNumber.GetValueOrDefault();
MinSpecial = _options.MinSpecial.GetValueOrDefault(); MinSpecial = _options.MinSpecial.GetValueOrDefault();
Special = _options.Special.GetValueOrDefault(); Special = _options.Special.GetValueOrDefault();
@@ -339,10 +728,30 @@ namespace Bit.App.Pages
IncludeNumber = _options.IncludeNumber.GetValueOrDefault(); IncludeNumber = _options.IncludeNumber.GetValueOrDefault();
} }
private void TriggerUsernamePropertiesChanged()
{
TriggerPropertyChanged(nameof(CatchAllEmailTypeSelected));
TriggerPropertyChanged(nameof(PlusAddressedEmailTypeSelected));
TriggerPropertyChanged(nameof(IncludeNumberRandomWordUsername));
TriggerPropertyChanged(nameof(CapitalizeRandomWordUsername));
TriggerPropertyChanged(nameof(SimpleLoginApiKey));
TriggerPropertyChanged(nameof(FirefoxRelayApiAccessToken));
TriggerPropertyChanged(nameof(AnonAddyDomainName));
TriggerPropertyChanged(nameof(AnonAddyApiAccessToken));
TriggerPropertyChanged(nameof(CatchAllEmailDomain));
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected));
TriggerPropertyChanged(nameof(UsernameTypeSelected));
TriggerPropertyChanged(nameof(PasswordTypeSelectedIndex));
TriggerPropertyChanged(nameof(GeneratorTypeSelected));
TriggerPropertyChanged(nameof(PlusAddressedEmail));
TriggerPropertyChanged(nameof(GeneratorTypeSelected));
TriggerPropertyChanged(nameof(UsernameTypeDescriptionLabel));
}
private void SetOptions() private void SetOptions()
{ {
_options.AllowAmbiguousChar = AllowAmbiguousChars; _options.AllowAmbiguousChar = AllowAmbiguousChars;
_options.Type = TypeSelectedIndex == 1 ? "passphrase" : "password"; _options.Type = PasswordTypeSelectedIndex == 1 ? "passphrase" : "password";
_options.MinNumber = MinNumber; _options.MinNumber = MinNumber;
_options.MinSpecial = MinSpecial; _options.MinSpecial = MinSpecial;
_options.Special = Special; _options.Special = Special;
@@ -355,5 +764,51 @@ namespace Bit.App.Pages
_options.Capitalize = Capitalize; _options.Capitalize = Capitalize;
_options.IncludeNumber = IncludeNumber; _options.IncludeNumber = IncludeNumber;
} }
private async void OnSubmitException(Exception ex)
{
_logger.Value.Exception(ex);
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));
}
}
private string GetUsernameTypeLabelDescription(UsernameType value)
{
switch (value)
{
case UsernameType.PlusAddressedEmail:
return AppResources.PlusAddressedEmailDescription;
case UsernameType.CatchAllEmail:
return AppResources.CatchAllEmailDescription;
case UsernameType.ForwardedEmailAlias:
return AppResources.ForwardedEmailDescription;
default:
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;
}
}
} }
} }

View File

@@ -107,15 +107,26 @@
StyleClass="box-value" /> StyleClass="box-value" />
</StackLayout> </StackLayout>
<StackLayout IsVisible="{Binding IsLogin}" Spacing="0" Padding="0"> <StackLayout IsVisible="{Binding IsLogin}" Spacing="0" Padding="0">
<StackLayout StyleClass="box-row, box-row-input"> <Grid StyleClass="box-row, box-row-input"
<Label RowDefinitions="Auto,*"
ColumnDefinitions="*,Auto">
<Label
Text="{u:I18n Username}" Text="{u:I18n Username}"
StyleClass="box-label" /> StyleClass="box-label"/>
<Entry <Entry
x:Name="_loginUsernameEntry" x:Name="_loginUsernameEntry"
Text="{Binding Cipher.Login.Username}" Text="{Binding Cipher.Login.Username}"
StyleClass="box-value" /> StyleClass="box-value"
</StackLayout> Grid.Row="1"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
Command="{Binding GenerateUsernameCommand}"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n GenerateUsername}" />
</Grid>
<Grid StyleClass="box-row, box-row-input"> <Grid StyleClass="box-row, box-row-input">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />

View File

@@ -84,6 +84,7 @@ namespace Bit.App.Pages
FieldOptionsCommand = new Command<CipherAddEditPageFieldViewModel>(FieldOptions); FieldOptionsCommand = new Command<CipherAddEditPageFieldViewModel>(FieldOptions);
PasswordPromptHelpCommand = new Command(PasswordPromptHelp); PasswordPromptHelpCommand = new Command(PasswordPromptHelp);
CopyCommand = new AsyncCommand(CopyTotpClipboardAsync, onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false); CopyCommand = new AsyncCommand(CopyTotpClipboardAsync, onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
GenerateUsernameCommand = new AsyncCommand(GenerateUsernameAsync, onException: ex => OnGenerateUsernameException(ex), allowsMultipleExecutions: false);
Uris = new ExtendedObservableCollection<LoginUriView>(); Uris = new ExtendedObservableCollection<LoginUriView>();
Fields = new ExtendedObservableCollection<CipherAddEditPageFieldViewModel>(); Fields = new ExtendedObservableCollection<CipherAddEditPageFieldViewModel>();
Collections = new ExtendedObservableCollection<CollectionViewModel>(); Collections = new ExtendedObservableCollection<CollectionViewModel>();
@@ -145,6 +146,7 @@ namespace Bit.App.Pages
public Command FieldOptionsCommand { get; set; } public Command FieldOptionsCommand { get; set; }
public Command PasswordPromptHelpCommand { get; set; } public Command PasswordPromptHelpCommand { get; set; }
public AsyncCommand CopyCommand { get; set; } public AsyncCommand CopyCommand { get; set; }
public AsyncCommand GenerateUsernameCommand { get; set; }
public string CipherId { get; set; } public string CipherId { get; set; }
public string OrganizationId { get; set; } public string OrganizationId { get; set; }
public string FolderId { get; set; } public string FolderId { get; set; }
@@ -592,6 +594,30 @@ namespace Bit.App.Pages
await Page.Navigation.PushModalAsync(new NavigationPage(page)); await Page.Navigation.PushModalAsync(new NavigationPage(page));
} }
public async Task GenerateUsernameAsync()
{
if (!string.IsNullOrWhiteSpace(Cipher?.Login?.Username)
&& !await _platformUtilsService.ShowDialogAsync(AppResources.AreYouSureYouWantToOverwriteTheCurrentUsername, null, AppResources.Yes, AppResources.No))
{
return;
}
var page = new GeneratorPage(false, async (username) =>
{
try
{
Cipher.Login.Username = username;
TriggerCipherChanged();
await Page.Navigation.PopModalAsync();
}
catch (Exception ex)
{
OnGenerateUsernameException(ex);
}
}, isUsernameGenerator: true, emailWebsite: Cipher?.Name, editMode: true);
await Page.Navigation.PushModalAsync(new NavigationPage(page));
}
public async void UriOptions(LoginUriView uri) public async void UriOptions(LoginUriView uri)
{ {
if (!(Page as CipherAddEditPage).DoOnce()) if (!(Page as CipherAddEditPage).DoOnce())
@@ -838,6 +864,12 @@ namespace Bit.App.Pages
_logger.Exception(ex); _logger.Exception(ex);
} }
} }
private async void OnGenerateUsernameException(Exception ex)
{
_logger.Exception(ex);
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
}
} }
public class CipherAddEditPageFieldViewModel : ExtendedViewModel public class CipherAddEditPageFieldViewModel : ExtendedViewModel

View File

@@ -141,7 +141,7 @@ namespace Bit.App.Pages
public bool IsIdentity => Cipher?.Type == Core.Enums.CipherType.Identity; public bool IsIdentity => Cipher?.Type == Core.Enums.CipherType.Identity;
public bool IsCard => Cipher?.Type == Core.Enums.CipherType.Card; public bool IsCard => Cipher?.Type == Core.Enums.CipherType.Card;
public bool IsSecureNote => Cipher?.Type == Core.Enums.CipherType.SecureNote; public bool IsSecureNote => Cipher?.Type == Core.Enums.CipherType.SecureNote;
public FormattedString ColoredPassword => PasswordFormatter.FormatPassword(Cipher.Login.Password); public FormattedString ColoredPassword => GeneratedValueFormatter.Format(Cipher.Login.Password);
public FormattedString UpdatedText public FormattedString UpdatedText
{ {
get get
@@ -751,7 +751,7 @@ namespace Bit.App.Pages
} }
} }
public FormattedString ColoredHiddenValue => PasswordFormatter.FormatPassword(_field.Value); public FormattedString ColoredHiddenValue => GeneratedValueFormatter.Format(_field.Value);
public Command ToggleHiddenValueCommand { get; set; } public Command ToggleHiddenValueCommand { get; set; }

View File

@@ -1,4 +1,4 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// <auto-generated> // <auto-generated>
// This code was generated by a tool. // This code was generated by a tool.
// Runtime Version:4.0.30319.42000 // Runtime Version:4.0.30319.42000
@@ -4150,5 +4150,149 @@ namespace Bit.App.Resources {
return ResourceManager.GetString("AreYouSureYouWantToEnableScreenCapture", resourceCulture); return ResourceManager.GetString("AreYouSureYouWantToEnableScreenCapture", resourceCulture);
} }
} }
public static string PasswordType {
get {
return ResourceManager.GetString("PasswordType", resourceCulture);
}
}
public static string WhatWouldYouLikeToGenerate {
get {
return ResourceManager.GetString("WhatWouldYouLikeToGenerate", resourceCulture);
}
}
public static string UsernameType {
get {
return ResourceManager.GetString("UsernameType", resourceCulture);
}
}
public static string PlusAddressedEmail {
get {
return ResourceManager.GetString("PlusAddressedEmail", resourceCulture);
}
}
public static string CatchAllEmail {
get {
return ResourceManager.GetString("CatchAllEmail", resourceCulture);
}
}
public static string ForwardedEmailAlias {
get {
return ResourceManager.GetString("ForwardedEmailAlias", resourceCulture);
}
}
public static string RandomWord {
get {
return ResourceManager.GetString("RandomWord", resourceCulture);
}
}
public static string EmailRequiredParenthesis {
get {
return ResourceManager.GetString("EmailRequiredParenthesis", resourceCulture);
}
}
public static string DomainNameRequiredParenthesis {
get {
return ResourceManager.GetString("DomainNameRequiredParenthesis", resourceCulture);
}
}
public static string APIKeyRequiredParenthesis {
get {
return ResourceManager.GetString("APIKeyRequiredParenthesis", resourceCulture);
}
}
public static string Service {
get {
return ResourceManager.GetString("Service", resourceCulture);
}
}
public static string AnonAddy {
get {
return ResourceManager.GetString("AnonAddy", resourceCulture);
}
}
public static string FirefoxRelay {
get {
return ResourceManager.GetString("FirefoxRelay", resourceCulture);
}
}
public static string SimpleLogin {
get {
return ResourceManager.GetString("SimpleLogin", resourceCulture);
}
}
public static string APIAccessToken {
get {
return ResourceManager.GetString("APIAccessToken", resourceCulture);
}
}
public static string AreYouSureYouWantToOverwriteTheCurrentUsername {
get {
return ResourceManager.GetString("AreYouSureYouWantToOverwriteTheCurrentUsername", resourceCulture);
}
}
public static string GenerateUsername {
get {
return ResourceManager.GetString("GenerateUsername", resourceCulture);
}
}
public static string EmailType {
get {
return ResourceManager.GetString("EmailType", resourceCulture);
}
}
public static string WebsiteRequired {
get {
return ResourceManager.GetString("WebsiteRequired", resourceCulture);
}
}
public static string UnknownXErrorMessage {
get {
return ResourceManager.GetString("UnknownXErrorMessage", resourceCulture);
}
}
public static string PlusAddressedEmailDescription {
get {
return ResourceManager.GetString("PlusAddressedEmailDescription", resourceCulture);
}
}
public static string CatchAllEmailDescription {
get {
return ResourceManager.GetString("CatchAllEmailDescription", resourceCulture);
}
}
public static string ForwardedEmailDescription {
get {
return ResourceManager.GetString("ForwardedEmailDescription", resourceCulture);
}
}
public static string Random {
get {
return ResourceManager.GetString("Random", resourceCulture);
}
}
} }
} }

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@@ -2314,4 +2314,79 @@ select Add TOTP to store the key safely</value>
<data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve"> <data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve">
<value>Are you sure you want to enable Screen Capture?</value> <value>Are you sure you want to enable Screen Capture?</value>
</data> </data>
<data name="PasswordType" xml:space="preserve">
<value>Password Type</value>
</data>
<data name="WhatWouldYouLikeToGenerate" xml:space="preserve">
<value>What would you like to generate?</value>
</data>
<data name="UsernameType" xml:space="preserve">
<value>Username Type</value>
</data>
<data name="PlusAddressedEmail" xml:space="preserve">
<value>Plus Addressed Email</value>
</data>
<data name="CatchAllEmail" xml:space="preserve">
<value>Catch-all Email</value>
</data>
<data name="ForwardedEmailAlias" xml:space="preserve">
<value>Forwarded Email Alias</value>
</data>
<data name="RandomWord" xml:space="preserve">
<value>Random Word</value>
</data>
<data name="EmailRequiredParenthesis" xml:space="preserve">
<value>Email (required)</value>
</data>
<data name="DomainNameRequiredParenthesis" xml:space="preserve">
<value>Domain Name (required)</value>
</data>
<data name="APIKeyRequiredParenthesis" xml:space="preserve">
<value>API Key (required)</value>
</data>
<data name="Service" xml:space="preserve">
<value>Service</value>
</data>
<data name="AnonAddy" xml:space="preserve">
<value>AnonAddy</value>
<comment>"AnonAddy" is the product name and should not be translated.</comment>
</data>
<data name="FirefoxRelay" xml:space="preserve">
<value>Firefox Relay</value>
<comment>"Firefox Relay" is the product name and should not be translated.</comment>
</data>
<data name="SimpleLogin" xml:space="preserve">
<value>SimpleLogin</value>
<comment>"SimpleLogin" is the product name and should not be translated.</comment>
</data>
<data name="APIAccessToken" xml:space="preserve">
<value>API Access Token</value>
</data>
<data name="AreYouSureYouWantToOverwriteTheCurrentUsername" xml:space="preserve">
<value>Are you sure you want to overwrite the current username?</value>
</data>
<data name="GenerateUsername" xml:space="preserve">
<value>Generate Username</value>
</data>
<data name="EmailType" xml:space="preserve">
<value>Email Type</value>
</data>
<data name="WebsiteRequired" xml:space="preserve">
<value>Website (required)</value>
</data>
<data name="UnknownXErrorMessage" xml:space="preserve">
<value>Unknown {0} error occurred.</value>
</data>
<data name="PlusAddressedEmailDescription" xml:space="preserve">
<value>Use your email provider's subaddress capabilities</value>
</data>
<data name="CatchAllEmailDescription" xml:space="preserve">
<value>Use your domain's configured catch-all inbox.</value>
</data>
<data name="ForwardedEmailDescription" xml:space="preserve">
<value>Generate an email alias with an external forwarding service.</value>
</data>
<data name="Random" xml:space="preserve">
<value>Random</value>
</data>
</root> </root>

View File

@@ -441,6 +441,8 @@ namespace Bit.App.Utilities
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService"); var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
var policyService = ServiceContainer.Resolve<IPolicyService>("policyService"); var policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
var searchService = ServiceContainer.Resolve<ISearchService>("searchService"); var searchService = ServiceContainer.Resolve<ISearchService>("searchService");
var usernameGenerationService = ServiceContainer.Resolve<IUsernameGenerationService>(
"usernameGenerationService");
await Task.WhenAll( await Task.WhenAll(
cipherService.ClearCacheAsync(), cipherService.ClearCacheAsync(),
@@ -454,6 +456,7 @@ namespace Bit.App.Utilities
passwordGenerationService.ClearCache(); passwordGenerationService.ClearCache();
policyService.ClearCache(); policyService.ClearCache();
searchService.ClearIndex(); searchService.ClearIndex();
usernameGenerationService.ClearCache();
} }
} }
} }

View File

@@ -16,7 +16,7 @@ namespace Bit.App.Utilities
{ {
return string.Empty; return string.Empty;
} }
return PasswordFormatter.FormatPassword((string)value); return GeneratedValueFormatter.Format((string)value);
} }
public object ConvertBack(object value, Type targetType, object parameter, public object ConvertBack(object value, Type targetType, object parameter,

View File

@@ -5,14 +5,14 @@ using Xamarin.Forms;
namespace Bit.App.Utilities namespace Bit.App.Utilities
{ {
/** /**
* Helper class to format a password with numeric encoding to separate * Helper class to format a password/username with numeric encoding to separate
* normal text from numbers and special characters. * normal text from numbers and special characters.
*/ */
class PasswordFormatter class GeneratedValueFormatter
{ {
/** /**
* This enum is used for the state machine when building the colorized * This enum is used for the state machine when building the colorized
* password string. * password/username string.
*/ */
private enum CharType private enum CharType
{ {
@@ -22,9 +22,9 @@ namespace Bit.App.Utilities
Special Special
} }
public static string FormatPassword(string password) public static string Format(string generatedValue)
{ {
if (password == null) if (generatedValue == null)
{ {
return string.Empty; return string.Empty;
} }
@@ -37,7 +37,7 @@ namespace Bit.App.Utilities
var result = string.Empty; var result = string.Empty;
// iOS won't hide the zero-width space char without these div attrs, but Android won't respect // iOS won't hide the zero-width space char without these div attrs, but Android won't respect
// display:inline-block and adds a newline after the password. Hence, only iOS gets the div. // display:inline-block and adds a newline after the password/username. Hence, only iOS gets the div.
if (Device.RuntimePlatform == Device.iOS) if (Device.RuntimePlatform == Device.iOS)
{ {
result += "<div style=\"display:inline-block; align-items:center; justify-content:center; text-align:center; word-break:break-all; white-space:pre-wrap; min-width:0\">"; result += "<div style=\"display:inline-block; align-items:center; justify-content:center; text-align:center; word-break:break-all; white-space:pre-wrap; min-width:0\">";
@@ -47,7 +47,7 @@ namespace Bit.App.Utilities
// state. // state.
var currentType = CharType.None; var currentType = CharType.None;
foreach (var c in password) foreach (var c in generatedValue)
{ {
// First, identify what the current char is. // First, identify what the current char is.
CharType charType; CharType charType;

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Models.Domain; using Bit.Core.Models.Domain;
using Bit.Core.Models.Request; using Bit.Core.Models.Request;
using Bit.Core.Models.Response; using Bit.Core.Models.Response;
@@ -82,5 +83,6 @@ namespace Bit.Core.Abstractions
Task<SendResponse> PutSendAsync(string id, SendRequest request); Task<SendResponse> PutSendAsync(string id, SendRequest request);
Task<SendResponse> PutSendRemovePasswordAsync(string id); Task<SendResponse> PutSendRemovePasswordAsync(string id);
Task DeleteSendAsync(string id); Task DeleteSendAsync(string id);
Task<string> GetUsernameFromAsync(ForwardedEmailServiceType service, UsernameGeneratorConfig config);
} }
} }

View File

@@ -151,5 +151,7 @@ namespace Bit.Core.Abstractions
Task<bool> GetScreenCaptureAllowedAsync(string userId = null); Task<bool> GetScreenCaptureAllowedAsync(string userId = null);
Task SetScreenCaptureAllowedAsync(bool value, string userId = null); Task SetScreenCaptureAllowedAsync(bool value, string userId = null);
Task SaveExtensionActiveUserIdToStorageAsync(string userId); Task SaveExtensionActiveUserIdToStorageAsync(string userId);
Task<UsernameGenerationOptions> GetUsernameGenerationOptionsAsync(string userId = null);
Task SetUsernameGenerationOptionsAsync(UsernameGenerationOptions value, string userId = null);
} }
} }

View File

@@ -0,0 +1,13 @@
using System.Threading.Tasks;
using Bit.Core.Models.Domain;
namespace Bit.Core.Abstractions
{
public interface IUsernameGenerationService
{
Task<string> GenerateAsync(UsernameGenerationOptions options);
void ClearCache();
Task<UsernameGenerationOptions> GetOptionsAsync();
Task SaveOptionsAsync(UsernameGenerationOptions options);
}
}

View File

@@ -5,6 +5,7 @@
public const int MaxAccounts = 5; public const int MaxAccounts = 5;
public const string AndroidAppProtocol = "androidapp://"; public const string AndroidAppProtocol = "androidapp://";
public const string iOSAppProtocol = "iosapp://"; public const string iOSAppProtocol = "iosapp://";
public const string DefaultUsernameGenerated = "-";
public static string StateVersionKey = "stateVersion"; public static string StateVersionKey = "stateVersion";
public static string StateKey = "state"; public static string StateKey = "state";
public static string PreAuthEnvironmentUrlsKey = "preAuthEnvironmentUrls"; public static string PreAuthEnvironmentUrlsKey = "preAuthEnvironmentUrls";
@@ -83,5 +84,6 @@
public static string ProtectedPinKey(string userId) => $"protectedPin_{userId}"; public static string ProtectedPinKey(string userId) => $"protectedPin_{userId}";
public static string LastSyncKey(string userId) => $"lastSync_{userId}"; public static string LastSyncKey(string userId) => $"lastSync_{userId}";
public static string BiometricUnlockKey(string userId) => $"biometricUnlock_{userId}"; public static string BiometricUnlockKey(string userId) => $"biometricUnlock_{userId}";
public static string UsernameGenOptionsKey(string userId) => $"usernameGenerationOptions_{userId}";
} }
} }

View File

@@ -0,0 +1,14 @@
using Bit.Core.Attributes;
namespace Bit.Core.Enums
{
public enum ForwardedEmailServiceType
{
[LocalizableEnum("AnonAddy")]
AnonAddy = 0,
[LocalizableEnum("FirefoxRelay")]
FirefoxRelay = 1,
[LocalizableEnum("SimpleLogin")]
SimpleLogin = 2,
}
}

View File

@@ -0,0 +1,12 @@
using Bit.Core.Attributes;
namespace Bit.Core.Enums
{
public enum GeneratorType
{
[LocalizableEnum("Password")]
Password = 0,
[LocalizableEnum("Username")]
Username = 1
}
}

View File

@@ -0,0 +1,12 @@
using Bit.Core.Attributes;
namespace Bit.Core.Enums
{
public enum UsernameEmailType
{
[LocalizableEnum("Random")]
Random = 0,
[LocalizableEnum("Website")]
Website = 1,
}
}

View File

@@ -0,0 +1,16 @@
using Bit.Core.Attributes;
namespace Bit.Core.Enums
{
public enum UsernameType
{
[LocalizableEnum("PlusAddressedEmail")]
PlusAddressedEmail = 0,
[LocalizableEnum("CatchAllEmail")]
CatchAllEmail = 1,
[LocalizableEnum("ForwardedEmailAlias")]
ForwardedEmailAlias = 2,
[LocalizableEnum("RandomWord")]
RandomWord = 3,
}
}

View File

@@ -0,0 +1,23 @@
using Bit.Core.Enums;
namespace Bit.Core.Models.Domain
{
public class UsernameGenerationOptions
{
public UsernameGenerationOptions() { }
public UsernameType Type { get; set; }
public ForwardedEmailServiceType ServiceType { get; set; }
public UsernameEmailType PlusAddressedEmailType { get; set; }
public UsernameEmailType CatchAllEmailType { get; set; }
public bool CapitalizeRandomWordUsername { get; set; }
public bool IncludeNumberRandomWordUsername { get; set; }
public string PlusAddressedEmail { get; set; }
public string CatchAllEmailDomain { get; set; }
public string FirefoxRelayApiAccessToken { get; set; }
public string SimpleLoginApiKey { get; set; }
public string AnonAddyApiAccessToken { get; set; }
public string AnonAddyDomainName { get; set; }
public string EmailWebsite { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
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

@@ -700,6 +700,66 @@ namespace Bit.Core.Services
} }
} }
public async Task<string> GetUsernameFromAsync(ForwardedEmailServiceType service, UsernameGeneratorConfig config)
{
using (var requestMessage = new HttpRequestMessage())
{
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}");
break;
case ForwardedEmailServiceType.SimpleLogin:
requestMessage.Headers.Add("Authentication", config.ApiToken);
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();
default:
return string.Empty;
}
}
}
private ErrorResponse HandleWebError(Exception e) private ErrorResponse HandleWebError(Exception e)
{ {
return new ErrorResponse return new ErrorResponse

View File

@@ -1145,6 +1145,22 @@ namespace Bit.Core.Services
await SetValueAsync(key, value, reconciledOptions); await SetValueAsync(key, value, reconciledOptions);
} }
public async Task<UsernameGenerationOptions> GetUsernameGenerationOptionsAsync(string userId = null)
{
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
await GetDefaultStorageOptionsAsync());
var key = Constants.UsernameGenOptionsKey(reconciledOptions.UserId);
return await GetValueAsync<UsernameGenerationOptions>(key, reconciledOptions);
}
public async Task SetUsernameGenerationOptionsAsync(UsernameGenerationOptions value, string userId = null)
{
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
await GetDefaultStorageOptionsAsync());
var key = Constants.UsernameGenOptionsKey(reconciledOptions.UserId);
await SetValueAsync(key, value, reconciledOptions);
}
public async Task<List<GeneratedPasswordHistory>> GetEncryptedPasswordGenerationHistory(string userId = null) public async Task<List<GeneratedPasswordHistory>> GetEncryptedPasswordGenerationHistory(string userId = null)
{ {
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
@@ -1458,6 +1474,7 @@ namespace Bit.Core.Services
await SetAutoDarkThemeAsync(null, userId); await SetAutoDarkThemeAsync(null, userId);
await SetAddSitePromptShownAsync(null, userId); await SetAddSitePromptShownAsync(null, userId);
await SetPasswordGenerationOptionsAsync(null, userId); await SetPasswordGenerationOptionsAsync(null, userId);
await SetUsernameGenerationOptionsAsync(null, userId);
} }
} }

View File

@@ -0,0 +1,211 @@
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Utilities;
namespace Bit.Core.Services
{
public class UsernameGenerationService : IUsernameGenerationService
{
private const string CATCH_ALL_EMAIL_DOMAIN_FORMAT = "{0}@{1}";
private readonly ICryptoService _cryptoService;
private readonly IApiService _apiService;
private readonly IStateService _stateService;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private UsernameGenerationOptions _optionsCache;
public UsernameGenerationService(
ICryptoService cryptoService,
IApiService apiService,
IStateService stateService)
{
_cryptoService = cryptoService;
_apiService = apiService;
_stateService = stateService;
}
public async Task<string> GenerateAsync(UsernameGenerationOptions options)
{
switch (options.Type)
{
case UsernameType.PlusAddressedEmail:
return await GeneratePlusAddressedEmailAsync(options);
case UsernameType.CatchAllEmail:
return await GenerateCatchAllAsync(options);
case UsernameType.ForwardedEmailAlias:
return await GenerateForwardedEmailAliasAsync(options);
case UsernameType.RandomWord:
return await GenerateRandomWordAsync(options);
default:
_logger.Value.Error($"Error UsernameGenerationService: UsernameType {options.Type} not implemented.");
return string.Empty;
}
}
public async Task<UsernameGenerationOptions> GetOptionsAsync()
{
if (_optionsCache == null)
{
var options = await _stateService.GetUsernameGenerationOptionsAsync();
_optionsCache = options ?? new UsernameGenerationOptions();
}
return _optionsCache;
}
public async Task SaveOptionsAsync(UsernameGenerationOptions options)
{
await _stateService.SetUsernameGenerationOptionsAsync(options);
_optionsCache = options;
}
public void ClearCache()
{
_optionsCache = null;
}
private async Task<string> GenerateRandomWordAsync(UsernameGenerationOptions options)
{
var listLength = EEFLongWordList.Instance.List.Count - 1;
var wordIndex = await _cryptoService.RandomNumberAsync(0, listLength);
var randomWord = EEFLongWordList.Instance.List[wordIndex];
if (string.IsNullOrWhiteSpace(randomWord))
{
_logger.Value.Error($"Error UsernameGenerationService: EEFLongWordList has NullOrWhiteSpace value at {wordIndex} index.");
return Constants.DefaultUsernameGenerated;
}
if (options.CapitalizeRandomWordUsername)
{
randomWord = Capitalize(randomWord);
}
if (options.IncludeNumberRandomWordUsername)
{
randomWord = await AppendRandomNumberToRandomWordAsync(randomWord);
}
return randomWord;
}
private async Task<string> GeneratePlusAddressedEmailAsync(UsernameGenerationOptions options)
{
if (string.IsNullOrWhiteSpace(options.PlusAddressedEmail) || options.PlusAddressedEmail.Length < 3)
{
return Constants.DefaultUsernameGenerated;
}
var atIndex = options.PlusAddressedEmail.IndexOf("@");
if (atIndex < 1 || atIndex >= options.PlusAddressedEmail.Length - 1)
{
return options.PlusAddressedEmail;
}
if (options.PlusAddressedEmailType == UsernameEmailType.Random)
{
var randomString = await RandomStringAsync(8);
return options.PlusAddressedEmail.Insert(atIndex, $"+{randomString}");
}
else
{
return options.PlusAddressedEmail.Insert(atIndex, $"+{options.EmailWebsite}");
}
}
private async Task<string> GenerateCatchAllAsync(UsernameGenerationOptions options)
{
var catchAllEmailDomain = options.CatchAllEmailDomain;
if (string.IsNullOrWhiteSpace(catchAllEmailDomain))
{
return Constants.DefaultUsernameGenerated;
}
if (options.CatchAllEmailType == UsernameEmailType.Random)
{
var randomString = await RandomStringAsync(8);
return string.Format(CATCH_ALL_EMAIL_DOMAIN_FORMAT, randomString, catchAllEmailDomain);
}
return string.Format(CATCH_ALL_EMAIL_DOMAIN_FORMAT, options.EmailWebsite, catchAllEmailDomain);
}
private async Task<string> GenerateForwardedEmailAliasAsync(UsernameGenerationOptions options)
{
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/"
});
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"
});
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;
}
private string Capitalize(string str)
{
return char.ToUpper(str[0]) + str.Substring(1);
}
private async Task<string> AppendRandomNumberToRandomWordAsync(string word)
{
if (string.IsNullOrWhiteSpace(word))
{
return word;
}
var randomNumber = await _cryptoService.RandomNumberAsync(1, 9999);
return word + randomNumber.ToString("0000");
}
}
}

View File

@@ -84,6 +84,7 @@ namespace Bit.Core.Utilities
var eventService = new EventService(apiService, stateService, organizationService, cipherService); var eventService = new EventService(apiService, stateService, organizationService, cipherService);
var userVerificationService = new UserVerificationService(apiService, platformUtilsService, i18nService, var userVerificationService = new UserVerificationService(apiService, platformUtilsService, i18nService,
cryptoService); cryptoService);
var usernameGenerationService = new UsernameGenerationService(cryptoService, apiService, stateService);
Register<ITokenService>("tokenService", tokenService); Register<ITokenService>("tokenService", tokenService);
Register<IApiService>("apiService", apiService); Register<IApiService>("apiService", apiService);
@@ -107,6 +108,7 @@ namespace Bit.Core.Utilities
Register<IEventService>("eventService", eventService); Register<IEventService>("eventService", eventService);
Register<IKeyConnectorService>("keyConnectorService", keyConnectorService); Register<IKeyConnectorService>("keyConnectorService", keyConnectorService);
Register<IUserVerificationService>("userVerificationService", userVerificationService); Register<IUserVerificationService>("userVerificationService", userVerificationService);
Register<IUsernameGenerationService>(usernameGenerationService);
} }
public static void Register<T>(string serviceName, T obj) public static void Register<T>(string serviceName, T obj)