mirror of
https://github.com/bitwarden/mobile
synced 2026-01-26 06:13:24 +00:00
Compare commits
10 Commits
v2.12.0
...
fido2-goog
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1461ab16b | ||
|
|
307a5a5843 | ||
|
|
d050215ebc | ||
|
|
67e26c778b | ||
|
|
efb10d155d | ||
|
|
913c673773 | ||
|
|
339decafe4 | ||
|
|
380c3583fb | ||
|
|
244a6a7f41 | ||
|
|
745b54bf2a |
175
.github/workflows/crowdin-sync.yml
vendored
175
.github/workflows/crowdin-sync.yml
vendored
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
106
src/Android/Fido2System/Fido2BuilderObject.cs
Normal file
106
src/Android/Fido2System/Fido2BuilderObject.cs
Normal 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
|
||||
249
src/Android/Fido2System/Fido2Service.cs
Normal file
249
src/Android/Fido2System/Fido2Service.cs
Normal 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
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -45,5 +45,6 @@ namespace Bit.App.Abstractions
|
||||
bool UsingDarkTheme();
|
||||
long GetActiveTime();
|
||||
void CloseMainApp();
|
||||
bool SupportsFido2();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
80
src/App/Resources/AppResources.Designer.cs
generated
80
src/App/Resources/AppResources.Designer.cs
generated
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
8
src/Core/Enums/Fido2CodesTypes.cs
Normal file
8
src/Core/Enums/Fido2CodesTypes.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Bit.Core.Enums
|
||||
{
|
||||
public enum Fido2CodesTypes
|
||||
{
|
||||
RequestSignInUser = 994,
|
||||
RequestRegisterNewKey = 995,
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
YubiKey = 3,
|
||||
U2f = 4,
|
||||
Remember = 5,
|
||||
OrganizationDuo = 6
|
||||
OrganizationDuo = 6,
|
||||
Fido2WebAuthn = 7,
|
||||
}
|
||||
}
|
||||
|
||||
16
src/Core/Models/Data/Fido2AssertionResponse.cs
Normal file
16
src/Core/Models/Data/Fido2AssertionResponse.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
14
src/Core/Models/Data/Fido2AuthenticatorSelection.cs
Normal file
14
src/Core/Models/Data/Fido2AuthenticatorSelection.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
15
src/Core/Models/Data/Fido2CredentialDescriptor.cs
Normal file
15
src/Core/Models/Data/Fido2CredentialDescriptor.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
12
src/Core/Models/Data/Fido2PubKeyCredParam.cs
Normal file
12
src/Core/Models/Data/Fido2PubKeyCredParam.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
14
src/Core/Models/Data/Fido2RP.cs
Normal file
14
src/Core/Models/Data/Fido2RP.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
16
src/Core/Models/Data/Fido2User.cs
Normal file
16
src/Core/Models/Data/Fido2User.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
@@ -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 & 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>
|
||||
@@ -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>
|
||||
@@ -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 & 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>
|
||||
Reference in New Issue
Block a user