1
0
mirror of https://github.com/bitwarden/mobile synced 2026-01-26 06:13:24 +00:00

Compare commits

...

10 Commits

Author SHA1 Message Date
Matt Portune
c1461ab16b FIDO2 implementation using Google Play Services on Android 2021-08-30 13:38:03 -04:00
Matt Portune
307a5a5843 FIDO2 WebAuthn support for mobile (#1519)
* FIDO2 / WebAuthn support for mobile

* fixes
2021-08-30 12:44:12 -04:00
Joseph Flinn
d050215ebc Simplifying Crowdin sync workflow (#1517) 2021-08-27 10:08:40 -07:00
Matt Portune
67e26c778b Revert "bump MinimumOSVersion to 12.0 (#1508)" (#1515)
This reverts commit 745b54bf2a.
2021-08-25 17:03:33 -04:00
Matt Portune
efb10d155d Revert "enable LLVM in iOS builds (#1510)" (#1514)
This reverts commit 244a6a7f41.
2021-08-25 17:03:02 -04:00
Matt Portune
913c673773 Revert "enable LLVM in iOS extensions (#1511)" (#1513)
This reverts commit 380c3583fb.
2021-08-25 17:02:49 -04:00
Joseph Flinn
339decafe4 Reverting changes to Chinese and Portuguese translations (#1512) 2021-08-24 11:57:34 -07:00
Matt Portune
380c3583fb enable LLVM in iOS extensions (#1511) 2021-08-23 11:40:16 -04:00
Matt Portune
244a6a7f41 enable LLVM in iOS builds (#1510) 2021-08-23 10:16:46 -04:00
Matt Portune
745b54bf2a bump MinimumOSVersion to 12.0 (#1508) 2021-08-20 09:54:39 -04:00
46 changed files with 1048 additions and 4863 deletions

View File

@@ -10,37 +10,12 @@ jobs:
crowdin-sync:
name: Autosync
runs-on: ubuntu-20.04
env:
CROWDIN_BASE_URL: "https://api.crowdin.com/api/v2/projects"
CROWDIN_PROJECT_ID: "269690"
env:
_CROWDIN_PROJECT_ID: "269690"
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Setup git config
run: |
git config user.name "github-actions"
git config user.email "<>"
- name: Get Crowndin Sync Branch
id: branch
run: |
BRANCH_NAME=crowdin-auto-sync
BRANCH_EXISTS=true
git fetch -a
git switch master
if [ $(git branch -a | egrep "remotes/origin/${BRANCH_NAME}$" | wc -l) -eq 0 ]; then
BRANCH_EXISTS=false
git switch -c $BRANCH_NAME
else
git switch $BRANCH_NAME
fi
git branch
echo "::set-output name=branch-exists::$BRANCH_EXISTS"
echo "::set-output name=branch-name::$BRANCH_NAME"
- name: Login to Azure
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
with:
@@ -53,135 +28,21 @@ jobs:
keyvault: "bitwarden-prod-kv"
secrets: "crowdin-api-token"
- name: Get Crowdin master branch id
id: crowdin-master-branch
- name: Download translations
uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea
env:
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
run: |
# Step 1: GET master branchId
BRANCH_ID=$(
curl -s -H "Authorization: Bearer $CROWDIN_API_TOKEN" \
$CROWDIN_BASE_URL/$CROWDIN_PROJECT_ID/branches | jq -r '.data[0].data.id'
)
echo "[*] Crowin master branch id: $BRANCH_ID"
echo "::set-output name=id::$BRANCH_ID"
- name: Get Crowdin build id
id: crowdin-build
env:
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
CROWDIN_MASTER_BRANCH_ID: ${{ steps.crowdin-master-branch.outputs.id }}
run: |
# Step 2: POST Build the translations and get store build id
BUILD_ID=$(
curl -X POST -s \
-H "Authorization: Bearer $CROWDIN_API_TOKEN" \
-H "Content-Type: application/json" \
$CROWDIN_BASE_URL/$CROWDIN_PROJECT_ID/translations/builds \
-d "{\"branchId\": $CROWDIN_MASTER_BRANCH_ID}" | jq -r '.data.id'
)
echo "[*] Crowin translations build id: $BRANCH_ID"
echo "::set-output name=id::$BUILD_ID"
- name: Wait for Crowdin build to finish
env:
MAX_TRIES: 12
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
CROWDIN_BUILD_ID: ${{ steps.crowdin-build.outputs.id }}
run: |
for try in {1..$MAX_TRIES}; do
BRANCH_STATUS=$(
curl -s -H "Authorization: Bearer $CROWDIN_API_TOKEN" \
$CROWDIN_BASE_URL/$CROWDIN_PROJECT_ID/translations/builds/$CROWDIN_BUILD_ID | jq -r '.data.status'
)
echo "[*] Build status: $BRANCH_STATUS"
if [[ "$BRANCH_STATUS" == "finished" ]]; then break; fi
if [[ "$try" == "$MAX_TRIES" ]]; then
echo "[!] Exceeded tries: $try"
exit 1
else
sleep 5
fi
done
- name: Get Crowdin download URL
id: crowdin-download-url
env:
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
CROWDIN_BUILD_ID: ${{ steps.crowdin-build.outputs.id }}
run: |
# Step 4: when build is finished, get download url
DOWNLOAD_URL=$(
curl -s -H "Authorization: Bearer $CROWDIN_API_TOKEN" \
$CROWDIN_BASE_URL/$CROWDIN_PROJECT_ID/translations/builds/$CROWDIN_BUILD_ID/download | jq -r '.data.url'
)
echo "[*] Crowin translations download url: $DOWNLOAD_URL"
echo "::set-output name=value::$DOWNLOAD_URL"
- name: Download Crowdin translations
env:
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
CROWDIN_DOWNLOAD_URL: ${{ steps.crowdin-download-url.outputs.value }}
SAVE_FILE: "translations.zip"
run: |
# Step 5: download the translations via the download url
curl -s $CROWDIN_DOWNLOAD_URL --output $SAVE_FILE
echo "[*] Saved to: $SAVE_FILE"
unzip -o $SAVE_FILE
rm $SAVE_FILE
- name: Check changes
id: files-changed
env:
BRANCH_NAME: ${{ steps.branch.outputs.branch-name }}
BRANCH_EXISTS: ${{ steps.branch.outputs.branch-exists }}
run: |
DIFF_BRANCH=master
if [[ "$BRANCH_EXISTS" == "true" ]]; then
DIFF_BRANCH=$BRANCH_NAME
fi
DIFF_LEN=$(git diff origin/${DIFF_BRANCH} | wc -l | xargs)
echo "[*] git diff lines: ${DIFF_LEN}"
echo "::set-output name=num::$DIFF_LEN"
- name: Commit changes
env:
BRANCH_NAME: ${{ steps.branch.outputs.branch-name }}
BRANCH_EXISTS: ${{ steps.branch.outputs.branch-exists }}
DIFF_BRANCH: master
DIFF_LEN: ${{ steps.files-changed.outputs.num }}
run: |
echo "=====Translations Changed====="
git diff --name-only origin/${DIFF_BRANCH}
echo "=============================="
if [ "$DIFF_LEN" != "0" ]; then
echo "[*] Adding new translations"
git add .
echo "[*] Committing"
git commit -m "Autosync Crowdin translations"
echo "[*] Pushing"
if [ "$BRANCH_EXISTS" == "false" ]; then
git push -u origin $BRANCH_NAME
else
git push
fi
else
echo "[*] No new docs"
fi
- name: Create/Update PR
if: ${{ steps.files-changed.outputs.num }} != 0
env:
BRANCH_NAME: ${{ steps.branch.outputs.branch-name }}
BRANCH_EXISTS: ${{ steps.branch.outputs.branch-exists }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "$BRANCH_EXISTS" == "false" ]; then
echo "[*] Creating PR"
gh pr create --title "Autosync Crowdin Translations" \
--body "Autosync the updated translations"
else
echo "[*] Existing PR updated"
fi
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
with:
config: crowdin.yml
crowdin_branch_name: master
upload_sources: false
upload_translations: false
download_translations: true
github_user_name: "github-actions"
github_user_email: "<>"
commit_message: "Autosync the updated translations"
localization_branch_name: crowdin-auto-sync
create_pull_request: true
pull_request_title: "Autosync Crowdin Translations"
pull_request_body: "Autosync the updated translations"

View File

@@ -1,3 +1,5 @@
project_id_env: _CROWDIN_PROJECT_ID
api_token_env: CROWDIN_API_TOKEN
files:
- source: /src/App/Resources/AppResources.resx
translation: /src/App/Resources/AppResources.%two_letters_code%.resx

View File

@@ -92,6 +92,7 @@
</PackageReference>
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.3.0.1" />
<PackageReference Include="Xamarin.Google.Dagger" Version="2.37.0" />
<PackageReference Include="Xamarin.GooglePlayServices.Fido" Version="118.1.0" />
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet">
<Version>117.0.0</Version>
</PackageReference>
@@ -115,6 +116,8 @@
<Compile Include="Effects\FixedSizeEffect.cs" />
<Compile Include="Effects\SelectableLabelEffect.cs" />
<Compile Include="Effects\TabBarEffect.cs" />
<Compile Include="Fido2System\Fido2BuilderObject.cs" />
<Compile Include="Fido2System\Fido2Service.cs" />
<Compile Include="Push\FirebaseMessagingService.cs" />
<Compile Include="Receivers\ClearClipboardAlarmReceiver.cs" />
<Compile Include="Receivers\RestrictionsChangedReceiver.cs" />

View File

@@ -0,0 +1,106 @@
#if !FDROID
using System.Collections.Generic;
using Android.Gms.Fido.Common;
using Android.Gms.Fido.Fido2.Api.Common;
using Bit.Core.Models.Data;
using Bit.Core.Models.Response;
using Bit.Core.Utilities;
using Java.Lang;
using Newtonsoft.Json.Linq;
namespace Bit.Droid.Fido2System
{
class Fido2BuilderObject
{
public static PublicKeyCredentialRequestOptions ParsePublicKeyCredentialRequestOptions(
Fido2AuthenticationChallengeResponse data)
{
if (data == null)
{
return null;
}
var builder = new PublicKeyCredentialRequestOptions.Builder();
if (!string.IsNullOrEmpty(data.Challenge))
{
builder.SetChallenge(CoreHelpers.Base64UrlDecode(data.Challenge));
}
if (data.AllowCredentials != null && data.AllowCredentials.Count > 0)
{
builder.SetAllowList(ParseCredentialDescriptors(data.AllowCredentials));
}
if (!string.IsNullOrEmpty(data.RpId))
{
builder.SetRpId(data.RpId);
}
if (data.Timeout > 0)
{
builder.SetTimeoutSeconds((Double)(data.Timeout / 1000));
}
if (data.Extensions != null)
{
builder.SetAuthenticationExtensions(ParseExtensions((JObject)data.Extensions));
}
return builder.Build();
}
private static List<PublicKeyCredentialDescriptor> ParseCredentialDescriptors(
List<Fido2CredentialDescriptor> listData)
{
if (listData == null || listData.Count == 0)
{
return new List<PublicKeyCredentialDescriptor>();
}
var credentials = new List<PublicKeyCredentialDescriptor>();
foreach (var data in listData)
{
string id = null;
string type = null;
var transports = new List<Transport>();
if (!string.IsNullOrEmpty(data.Id))
{
id = data.Id;
}
if (!string.IsNullOrEmpty(data.Type))
{
type = data.Type;
}
if (data.Transports != null && data.Transports.Count > 0)
{
foreach (var transport in data.Transports)
{
transports.Add(Transport.FromString(transport));
}
}
credentials.Add(new PublicKeyCredentialDescriptor(type, CoreHelpers.Base64UrlDecode(id), transports));
}
return credentials;
}
private static AuthenticationExtensions ParseExtensions(JObject extensions)
{
var builder = new AuthenticationExtensions.Builder();
if (extensions.ContainsKey("appid"))
{
var appId = new FidoAppIdExtension((string)extensions.GetValue("appid"));
builder.SetFido2Extension(appId);
}
if (extensions.ContainsKey("uvm"))
{
var uvm = new UserVerificationMethodExtension((bool)extensions.GetValue("uvm"));
builder.SetUserVerificationMethodExtension(uvm);
}
return builder.Build();
}
}
}
#endif

View File

@@ -0,0 +1,249 @@
#if !FDROID
using Android.App;
using Android.Content;
using Android.Gms.Fido;
using Android.Gms.Fido.Fido2;
using Android.Gms.Fido.Fido2.Api.Common;
using Android.Gms.Tasks;
using Android.Util;
using AndroidX.AppCompat.App;
using Bit.App.Services;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.Request;
using Bit.Core.Models.Response;
using Bit.Core.Utilities;
using Java.Lang;
using Newtonsoft.Json;
using Xamarin.Forms;
using Enum = System.Enum;
namespace Bit.Droid.Fido2System
{
public class Fido2Service
{
public static readonly string _tag_log = "Fido2Service";
public static Fido2Service INSTANCE = new Fido2Service();
private readonly MobileI18nService _i18nService;
private readonly IPlatformUtilsService _platformUtilsService;
private AppCompatActivity _activity;
private Fido2ApiClient _fido2ApiClient;
private Fido2CodesTypes _fido2CodesType;
public Fido2Service()
{
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService") as MobileI18nService;
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
}
public void Start(AppCompatActivity activity)
{
_activity = activity;
_fido2ApiClient = Fido.GetFido2ApiClient(_activity);
}
public void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
if (resultCode == Result.Ok && Enum.IsDefined(typeof(Fido2CodesTypes), requestCode))
{
switch ((Fido2CodesTypes)requestCode)
{
case Fido2CodesTypes.RequestSignInUser:
var errorExtra = data?.GetByteArrayExtra(Fido.Fido2KeyErrorExtra);
if (errorExtra != null)
{
HandleErrorCode(errorExtra);
}
else
{
if (data != null)
{
SignInUserResponse(data);
}
}
break;
// TODO: Key registration, should we ever choose to implement client-side
/*case Fido2CodesTypes.RequestRegisterNewKey:
errorExtra = data?.GetByteArrayExtra(Fido.Fido2KeyErrorExtra);
if (errorExtra != null)
{
HandleErrorCode(errorExtra);
}
else
{
if (data != null)
{
// begin registration flow
}
}
break;*/
}
}
else if (resultCode == Result.Canceled && Enum.IsDefined(typeof(Fido2CodesTypes), requestCode))
{
Log.Info(_tag_log, "cancelled");
_platformUtilsService.ShowDialogAsync(_i18nService.T("Fido2AbortError"),
_i18nService.T("Fido2Title"));
}
}
public void OnSuccess(Object result)
{
if (result != null && Enum.IsDefined(typeof(Fido2CodesTypes), _fido2CodesType))
{
try
{
_activity.StartIntentSenderForResult(((PendingIntent)result).IntentSender, (int)_fido2CodesType,
null, 0, 0, 0);
}
catch (System.Exception e)
{
Log.Error(_tag_log, e.Message);
_platformUtilsService.ShowDialogAsync(_i18nService.T("Fido2SomethingWentWrong"),
_i18nService.T("Fido2Title"));
}
}
}
public void OnFailure(Exception e)
{
Log.Error(_tag_log, e.Message ?? "OnFailure: No error message returned");
_platformUtilsService.ShowDialogAsync(_i18nService.T("Fido2SomethingWentWrong"),
_i18nService.T("Fido2Title"));
}
public void OnComplete(Task task)
{
Log.Debug(_tag_log, "OnComplete");
}
public async System.Threading.Tasks.Task SignInUserRequestAsync(string dataJson)
{
try
{
var dataObject = JsonConvert.DeserializeObject<Fido2AuthenticationChallengeResponse>(dataJson);
_fido2CodesType = Fido2CodesTypes.RequestSignInUser;
var options = Fido2BuilderObject.ParsePublicKeyCredentialRequestOptions(dataObject);
var task = _fido2ApiClient.GetSignPendingIntent(options);
task.AddOnSuccessListener((IOnSuccessListener)_activity)
.AddOnFailureListener((IOnFailureListener)_activity)
.AddOnCompleteListener((IOnCompleteListener)_activity);
}
catch (System.Exception e)
{
Log.Error(_tag_log, e.StackTrace);
await _platformUtilsService.ShowDialogAsync(_i18nService.T("Fido2SomethingWentWrong"),
_i18nService.T("Fido2Title"));
}
finally
{
Log.Info(_tag_log, "SignInUserRequest() -> finally()");
}
}
private void SignInUserResponse(Intent data)
{
try
{
var response =
AuthenticatorAssertionResponse.DeserializeFromBytes(
data.GetByteArrayExtra(Fido.Fido2KeyResponseExtra));
var responseJson = JsonConvert.SerializeObject(new Fido2AuthenticationChallengeRequest
{
Id = CoreHelpers.Base64UrlEncode(response.GetKeyHandle()),
RawId = CoreHelpers.Base64UrlEncode(response.GetKeyHandle()),
Type = "public-key",
Response = new Fido2AssertionResponse
{
AuthenticatorData = CoreHelpers.Base64UrlEncode(response.GetAuthenticatorData()),
ClientDataJson = CoreHelpers.Base64UrlEncode(response.GetClientDataJSON()),
Signature = CoreHelpers.Base64UrlEncode(response.GetSignature()),
UserHandle = (response.GetUserHandle() != null
? CoreHelpers.Base64UrlEncode(response.GetUserHandle()) : null),
},
Extensions = null
}
);
Device.BeginInvokeOnMainThread(() => ((MainActivity)_activity).Fido2Submission(responseJson));
}
catch (System.Exception e)
{
Log.Error(_tag_log, e.Message);
_platformUtilsService.ShowDialogAsync(_i18nService.T("Fido2SomethingWentWrong"),
_i18nService.T("Fido2Title"));
}
finally
{
Log.Info(_tag_log, "SignInUserResponse() -> finally()");
}
}
public void HandleErrorCode(byte[] errorExtra)
{
var error = AuthenticatorErrorResponse.DeserializeFromBytes(errorExtra);
if (error.ErrorMessage.Length > 0)
{
Log.Info(_tag_log, error.ErrorMessage);
}
string message = "";
if (error.ErrorCode == ErrorCode.AbortErr)
{
message = "Fido2AbortError";
}
else if (error.ErrorCode == ErrorCode.TimeoutErr)
{
message = "Fido2TimeoutError";
}
else if (error.ErrorCode == ErrorCode.AttestationNotPrivateErr)
{
message = "Fido2PrivacyError";
}
else if (error.ErrorCode == ErrorCode.ConstraintErr)
{
message = "Fido2SomethingWentWrong";
}
else if (error.ErrorCode == ErrorCode.DataErr)
{
message = "Fido2ServerDataFail";
}
else if (error.ErrorCode == ErrorCode.EncodingErr)
{
message = "Fido2SomethingWentWrong";
}
else if (error.ErrorCode == ErrorCode.InvalidStateErr)
{
message = "Fido2SomethingWentWrong";
}
else if (error.ErrorCode == ErrorCode.NetworkErr)
{
message = "Fido2NetworkFail";
}
else if (error.ErrorCode == ErrorCode.NotAllowedErr)
{
message = "Fido2NoPermission";
}
else if (error.ErrorCode == ErrorCode.NotSupportedErr)
{
message = "Fido2NotSupportedError";
}
else if (error.ErrorCode == ErrorCode.SecurityErr)
{
message = "Fido2SecurityError";
}
else if (error.ErrorCode == ErrorCode.UnknownErr)
{
message = "Fido2SomethingWentWrong";
}
else
{
message = "Fido2SomethingWentWrong";
}
_platformUtilsService.ShowDialogAsync(_i18nService.T(message), _i18nService.T("Fido2Title"));
}
}
}
#endif

View File

@@ -9,6 +9,7 @@ using Bit.Core.Utilities;
using Bit.Core.Abstractions;
using System.IO;
using System;
using System.Collections.Generic;
using Android.Content;
using Bit.Droid.Utilities;
using Bit.Droid.Receivers;
@@ -17,7 +18,11 @@ using Bit.Core.Enums;
using Android.Nfc;
using Bit.App.Utilities;
using System.Threading.Tasks;
using Android.Util;
using AndroidX.Core.Content;
#if !FDROID
using Bit.Droid.Fido2System;
#endif
using ZXing.Net.Mobile.Android;
namespace Bit.Droid
@@ -42,7 +47,10 @@ namespace Bit.Droid
@"text/*"
})]
[Register("com.x8bit.bitwarden.MainActivity")]
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity,
Android.Gms.Tasks.IOnSuccessListener,
Android.Gms.Tasks.IOnCompleteListener,
Android.Gms.Tasks.IOnFailureListener
{
private IDeviceActionService _deviceActionService;
private IMessagingService _messagingService;
@@ -57,6 +65,7 @@ namespace Bit.Droid
private string _activityKey = $"{nameof(MainActivity)}_{Java.Lang.JavaSystem.CurrentTimeMillis().ToString()}";
private Java.Util.Regex.Pattern _otpPattern =
Java.Util.Regex.Pattern.Compile("^.*?([cbdefghijklnrtuv]{32,64})$");
private string _fidoDataJson;
protected override void OnCreate(Bundle savedInstanceState)
{
@@ -91,6 +100,7 @@ namespace Bit.Droid
#if !FDROID
var appCenterHelper = new AppCenterHelper(_appIdService, _userService);
var appCenterTask = appCenterHelper.InitAsync();
Fido2Service.INSTANCE.Start(this);
#endif
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
@@ -112,6 +122,14 @@ namespace Bit.Droid
{
Xamarin.Forms.Device.BeginInvokeOnMainThread(() => Finish());
}
else if (message.Command == "listenFido2")
{
ListenFido2((Dictionary<string, object>)message.Data);
}
else if (message.Command == "listenFido2TryAgain")
{
ListenFido2();
}
else if (message.Command == "listenYubiKeyOTP")
{
ListenYubiKey((bool)message.Data);
@@ -260,6 +278,34 @@ namespace Bit.Droid
return;
}
}
else if (resultCode == Result.Ok &&
Enum.IsDefined(typeof(Fido2CodesTypes), requestCode))
{
#if !FDROID
Fido2Service.INSTANCE.OnActivityResult(requestCode, resultCode, data);
#endif
}
}
public void OnSuccess(Java.Lang.Object result)
{
#if !FDROID
Fido2Service.INSTANCE.OnSuccess(result);
#endif
}
public void OnComplete(Android.Gms.Tasks.Task task)
{
#if !FDROID
Fido2Service.INSTANCE.OnComplete(task);
#endif
}
public void OnFailure(Java.Lang.Exception e)
{
#if !FDROID
Fido2Service.INSTANCE.OnFailure(e);
#endif
}
protected override void OnDestroy()
@@ -268,6 +314,41 @@ namespace Bit.Droid
_broadcasterService.Unsubscribe(_activityKey);
}
private void ListenFido2(Dictionary<string, object> data = null)
{
if (!_deviceActionService.SupportsFido2())
{
return;
}
#if !FDROID
RunOnUiThread(async () =>
{
try
{
if (data != null)
{
_fidoDataJson = Newtonsoft.Json.JsonConvert.SerializeObject(data);
await Fido2Service.INSTANCE.SignInUserRequestAsync(_fidoDataJson);
}
else
{
await Fido2Service.INSTANCE.SignInUserRequestAsync(_fidoDataJson);
}
}
catch (Exception e)
{
Log.Error(Fido2Service._tag_log, e.Message);
}
});
#endif
}
public void Fido2Submission(string token)
{
_messagingService.Send("gotFido2Token", token);
}
private void ListenYubiKey(bool listen)
{
if (!_deviceActionService.SupportsNfc())

View File

@@ -776,6 +776,17 @@ namespace Bit.Droid.Services
_messagingService.Send("finishMainActivity");
}
public bool SupportsFido2()
{
#if !FDROID
if ((int)Build.VERSION.SdkInt >= 21)
{
return true;
}
#endif
return false;
}
private bool DeleteDir(Java.IO.File dir)
{
if (dir != null && dir.IsDirectory)

View File

@@ -45,5 +45,6 @@ namespace Bit.App.Abstractions
bool UsingDarkTheme();
long GetActiveTime();
void CloseMainApp();
bool SupportsFido2();
}
}

View File

@@ -82,7 +82,7 @@
</Grid>
</StackLayout>
<StackLayout Padding="10, 0">
<Button Text="{u:I18n LogIn}" Clicked="LogIn_Clicked" IsEnabled="{Binding LoginEnabled}"/>
<Button Text="{u:I18n LogIn}" Clicked="LogIn_Clicked" />
</StackLayout>
</StackLayout>
</ScrollView>

View File

@@ -16,6 +16,8 @@ namespace Bit.App.Pages
private readonly LoginPageViewModel _vm;
private readonly AppOptions _appOptions;
private bool _inputFocused;
public LoginPage(string email = null, AppOptions appOptions = null)
{
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
@@ -58,13 +60,10 @@ namespace Bit.App.Pages
{
base.OnAppearing();
await _vm.InitAsync();
if (string.IsNullOrWhiteSpace(_vm.Email))
if (!_inputFocused)
{
RequestFocus(_email);
}
else
{
RequestFocus(_masterPassword);
RequestFocus(string.IsNullOrWhiteSpace(_vm.Email) ? _email : _masterPassword);
_inputFocused = true;
}
}

View File

@@ -33,7 +33,6 @@ namespace Bit.App.Pages
private bool _showPassword;
private string _email;
private string _masterPassword;
private bool _loginEnabled = true;
public LoginPageViewModel()
{
@@ -73,16 +72,6 @@ namespace Bit.App.Pages
set => SetProperty(ref _masterPassword, value);
}
public bool LoginEnabled {
get => _loginEnabled;
set => SetProperty(ref _loginEnabled, value);
}
public bool Loading
{
get => !LoginEnabled;
set => LoginEnabled = !value;
}
public Command LogInCommand { get; }
public Command TogglePasswordCommand { get; }
public string ShowPasswordIcon => ShowPassword ? "" : "";
@@ -106,7 +95,7 @@ namespace Bit.App.Pages
RememberEmail = rememberEmail.GetValueOrDefault(true);
}
public async Task LogInAsync()
public async Task LogInAsync(bool showLoading = true)
{
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
{
@@ -140,10 +129,9 @@ namespace Bit.App.Pages
ShowPassword = false;
try
{
if (!Loading)
if (showLoading)
{
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
Loading = true;
}
var response = await _authService.LogInAsync(Email, MasterPassword, _captchaToken);
@@ -156,25 +144,21 @@ namespace Bit.App.Pages
await _storageService.RemoveAsync(Keys_RememberedEmail);
}
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
await _deviceActionService.HideLoadingAsync();
if (response.CaptchaNeeded)
{
if (await HandleCaptchaAsync(response.CaptchaSiteKey))
{
await LogInAsync();
await LogInAsync(false);
_captchaToken = null;
return;
}
else
{
Loading = false;
return;
}
return;
}
MasterPassword = string.Empty;
_captchaToken = null;
await _deviceActionService.HideLoadingAsync();
if (response.TwoFactor)
{
StartTwoFactorAction?.Invoke();
@@ -198,7 +182,6 @@ namespace Bit.App.Pages
AppResources.AnErrorHasOccurred);
}
}
Loading = false;
}
public void TogglePassword()

View File

@@ -123,43 +123,28 @@ namespace Bit.App.Pages
"domain_hint=" + Uri.EscapeDataString(OrgIdentifier);
WebAuthenticatorResult authResult = null;
bool cancelled = false;
try
{
authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url),
new Uri(redirectUri));
}
catch (TaskCanceledException taskCanceledException)
catch (TaskCanceledException)
{
// user canceled
await _deviceActionService.HideLoadingAsync();
return;
}
var code = GetResultCode(authResult, state);
if (!string.IsNullOrEmpty(code))
{
await LogIn(code, codeVerifier, redirectUri);
}
else
{
await _deviceActionService.HideLoadingAsync();
cancelled = true;
}
catch (Exception e)
{
// WebAuthenticator throws NSErrorException if iOS flow is cancelled - by setting cancelled to true
// here we maintain the appearance of a clean cancellation (we don't want to do this across the board
// because we still want to present legitimate errors). If/when this is fixed, we can remove this
// particular catch block (catching taskCanceledException above must remain)
// https://github.com/xamarin/Essentials/issues/1240
if (Device.RuntimePlatform == Device.iOS)
{
await _deviceActionService.HideLoadingAsync();
cancelled = true;
}
}
if (!cancelled)
{
var code = GetResultCode(authResult, state);
if (!string.IsNullOrEmpty(code))
{
await LogIn(code, codeVerifier, redirectUri);
}
else
{
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
AppResources.AnErrorHasOccurred);
}
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
AppResources.AnErrorHasOccurred);
}
}

View File

@@ -21,7 +21,7 @@
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" IsEnabled="{Binding SubmitEnabled}"/>
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" />
</ContentPage.ToolbarItems>
<ScrollView>

View File

@@ -11,6 +11,8 @@ namespace Bit.App.Pages
private readonly IMessagingService _messagingService;
private readonly RegisterPageViewModel _vm;
private bool _inputFocused;
public RegisterPage(HomePage homePage)
{
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
@@ -45,7 +47,11 @@ namespace Bit.App.Pages
protected override void OnAppearing()
{
base.OnAppearing();
RequestFocus(_email);
if (!_inputFocused)
{
RequestFocus(_email);
_inputFocused = true;
}
}
private async void Submit_Clicked(object sender, EventArgs e)

View File

@@ -22,7 +22,6 @@ namespace Bit.App.Pages
private readonly IPlatformUtilsService _platformUtilsService;
private bool _showPassword;
private bool _acceptPolicies;
private bool _submitEnabled = true;
public RegisterPageViewModel()
{
@@ -60,16 +59,6 @@ namespace Bit.App.Pages
get => _acceptPolicies;
set => SetProperty(ref _acceptPolicies, value);
}
public bool SubmitEnabled
{
get => _submitEnabled;
set => SetProperty(ref _submitEnabled, value);
}
public bool Loading
{
get => !SubmitEnabled;
set => SubmitEnabled = !value;
}
public Thickness SwitchMargin
{
@@ -96,7 +85,7 @@ namespace Bit.App.Pages
protected override IDeviceActionService deviceActionService => _deviceActionService;
protected override IPlatformUtilsService platformUtilsService => _platformUtilsService;
public async Task SubmitAsync()
public async Task SubmitAsync(bool showLoading = true)
{
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
{
@@ -143,6 +132,11 @@ namespace Bit.App.Pages
}
// TODO: Password strength check?
if (showLoading)
{
await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
}
Name = string.IsNullOrWhiteSpace(Name) ? null : Name;
Email = Email.Trim().ToLower();
@@ -172,14 +166,8 @@ namespace Bit.App.Pages
try
{
if (!Loading)
{
await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
Loading = true;
}
await _apiService.PostRegisterAsync(request);
await _deviceActionService.HideLoadingAsync();
Loading = false;
_platformUtilsService.ShowToast("success", null, AppResources.AccountCreated,
new System.Collections.Generic.Dictionary<string, object>
{
@@ -193,19 +181,12 @@ namespace Bit.App.Pages
{
if (await HandleCaptchaAsync(e.Error.CaptchaSiteKey))
{
await SubmitAsync();
await SubmitAsync(false);
_captchaToken = null;
return;
}
else
{
await _deviceActionService.HideLoadingAsync();
Loading = false;
return;
};
return;
}
await _deviceActionService.HideLoadingAsync();
Loading = false;
if (e?.Error != null)
{
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),

View File

@@ -102,6 +102,30 @@
</StackLayout>
</StackLayout>
</StackLayout>
<StackLayout Spacing="20" Padding="0" IsVisible="{Binding Fido2Method, Mode=OneWay}">
<Label
Text="{u:I18n Fido2Instruction}"
Margin="10, 20, 10, 0"
HorizontalTextAlignment="Center" />
<Image
Source="yubikey.png"
Margin="10, 0"
WidthRequest="266"
HeightRequest="160"
HorizontalOptions="Center" />
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="{u:I18n RememberMe}"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding Remember}"
StyleClass="box-value"
HorizontalOptions="End" />
</StackLayout>
</StackLayout>
</StackLayout>
<StackLayout Spacing="0" Padding="0" IsVisible="{Binding DuoMethod, Mode=OneWay}"
VerticalOptions="FillAndExpand">
<controls:HybridWebView

View File

@@ -75,6 +75,18 @@ namespace Bit.App.Pages
});
}
}
else if (message.Command == "gotFido2Token")
{
var token = (string)message.Data;
if (!string.IsNullOrWhiteSpace(token))
{
Device.BeginInvokeOnMainThread(async () =>
{
_vm.Token = token;
await _vm.SubmitAsync();
});
}
}
else if (message.Command == "resumeYubiKey")
{
if (_vm.YubikeyMethod)
@@ -168,11 +180,22 @@ namespace Bit.App.Pages
}
}
private void TryAgain_Clicked(object sender, EventArgs e)
private async void TryAgain_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
if (_vm.YubikeyMethod)
if (_vm.Fido2Method)
{
if (Device.RuntimePlatform == Device.Android)
{
_messagingService.Send("listenFido2TryAgain", true);
}
else
{
await _vm.Fido2AuthenticateAsync();
}
}
else if (_vm.YubikeyMethod)
{
_messagingService.Send("listenYubiKeyOTP", true);
}

View File

@@ -7,9 +7,15 @@ using Bit.Core.Exceptions;
using Bit.Core.Models.Request;
using Bit.Core.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Bit.App.Utilities;
using Newtonsoft.Json;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
@@ -27,7 +33,6 @@ namespace Bit.App.Pages
private readonly IBroadcasterService _broadcasterService;
private readonly IStateService _stateService;
private bool _u2fSupported = false;
private TwoFactorProviderType? _selectedProviderType;
private string _totpInstruction;
private string _webVaultUrl = "https://vault.bitwarden.com";
@@ -65,6 +70,8 @@ namespace Bit.App.Pages
public bool DuoMethod => SelectedProviderType == TwoFactorProviderType.Duo ||
SelectedProviderType == TwoFactorProviderType.OrganizationDuo;
public bool Fido2Method => SelectedProviderType == TwoFactorProviderType.Fido2WebAuthn;
public bool YubikeyMethod => SelectedProviderType == TwoFactorProviderType.YubiKey;
public bool AuthenticatorMethod => SelectedProviderType == TwoFactorProviderType.Authenticator;
@@ -73,7 +80,7 @@ namespace Bit.App.Pages
public bool TotpMethod => AuthenticatorMethod || EmailMethod;
public bool ShowTryAgain => YubikeyMethod && Device.RuntimePlatform == Device.iOS;
public bool ShowTryAgain => (YubikeyMethod && Device.RuntimePlatform == Device.iOS) || Fido2Method;
public bool ShowContinue
{
@@ -97,6 +104,7 @@ namespace Bit.App.Pages
{
nameof(EmailMethod),
nameof(DuoMethod),
nameof(Fido2Method),
nameof(YubikeyMethod),
nameof(AuthenticatorMethod),
nameof(TotpMethod),
@@ -128,10 +136,7 @@ namespace Bit.App.Pages
_webVaultUrl = _environmentService.WebVaultUrl;
}
// TODO: init U2F
_u2fSupported = false;
SelectedProviderType = _authService.GetDefaultTwoFactorProvider(_u2fSupported);
SelectedProviderType = _authService.GetDefaultTwoFactorProvider(_platformUtilsService.SupportsFido2());
Load();
}
@@ -147,8 +152,15 @@ namespace Bit.App.Pages
var providerData = _authService.TwoFactorProvidersData[SelectedProviderType.Value];
switch (SelectedProviderType.Value)
{
case TwoFactorProviderType.U2f:
// TODO
case TwoFactorProviderType.Fido2WebAuthn:
if (Device.RuntimePlatform == Device.Android)
{
_messagingService.Send("listenFido2", providerData);
}
else
{
Fido2AuthenticateAsync(providerData);
}
break;
case TwoFactorProviderType.YubiKey:
_messagingService.Send("listenYubiKeyOTP", true);
@@ -183,10 +195,73 @@ namespace Bit.App.Pages
{
_messagingService.Send("listenYubiKeyOTP", false);
}
ShowContinue = !(SelectedProviderType == null || DuoMethod);
ShowContinue = !(SelectedProviderType == null || DuoMethod || Fido2Method);
}
public async Task SubmitAsync()
public async Task Fido2AuthenticateAsync(Dictionary<string, object> providerData = null)
{
await _deviceActionService.ShowLoadingAsync(AppResources.Validating);
if (providerData == null)
{
providerData = _authService.TwoFactorProvidersData[TwoFactorProviderType.Fido2WebAuthn];
}
var callbackUri = "bitwarden://webauthn-callback";
var data = AppHelpers.EncodeDataParameter(new
{
callbackUri = callbackUri,
data = JsonConvert.SerializeObject(providerData),
btnText = AppResources.Fido2AuthenticateWebAuthn,
});
var url = _webVaultUrl + "/webauthn-mobile-connector.html?" + "data=" + data +
"&parent=" + Uri.EscapeDataString(callbackUri) + "&v=2";
WebAuthenticatorResult authResult = null;
try
{
var options = new WebAuthenticatorOptions
{
Url = new Uri(url),
CallbackUrl = new Uri(callbackUri),
PrefersEphemeralWebBrowserSession = true,
};
authResult = await WebAuthenticator.AuthenticateAsync(options);
}
catch (TaskCanceledException)
{
// user canceled
await _deviceActionService.HideLoadingAsync();
return;
}
string response = null;
if (authResult != null && authResult.Properties.TryGetValue("data", out var resultData))
{
response = Uri.UnescapeDataString(resultData);
}
if (!string.IsNullOrWhiteSpace(response))
{
Token = response;
await SubmitAsync(false);
}
else
{
await _deviceActionService.HideLoadingAsync();
if (authResult != null && authResult.Properties.TryGetValue("error", out var resultError))
{
await _platformUtilsService.ShowDialogAsync(resultError, AppResources.AnErrorHasOccurred);
}
else
{
await _platformUtilsService.ShowDialogAsync(AppResources.Fido2SomethingWentWrong,
AppResources.AnErrorHasOccurred);
}
}
}
public async Task SubmitAsync(bool showLoading = true)
{
if (SelectedProviderType == null)
{
@@ -213,7 +288,10 @@ namespace Bit.App.Pages
try
{
await _deviceActionService.ShowLoadingAsync(AppResources.Validating);
if (showLoading)
{
await _deviceActionService.ShowLoadingAsync(AppResources.Validating);
}
var result = await _authService.LogInTwoFactorAsync(SelectedProviderType.Value, Token, Remember);
var task = Task.Run(() => _syncService.FullSyncAsync(true));
await _deviceActionService.HideLoadingAsync();

View File

@@ -1,13 +1,10 @@
using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Newtonsoft.Json;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
@@ -22,7 +19,7 @@ namespace Bit.App.Pages
protected async Task<bool> HandleCaptchaAsync(string CaptchaSiteKey)
{
var callbackUri = "bitwarden://captcha-callback";
var data = EncodeDataParameter(new
var data = AppHelpers.EncodeDataParameter(new
{
siteKey = CaptchaSiteKey,
locale = i18nService.Culture.TwoLetterISOLanguageName,
@@ -37,27 +34,19 @@ namespace Bit.App.Pages
bool cancelled = false;
try
{
authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url),
new Uri(callbackUri));
var options = new WebAuthenticatorOptions
{
Url = new Uri(url),
CallbackUrl = new Uri(callbackUri),
PrefersEphemeralWebBrowserSession = true,
};
authResult = await WebAuthenticator.AuthenticateAsync(options);
}
catch (TaskCanceledException)
{
await deviceActionService.HideLoadingAsync();
cancelled = true;
}
catch (Exception e)
{
// WebAuthenticator throws NSErrorException if iOS flow is cancelled - by setting cancelled to true
// here we maintain the appearance of a clean cancellation (we don't want to do this across the board
// because we still want to present legitimate errors). If/when this is fixed, we can remove this
// particular catch block (catching taskCanceledException above must remain)
// https://github.com/xamarin/Essentials/issues/1240
if (Device.RuntimePlatform == Device.iOS)
{
await deviceActionService.HideLoadingAsync();
cancelled = true;
}
}
if (cancelled == false && authResult != null &&
authResult.Properties.TryGetValue("token", out _captchaToken))
@@ -71,18 +60,5 @@ namespace Bit.App.Pages
return false;
}
}
private string EncodeDataParameter(object obj)
{
string EncodeMultibyte(Match match)
{
return Convert.ToChar(Convert.ToUInt32($"0x{match.Groups[1].Value}", 16)).ToString();
}
var escaped = Uri.EscapeDataString(JsonConvert.SerializeObject(obj));
var multiByteEscaped = Regex.Replace(escaped, "%([0-9A-F]{2})", EncodeMultibyte);
return Convert.ToBase64String(Encoding.UTF8.GetBytes(multiByteEscaped));
}
}
}

View File

@@ -137,8 +137,6 @@
AutomationProperties.Name="{u:I18n File}"
Grid.Column="0">
<VisualStateManager.VisualStateGroups>
<!-- Rider users, if the x:Name values below are red, it's a known issue: -->
<!-- https://youtrack.jetbrains.com/issue/RSRP-479388 -->
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
@@ -163,8 +161,6 @@
AutomationProperties.Name="{u:I18n Text}"
Grid.Column="1">
<VisualStateManager.VisualStateGroups>
<!-- Rider users, if the x:Name values below are red, it's a known issue: -->
<!-- https://youtrack.jetbrains.com/issue/RSRP-479388 -->
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>

View File

@@ -1,7 +1,6 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@@ -10,7 +9,6 @@
namespace Bit.App.Resources {
using System;
using System.Reflection;
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
@@ -3562,5 +3560,83 @@ namespace Bit.App.Resources {
return ResourceManager.GetString("CaptchaFailed", resourceCulture);
}
}
public static string Fido2Title {
get {
return ResourceManager.GetString("Fido2Title", resourceCulture);
}
}
public static string Fido2Instruction {
get {
return ResourceManager.GetString("Fido2Instruction", resourceCulture);
}
}
public static string Fido2Desc {
get {
return ResourceManager.GetString("Fido2Desc", resourceCulture);
}
}
public static string Fido2AuthenticateWebAuthn {
get {
return ResourceManager.GetString("Fido2AuthenticateWebAuthn", resourceCulture);
}
}
public static string Fido2SomethingWentWrong {
get {
return ResourceManager.GetString("Fido2SomethingWentWrong", resourceCulture);
}
}
public static string Fido2AbortError {
get {
return ResourceManager.GetString("Fido2AbortError", resourceCulture);
}
}
public static string Fido2NetworkFail {
get {
return ResourceManager.GetString("Fido2NetworkFail", resourceCulture);
}
}
public static string Fido2NoPermission {
get {
return ResourceManager.GetString("Fido2NoPermission", resourceCulture);
}
}
public static string Fido2NotSupportedError {
get {
return ResourceManager.GetString("Fido2NotSupportedError", resourceCulture);
}
}
public static string Fido2PrivacyError {
get {
return ResourceManager.GetString("Fido2PrivacyError", resourceCulture);
}
}
public static string Fido2SecurityError {
get {
return ResourceManager.GetString("Fido2SecurityError", resourceCulture);
}
}
public static string Fido2ServerDataFail {
get {
return ResourceManager.GetString("Fido2ServerDataFail", resourceCulture);
}
}
public static string Fido2TimeoutError {
get {
return ResourceManager.GetString("Fido2TimeoutError", resourceCulture);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2016,4 +2016,43 @@
<data name="CaptchaFailed" xml:space="preserve">
<value>Captcha Failed. Please try again.</value>
</data>
<data name="Fido2Title" xml:space="preserve">
<value>FIDO2 WebAuthn</value>
</data>
<data name="Fido2Instruction" xml:space="preserve">
<value>To continue, have your FIDO2 WebAuthn enabled security key ready, then follow the instructions after clicking 'Authenticate WebAuthn' on the next screen.</value>
</data>
<data name="Fido2Desc" xml:space="preserve">
<value>Authentication using FIDO2 WebAuthn, you can authenticate using an external security key.</value>
</data>
<data name="Fido2AuthenticateWebAuthn" xml:space="preserve">
<value>Authenticate WebAuthn</value>
</data>
<data name="Fido2SomethingWentWrong" xml:space="preserve">
<value>Something Went Wrong. Try again.</value>
</data>
<data name="Fido2AbortError" xml:space="preserve">
<value>Aborted FIDO2 operation. Try again.</value>
</data>
<data name="Fido2NetworkFail" xml:space="preserve">
<value>No internet connection. Try again.</value>
</data>
<data name="Fido2NoPermission" xml:space="preserve">
<value>Permission was not given. Try again.</value>
</data>
<data name="Fido2NotSupportedError" xml:space="preserve">
<value>Unsupported device.</value>
</data>
<data name="Fido2PrivacyError" xml:space="preserve">
<value>Privacy issues encountered. Try again.</value>
</data>
<data name="Fido2SecurityError" xml:space="preserve">
<value>Security issues encountered.</value>
</data>
<data name="Fido2ServerDataFail" xml:space="preserve">
<value>The server returned invalid data. Try again.</value>
</data>
<data name="Fido2TimeoutError" xml:space="preserve">
<value>Timeout for FIDO2. Try again</value>
</data>
</root>

File diff suppressed because it is too large Load Diff

View File

@@ -129,9 +129,9 @@ namespace Bit.App.Services
return true;
}
public bool SupportsU2f()
public bool SupportsFido2()
{
return false;
return _deviceActionService.SupportsFido2();
}
public void ShowToast(string type, string title, string text, Dictionary<string, object> options = null)

View File

@@ -8,10 +8,13 @@ using Bit.Core.Models.View;
using Bit.Core.Utilities;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Bit.App.Models;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Newtonsoft.Json;
using Xamarin.Essentials;
using Xamarin.Forms;
@@ -470,5 +473,17 @@ namespace Bit.App.Utilities
}
return false;
}
public static string EncodeDataParameter(object obj)
{
string EncodeMultibyte(Match match)
{
return Convert.ToChar(Convert.ToUInt32($"0x{match.Groups[1].Value}", 16)).ToString();
}
var escaped = Uri.EscapeDataString(JsonConvert.SerializeObject(obj));
var multiByteEscaped = Regex.Replace(escaped, "%([0-9A-F]{2})", EncodeMultibyte);
return Convert.ToBase64String(Encoding.UTF8.GetBytes(multiByteEscaped));
}
}
}

View File

@@ -17,7 +17,7 @@ namespace Bit.Core.Abstractions
Dictionary<TwoFactorProviderType, TwoFactorProvider> TwoFactorProviders { get; set; }
Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProvidersData { get; set; }
TwoFactorProviderType? GetDefaultTwoFactorProvider(bool u2fSupported);
TwoFactorProviderType? GetDefaultTwoFactorProvider(bool fido2Supported);
bool AuthingWithSso();
bool AuthingWithPassword();
List<TwoFactorProvider> GetSupportedTwoFactorProviders();

View File

@@ -25,7 +25,7 @@ namespace Bit.Core.Abstractions
Task<bool> ShowPasswordDialogAsync(string title, string body, Func<string, Task<bool>> validator);
void ShowToast(string type, string title, string text, Dictionary<string, object> options = null);
void ShowToast(string type, string title, string[] text, Dictionary<string, object> options = null);
bool SupportsU2f();
bool SupportsFido2();
bool SupportsDuo();
Task<bool> SupportsBiometricAsync();
Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null);

View File

@@ -0,0 +1,8 @@
namespace Bit.Core.Enums
{
public enum Fido2CodesTypes
{
RequestSignInUser = 994,
RequestRegisterNewKey = 995,
}
}

View File

@@ -8,6 +8,7 @@
YubiKey = 3,
U2f = 4,
Remember = 5,
OrganizationDuo = 6
OrganizationDuo = 6,
Fido2WebAuthn = 7,
}
}

View File

@@ -0,0 +1,16 @@
using Newtonsoft.Json;
namespace Bit.Core.Models.Data
{
public class Fido2AssertionResponse : Data
{
[JsonProperty("authenticatorData")]
public string AuthenticatorData { get; set; }
[JsonProperty("signature")]
public string Signature { get; set; }
[JsonProperty("clientDataJson")]
public string ClientDataJson { get; set; }
[JsonProperty("userHandle")]
public string UserHandle { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using Newtonsoft.Json;
namespace Bit.Core.Models.Data
{
public class Fido2AuthenticatorSelection : Data
{
[JsonProperty("authenticatorAttachment")]
public string AuthenticatorAttachment { get; set; }
[JsonProperty("userVerification")]
public string UserVerification { get; set; }
[JsonProperty("requireResidentKey")]
public string RequireResidentKey { get; set; }
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Bit.Core.Models.Data
{
public class Fido2CredentialDescriptor : Data
{
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("transports", NullValueHandling = NullValueHandling.Ignore)]
public List<string> Transports { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
using Newtonsoft.Json;
namespace Bit.Core.Models.Data
{
public class Fido2PubKeyCredParam : Data
{
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("alg")]
public int Alg { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using Newtonsoft.Json;
namespace Bit.Core.Models.Data
{
public class Fido2RP : Data
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("icon")]
public string Icon { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
using Newtonsoft.Json;
namespace Bit.Core.Models.Data
{
public class Fido2User : Data
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("displayName")]
public string DisplayName { get; set; }
[JsonProperty("icon")]
public string Icon { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
using Bit.Core.Models.Data;
using Newtonsoft.Json;
namespace Bit.Core.Models.Request
{
public class Fido2AuthenticationChallengeRequest
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("rawId")]
public string RawId { get; set; }
[JsonProperty("response")]
public Fido2AssertionResponse Response { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("extensions", NullValueHandling = NullValueHandling.Ignore)]
public string Extensions { get; set; }
}
}

View File

@@ -0,0 +1,22 @@
using System.Collections.Generic;
using Bit.Core.Models.Data;
using Newtonsoft.Json;
namespace Bit.Core.Models.Response
{
public class Fido2AuthenticationChallengeResponse
{
[JsonProperty("challenge")]
public string Challenge { get; set; }
[JsonProperty("rpId")]
public string RpId { get; set; }
[JsonProperty("timeout")]
public double Timeout { get; set; }
[JsonProperty("allowCredentials")]
public List<Fido2CredentialDescriptor> AllowCredentials { get; set; }
[JsonProperty("userVerification")]
public string UserVerification { get; set; }
[JsonProperty("extensions", NullValueHandling = NullValueHandling.Ignore)]
public object Extensions { get; set; }
}
}

View File

@@ -0,0 +1,28 @@
using System.Collections.Generic;
using Bit.Core.Models.Data;
using Newtonsoft.Json;
namespace Bit.Core.Models.Response
{
public class Fido2RegistrationChallengeResponse
{
[JsonProperty("challenge")]
public string Challenge { get; set; }
[JsonProperty("timeout")]
public double Timeout { get; set; }
[JsonProperty("rp")]
public Fido2RP Rp { get; set; }
[JsonProperty("user")]
public Fido2User User { get; set; }
[JsonProperty("pubKeyCredParams")]
public List<Fido2PubKeyCredParam> PubKeyCredParams { get; set; }
[JsonProperty("excludeCredentials")]
public List<Fido2CredentialDescriptor> ExcludeCredentials { get; set; }
[JsonProperty("authenticatorSelection")]
public Fido2AuthenticatorSelection AuthenticatorSelection { get; set; }
[JsonProperty("attestation", NullValueHandling = NullValueHandling.Ignore)]
public object Attestation { get; set; }
[JsonProperty("extensions", NullValueHandling = NullValueHandling.Ignore)]
public object Extensions { get; set; }
}
}

View File

@@ -76,9 +76,9 @@ namespace Bit.Core.Services
Priority = 10,
Sort = 4
});
TwoFactorProviders.Add(TwoFactorProviderType.U2f, new TwoFactorProvider
TwoFactorProviders.Add(TwoFactorProviderType.Fido2WebAuthn, new TwoFactorProvider
{
Type = TwoFactorProviderType.U2f,
Type = TwoFactorProviderType.Fido2WebAuthn,
Priority = 4,
Sort = 5,
Premium = true
@@ -114,8 +114,8 @@ namespace Bit.Core.Services
string.Format("Duo ({0})", _i18nService.T("Organization"));
TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].Description =
_i18nService.T("DuoOrganizationDesc");
TwoFactorProviders[TwoFactorProviderType.U2f].Name = _i18nService.T("U2fTitle");
TwoFactorProviders[TwoFactorProviderType.U2f].Description = _i18nService.T("U2fDesc");
TwoFactorProviders[TwoFactorProviderType.Fido2WebAuthn].Name = _i18nService.T("Fido2Title");
TwoFactorProviders[TwoFactorProviderType.Fido2WebAuthn].Description = _i18nService.T("Fido2Desc");
TwoFactorProviders[TwoFactorProviderType.YubiKey].Name = _i18nService.T("YubiKeyTitle");
TwoFactorProviders[TwoFactorProviderType.YubiKey].Description = _i18nService.T("YubiKeyDesc");
}
@@ -192,9 +192,10 @@ namespace Bit.Core.Services
{
providers.Add(TwoFactorProviders[TwoFactorProviderType.Duo]);
}
if (TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.U2f) && _platformUtilsService.SupportsU2f())
if (TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.Fido2WebAuthn) &&
_platformUtilsService.SupportsFido2())
{
providers.Add(TwoFactorProviders[TwoFactorProviderType.U2f]);
providers.Add(TwoFactorProviders[TwoFactorProviderType.Fido2WebAuthn]);
}
if (TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.Email))
{
@@ -203,7 +204,7 @@ namespace Bit.Core.Services
return providers;
}
public TwoFactorProviderType? GetDefaultTwoFactorProvider(bool u2fSupported)
public TwoFactorProviderType? GetDefaultTwoFactorProvider(bool fido2Supported)
{
if (TwoFactorProvidersData == null)
{
@@ -223,7 +224,7 @@ namespace Bit.Core.Services
var provider = TwoFactorProviders[providerKvp.Key];
if (provider.Priority > providerPriority)
{
if (providerKvp.Key == TwoFactorProviderType.U2f && !u2fSupported)
if (providerKvp.Key == TwoFactorProviderType.Fido2WebAuthn && !fido2Supported)
{
continue;
}

View File

@@ -88,7 +88,8 @@ namespace Bit.iOS.Core.Services
var loadingIndicator = new UIActivityIndicatorView(new CGRect(10, 5, 50, 50));
loadingIndicator.HidesWhenStopped = true;
loadingIndicator.ActivityIndicatorViewStyle = UIActivityIndicatorViewStyle.Gray;
loadingIndicator.ActivityIndicatorViewStyle = ThemeHelpers.LightTheme ? UIActivityIndicatorViewStyle.Gray :
UIActivityIndicatorViewStyle.White;
loadingIndicator.StartAnimating();
_progressAlert = UIAlertController.Create(null, text, UIAlertControllerStyle.Alert);
@@ -446,6 +447,27 @@ namespace Bit.iOS.Core.Services
throw new NotImplementedException();
}
public bool SupportsFido2()
{
// FIDO2 WebAuthn supported on 13.3+
var versionParts = UIDevice.CurrentDevice.SystemVersion.Split('.');
if (versionParts.Length > 0 && int.TryParse(versionParts[0], out var version))
{
if (version == 13)
{
if (versionParts.Length > 1 && int.TryParse(versionParts[1], out var minorVersion))
{
return minorVersion >= 3;
}
}
else if (version > 13)
{
return true;
}
}
return false;
}
private void ImagePicker_FinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs e)
{
if (sender is UIImagePickerController picker)

View File

@@ -239,6 +239,16 @@ namespace Bit.iOS
return base.OpenUrl(app, url, options);
}
public override bool ContinueUserActivity(UIApplication application, NSUserActivity userActivity,
UIApplicationRestorationHandler completionHandler)
{
if (Xamarin.Essentials.Platform.ContinueUserActivity(application, userActivity, completionHandler))
{
return true;
}
return base.ContinueUserActivity(application, userActivity, completionHandler);
}
public override void FailedToRegisterForRemoteNotifications(UIApplication application, NSError error)
{
_pushHandler?.OnErrorReceived(error);

View File

@@ -1,153 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Name" xml:space="preserve">
<value>Bitwarden</value>
<comment>Max 30 characters</comment>
</data>
<data name="Description" xml:space="preserve">
<value>O bitwarden é a maneira mais fácil e segura de armazenar todas as suas credenciais e senhas mantendo-as convenientemente sincronizadas entre todos os seus dispositivos.
O furto de senhas é um problema sério. Os sites e aplicativos que os utiliza estão sob ataque todos os dias. Quebras de segurança ocorrem e as suas senhas são furtadas. Quando você reutiliza as mesmas senhas entre aplicativos e sites, os hackers podem facilmente acessar o seu e-mail, banco, e outras contas importantes.
Os especialistas de segurança recomendam que utilize uma senha diferente e aleatoriamente gerada para todas as contas que você cria. Mas como gerenciar todas essas senhas? O bitwarden torna-lhe fácil criar, armazenar, e acessar as suas senhas.
O bitwarden armazena todas as suas credenciais num cofre encriptado que sincroniza entre todos os seus dispositivos. Como são completamente encriptados antes de se quer sair do seu dispositivo, apenas você tem acesso aos seus dados. Nem se quer a equipe do bitwarden pode ler os seus dados, mesmo se quiséssemos. Os seus dados são selados com encriptação AES-256 bits, salted hashing, e PBKDF2 SHA-256.</value>
<comment>Max 4000 characters</comment>
</data>
<data name="Keywords" xml:space="preserve">
<value>bitwarden,bit warden,8bit,senha,login,gerenciador de senha grátis,gerenciador de senha,gerenciador de credencial</value>
<comment>Max 100 characters</comment>
</data>
<data name="Screenshot1" xml:space="preserve">
<value>Gerencie todas as suas credenciais a partir de um cofre seguro</value>
</data>
<data name="Screenshot2" xml:space="preserve">
<value>Gere automaticamente senhas fortes, aleatórias e seguras</value>
</data>
<data name="Screenshot3" xml:space="preserve">
<value>Proteja o seu cofre com Touch ID, código PIN, ou senha mestra</value>
</data>
<data name="Screenshot4" xml:space="preserve">
<value>Autopreencha credenciais a partir do Safari, Chrome, e centenas de outros aplicativos</value>
</data>
<data name="Screenshot5" xml:space="preserve">
<value>Sincronize e acesse o seu cofre através de múltiplos dispositivos</value>
</data>
</root>

View File

@@ -1,171 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Name" xml:space="preserve">
<value>Bitwarden 密碼管理工具</value>
<comment>Max 30 characters</comment>
</data>
<data name="Description" xml:space="preserve">
<value>Bitwarden, Inc. 是 8bit Solutions LLC 的母公司。
被 THE VERGE、U.S. NEWS &amp; WORLD REPORT、CNET 等評為最佳密碼管理器。
從任何地方不限制設備管理、存儲、保護和共享無限的密碼。Bitwarden 為每個人提供開源的密碼管理解決方案,無論是在家裡,在工作中,還是在旅途中。
基於安全要求,為你經常訪問的每個網站生成強大、唯一和隨機的密碼。
Bitwarden Send 快速傳輸加密的信息---文檔和文本---直接給任何人。
Bitwarden 為公司提供團隊和企業計劃,因此你可以安全地與同事共享密碼。
為何選擇 Bitwarden
世界級的加密技術
密碼受到先進的端到端加密AES-256 位、鹽化標籤和 PBKDF2 SHA-256的保護為您的數據保持安全和隱密。
內置密碼生成器
基於安全要求,為你經常訪問的每個網站生成強大、唯一和隨機的密碼。
全球翻譯
Bitwarden 的翻譯有 40 種語言,而且還在不斷增加,感謝我們的全球社區。
跨平台的應用程式
從任何瀏覽器、行動裝置或桌面作業系統,以及更多的地方,在您的 Bitwarden 密碼庫中保護和分享敏感數據。</value>
<comment>Max 4000 characters</comment>
</data>
<data name="Keywords" xml:space="preserve">
<value>bitwarden, bit warden, 8 bit, 密碼, 登入, 免費密碼管理工具, 密碼管理工具, 登入管理工具</value>
<comment>Max 100 characters</comment>
</data>
<data name="Screenshot1" xml:space="preserve">
<value>在一個安全的密碼庫中管理您的所有的密碼</value>
</data>
<data name="Screenshot2" xml:space="preserve">
<value>自動產生高強度、隨機並安全的密碼</value>
</data>
<data name="Screenshot3" xml:space="preserve">
<value>使用 Touch ID 、PIN 碼或主密碼保護您的密碼庫</value>
</data>
<data name="Screenshot4" xml:space="preserve">
<value>在 Safari、Chrome 和數以百計的程式中自動填入登入資料</value>
</data>
<data name="Screenshot5" xml:space="preserve">
<value>在多部裝置上同步和存取密碼庫</value>
</data>
</root>

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@@ -118,44 +118,44 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Title" xml:space="preserve">
<value>Bitwarden - Gerenciador de Senhas</value>
<value>bitwarden Gestor de palavras-passe</value>
<comment>Max 30 characters</comment>
</data>
<data name="ShortDescription" xml:space="preserve">
<value>Bitwarden é um gerenciador de senha que ajuda a mantê-lo seguro on-line.</value>
<value>O bitwarden é um gestor de palavras-passe que lhe ajuda a manter seguro online.</value>
<comment>Max 80 characters</comment>
</data>
<data name="FullDesciption" xml:space="preserve">
<value>O bitwarden é a maneira mais fácil e segura de armazenar todas as suas credenciais e senhas mantendo-as convenientemente sincronizadas entre todos os seus dispositivos.
<value>O bitwarden é a maneira mais fácil e segura de armazenar todas as suas credenciais e palavras-passe mantendo-as convenientemente sincronizadas entre todos os seus dispositivos.
O furto de senhas é um problema sério. Os sites e aplicativos que os utiliza estão sob ataque todos os dias. Quebras de segurança ocorrem e as suas senhas são furtadas. Quando você reutiliza as mesmas senhas entre aplicativos e sites, os hackers podem facilmente acessar o seu e-mail, banco, e outras contas importantes.
O furto de palavras-passe é um problema sério. Os websites e aplicações que utiliza estão sob ataque todos os dias. Quebras de segurança ocorrem e as suas palavras-passe são furtadas. Quando reutiliza as mesmas palavras-passe entre aplicações e websites, os hackers podem facilmente aceder ao seu email, banco, e outras contas importantes.
Os especialistas de segurança recomendam que utilize uma senha diferente e aleatoriamente gerada para todas as contas que você cria. Mas como gerenciar todas essas senhas? O bitwarden torna-lhe fácil criar, armazenar, e acessar as suas senhas.
Os especialistas de segurança recomendam que utilize uma palavra-passe diferente e aleatoriamente gerada para todas as contas que cria. Mas como é que gere todas essas palavras-passe? O bitwarden torna-lhe fácil criar, armazenar, e aceder às suas palavras-passe.
O bitwarden armazena todas as suas credenciais num cofre encriptado que sincroniza entre todos os seus dispositivos. Como são completamente encriptados antes de se quer sair do seu dispositivo, apenas você tem acesso aos seus dados. Nem se quer a equipe do bitwarden pode ler os seus dados, mesmo se quiséssemos. Os seus dados são selados com encriptação AES-256 bits, salted hashing, e PBKDF2 SHA-256.</value>
<comment>Max 4000 characters</comment>
O bitwarden armazena todas as suas credenciais num cofre encriptado que sincroniza entre todos os seus dispositivos. Como são completamente encriptados antes de se quer sair do seu dispositivo, apenas você tem acesso aos seus dados. Nem se quer a equipa do bitwarden pode ler os seus dados, mesmo se quiséssemos. Os seus dados são selados com encriptação AES-256 bits, salted hashing, e PBKDF2 SHA-256.</value>
<comment>Max 4000 characters</comment>
</data>
<data name="FeatureGraphic" xml:space="preserve">
<value>Um gerenciador de senhas gratuito e seguro para todos os seus dispositivos</value>
<value>Um gestor de palavras-passe seguro e gratuito para todos os seus dispositivos</value>
</data>
<data name="Screenshot1" xml:space="preserve">
<value>Gerencie todas as suas credenciais a partir de um cofre seguro</value>
<value>Gira todas as suas credenciais e palavras-passe a partir de um cofre seguro</value>
</data>
<data name="Screenshot2" xml:space="preserve">
<value>Gera automaticamente senhas fortes, aleatórias e seguras</value>
<value>Gira automaticamente palavras-passe fortes, aleatórias e seguras</value>
</data>
<data name="Screenshot3" xml:space="preserve">
<value>Proteja seu cofre com impressão digital, código PIN, ou senha mestra</value>
<value>Proteja o seu cofre com impressão digital, código PIN, ou palavra-passe mestra</value>
</data>
<data name="Screenshot4" xml:space="preserve">
<value>Autopreencha rapidamente credenciais dentro de seu navegador web e outros aplicativos</value>
<value>Auto-preencha rapidamente credenciais de dentro do seu navegador web e outras aplicação</value>
</data>
<data name="Screenshot5" xml:space="preserve">
<value>Sincronize e acesse o seu cofre de vários dispositivos
<value>Sincronize e aceda ao seu cofre a partir de múltiplos dispositivos
- Celular
- Telemóvel
- Tablet
- Computador
- Web</value>
</data>
</root>
</root>

View File

@@ -1,179 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Title" xml:space="preserve">
<value>Bitwarden 密碼管理工具</value>
<comment>Max 30 characters</comment>
</data>
<data name="ShortDescription" xml:space="preserve">
<value>Bitwarden 是一款帳戶和密碼的管理工具,可幫助您在上網時保持安全。</value>
<comment>Max 80 characters</comment>
</data>
<data name="FullDesciption" xml:space="preserve">
<value>Bitwarden, Inc. 是 8bit Solutions LLC 的母公司。
被 THE VERGE、U.S. NEWS &amp; WORLD REPORT、CNET 等評為最佳密碼管理器。
從任何地方不限制設備管理、存儲、保護和共享無限的密碼。Bitwarden 為每個人提供開源的密碼管理解決方案,無論是在家裡,在工作中,還是在旅途中。
基於安全要求,為你經常訪問的每個網站生成強大、唯一和隨機的密碼。
Bitwarden Send 快速傳輸加密的信息---文檔和文本---直接給任何人。
Bitwarden 為公司提供團隊和企業計劃,因此你可以安全地與同事共享密碼。
為何選擇 Bitwarden
世界級的加密技術
密碼受到先進的端到端加密AES-256 位、鹽化標籤和 PBKDF2 SHA-256的保護為您的數據保持安全和隱密。
內置密碼生成器
基於安全要求,為你經常訪問的每個網站生成強大、唯一和隨機的密碼。
全球翻譯
Bitwarden 的翻譯有 40 種語言,而且還在不斷增加,感謝我們的全球社區。
跨平台的應用程式
從任何瀏覽器、行動裝置或桌面作業系統,以及更多的地方,在您的 Bitwarden 密碼庫中保護和分享敏感數據。</value>
<comment>Max 4000 characters</comment>
</data>
<data name="FeatureGraphic" xml:space="preserve">
<value>安全、免費、跨平台的密碼管理工具</value>
</data>
<data name="Screenshot1" xml:space="preserve">
<value>在一個安全的密碼庫中管理您的所有的密碼</value>
</data>
<data name="Screenshot2" xml:space="preserve">
<value>自動產生高強度、隨機並安全的密碼</value>
</data>
<data name="Screenshot3" xml:space="preserve">
<value>使用指紋、PIN 碼或主密碼保護您的密碼庫</value>
</data>
<data name="Screenshot4" xml:space="preserve">
<value>幫您在網頁和程式中自動填入帳戶和密碼</value>
</data>
<data name="Screenshot5" xml:space="preserve">
<value>在多部裝置上同步和存取密碼庫
- 手機
- 平板電腦
- 桌面電腦
- 網頁</value>
</data>
</root>